Fork me on GitHub

H.264/AVC编解码技术及JM源码分析(五)——帧间预测

相比帧内预测,帧间预测要更加复杂,因为其本质上是提高更高压缩效率的技术,其中涉及到诸如亚像素插值、运动搜索、多参考帧管理、加权预测等等的知识点。这些知识内容繁多,加之笔者也没有深入的了解,因此本文的内容只会讲解少量知识点,而更多注重于结合代码去了解整个帧间预测的流程,同时建议读者从其他书籍获取更加详尽的讲解。

运动估计、运动补偿与运动矢量

首先要讲的是帧间预测中三个最基本的概念,其中运动估计和运动补偿常常令人混淆不清:

  • 运动估计(Motion Estimation,ME):对当前编码块,在已编码的参考帧中利用运动搜索技术寻找最佳的匹配块,并计算出两个块的偏移量的过程。
  • 运动矢量(Motion Vector,MV):上面计算出的这个偏移量就叫运动矢量,通常使用参考帧的块坐标减去当前块的坐标,x和y分量分开保存。可知每一个分块会对应一个MVx和一个MVy。
  • 运动补偿(Motion Compensation,MC):利用运动矢量和某种帧间预测的方法,从而估计出当前块的像素值的过程。

由定义可以看出一般这三者的先后顺序会是:运动估计-运动矢量-运动补偿。

树状分块

树状分块其实就是我们熟知的宏块分割方式,一般可以分为16x16、两个16x8、两个8x16、四个8x8,当出现8x8分块时还可以进一步做子宏块划分,即8x8、两个8x4、两个4x8和四个4x4。需要注意的是只有帧间宏块才具备以上的全部分块方式,而帧内宏块只有16x16、8x8和4x4。

多参考帧

H.264运用了多参考帧技术,多个参考帧会维护在一个列表中。双向预测时有两个参考帧列表LIST_0和LIST_1,分别存放前向预测后后向预测所用的参考帧,如果是单向预测则只会用到其中一个。保存参考帧的是称为解码图像缓冲区(Decoded Picture Buffer,DPB)的结构,包含了短期参考帧、长期参考帧和非参考帧,默认情况下参考帧列表的最大长度为16。

MV预测

MV较多的情况下也可能占据较多的数据量,因此也必须对MV进行预测编码,最终编码的是实际MV与预测MV(MVp)的差值MVD,即MVD = MV-MVp。如图所示,蓝色的为当前块,可以是任意一种分块大小,首先获取它相邻的四个4x4块,然后再获取每个4x4块所属的分块(也可能是任意分块大小,而且有可能和当前块属于同一宏块),预测规则如下:

  1. 当前块是16x8时,若是在上部的16x8,则MVp为B的MV;若是在下部的16x8,则MVp为A的MV。前提是B/C可用,否则转入中值预测。
  2. 当前块是8x16时,若是在左部的8x16,则MVp为A的MV;若是在右部的8x16,则MVp为C的MV。前提是A/C可用,否则转入中值预测。
  3. 如果不是以上的分块,则MVp为A、B、C三者MV的中值

对于中值预测的情况,还有以下规定:

  1. A、B、C均不可用时,不做预测,直接编码MV。
  2. A、B、C只有一个可用时,MVp为可用块的MV。
  3. A、B、C有两个可用时,不可用块的MV设为0,仍然取中值。

可用情况判断:

  1. A、B、C参考帧和当前块参考帧不同的时候不可用。
  2. A、B、C是帧内编码的块时不可用。
  3. C不可用时,若D可用则用D代替C(即D是备用的)。

MV预测

Skip模式

Skip模式是对宏块的一种特殊的帧间编码模式,在P和B帧中使用这种模式的宏块分别叫P_Skip宏块和B_Skip宏块。Skip模式实质上就是除了标记自身为Skip宏块以外,不用编码任何的信息,可以极大的节省比特数。其原理是解码时直接使用预测的MVp作为其MV,用该MV进行运动补偿得到预测像素值,将预测像素值直接作为真正的像素值。该模式通常出现在连续的平坦区域或者连续的缓慢运动中。

P_Skip宏块的条件:

  1. 最佳模式为P_L0_16x16。
  2. 参考帧为LIST_0中的第一个。
  3. MVD为0.
  4. 系数全为0。

B_Skip宏块的条件:

  1. 最佳模式为B_Direct_16x16。
  2. 系数全为0。

B帧的预测模式

不同于P帧只有前向预测模式(L0),B帧根据利用的参考帧列表不同一共有四种预测模式:利用LIST_0的前向预测(L0);利用LIST_1的后向预测(L1)、利用LIST_0和LIST_1的双向预测(Bipred)和直接预测(Direct)。每个分块都可以独立从中选择一种预测模式,但有两种特殊情况:

  1. 对于16x8或8x16的分块来说不能使用直接预测模式。
  2. 8x8分块的预测模式应用到其所有的子块中,即8x4/4x8/4x4均与其相应的8x8块的预测模式相同。

Direct模式也是一种特殊的模式,其MVD和参考帧索引可以通过参考帧列表中的已编码帧计算得出,因为不需要编码这两项。有B_Direct_16x16和B_Direct_8x8两种,值得注意的是,标准中把B_Skip和Direct模式的宏块/块用同一个值标记,后面再通过是否有非零系数来区分两者。

