H.264编码原理


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

0、序言

        首先要弄明白编码的目的,有目的的学习效率会更好。编码是为了将数据进行压缩,这样在传输的过程中就不会使资源被浪费,用一个简单的例子来说明编码的必要性:

        

    而之所以会有视频编码,

    关键就在于此:一个视频,如果未经编码,它的体积是非常庞大的。


    以一个分辨率1920×1280,帧率30的视频为例:

共:1920×1280=2,073,600(Pixels 像素),每个像素点是24bit(前面算过的哦);
也就是:每幅图片2073600×24=49766400 bit,8 bit(位)=1 byte(字节);
所以:49766400bit=6220800byte≈6.22MB。


    这是一幅1920×1280图片的原始大小,再乘以帧率30。

    也就是说:每秒视频的大小是186.6MB每分钟大约是11GB,一部90分钟的电影,约是1000GB。。。

    怎么样呢?

    就算你现在电脑硬盘是4TB的(实际也就3600GB),也放不下几部大片呀!

    不仅要存储,还要传输,不然视频从哪来呢?

    如果按照100M的网速(12.5MB/s),下刚才那部电影,需要22个小时。。。

    我的天哪,再次崩溃。。。

    正因为如此,屌丝工程师们就提出了,必须对视频进行编码


1、前言

    H264里面有些概念一定要弄明白,否则因为有些概念模糊所以在理解某些知识点十分吃力,例如序列、GOP等等。

    H264在视频采集到输出中属于编解码层次的数据,如下图所示,是在采集数据后做编码压缩时通过编码标准编码后所呈现的数据。

image.png


2、H264相关概念

2.1序列

      H264编码标准中所遵循的理论依据个人理解成:参照一段时间内相邻的图像中,像素、亮度与色温的差别很小。所以当面对一段时间内图像我们没必要去对每一幅图像进行完整一帧的编码,而是可以选取这段时间的第一帧图像作为完整编码,而下一幅图像可以记录与第一帧完整编码图像像素、亮度与色温等的差别即可,以此类推循环下去。

       什么叫序列呢?上述的这段时间内图像变化不大的图像集我们就可以称之为一个序列。序列可以理解为有相同特点的一段数据。但是如果某个图像与之前的图像变换很大,很难参考之前的帧来生成新的帧,那么久结束删一个序列,开始下一段序列。重复上一序列的做法,生成新的一段序列。


2.2、帧类型

         H264结构中,一个视频图像编码后的数据叫做一帧,一帧由一个片(slice)或多个片组成,一个片由一个或多个宏块(MB)组成,一个宏块由16x16的yuv数据组成。宏块作为H264编码的基本单位。 

        在H264协议内定义了三种帧,分别是I帧、B帧与P帧

    I帧就是之前所说的一个完整的图像帧,而B、帧与P帧所对应的就是之前说的不编码全部图像的帧。

    P帧与B帧的差别就是P帧是参考之前的I帧而生成的,而B帧是参考前后图像帧编码生成的。


2.3、GOP(画面组,图像组)

        GOP我个人也理解为跟序列差不多意思,就是一段时间内变化不大的图像集。

    GOP结构一般有两个数字,如M=3,N=12

    M指定I帧和P帧之间的距离,N指定两个I帧之间的距离。

    上面的M=3,N=12,GOP结构为:IBBPBBPBBPBBI。

    在一个GOP内I frame解码不依赖任何的其它帧,p frame解码则依赖前面的I frame或P frame,B frame解码依赖前最近的一个I frame或P frame 及其后最近的一个P frame。


2.4、IDR帧(关键帧)

        在编码解码中为了方便,将GOP中首个I帧要和其他I帧区别开,把第一个I帧叫IDR,这样方便控制编码和解码流程,所以IDR帧一定是I帧,但I帧不一定是IDR帧;IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始算新的序列开始编码。I帧有被跨帧参考的可能,IDR不会。

    I帧不用参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。

    IDR就不允许这样,例如:

    IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15   

    这里的B8可以跨过I10去参考P7

    ------------------------------------------------------------------------

    IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 

    这里的B9就只能参照IDR8和P11,不可以参考IDR8前面的帧


作用:

    H.264引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。




3、H264压缩方式

    H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。

    帧内(Intraframe)压缩也称为空间压缩(Spatialcompression)。当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。


    帧间(Interframe)压缩的原理是:相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩(Temporalcompression),它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值(Framedifferencing)算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。


3.1、压缩方式说明

Step1:分组,也就是将一系列变换不大的图像归为一个组,也就是一个序列,也可以叫GOP(画面组);

Step2:定义帧,将每组的图像帧归分为I帧、P帧和B帧三种类型;

Step3:预测帧, 以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;

Step4:数据传输, 最后将I帧数据与预测的差值信息进行存储和传输。




4、H264分层结构

    H264的主要目标是为了有高的视频压缩比和良好的网络亲和性,为了达成这两个目标,H264的解决方案是将系统框架分为两个层面,分别是视频编码层面(VCL)网络抽象层面(NAL),如下图;

