在H.264的解码过程中,每一帧的数据按照相应的NAL Unit在码流中的顺序传入解码器进行解码。需注意的是,首先传入解码器的视频帧的NAL unit,解码完成后其对应的图像不一定会首先显示。其原因是由于B帧的存在,视频帧在输出时会进行顺序重排。视频帧的NAL Unit数据传入解码器的顺序称之为解码顺序,而解码完成后图像显示或输出的顺序称之为显示顺序。解码顺序和显示顺序的概念如下图所示:
在H.264的码流中,表示解码顺序和显示顺序分别有相应的语法元素表示。其中解码顺序由frame_num表示,而显示顺序由picture_order_count表示。这两个值都会在码流中保存,并在读取slice信息时解析。这两个值在SliceHeader中按下列形式保存:
某一帧图像在解码完成后,可能会被保存于解码图像缓存中,用于后续图像帧间预测的参考帧。在解码图像缓存中,用于P帧或B帧帧间编码的参考帧图像保存为一个或两个参考帧列表,列表中参考帧的顺序可以通过某个专门的过程进行重排列。
在每一个slice的slice_header结构中都可以解析出frame_num这一语法元素,该值表示了当前slice在一个GOP中的解码顺序值。
在一个GOP中,第一个slice即作为随机接入点的IDR slice,其frame_num值为0,表示当前slice是一个GOP的起点。GOP中的其他slice按照相应距离IDR的顺序按1递增。当另一个语法元素gaps_in_frame_num_value_allowed存在时,slice可以以大于1的值递增,此时缺失的frame_num值需要解码器用空slice数据进行填充。
POC即picture order count,是用于表示视频帧显示顺序的值。视频中IDR的第一个field作为POC的开始,其值为0。在H.264的标准中,POC的计算方法在标准文档中的8.2.1节中定义。
对于H.264的码流,有三种结构会被赋予POC的值:coded frame(编码帧), coded field(编码场)和complementary field pair(互补参考场对),每种类型的POC都由TopFieldOrderCnt和BottomFieldOrderCnt这两个值的一个或两个组成:
在H.264中,TopFieldOrderCnt和BottomFieldOrderCnt共定义了3种解析方法,由sps中的值pic_order_cnt_type决定。
当pic_order_cnt_type为0时,POC的值通过slice_header中的数据计算得到。计算方式如下:
中间变量prevPicOrderCntMsb和prevPicOrderCntLsb可以认为是当前帧前面一帧的POC数据,其计算方式为:
该值可以认为是当前帧的POC的高位值。在该过程中需要两个从码流中解析出的语法元素值:
获取到了上述数据之后,可依据标准文档中的公式8-3计算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
对于一个非底场的slice,TopFieldOrderCnt的计算方式非常简单,即将PicOrderCntMsb与pic_order_cnt_lsb求和即可:
TopFieldOrderCnt = PicOrderCntMsb + pic_order_cnt_lsb
对于一个非顶场的slice,BottomFieldOrderCnt的计算方式根据field_pic_flag值判断。若field_pic_flag为0,即当前slice按帧编码,则BottomFieldOrderCnt的计算方法为:
BottomFieldOrderCnt = TopFieldOrderCnt + delta_pic_order_cnt_bottom
否则,当field_pic_flag为1,即当前slice为一帧的底场时,BottomFieldOrderCnt的计算方法为:
BottomFieldOrderCnt = PicOrderCntMsb + pic_order_cnt_lsb
当pic_order_cnt_type为1时,采用这种模式计算POC的值。计算步骤如下:
变量prevFrameNum被赋值为按照解码顺序在当前帧之前一帧的frame_num;类似地,变量prevFrameNumOffset被赋值为按照解码顺序在当前帧之前一帧的FrameNumOffset;
FrameNumOffset计算方法根据当前帧是否是IDR,以及prevFrameNum与frame_num的比较关系计算得到:
FrameNumOffset = prevFrameNumOffset + MaxFrameNum
判断从sps中读取的值num_ref_frames_in_pic_order_cnt_cycle,若该值为非0,则absFrameNum的计算方法为:
absFrameNum = FrameNumOffset + frame_num
否则,absFrameNum的值为0。另外,若nal_ref_idc值为0且absFrameNum非0,absFrameNum需再减去1:
absFrameNum = absFrameNum − 1
当absFrameNum大于0时,picOrderCntCycleCnt和frameNumInPicOrderCntCycle分别为absFrameNum - 1除以num_ref_frames_in_pic_order_cnt_cycle的商和余数:
picOrderCntCycleCnt = ( absFrameNum − 1 ) / num_ref_frames_in_pic_order_cnt_cycle
frameNumInPicOrderCntCycle = ( absFrameNum − 1 ) % num_ref_frames_in_pic_order_cnt_cycle
下一步根据absFrameNum的计算expectedPicOrderCnt的值。若absFrameNum的值为0,则expectedPicOrderCnt的值亦为0;否则该值由picOrderCntCycleCnt、ExpectedDeltaPerPicOrderCntCycle和offset_for_ref_frame共同计算得到:
if( absFrameNum > 0 ){
expectedPicOrderCnt = picOrderCntCycleCnt * ExpectedDeltaPerPicOrderCntCycle;
for( i = 0; i <= frameNumInPicOrderCntCycle; i++ )
expectedPicOrderCnt = expectedPicOrderCnt + offset_for_ref_frame[ i ];
} else
expectedPicOrderCnt = 0
如果nal_ref_idc的值为0,expectedPicOrderCnt再增加offset_for_non_ref_pic:
expectedPicOrderCnt = expectedPicOrderCnt + offset_for_non_ref_pic;
对于帧编码的slice,TopFieldOrderCnt和BottomFieldOrderCnt通过上面计算得到的expectedPicOrderCnt,以及sps中读取的语法元素delta_pic_order_cnt和offset_for_top_to_bottom_field计算得到:
TopFieldOrderCnt = expectedPicOrderCnt + delta_pic_order_cnt[0];
BottomFieldOrderCnt = TopFieldOrderCnt +offset_for_top_to_bottom_field + delta_pic_order_cnt[1];
当pic_order_cnt_type为2时,采用这种模式计算POC的值。计算步骤如下:
FrameNumOffset和prevFrameNumOffset同模式2中的计算方法类似,FrameNumOffset计算方法根据当前帧是否是IDR,以及prevFrameNum与frame_num的比较关系计算得到:
FrameNumOffset = prevFrameNumOffset + MaxFrameNum
计算tempPicOrderCnt:
tempPicOrderCnt = 2 * ( FrameNumOffset + frame_num ) − 1
tempPicOrderCnt = 2 * ( FrameNumOffset + frame_num )
最后,在帧编码的条件下,TopFieldOrderCnt和BottomFieldOrderCnt的值都与tempPicOrderCnt相等:
TopFieldOrderCnt = tempPicOrderCnt
BottomFieldOrderCnt = tempPicOrderCnt
当H.264作为参考帧的某一帧解码完成后,该帧的数据将会保存在解码图像缓存区中,并且按照相应的规则标记为短期或长期参考帧。其中,短期参考帧由上文中提到的frame_num标记,长期参考帧由另一个值LongTermPicNum标记。
每一个P帧的解码对应一个参考帧列表RefPicList0,每一个B帧对应两个独立的参考帧列表RefPicList0和RefPicList1。
参考帧的索引值用于从参考帧列表中获取数据。在参考帧列表的初始化、更新,参考帧的标记,以及处理非连续的frame_num时,需要计算参考帧的图像序号,其中主要有FrameNum, FrameNumWrap, PicNum, LongTermFrameIdx 和 LongTermPicNum等。
对于一个短期参考帧,计算FrameNum和FrameNumWrap。当前帧的FrameNum和FrameNumWrap计算方法为:
FrameNumWrap = FrameNum - MaxFrameNum
否则,FrameNumWrap的计算方式为:FrameNumWrap = FrameNum
对于一个长期参考帧,计算其LongTermFrameIdx的值。该过程在下节中详细讨论。
最后,对于每一个短期参考帧图像,计算PicNum值,对于一个长期参考帧图像,计算LongTermPicNum。如果当前帧为帧编码,即field_pic_flag为0,则二者的值分别与FrameNumWrap和LongTermPicNum相等:
PicNum = FrameNumWrap
LongTermPicNum = LongTermFrameIdx
在JM8.6代码中的体现如下:
if (currPicStructure == FRAME)
{
for (i=0; iis_used==3)
{
if ((dpb.fs_ref[i]->frame->used_for_reference)&&(!dpb.fs_ref[i]->frame->is_long_term))
{
if( dpb.fs_ref[i]->frame_num > img->frame_num )
{
dpb.fs_ref[i]->frame_num_wrap = dpb.fs_ref[i]->frame_num - MaxFrameNum;
}
else
{
dpb.fs_ref[i]->frame_num_wrap = dpb.fs_ref[i]->frame_num;
}
dpb.fs_ref[i]->frame->pic_num = dpb.fs_ref[i]->frame_num_wrap;
dpb.fs_ref[i]->frame->order_num=list0idx;
}
}
}
}
当NALU中的nal_ref_idc值非0,即当前NALU所代表的图像会被作为参考帧的时候,会执行参考帧的标记过程。执行该过程的主要原因可以理解为,由于当前帧会作为参考帧数据放入DPB中,当前DPB中已有的参考帧的性质可能会发生变化,即短期参考帧可能会变为长期参考帧,或者某个参考帧可能会被标记为不再用做参考,因此需要对DPB中的参考帧数据进行重新标记。
如同其命名所表示的含义一样,一个被标记为“用于短期参考”或“用于长期参考”的视频帧在解码过程中可以作为后续帧的参考数据,直到该参考帧被标记为“不再用作参考”为止。将一个参考帧标记为“不再用作参考”的方法通常有两种:
在标准文档8.2.5.1节中,解码参考帧的标记过程按如下步骤执行:
下面首先介绍滑动窗口法:
从上一小节中可以看出,滑动窗口法的效果主要在于将过期的短期参考帧从DPB中移除出去,并不涉及到对长期参考帧的操作(除非遇到IDR时将DPB全部清空)。而自适应内存控制法的操作流程比滑动窗口法要复杂得多。
执行自适应内存控制法标记参考帧的条件是adaptive_ref_pic_marking_mode_flag为1,此时slice_header中会包含一些附加的语法元素信息,如下表所示:
从上图的dec_ref_pic_marking结构中可以看出,如果adaptive_ref_pic_marking_mode_flag的值为1,那么其中将会多出若干个值:
其中,memory_management_control_operation可以取的范围为1~6,分别代表了不同的操作。
当memory_management_control_operation为1时,自适应内存控制过程会将某一个短期参考帧标记为“不作为参考”。具体的执行过程为:
picNumX = CurrPicNum − ( difference_of_pic_nums_minus1 + 1 )
其中,CurrPicNum为当前帧的frame_number,difference_of_pic_nums_minus1从dec_ref_pic_marking中解析得到。当memory_management_control_operation为2时,自适应内存控制过程会将某一个长期参考帧标记为“不作为参考”。具体的执行过程很简单,索引为LongTermPicNum等同于long_term_pic_num的长期参考帧将被标记为不作为参考。
当memory_management_control_operation为3时,自适应内存控制过程会将某一个短期参考帧标记为“作为长期参考帧”。在这种情况下,码流中会同时包含difference_of_pic_nums_minus1以及long_term_frame_idx这两个值。执行过程如下:
当memory_management_control_operation为4时,执行计算MaxLongTermFrameIdx的操作。计算过程如下:
所有被标记为“用作长期参考”且LongTermFrameIdx大于了MaxLongTermFrameIdx的图像都会被标记为“不作为参考”。
当memory_management_control_operation为5时,执行清空参考帧列表操作。该过程会将所有参考帧标记为“不作为参考”并将MaxLongTermFrameIdx设置为“无长期参考帧索引”。
当memory_management_control_operation为6时,将当前帧标记为长期参考帧。在这种情况下,需从码流中解析出long_term_frame_idx。执行过程如下:
在当前帧标记完成后,所有被标记为“作为参考帧”的帧、场和互补场对的数量综合不能超过Max( max_num_ref_frames, 1 )规定的值。