☰
HEVC AMVP预测机制详解:xEstimateMvPredAMVP与fillMVPCand实战
2026/10/15 12:30:13 网站建设 项目流程

1. 从码流到运动矢量:AMVP机制到底在解决什么问题

做HEVC编码器开发的人,早晚都会撞上xEstimateMvPredAMVP这个函数。它藏在TEncSearch::xEstimateMvPredAMVP里,是帧间预测中运动矢量预测(MVP)的核心入口。很多人第一次读这段代码时的感受是:明明只是"猜一个MV",为什么要写这么多分支、这么多候选列表操作?

答案要从HEVC的设计目标说起。相比上一代标准,HEVC在码率上要再省一半,而运动信息的编码开销在低码率场景下占比极高。一个4x4或8x8的PU,如果每个都独立编码MV的x、y分量,光是运动信息就能吃掉大量比特。AMVP(Advanced Motion Vector Prediction)的思路是:利用空域和时域上相邻块的运动相关性,先构造一个候选MV列表,编码器从中选一个最接近真实运动矢量的作为预测值,然后只编码"选了第几个候选"(即MVP索引)加上预测残差(MVD)。这样MV的编码代价从"两个分量绝对值"降为"一个索引加两个小残差"。

xEstimateMvPredAMVP的职责就是:给定当前PU的位置、参考图像、搜索范围,构造候选列表,选出最优预测MV,并返回对应的MVP索引和预测代价。它不负责最终的率失真决策,那是xCheckRDCostInter的事,但它提供的预测质量直接决定了后续搜索的起点和编码效率。

理解这个函数,需要先理解三个前置概念:空域候选来自当前帧相邻PU,时域候选来自参考帧同位置PU,候选列表的裁剪与填充由fillMVPCand完成。这三者构成了AMVP预测的完整链路。下面我会从调用时机、候选构造、列表填充、实际调试经验四个层面,把这个过程拆开讲透。

1.1 为什么AMVP不是简单的"取左边和上边"

很多初学者会直觉认为:预测MV嘛,取左边块和上边块的MV平均一下不就行了。但HEVC的AMVP远比这复杂,原因有三。

第一,相邻块的可用性不是确定的。当前PU在图像边界、Slice边界、Tile边界时,左侧或上侧块可能不存在,或者属于不同的Slice不能跨边界预测。代码里大量isAvailable判断就是在处理这些情况。

第二,相邻块的运动信息可能不可靠。如果左边块是帧内预测块,它根本没有MV;如果左边块的参考帧和当前PU的参考帧不一致,它的MV也不能直接拿来用,需要做时域缩放(MV Scaling)。

第三,候选之间可能重复。左边块和上边块的MV可能完全相同,如果不去重,列表里全是冗余候选,编码索引就浪费了。fillMVPCand里的去重逻辑就是干这个的。

所以AMVP的本质是一个带可用性检查、参考帧一致性校验、时域缩放、去重和填充的候选列表构造过程。xEstimateMvPredAMVP是这个过程的外层调度者,fillMVPCand是具体的列表填充执行者。

1.2 函数在编码流程中的位置

在HM或VTM的编码主循环里,xEstimateMvPredAMVP通常在xCheckRDCostInter之前被调用,或者作为predInterSearch的一部分。它的输出包括:

  • pcMvPred:选中的预测MV
  • ruiMVPIdx:MVP索引(0或1,因为AMVP列表最多2个候选)
  • riMVPBits:编码该索引所需的比特数

这些结果会传给后续的运动搜索模块,作为搜索中心点。如果预测MV已经很接近真实MV,搜索范围可以缩小,搜索点数减少,编码速度提升。这也是为什么AMVP预测质量对编码器整体性能影响巨大。

注意:AMVP列表长度固定为2,这是标准规定的。即使构造出更多候选,最终也只保留前两个。这一点和Merge模式不同,Merge列表可以到5个。

2. xEstimateMvPredAMVP的入参与内部调度逻辑

打开TEncSearch::xEstimateMvPredAMVP的函数签名,参数列表通常长这样(以HM为例):

