原始数据格式:YUV、RGB、PCM 数据排布与内存对齐

各位同学,今天我们来聊聊编解码里最基础、也最容易踩坑的东西——原始数据格式。说白了,就是摄像头采集到的 YUV、屏幕截图拿到的 RGB、麦克风录进来的 PCM,它们在内存里到底是怎么摆的?

我刚开始做音视频的时候,觉得这玩意儿有啥好学的?不就是几个像素点、几个采样值嘛。结果第一次做渲染优化,画面花成彩虹糖,查了两天才发现是 YUV 的 stride 没对齐。嗯,从那以后我再也不敢小看数据排布了。

核心认知:原始数据格式决定了编解码器的输入输出规范,内存对齐决定了硬件加速能否跑起来。这两件事搞不定,后面的流程全是白搭。

一、RGB 数据排布:最直观,但最占带宽

RGB 大家应该最熟悉。每个像素由 R、G、B 三个分量组成,每个分量通常占 8 bit(1 字节)。所以一个像素就是 3 字节,一张 1920×1080 的图,裸数据就是 1920×1080×3 ≈ 5.9 MB。

常见的排布方式有两种:

  • RGB24(像素交错):内存里按 R、G、B、R、G、B……顺序连续排列。这是最常用的格式,OpenCV 默认就是 BGR24(注意顺序反了)。
  • RGB32(含 Alpha):每个像素占 4 字节,多出来的 Alpha 通道用于透明度。Windows 的 GDI 和 DirectX 常用这种。

我在项目中遇到过一个问题:用 libyuv 做 RGB 转 YUV,结果颜色偏绿。查了半天发现是输入数据是 BGR 顺序,但 libyuv 默认当 RGB 处理。所以这里提醒大家——一定要确认你的 RGB 是 R 在前还是 B 在前。

格式 每像素字节数 内存排布 常见场景
RGB24 3 R0 G0 B0 R1 G1 B1 ... OpenCV、BMP
BGR24 3 B0 G0 R0 B1 G1 R1 ... OpenCV 默认、Windows
RGBA32 4 R0 G0 B0 A0 R1 G1 B1 A1 ... WebGL、PNG
BGRA32 4 B0 G0 R0 A0 B1 G1 R1 A1 ... DirectX、GDI

二、YUV 数据排布:编解码的“通用语言”

YUV 是视频编解码领域的事实标准。为什么不用 RGB?因为人眼对亮度(Y)比对色度(UV)更敏感。把色度信息压缩一下,带宽能省一半,画质损失还看不出来。

YUV 的排布方式比 RGB 复杂得多。我把它分成三类来讲:

1. 平面模式(Planar)

Y、U、V 三个分量分别存在独立的内存区域。比如 I420(也叫 YU12),内存里先放所有 Y,再放所有 U,最后放所有 V。这种格式对硬件编码器最友好,因为可以直接 DMA 搬运。

// I420 内存布局示意(4x4 像素)
YYYYYYYYYYYYYYYY  // 16 个 Y
UUUU              // 4 个 U(每 2x2 共享一个)
VVVV              // 4 个 V(每 2x2 共享一个)

2. 半平面模式(Semi-Planar)

Y 单独一个平面,U 和 V 交错放在另一个平面。NV12 就是典型代表——Y 平面 + UV 交错平面。Android 的 Camera2 默认输出就是 NV12,iOS 的 CVPixelBuffer 也常用这种。

// NV12 内存布局示意(4x4 像素)
YYYYYYYYYYYYYYYY  // 16 个 Y
UVUVUVUV          // 8 个 UV 交错(4 个 U + 4 个 V)

3. 交错模式(Interleaved)

Y、U、V 全部交错在一起,比如 YUYV、UYVY。这种格式在 USB 摄像头里很常见,因为传输时不需要拆包重组。但编解码器通常不喜欢它,处理起来效率低。

