第二章 视频编解码基础: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 本章小结

这一章的内容,说白了就是三件事:

  1. 编码原理:去掉空间、时间、视觉冗余。H.265比H.264更狠,压缩率更高。
  2. 码流结构:NAL单元是基本单位,SPS/PPS是解码器的说明书。驱动开发时,一定要处理好SPS/PPS的缓存和注入。
  3. YUV格式:海思平台用NV12(YUV420 Semi-Planar)。操作YUV数据时,Stride比Width更重要。

下一章,我们会进入实战环节——搭建海思视频编解码的开发环境。到时候我会手把手教你配置SDK、编译内核模块、跑通第一个编解码示例。准备好了吗?