2.参考图像列表
解码器每解码完一幅图像,都会判断该图像是否用于参考,并标记相应的参考图像,而且会在解码下一幅图像前,将参考图像列表初始化好;解码下一幅图像时,先根据图像的片头信息判断是否需要对参考列表重排序,如果需要,就根据片头的附加信息重新排序,之后开始对图像解码,解码完成后,重新进行参考图像的标记……如此循环。(IDR帧不需要参考帧,它解码完后标记产生第一个参考图像,供之后的图像解码时使用)
a.参考图像列表的初始化
前面我们已经知道,参考图像有短期参考和长期参考,这两种参考帧分别以不同的方式表示,其中短期参考图像使用 FrameNum(最终用的是PicNum) 的值作为标志,而长期参考图像则被分配一个长期参考帧索引LongTermFrameIdx(最终用的是LongTermPicNum)。参考图像列是一个由变量PicNum 和LongTermPicNum构成的数组,当解码一个宏块时,要指定参考图像,只需指定所需要的参考图像在这个数组中对应的索引值。关于LongTermFrameIdx的相关说明在下面介绍“参考图像的标记-几个重要的结构体”中说到,不妨先跳到那里去看看,这样继续往下看时也就少了些疑惑。
另外,当产生的RefPicList0 或 RefPicList1 中的分量个数分别大于num_ref_idx_l0_active_minus1 + 1 或 num_ref_idx_l1_active_minus1+ 1 时,多余的分量将从列表中丢弃;分量个数分别小于 num_ref_idx_l0_active_minus1 + 1 或num_ref_idx_l1_active_minus1 + 1 时,列表中剩余的分量设置为"no reference picture"。
在初始化图像列表之前,需要先计算出PicNum,这个在前面已经提及。
上图的计算过程用到了FrameNumWrap(每个参考图像都对应一个FrameNum和FrameNumWrap),这个变量是这样取值的:如果FrameNum(参考帧的frame_num) 不大于当前图像片头中的frame_num ,则FrameNumWrap 等于FrameNum;否则,FrameNumWrap 等于FrameNum 减去MaxFrameNum。使用FrameNumWrap的原因是:若直接使用frame_num来计算PicNum,由于frame_num是在0到MaxFrameNum之间循环变化的,所以如果当前图像的frame_num正好处于临界值,比如0时,那前几帧(按解码顺序)参考图像的frame_num就会比当前图像的frame_num(此处假设是0)大,由此计算出来的PicNum也就会比CurrPicNum大,这在处理上会带来一些麻烦,而对于FrameNumWrap,当前图像前几个参考帧对应的FrameNumWrap始终会比当前图像的frame_num小(如果当前图像处于临界位置0,那前几帧参考图像的FrameNumWrap就变成了负值),因此使用FrameNumWrap计算PicNum的话,只要某个参考图像在解码顺序上比当前图像靠前,那它对应的PicNum就比CurrPicNum小,不需要再分情况讨论。这个特点为场图像内P和SP片的参考帧列表中短期参考图像的排序提供了方便。
下面开始详细讨论图像列表的初始化。根据参考图像是帧还是场、当前片是P(或SP)片还是B片,可将这一节分为四部分讨论。
(1) “帧”中P和SP片的参考帧列表的初始化
排序规则:短期参考帧(或短期补充参考场对)位于长期参考帧(或长期参考场对)之前;短期参考帧和短期补充参考场对根据PicNum 值进行降序排列;长期参考帧和长期参考场对根据LongTermPicNum 值进行升序排列。由此可直接得到RefPicList0列表。
可见,对于P片和SP片,其参考图像列表中的短期参考帧都是基于PicNum排序的。
(2) “场”中P和SP片的参考帧列表的初始化
与(1)中的情况相比,参考图像列表中的每个场都有单独的列表索引,而且对一个场解码时,可用的参考图像数将是解码一个帧时的两倍。在对场中P和SP片参考图像列表初始化时,先要将短期参考图像和长期参考图像分别生成一个refFrameList0ShortTerm和refFrameList0LongTerm,这两个列表用于确定最后的RefPicList0。
refFrameList0ShortTerm的生成:所有包含一个或多个标记为“用于短期参考”场的帧,均包含在短期参考帧列表refFrameList0ShortTerm中。若当前场是互补参考场对中的第二场(解码顺序),并且第一场被标记为“用于短期参考”时,则该“第一场”包含在短期参考帧列表refFrameList0ShortTerm中。refFrameList0ShortTerm的顺序以具有最大FrameNumWrap 值的帧开始,以递减顺序直到具有最小FrameNumWrap 值的帧为止。(P帧是单向参考, FrameNumWrap越大,说明越靠近当前图像)。
refFrameListLongTerm的生成:所有包含一个或多个帧被标记为“用于长期参考”场的帧,均包含在长期参考帧列表LongTermFrameIdx中。当当前场是互补参考场对中的第二场(解码顺序),并且第一场被标记为“用于长期参考”时,则该第一场包含在长期参考帧列表refFrameList0LongTerm中。refFrameList0LongTerm 的顺序以具有最小LongTermFrameIdx值的帧开始,以递增顺序直到具有最大LongTermFrameIdx值的帧为止。
至于如何由refFrameList0ShortTerm 和refFrameList0LongTerm得到RefPicList0,将在后面提到。
(3) “帧”中B片的参考帧列表的初始化
B片需要双向参考。与P片不同的是,B片的RefPicList0 和RefPicList1 中短期参考图像的排列顺序取决于输出次序(而不再是FrameNum),即由PicOrderCnt( )给定。
函数PicOrderCnt( picX )如下:
if( picX 是一帧或互补场对 )
PicOrderCnt(picX)= picX 互补场对的Min(TopFieldOrderCnt,BottomFieldOrderCnt )
else if( picX 是顶场)
PicOrderCnt( picX ) = 场picX 的TopFieldOrderCnt
else if( picX 是底场)
PicOrderCnt( picX ) = 场picX 的BottomFieldOrderCnt
排序规则:短期参考帧和短期参考场对均排在长期参考帧和长期参考场对之前。
具体如下:
对于RefPicList0:
可将列表分为三段,头一段中所有的图像的POC都比当前图像的小,且按POC降序排列;中间一段,所有图像的POC都比当前图像的大,且按POC升序排列;前两段都是短期参考帧,最后一段是长期参考帧,各图像按其LongTermPicNum升序排列。
对于RefPicList1:
与RefPicList0类似,也可将列表分为三段:头一段中所有的图像的POC都比当前图像的大,且按POC升序排列;中间一段,所有图像的POC都比当前图像的小,且按POC降序排列;最后一段长期参考帧,与RefPicList0一样,也是按LongTermPicNum升序排列。
(4) “场”中B片的参考帧列表的初始化
需要先生成refFrameList0ShortTerm, refFrameList1ShortTerm 和 refFrameListLongTerm三个参考列表。然后用refFrameList0ShortTerm和refFrameListLongTerm可得到RefPicList0,用refFrameList1ShortTerm和refFrameListLongTerm可得到RefPicList1。
refFrameList0ShortTerm的生成:
分成两段:头一段中所有的图像的POC都比当前图像的小或相等(在帧模式下没有相等的POC,但是场模式下就可能有,比如一帧中的两场就可以有相等的POC),且按POC降序排列;第二段中,将剩下的POC比当前图像大的参考图像按POC升序排列。
refFrameList1ShortTerm的生成:
也分两段:头一段中所有的图像的POC都比当前图像的大(此处不包含相等),按POC升序排列;剩下的POC比当前图像小的参考图像按POC降序排列。
refFrameListLongTerm的生成:
按LongTermFrameIdx升序排列。
至于如何用这三个列表最终生成RefPicList0和RefPicList1,将在下面讲述。
(剩余的解释) 从refFrameListXShortTerm (X 可为 0 或1) 和refFrameListLongTerm得到RefPicListX的步骤:(本次排序的主要特点就是尽可能使奇偶场交叉出现)
从与当前场具有相同奇偶性的场(如果存在)开始,对短期参考场进行排序,其方法为从有序的帧列表refFrameListXShortTerm中通过交替选择具有不同奇偶性的场作为参考场。当一个参考帧的一个场还没有解码或被标记为“用于短期参考”时,忽略该场,并且把refFrameListXShortTerm帧列表中与该场具有相同奇偶性的下一个可用已存参考场插入到RefPicListX中。当已经排好序的refFrameListXShortTerm帧列表中没有更多的具有交替奇偶性的短期参考场时,把接下来的奇偶性可用、还没有被索引的场按照它们在已经排好序的refFrameListXShortTerm帧列表中具有的顺序插入到RefPicListX中。
从与当前场具有相同奇偶性的场(如果存在)开始,对长期参考场进行排序,其方法为从有序的帧列表refFrameListLongTerm 中通过交替选择具有不同奇偶性的场作为参考场。当一个参考帧的一个场还没有解码或被标记为“用于长期参考”时,忽略该场,并且把refFrameListLongTerm 帧列表中与该场具有相同奇偶性的下一个可用已存参考场插入到RefPicListX中。当已经排好序的refFrameListLongTerm 帧列表中没有更多的 具有交替奇偶性的长期参考场时,把剩下的奇偶性可用、还没有被索引的场按照它们在已经排好序的帧refFrameListLongTerm 帧列表中具有的顺序插入到RefPicListX中。
b.参考图像列表的重排序
从上面的参考图像列表的初始化过程中,我们可以看到,不论是四种情况中的哪一种,最后的参考图像列表总是可以看成两部分,第一部分是短期参考图像列表,剩下部分是长期参考列表。下面将分这两种情况讲述参考帧的重排序过程,重排序的目的是为了将需要的参考帧放在参考列表的前面,这样指定参考图像索引号时就可以用更小的数(更少的比特数)。
1)短期参考帧的重排序
首先,根据片头ref_pic_list_reordering( ) 中出现的所有abs_diff_pic_num_minus1元素计算出它们对应的参考图像的实际序号(picNum),并根据这些序号找到对应参考图像。然后按照各个“abs_diff_pic_num_minus1”在码流中出现的顺序,将它们对应的参考图像,依次移动到短期参考列表的开头位置(这些参考图像原来就在短期参考列表中,但重排序前他们在这个列表中的位置可能较靠后,重排序之后就会移动到列表前面)。
由abs_diff_pic_num_minus1计算picNum一般分为两步:
首先,根据片头ref_pic_list_reordering( ) 中出现的所有abs_diff_pic_num_minus1元素计算出它们对应的参考图像的序号picNumLXNoWrap,计算这个序号时考虑了picNumLXNoWrap的循环计数特性,它的值总在0到MaxPicNum之间(MaxPicNum在前面介绍frame_num元素时提到过):
— 如果reordering_of_pic_nums_idc等于0
if( picNumLXPred − ( abs_diff_pic_num_minus1 + 1 ) < 0 )
picNumLXNoWrap=picNumLXPred−(abs_diff_pic_num_minus1+1)+MaxPicNum
else
picNumLXNoWrap = picNumLXPred − ( abs_diff_pic_num_minus1 + 1 )
— 否则(reordering_of_pic_nums_idc 等于1)
if( picNumLXPred + ( abs_diff_pic_num_minus1 + 1 ) >= MaxPicNum )
picNumLXNoWrap=picNumLXPred+(abs_diff_pic_num_minus1+1)−MaxPicNum
else
picNumLXNoWrap = picNumLXPred + ( abs_diff_pic_num_minus1 + 1 )
其中picNumLXPred代表前一个picNumLXNoWrap值;而当计算第一个picNumLXNoWrap时,picNumLXPred=CurrPicNum。
然后,根据picNumLXNoWrap计算对应的picNumLX(类似于之前由frame_num计算FrameNumWrap):如果某个图像的picNumLXNoWrap比CurrPicNum大,则picNumLX=picNumLXNoWrap-MaxPicNum ;否则,picNumLX=picNumLXNoWrap。picNumLX就是某个参考图像的picNum值。
2)长期参考帧的重排序
大致过程与短期参考帧类似,不过这时不再需要计算picNumLXNoWrap和picNumLX这些过程了,因为对长期参考帧重排序时,片头ref_pic_list_reordering( ) 中的“long_term_pic_num”元素直接指定了参考图像的LongTermPicNum。
剩下的主要任务就是:按照各个“long_term_pic_num”在码流中出现的顺序,将它们对应的长期参考图像,依次移动到长期参考列表的开头位置(这些参考图像原来就在长期参考列表中,但重排序前他们在这个列表中的位置可能较靠后,重排序之后就会移动到列表前面)。需要注意的是,虽然是“开头位置”,但这个开头位置是针对参考列表的第二部分,即“长期参考列表”来说的,所有的长期参考图像依然都在短期参考列表之后。
c.参考图像的标记
为了方便后面的理解,这里要先简单介绍几个数据结构:
DPB(DecodedPicture Buffer),它保存了编、解码过程中所有的重建图像(在JM的参考代码中,DecodedPictureBuffer这个结构体是在mbuffer.h中定义的),这里说的“所有重建图像”,并不是真的指所有解码过的图像,那样内存开销太大了,dpb中的图像每隔一段时间会清空一次,“所有”指的是,自上一次清空之后,所有新解码的图像。什么时候dpb会清空一次呢?一般当IDR帧到来时,解码器会根据该IDR帧片头的dec_ref_pic_marking( )中的no_output_of_prior_pics_flag,来决定是否清空dpb,这个在前面已经说过。
一个dpb结构中,包含了三个FrameStore数组,准确的说是三个FrameStore类型的指针(一个FrameStore结构描述一个帧),为什么需要三个呢?其中,一个数组包含了所有重建帧(这个数组叫fs),而另外两个,一个仅包含了所有短期参考帧(数组名fs_ref),一个仅包含了所有长期参考帧(fs_ltref)。FrameStore是个很关键的结构体,它记录了一帧图像的很多有用信息:在帧模式下,它的成员变量标记了当前帧是否是参考帧,以及是否是长期参考;在场模式下,它的成员变量记录了本帧中的每个场的特性,包括某个场是否是参考场、以及是否是长期参考、是否已解码等。如果某帧(或帧中某场)用于长期参考,那此帧对应的LongTermFrameIdx也在这个结构中给出,LongTermFrameIdx表示当前帧在数组fs_ltref中的索引。另外,这个结构体的“is_non_existent”成员用来标记当前帧是否是“不存在”帧。
一个FrameStore结构中,包含三个StorablePicture结构,一个StorablePicture结构描述一幅图像,为什么需要三个呢?一个用于描述整帧图像,另外两个分别用于描述当前帧中顶、底两场图像。StorablePicture也是一个很重要的结构体,它记录了本图像的POC、顶场POC、底场POC等信息。另外,当本图像用于短期参考时,图像对应的PicNum就记录在这个结构体中;当本图像用长期参考时,图像对应的long_term_pic_num也记录在这个结构体中。
有了这些简单的认识后,来看一个序列中参考图像的标记过程。
在每一个序列的开始,会有一个IDR帧,对于IDR帧,不需要参考图像,这时会将之前所有标记为“短期参考”或“长期参考”的图像,都重新标记为“不用于参考”,即清空参考图像列表(此外,当一个片的片头出现memory_management_control_operation等于5的情况时,也会清空参考图像列表);并且会根据片头dec_ref_pic_marking( )中的no_output_of_prior_pics_flag,来决定是否清空dpb中的图像;然后根据long_term_reference_flag将当前的IDR帧标记为短期或长期参考:
如果long_term_reference_flag 等于0 ,则该IDR 图像将被标记为"短期参考" ,并将MaxLongTermFrameIdx 设为“非长期帧索引”(即标记为 -1 )。
否则,该IDR 图像需要被标记为"长期参考" 。 LongTermFrameIdx应被置为0,并将MaxLongTermFrameIdx设为0。(每标记一个长期参考的图像,MaxLongTermFrameIdx会加1,它始终是长期参考图像数组中最后一个长期参考图像的索引)。
上面介绍的IDR图像的标记过程,对于之后的非IDR图像,情况就比较多,看标准的话内容较多,但毕厚杰书中的插图比较直观。
当frame_num允许间隔并且间隔出现时,应为属于“不存在”图像的frame_num 的每一个值产生和标记一个帧,并将生成的这些帧也可标记为“不存在non-existing”或“用于短期参考”(注意,标准中只是允许将这些不存在帧“标记”为用于短期参考,但实际解码时不能真的以他们做参考,否则会引起错误,因此最好都标记成不存在)。
当片头dec_ref_pic_marking( )结构中的adaptive_ref_pic_marking_mode_flag 元素等于0 时,解码完当前片后,要用自动划窗法来标记参考帧。
自动滑窗法:即FIFO的方法,当DPB中参考帧总数量(包括短期和长期参考帧) 等于序列参数集中的num_ref_frames 时,将DPB中PicNum最小的短期参考图像,即最早的参考图像,移出DPB。注意,调用这个过程时,只是将最早的参考图像标记为非参考,而不负责将当前正解码的图像标记为短期参考,因为每个这样的片解码完后(这时当前的图像不一定解码完),都会调用一次这个过程,而如果解码完此片后当前图像还没有解码完,当然就不能标记当前图像。从JM提供的代码中可以看出,每解码完一帧图像,都会调用一次store_picture_in_dpb函数,在这个函数的最后会自动根据刚刚解码的图像的相应参数(参考帧标志位)来决定是否将当前图像标记为短期参考。
当片头dec_ref_pic_marking( )结构中的adaptive_ref_pic_marking_mode_flag 元素等于1 时,解码完当前片后,会用自适应内存控制标记过程。
自适应内存控制标记过程:
(附)
注:
下面介绍的几个函数的定义都在mbuffer.c文件中;
相关变量(dpb和listX数组)也都在mbuffer.c文件中定义,listX数组的定义为“StorablePicture **listX[6]”,但实际上我们只用了listX[0]和listX[1];
相关的结构体如DecodedPictureBuffer、FrameStore、StorablePicture等在mbuffer.h中定义;
最好先回头熟悉一下我上面介绍的三个结构体再往下看;
函数介绍:
store_picture_in_dpb函数,是在解码完一幅图像时调用,它负责将重建图像存储;其内部会进行参考图像的标记工作,在“标记”这一步,一般就是修改图像结构体(dpb.fs[i]->frame)中相应的标识变量的值,这些标识变量负责标记当前图像是否用于参考,以及是长期还是短期参考;修改完这些变量后,调用update_ref_list函数即可。
update_ref_list 函数,根据DPB中包含的所有图像结构体(dpb中的fs数组)中相关的标识变量,判断哪些是短期参考图像,然后将短期参考图像填充到dpb.ref_fs数组中,这之后ref_fs中就存在有用的信息了。与此对应的,update_ltref_list函数则用于对长期参考图像数组dpb.ltref_fs[]进行操作。(这一段中我提到了很多“数组”,但实际上在JM代码中它们只是指针)。
上面的三个函数都是用于参考图像的标记过程,但它们都不会操作参考图像列表(ListX数组),而只是在dpb内部进行操作。
init_lists() 初始化参考图像列表。根据dpb.ref_fs和dpb.ltref_fs数组中的内容,对ListX[0]和ListX[1]进行初始化。实际上就是将先将短期参考数组dpb.ref_fs[]的内容复制到ListX[0](或ListX[1],下面不再重复提示)中,然后对ListX[0]按PicNum排序;接着再将长期参考数组dpb.ltref_fs[]的内容复制到ListX[0]后半部分,然后对ListX[0]后半部分按LontTermPicNum排序。
reorder_ref_pic_list函数以及该函数内部调用的reorder_short_term、reorder_long_term,都用于参考图像列表重排序。他们都是根据片头ref_pic_list_reordering()中相应元素的值对ListX[0]和ListX[1]进行操作。