我的建议:如果做实时编码,优先用 NV12 或 I420。NV12 在硬件编码器上支持最广,I420 在软件编码器(如 x264)上性能最好。YUYV 这种交错格式,能不用就别用——除非你接的是 UVC 摄像头,没得选。

三、PCM 数据排布:音频的“裸数据”

PCM 就是音频的原始数据。麦克风采集到的模拟信号经过 ADC 量化后,变成一串数字采样点。每个采样点通常占 16 bit(2 字节)或 24 bit(3 字节)。

PCM 的排布主要看两个参数:

  • 采样格式:S16LE(有符号 16 位小端)、S24LE、S32LE、FLOAT(浮点)
  • 声道排布:交错(L R L R ...)还是平面(L L L ... R R R ...)

我记得有一次做音频采集优化,发现用交错格式写入 ALSA 驱动时,CPU 占用率比平面格式高了 30%。原因是交错格式需要频繁切换内存地址,缓存命中率低。后来改成平面格式,问题就解决了。

格式 位深 声道排布 常见场景
S16LE 交错 16 bit L0 R0 L1 R1 ... WAV、ALSA 默认
S16LE 平面 16 bit L0 L1 ... R0 R1 ... FFmpeg 内部、某些 DSP
FLOAT 交错 32 bit L0 R0 L1 R1 ... CoreAudio、音频处理

四、内存对齐:硬件加速的“入场券”

内存对齐,说白了就是让数据的起始地址和每行字节数,是某个值的整数倍。这个值通常是 16、32、64 或 128,取决于硬件平台。

为什么需要对齐?

  • CPU 访问效率:未对齐的数据可能导致多次内存访问,甚至触发异常。
  • 硬件编码器要求:大多数硬件编码器要求每行 stride 是 16 或 32 的倍数。
  • SIMD 指令集:SSE、NEON 等指令要求数据 16 字节对齐,否则性能暴跌。

我曾经在调试一个 Android 编码器时,发现输入 NV12 的 stride 是 1920(刚好 16 对齐),但硬件编码器要求 stride 是 32 对齐。结果编码出来的视频全是马赛克。后来把 stride 补到 1920 + 16 = 1936,问题就解决了。

注意:stride 和 width 不是一回事。stride 是内存中每行占用的实际字节数(包含对齐填充),width 是图像的逻辑宽度。很多新手直接把 width 当 stride 传,结果画面错位或花屏。

五、知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到从采集到编码,数据格式和内存对齐是如何贯穿全链路的。

原始数据格式与内存对齐知识体系 原始数据格式与内存对齐知识体系 摄像头采集 屏幕截图 麦克风采集 YUV 格式 I420 / NV12 / YUYV 平面 / 半平面 / 交错 RGB 格式 RGB24 / BGR24 / RGBA 像素交错排布 PCM 格式 S16LE / FLOAT 交错 / 平面排布 内存对齐(Stride / 地址对齐) 16 字节 / 32 字节 / 64 字节对齐 · 硬件编码器要求 · SIMD 优化 编码器输入(硬件 / 软件) 关键:数据格式决定编码器兼容性,内存对齐决定硬件加速能否生效

六、实战建议:如何选择数据格式

说了这么多理论,最后给几条实战建议:

  1. 视频采集端:优先用 NV12(Android/iOS 原生支持),其次 I420(软件编码兼容性好)。
  2. 屏幕录制:Windows 用 BGRA,macOS 用 BGRA 或 NV12,Linux 用 XRGB。
  3. 音频采集:16 bit 交错格式兼容性最好,但如果你要做音频处理(降噪、混音),平面格式更方便。
  4. 内存对齐:先查硬件编码器的文档,看它要求 stride 是 16 还是 32 对齐。不确定时,按 32 对齐最保险。
  5. 调试技巧:用 ffprobe 或 mediainfo 查看编码后的文件,确认实际使用的格式和 stride。如果画面花屏,先检查 stride 对不对。

一个小工具:我写了个 Python 脚本,可以打印任意 YUV/RGB 文件的 stride 和内存布局。需要的同学可以课后找我拿。调试时特别好用。