在我们做音视频产品的时候,经常会把音视频数据进行网络传输,而此时音视频数据就需要进行编码(所谓编码就是指压缩)。因为在网络传输的时候,网络带宽有限,若此时网络传输的时候还用原始数据进行传输的时候,则会对网络带宽造成极大的负担。比方说一个分辨率为1280 * 720 帧率为30帧的视频,按照YUV420格式的计算,它每秒传输的数据量就是1280* 720* 30 * 3/2 ~= 39.5M,这个数据量是极其惊人的。
所以此时,我们就要引入视频编码技术来压缩视频,让其体积大小能够大幅度缩小,这样在网络传输的时候就会大大降低网络负担。在音视频开发中,一般分为H264、H265这两种最常见的编码格式。H264的压缩比能够达到1:100,H265的压缩比能够达到1:200,但是目前业内由于HEVC技术还没完全成熟,所以绝大部分设备都是用H264进行压缩处理。
一.VCL层和NAL层的讲解
H264编码格式是目前业内最流行的视频编码格式,它是MPEG-4的第十个部分。H264具有高压缩率、高图像质量、网络适应性很强等特点。在同等的图像质量下,H264的压缩比远超绝大部分编码格式(HEVC除外)。在H264(HEVC)编码框架中分为两大层,一层是VCL(Video Coding Layer)、另外一层是NAL层(Network Abstraction Layer)。VCL层主要负责内容的表示(如下图),NAL层主要负责对H264数据进行打包和传输,下面我们来重点介绍这两层里面包含的知识点。
1. VCL层的知识点:
VCL层包含了四个比较重要的知识点,分别是帧内预测压缩、帧间预测压缩、变换量化、熵编码。
帧内压缩:也称之为空间压缩,当压缩一张图片的时候,若仅仅考虑帧的数据也不考虑相邻帧和帧之间的冗余数据,这样的方式就叫帧内压缩。在H264中I帧生成的原理就是帧内压缩,帧内压缩可以独立解码出一帧完整的图像而不需要参考任何帧,帧内压缩表现出来的数据是最大16 x 16的宏块(它也包括:4x4,8x8)。帧内压缩本质上是在空间的XY轴上进行压缩,它的压缩率比较小。下面9张图是9种不同的预测方式
帧间压缩:帧间压缩是通过对比相邻两帧之间的数据进行压缩,并且进一步提高压缩量。用这种方法可以先编码出一个完整的图像1帧,紧接着2帧的数据就不编码出一张完整的图像,而是只写入和1帧的不同的数据,这样2帧数据的大小则会大大降低。以此类推,3帧的数据参考2帧数据,并且也只写入2帧不同的数据,帧间压缩表现出来的数据同样也是最大16 x 16的宏块(它也包括:4x4,8x8)。按照这样的方法,不停地循环下去。这样的方式,就是P帧、B帧的实现方法。
变换量化:
为了要让压缩的H264图像在网络传输中节省更多的码率,需要采用变换编码以及量化技术来消除图像信号中的相关性以及减小图像编码中的动态变换范围。变换编码的原理就是把图像时域信号变换成频域信号,在频域范围内,绝大部分信号集中在低频区域,相对时域信号,码率能够大幅度下降。而在H264中通常用下面方法进行处理:H264数据经过帧内压缩(16 * 16亮度、4 * 4亮度、8 * 8亮度)、帧间预测(4 * 4 ~ 16 * 16亮度)得到了残差值。这些残差值需要经过冗余的统计,并进行数据的变换和量化操作。
对于H264的16 x 16的亮度块,通过4x4的前向DCT变换,然后对16个DC系数再进行4 x4 的Hadmard变换,最后把这16个DC系数进行量化。
而对于8x8的色度块,则进行4x4的DCT变换后,得到4个DC和60个AC系数并对DC系数进行2x2的Hadmard变换。变换后则对DC系数和AC系数进行量化操作。
要值得注意的是,由于变换块越大,编码的效率越高,图像的还原度越好。所以为了让其编码质量得到更好的效果,在H264的高清(HD)档次,它支持8x8的DCT变换,并且不需要对DC系数进行Hadmard变换,DCT变换后则对DC和AC系数进行统一处理。
熵编码压缩是一种无损压缩模式,其实现原理是使用的编码模式来表示输入的数据,并达到压缩效果。常用的熵编码方式分别是变长编码(CAVLC)和算术编码(CABAC)。
变长编码(CAVLC):对出现概率大的符号,对出现概率小的符号提供长字节二进制码。
算术编码(CABAC):采用一个浮点数代替一串输入符号,范围是[0,1)
具体的参考(https://blog.csdn.net/yanceyxin/article/details/82965656)
CABAC的编码压缩率远远大于CAVLC,CABAC具有更高的编码效果。
2. NAL层
(Network Abstraction Layer):负责以网络所要求的方式进行打包和传送,下图是一个NAL层的组成部分(这里主要介绍H264码流的NAL,H265的后面会讲)
NAL层一般由三部分组成,分别是StartCode(起始码:必须是00 00 00 01)、NALU Header(NALU头部)、NALU Payload(具体传输的视频内容)。
比方说一串H264码流的NALU:00 00 00 01 68 43 A0 25 56 2E....
其中:[00 00 00 01]是StartCode也就是起始码
[68] 是NAL头部
[43 A0 25 56 2E...]就是NALU Payload的数据
2.1. StartCode起始码:
起始码是所有H264码流开始时候的分隔符,一般分为0x000001和0x00000001两种
2.2. NAL头部:
NALU头部长度为一个字节,它的语法是NALU类型(5bit)、重要性位(2bit)、禁止位(1bit)
NALU类型的图表:
比方说:NALU头部是0x68,转换为二进制数据就是0110 1000.
这表示,禁止位为0,NAL重要性为11(最重要),01000换算成HEX格式就是8也就是PPS。
2.3. NALU Payload:
二. H264重点帧
在视频中,帧是视频传输的最小单位。一帧实际上静态图片,视频本质上是由连续的静态图片组成,我们经常耳熟能详的30帧、60帧实际上指的就是静态图片的数量。比方说30帧:它指的是1秒内有30张静态图片;而60帧,它指的是1秒内有60张静态图片。换言之,帧率越高,它所生成的静态图片就越多,视频质量就越好、视频的流畅度也更好。
1.H264重点帧类型的讲解
在H264中,最重要的NALU是SPS、PPS、SEI、I帧。下面是一个经典的NALU,一般而言H264开头都是以SPS、PPS为开头,若SEI有数据则添加SEI数据(SEI帧可有可无),紧接着就是I帧.
1.1. SPS:
全称是序列参数集(它的NALU是00 00 00 01 67),它保存了一组编码视频序列的全局参数。也就是原始视频经过压缩编码后组成的序列,而每一帧编码数据它所依赖的全局参数就保存到图像参数集里面。通常来说,SPS和PPS都是整个NALU的起始位置。下面我们来看看,SPS里面包含什么内容:
上面这张图是通过H264分析软件分析出每一帧数据的具体情况,这里面的内容很多,我们挑几个比较重要的来讲解:
1.1.1. profile_idc:
在H264中,通常定义了三种档次的profile
基准档次:baseline profile,profile_idc =66
主要档次:main profile, profile_idc = 77
扩展档次:extended profile , profile_idc = 88
最高档次:high profile, profile_idc = 100
1.1.2. level_idc:
标识当前的码流Level,编码的Level定义了某些特殊环境下最大的视频分辨率、帧率等参数,码流的等级由level_idc所决定。比方说在当前码流中,level_idc = 40,则说明该码流的等级是4.0.
1.1.3. seq_parameter_set_id:
表示当前的序列参数集id,通过该id号,pps图像参数集合可以引用其表示的sps参数。
1.1.4. log2_max_frame_num_minus4:
用于计算MaxFrameNum的数值,它的计算公式是MaxFrame = 2(log2_max_frame_num_minus4+4)。MaxFrameNum是frame_num的最大上限值,frame_num是图像序号的一种表示方法,在帧间预测中常用作一种参考帧标记手段,要注意的是frame_num是循环计数,当它达到MaxFrameNum后则从0重新计数。在当前码流中MaxFrameNum = 2(12+4) = 2^16 = 65536
1.1.5. pic_order_cnt_type:
表示解码picture order count(POC)的方法,它用来表示解码帧的显示顺序,当码流中存在B帧的时候,解码顺序和显示顺序不一样,视频帧显示需要根据POC重新排序,否则将会出现跳帧和不连续情况,比方说编码序列IPBPB,序号分别是0,4,2,8,6。 在H264里面每个帧里面都分别有两个序列号,一个是顶场序列号(TopFieldOrderCnt),一个是底场序列号(BottomFieldOrder),分别是0,0,4,4,2,2,8,8,6,6。POC是另外一种计算图像序列号的方法,该语法的取值一般是0、1、2三种,下面我们来看看0,1,2三个的具体不同。
1.1.5.1. 当pic_order_cnt_type == 0;
通过前一帧的参考图像的picOrderCntMsb计算当前图像的TopFieldPOC(顶场序列号)和BottomFieldPOC(底场序列号)。picOrderCntMsb的计算方式如下:
若当前是IDR帧,prevPicOrderCntMsb = prevPicOrderCntLsb = 0;
若当前是非IDR帧,分三种情况:
前一个参考图像是mmco = 5(memory_management_control_operation),并且前一个参考图像不是底场,则prevPicOrderCntMsb= 0,prevPicOrderCntLsb等于前一个参考图像的TopFieldPOC;
前一个参考图像是mmco = 5(memory_management_control_operation),并且前一个参考图像是底场,则prevPicOrderCntMsb= 0 = prevPicOrderCntLsb = 0;
前一个参考图像是mmco != 5(memory_management_control_operation),prevPicOrderCntMsb等于前一个参考图像的PicOrderCntMsb,prevPicOrderCntLsb等于前一个参考图像的prevPicOrderCntLsb。
换成表达式就是,PicOrderCntMsb计算方式如下:
if( (pic_order_cnt_lsb < prevPicOrderCntLsb) && ( ( prevPicOrderCntLsb − pic_order_cnt_lsb ) >= ( MaxPicOrderCntLsb / 2 ) ) )
PicOrderCntMsb = prevPicOrderCntMsb + MaxPicOrderCntLsb;
else if( ( pic_order_cnt_lsb > prevPicOrderCntLsb ) &&
( ( pic_order_cnt_lsb − prevPicOrderCntLsb ) > ( MaxPicOrderCntLsb / 2 ) ) )
PicOrderCntMsb = prevPicOrderCntMsb − MaxPicOrderCntLsb
else
PicOrderCntMsb = prevPicOrderCntMsb
再通过PicOrderCntMsb来计算顶场:
TopFieldOrderCnt = PicOrderCntMsb + pic_order_cnt_lsb(pic_order_cnt_lsb要通过slice_header解析出来)
然后通过TopFieldOrderCnt计算底场数据:
if( !field_pic_flag ) //帧格式
BottomFieldOrderCnt = TopFieldOrderCnt + delta_pic_order_cnt_bottom
else // 场格式
BottomFieldOrderCnt = PicOrderCntMsb + pic_order_cnt_lsb
1.1.5.2. 当pic_order_cnt_type == 1;
通过前一帧的参考图像的FrameNumOffset计算当前图像的TopFieldPOC(顶场序列号)和BottomFieldPOC(底场序列号)。FrameNumOffset的计算方式如下:若是IDR帧则FrameNumOffset = 0,若不是IDR帧并且prevFrameNum大于frame_num则是以下计算方式:
FrameNumOffset = prevFrameNumOffset + MaxFrameNum。所以总的表达式为:
if(IDRFLAG)
FrameNumOffset = 0;
else if(prevFrameNum > frame_num)
FrameNumOffset = prevFrameNum + MaxFrameNum;
else
FrameNumOffset = prevFrameNum ;
下一步是通过FrameNumOffset 来计算absFrameNum,代码如下:
if( num_ref_frames_in_pic_order_cnt_cycle != 0 )
absFrameNum = FrameNumOffset + frame_num
else
absFrameNum = 0
if( nal_ref_idc = = 0 && absFrameNum > 0 )
absFrameNum = absFrameNum − 1
If(absFrame > 0)
{
picOrderCntCycleCnt = ( absFrameNum − 1 ) / num_ref_frames_in_pic_order_cnt_cycle
frameNumInPicOrderCntCycle = ( absFrameNum − 1 ) % num_ref_frames_in_pic_order_cnt_cycle
}
然后在计算expectedPicOrderCnt
if( absFrameNum > 0 ){
expectedPicOrderCnt = picOrderCntCycleCnt * ExpectedDeltaPerPicOrderCntCycle
for( i = 0; i <= frameNumInPicOrderCntCycle; i++ )
expectedPicOrderCnt = expectedPicOrderCnt + offset_for_ref_frame[ i ]
} else
expectedPicOrderCnt = 0
if( nal_ref_idc = = 0 )
expectedPicOrderCnt = expectedPicOrderCnt + offset_for_non_ref_pic
}
最后通过expectedPicOrderCnt来计算TopFieldOrderCnt和BottomFieldOrderCnt
f( !field_pic_flag ) {
TopFieldOrderCnt = expectedPicOrderCnt + delta_pic_order_cnt[ 0 ]
BottomFieldOrderCnt = TopFieldOrderCnt +
offset_for_top_to_bottom_field + delta_pic_order_cnt[ 1 ]
} else if( !bottom_field_flag )
TopFieldOrderCnt = expectedPicOrderCnt + delta_pic_order_cnt[0]
else
BottomFieldOrderCnt = expectedPicOrderCnt + offset_for_top_to_bottom_field + delta_pic_order_cnt[0]
1.1.5.3. 当pic_order_cnt_type ==2;
这种方法不会出现B帧,换言之就是不能出现连续的非参考帧并且解码输出的顺序和显示顺序一样。
1.1.6. log2_max_pic_order_cnt_lsb_mins4
用于计算MaxPicOrderCntLsb的值,该数值表示的是POC的上限。
MaxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_mins4 +4)
当前H264码流中,MaxPicOrderCntLsb = 2^(12+4)=2^16 = 65536
1.1.7. max_num_ref_frames
表示最大参考帧数目,在这个H264码流里面是1
1.1.8. gaps_in_frame_num_allow_flag
标识位,这个标识位说明的是frame_num中是否允许不连续的数值。这个的数值是1,表示的是允许连续的数值
1.1.9. pic_height_in_mbs_minus1
用于计算图像的宽度,在这里它的单位是宏块个数(在这里显示是119)。所以这个图像的实际宽度是,
frame_width = 16 * (pic_height_in_mbs_minus1 + 1) = 16 * 120 = 1920
1.1.10. pic_height_in_map_units_minus1
使用PicHeightInMapUnits来衡量一幅图像的高度,PicHeightInMapUnits并不是图像以像素为单位的高度,而是需要考虑此宏块是帧编码还是场编码
PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1 = 67+1 =68
1.1.11. frame_mbs_only_flag
宏块编码方式的标识符,0表示的是宏块可能以帧编码或者场编码进行编码。当1的时候,所有的编码都是以帧编码的方式进行编码。要值得注意的是,这里取值的不同PicHeightInMapUnits所代表的意义也不相同,0的时候它是以场数据来对宏块进行计算,1的时候表示的是一帧数据按照宏块计算高度。计算的公式如下:
FrameHeightInMbs = (2 - frame_mbs_only_flag) * PicHeightInMapUnits。
注意:帧编码,参考为帧图像,采用帧运动补偿,两个场联合编码;场编码:参考为场图像,两个场分别编码,采用场运动补偿。帧编码:适用于相对于静止的画面,或者说运动画面偏小的图像。场编码:适用于运动激烈的场景,也就是画面中的人物在短时间内会有很大的变化。
1.1.12. mb_adaptive_frame_field_flag
标识符,说明是否采用宏块级别的帧场自适应编码。当改标识符为0的时候,不存在帧编码和场编码切换;当标识符为1的时候,宏块存在帧编码和场编码之间的切换。
1.1.13. direct_8x8_inference_flag
标识符,它用于B_Skip、B_Direct模式运动矢量来推导
1.1.14. frame_cropping_flag
标识符,是否对输出的图像进行裁剪
1.1.15. vui_parameter_present_flag
标识符,说明SPS里面是否有VUI信息。VUI信息指的是视频可用性信息,编码器在SPS里面将VUI信息传输给解码器,解码器通过接收对应的VUI信息做一些视频矫正处理。
1.2. pps:
除了序列参数集SPS之外,H264还有另外一个重要的参数集合Picture Parameter Set图像参数集合(PPS,它的NALU是它的NALU是00 00 00 01 68)。在一般的情况,PPS经常和SPS联合在一起保存到视频文件的头部里面。下面我们来看看,PPS具体的参数。
1.2.1. pic_parameter_set_id
表示当前的PPS的id,某个pps的码流中会被相应的slice引用,slice引用的PPS的方式保存PPS的id值。PPS的取值范围[0,255]。
1.2.2. seq_parameter_set_id:
表示当前pp引用的激活sps的id号,通过这个方式pps也可以获取到sps的参数值。取值范围在[0,31]
1.2.3. entropy_coding_mode_flag:
该标识符表示码流中摘编码/解码选择的算法,对于这部分语法元素,不同的编码配置,它的方式也是有所不同的。比方说在一个宏块元素中,宏块类型mb_type的语法描述符是”ue(v)|ae(v)”,在baseline profile设置下采用哥伦布指数进行编码,在main profile则采用CABAC方式进行编码。如图所示,它是采用CAVLC方法进行编码。
1.2.4. bottom_fileld_pic_order_in_frame_present_flag:
标识用于delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识,这两个语法元素表示了某一帧的底场的POC计算方法。
1.2.5. num_slice_groups_minus1
表示某一帧中slice groups个数。当该数值为0的时候,一帧中所有的slice都属于一个slice group。
1.2.6. num_ref_idx_l0_default_active_minus1、num_ref_idx_l1_default_active_minus1
表示当Slice Header中num_ref_idx_active_override_flag标识符为0时,slice的语法元素num_ref_idx_l0_default_active_minus1和num_ref_idx_l1_default_active_minus1的默认值。
1.2.7. weighted_pred_flag
标识符,表示在P/SP slice中是否开启加权预测。
1.2.8. weighted_bipred_flag
表示在B Slice中加权预测方法,取值范围是[0,2]。0表示的是默认加权预测,1表示的是显式加权预测,2表示隐式加权预测。
1.2.9. pic_init_qp_minus_26和pic_init_qs_minus26
表示初始化的量化参数。实际的量化参数由该参数、slice header中的slice_qp_delta、slice_qs_delta计算得到
1.2.10. chroma_qp_index_offset
用于计算色度分量的量化参数,取值范围是[-12,12]
1.2.11. deblocking_filter_control_present_flag:
标识位,用于Slice Header中是否存在用于去块滤波器控制的信。当该标识符为1的时候,slice header中包含去块铝箔相应的信息;当标识符为0的时候,slice header没有任何信息
1.2.12. constrained_intra_pred_flag:
若标识符为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位为0时,标识宏块可以使用来自Inter类型宏块的信息。
1.2.13. redundant_pic_cnt_present_flag:
标识位,用于表示Slice header中是否存在redundant_pic_cnt的语法元素。当该标识位为1时,slice header包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。
1.3. SEI帧的讲解
SEI是一种用于视频流传输中的额外附加信息(它在传输的时候可有可无),SEI是H264标准的一部分。SEI可以传输多种类型的信息,如字幕、时间戳等信息。SEI信息通过H264码流传输到解码器,解码器通过解析SEI对视频进行后处理操作。SEI的NALU格式是00 00 00 01 06 05 +payload_size + uuid + payload_content,具体的如下图:
00 00 00 01:SEI帧的StartCode
06:NALU类型为SEI
05:SEI的 payload type,这里的sei payload遵循user_data_unregistered()语法
2F:05后面的这个值是SEI的长度,SEI的长度是包含了UUID+PAYLOAD_CONTENT的总长度。
UUID:是SEI帧通用唯一标识码,相当于SEI的身份证号一样。在上面这张图里面,UUID是dc45e9bde6d948b7962cd820d923eeef
payload_content:具体的sei内容,如字幕、时间戳。
1.4. I帧
I帧就是我们常说的关键帧(NALU:00 00 00 01 65),它不需要参考任何帧就可以拥有一副完整的画面。
1.5. P帧
P帧是前向预测帧,它需要根据本帧和相邻的前一帧(I帧或P帧)的不同点来压缩本帧数据。下面是P帧的图解:
从上面这张图我们可以看出来,P帧(1号)的压缩数据需要参考I帧的数据进行视频的压缩,而P帧(2号)需要参考P帧(1号)的数据进行视频压缩。换言之,P帧它需要不停地参考前面帧的数据才能够压缩本帧数据.
1.6. B帧
B帧属于双向预测的帧,它需要根据相邻的前一帧数据、以及后一帧数据的不同点来压缩本帧,换言之B帧只记录本帧和前后帧的差值。B帧的压缩比是三种帧里面最高的,压缩比能够达到200:1。B帧常用在高清电影的和蓝光影响的录制。
三、GOP
GOP(Group of Pictures,中文名称是图像组)它是把一个图像序列中连续几个图像组成一个小组,它本质上是两个I帧的距离。通常来说,GOP的长度越长P帧/B帧的数量就会越多,压缩比更高,画面质量越好,所以在音视频开发中经常会用GOP的长度来改善画面质量。下面是通过StreamEye的软件来分析GOP的结构。
上面这张图是通过H264解析工具StreamEye来获取到对应H264的GOP信息,可以看到每一个画矩形框的部分就是I帧。从这张图的数据来看每个I帧的间隔都是30,这一点可以从下面statistcs数据的I distance参数可以看到。
GOP分为两种:一种是闭合GOP,另外一种是开放GOP。
闭合GOP是指不对外开放的GOP结构,它的特点是GOP内的帧不可以参考其前后其他的帧,闭合GOP一般都是以I帧开头,下面是闭合GOP结构:
而开放GOP的特点是:允许其内的帧参考其他GOP内的帧,一般而言在有B帧的情况下才会出现open-gop。如下图,末尾的两个B帧需要依赖下一个GOP中的I帧进行解码。
GOP的长度设置:GOP的设置,一般是帧率的整数倍。比方说视频的帧率是30帧,则GOP的长度最好设置为30、60等。若帧率是60,则把GOP设置为60、120等。