深入理解 YUV 颜色空间:从原理到 Android 视频渲染

在视频处理和图像渲染领域,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 标准):

math 复制代码
R = Y + 1.402 × (V - 128)
G = Y - 0.344 × (U - 128) - 0.714 × (V - 128)
B = Y + 1.772 × (U - 128)

分析:

  • U - 128V - 128 决定了颜色偏移方向。
  • U = 0, V = 0 时,转换公式导致 G 分量大幅提升,而 R、B 下降,画面会出现 明显的绿色偏色
  • 正确的 U/V 取值应在 128 附近,否则颜色失真。

4. 为什么摄像头渲染 YUV 会出现绿色?

如果你在 Android 设备上直接渲染摄像头的 YUV 视频流,可能会发现画面出现明显的绿色偏色。常见原因包括:

  1. U/V 分量错误:

    • 摄像头数据可能没有正确初始化 U/V 分量,导致它们被误设为 0。
    • 在 NV21/NV12 这种格式中,U/V 是交错存储的,解析错误可能导致 UV 变为 0。
  2. 颜色转换问题:

    • 颜色转换时如果 UV 偏离 128,可能会导致 G 颜色过度增强。
    • 计算公式中 U = 0, V = 0 计算出的 G 值较大,使画面偏绿。

解决方案:

  • 确保 UV 分量初始化正确,避免 U/V 直接被置 0。
  • 检查 YUV 转 RGB 公式,确保转换时正确解析了 U/V 分量。

5. 如何在 Android 上正确渲染 YUV?

在 Android 中,我们通常使用 SurfaceView + MediaCodecOpenGL ES 来渲染 YUV 视频。

方案 1:使用 YUV to RGB 直接转换

可以使用 ScriptIntrinsicYuvToRGB 进行快速转换:

kotlin 复制代码
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 纹理。

示例代码:

glsl 复制代码
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 开发中,可使用 RenderScriptOpenGL 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(色度)

假设:

plaintext 复制代码
Y1 = 100, Y2 = 150
Y3 = 200, Y4 = 250
U = 128, V = 128  (中性颜色)

这个数据对应的 YUV 420 格式 字节数组

kotlin 复制代码
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

kotlin 复制代码
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 结论

  1. U = 128, V = 128 代表 无色(中性) ,转换后 R=G=B=Y ,所以变成 灰度图
  2. Y 影响亮度 ,所以 Y1=100 是深灰色,Y4=250 是浅灰色
  3. 如果 U/V 不是 128,颜色就会偏向某种色彩(如蓝、红、绿等)。
相关推荐
贺biubiu4 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
xuekai200809014 小时前
mysql-组复制 -8.4.7 主从搭建
android·adb
nono牛5 小时前
ps -A|grep gate
android
未知名Android用户6 小时前
Android动态变化渐变背景
android
行业探路者6 小时前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检
nono牛7 小时前
Gatekeeper 的精确定义
android
stevenzqzq8 小时前
android启动初始化和注入理解3
android
城东米粉儿10 小时前
compose 状态提升 笔记
android
粤M温同学11 小时前
Android 实现沉浸式状态栏
android
Android系统攻城狮11 小时前
Android16音频之获取Record状态AudioRecord.getState:用法实例(一百七十七)
音视频·android16·音频进阶