文章目录
一、写在前面
就解码而言,AVC是比较复杂的一个解码器,主要体现在它的多参考帧管理,包括图像序号的计算,参考图像列表的管理,参考图像的标记过程。这里将忽略B帧与场模式,以使讨论更集中在参考帧管理原理上。
本文依据毕厚杰的《新一代视频压缩编码标准》与多篇文章(多位博主的文章,引用见参考文献),整理与补充关于H.264多参考帧管理的原理与实现,争取做到通俗易懂,如有错漏,还请拍砖。
二 、背景
H.264 包含 VCL(视频编码层)和 NAL(网络提取层)。VCL 包括核心压缩引擎和块、宏块和片的语法级别定义,它的设计目标是尽可能地独立于网络得进行高效地编解码;而 NAL则负责将 VCL 产生的比特字符串适配到各种各样的网络和多元环境中,它覆盖了所有片级别以上的语法级别,同时支持以下功能:支持独立片解码;起始码唯一保证;支持 SEI;支持流格式编码数据传送。
总体来说,NAL 解码器负责将符合 H.264 码流规范的压缩视频流解码,并进行图像重建。根据[图 解码功能框图]的解码器框图,我们可以看到基本的解码流程如下:解码器从 NAL 中接收压缩的比特流,经过对码流进行熵解码获得一系列量化系数 X;这些系数经过反量化和反变换得到残差数据 D;解码器使用从码流中解码得到的头信息创建一个预测块 PRED,PRED 与残差数据 D 求和得到图像块数据 uF;最后每个 uF 通过去方块滤波得到重建图像的解码块 F。
一帧图像的完整解码过程如[图 解码流程图]。
H.264参考帧列表的管理主要包括参考帧列表的初始化、参考帧列表的重排序和参考图像的标记这三个步骤。
三、参考图像列表重排序
3.1 参考图像列表:来源、初始化、排序
一般来说,H.264会把需要编码的图像分为三种类型:I、P、B,其中的B、P类型的图像由于采用了帧间编码的这种编码方式,而帧间编码又是以参考图像为基础进行的,因此需要有个参考图像列表来管理之前生成的参考图像,方便用于对当前图像进行编码。
参考图像的来源
参考图像列表中的参考图像来自于解码图像缓存(DPB)中的已解码参考图像,DPB中可能包含参考以及非参考图像,参考图像列表把DPB中的参考图像整合成列表(数组)的形式,便于后续的排序等操作 。
参考图像的初始化
1、将DBP中符合要求的重建图像放到refPicList0、refPicList1
2、排序
参考图像列表的排序方式
参考图像分为短期参考图像(short-term reference)与长期参考图像(long-term reference),在某一个时间点上,参考图像只能是这两种的其中一种(非短即长)。参考图像列表分为两个部分:短期参考部分,长期参考部分。短期参考部分排在列表前头,长期排在后面。
P帧排序
一般来说,距离当前图像最近的参考图像会被当前图像用作最多的参考,距离越远则参考得越少,短期参考图像列表就是依据这种规律来进行排序的。
短期参考图像的序号由FrameNumWrap进行标记,以GOP为一个周期,GOP的第一帧的FrameNumWrap为0,后面持续递增,到当前帧是FrameNumWrap为最大,因此FrameNumWrap越大代表距离当前图像越近,因此将参考帧以FrameNumWrap降序方式放在refPicList0的起始位置。 长期参考图像的序号由LongTermPicNum进行标记,以升序的方式进行排序,(LongTermPicNum由MMCO分配) 。
B帧排序
B帧的参考帧排列方式与P帧并不完全相同,B帧的短期参考帧是以POC进行排序,其中有前向参考列表refPicList0与后向参考列表refPicList1
- 当参考帧的POC小于当前图像的POC时,将参考帧以POC降序方式放在refPicList0的起始位置上。然后剩余的短期参考图像按照POC升序的方式附加到refPicList0
- 当参考帧的POC大于当前图像的POC时,将参考帧以POC升序方式放在refPicList1的起始位置上。然后剩余的短期参考图像按照POC降序的方式附加到refPicList1
长期参考图像的序号由LongTermPicNum进行标记,以升序的方式进行排序,分别放进refPicList0与refPicList1中 。
为什么要重排序?
解码时,非I帧图像的解码将会以参考图像队列中的某一参考图像作为参考,宏块层需要有一个句法元素告诉解码器,当前帧所要参考的参考图像是哪一帧。编码器会给每一个参考帧分配一个唯一性的标识,也就是句法元素frame_num,但这个句法元素并非解码的时候宏块层所指定的序号,宏块层指定的序号是一个ref_id。为什么编码器在编码的时候,不在宏块层标明参考图像的frame_num,而是标明一个ref_id呢,它的目的在于节省码流。
什么是ref_id。编码器和解码器同步地维护一个参考帧队列,每解码一个片就将该队列刷新一下,把各图像按照特定的规则进行排序,排序后各图像在队列中的序号就是该图像的ref_id值。
frame_num的数值随着编解码的进行,会越来越大,如果直接用frame_num做标识,那码流的比特数开销会很大。假设,一帧1920x1088的图像以8x8的宏块单元可划分为240x136个宏块,每个宏块指定一个参考帧的序号,这个序号如果是frame_num,frame_num可能处于[0, 134]的区间,比特数开销大,如果这个是序号是ref_id,ref_id只处于[0, num_ref_idx_10_active_munus1]的区间,比特数开销小。
因此将frame_num映射为一个更小的变量ref_id。
3.2 重排序的实现过程
参考图象重排序的大致过程为:对参考图像列表中的参考图像进行遍历,每遍历到一个参考图像,读入句法元素reordering_of_pic_nums_idc,更新其参考图像序号并调整列表。
我们先看一下重排序的句法。如下:
句法元素reordering_of_pic_nums_idc表示一种重排序操作,在重排序句法中,ref_pic_list_reoredering_flag_10指明是否进行重排序操作,如果ref_pic_list_reoredering_flag_10为真,则表示为紧跟着会有一系列句法元素用于参考帧队列的重排序。reordering_of_pic_nums_idc与句法元素abs_diff_pic_num_minus1或long_term_pic_num配合,指明执行哪种重排序操作。语义如下表:
表 重排序操作句法
reordering_of_pic_nums_idc | 操作 |
---|---|
0 | 短期参考帧重排序,abs_diff_pic_num_minus1会出现在码流中,从当前图像的PicNum减去(abs_diff_pic_num_minus1+1)后指明需要重排序的图像 |
1 | 短期参考帧重排序,abs_diff_pic_num_minus1会出现在码流中,从当前图像的PicNum加上(abs_diff_pic_num_minus1+1)后指明需要重排序的图像 |
2 | 长期参考帧重排序,long_term_pic_num会出现在码流中,指明需要重排序的图像。 |
3 | 结束循环,退出重排序操作。 |
重排序实例1
我们直接看一个例子,假设现在在解码一个P slice,P帧只需要用到list0列表中的参考图像作参考,当前list0一共有5帧参考图像,而目前正解码的P帧的frame_num为158。
list0: [157, 155, 153, 1, 3]
前三帧为短期参考帧,降序排序,序号为[157, 155, 153],后两帧为长期参考帧,升序排列,序号为[1, 3]。
当我们解析到Slice层的重排序句法时,ref_pic_list_reoredering_flag_10为真,第1个reordering_of_pic_nums_idc为0,那么将对短期参考帧重排序。初始时,ref_id为0(即refIdxL0 = 0),需要重排序的短期参考图像经过固定流程的计算,确认其序号为153。参考列表第1个元素将更新为153,而随后的短期参考图像将后移一位覆盖,也就是157,155均向后移一位,155将覆盖原153。这个所谓的固定流程的计算,也就是从当前图像的PicNum减去(abs_diff_pic_num_minus1+1),这个计算的流程将在后续详解。
第二步如第一步类似的操作,eordering_of_pic_nums_idc为1,将对短期参考帧重排序,经过固定流程的计算,需要重排序的短期参考图像序号为155,则参考列表第2个元素将更新为155,而随后的短期参考图像将后移一位覆盖,也就是157向后移一位,157将覆盖原155。
第三步,reordering_of_pic_nums_idc = 2,我们将对长期参考帧重排序,long_term_pic_num = 3,此时refIdxL0为2,那么列表第2位的元素将从157更新为3,原157, 1这两个元素将后移一位,而最末尾的元素3是被指定要重排序的,因此将被移出列表。
第四步,reordering_of_pic_nums_idc = 3,此时重排序结束。
接下来我们看重排序实例2图解,将会更进一步理解重排序的过程。
重排序实例2
当前参考图像列表为[3, 2, 1, 0, 0, 1, 2],当前解码帧CurrPicNum为4,于是picNumLXPred为4,开始读取重排序句法并进行操作,第一个操作,短期参考帧重排序,经过f函数的计算,指明需要重排序的图像为元素0,图像序号为0的帧将被更新为列表第1个元素,图像序号为3、2、1的帧往后移一位,picNumLXPred更新为0。
长期参考图像的重排序比较简单,重排序句法中的long_term_pic_num指明了所要重排序的长期参考帧图像序号,不需要像短期参考帧一样经过麻烦的计算。
- 当中的浅紫灰色格子为重排序操作,对于短期参考图像,重排序经过计算而指定序号的图像,对于长期参考图像,直接通过操作数指定图像;
- 后面还没进行到的列表项会被后移;
- 划掉的格子为重复的图像,会被消除,如果被消除项后面还有其他项,后面的项会被前移填补前面被消除项的空格。
待重排短期参考图像序号的计算
[图 短期参考帧的重排序流程]展示了在读取了一个reordering_of_pic_nums_idc之后对于一帧参考图像的重排序过程,首先计算出待排序的参考图像的图像序号picNumLX,refIdxLX为参考列表待更新的目标位置,“调整列表”意味着将图像序号picNumLX的参考图像更新到该目标位置refIdxLX,其后的短期参考帧顺序后移覆盖,refIdxLX自增,下一轮所计算出待排序的参考图像的图像序号picNumLX将更新到新的refIdxLX位置。图(b)与图(c)展示了picNumLX的计算过程,对于短期参考帧而言,picNumLX 是多个变量的映射。
picNumLX = f(reoerdering_of_pic_nums_idc, picNumLXPred, abs_diff_pic_num_minus1, MaxPicNum, CurrPicNum)
当前解码帧图像序号为CurrPicNum。
初始预测值picNumLXPred = CurrPicNum,每一轮操作后,picNumLXPred更新为picNumLX。
实际值为abs_diff_pic_num_minus1 + 1。
MaxPicNum = frame_num的最大值。
3.3 重排序代码解析(JM)
参考帧列表的重排序主要在reorder_ref_pic_list函数中实现。
/*!
************************************************************************
* \brief
* Reordering process for reference picture lists
*
************************************************************************
*/
void reorder_ref_pic_list(StorablePicture **list, int *list_size, int num_ref_idx_lX_active_minus1, int *reordering_of_pic_nums_idc, int *abs_diff_pic_num_minus1, int *long_term_pic_idx)
{
int i;
int maxPicNum, currPicNum, picNumLXNoWrap, picNumLXPred, picNumLX;
int refIdxLX = 0;
if (img->structure==FRAME) //!< 帧模式
{
maxPicNum = img->MaxFrameNum; //!< frame_num的最大值
currPicNum = img->frame_num; //!< 当前帧的frame_num
}
else
{
maxPicNum = 2 * img->MaxFrameNum;
currPicNum = 2 * img->frame_num + 1;
}
picNumLXPred = currPicNum; //!< 预测值变量
for (i=0; reordering_of_pic_nums_idc[i]!=3; i++) //!< 重排序操作循环
{
if (reordering_of_pic_nums_idc[i]>3) //!< 该句法元素取值范围为0~3,3表示结束循环,退出重排序操作
error ("Invalid remapping_of_pic_nums_idc command", 500);
if (reordering_of_pic_nums_idc[i] < 2) //!< 短期参考帧重排序
{
if (reordering_of_pic_nums_idc[i] == 0) //!< 当前帧的PicNum减去(abs_diff_pic_num_minus1[i] + 1)指明需要重排序的图像
{
if( picNumLXPred - ( abs_diff_pic_num_minus1[i] + 1 ) < 0 ) //!< 预测值小于实际值
picNumLXNoWrap = picNumLXPred - ( abs_diff_pic_num_minus1[i] + 1 ) + maxPicNum; //!< frame_num循环计数
else
picNumLXNoWrap = picNumLXPred - ( abs_diff_pic_num_minus1[i] + 1 );
}
else // (remapping_of_pic_nums_idc[i] == 1) //!< 当前帧的PicNum加上(abs_diff_pic_num_minus1[i] + 1)指明需要重排序的图像
{
if( picNumLXPred + ( abs_diff_pic_num_minus1[i] + 1 ) >= maxPicNum ) //!< 预测值大于实际值
picNumLXNoWrap = picNumLXPred + ( abs_diff_pic_num_minus1[i] + 1 ) - maxPicNum; //!< frame_num循环计数
else
picNumLXNoWrap = picNumLXPred + ( abs_diff_pic_num_minus1[i] + 1 );
}
picNumLXPred = picNumLXNoWrap;
if( picNumLXNoWrap > currPicNum )
picNumLX = picNumLXNoWrap - maxPicNum;
else
picNumLX = picNumLXNoWrap;
//!< picNumLX存放的是需要移至refIdxLX位置的短期参考图像的PicNum
reorder_short_term(list, num_ref_idx_lX_active_minus1, picNumLX, &refIdxLX); //!< 调用该函数实现短期参考帧的重排序
}
else //(remapping_of_pic_nums_idc[i] == 2) //!< 长期参考帧重排序
{//!< long_term_pic_idx[i]存放的是需要移至refIdxLX位置的短期参考图像的序号
reorder_long_term(list, num_ref_idx_lX_active_minus1, long_term_pic_idx[i], &refIdxLX); //!< 调用该函数实现长期参考帧的重排序
}
}
// that's a definition
*list_size = num_ref_idx_lX_active_minus1 + 1;
}
reorder_short_term函数如下:
/*!
************************************************************************
* \brief
* Reordering process for short-term reference pictures 短期参考帧重排序
*
************************************************************************
*/
static void reorder_short_term(StorablePicture **RefPicListX, int num_ref_idx_lX_active_minus1, int picNumLX, int *refIdxLX)
{
int cIdx, nIdx;
StorablePicture *picLX;
picLX = get_short_term_pic(picNumLX); //!< 根据给定的picNumLX获取相应的短期参考帧
for( cIdx = num_ref_idx_lX_active_minus1+1; cIdx > *refIdxLX; cIdx-- )
RefPicListX[ cIdx ] = RefPicListX[ cIdx - 1]; //!< 将refIdxLX位置之后的参考帧依次后移
RefPicListX[ (*refIdxLX)++ ] = picLX; //!< 将给定的picNumLX短期参考帧存至refIdxLX位置
nIdx = *refIdxLX;
for( cIdx = *refIdxLX; cIdx <= num_ref_idx_lX_active_minus1+1; cIdx++ )
if (RefPicListX[ cIdx ])
if( (RefPicListX[ cIdx ]->is_long_term ) || (RefPicListX[ cIdx ]->pic_num != picNumLX )) //!< 该操作可将参考帧列表中重复的序号为picNumLX的参考帧覆盖掉
RefPicListX[ nIdx++ ] = RefPicListX[ cIdx ];
}
获取长期参考帧的函数get_short_term_pic如下:
/*!
************************************************************************
* \brief
* Returns short term pic with given picNum 返回给定picNum序号的短期参考帧
*
************************************************************************
*/
static StorablePicture* get_short_term_pic(int picNum)
{
unsigned i;
for (i=0; i<dpb.ref_frames_in_buffer; i++) //!< 遍历dpb中的参考帧
{
if (img->structure==FRAME) //!< 帧模式
{
if (dpb.fs_ref[i]->is_reference == 3)
if ((!dpb.fs_ref[i]->frame->is_long_term)&&(dpb.fs_ref[i]->frame->pic_num == picNum))
return dpb.fs_ref[i]->frame; //!< 找到pic_num等于picNum且不是长期参考帧的短期参考帧,返回
}
else //!< 场模式(略过)
{
if (dpb.fs_ref[i]->is_reference & 1)
if ((!dpb.fs_ref[i]->top_field->is_long_term)&&(dpb.fs_ref[i]->top_field->pic_num == picNum))
return dpb.fs_ref[i]->top_field;
if (dpb.fs_ref[i]->is_reference & 2)
if ((!dpb.fs_ref[i]->bottom_field->is_long_term)&&(dpb.fs_ref[i]->bottom_field->pic_num == picNum))
return dpb.fs_ref[i]->bottom_field;
}
}
// return NULL;
return no_reference_picture;
}
reorder_long_term是长期参考帧的重排序实现,比较简单。
/*!
************************************************************************
* \brief
* Reordering process for long-term reference pictures 长期参考帧重排序
*
************************************************************************
*/
static void reorder_long_term(StorablePicture **RefPicListX, int num_ref_idx_lX_active_minus1, int LongTermPicNum, int *refIdxLX)
{
int cIdx, nIdx;
StorablePicture *picLX;
picLX = get_long_term_pic(LongTermPicNum); //!< 获取给定LongTermPicNum的相应的长期参考帧
for( cIdx = num_ref_idx_lX_active_minus1+1; cIdx > *refIdxLX; cIdx-- )
RefPicListX[ cIdx ] = RefPicListX[ cIdx - 1]; //!< 将refIdxLX之后的参考帧依次右移
RefPicListX[ (*refIdxLX)++ ] = picLX; //!< 将LongTermPicNum对应的长期参考帧存至refIdxLX位置
nIdx = *refIdxLX;
for( cIdx = *refIdxLX; cIdx <= num_ref_idx_lX_active_minus1+1; cIdx++ )
if (RefPicListX[ cIdx ])
if( (!RefPicListX[ cIdx ]->is_long_term ) || (RefPicListX[ cIdx ]->long_term_pic_num != LongTermPicNum )) //!< 可将序号为LongTermPicNum重复的参考帧覆盖掉
RefPicListX[ nIdx++ ] = RefPicListX[ cIdx ];
}
获取长期参考帧的函数get_long_term_pic 实现如下:
/*!
************************************************************************
* \brief
* Returns long term pic with given LongtermPicNum
*
************************************************************************
*/
static StorablePicture* get_long_term_pic(int LongtermPicNum)
{
unsigned i;
for (i=0; i<dpb.ltref_frames_in_buffer; i++) //!< 遍历dpb中所有的长期参考帧
{
if (img->structure==FRAME) //!< 帧模式
{
if (dpb.fs_ltref[i]->is_reference == 3)
if ((dpb.fs_ltref[i]->frame->is_long_term)&&(dpb.fs_ltref[i]->frame->long_term_pic_num == LongtermPicNum))
return dpb.fs_ltref[i]->frame; //!< 找到long_term_pic_num等于LongtermPicNum的长期参考帧,返回
}
else //!< 场模式(略过)
{
if (dpb.fs_ltref[i]->is_reference & 1)
if ((dpb.fs_ltref[i]->top_field->is_long_term)&&(dpb.fs_ltref[i]->top_field->long_term_pic_num == LongtermPicNum))
return dpb.fs_ltref[i]->top_field;
if (dpb.fs_ltref[i]->is_reference & 2)
if ((dpb.fs_ltref[i]->bottom_field->is_long_term)&&(dpb.fs_ltref[i]->bottom_field->long_term_pic_num == LongtermPicNum))
return dpb.fs_ltref[i]->bottom_field;
}
}
return NULL;
}
四、参考图像的标记过程
我们回顾一下之前说的H.264参考帧列表管理的三个步骤。
H.264参考帧列表的管理主要包括参考帧列表的初始化、参考帧列表的重排序和参考图像的标记这三个步骤。
参考帧列表的初始化是将DBP中符合要求的重建图像放到refPicList0、refPicList1并且按规则排序。重排序(Reordering)操作是对参考帧队列(refPicList0,refPicList1)重新排序,而标记(Marking)操作负责将参考图像移入或移出解码图像缓存,即DPB。
为什么要标记?
解码器在完成一幅图像的解码后,需要对已解码图像进行存储处理,如果此图像用于其他图像的参考,则要进行DPB中已解码图像的标记操作,DPB标记完成后,如果需要的话,则把当前图像插入DPB。DPB的长度大小是有限制的,更倾向于存储对后面编码有用的图像(也就是参考图像),非参考图像将被移出DPB,而且,长短期参考图像的互转将在标记中完成。标记完后,下一帧图像在解码时,参考图像列表的初始化才能从新的DPB中取出经过更新的参考图像。
4.1 标记的实现过程
如果当前图像不作参考,则不需要进行标记操作。 标记句法参见[图 参考图像序列标记句法] 。
1、保存重建图像到DPB
如果当前图像是IDR帧,则根据读入的语法元素no_output_of_prior_pics_flag ,指明是否清空DPB,0表示DPB中所有的图像不进行输出,但是需要从DPB中移除,1表示从DPB中移除所有图像并输出到磁盘,另外,还将根据long_term_reference_flag 确定将IDR帧作为长期参考还是短期参考。参见[图 参考图像标记流程]。
非IDR帧情况,当前帧作为短期参考插入DPB,插入DPB之前需要对当前DPB进行重新标记,如果adaptive_ref_pic_marking_mode指定为0,则采用滑动窗的机制进行标记,如果adaptive_ref_pic_marking_mode指定为1,则其中有6种标记方法,即内存管理控制操作(memory management control operation) 。参见[图 参考图像的自适应内存控制标记过程]。
2、参考图像标记
参考图像标记有两种方法,自动滑窗法和自定义标记法,由相应的句法元素确定采用何种方法。
自动滑窗法:即FIFO的方法,当DPB已满时,将DPB中PicNum最小的短期参考图像(即最早的参考图像)移出DPB。
自定义标记法(adaptive_ref_pic_marking_mode):在句法元素中指定将某一参考图像设置为何种类型的参考图像或移出。参见[表x 自定义参考图象标记命令]。
表 自定义参考图象标记命令
mmco | 参数1 | 参数2 | 操作描述 |
---|---|---|---|
0 | - | - | 结束mmco |
1 | difference_of_ pic_nums_minus1 | - | 求得短期参考图像picNumX后,把picNumX标记为非参考图像 |
2 | - | long_term_pic_num | 求得长期参考图像LongTermPicNum后,把该长期参考图像标记为非参考图像 |
3 | difference_of_ pic_nums_minus1 | long_term_pic_num | 把短期参考图像picNumX,标记为长期参考图像,并赋值当前图像的LongTermPicNum = long_term_pic_num |
4 | MaxLongTerm FrameIdx | - | 把LongTermPicNum > MaxLongTermFrameIdx 的长期参考图像都从DPB移除 |
5 | - | - | 把DPB中所有的参考图像移除 |
6 | - | long_term_pic_num | 把当前参考图像标记为长期参考图像,并赋值当前图像的LongTermPicNum = long_term_pic_num |
其中
- 短期参考图像序号为picNumX = CurrPicNum − ( difference_of_pic_nums_minus1 + 1 )
- 长期参考图像序号为LongTermPicNum = long_term_pic_num(也就是说长期参考图像可以从上表3,6项指定)
DPB管理策略
我们可能要明白一点,标记与DPB管理是不一样的,但又具有一定的联系,联系的地方在于(1)。
DPB对解码图像的存储有个策略:更倾向于存储对后面编码有用的图像(也就是参考图像)。
虽然说DPB中也可以存储非参考图像,在DPB没满的时候,会无差别地把参考图像与非参考图像一并插入DPB中;
但是一旦DPB满了之后:
如果新重建的图像为参考图像,该参考图像需要插入DPB
- 如果DPB中没有非参考图像,会按照滑动窗口模式把DPB序号最小的参考图像移除; (1)
- 如果DPB中存在非参考图像,会把DPB中已经输出到磁盘的非参考图像移除; (2)
如果新重建的图像为非参考图像
- 如果DPB中不存在比当前图像POC更小的非参考图像,当前非参考图像会被直接输出到磁盘,而不插入DPB; (3)
- 如果DPB中存在比当前图像POC更小的非参考图像,会把DPB中POC最小的参考图像移除,插入当前非参考图像;(4)
4.2 标记代码解析
我们看一下JM中解码完一帧图像如果标记并插入DPB中。相关函数为store_picture_in_dpb。其中不重要的代码已经省略。从这段代码中,我们可以领略到DPB管理策略的流程,正如DPB管理策略章节所述。
File location in JM: ldecod/src/mbuffer.c
/*!
************************************************************************
* \brief
* Store a picture in DPB. This includes cheking for space in DPB and
* flushing frames.
* If we received a frame, we need to check for a new store, if we
* got a field, check if it's the second field of an already allocated
* store.
*
* \param p_Vid
* VideoParameters
* \param p
* Picture to be stored
*
************************************************************************
*/
void store_picture_in_dpb(DecodedPictureBuffer *p_Dpb, StorablePicture* p)
{
VideoParameters *p_Vid = p_Dpb->p_Vid;
unsigned i;
int poc, pos;
p_Vid->last_has_mmco_5=0;
p_Vid->last_pic_bottom_field = (p->structure == BOTTOM_FIELD);
if (p->idr_flag)
{
idr_memory_management(p_Dpb, p);//当前解码图像非IDR帧的情况处理
// picture error concealment
memset(p_Vid->pocs_in_dpb, 0, sizeof(int)*100);
}
else
{
// adaptive memory management
if (p->used_for_reference && (p->adaptive_ref_pic_buffering_flag))
adaptive_memory_management(p_Dpb, p);//自适应内存标记
}
if ((p->structure==TOP_FIELD)||(p->structure==BOTTOM_FIELD))
{
、、、、、、//场模式相关,忽略
}
// this is a frame or a field which has no stored complementary field
// sliding window, if necessary
if ((!p->idr_flag)&&(p->used_for_reference && (!p->adaptive_ref_pic_buffering_flag)))
{
sliding_window_memory_management(p_Dpb, p);
}
// first try to remove unused frames
if (p_Dpb->used_size==p_Dpb->size)
{
// picture error concealment
if (p_Vid->conceal_mode != 0)
conceal_non_ref_pics(p_Dpb, 2);
remove_unused_frame_from_dpb(p_Dpb);
if(p_Vid->conceal_mode != 0)
sliding_window_poc_management(p_Dpb, p);
}
// then output frames until one can be removed
while (p_Dpb->used_size == p_Dpb->size)
{
// non-reference frames may be output directly
if (!p->used_for_reference)
{
get_smallest_poc(p_Dpb, &poc, &pos);
}
// flush a frame
output_one_frame_from_dpb(p_Dpb);//新重建的图像为非参考图像的情况下,DPB的管理(3)(4)
}
// store at end of buffer
insert_picture_in_dpb(p_Vid, p_Dpb->fs[p_Dpb->used_size],p);//将当前解码图像插入DPB
update_ref_list(p_Dpb);
update_ltref_list(p_Dpb);
check_num_ref(p_Dpb);
dump_dpb(p_Dpb);
}
接下来,看一下自适应标记的函数adaptive_memory_management实现,代码非常清晰,正如标记的实现过程所述,不赘述。
File location in JM: ldecod/src/mbuffer.c
/*!
************************************************************************
* \brief
* Perform Adaptive memory control decoded reference picture marking process
************************************************************************
*/
static void adaptive_memory_management(DecodedPictureBuffer *p_Dpb, StorablePicture* p)
{
DecRefPicMarking_t *tmp_drpm;
VideoParameters *p_Vid = p_Dpb->p_Vid;
p_Vid->last_has_mmco_5 = 0;
while (p->dec_ref_pic_marking_buffer)
{
tmp_drpm = p->dec_ref_pic_marking_buffer;
switch (tmp_drpm->memory_management_control_operation)
{
case 0:
break;
case 1:
mm_unmark_short_term_for_reference(p_Dpb, p, tmp_drpm->difference_of_pic_nums_minus1);
update_ref_list(p_Dpb);
break;
case 2:
mm_unmark_long_term_for_reference(p_Dpb, p, tmp_drpm->long_term_pic_num);
update_ltref_list(p_Dpb);
break;
case 3:
mm_assign_long_term_frame_idx(p_Dpb, p, tmp_drpm->difference_of_pic_nums_minus1, tmp_drpm->long_term_frame_idx);
update_ref_list(p_Dpb);
update_ltref_list(p_Dpb);
break;
case 4:
mm_update_max_long_term_frame_idx (p_Dpb, tmp_drpm->max_long_term_frame_idx_plus1);
update_ltref_list(p_Dpb);
break;
case 5:
mm_unmark_all_short_term_for_reference(p_Dpb);
mm_unmark_all_long_term_for_reference(p_Dpb);
p_Vid->last_has_mmco_5 = 1;
break;
case 6:
mm_mark_current_picture_long_term(p_Dpb, p, tmp_drpm->long_term_frame_idx);
check_num_ref(p_Dpb);
break;
default:
error ("invalid memory_management_control_operation in buffer", 500);
}
p->dec_ref_pic_marking_buffer = tmp_drpm->Next;
free (tmp_drpm);
}
、、、、、、//忽略
}
五、总结
在H.264解码中,相对难点在于参考帧管理与标记两部分,管理与标记的目的都在于为解码某帧图像提供合适参考队列,同时码流可以以最小的比特开销指定参考图像。理解其设计可以从原理、目的、句法、流程以及代码等方面出发,JM的代码相对更加清晰易懂,而毕厚杰的书须结合一些文章一起对照着看,否则实际上确实很难理解。
参考文献
[1]《 新一代视频压缩编码标准》,毕厚杰
[2] https://www.cnblogs.com/TaigaCon/p/3715276.html
[3] https://blog.youkuaiyun.com/sunshine1314/article/details/575598
[5] https://blog.youkuaiyun.com/hevc_cjl/article/details/8282077#