一.帧的概念
在视频中,帧是视频传输的最小单位。一帧实际上静态图片,视频本质上是由连续的静态图片组成,我们经常耳熟能详的30帧、60帧实际上指的就是静态图片的数量。比方说30帧:它指的是1秒内有30张静态图片;而60帧,它指的是1秒内有60张静态图片。换言之,帧率越高,它所生成的静态图片就越多,视频质量就越好、视频的流畅度也更好。
二.H264重点帧类型的讲解
在H264中,最重要的NALU是SPS、PPS、SEI、I帧。下面是一个经典的NALU,一般而言H264开头都是以SPS、PPS为开头,若SEI有数据则添加SEI数据(SEI帧可有可无),紧接着就是I帧。
2.1. SPS:全称是序列参数集(它的NALU是00 00 00 01 67),它保存了一组编码视频序列的全局参数。也就是原始视频经过压缩编码后组成的序列,而每一帧编码数据它所依赖的全局参数就保存到图像参数集里面。通常来说,SPS和PPS都是整个NALU的起始位置。下面我们来看看,SPS里面包含什么内容:
上面这张图是通过H264分析软件分析出每一帧数据的具体情况,这里面的内容很多,我们挑几个比较重要的来讲解:
2.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
2.1.2. level_idc:
标识当前的码流Level,编码的Level定义了某些特殊环境下最大的视频分辨率、帧率等参数,码流的等级由level_idc所决定。比方说在当前码流中,level_idc = 40,则说明该码流的等级是4.0.
2.1.3. seq_parameter_set_id:
表示当前的序列参数集id,通过该id号,pps图像参数集合可以引用其表示的sps参数。
2.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
2.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三个的具体不同。
2.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
2.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]
2.1.5.3. 当pic_order_cnt_type ==2; 这种方法不会出现B帧,换言之就是不能出现连续的非参考帧并且解码输出的顺序和显示顺序一样。
2.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
2.1.7. max_num_ref_frames
表示最大参考帧数目,在这个H264码流里面是1
2.1.8. gaps_in_frame_num_allow_flag
标识位,这个标识位说明的是frame_num中是否允许不连续的数值。这个的数值是1,表示的是允许连续的数值
2.1.9. pic_height_in_mbs_minus1
用于计算图像的宽度,在这里它的单位是宏块个数(在这里显示是119)。所以这个图像的实际宽度是,
frame_width = 16 * (pic_height_in_mbs_minus1 + 1) = 16 * 120 = 1920
2.1.10. pic_height_in_map_units_minus1
使用PicHeightInMapUnits来衡量一幅图像的高度,PicHeightInMapUnits并不是图像以像素为单位的高度,而是需要考虑此宏块是帧编码还是场编码
PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1 = 67+1 =68
2.1.11. frame_mbs_only_flag
宏块编码方式的标识符,0表示的是宏块可能以帧编码或者场编码进行编码。当1的时候,所有的编码都是以帧编码的方式进行编码。要值得注意的是,这里取值的不同PicHeightInMapUnits所代表的意义也不相同,0的时候它是以场数据来对宏块进行计算,1的时候表示的是一帧数据按照宏块计算高度。计算的公式如下:
FrameHeightInMbs = (2 - frame_mbs_only_flag) * PicHeightInMapUnits。
注意:帧编码,参考为帧图像,采用帧运动补偿,两个场联合编码;场编码:参考为场图像,两个场分别编码,采用场运动补偿。帧编码:适用于相对于静止的画面,或者说运动画面偏小的图像。场编码:适用于运动激烈的场景,也就是画面中的人物在短时间内会有很大的变化。
2.1.12. mb_adaptive_frame_field_flag
标识符,说明是否采用宏块级别的帧场自适应编码。当改标识符为0的时候,不存在帧编码和场编码切换;当标识符为1的时候,宏块存在帧编码和场编码之间的切换。
2.1.13. direct_8x8_inference_flag
标识符,它用于B_Skip、B_Direct模式运动矢量来推导
2.1.14. frame_cropping_flag
标识符,是否对输出的图像进行裁剪
2.1.15. vui_parameter_present_flag
标识符,说明SPS里面是否有VUI信息。VUI信息指的是视频可用性信息,编码器在SPS里面将VUI信息传输给解码器,解码器通过接收对应的VUI信息做一些视频矫正处理。
2.2. pps:
除了序列参数集SPS之外,H264还有另外一个重要的参数集合Picture Parameter Set图像参数集合(PPS,它的NALU是它的NALU是00 00 00 01 68)。在一般的情况,PPS经常和SPS联合在一起保存到视频文件的头部里面。下面我们来看看,PPS具体的参数。
2.2.1. pic_parameter_set_id
表示当前的PPS的id,某个pps的码流中会被相应的slice引用,slice引用的PPS的方式保存PPS的id值。PPS的取值范围[0,255]。
2.2.2. seq_parameter_set_id:
表示当前pp引用的激活sps的id号,通过这个方式pps也可以获取到sps的参数值。取值范围在[0,31]
2.2.3. entropy_coding_mode_flag:
该标识符表示码流中摘编码/解码选择的算法,对于这部分语法元素,不同的编码配置,它的方式也是有所不同的。比方说在一个宏块元素中,宏块类型mb_type的语法描述符是”ue(v)|ae(v)”,在baseline profile设置下采用哥伦布指数进行编码,在main profile则采用CABAC方式进行编码。如图所示,它是采用CAVLC方法进行编码。
2.2.4. bottom_fileld_pic_order_in_frame_present_flag:
标识用于delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识,这两个语法元素表示了某一帧的底场的POC计算方法。
2.2.5. num_slice_groups_minus1
表示某一帧中slice groups个数。当该数值为0的时候,一帧中所有的slice都属于一个slice group。
2.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的默认值。
2.2.7. weighted_pred_flag
标识符,表示在P/SP slice中是否开启加权预测。
2.2.8. weighted_bipred_flag
表示在B Slice中加权预测方法,取值范围是[0,2]。0表示的是默认加权预测,1表示的是显式加权预测,2表示隐式加权预测。
2.2.9. pic_init_qp_minus_26和pic_init_qs_minus26
表示初始化的量化参数。实际的量化参数由该参数、slice header中的slice_qp_delta、slice_qs_delta计算得到
2.2.10. chroma_qp_index_offset
用于计算色度分量的量化参数,取值范围是[-12,12]
2.2.11. deblocking_filter_control_present_flag:
标识位,用于Slice Header中是否存在用于去块滤波器控制的信。当该标识符为1的时候,slice header中包含去块铝箔相应的信息;当标识符为0的时候,slice header没有任何信息
2.2.12. constrained_intra_pred_flag:
若标识符为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位为0时,标识宏块可以使用来自Inter类型宏块的信息。
2.2.13. redundant_pic_cnt_present_flag:
标识位,用于表示Slice header中是否存在redundant_pic_cnt的语法元素。当该标识位为1时,slice header包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。
2.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内容,如字幕、时间戳。
2.4. I帧
I帧就是我们常说的关键帧(NALU:00 00 00 01 65),它不需要参考任何帧就可以拥有一副完整的画面。
2.5. P帧
P帧是前向预测帧,它需要根据本帧和相邻的前一帧(I帧或P帧)的不同点来压缩本帧数据。下面是P帧的图解:
从上面这张图我们可以看出来,P帧(1号)的压缩数据需要参考I帧的数据进行视频的压缩,而P帧(2号)需要参考P帧(1号)的数据进行视频压缩。换言之,P帧它需要不停地参考前面帧的数据才能够压缩本帧数据。
2.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等。