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。

我的习惯: 在采集端,摄像头出来的原始数据通常是 MJPEG 或 NV12。我一般直接转成 NV12 喂给编码器,避免 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 是绝对主流。

避坑指南: 我曾经在某个项目中,把 YUV422 的数据直接当 YUV420 喂给编码器,结果画面出现严重的「彩色锯齿」。后来排查了半天,才发现是 UV 分量的位置算错了。记住:采样格式不匹配,画面必出问题。

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 的转换函数才搞定。

我的建议: 统一用 NV12 作为内部格式。如果采集到 NV21,写一个简单的转换函数。别偷懒,否则后面调试颜色问题会让你崩溃。

3.6 一张图看懂 YUV 家族

下面这张 SVG 图,把 YUV 的采样格式、内存布局、常见格式串起来了。你可以把它当作「YUV 速查表」。

YUV 数据格式全景图 YUV444 YUV422 YUV420(主流) Planar(I444) Planar(I422) Planar / Semi-Planar I444 / YUV444P I422 / YUYV I420 / NV12 / NV21 NV12: Y + UV 交错 NV21: Y + VU 交错 💡 关键结论 • H.265 编码器首选 YUV420 格式 • NV12 是跨平台最通用的选择 • 注意 Stride 对齐,否则画面异常

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 值
    }
}
注意: 如果 Stride 不等于 Width,上面的代码要改成用 Stride 遍历。否则你会读到填充区的垃圾数据,画面出现「绿边」或「花屏」。

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)需要先解包。