Void TEncSearch::xEstimateMvPredAMVP( TComDataCU* pcCU, TComYuv* pcYuvOrg, UInt uiPartIdx, UInt uiPartAddr, RefPicList eRefPicList, Int iRefIdx, TComMv& rcMvPred, UInt& ruiMVPIdx, UInt& ruiMVPBits, Bool bFilled, Distortion* puiDistBiP)

参数虽多,但核心逻辑可以归纳为四步:准备候选列表、填充候选、评估候选代价、返回最优。下面逐层拆解。

2.1 候选列表的两种来源:空域与时域

AMVP的候选列表由空域候选和时域候选组成,标准规定最多5个候选参与构造,但最终只取前2个。空域候选来自当前PU的左侧块(A0、A1)和上侧块(B0、B1、B2),时域候选来自参考帧同位置块(C0、C1)。

在xEstimateMvPredAMVP内部,通常会先调用fillMVPCand来填充空域候选,再根据配置决定是否加入时域候选。空域候选的扫描顺序是有讲究的:左侧优先A0(左下)再A1(左),上侧优先B0(右上)再B1(上)再B2(左上)。这个顺序不是随意的,而是因为距离当前PU越近的块,运动相关性通常越强。

代码里会维护一个TComMv cMvPred[2]数组和一个Bool bAvailable[2]标志数组,分别存储候选MV和可用性。每加入一个候选,都要检查它是否与已有候选重复。去重时通常比较MV的x、y分量是否完全相等,有些实现还会考虑参考帧索引是否一致。

2.2 参考帧一致性校验与时域缩放

这是AMVP里最容易出错的地方。假设当前PU参考的是RefList0的第0帧,而左边块参考的是RefList0的第2帧。两者的时间距离不同,左边块的MV不能直接作为预测值,必须按时间距离比例缩放。

缩放公式大致是:

mvScaled = mvLeft * (tdCurrent / tdLeft)

其中tdCurrent是当前帧与当前参考帧的时间距离,tdLeft是左边块所在帧与其参考帧的时间距离。代码里通常用TComMv::scaleMv或类似函数完成,内部会做整数运算和clip,避免溢出。

如果缩放后的MV超出搜索范围,或者时间距离为0导致除零,这个候选就要被丢弃。实际调试时,我遇到过因为tdLeft为0导致候选被错误丢弃的情况,后来发现是参考帧配置有问题,两个参考帧指向了同一帧。

2.3 候选不足时的填充策略

标准规定AMVP列表必须填满2个候选。如果空域和时域候选加起来不足2个,就需要填充。填充策略通常是:

  • 如果列表为空,加入零MV(0,0)
  • 如果列表只有1个候选,再加入零MV
  • 如果零MV与已有候选重复,则加入(0,1)或(1,0)等非零小MV

fillMVPCand函数的名字就来源于此——它负责把候选列表"填满"。这个函数在HM中通常接收候选数组、可用性数组、参考帧索引等参数,内部循环检查并填充。

提示:填充用的零MV不是随便选的。零MV代表"静止"假设,在大多数自然视频中,静止或准静止块占比很高,零MV作为兜底候选有统计上的合理性。

3. fillMVPCand的候选构造细节与去重逻辑

fillMVPCand是AMVP预测的实际执行者。它的输入通常包括当前CU、PartIdx、参考帧列表、参考帧索引,输出是填充好的候选MV数组。理解这个函数,关键要抓住三个动作:扫描相邻块、校验候选有效性、去重后写入列表。

3.1 空域候选的扫描顺序与可用性判断

空域候选的扫描不是简单的"左、上"两个方向,而是细分为多个位置。以左侧为例,A0是左下角块,A1是左侧相邻块;上侧B0是右上角块,B1是上侧相邻块,B2是左上角块。扫描顺序通常是A0→A1→B0→B1→B2,但不同实现可能微调。

每个位置都要判断:

  • 该位置是否在当前图像内(边界检查)
  • 该位置是否与当前PU属于同一Slice/Tile(跨边界检查)
  • 该位置是否是帧间预测块(帧内块无MV)
  • 该位置的参考帧索引是否有效

