【H.264/AVC视频编解码技术详解】二十四、帧间预测编码(2):解码、显示顺序与图像管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shaqoneal/article/details/88351985

《H.264/AVC视频编解码技术详解》视频教程已经在“CSDN学院”上线,视频中详述了H.264的背景、标准协议和实现,并通过一个实战工程的形式对H.264的标准进行解析和实现,欢迎观看!

“纸上得来终觉浅,绝知此事要躬行”,只有自己按照标准文档以代码的形式操作一遍,才能对视频压缩编码标准的思想和方法有足够深刻的理解和体会!

链接地址:H.264/AVC视频编解码技术详解

GitHub代码地址:点击这里


一、基本概念

在H.264的解码过程中,每一帧的数据按照相应的NAL Unit在码流中的顺序传入解码器进行解码。需注意的是,首先传入解码器的视频帧的NAL unit,解码完成后其对应的图像不一定会首先显示。其原因是由于B帧的存在,视频帧在输出时会进行顺序重排。视频帧的NAL Unit数据传入解码器的顺序称之为解码顺序,而解码完成后图像显示或输出的顺序称之为显示顺序。解码顺序和显示顺序的概念如下图所示:


在H.264的码流中,表示解码顺序和显示顺序分别有相应的语法元素表示。其中解码顺序由frame_num表示,而显示顺序由picture_order_count表示。这两个值都会在码流中保存,并在读取slice信息时解析。这两个值在SliceHeader中按下列形式保存:


某一帧图像在解码完成后,可能会被保存于解码图像缓存中,用于后续图像帧间预测的参考帧。在解码图像缓存中,用于P帧或B帧帧间编码的参考帧图像保存为一个或两个参考帧列表,列表中参考帧的顺序可以通过某个专门的过程进行重排列。

二、解码顺序计数值frame_num

在每一个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数据进行填充。

三、显示顺序标志值picture_order_count

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这两个值的一个或两个组成:

  1. 对于每一个编码帧,poc包含两个值TopFieldOrderCnt和BottomFieldOrderCnt;
  2. 对于每一个编码场,poc包含一个值,如果该field为顶场则为TopFieldOrderCnt,如果是底场为BottomFieldOrderCnt;
  3. 对于每一个互补参考场对,POC包含两个值,对顶场为TopFieldOrderCnt,对底场为BottomFieldOrderCnt;

在H.264中,TopFieldOrderCnt和BottomFieldOrderCnt共定义了3种解析方法,由sps中的值pic_order_cnt_type决定。

3.1 Slice Header中直传模式

当pic_order_cnt_type为0时,POC的值通过slice_header中的数据计算得到。计算方式如下:

3.1.1 计算中间变量prevPicOrderCntMsb和prevPicOrderCntLsb

中间变量prevPicOrderCntMsb和prevPicOrderCntLsb可以认为是当前帧前面一帧的POC数据,其计算方式为:

  • 如果当前帧为IDR帧,则prevPicOrderCntMsb和prevPicOrderCntLsb都设为0;
  • 如果当前帧为非IDR帧,则prevPicOrderCntMsb和prevPicOrderCntLsb都取自按照解码顺序的前一个参考帧的数据,prevPicOrderCntMsb的值为该帧的PicOrderCntMsb,prevPicOrderCntLsb的值为该帧的pic_order_cnt_lsb。

3.1.2 计算当前帧的PicOrderCntMsb

该值可以认为是当前帧的POC的高位值。在该过程中需要两个从码流中解析出的语法元素值:

  • pic_order_cnt_lsb:从slice_header结构中读取;
  • MaxPicOrderCntLsb:从sps结构中的log2_max_pic_order_cnt_lsb_minus4计算得到;

获取到了上述数据之后,可依据标准文档中的公式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

3.1.3 计算TopFieldOrderCnt和BottomFieldOrderCnt

对于一个非底场的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

3.2 预测与差分方式

当pic_order_cnt_type为1时,采用这种模式计算POC的值。计算步骤如下:

3.2.1 计算中间变量prevFrameNum、FrameNumOffset和prevFrameNumOffset

变量prevFrameNum被赋值为按照解码顺序在当前帧之前一帧的frame_num;类似地,变量prevFrameNumOffset被赋值为按照解码顺序在当前帧之前一帧的FrameNumOffset;
FrameNumOffset计算方法根据当前帧是否是IDR,以及prevFrameNum与frame_num的比较关系计算得到:

  • 若当前帧为IDR,则FrameNumOffset为0;
  • 若当前帧非IDR,且prevFrameNum大于frame_num,则计算方式为:
    	FrameNumOffset = prevFrameNumOffset + MaxFrameNum
    
  • 若当前帧非IDR,且prevFrameNum小于frame_num,则FrameNumOffset的值即为prevFrameNumOffset;

