3、YUV数据格式详解:YUV420/YUV422/YUV444采样格式、内存布局与Stride对齐、NV12与NV21的区别
YUV 格式,说白了就是视频编解码的「血液」。我见过不少新手,代码写得很漂亮,结果一跑起来花屏、绿屏,最后发现是 YUV 数据摆错了位置。这一节,咱们就把 YUV 的底裤扒干净。
3.1 为什么是 YUV,不是 RGB?
人眼对亮度(Y)敏感,对色度(U/V)不敏感。这是 YUV 存在的根本原因。RGB 三个分量同等重要,压缩空间小。YUV 可以把色度信息砍掉一半甚至更多,画质损失却不大。
举个例子:一张 1920x1080 的图片,RGB 需要 1920×1080×3 = 6.22 MB。换成 YUV420,只需要 1920×1080×1.5 = 3.11 MB。省了一半带宽。H.265 编码器吃的就是 YUV 数据,不是 RGB。
3.2 三种采样格式:YUV444、YUV422、YUV420
这三种格式的区别,说白了就是「色度信息保留多少」。
| 采样格式 | Y 分量 | U/V 分量 | 数据量比例 | 典型场景 |
|---|---|---|---|---|
| YUV444 | 每个像素都有 | 每个像素都有 | 3 字节/像素 | 专业调色、屏幕录制 |
| YUV422 | 每个像素都有 | 每 2 个像素共享一组 | 2 字节/像素 | 广播级视频、高端采集卡 |
| YUV420 | 每个像素都有 | 每 4 个像素共享一组 | 1.5 字节/像素 | H.264/H.265 编码、直播、视频通话 |
YUV444:每个像素都保留完整的 Y、U、V。画质最好,但数据量最大。我只有在做屏幕录制或后期调色时才用,编码器一般不直接吃这个。
YUV422:水平方向每两个像素共享一组 UV。画质折中。我记得以前做广电项目时,SDI 采集卡出来的就是 YUV422,编码前还得转成 420。
YUV420:水平和垂直方向都减半。这是 H.265 编码的「标准口粮」。你想想看,如果编码器吃 444,码率直接翻倍,画质提升却微乎其微。所以 420 是绝对主流。
3.3 内存布局:Planar 与 Packed
YUV 数据在内存里怎么摆?两种方式:Planar(平面)和 Packed(打包)。
- Planar:Y 分量一整块,U 一整块,V 一整块。比如 I420(YUV420P)。
- Packed:Y、U、V 交错排列。比如 NV12、NV21。
H.265 编码器通常要求 Planar 输入,尤其是 I420 或 NV12。我个人的建议是:统一用 NV12,因为它在内存访问上更友好,硬件编码器也普遍支持。
3.4 Stride 对齐:一个容易被忽略的坑
Stride(步长)指的是内存中一行数据的实际字节数。它不一定等于宽度乘以像素字节数。
为什么?因为硬件(GPU、DMA)要求内存地址对齐,通常是 16 字节或 32 字节对齐。比如一张 1920 宽的 YUV420 图像,Y 分量的宽度是 1920 字节。但实际 Stride 可能是 1920,也可能是 1936(对齐到 16 的倍数)。
我曾经在一个项目中,直接从摄像头 buffer 里取数据,没管 Stride,结果画面右侧出现了一条「绿边」。后来发现是 Stride 比宽度多了 16 个字节,我没跳过填充区。
Y 分量大小 = Stride × Height
U 分量大小 = Stride/2 × Height/2
V 分量大小 = Stride/2 × Height/2
注意: 不要用 Width 去算,要用 Stride。
3.5 NV12 与 NV21:Android 和 iOS 的「亲儿子」
NV12 和 NV21 都属于 YUV420 的变种,区别在于 UV 分量的排列顺序。
| 格式 | 内存布局 | 典型平台 | 编码器偏好 |
|---|---|---|---|
| NV12 | Y 平面 + UV 交错(U 在前,V 在后) | Windows、Intel、大部分硬件编码器 | 最常用 |
| NV21 | Y 平面 + VU 交错(V 在前,U 在后) | Android Camera | 需要转换 |
NV12 的 UV 排列:U、V、U、V……
NV21 的 UV 排列:V、U、V、U……
就这么一点差别,坑了多少人。我记得有一次做 Android 端的视频采集,Camera 出来的是 NV21,我直接丢给编码器,结果画面颜色完全不对。后来加了一个 NV21 转 NV12 的转换函数才搞定。
3.6 一张图看懂 YUV 家族
下面这张 SVG 图,把 YUV 的采样格式、内存布局、常见格式串起来了。你可以把它当作「YUV 速查表」。
3.7 实战:如何正确读取 NV12 数据
假设你有一个 NV12 的 buffer,宽度 1920,高度 1080,Stride 为 1920(对齐到 16 字节)。
// NV12 内存布局示例
// Y 平面:Stride × Height = 1920 × 1080
// UV 平面:Stride × Height/2 = 1920 × 540
uint8_t* nv12_buffer = ...; // 假设已经填充好数据
// 读取 Y 分量
uint8_t* y_plane = nv12_buffer;
for (int h = 0; h < 1080; h++) {
for (int w = 0; w < 1920; w++) {
uint8_t y = y_plane[h * stride + w];
// 处理 Y 值
}
}
// 读取 UV 分量(交错)
uint8_t* uv_plane = nv12_buffer + stride * 1080;
for (int h = 0; h < 540; h++) {
for (int w = 0; w < 1920; w += 2) {
uint8_t u = uv_plane[h * stride + w]; // U 在前
uint8_t v = uv_plane[h * stride + w + 1]; // V 在后
// 处理 U、V 值
}
}
3.8 总结一下
- YUV420 是 H.265 编码的标配,别用 444 或 422 去喂编码器,除非你明确知道自己在做什么。
- NV12 是跨平台最通用的格式。Windows、Linux、macOS、iOS 都支持。Android 原生是 NV21,记得转一下。
- Stride 对齐 是新手最容易踩的坑。拿到 buffer 后,先确认 Stride,再用 Stride 去算偏移。
- Planar vs Packed:编码器通常要 Planar(I420 或 NV12)。Packed 格式(如 YUYV)需要先解包。