在视频处理和图像渲染领域,YUV 颜色空间被广泛用于压缩和传输视频数据。然而,在实际开发过程中,很多开发者会遇到 YUV 颜色偏色 的问题,例如 画面整体偏绿。这通常与 U、V 分量的取值有关。那么,YUV 颜色是如何转换为 RGB 的?为什么 U/V 分量的错误会导致颜色偏移?在 Android 设备上如何正确渲染 YUV?
本文将带你深入理解 YUV 颜色空间,并解析其在 Android 开发中的应用。
1. 为什么要使用 YUV?
在计算机图像处理中,我们最常见的颜色空间是 RGB(红、绿、蓝),但 YUV 颜色空间更适用于视频压缩和传输,原因包括:
- 人眼对亮度 (Luminance) 更敏感,对色度 (Chrominance) 较不敏感。
- YUV 颜色空间允许色度子采样(Chroma Subsampling),减少数据量,提高压缩效率。
- 视频格式(如 MPEG、H.264、H.265)广泛使用 YUV 以优化带宽和存储成本。
2. YUV 颜色空间基础
YUV 颜色空间主要由三个分量组成:
- Y(亮度,Luminance):表示像素的亮度信息,取值范围通常是
[0, 255]
。 - U(蓝色色度,Chrominance blue):表示颜色的蓝色分量偏移,取值范围通常是
[0, 255]
,中性值为128
。 - V(红色色度,Chrominance red):表示颜色的红色分量偏移,取值范围通常是
[0, 255]
,中性值为128
。
重要概念:
- 当 U = 128, V = 128 时,表示没有色彩偏移,即灰度图像。
- U、V 偏离 128 时,画面会偏向不同颜色。
常见的 YUV 采样格式
YUV 数据通常以不同的方式存储,常见的格式包括:
- YUV420 (NV12, NV21, I420):色度分量的分辨率是亮度分量的一半。
- YUV422:色度水平采样减半,但垂直方向采样保持完整。
- YUV444:无色度子采样,U/V 和 Y 具有相同分辨率。
YUV420 是 Android 设备摄像头常见的输出格式,如 NV21。
3. YUV 到 RGB 颜色转换
在渲染 YUV 图像时,我们需要将其转换为 RGB 格式,以便在屏幕上正确显示。
YUV 转 RGB 公式(BT.601 标准):
R = Y + 1.402 × (V - 128)
G = Y - 0.344 × (U - 128) - 0.714 × (V - 128)
B = Y + 1.772 × (U - 128)
分析:
U - 128
和V - 128
决定了颜色偏移方向。U = 0, V = 0
时,转换公式导致 G 分量大幅提升,而 R、B 下降,画面会出现 明显的绿色偏色。- 正确的 U/V 取值应在
128
附近,否则颜色失真。
4. 为什么摄像头渲染 YUV 会出现绿色?
如果你在 Android 设备上直接渲染摄像头的 YUV 视频流,可能会发现画面出现明显的绿色偏色。常见原因包括:
U/V 分量错误:
- 摄像头数据可能没有正确初始化 U/V 分量,导致它们被误设为 0。
- 在 NV21/NV12 这种格式中,U/V 是交错存储的,解析错误可能导致 UV 变为 0。
颜色转换问题:
- 颜色转换时如果 UV 偏离
128
,可能会导致 G 颜色过度增强。 - 计算公式中
U = 0, V = 0
计算出的 G 值较大,使画面偏绿。
- 颜色转换时如果 UV 偏离
解决方案:
- 确保 UV 分量初始化正确,避免 U/V 直接被置 0。
- 检查 YUV 转 RGB 公式,确保转换时正确解析了 U/V 分量。
5. 如何在 Android 上正确渲染 YUV?
在 Android 中,我们通常使用 SurfaceView + MediaCodec 或 OpenGL ES 来渲染 YUV 视频。
方案 1:使用 YUV to RGB
直接转换
可以使用 ScriptIntrinsicYuvToRGB
进行快速转换:
val rs = RenderScript.create(context)
val script = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
script.setInput(yuvAllocation)
script.forEach(rgbAllocation)
方案 2:使用 OpenGL ES 进行 YUV 渲染
如果你需要高效渲染 YUV 数据,可以使用 OpenGL ES 纹理,将 Y、U、V 分量分别绑定到 GL_LUMINANCE
纹理。
示例代码:
vec3 yuv;
yuv.x = texture2D(y_texture, texCoord).r;
yuv.y = texture2D(u_texture, texCoord).r - 0.5;
yuv.z = texture2D(v_texture, texCoord).r - 0.5;
vec3 rgb = mat3( 1, 1, 1,
0, -0.344, 1.772,
1.402, -0.714, 0) * yuv;
6. 总结
- YUV 颜色空间适用于视频压缩,U/V 分量的正确取值对颜色显示至关重要。
- YUV 转 RGB 需要正确的转换公式,否则可能导致画面偏色(如绿色)。
- 在 Android 开发中,可使用
RenderScript
或OpenGL ES
进行 YUV 渲染。 - 调试 YUV 渲染时,务必检查 U/V 分量,确保它们不是 0,而是接近
128
。
如果你遇到 Android 设备摄像头输出偏绿色的问题,可以先检查 YUV 数据的 UV 分量,并尝试调整颜色转换公式。
7. 附录:理解YUV转RGB举例
我们用一个 简单的 YUV 420 示例,手动计算 YUV → RGB,然后用 Kotlin 代码 还原它。
7.1 假设我们有一个 2x2 的 YUV 420 数据
我们先定义一个 简单的 2×2 像素的 YUV 420 数据:
Y Y
Y Y
U V
每个像素都有 Y(亮度),但所有像素 共享 U/V(色度)。
假设:
Y1 = 100, Y2 = 150
Y3 = 200, Y4 = 250
U = 128, V = 128 (中性颜色)
这个数据对应的 YUV 420 格式 字节数组:
val yuv420 = byteArrayOf(
100, 150, // Y 分量
200, 250, // Y 分量
128, 128 // UV 分量(2x2 像素共用)
)
7.2 使用 YUV 转换公式 计算 RGB
YUV 420 转 RGB 的标准公式:
[
R = Y + 1.402 \times (V - 128)
]
[
G = Y - 0.344 \times (U - 128) - 0.714 \times (V - 128)
]
[
B = Y + 1.772 \times (U - 128)
]
因为 U = 128, V = 128,所以:
[
(V - 128) = 0, \quad (U - 128) = 0
]
代入公式:
[
R = Y
]
[
G = Y
]
[
B = Y
]
所以,这个 YUV 数据转换后的 RGB 颜色是:
(Y1=100) → (100,100,100) 灰色
(Y2=150) → (150,150,150) 灰色
(Y3=200) → (200,200,200) 灰色
(Y4=250) → (250,250,250) 灰色
结论:当 U=128, V=128 时,所有颜色都是 灰色(R=G=B)。
7.3 用 Kotlin 代码 实现 YUV 420 转 RGB
fun yuvToRgb(y: Int, u: Int, v: Int): Triple<Int, Int, Int> {
val c = y
val d = u - 128
val e = v - 128
val r = (c + 1.402 * e).toInt().coerceIn(0, 255)
val g = (c - 0.344 * d - 0.714 * e).toInt().coerceIn(0, 255)
val b = (c + 1.772 * d).toInt().coerceIn(0, 255)
return Triple(r, g, b)
}
fun main() {
val yuv420 = byteArrayOf(
100, 150,
200, 250,
128, 128
)
val y1 = yuv420[0].toInt() and 0xFF
val y2 = yuv420[1].toInt() and 0xFF
val y3 = yuv420[2].toInt() and 0xFF
val y4 = yuv420[3].toInt() and 0xFF
val u = yuv420[4].toInt() and 0xFF
val v = yuv420[5].toInt() and 0xFF
println("Pixel 1 (Y1=100): ${yuvToRgb(y1, u, v)}")
println("Pixel 2 (Y2=150): ${yuvToRgb(y2, u, v)}")
println("Pixel 3 (Y3=200): ${yuvToRgb(y3, u, v)}")
println("Pixel 4 (Y4=250): ${yuvToRgb(y4, u, v)}")
}
运行结果
Pixel 1 (Y1=100): (100, 100, 100)
Pixel 2 (Y2=150): (150, 150, 150)
Pixel 3 (Y3=200): (200, 200, 200)
Pixel 4 (Y4=250): (250, 250, 250)
7.4 结论
- U = 128, V = 128 代表 无色(中性),转换后 R=G=B=Y,所以变成 灰度图。
- Y 影响亮度,所以 Y1=100 是深灰色,Y4=250 是浅灰色。
- 如果 U/V 不是 128,颜色就会偏向某种色彩(如蓝、红、绿等)。