3.2.2 计算中间变量absFrameNum、picOrderCntCycleCnt、frameNumInPicOrderCntCycle和expectedPicOrderCnt

判断从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;

3.2.3 计算TopFieldOrderCnt和BottomFieldOrderCnt

对于帧编码的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];

3.3 显示顺序与解码顺序一致

当pic_order_cnt_type为2时,采用这种模式计算POC的值。计算步骤如下:

FrameNumOffset和prevFrameNumOffset同模式2中的计算方法类似,FrameNumOffset计算方法根据当前帧是否是IDR,以及prevFrameNum与frame_num的比较关系计算得到:

  • 若当前帧为IDR,则FrameNumOffset为0;
  • 若当前帧非IDR,且prevFrameNum大于frame_num,则计算方式为:
    FrameNumOffset = prevFrameNumOffset + MaxFrameNum
    
  • 若当前帧非IDR,且prevFrameNum小于frame_num,则FrameNumOffset的值即为prevFrameNumOffset;

计算tempPicOrderCnt:

  • 若当前帧为IDR,则tempPicOrderCnt值为0;
  • 若当前帧为非IDR,且nal_ref_idc为0,则tempPicOrderCnt的计算方法为
    tempPicOrderCnt = 2 * ( FrameNumOffset + frame_num ) − 1
    
  • 否则,tempPicOrderCnt的计算方法为:
    tempPicOrderCnt = 2 * ( FrameNumOffset + frame_num )
    

最后,在帧编码的条件下,TopFieldOrderCnt和BottomFieldOrderCnt的值都与tempPicOrderCnt相等:

TopFieldOrderCnt = tempPicOrderCnt
BottomFieldOrderCnt = tempPicOrderCnt

四、图像管理

当H.264作为参考帧的某一帧解码完成后,该帧的数据将会保存在解码图像缓存区中,并且按照相应的规则标记为短期或长期参考帧。其中,短期参考帧由上文中提到的frame_num标记,长期参考帧由另一个值LongTermPicNum标记。

每一个P帧的解码对应一个参考帧列表RefPicList0,每一个B帧对应两个独立的参考帧列表RefPicList0RefPicList1

4.1 计算图像序号

参考帧的索引值用于从参考帧列表中获取数据。在参考帧列表的初始化、更新,参考帧的标记,以及处理非连续的frame_num时,需要计算参考帧的图像序号,其中主要有FrameNum, FrameNumWrap, PicNum, LongTermFrameIdx 和 LongTermPicNum等。

对于一个短期参考帧,计算FrameNum和FrameNumWrap。当前帧的FrameNum和FrameNumWrap计算方法为:

  • 首先设FrameNum的值为对应的短期参考帧的frame_num的值;
  • 如果FrameNum的值大于当前帧slice_header中解析出的frame_num值,则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; i<dpb.ref_frames_in_buffer; i++)
    {
      if (dpb.fs_ref[i]->is_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;
        }
      }
    }
  }

4.2 解码参考帧的标记

当NALU中的nal_ref_idc值非0,即当前NALU所代表的图像会被作为参考帧的时候,会执行参考帧的标记过程。执行该过程的主要原因可以理解为,由于当前帧会作为参考帧数据放入DPB中,当前DPB中已有的参考帧的性质可能会发生变化,即短期参考帧可能会变为长期参考帧,或者某个参考帧可能会被标记为不再用做参考,因此需要对DPB中的参考帧数据进行重新标记。

如同其命名所表示的含义一样,一个被标记为“用于短期参考”或“用于长期参考”的视频帧在解码过程中可以作为后续帧的参考数据,直到该参考帧被标记为“不再用作参考”为止。将一个参考帧标记为“不再用作参考”的方法通常有两种:

  • 滑动窗口法:通过一种“先进先出”机制进行的方法;
  • 自适应内存控制法;

在标准文档8.2.5.1节中,解码参考帧的标记过程按如下步骤执行:

  1. 首先,确保当前帧的所有slice解码完成;
  2. 随后,判断当前帧的帧类型。如果当前帧为一个IDR帧,则进行以下操作:
    • 将所有的参考帧标记为“不作为参考”;
    • 如果long_term_reference_flag为0,则该IDR帧被标记为“作为短期参考”,且MaxLongTermFrameIdx设为“无长期参考帧索引”;
    • 如果long_term_reference_flag为1,则该IDR帧被标记为“作为长期参考”,其LongTermFrameIdx设为0,并且MaxLongTermFrameIdx设为0;
  3. 如果当前帧为非IDR帧,则执行以下操作:
    • 如果adaptive_ref_pic_marking_mode_flag为1,则进行自适应内存控制法标记参考帧;
    • 如果adaptive_ref_pic_marking_mode_flag为0,则进行滑动窗口法标记参考帧;
  4. 如果当前帧为非IDR帧,且没有因为memory_management_control_operation的值等于6而被标记为“用于长期参考”,则该帧被标记为“用于短期参考”。