相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//如果不是I帧
if (!intra)
{
//===== set skip/direct motion vectors =====
//如果SKIP或者Direct模式可用,则设置它们的运动矢量
if (enc_mb.valid[0])
{
if (bslice)
currSlice->Get_Direct_Motion_Vectors (currMB);
else
FindSkipModeMotionVector (currMB);
}
if (p_Inp->CtxAdptLagrangeMult == 1)
{
get_initial_mb16x16_cost(currMB);
}

//===== MOTION ESTIMATION FOR 16x16, 16x8, 8x16 BLOCKS =====
//P16x16、P8x16、P16x8,注意P帧和B帧共用名字
for (mode = 1; mode < 4; mode++)
{
//假设当前模式是最佳模式
best.mode = (char) mode;
best.bipred = 0;
b8x8info->best[mode][0].bipred = 0;

//提前已经确定好了哪些模式可用,这里只需要判断一下就行
if (enc_mb.valid[mode])
{
//16x16只有1个块,16x8和8x16则有两个块(对应了运动矢量的数量)
for (cost=0, block=0; block<(mode==1?1:2); block++)
{
update_lambda_costs(currMB, &enc_mb, lambda_mf);
//运动搜索
PartitionMotionSearch (currMB, mode, block, lambda_mf);

//--- set 4x4 block indices (for getting MV) ---
j = (block==1 && mode==2 ? 2 : 0);
i = (block==1 && mode==3 ? 2 : 0);

//--- get cost and reference frame for List 0 prediction 从List 0中计算出最合适的参考帧---
bmcost[LIST_0] = DISTBLK_MAX;
list_prediction_cost(currMB, LIST_0, block, mode, &enc_mb, bmcost, best.ref);

//如果是B帧
if (bslice)
{
//--- get cost and reference frame for List 1 prediction 从List 1中计算出最合适的参考帧---
bmcost[LIST_1] = DISTBLK_MAX;
list_prediction_cost(currMB, LIST_1, block, mode, &enc_mb, bmcost, best.ref);

// Compute bipredictive cost between best list 0 and best list 1 references 计算双向参考的cost
list_prediction_cost(currMB, BI_PRED, block, mode, &enc_mb, bmcost, best.ref);

// currently Bi predictive ME is only supported for modes 1, 2, 3 and ref 0
if (is_bipred_enabled(p_Vid, mode))
{
//还要算一次双向参考的cost?
get_bipred_cost(currMB, mode, block, i, j, &best, &enc_mb, bmcost);
}
else
{
bmcost[BI_PRED_L0] = DISTBLK_MAX;
bmcost[BI_PRED_L1] = DISTBLK_MAX;
}

// Determine prediction list based on mode cost 比较刚刚算出的各个cost,得到最佳的参考方式
determine_prediction_list(bmcost, &best, &cost);
}
else // if (bslice)
{
best.pdir = 0;
cost += bmcost[LIST_0]; //非B帧直接就是List 0
}

//根据刚才的选择,给各种参数赋值
assign_enc_picture_params(currMB, mode, &best, 2 * block);

//----- set reference frame and direction parameters -----
//由于这三种分块方式都大于8x8,所以其包含的4个或2个8x8块会用同一种参数,于是拷贝一下
set_block8x8_info(b8x8info, mode, block, &best);

//--- set reference frames and motion vectors ---
if (mode>1 && block == 0)
currSlice->set_ref_and_motion_vectors (currMB, motion, &best, block);
} // for (block=0; block<(mode==1?1:2); block++)

//如果cost更低,则替换参数
if (cost < min_cost)
{
md_best.mode = (byte) mode;
md_best.cost = cost;
currMB->best_mode = (short) mode;
min_cost = cost;
if (p_Inp->CtxAdptLagrangeMult == 1)
{
adjust_mb16x16_cost(currMB, cost);
}
}
} // if (enc_mb.valid[mode])
} // for (mode=1; mode<4; mode++)

//P8x8
if (enc_mb.valid[P8x8])
{
currMB->valid_8x8 = FALSE;

//如果是8x8变换,默认不开启
if (p_Inp->Transform8x8Mode)
{
//...
}// if (p_Inp->Transform8x8Mode)

currMB->valid_4x4 = FALSE;
if (p_Inp->Transform8x8Mode != 2)
{
currMB->luma_transform_size_8x8_flag = FALSE; //switch to 8x8 transform size
ResetRD8x8Data(p_Vid, p_RDO->tr4x4);
//=================================================================
// Check 8x8, 8x4, 4x8 and 4x4 partitions with transform size 4x4
//=================================================================
//===== LOOP OVER 8x8 SUB-PARTITIONS (Motion Estimation & Mode Decision) =====
//对每一个8x8块进行子宏块模式决策
for (block = 0; block < 4; block++)
{
currSlice->submacroblock_mode_decision(currMB, &enc_mb, p_RDO->tr4x4, p_RDO->coefAC8x8[block], block, &cost);
if(!currMB->valid_4x4)
break;
set_subblock8x8_info(b8x8info, P8x8, block, p_RDO->tr4x4);
}
}// if (p_Inp->Transform8x8Mode != 2)

if (p_Inp->RCEnable)
rc_store_diff(currSlice->diffy, &p_Vid->pCurImg[currMB->opix_y], currMB->pix_x, mb_pred);

p_Vid->giRDOpt_B8OnlyFlag = FALSE;
}
}
else // if (!intra)
{
min_cost = DISTBLK_MAX;//I帧时直接初始化min_cost
}

这段代码在md_high.c的encode_one_macroblock_high(),这个函数是模式决策的核心,然后再讨论。在这里主要判断帧类型,若是P/B帧,则分别对各种可用的帧间预测模式进行进行运动估计并完成决策。