这些判断在代码里通常是一连串if语句,看起来冗长但缺一不可。我见过有人为了"优化"把某些检查去掉,结果在边界PU上出现越界访问或错误预测,编码质量反而下降。

3.2 时域候选的获取与缩放

时域候选来自参考帧中与当前PU同位置的块。获取时域候选需要先定位同位置块,然后读取它的MV和参考帧索引。如果同位置块是帧内块,或者它的参考帧与当前PU的参考帧不一致,就需要缩放或丢弃。

时域缩放的精度直接影响预测质量。HEVC标准规定了缩放的计算方式,但具体实现中要注意整数运算的舍入方向。有些实现用右移,有些用带舍入的除法,结果会有细微差异。实测中这种差异对码率的影响通常在0.1%以内,但在低码率下可能被放大。

3.3 去重:为什么重复候选必须剔除

去重是fillMVPCand里最容易被忽视但最重要的步骤。假设左边块和上边块的MV完全相同,如果不去重,列表里就是两个一样的候选。编码器选索引0还是索引1,残差都一样,但索引1需要多一个比特。在大量PU上累积,这就是可观的浪费。

去重的比较通常是精确比较MV的x、y分量。有些实现还会比较参考帧索引,因为即使MV相同,参考帧不同也意味着不同的预测块。标准对此没有强制规定,但主流实现都会做参考帧索引检查。

去重后的列表如果不足2个,就进入填充流程。填充时也要去重,确保最终列表里没有重复候选。

// 伪代码示意:候选去重与填充 for (int i = 0; i < numCand; i++) { bool isDuplicate = false; for (int j = 0; j < numAdded; j++) { if (cand[i] == list[j]) { isDuplicate = true; break; } } if (!isDuplicate) { list[numAdded++] = cand[i]; } } // 填充至2个 while (numAdded < 2) { TComMv fillMv(0, 0); if (numAdded == 1 && list[0] == fillMv) { fillMv = TComMv(0, 1); } list[numAdded++] = fillMv; }

这段伪代码展示了去重和填充的基本逻辑。实际代码会更复杂,因为要处理参考帧索引、可用性标志等。

4. 预测代价评估与MVP索引选择

候选列表构造完成后,xEstimateMvPredAMVP需要评估每个候选的代价,选出最优的MVP索引。代价评估通常包括两部分:预测残差的失真和编码MVP索引的比特数。

4.1 失真计算:SAD还是SATD

在AMVP阶段,为了速度,通常用SAD(绝对差之和)而不是SATD(哈达玛变换后的绝对差之和)。SAD计算简单,适合快速筛选。具体做法是:用候选MV从参考帧取预测块,与当前原始块逐像素求绝对差,累加得到SAD。

有些实现会同时计算亮度分量的SAD和色度分量的SAD,加权后作为总失真。权重通常亮度占主导,因为人眼对亮度更敏感。

4.2 比特代价:MVP索引的编码开销

MVP索引只有0和1两种可能,编码开销很小但不可忽略。在HM中,索引0通常用1个比特,索引1用2个比特(类似一元码)。所以选择索引1时,代价要加上额外的比特惩罚。

代价函数大致是:

cost = distortion + lambda * bits

其中lambda是拉格朗日乘子,与QP相关。QP越大,lambda越大,比特惩罚越重,编码器越倾向于选索引0。

4.3 实测中的选择倾向与异常情况

在实际编码中,索引0被选中的概率远高于索引1。这是因为空域候选的第一个(通常是A0或A1)往往就是最优预测。如果发现索引1被频繁选中,可能意味着候选构造顺序有问题,或者去重逻辑有bug。

我遇到过一种异常:在某些运动剧烈的序列上,索引1选中率异常高。排查后发现是时域候选的缩放方向搞反了,导致时域候选反而比空域候选更接近真实MV。修正缩放方向后,索引分布恢复正常。

注意:AMVP预测只是提供搜索起点,最终MV由运动搜索决定。所以即使AMVP预测不完美,后续搜索仍可能找到好的MV。但好的预测能缩小搜索范围,提升编码速度。

