原始数据格式: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 传,结果画面错位或花屏。
五、知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到从采集到编码,数据格式和内存对齐是如何贯穿全链路的。
六、实战建议:如何选择数据格式
说了这么多理论,最后给几条实战建议:
- 视频采集端:优先用 NV12(Android/iOS 原生支持),其次 I420(软件编码兼容性好)。
- 屏幕录制:Windows 用 BGRA,macOS 用 BGRA 或 NV12,Linux 用 XRGB。
- 音频采集:16 bit 交错格式兼容性最好,但如果你要做音频处理(降噪、混音),平面格式更方便。
- 内存对齐:先查硬件编码器的文档,看它要求 stride 是 16 还是 32 对齐。不确定时,按 32 对齐最保险。
- 调试技巧:用
ffprobe或mediainfo查看编码后的文件,确认实际使用的格式和 stride。如果画面花屏,先检查 stride 对不对。
一个小工具:我写了个 Python 脚本,可以打印任意 YUV/RGB 文件的 stride 和内存布局。需要的同学可以课后找我拿。调试时特别好用。