第二章 视频编解码基础:H.264/H.265编码原理、码流结构与YUV格式
各位同学,欢迎来到第二章。这一章是硬骨头,但也是你后面写驱动、调视频质量绕不开的基础。我个人习惯把这一章叫做「解码器的世界观」——你只有知道码流里藏了什么,才能知道驱动该干什么。
咱们直接开干。
2.1 H.264/H.265编码原理:到底在压缩什么?
先说个最朴素的问题:视频为什么要压缩?
你想想看,一帧1080p的原始图像,YUV420格式下,大小是1920×1080×1.5 ≈ 3MB。30帧每秒,就是90MB/s的数据量。这还没算音频。网络带宽扛不住,存储也扛不住。
所以我们要压缩。H.264和H.265干的事,说白了就是「去掉冗余」。冗余分三种:
- 空间冗余:同一帧里,相邻像素长得像。比如蓝天背景,一大片蓝色没必要每个像素都存。
- 时间冗余:相邻帧之间,内容变化不大。比如镜头固定时,背景几乎不变。
- 视觉冗余:人眼对高频细节不敏感。丢掉一些细节,你看不出来。
H.264和H.265的核心思路是一样的:帧内预测 + 帧间预测 + 变换量化 + 熵编码。但H.265在细节上做了大量优化,压缩率能比H.264高30%~50%。
我在项目中遇到过一个典型场景:用海思芯片做IPC(网络摄像头),客户要求1080p@30fps,码率控制在2Mbps以内。H.264勉强能看,但H.265画质明显好一截。说白了,H.265就是为低带宽高画质而生的。
2.2 码流结构:NAL单元与SPS/PPS
码流结构是驱动开发最常打交道的东西。你从海思的VDEC模块拿到的数据,就是一段一段的NAL单元。
2.2.1 NAL单元(Network Abstraction Layer)
H.264/H.265的码流由一系列NAL单元组成。每个NAL单元包含一个字节的头部和若干字节的载荷。
NAL头部的结构很简单:
+---------------+---------------+
| forbidden_bit | nal_unit_type |
| (1 bit) | (5 bits) |
+---------------+---------------+
| nal_ref_idc | reserved |
| (2 bits) | (2 bits) |
+---------------+---------------+
对于H.264,nal_unit_type是关键。常见的类型有:
| nal_unit_type | 含义 | 说明 |
|---|---|---|
| 1 | 非IDR图像片 | 普通帧数据 |
| 5 | IDR图像片 | 关键帧,解码器重置 |
| 7 | SPS | 序列参数集 |
| 8 | PPS | 图像参数集 |
| 9 | 访问单元分隔符 | 帧边界标记 |
H.265的NAL类型更多,但核心逻辑一样。我建议你记住:SPS和PPS是解码器的「说明书」,没有它们,解码器不知道图像宽高、编码档次、参考帧数量这些关键参数。
我曾经踩过一个坑:在对接某个第三方编码器时,对方给的码流里SPS和PPS只发了一次,然后就是连续的视频帧。结果海思VDEC在丢包后重新开始解码时,因为没有SPS/PPS,直接解码失败。后来我在驱动里加了个缓存逻辑,把最近一次的SPS/PPS保存下来,每次VDEC复位时重新注入。问题解决。
2.2.2 SPS与PPS详解
SPS(Sequence Parameter Set)包含的是全局参数:
- profile_idc:编码档次,比如Baseline、Main、High
- level_idc:编码级别,决定了分辨率、帧率的上限
- pic_width_in_mbs_minus1:图像宽度(以宏块为单位)
- pic_height_in_map_units_minus1:图像高度
- log2_max_frame_num_minus4:帧号位数
PPS(Picture Parameter Set)包含的是每帧的参数:
- pic_init_qp_minus26:初始量化参数
- deblocking_filter_control_present_flag:去块滤波控制
- entropy_coding_mode_flag:熵编码模式(CAVLC还是CABAC)
在海思平台上,你通过HI_MPI_VDEC_SendStream发送码流时,需要把SPS和PPS作为单独的NAL单元先发过去。顺序是:SPS → PPS → IDR帧 → 非IDR帧。
小技巧:调试时如果解码器报错,先检查SPS/PPS是否正确。我习惯在驱动里打印SPS的profile和level,跟编码器配置对比一下。很多时候问题就出在这里。
2.3 YUV格式详解:驱动开发必须掌握的像素格式
YUV是视频处理的核心格式。海思的VPSS、VENC、VDEC模块,内部处理的都是YUV数据。你如果不懂YUV,写出来的驱动大概率会有颜色问题。
2.3.1 YUV vs RGB
YUV和RGB的区别,说白了就是:
- RGB:三个分量都重要,适合显示
- YUV:Y(亮度)重要,UV(色度)可以压缩,适合传输和存储
人眼对亮度敏感,对色度不敏感。所以我们可以把UV分量缩小,这就是YUV采样的由来。
2.3.2 常见的YUV采样格式
| 格式 | 采样方式 | 每像素比特数 | 典型应用 |
|---|---|---|---|
| YUV444 | 每个像素都有Y、U、V | 24 bpp | 专业视频编辑 |
| YUV422 | 每2个像素共享一组UV | 16 bpp | 广播级视频 |
| YUV420 | 每4个像素共享一组UV | 12 bpp | H.264/H.265编码 |
海思平台最常用的是YUV420。为什么呢?因为H.264/H.265编码器默认就是YUV420输入。你给VENC送YUV444的数据,它也会先转换成YUV420再编码。
2.3.3 YUV420的内存布局
YUV420有两种存储方式:Planar和Semi-Planar。海思平台用的是Semi-Planar,也叫NV12。
NV12的内存布局是这样的:
Y平面:W × H 字节
UV平面:W × H/2 字节(U和V交错存储)
举个例子,一张1920×1080的NV12图像:
- Y数据:1920 × 1080 = 2,073,600 字节
- UV数据:1920 × 540 = 1,036,800 字节
- 总大小:3,110,400 字节
UV平面的排列是:U0, V0, U1, V1, U2, V2, ... 每个U和V都是1字节。
我建议你在调试时,如果发现图像颜色不对,先检查YUV数据的内存布局。我曾经遇到一个情况:编码器输出的图像偏绿,查了半天发现是VPSS输出的UV顺序反了——应该是UV交错,结果给成了VU交错。改了一行代码就解决了。
2.3.4 海思平台上的YUV操作
在海思SDK中,YUV数据通常通过以下方式传递:
// 获取VPSS输出的一帧YUV数据
HI_MPI_VPSS_GetChnFrame(VpssGrp, VpssChn, &pstVideoFrame, -1);
// pstVideoFrame结构体中的关键字段
pstVideoFrame->stVFrame.u32Width // 图像宽度
pstVideoFrame->stVFrame.u32Height // 图像高度
pstVideoFrame->stVFrame.enPixelFormat // 像素格式,通常是PIXEL_FORMAT_YVU_SEMIPLANAR_420
pstVideoFrame->stVFrame.u32Stride[0] // Y平面步长(对齐后的宽度)
pstVideoFrame->stVFrame.u32Stride[1] // UV平面步长
pstVideoFrame->stVFrame.virAddr[0] // Y平面虚拟地址
pstVideoFrame->stVFrame.virAddr[1] // UV平面虚拟地址
注意:u32Stride[0]不一定等于u32Width。海思为了内存对齐,Stride通常是16或32的倍数。比如宽度1920,Stride可能是1920或1936。你操作YUV数据时,一定要用Stride,不要用Width,否则会出现图像错位。
2.4 本章小结
这一章的内容,说白了就是三件事:
- 编码原理:去掉空间、时间、视觉冗余。H.265比H.264更狠,压缩率更高。
- 码流结构:NAL单元是基本单位,SPS/PPS是解码器的说明书。驱动开发时,一定要处理好SPS/PPS的缓存和注入。
- YUV格式:海思平台用NV12(YUV420 Semi-Planar)。操作YUV数据时,Stride比Width更重要。
下一章,我们会进入实战环节——搭建海思视频编解码的开发环境。到时候我会手把手教你配置SDK、编译内核模块、跑通第一个编解码示例。准备好了吗?