文档目录

一、两种主流布局: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
    }
}