一、两种主流布局:Planar vs Semi-Planar
这是 C++ 处理视频时最常搞错的地方。FFmpeg、OpenCV、硬件编码器返回的 YUV 数据格式不同。
YUV420P(Planar)— FFmpeg 的默认格式 av_pix_fmt_yuv420p
内存:YYYYYYYYYYYYYYYY...UUUUUUUU...VVVVVVVV
[全部 Y 行] [全部 U 行] [全部 V 行]
索引:
Y 平面:data[0], linesize[0] = width
U 平面:data[1], linesize[1] = width/2
V 平面:data[2], linesize[2] = width/2
获取像素 (x,y) 的 Y/U/V:
y = data[0][y * linesize[0] + x]
u = data[1][y/2 * linesize[1] + x/2]
v = data[2][y/2 * linesize[2] + x/2]
NV12(Semi-Planar)— 硬件编码器的默认格式(Intel QSV、VideoToolbox、MPP)
内存:YYYYYYYYYYYYYYYY...UVUVUVUVUV
[全部 Y 行] [UV 交错排列]
Y 平面:data[0], linesize[0] = width
UV 平面:data[1], linesize[1] = width(UV 交替,每个 U/V 各 1 byte)
注意:linesize[1] 是 width 而不是 width,因为 UV 交织在一起
获取像素 (x,y) 的 Y/U/V:
y = data[0][y * linesize[0] + x]
uv_index = (y/2) * linesize[1] + (x/2) * 2
u = data[1][uv_index]
v = data[1][uv_index + 1]
二、手写 YUV420P ↔ NV12 转换
这两个格式之间互相转换是音视频开发者的基本功:
// Planar YUV420P → Semi-Planar NV12
void yuv420p_to_nv12(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
uint8_t* dst_y, uint8_t* dst_uv,
int width, int height) {
int y_size = width * height;
int uv_size = (width / 2) * (height / 2);
// ① 拷贝 Y 平面(完全一样)
memcpy(dst_y, y_plane, y_size);
// ② 交错 UV 平面
for (int i = 0; i < uv_size; i++) {
dst_uv[i * 2] = u_plane[i]; // U
dst_uv[i * 2 + 1] = v_plane[i]; // V
}
}
// Semi-Planar NV12 → Planar YUV420P
void nv12_to_yuv420p(const uint8_t* src_y, const uint8_t* src_uv,
uint8_t* dst_y, uint8_t* dst_u, uint8_t* dst_v,
int width, int height) {
int y_size = width * height;
int uv_size = (width / 2) * (height / 2);
memcpy(dst_y, src_y, y_size);
for (int i = 0; i < uv_size; i++) {
dst_u[i] = src_uv[i * 2]; // 取 U
dst_v[i] = src_uv[i * 2 + 1]; // 取 V
}
}
三、stride(linesize)陷阱
// ⚠️ 不要假设 linesize == width!
// 硬件编码器/采集设备为了对齐要求,每行可能有 padding:
// 假设图像宽 1920,但采集卡要求每行 32 字节对齐
// width = 1920, linesize = 1920(3 × 640,刚好 32 对齐?不,1920 % 32 = 0 ✅)
// 但如果是 1280x720:1280 % 32 = 0 ✅
// 如果是 1922x1080:1922 % 32 = 2 ❌ → linesize = 1922 + 30 = 1952
// ❌ 错误做法
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel = data[y * width + x]; // 如果 linesize > width,读到的是 padding
}
}
// ✅ 正确做法
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel = data[y * linesize + x]; // 跳过每行末尾的 padding
}
}