image.png


    VLC层是对核心算法引擎、块、宏块及片的语法级别的定义,负责有效表示视频数据的内容,最终输出编码完的数据SODB

    NAL层定义了片级以上的语法级别(如序列参数集参数集和图像参数集,针对网络传输,后面会描述到),负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。NAL层将SODB打包成RBSP然后加上NAL头组成一个NALU单元,具体NAL单元的组成也会在后面详细描述。

        这里说一下SODB与RBSP的关联,具体结构如图3所示:

SODB: 数据比特串,是编码后的原始数据;

RBSP: 原始字节序列载荷,是在原始编码数据后面添加了结尾比特,一个bit“1”和若干个比特“0”,用于字节对齐。


image.png



SODB RBSP EBSP的区别

SODB(String of Data Bits,数据比特串):

    最原始,未经过处理的编码数据



RBSP(Raw Byte Sequence Payload,原始字节序列载荷):

    在SODB的后面填加了结尾bit(RBSP trailing bits 一个bit ‘1’)若干bit ‘0’,以便字节对齐。



EBSP(Encapsulated Byte Sequence Payload, 扩展字节序列载荷):

    NALU的起始码为0x000001或0x00000001(起始码包括两种:3 字节(0x000001) 和 4 字节(0x00000001),在 SPS、PPS 和 Access Unit 的第一个 NALU 使用 4 字节起始码,其余情况均使用 3 字节起始码。)


仿校验字节(0x03):

    同时H264规定,当检测到0x000000时,也可以表示当前NALU的结束。那这样就会产生一个问题,就是如果在NALU的内部,出现了0x000001或0x000000时该怎么办?

    在RBSP基础上填加了仿校验字节(0x03)它的原因是:在NALU加到Annexb上时,需要填加每组NALU之前的开始码StartCodePrefix,如果该NALU对应的slice为一帧的开始则用4位字节表示,0x00000001,否则用3位字节表示0x000001.为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,就插入一个字节的0x03解码时将0x03去掉。也称为脱壳操作


关系图:

image.png



5.H264码流结构

    在具体讲述NAL单元前,十分有必要先了解一下H264的码流结构;

    在经过编码后的H264的码流如下图所示,从图中我们需要得到一个概念,H264码流是由一个个的NAL单元组成,其中SPS、PPS、IDR和SLICE是NAL单元某一类型的数据。

image.png



6、H264的NAL单元

6.1、H264的NAL结构

    在实际的网络数据传输过程中H264的数据结构是以NALU(NAL单元)进行传输的,

    传输数据结构组成为[NALU Header]+[RBSP],如下图所示:

image.png

     从之前的分析我们可以知道,VCL层编码后的视频帧数据,帧有可能是I/B/P帧,这些帧也可能是属于不同的序列之中;

    同一序列也还有相应的序列参数集与图片参数集;

    综上所述,想要完成准确无误视频的解码,除了需要VCL层编码出来的视频帧数据,同时还需要传输序列参数集图像参数集等等,所以RBSP不单纯只保存I/B/P帧的数据编码信息,还有其他信息也可能出现在里面。

    上面知道NAL单元是作为实际视频数据传输的基本单元,NALU头是用来标识后面RBSP是什么类型的数据,同时记录RBSP数据是否会被其他帧参考以及网络传输是否有错误,所以针对NAL头和RBSP的作用以及结构与所承载的数据需要做个简单的了解;


6.2、NAL头

一、 NAL头的组成

    NAL单元的头部是由forbidden_bit(1bit),nal_reference_bit(2bits)(优先级),nal_unit_type(5bits)(类型)三个部分组成的,组成如图6所示:

1、F(forbiden):禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误;

2、NRI:参考级别,占用NAL头的第二到第三个位;值越大,该NAL越重要。

3、Type:Nal单元数据类型,也就是标识该NAL单元的数据类型是哪种,占用NAL头的第四到第8个位;


image.png

二、 NAL单元数据类型

    NAL类型主要就是下面图中这些类型每个类型都有特殊的作用;

image.png

image.png


    在具体介绍NAL数据类型前,有必要知道NAL分为VCL非VCLNAL单元。

    在图中有介绍(图表中DIR应该为IDR),其中SPS、SEI、PPS等非VCL的NAL参数对解码和显示视频都是很有用的。


    而另外一个需要了解的概念就是参数集(Parameter sets),参数集是携带解码参数的NAL单元,参数集对于正确解码是非常重要的,在一个有损耗的传输场景中,传输过程中比特列或包可能丢失或损坏,在这种网络环境下,参数集可以通过高质量的服务来发送,比如向前纠错机制或优先级机制。Parameter sets与其之外的句法元素之间的关系如下图所示:


image.png

每种类型都有代表一种数据类型,比较重要的以下几种做个简单的介绍:

1、非VCL的NAL数据类型:

1)、SPS(序列参数集):SPS对如标识符、帧数以及参考帧数目、解码图像尺寸和帧场模式等解码参数进行标识记录。

