H264编码基础
0x1 基本介绍
视频是由一帧帧图像组成,视频为了不卡顿,一秒钟至少要16帧画面,但是图片内容太大,传输不现实。因此需要对他们编码。
官方文档:http://www.itu.int/rec/T-REC-H.264 翻译文档:http://www.itu.int/rec/T-REC-H.264-200503-S/en
0x11 过程
H264通过比较相邻几张图像的像素差(编码),来保存这几张图片的真实数据,当两张图片的像素点不大的时候则不去编码来减小传递张数(压缩)。来大大提高视频质量和传输速率。
例:
- 编码得出一个完整的图片A
- 将第二张图片与第一张图片A进行比较,获取的像素差,作为第二张图片B
- 同上,获取图片C
- 当此帧图片与C想差很大时,我们重新对此帧进行编码,获取图片D
- 同上
编码的原理:
- 空间冗余:同一幅图像的相邻像素点具有连贯性。
- 时间冗余:一组连续的画面之间存在着关联性。
- 结构冗余:某些场景中存在着明显的图像分布模式,有规律可循。比如:方格地板,蜂窝。
- 视觉冗余:人眼对图像细节的分辨率有限,对亮度比对色度更敏感。
- 知识冗余:许多图像的理解和某些知识有关联。比如人类面部的固定结构。
在进行当前信号编码时,编码器首先会产生对当前信号做预测的信号,称作预测信号。也是为了解决两个方向的数据冗余
。
- 时间上的预测(interprediction):由于视频相邻的两帧之间内容相似,使用先前帧的信号做预测。
- 空间上的预测 (intra prediction):视频的某一帧内部的相邻像素存在相似性,即使用同一张帧之中相邻像素的信号做预测
0x111 视频压缩编码的基本技术
0x1111 预测编码
视频处理中的预测编码主要分为两大类:帧内预测和帧间预测。
- 时间上的预测(帧间预测) 帧间预测的实际值位于当前帧,预测值位于参考帧,用于消除图像的时间冗余。例如我们只需要保存一帧的图形数据,而其他的帧都在这一帧上按规则预测出来。帧间预测的压缩率高于帧内预测,然而不能独立解码,必须在获取参考帧数据之后才能重建当前帧。
- 空间上的预测(帧内预测) 预测值与实际值位于同一帧,用于消除图像的空间冗余。例如人眼在对图像识别时,对低频的亮度很敏感,而对高频下的亮度不太敏感。帧内预测技术就是对人眼中不敏感的数据去除。帧内预测的特点是压缩率相对较低,然而可以独立解码,不依赖其他帧的数据,通常在视频中的关键帧都采用帧内预测。
通常在视频码流中,I帧全部使用帧内编码
,P帧/B帧中的数据
可能使用帧内或者帧间编码
。
0x1112 变换编码
为了提高编码效率,主流的视频编码均属于有损编码,因此会对视频内的图片造成信息损失。视频编码算法常用正交变换进行变换编码,常用的变换方法有:**离散余弦变换(DCT)、离散正弦变换(DST)**等。
- DCT
将上述的残差数据做整数离散余弦变换,去除数据的相关性,进行二次压缩数据。如一个图片为 4x4 的图片矩阵:
变换后:
可以看到能量(低频信号)集中到了左上方,而变换后的数据完全可以通过反变换还原。为了达到压缩数据,我们需要丢弃掉一些能量低的数据(高频信号),而对图像质量影响很小。
最后再去除相关性,可以得到更大的压缩量。具体的转换可以看下大佬的博客:https://www.cnblogs.com/xkfz007/archive/2012/07/31/2616791.html
0x1113 熵编码
主要用于消除视频信息中的统计冗余。由于信号中每个符号出现的概率并不一致,导致使用统一长度的码字表示所有符号会造成浪费。通过熵编码,可以针对不同语法元素分配不同长度的码元,消除视频信息中由于符号概率导致的冗余。常用的编码方式有:指数哥伦布编码(UVLC)、变长编码(CAVLC)、二进制算术编码(CABAC)等。
CABAC
上面的帧内压缩是属于有损压缩技术。也就是说图像被压缩后,无法完全复原。而CABAC属于无损压缩技术。属于熵编码的一种,包括霍夫曼编码、指数哥伦布编码、CABAC等等。这类编码主旨就是,找到一种编码,使得码字的平均码长达到熵极限。如:取得概率较大的符号,取较短的码长,而对于概率较小的符号,取较大的码长。
熵:信息越是随机,它的熵值越高。而信息熵,就是为了解决信息的量化度量问题,它描述了整个信源的平均信息量。在使用熵编码时,码字的平均码长尽量达到熵极限,表明熵编码的压缩效率越高。
H264编码使用的是0阶哥伦布编码方式压缩,但是这种方式可能在某些时候不减数据量,反而增大。而使用CABAC则可以大大减小数据量
0x1114 运动估计和运动补偿
- 运动估计:将当前的输入图像分割成若干不相重叠的小图像子块,然后在前一或者后一图像某个搜索窗口的范围内为每一个图像块寻找一个与之最为相似的图像块。
- 运动补偿:通过计算最相似的图像块与该图像块之间的位置信息,得到了一个运动矢量。在编码过程中就可以将当前图像中的块与参考图像运动矢量所指向的最相思的图像块相减,得到一个残差图像块。
0x112 H264编码过程
简介
- 每一帧的H图像被分为一个或多个条带(slice)进行编码。
- 每一个条带包含多个宏块(MB)。他是H264中基本的编码单元,基本结构包含一个16×16个亮度像素块和两个8×8色度像素块,以及其他一些宏块头信息。
- 在对宏块进行编码时,每一个宏块会分割成多种不同大小的子块进行预测。帧内预测采用的块大小可能为16×16或者4×4,帧间预测采用的块可能有7种不同的形状:16×16、16×8、8×16、8×8、8×4、4×8和4×4。
- 在变换编码方面,针对预测残差数据进行的变换块大小为4×4或8×8。
- H.264标准中采用的熵编码方法主要有上下文自适应的变长编码CAVLC和上下文自适应的二进制算数编码CABAC,根据不同的语法元素类型指定不同的编码方式。通过这两种熵编码方式达到一种编码效率与运算复杂度之间的平衡。
- 条带也具有不同的类型,最常用的有I条带、P条带和B条带。另外,为了支持码流切换,在扩展档次中还定义了SI和SP片。
- I条带:帧内编码条带,只包含I宏块;
- P条带:单向帧间编码条带,可能包含P宏块和I宏块;
- B条带:双向帧间编码条带,可能包含B宏块和I宏块;
- 视频编码中采用的如预测编码、变化量化、熵编码等编码工具主要工作在slice层或以下,这一层通常被称为**视频编码层(Video Coding Layer, VCL)**。
- 相对的,在slice以上所进行的数据和算法通常称之为**网络抽象层(Network Abstraction Layer, NAL)**。主要意义在于提升H.264格式的视频对网络传输和数据存储的亲和性。
H264的三种档次
- 基准档次(Baseline Profile):主要用于视频会议、可视电话等低延时实时通信领域;支持I条带和P条带,熵编码支持CAVLC算法。
- 主要档次(Main Profile):主要用于数字电视广播、数字视频数据存储等;支持视频场编码、B条带双向预测和加权预测,熵编码支持CAVLC和CABAC算法。
- 扩展档次(Extended Profile):主要用于网络视频直播与点播等;支持基准档次的所有特性,并支持SI和SP条带,支持数据分割以改进误码性能,支持B条带和加权预测,但不支持CABAC和场编码。
交错视频编码
针对隔行扫描的视频,H.264专门定义了用于处理此类交错视频的算法。
- PicAFF:Picture Adaptive Frame Field——图像层的帧场自适应
- MBAFF:MacroBlock Adaptive Frame Field——宏块层的帧场自适应
0x12 专业术语
一些需要提前准备的信息打基础,否则大量的术语会懵了。
0x121 帧
上述的过程就是H264的编码的大部分过程(核心算法)。这几个取帧的命名有以下几个:
I帧:完整编码。该帧可压缩程度最低,也不需要通过其他视频帧解码。自身可以通过视频解压算法解压成一张单独的完整的图片。
IDR帧:一个序列的第一个图像叫做IDR图像(立即刷新图像),IDR帧都是I帧。引入IDR是为了在解码的时候可以立即同步,将已解码的数据全部抛出。
P帧:参考之前的I帧生成的只包含差异部分编码的帧。该帧可以引用前面的帧的数据来解压缩并且相对于I帧来说,该帧可以压缩程度更高。需要参考其前面的一个I frame 或者B frame来生成一张完整的图片。
B帧:参考前后的帧编码的帧叫B帧。该帧可以引用前面的帧和后面的帧的数据,从而压缩程度最高。参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片。相较于P帧,压缩量更大,预测效果更好,但是在实时互动的情况下,会引起延时,特别是在网络较差的情况。I帧为帧内压缩,P帧和B帧为帧间压缩。B帧不能作为参考帧。
0x122 场
帧和场都是来产生一个编码的图像,一帧都是一个完整的图像,但是场却不一定。在使用隔行扫描的时候,就会有一帧为两场的情况。这是因为当屏幕过大导致的逐行扫描可能导致图像上下亮度不一致导致的缓和显示方式。
0x123 SPS 和 PPS
I帧、P帧、B帧都是被封装成一个或者多个NALU进行传输或者存储的。 每一个I帧开始之前也有非VCL的NALU单元,用于保存其他信息,它们是PPS
、SPS
。
- PPS(Picture Parameter Sets):图像参数集
- SPS(Sequence Parameter Set):序列参数集
0x124 GOP
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流。也称GOP序列
,每一个序列的第一个图片是IDR图像
(立即刷新图像),也就是I帧
。其算法是:在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内,我们认为这样的图可以分到一组。
0x125 NALU
NALU是将每一帧数据写入到一个NALU单元中,进行传输或存储的NALU设计的目的,是根据不同的网络把数据打包成相应的格式,将VCL产生的比特字符串适配到各种各样的网络和多元环境中。
NALU是将每一帧数据写入到一个NALU单元中,进行传输或存储的。
NALU的封装方式:NALU分为NALU头和NALU体
0x126 字节流格式和RTP格式
多个NALU数据组合在一起形成总的输出码流。对于不同的应用场景,NAL规定了一种通用的格式适应不同的传输封装类型。
- 字节流格式 大多数编码器实现的默认输出格式。字节流格式以连续的bit字节形式传输码流。
- RTP包格式 包格式将NALU按照RTP数据包的格式封装。因此不需要像流传输那样分割识别码,并且很好解包,但是封装格式并没有在标准协议文档中明确规定,仅此使用起来会有一定风险。
0x127 片(slice)
每帧图片中都含有多个切片,他们承载这多个宏块数据。片是H264中提出的新概念,在编码图片后切分并整合出来的一个概念。
片之所以被创造出来,主要目的是为限制误码的扩散和传输。使编码片相互间是独立的。某片的预测不能以其他片中的宏块为参考图像,这样某一片中的预测误差才不会传播到其他片中。片又分为:切片头
和切片数据
类型:
- I片:只包含I宏块
- P片:包含P和I宏块
- B片:包含B和I宏块
- SP片:包含P 和/或 I宏块,用于不同码流之间的切换
- SI片:一种特殊类型的编码宏块
0x128 宏块(macroblock)和子块
H264默认是使用16x16大小的区域作为一个宏块。宏块是视频信息的主要承载者。也就是我们最想知道的图片信息(YUV格式)
。一个编码图像通常划分为多个宏块组成。
如果需要更高的压缩率,还可以在宏块的基础上划出更小的子块。如:16x8、8x16、8x8、.. 4x4。
0x129 运动方向/矢量(MV)
编码图像中的当前宏块相对于参考图像的宏块所移动的距离和方向就是运动向量。
0x12A 运动估计(ME)
寻找最佳匹配宏块的过程。
0x12B 运动补偿(MC)
描述相邻(编码关系伤的相邻,在播放顺序上两帧未必相邻)差别的方法,例:描述前一帧的每个小块如何移动到当前帧中的某个位置。
0x12C Inter-Prediction
参考帧(编码后且经过滤波)中的宏块经运动补偿得到。
0x12D Intra-Prediction
根据当前帧中已编码的宏块(未经过滤波)对当前宏块预测。
0x12E 残差(剩余)宏块
当前宏块减去预测宏块就得残差宏块,表示预测的误差。
0x12F pts/dts
- PTS(Presentation Time Stamp):PTS 主要用于度量解码后的视频帧什么时候被显示出来。
- DTS(Decode Time Stamp):DTS 主要是标识内存中的 bit 流什么时候开始送入解码器中进行解码。
DTS 与 PTS 的不同:DTS 主要用户视频的解码,在解码阶段使用。PTS主要用于视频的同步和输出,在 display 的时候使用。再没有 B frame 的时候输出顺序是一样的。
0x12F1(先这样写) 码流分层
0x13 分层设计
0x131 层级
H264在概念上分为两层:
- VCL:视频编码层,负责高效的内容表示。
- NAL:网络提取层,负责以网络所要求的恰当的方式对数据进行打包和传送。
0x2 编码(原始码)
264的两种码流格式,它们分别为:字节流格式和RTP包格式。
- 字节流格式:这是在h264官方协议文档中规定的格式,所以它也成为了大多数编码器,默认的输出格式。它的基本数据单位为NAL单元,也即NALU。
- RTP包格式:在这种格式中,NALU并不需要起始码Start_Code来进行识别,而是在NALU开始的若干字节(1,2,4字节),代表NALU的长度。
0x21 起始码与NALU
前面说到264的字节流就是起始码 NALU ...
- 起始码:00 00 00 01 或者 00 00 01
- NALU:每一帧的封装,分为
NALU header
和EBSP
0x22 NALU
分为NALU header
和EBSP
。
0x221 NALU header
由三个元素组成:forbidden_zero_bit(1 bit)
、nal_ref_idc(2 bit)
和nal_unit_type(5 bit)
。它们总共占据一个字节。
0x2211 forbidden_zero_bit - 1bit
这个值应该为0,当它不为0时,表示网络传输过程中,当前NALU中可能存在错误,解码器可以考虑不对这个NALU进行解码。
0x2212 nal_ref_idc(优先级) - 2bit
取值0~3,代表当前这个NALU的重要性,取值越大,代表当前NALU越重要。尤其是当前NALU为图像参数集、序列参数集或IDR图像时,或者为参考图像条带(片/Slice),或者为参考图像的条带数据分割时,nal_ref_idc值肯定不为0。而当NALU 类型,nal_unit_type为6、9、10、11、或12时,nal_ref_idc都为0。
0x2213 nal_unit_type(类型) - 5bit
表示NALU Header后面的RBSP的数据结构的类型。
需要注意的几个值,他们在编码解码中需要特别注意:
- 1~5:表示RBSP里面包含的数据为条带(片/Slice)数据,所以值为1-5的NALU统称为VCL(视像编码层)单元,其他的NALU则称为非VCL NAL单元。5为I帧,1为P帧。
- 7:代表当前NALU为序列参数集,SPS。
- 8:为8时为图像参数集,PPS。
- 9:为访问单元分隔符
- 14~31:目前几乎用不到。
0x222 EBSP - 扩展字节序列载荷
首先这里有三个概念:EBSP、RBSP、SODB。(大小顺序从左到右)
- EBSP:扩展字节序列载荷,完整的NALU体。
- RBSP:原始字节序列载荷,EBSP 0x03(防止竞争校验字节)。
- SODB:最原始的编码数据,SODB RBSP_STOP_ONE_BIT(1) RBSP_ALIGNMENT_ZERO_BIT(0…)
0x2221 EBSP(原始字节序列载荷)与RBSP的差距差别0x03
由于NALU依靠起始码来分割NALU段,0x03就是为了防止这些可能在出现的干扰(如:在NALU中出现了0x000001或0x00000就会被认为是新NALU点,但这样肯定是不对的)。为了解决这个问题,提供了0x03的一个安全机制。当检测到它们存在时,编码器就在最后一个字节前,插入一个新的字节:0x03。
0x00 00 00 -> 0x00 00 03 00 0x00 00 01 -> 0x00 00 03 01 0x00 00 02 -> 0x00 00 03 02 0x00 00 03 -> 0x00 00 03 03
这里的 0x000000 和 0x000001上面已经讲了,0x000002作为保留项,0x000003则是因为在NALU内部也存在使用情况。
0x2222 RBSP与SODB(数据字节流)的差别
差别在于SODB的尾部添加结束字符。有以下两种方式
方式一大多数的NALU使用的RBSP_STOP_ONE_BIT、RBSP_ALIGNMENT_ZERO_BIT
。
RBSP_STOP_ONE_BIT:1个比特,值为1。 RBSP_ALIGNMENT_ZERO_BIT:值为0,为使字节对齐。
也即:SODB RBSP_STOP_ONE_BIT RBSP_ALIGNMENT_ZERO_BIT(0…)
方式二另一种尾部,就是当NALU类型为片时,也即nal_unit_type等于1~5时。只是当entropy_coding_mode_flag值为1,采用CABAC无损压缩技术,并且more_rbsp_trailing_data()返回为true,也即RBSP中有更多数据时,添加一个或多个0x0000。
以上就是EBSP的这个数据结构。