深入理解 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,颜色就会偏向某种色彩(如蓝、红、绿等)。
相关推荐
斗锋在干嘛3 小时前
Android里面内存优化
android
zhslhm4 小时前
Moo0 VideoResizer,简单高效压缩视频!
音视频·视频压缩技巧·视频文件瘦身·数字媒体优化
jiet_h4 小时前
深入解析Kapt —— Kotlin Annotation Processing Tool 技术博客
android·开发语言·kotlin
alexhilton4 小时前
实战:探索Jetpack Compose中的SearchBar
android·kotlin·android jetpack
uhakadotcom5 小时前
EventBus:简化组件间通信的利器
android·java·github
花落已飘5 小时前
音视频基础(音视频的录制和播放原理)
音视频
9527华安6 小时前
Xilinx系列FPGA实现HDMI2.1视频收发,支持8K@60Hz分辨率,提供2套工程源码和技术支持
fpga开发·音视频·8k·hdmi2.1
笑鸿的学习笔记6 小时前
ROS2笔记之服务通信和基于参数的服务通信区别
android·笔记·microsoft
8931519607 小时前
Android开发融云获取多个会话的总未读数
android·android开发·android教程·融云获取多个会话的总未读数·融云未读数
zjw_swun7 小时前
实现了一个uiautomator玩玩
android