2)、PPS(图像参数集):PPS对如熵编码类型、有效参考图像的数目和初始化等解码参数进行标志记录。

3)、SEI(补充增强信息):这部分参数可作为H264的比特流数据而被传输,每一个SEI信息被封装成一个NAL单元。SEI对于解码器来说可能是有用的,但是对于基本的解码过程来说,并不是必须的。


@:先标记一下,SPS、PPS内容是编码器给的。


2、VCL的NAL数据类型

1)、 头信息块,包括宏块类型,量化参数,运动矢量。这些信息是最重要的,因为离开他们,被的数据块种的码元都无法使用。该数据分块称为A类数据分块。

2)、 帧内编码信息数据块,称为B类数据分块。它包含帧内编码宏块类型,帧内编码系数。对应的slice来说,B类数据分块的可用性依赖于A类数据分块。和帧间编码信息数据块不通的是,帧内编码信息能防止进一步的偏差,因此比帧间编码信息更重要。

3)、 帧间编码信息数据块,称为C类数据分块。它包含帧间编码宏块类型,帧间编码系数。它通常是slice种最大的一部分。帧间编码信息数据块是不重要的一部分。它所包含的信息并不提供编解码器之间的同步。C类数据分块的可用性也依赖于A类数据分块,但于B类数据分块无关。

以上三种数据块每种分割被单独的存放在一个NAL单元中,因此可以被单独传输。


6.3、H264的NAL单元与片,宏之间的联系

    其实到这里可能就比较难理解了,为什么数据NAL单元中有这么多数据类型,这个SLICE又是什么东西,为什么不直接是编码后出来的原始字节序列载荷,所以我觉得在这里再讲述帧所细分的一些片和宏的概念应该是比较合适的,也是能够参照上下文更能理解这些概念的位置,又能给这些困惑做一个合理一点的解释,所以在此做一个描述:


1帧(一幅图像) = 1~N个片(slice)  //也可以说1到多个片为一个片组

1个片 = 1~N个宏块(Marcroblock)

1个宏块 = 16X16的YUV数据(原始视频采集数据)


    从数据层次角度来说,一幅原始的图片可以算作广义上的一帧,帧包含片组和片,片组由片来组成,片由宏块来组成,每个宏块可以是4*4、8*8、16*16像素规模的大小,它们之间的联系如图10所示。

    每个片都是一个独立的编码单位。


image.png


    从容纳数据角度来说,NAL单元除了容纳Slice编码的码流外,还可以容纳其他数据,这也就是为什么有SPS、PPS等这些数据出现的原因,并且这些数据在传输H264码流的过程中起到不可或缺的作用,具体作用上面也是有讲到的。

那么也就可以对下面这些概念做一个大小的排序了:

                序列>图像>片>宏>像素(当然还有片组、亚宏块等等这些概念,初步了解就不了解这么深了,后面再慢慢研究)


同时有几点需要说明一下,这样能便于理解NAL单元:

(1)、如果不采用 FMO(灵活宏块排序) 机制,则一幅图像只有一个片组;

(2)、如果不使用多个片,则一个片组只有一个片;

(3)、如果不采用 DP(数据分割)机制,则一个片就是一个 NALU,一个 NALU 也就是一个片。

   否则,一个片的组成需要由 三个 NALU 组成,也就是上面说到的A、B、C类数据块。


这时候在看下面这幅码流数据分层,下图就比较能理解整体的码流结构组成了;


image.png



    如我们所见,每个分片也包含着数据两部分,

    分片头中包含着分片类型、分片中的宏块类型、分片帧的数量以及对应的帧的设置和参数等信息,

    而分片数据中则是宏块,这里就是我们要找的存储像素数据的地方;

    宏块是视频信息的主要承载者,因为它包含着每一个像素的亮度和色度信息。

    视频解码最主要的工作则是提供高效的方式从码流中获得宏块中的像素阵列。

    宏块数据的组成如下图所示:

image.png                                   


    从上图中,可以看到,宏块中包含了宏块类型、预测类型、Coded Block Pattern、Quantization Parameter、像素的亮度和色度数据集等等信息。

    至此,我们对 H.264 的码流数据结构应该有了一个大致的了解。


需要注意的几点:

    H.264/AVC标准对送到解码器的NAL单元顺序是有严格要求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则解码器不能够正确解码。

    序列参数集(sps)NAL单元 必须在传送所有以此参数集为参考的其他NAL单元之前传送,不过允许这些NAL单元中间出现重复的序列参数集NAL单元。所谓重复的详细解释为:序列参数集NAL单元都有其专门的标识,如果两个序列参数集NAL单元的标识相同,就可以认为后一个只不过是前一个的拷贝,而非新的序列参数集。

    图像参数集(pps)NAL单元 必须在所有以此参数集为参考的其他NAL单元之前传送,不过允许这些NAL单元中间出现重复的图像参数集NAL单元,这一点与上述的序列参数集NAL单元是相同



好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***