YUV 如何转 RGB
YUV和RGB是两种常见的颜色编码格式,主要用于图像和视频处理。
RGB(红绿蓝)是一种基于加色模型的颜色表示方法,广泛用于显示设备(如屏幕)和图像处理。它由R(Red,红色)、G(Green,绿色)、B(Blue,蓝色)三个通道组成。每个通道通常用8位表示(0-255),因此RGB图像每个像素需要24位(3字节)来存储全彩信息。例如,纯红色的像素值为(255, 0, 0),白色为(255, 255, 255)。
RGB直接对应人眼感知的红、绿、蓝三原色,适合显示和渲染。每个像素存储三个分量,数据量较大,不适合视频压缩和传输。主要用于显示器、相机传感器、图像编辑软件等需要高保真的场景。
RGB 有两个变种。1.RGBA增加了Alpha通道,用于表示透明度。2.16位/通道或浮点格式用于高动态范围(HDR)图像。
YUV 是一种颜色编码方式,常见于视频压缩和传输(如MPEG、H.264)。它将颜色信息分为亮度(Y)和色度(U、V)两个部分。Y(Luma,亮度)表示图像的明暗信息(灰度值),不含颜色信息。Y值决定了图像的黑白效果。U(Cb,蓝色色度)表示蓝色分量与亮度的差值。V(Cr,红色色度)表示红色分量与亮度的差值。色度(U、V)结合亮度(Y)可以重建出完整的颜色信息。
人眼对亮度变化更敏感,对色度变化的感知较弱。因此,YUV允许对色度(U、V)进行亚采样(减少分辨率),从而显著降低数据量。亚采样常见有三种类型,4:4:4(YUV444):每个像素都有完整的Y、U、V分量,数据量与RGB相当。4:2:2(YUV422):每两个像素共享一组U、V值,色度数据减半,常用于高质量视频。4:2:0(YUV420):每四个像素(2x2块)共享一组U、V值,色度数据只有1/4,常见于视频压缩(如H.264、JPEG),这种采样方式是最常用的,如 NV12 / NV21。通过亚采样,YUV格式显著减少数据量,适合视频编码和传输。
YUV 适用于视频编码和解码(如MPEG、H.264、H.265)、电视广播、流媒体、图像压缩格式(如JPEG)。
YUV与RGB的比较:
特性 | RGB | YUV |
---|---|---|
颜色表示 | 红、绿、蓝三原色 | 亮度(Y)+色度(U、V) |
数据量 | 较大(每个像素3字节) | 可通过亚采样减少数据量(如4:2:0) |
人眼感知 | 直接对应显示颜色 | 亮度优先,色度可压缩 |
应用场景 | 显示、图像编辑 | 视频压缩、传输、广播 |
转换复杂度 | 直接使用,无需转换 | 需要与RGB相互转换 |
压缩效率 | 较低 | 较高 |
YUV 转 RGB 有多种标准(BT.601、BT.709)。下面用 BT.601 标准(SD 视频):
假设 YUV 范围:
- Y ∈ [0, 255]
- U ∈ [0, 255]
- V ∈ [0, 255]
转换公式:
\(\begin{cases} R = Y + 1.402 \times (V - 128) \\ G = Y - 0.344136 \times (U - 128) - 0.714136 \times (V - 128) \\ B = Y + 1.772 \times (U - 128) \end{cases}\)
注意:
- 转换后需要 截断,保证 R/G/B ∈ [0, 255]。
- 如果使用浮点计算,效果最好。整型近似也可,但会有误差。
代码示例:
1 | fun yuvToRgb(y: Int, u: Int, v: Int): Int { |
通过预计算 YUV 到 RGB 的映射表,可以在转换时直接查表,避免重复计算。
示例代码:
1 | val yuvToRgbLut = Array(256) { Array(256) { Array(256) { 0 } } } |
使用现代CPU的SIMD(Single Instruction, Multiple Data,单指令多数据)指令集(如SSE、AVX、NEON)可以加速YUV到RGB的转换。每个像素的转换公式相同,适合SIMD的并行处理。图像数据通常是连续的像素数组,可以一次性加载多个像素到SIMD寄存器中。SIMD指令(如SSE处理128位,AVX处理256位)允许同时处理多个像素(例如,SSE一次处理4个像素的YUV值,AVX可处理8个)。SIMD指令集(如SSE、AVX、NEON)通过宽寄存器和专用指令(如加法、乘法、打包/解包)并行执行这些运算,从而显著提高性能。
YUV数据通常存储为连续的Y、U、V分量(例如,YUV420格式中Y是单独的平面,U和V是交错或分开的平面)。需要确保数据加载到SIMD寄存器时是内存对齐的(16字节对齐用于SSE,32字节对齐用于AVX),以避免性能损失。视频处理中,YUV数据通常是8位无符号整数(uint8_t),RGB输出也是8位。SIMD整数指令(如_mm_add_epi16、 _mm_madd_epi16)可以高效处理这些数据。如果需要更高的精度(如浮点系数),可以使用32位浮点SIMD指令(如_mm_mul_ps、 _mm_add_ps),但需要额外的类型转换。YUV的Y范围通常是16-235,U、V范围是16-240,需在转换前进行偏移校正(例如,Y = Y - 16)。
将Y、U、V值加载到SIMD寄存器后,使用向量乘法和加法指令并行计算。例如 SSE 使用_mm_mul_ps进行系数乘法,_mm_add_ps进行加法。一次处理4个(SSE)或8个(AVX)像素的Y、U、V值,生成对应的R、G、B值。由于不同的亚采样的 Y、U、V 分量的比例不同,需要分别处理。4:4:4格式:每个像素都有独立的Y、U、V值,直接加载并处理。4:2:0格式:需要对U、V值进行插值或重复,如使用SIMD的广播指令(如_mm_set1_epi16)将单个U、V值复制到多个像素的寄存器中。对于高质量转换,可以使用SIMD指令实现双线性插值,平滑U、V值的过渡。
RGB值需限制在0-255范围内。SIMD提供饱和指令(如SSE的_mm_packus_epi16或NEON的vqmovn_u16)以防止溢出。使用对齐存储指令(如_mm_store_si128或_mm256_store_si256)将结果写回内存。
以下是一个简化的SSE实现,用于4:4:4 YUV到RGB的转换(假设输入是8位整数,输出也是8位):
1 |
|
FFmpeg的libswscale库已高度优化YUV到RGB转换,支持SIMD(SSE、AVX、NEON)。
在 Android 平台上,可直接使用 OpenGL Shader 进行转换。这个方案适合适合 Android 相机预览或视频播放器,支持 NV12/NV21。它的原理是把 YUV 数据上传成纹理,通过 Shader 在 GPU 上做 YUV→RGB 转换,性能极高,几乎不占 CPU。
顶点 Shader 示例代码:
1 | // vertex_shader.glsl |
片元 Shader(支持 NV21/NV12)示例代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// fragment_shader.glsl
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D yTexture;
uniform sampler2D uvTexture;
uniform int isNV21; // 0 = NV12, 1 = NV21
void main() {
float y = texture2D(yTexture, vTexCoord).r;
vec2 uv = texture2D(uvTexture, vTexCoord).ra; // R->U, A->V for two-channel texture
float u = uv.x - 0.5;
float v = if(isNV21 == 1) uv.x - 0.5 else uv.y - 0.5;
float r = y + 1.402 * v;
float g = y - 0.344136 * u - 0.714136 * v;
float b = y + 1.772 * u;
gl_FragColor = vec4(r, g, b, 1.0);
}
上传 YUV 纹理示例代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27fun uploadYUVTextures(
yData: ByteArray, uvData: ByteArray, width: Int, height: Int
): IntArray {
val textures = IntArray(2)
GLES20.glGenTextures(2, textures, 0)
// Y 纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
width, height, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(yData)
)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
// UV 纹理 (宽/2 x 高/2)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[1])
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE_ALPHA,
width / 2, height / 2, 0, GLES20.GL_LUMINANCE_ALPHA, GLES20.GL_UNSIGNED_BYTE,
ByteBuffer.wrap(uvData)
)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
return textures
}
获取 RGB 数据示例代码: 1
2
3
4
5
6
7fun getRGBData(textureId: Int, width: Int, height: Int): ByteArray {
val rgbData = ByteArray(width * height * 4) // RGBA
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebufferId)
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(rgbData))
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
return rgbData
}
通过 GPU 加速,1080p/4K 的视频可以实现实时的 YUV 到 RGB 的转换,极大地提高了性能和效率。通过调整纹理上传,可以支持不同的 YUV 格式,例如 NV12、NV21、I420 等,灵活扩展。在 Android 平台上,这种方法可以与视频播放器和相机预览无缝集成。