5. 调试AMVP预测的实战经验与常见坑

理论讲完了,下面分享一些实际调试AMVP预测时踩过的坑和总结的技巧。这些内容在标准文档里找不到,但在一线开发中非常实用。

5.1 候选列表为空的崩溃问题

最常见的问题是候选列表为空时没有正确填充,导致后续访问越界。标准要求列表必须填满2个,但有些实现在边界条件下会漏掉填充步骤。比如当前PU在图像最左上角,左侧和上侧都不可用,时域候选也无效,这时如果填充逻辑没覆盖,就会崩溃。

解决方法是在fillMVPCand末尾强制检查列表长度,不足2个就补零MV。这个检查要放在所有候选构造逻辑之后,确保无论前面发生什么,列表都是满的。

5.2 时域缩放导致的精度损失

时域缩放涉及整数除法,不同实现的结果可能不同。有些实现为了速度用位移代替除法,但位移的舍入方向如果和标准不一致,会导致预测MV偏差。在低码率下,这种偏差可能让编码器选错MVP索引,增加比特开销。

建议在缩放函数里加断言,检查缩放后的MV是否在合理范围内。如果发现缩放后MV异常大或异常小,说明时间距离计算有问题。

5.3 参考帧索引不匹配的隐蔽bug

参考帧索引不匹配是隐蔽性很强的bug。假设当前PU参考RefList0的第1帧,左边块参考RefList0的第0帧。如果代码忘记检查参考帧索引,直接把左边块的MV拿来用,预测就会偏差。这种bug在简单序列上可能不明显,但在多参考帧配置下会显著影响编码质量。

排查方法是:在候选加入列表前打印参考帧索引,对比当前PU的参考帧索引。如果不一致且没有做缩放,就是bug。

5.4 去重逻辑的性能影响

去重需要两两比较候选MV,候选数量少时开销可忽略。但如果实现里对每个候选都遍历整个列表,在候选多时会有性能问题。优化方法是用哈希或排序去重,但AMVP候选最多5个,实际影响很小。真正需要注意的是去重时的比较精度:如果用浮点比较,可能因为精度问题漏掉重复候选;用整数比较则没有这个问题。

5.5 与Merge模式的交互

AMVP和Merge模式共享部分候选构造逻辑,但列表长度和用途不同。调试时要注意区分:Merge列表可以到5个,AMVP只有2个。如果发现AMVP列表里出现了第3个候选,说明代码复用了Merge的列表长度,需要修正。

另外,Merge模式选中的MV会直接影响后续PU的AMVP候选,因为相邻块的运动信息变了。这种交互在调试时容易被忽视,导致预测结果与预期不符。

6. 从AMVP预测看HEVC帧间编码的设计哲学

把xEstimateMvPredAMVP和fillMVPCand读透之后,会发现HEVC帧间编码的设计哲学其实很清晰:用最小的比特代价,尽可能准确地预测运动信息。AMVP通过空域和时域相关性构造候选,用索引代替完整MV编码,这是"省比特";通过缩放和去重保证候选质量,这是"保准确"。

这种设计在工程上有很多值得借鉴的地方。比如候选列表的固定长度简化了编解码器实现,去重和填充保证了鲁棒性,代价评估的拉格朗日乘子把失真和比特统一到一个框架里。这些思路不仅适用于视频编码,在其他需要预测和压缩的场景里也有参考价值。

实际开发中,AMVP预测的优化空间主要在候选构造顺序和缩放精度上。顺序影响预测质量,精度影响一致性。两者都需要在标准框架内做权衡,不能随意改动。我个人的经验是:先把标准规定的逻辑写对,再考虑优化;优化时用码率-失真曲线验证,不要凭感觉改。

最后分享一个小技巧:调试AMVP时,可以打开HM的trace功能,打印每个PU的候选列表、选中索引和预测MV。对比原始MV和预测MV的差异,能快速定位是候选构造问题还是代价评估问题。这个习惯帮我省了很多排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询