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函数)。