x264代码剖析(十三):核心算法之帧间预测函数x264_mb_analyse_inter_*()
帧间预测是指利用视频时间域相关性,使用临近已编码图像像素预测当前图像的像素,以达到有效去除视频时域冗余的目的。由于视频序列通常包括较强的时域相关性,因此预测残差值接近于0,将残差信号作为后续模块的输入进行变换、量化、扫描及熵编码,可实现对视频信号的高效压缩。
本文将重点讨论基本档次支持的P片帧间预测工具以及主要和扩展档次支持的B片和加权预测等帧间预测工具,最后分析了帧间预测函数的主要功能。
1、可变尺寸块运动补偿
在H.264中,每个16*16大小的宏块可以有4种分割方式:一个16*16,两个16*8,两个8*16,四个8*8,其运动补偿也有相应的四种,如下图所示。
而8*8模式的每个子宏块还可以进一步用4种方式再进行分割:一个8*8,两个8*4,两个4*8,四个4*4,其运动补偿也有相应的四种,如下图所示。
这些分割和宏块大大提高了各宏块之间的关联性,这种分割下的运动补偿称为树状结构运动补偿。每个分割或子宏块都有一个独立的运动补偿,每个运动矢量MV必须被编码和传输,分割的选择也需编码到压缩比特流中。对于大的分割尺寸而言,MV选择和分割类型只需要少量的比特来表征,但是运动补偿残差在多细节区域能量将非常高;对于小的尺寸而言,分割运动补偿残差能量低,但是需要较多的比特来表征MV和分割选择,分割尺寸的选择影响了压缩性能。整体而言,大的分割尺寸适合于平坦区域,而小的尺寸适合于多细节区域。
2、运动矢量
帧间编码宏块的每个分割或者子宏块都是从参考图像的某一相同尺寸区域预测而得。两者之间的差异(MV),对于亮度成分采用1/4像素精度,而对于色度成分则采用1/8像素精度。亚像素位置的亮度和色度像素并不存在于参考图像中,需利用临近已编码点进行内插而得。如果MV的垂直和水平分量为整数,则参考块相应像素实际存在,如果其中一个或两个为分数,则预测像素要通过参考帧中相应像素内插获得。
3、MV预测
每个分割MV的编码需要相当的比特数,特别是使用小的分割尺寸时。为减少传输比特数,利用临近分割的MV较强的相关性,MV可由临近已编码分割的MV预测而得。预测矢量MV(p)基于已计算MV和MVD(预测与当前的差异),并被编码和传送。MV(p)取决于运动补偿尺寸和临近MV的有无。
举例说明:如下图,E为当前宏块或宏块分割子宏块,A/B/C分别为E的左、上、右上方的三个相对应块。如果E的左边不止一个分割,取其最上的一个为A;上方不止一个分割时,取其最左边一个为B,如右下图所示,左下图显示了所有分割有相同尺寸时的临近分割选择,右下图给出了不同尺寸时的临近分割的选择。
其中:
(1)、传输分割不包括16*8和8*16时,MV(p)为A/B/C分割MV的中值;
(2)、16*8分割,上面部分MV(p)由B预测,下面部分由A预测;
(3)、8*16分割,左面部分MV(p)由A预测,右面部分由C预测;
(4)、Skipped宏块,MV(p)为A/B/C分割MV的中值。
当已传送块不存在时,MV(p)选择需重新调整。在解码端,MV(p)以相同的方式形成并加到MVD上,对于跳跃宏块来说,不存在MVD,运动补偿宏块也由MV直接生成。
4、B片预测
B片中的帧间编码宏块的每个子块都是由一个或两个参考图像预测而得,该参考图像在当前图像的前面或后面。参考图像存储于编解码器中,其选择有多种方式。下图显示了三种方式:一个前向一个后向、两个前向、两个后向。
4.1、参考图像
B片用到了两个已编码图像列表:list0和list1,包括短期和长期两种。这两个列表都可包含前向和后向的已编码图像,图像的显示顺序排列采用图像序号POC表示。其中:
List0:最近前向图像标为index0,接着是其余前向图像(POC递减顺序)以及后向图像(POC递增顺序)。
List1:最近后向图像标为index0,接着是其余后向图像(POC递增顺序)以及前向图像(POC递减顺序)。
举例说明:一个H.264解码器存储了6幅短期参考图像。其POC分别为:123,125,126,128,129和130。当前图像为127。所有6幅短期参考图像在list0和list1中都标为“用作参考”,如下表所示。
4.2、预测模式选择
B片的预测方式包括:宏块分割方式、双向选择方式、参考列表选择方式等等。具体说,
B片中宏块分割可由多种预测方式中的一种实现,如直接模式、利用list0的运动补偿模式、利用list1的运动补偿模式或者利用list0和list1的双向运动补偿模式。每个分割可选择各自的不同的预测模式如下表。如果8×8分割被使用,每个8×8分割所选择的模式适用于分割中的所有亚分割。
下图给出一个例子,左边的两个16×8分割分别使用List0和双向预测模式,而右边的 4 个8×8分割分别采用直接、list0、list1和双向预测四种模式。
4.3、双向预测
所谓双向预测就是参考块是由list0和list1的参考图像得出的,从list0和list1分别得出两个运动补偿参考区域(需要两个MV),而预测块的像素取list0和list1相应像素的平均值。
4.4、直接预测
直接预测模式编码的B片宏块或宏块分割不传送MV。而是由解码器计算list0和 list1 中已编码MV,得出解码残差像素的双向预测运动补偿。B片中的skipped宏块便由解码器用直接模式重建而得。
直接预测模式具体可分为空域和时域两种模式,片头会指明将用时间还是空间方式计算直接模式或其分割的矢量。
在空间模式中, list0 和 list1 预测矢量计算如下:如果第一幅 list1 参考图像的 co-located MB 或分割有一个 MV 幅度上小于±1/2 亮度像素,其一个或两个预测矢量置为 0;否则预测 list0 和 list1 矢量用以计算双向运动补偿。
在时间模式中,计算步骤如下:
1)、找出 list1 图像 co-located MB 或分割相应的 list0 参考图像。该 list0 参考作为当前 MB 或分割的 list0 参考;
2)、找出 list1 图像 co-located MB 或分割相应的 list0MV;
3)、计算当前图像和 list1 图像的 POC 的 MV,作为新的 list1 MV1;
4)、计算当前图像和 list0 图像的 POC 的 MV,作为新的 list0 MV0。
这些模式在预测参考宏块或分割不提供或帧内编码等情况下需作出调整。举例:当前宏块 list1 参考在当前帧两幅图像后出现,如下图所示。List1参考 co-located MB有一MV(+2.5,+5),指向list0参考图像(出现于当前图像3幅图像前)。解码器分别计算指向
list1和 list0 的MV1(-1,-2)和MV0(+1.5,+3)。
5、加权预测
加权预测是一种用来修正 P 或
B 片中运动补偿预测像素方法。 H.264 中由 3 中加权预测类型:
1)、P 片宏块“explicit”加权预测;
2)、B 片宏块“explicit”加权预测;
3)、B 片宏块“implicit”加权预测;
每个预测像素 pred0(i,j)和 pred1(i,j)在运动补偿之前通过加权系数 ω0 和 ω1 修正。在“explicit”类型中,加权系数由编码器决定并在片头中传输。在“implicit”类型中,系数 ω0 和 ω1 由相应 list0 和 list1 参考图像的时间位置推出。大的系数用于时间上接近当前图像的情况,小的则用于时间上远离当前图像的情况。
可见, H.264 采用树状结构的运动补偿技术,提高了预测能力。特别是,小块预测提高了模型处理更好的运动能够描述的能力,产生更好的图像质量。 H.264 运动向量的精度提高到 1/4 像素(亮度),运动补偿算法的预测能力得到进一步提高。 H.264 还提供多参考帧可选模式,这将产生更好的视频质量和效率更高的视频编码。相对于 1 帧参考, 5 个参考帧可以节约 5%~10%的比特率,且有助于比特流的恢复。当然,并不是说参考帧越多越好,经实验,考虑到缓冲区的能力和编码器的效率,目前一般都选取3~5个参考帧。
6、帧间预测函数x264_mb_analyse_inter_*()
帧间预测的帧类型大多是P帧或B帧,也有可能有I帧,对于P或B帧,则进行如下的流程:
a)、调用x264_macroblock_probe_pskip()分析是否为Skip宏块,如果是的话则不再进行下面分析。
b)、调用x264_mb_analyse_inter_p16x16()分析P16x16帧间预测的代价。
c)、调用x264_mb_analyse_inter_p8x8()分析P8x8帧间预测的代价。
d)、如果P8x8代价值小于P16x16,则依次对4个8x8的子宏块分割进行判断:
i、调用x264_mb_analyse_inter_p4x4()分析P4x4帧间预测的代价。
ii、如果P4x4代价值小于P8x8,则调用
x264_mb_analyse_inter_p8x4()和x264_mb_analyse_inter_p4x8()分析P8x4和P4x8帧间预测的代价。
e)、如果P8x8代价值小于P16x16,调用x264_mb_analyse_inter_p16x8()和x264_mb_analyse_inter_p8x16()分析P16x8和P8x16帧间预测的代价。
f)、此外还要调用x264_mb_analyse_intra(),检查当前宏块作为Intra宏块编码的代价是否小于作为P宏块编码的代价(P
Slice中也允许有Intra宏块)。
以x264_mb_analyse_inter_p16x16()函数为例,其函数关系图如下所示:
下面同样以x264_mb_analyse_inter_p16x16()为例分析P16x16宏块的帧间预测方式。该函数的定义位于encoder\analyse.c,代码如下:
/******************************************************************/ /******************************************************************/ /* ======Analysed by RuiDong Fang ======Csdn Blog:http://blog.csdn.net/frd2009041510 ======Date:2016.03.17 */ /******************************************************************/ /******************************************************************/ /************====== x264_mb_analyse_inter_p16x16()函数 ======************/ /* 功能:帧间预测:16*16大小的P Slice */ static void x264_mb_analyse_inter_p16x16( x264_t *h, x264_mb_analysis_t *a ) { //运动估计相关的信息 //后面的初始化工作主要是对该结构体赋值 x264_me_t m; int i_mvc; ALIGNED_4( int16_t mvc[8][2] ); int i_halfpel_thresh = INT_MAX; int *p_halfpel_thresh = (a->b_early_terminate && h->mb.pic.i_fref[0]>1) ? &i_halfpel_thresh : NULL; /* 16x16 Search on all ref frame */ m.i_pixel = PIXEL_16x16;//设定像素分块大小 LOAD_FENC( &m, h->mb.pic.p_fenc, 0, 0 ); a->l0.me16x16.cost = INT_MAX; //循环搜索所有的参考帧 //i_ref //mb.pic.i_fref[0]存储了参考帧的个数 for( int i_ref = 0; i_ref < h->mb.pic.i_fref[0]; i_ref++ ) { m.i_ref_cost = REF_COST( 0, i_ref ); i_halfpel_thresh -= m.i_ref_cost; /* search with ref */ //加载半像素点的列表 //参考列表的4个分量列表,包括yN(整点像素),yH(1/2水平内插),yV(1/2垂直内插), yHV(1/2斜对角内插) LOAD_HPELS( &m, h->mb.pic.p_fref[0][i_ref], 0, i_ref, 0, 0 ); LOAD_WPELS( &m, h->mb.pic.p_fref_w[i_ref], 0, i_ref, 0, 0 ); x264_mb_predict_mv_16x16( h, 0, i_ref, m.mvp ); //////////////////获得预测的运动矢量MV(通过取中值) if( h->mb.ref_blind_dupe == i_ref ) { CP32( m.mv, a->l0.mvc[0][0] ); x264_me_refine_qpel_refdupe( h, &m, p_halfpel_thresh ); } else { x264_mb_predict_mv_ref16x16( h, 0, i_ref, mvc, &i_mvc ); x264_me_search_ref( h, &m, mvc, i_mvc, p_halfpel_thresh ); ////////////////关键:运动估计(搜索参考帧) } /* save mv for predicting neighbors */ CP32( h->mb.mvr[0][i_ref][h->mb.i_mb_xy], m.mv ); CP32( a->l0.mvc[i_ref][0], m.mv ); /* early termination * SSD threshold would probably be better than SATD */ if( i_ref == 0 && a->b_try_skip && m.cost-m.cost_mv < 300*a->i_lambda && abs(m.mv[0]-h->mb.cache.pskip_mv[0]) + abs(m.mv[1]-h->mb.cache.pskip_mv[1]) <= 1 && x264_macroblock_probe_pskip( h ) ) { h->mb.i_type = P_SKIP; x264_analyse_update_cache( h, a ); assert( h->mb.cache.pskip_mv[1] <= h->mb.mv_max_spel[1] || h->i_thread_frames == 1 ); return; } m.cost += m.i_ref_cost; i_halfpel_thresh += m.i_ref_cost; if( m.cost < a->l0.me16x16.cost ) h->mc.memcpy_aligned( &a->l0.me16x16, &m, sizeof(x264_me_t) ); } x264_macroblock_cache_ref( h, 0, 0, 4, 4, 0, a->l0.me16x16.i_ref ); assert( a->l0.me16x16.mv[1] <= h->mb.mv_max_spel[1] || h->i_thread_frames == 1 ); h->mb.i_type = P_L0; if( a->i_mbrd ) { x264_mb_init_fenc_cache( h, a->i_mbrd >= 2 || h->param.analyse.inter & X264_ANALYSE_PSUB8x8 ); if( a->l0.me16x16.i_ref == 0 && M32( a->l0.me16x16.mv ) == M32( h->mb.cache.pskip_mv ) && !a->b_force_intra ) { h->mb.i_partition = D_16x16; x264_macroblock_cache_mv_ptr( h, 0, 0, 4, 4, 0, a->l0.me16x16.mv ); a->l0.i_rd16x16 = x264_rd_cost_mb( h, a->i_lambda2 ); if( !(h->mb.i_cbp_luma|h->mb.i_cbp_chroma) ) h->mb.i_type = P_SKIP; } } }
从代码中可以看出,函数x264_mb_analyse_inter_p16x16()首先初始化了x264_me_t结构体相关的信息,接着x264_mb_predict_mv_16x16()函数获得预测的运动矢量MV,然后调用x264_me_search_ref()进行运动估计,最后统计运动估计的开销。其中x264_me_search_ref()完成了运动搜索的流程,相对比较复杂,是帧间预测的重点。
总而言之,在H.264中,对于P帧,每个16*16大小的宏块可以有4种分割方式:一个16*16(对应于x264_mb_analyse_inter_p16x16函数),两个16*8(对应于x264_mb_analyse_inter_p16x8函数),两个8*16(对应于x264_mb_analyse_inter_p8x16函数),四个8*8(对应于x264_mb_analyse_inter_p8x8函数);而8*8模式的每个子宏块还可以进一步用4种方式再进行分割:一个8*8(对应于x264_mb_analyse_inter_p8x8函数),两个8*4(对应于x264_mb_analyse_inter_p8x4函数),两个4*8(对应于x264_mb_analyse_inter_p4x8函数),四个4*4(对应于x264_mb_analyse_inter_p4x4函数)。对于B帧,每个16*16大小的宏块也可以有4种分割方式:一个16*16(对应于x264_mb_analyse_inter_b16x16函数),两个16*8(对应于x264_mb_analyse_inter_b16x8函数),两个8*16(对应于x264_mb_analyse_inter_b8x16函数),四个8*8(对应于x264_mb_analyse_inter_b8x8函数)。