4.2.1 滑动窗口法

下面首先介绍滑动窗口法:

  1. 如果当前图像是一个 complementary reference field pair 中按照解码顺序的第二个场,且第一场被标记为“用作短期参考”,那么当前图像和该complementary reference field pair都被标记为“作为短期参考”。
  2. 否则的话,根据如下步骤执行:
    1. 设numShortTerm为作为短期参考的编码帧、编码场或互补场对的总数,numLongTerm为长期参考帧/场/场对的总数;
    2. 当numShortTerm与numLongTerm之和达到Max(max_num_ref_frames, 1)时,在满足numShortTerm大于0的前提下,FrameNumWrap值最小的那个作为短期参考的编码帧、编码场或互补场对将被标记为“不作为参考”。

4.2.2 自适应内存控制法

从上一小节中可以看出,滑动窗口法的效果主要在于将过期的短期参考帧从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;
  • difference_of_pic_nums_minus1;
  • long_term_pic_num;
  • long_term_frame_idx;
  • max_long_term_frame_idx_plus1

其中,memory_management_control_operation可以取的范围为1~6,分别代表了不同的操作。

4.2.2.1 将短期参考帧标记为“不作为参考”

当memory_management_control_operation为1时,自适应内存控制过程会将某一个短期参考帧标记为“不作为参考”。具体的执行过程为:

  1. 首先计算picNumX。计算方法为:
    picNumX = CurrPicNum − ( difference_of_pic_nums_minus1 + 1 )
    
    其中,CurrPicNum为当前帧的frame_number,difference_of_pic_nums_minus1从dec_ref_pic_marking中解析得到。
  2. 对于帧编码的图像,PicNum等于picNumX的短期参考帧会被标记为“不作为参考”;

4.2.2.2 将长期参考帧标记为“不作为参考”

当memory_management_control_operation为2时,自适应内存控制过程会将某一个长期参考帧标记为“不作为参考”。具体的执行过程很简单,索引为LongTermPicNum等同于long_term_pic_num的长期参考帧将被标记为不作为参考。

4.2.2.3 将长期参考帧标记为“短期参考帧”

当memory_management_control_operation为3时,自适应内存控制过程会将某一个长期参考帧标记为“作为短期参考帧”。在这种情况下,码流中会同时包含difference_of_pic_nums_minus1以及long_term_frame_idx这两个值。执行过程如下:

  1. 按照4.2.2.1中的方法计算picNumX;
  2. 如果long_term_frame_idx对应的长期参考帧存在,则该长期参考帧标记为“不作为参考”;
  3. 对于帧编码图像,由picNumX所代表的短期参考帧,将被标记为长期参考帧,并将对应的LongTermFrameIdx设为long_term_frame_idx。

4.2.2.4 计算MaxLongTermFrameIdx

当memory_management_control_operation为4时,执行计算MaxLongTermFrameIdx的操作。计算过程如下:

  • 如果码流中解析出的max_long_term_frame_idx_plus1的值为0,则MaxLongTermFrameIdx被设置为“无长期参考帧索引”;
  • 否则,MaxLongTermFrameIdx的值设置为max_long_term_frame_idx_plus1-1。

所有被标记为“用作长期参考”且LongTermFrameIdx大于了MaxLongTermFrameIdx的图像都会被标记为“不作为参考”。

4.2.2.5 清空参考帧列表

当memory_management_control_operation为5时,执行清空参考帧列表操作。该过程会将所有参考帧标记为“不作为参考”并将MaxLongTermFrameIdx设置为“无长期参考帧索引”。

4.2.2.6 将当前帧标记为长期参考帧

当memory_management_control_operation为6时,将当前帧标记为长期参考帧。在这种情况下,需从码流中解析出long_term_frame_idx。执行过程如下:

  • 如果long_term_frame_idx对应的长期参考帧存在,则该长期参考帧标记为“不作为参考”;
  • 将当前帧标记为“作为长期参考”,并将其LongTermFrameIdx设置为long_term_frame_idx;

在当前帧标记完成后,所有被标记为“作为参考帧”的帧、场和互补场对的数量综合不能超过Max( max_num_ref_frames, 1 )规定的值。

猜你喜欢

转载自blog.csdn.net/shaqoneal/article/details/88351985