H.264编码详解
H.264即AVC(Advanced Video Coding),在下文中首先说明H.264编码的码流结构,然后会进一步深入说明H.264编码的方法和原理。
码流结构解析
H.264码流是由一个个的NALU组成的,每个NALU用StartCodePrefix分隔。如下表示NALU,
[StartCode] [NALU Header] [NALU Payload]
其中StartCode作为分隔符可以是00 00 00 01 或 00 00 01两种,由后续数据类型决定。比如当后续数据类型是SPS、PPS、SEI以及该NALU对应Slice为一帧的开始(图像帧可以切分成多个Slice装入多个NALU)时,startcode为4个字节。
NALU Header占一个字节,用以表示该NALU的重要程度和类型,有以下几种,
其中第1位始终置0,
第2~3位表示参考级别,值越大越重要,
后5位表示NALU数据类型。
NALU Payload则是负载NALU Header中说明类型的数据,后续会详细说明。
将上述说明的多个NALU串在一起就组成了H.264码流,如下图所示,
H.264的分层结构
H.264将系统框架分为了两层,分别是视频编码层(VCL)和网络抽象层(NAL)。VCL则是负责有效表示视频的数据,而NAL则是负责抽象意义上将表示视频的数据进行封装。为方便后续对VLC层和NAL层进行说明,需要先了解以下几个概念:
- 原始数据比特串(SODB),是由编码器直接输出的原始编码数据,即VLC数据,以位为单位。
- 原始字节序列载荷(RBSP),是在SODB比特串末尾增加trail补全成字节序列。
-
扩展字节序列载荷(EBSP),在RBSP的基础上增加了仿校验字节(0x03)。就是在编码过程中遇到00 00两字节连续时,在这两字节后面插入0x03,解码时将0x03脱壳去掉,其目的就是防止编码内容与开始码冲突。
以上三个概念可以结合下面这张图理解,
理解了以上几个概念后,对VCL和NAL两层做进一步的说明。
VCL是通过编码器算法直接将图片输出成了SODB,它表示图像被压缩后的编码比特流。
NAL层则负责片级以上的语法级别,比如将SODB打包成RBSP并加上NAL头组成NALU;比如将SPS和PPS(两者由编码器生成)加上NAL头组成NALU,后续会说明这两者。
总结一句话就是,VCL负责将图片编码成原始比特流,NAL负责处理原始比特流并打包组成NALU,结合下图理解。
NALU负载内容解析
前面介绍H.264的码流结构以及分层结构。其中,在描述 [NALU Payload]的时候较为简单,除了特殊的参数集,只说了NAL层将VCL编码的原始比特流打包后封装到[NALU Payload]中。因此,接下来将进一步说明[NALU Payload]中的数据结构。
根据NALU负载内容可以将NALU分为VCL和非VCl的NALU。nal_unit_type的值在1~5的NALU称为VCL NALU(各种帧数据),其余称为非VCL NALU(SPS、SEI、PPS)。
非VCL NALU
下面介绍两个必要的非VCL NALU,
- SPS:序列参数集,记录了编码的Profile(NAL-unit规范 12种)、level、图像宽高等信息,用以传输后解码。
- PPS:图像参数集,每一帧编码后数据所依赖的参数保存在PPS中。
一般这两者位于整个码流的起始位置,在某些情况下也可能出现在中间,比如,一是中间开始编码,二是编码器在编码过程中改变了参数。SPS的作用范围是大于PPS的作用范围,因为一个序列是由多个图像组成(后续会详细说明),因此属于同一个图像帧但切片成多个NALU可以引用同一个PPS,多个图像帧但属于同一个序列那就可以引用一个SPS。可以结合下图理解,
VCL NALU
VCL NALU包括负载类型有IDR帧、I帧、P帧、B帧,这些是在VCL层通过编码器编码出来的数据。在进一步说明VCL NALU负载内容前,需要了解以下几个概念,
- 序列(Sequence):序列代表视频序列中连续几个相关帧所组成的一个独立的单位。每个序列有固定结构,比如1SPS+1PPS+1SEI+1I帧+若干P帧+若干B帧。其中非VCL NALU用于描述Sequence的图像信息,方便解码。每个Sequence有且只有一个I帧。
- 帧:一个视频图像编码后的数据叫一帧,由至少一个片(Slice)组成,介绍下面几种帧
- I帧:关键帧,所有宏块都是帧内预测,因此可以单独解码成一副图像。I帧大小和压缩算法有关,码率比较高。其中有种特殊的IDR帧,该帧会立刻刷新,使得后续的P帧、B帧不能使用这IDR帧之前的参考帧。
- P帧:属于前向预测编码帧,参考前面最靠近它的I帧或者P帧进行帧间预测(可以参考多帧)。
- B帧:双向预测编码帧,参考前面的I帧或P帧和后面的P帧进行帧间预测。B帧可以作为参考帧,但在低端设备的H.264配置中一般不使用B帧,因为B帧会进行重排,影响解码器缓冲区大小,增加延迟。
-
片(Slice):一张图片可以编码为多个片,每个片由至少一个宏块组成。分为I、P、B、SP、SI五种条带类型(最后一列是NAL-unit规范 12种),
-
宏块:宏块是H.264编码的基本单位,每个宏块可以采用不同的预测编码类型。通常宏块是大小为16×16的YUV数据组成。宏块分为I、P、B宏块,其中I宏块(帧内预测宏块)只能利用当前片中以解码的像素作为参考进行帧内预测;P宏块(帧间前向预测宏块)可以利用前面已经解码的图像作为参考图像进行帧内预测;B宏块(帧间双向预测宏块)是利用前后参考图像进行帧内预测。I帧只包含I宏块,P帧包含I宏块或P宏块,B帧包含I宏块或双向预测宏块。
GOP:指两个I帧之间的距离,下图展示码率、GOP、图像效果间的关系。
上面概念对视频图像流进行逐步拆解,可以表示成下面示意图的形式,
理解了上面几个概念后进一步理解VCL NALU负载内容。图像帧一般会分为多个slice编码后打包进多个NALU(如果采用数据分割机制,那一个片需要分为3个NALU,参考FFmpeg入门详解P366),因此对slice的结构进行解析。
Slice结构解析
每个分片包含分片头和分片数据两个部分。分片头中包含分片类型、分片宏块类型、分片帧的数量以及对应的帧等信息。分片数据中则是宏块,就是存储像素数据的地方。如下图所示,
宏块结构解析
上面说到分片数据中就是宏块,宏块是视频信息的主要承载者,它包含了宏块类型、预测类型、亮度、色度等信息(这里不详细说明),如下图所示,
H.264编码基本原理
H.264压缩技术主要采用了以下几种方法对视频数据进行压缩(后文只是介绍说明方法的基本原理,DCT、量化、熵编码是帧内帧间预测的一部分):
- 帧内预测:空间上解决数据冗余问题。
- 帧间预测(运动估计与运动补偿):时间上解决数据冗余问题。
- 离散余弦变换(DCT):将空间上相关性变为频域上无关的数据。
- 量化
- 熵编码
帧内预测
帧内压缩是生成I帧的算法,大体过程是分为以下几步,
- 划分块,下面介绍两种分块
- 选择预测模式,不同分块有不同预测模式
- 预测计算,计算预测块的值
- 计算残差
- 编码:DCT、量化、熵编码
- 解码时:重构,去块效应滤波
这里主要介绍4×4的亮度分量预测、16×16的亮度分量预测、色度分量预测。一般4×4用于图像细节部分,16×16用于平坦区域,同时设定多种预测模式匹配图像像素分布规律。
4×4亮度分量预测
在该情况下,该宏块的亮度分量被分为16个4×4大小的子块(宏块总大小还是16×16),每个4×4大小的子块作为一个帧内预测的基本单元。
帧内预测参考每像素块的相邻像素来预测,4×4亮度分量使用子块上方4个,右上方4个,左侧4个,左上顶点1个,总共13个像素来预测构建。可以参考下图,小写字母为要预测的像素,大写字母为参考像素。
a~p的预测实现有9种模式,如下图所示,
模式0~1:简单明了,不多做解释
模式2(DC预测):一种预测方式(这里不做详细介绍),就是别的预测模式只有在所需预测像素全部提供才能使用,而DC预测根据A~M中已编码像素预测。
模式3~8:将参考像素加权平均求得,不展开说明。
16×16亮度分量预测
这种情况下,宏块不分成16个子块,而是16×16亮度成分整体用来预测,所以一般用在图像平坦没有很多细节的宏块。同样介绍它的4种预测模式,如下图所示,
模式0~1:分别是垂直和水平
模式2(DC):由上边和左边像素平均值推出相应像素值(全都一样)。
模式3(平面):利用线性“plane”函数及左、上像素推出相应像素值。
帧间预测
帧间预测大体流程如下所述,
划分块:对于H.264来讲,是对16×16的亮度块和8×8的色度块进行帧间预测编码。其中16×16的亮度宏块又可以分成16×16、16×8、8×16、8×8四种子块进行帧间预测(对于8×8的色度块,一样分为四种,不过大小都变成前面的1/4)。
运动估计(后续说明)
运动补偿(后续说明)
编码:DCT、量化、熵编码
解码时:重构、去块效应滤波
运动估计
运动估计基本思想是针对宏块,并假定宏块内所有像素的位移量都相同,然后针对该宏块到参考帧某一特定搜索范围内根据一定匹配准则找到与当前块最相似的块(匹配块),匹配块再与当前块计算相对位移(运动矢量)。在运动估计中,上诉的每种分割都要尝试,计算出运动搜索结果的代价,选择最小代价的分割方式进行编码。整个方法分为运动搜索方法、运动估计准则、亚像素插值、运动矢量估计四个流程部分,这里介绍前两个部分。
运动搜索方法
运动搜索就是运动的第一步,就是在参考帧允许的搜索范围进行搜索匹配帧,分为全局搜索和快速搜索。
全局搜索:将所有可能进行搜索比较,虽然能找到最佳块,但是效率太低
快速搜索:常见的快速搜索算法有三步法、二维对数法、交叉法、菱形法等(不在本部分展开说明)
第一步确定搜索起始点
第二步判断该点有没有达到最佳匹配块的要求和能不能进一步继续搜索
第三步按照搜索规则,以起始点周围点为新的起始点进行递归搜索
运动估计准则
运动估计准则就是找到匹配块后计算编码的代价,设S(x,y)是大小为mxn的搜索图像,T(x,y)是MxN的模板图像,常见的运动估计准则有,
SAD(绝对误差和):
SATD
SSD(差值平方和):
运动补偿
运动估计是运动补偿的前提,在运动估计找到最合适的匹配块后计算得到相应的运动向量。运动补偿的作用是根据匹配帧和运动向量生成预测帧,然后预测帧与真实块计算残差块,达到压缩的目的。
在恢复的时候,根据预测帧和残差块恢复真实块。
离散余弦变换(DCT)
DCT将图像信息从空域转换成频域,做法是将图像转换成不同幅值和频率的余弦函数的总和,如8×8的像素块就要转换成8×8个基本的余弦函数的总和,而DCT就是求这64个余弦函数的权重(系数矩阵)。下面说明变换公式,
公式中f(x, y)为M×N的数字图像矩阵,F(u, v)是对应DCT变换后的系数矩阵(因为DCT是n方的计算复杂度,因此都是分块后进行的DCT)。C(u)是归一化矩阵,
二维的DCT变换也可以用矩阵直接计算(代码都是这样实现的),
其中C是对应的DCT变换矩阵,A是像素信息矩阵,B是DCT变换后对应的系数矩阵。其中变化矩阵
解码时做逆变化
(C是正交矩阵,逆等于转置)。
量化
上面提到DCT,在经过DCT变换后,有效的图像信息会聚集在低频(如左上角),而高频(如右下角)的信息一般是不必要的,而通过量化则可以去掉这些不必要的高频信息以达到压缩的效果。用公式来表示量化,
F Q = r o u n d ( y / Q s t e p ) FQ = round(y/Qstep)
FQ=round(y/Qstep)
这里FQ是量化后的值,y是原来的系数,Qstep是量化步长(对亮度一般有52种,对于色度有40种),round表示向下取整(量化后一般右下角的数据都会舍去,就是变成0)。步长太小,压缩效率越低,步长太大可能会丢失过多图像信息。
解码时反量化(乘上量化步长)。
熵编码
熵编码用于消除编码中的统计冗余(新的语法元素,比如哈夫曼编码),但h264中用的是CABAC和CAVLC(这两种编码不展开说明)。
在量化后因为高频信息(右下角)大部分为0(舍去),用Z扫描,那么会得到一个尾部带有大量0的编码,这时通过熵编码就可以很大程度的压缩编码信息。
- 参考H.264官方中文版本