news 2026/10/15 13:34:25

HEVC帧间预测MVP全解析:MV获取、传递与存储的代码链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HEVC帧间预测MVP全解析:MV获取、传递与存储的代码链路

刚开始啃HEVC参考软件HM代码的时候,绝大多数人都会卡在帧间预测这一块。帧间预测是H.265压缩率的主要来源,而MVP(Motion Vector Prediction,运动向量预测)又是帧间预测里最绕的一环。我见过很多初学HEVC代码的同学花了两三周还在ME搜索和变换量化之间打转,真正把MV的获取、传递和存储链路梳理清楚的,屈指可数。其实这几个环节在代码层面是一条完整的单向流水线,只要把“MV从哪里来、经过哪些数据结构、最终存到哪里、什么时候被再次读出来”这条主线抓住,整个帧间预测的实现逻辑就豁然开朗了。

这篇文章不是标准条文解读,而是基于HM参考软件实现的一次代码阅读笔记,同时也会牵扯到HEVC标准层面对MVP的约束。我会把MVP过程中MV的获取、传递、存储拆成三个独立但又互相咬合的话题来聊,并且会补充我在读码和调试过程中踩过的一些坑。如果你正准备学习HEVC代码,或者正在给某个视频编码器做帧间预测模块的开发和维护,这篇文章可以帮你少走很多弯路。

1. 从一帧画面说起:为什么要单独研究MVP里的MV生命周期

1.1 帧间预测的底层逻辑:消除时间冗余

视频编码之所以能压得这么狠,核心在于时间冗余。视频里相邻两帧的内容往往高度相似,与其完整编码当前帧每个像素,不如以之前帧为参考,描述“这一块从哪个位置搬过来的”。这个“搬移”参数就是运动向量(Motion Vector,简称MV),它通常是一个二维坐标,表示当前预测块相对参考块的水平偏移和垂直偏移。

但这里有个容易搞混的点:MV并不是高空掉下来的绝对值。编码器在进行运动估计(ME)时,会在参考帧里搜索最匹配的块,得到一个“真实”的MV,但如果直接把完整MV用二进制写进码流,代价太大了。于是标准设计了一个关键技巧:先根据当前块周围已经编码/解码完成的运动信息,预测出一个大概率非常接近真实MV的值,也就是MVP,然后只编码真实MV与MVP之间的差值MVD。解码端用同样的方式推导MVP,再加上MVD,就能无损还原真实MV。这个“预测-残差校正”的思路,和帧内预测做像素残差是一模一样的道理,只不过对象从像素换成了运动信息。

1.2 MVP在编码器里的位置和作用

MVP在编码器端主要有两个作用。第一个作用是为了划算地表示MV,即AMVP(Advanced Motion Vector Prediction)模式:编码器构造一个候选MVP列表,从中选一个作为预测值,再传一个索引和MVD。第二个作用是彻底不传MVD的Merge模式:编码器直接把某个候选的完整MV和参考索引搬过来用,码流里只传一个候选索引,连MVD都省了。

不管哪种作用,都逃不开一个核心操作:从已经处理过的相邻块或时间同位块里“借”运动信息。既然是“借”,就必须有一套严格可复现的读写规则。读码时你会发现,标准里大量篇幅在描述候选位置的排序、可用性判断、缩放规则,本质上就是在定义这套“借”的规章制度。这就直接引出了本文的核心命题:MV是怎么算出来的,怎么从块A传到块B,又怎么从第n帧跑到第n+2帧去参与预测。

1.3 一次完整的MV生命周期概览

为了让你先有一个整体地图,我把MV的生命周期粗略画了一条链路,后面每一节都会对应展开:

运动估计搜索得到初始MV。ME模块会在参考帧中搜索,得到一组或多组率失真最优的MV候选。然后编码器构造MVP候选列表,把预测MV算出来,并计算出MVD。编码完成后,MV信息和模式信息被写入到当前PU(预测单元)的运动参数中。当前帧的CTU(编码树单元)被刷新时,所有PU的MV和参考索引会被收集并写入参考帧运动场。当后续帧做时域MVP时,解码器或编码器再从运动场里读出同位块的MV,做缩放后参与候选列表构造。

你可以把这理解为工厂生产线的四个工位:生产、包装、入库、出库。任何一环断了,帧间预测都会直接崩掉。后面的章节,我会按照这个顺序依次拆解。

2. MV的获取:编码端负责“生产”MV的两条路径

2.1 运动估计搜索:ME模块怎么算出真实MV

运动估计的目标不是“找到最匹配的块”这么简单,它需要在匹配代价和码率代价之间做权衡。HEVC参考软件HM里的运动估计主要分为整像素搜索和分像素细化两个阶段。整像素搜索阶段会在参考帧的一个搜索窗口内,用SAD(绝对误差和)或SATD等准则评估候选位置;找到整像素最佳位置之后,再用插值滤波得到二分之一和四分之一像素精度的像素值,继续做细化搜索。因为参考帧中并不存在真实的“中间像素”,分像素位置的值全部要靠抽头滤波插出来,所以运动补偿模块和插值滤波器在实现上必须高度配合。

这一阶段产生的MV,会以编码树单元(CTU)为粒度进行管理。HM的ME代码里,每个CU都会维护一组预测运动信息候选,在xEstimateInterPUV这类函数中,会有专门的循环去调用xTZSearch系列函数。TZ搜索是HM里默认的快速搜索算法,它结合了菱形搜索和光栅扫描,比单纯的全搜索省很多运算量。你在读代码时不需要把TZ的每个参数都背下来,但需要关注一个关键点:ME搜索得到的MV,在后续构造MVP列表时同样会被当作“已存在的运动信息”使用,因为MV一旦确定下来,当前CU在编码过程中就处于“可被周围块参考”的状态。

这里有一个新手经常困惑的地方:ME搜索出来的MV,和MVP列表里的MV到底什么关系?答案是:ME产生的是“真实MV”,MVP列表产生的是“预测MV”,两者相减才是MVD。同一个运动矢量,既可能作为真实值被编码,又可能作为其他块的预测值被读取,这就是MV传递的第一个含义:信息复用。

2.2 预测候选推导:空域与时域MVP的生成规则

MVP的候选来源分两大类:空间域和时间域。空间域候选来自当前PU周围已经编码的相邻块,HM里分五个位置取候选,分别是左侧两块A0、A1,上方三块B0、B1、B2。候选推导顺序是固定的:先取A1,再取B1,然后B0、A0,最后B2。取出来之后做可用性检查,如果相邻块不存在、不在当前帧内、或者没有用帧间模式编码,就直接跳过。这里和H.264的向量预测不同,HEVC对空域候选的去重要求更严格,相同运动参数的候选会被合并掉,避免重复进入列表。目的很简单:候选列表长度有限,必须尽量装“信息量”大的选项。

时间域候选是HEVC相比H.264变化最大的地方,也是存储依赖最重的环节。它不参考当前帧周围的块,而是跑到当前帧的参考帧里,找到和当前块位置对应的同位块,把同位块的运动向量拿过来用。因为当前块和参考块所在的帧时间距离不同,拿来的MV不能直接用,需要做一步缩放:缩放因子由两个POC(图像序列号)距离决定,即当前帧与当前参考帧的距离、当前参考帧与参考帧的参考帧的距离。这段逻辑在HM的xGetColMVP里写得很清楚,核心变量就是iColPOC和iCurrPOC之间的比值。

2.3 实际MV与预测MV的偏差处理:MVD与参考索引

有了真实MV和预测MVP,编码器要计算差向量MVD。这个差向量和参考索引一起经过熵编码写入码流。解码端必须用完全相同的规则重新推导MVP,才能正确恢复真实MV。因此整个推导过程不允许存在任何“猜测”,每一处候选位置的选择、可用性判断、优先级排序,都必须严格按照标准定义执行。这也是视频编码标准那么冗长的根本原因:它必须让编码器与解码器在信息不完全一致的情况下,仍能复现完全相同的决策路径。

在实际代码里,你会在编码器里看到大量getMVPIdx之类的函数,它们负责返回最终选择的MVP在候选列表里的索引。这里有一个很容易被忽略的细节:AMVP的候选列表只装两个候选项,选MVP时通常通过率失真代价来比较;而Merge模式的候选列表最多装五个,Merge模式下不传MVD,只传索引。编码器在选择Merge候选和AMVP候选时,往往要跑一遍运动补偿甚至残差变换,这个开销不小,所以很多工程优化会直接通过早期跳过机制减少Merge候选的率失真计算。

3. MV的传递:从PU边界到参考像素的完整链路

3.1 编码树结构中的传递路径:CTU到CU再到PU

HEVC的编码划分是树状的:一帧图像先切成若干CTU,每个CTU可以递归四分叉划分成CU,CU再划分成PU。运动信息是挂在PU层面上的,因为一个CU如果采用帧间模式,至少包含一个PU,每个PU拥有独立的MV和参考索引。所以从数据流的角度看,MV的传递路径是:CTU的候选信息先传递给CU的预测结构,CU再根据模式决定PU如何切分,最后每个PU从自身维护的运动参数结构里读取和写入MV。

在HM代码里,这个“挂载”过程体现在TComDataCU的成员变量上。每个CU都持有自己的CUMvField,里面以固定粒度的数组保存了MV和参考索引。你在调试时可以打印某个CU内部每一个4x4块左上角的运动参数,会发现同一个PU内部的运动参数是完全相同的。这就是所谓的“运动参数以PU为粒度共享”:数据存储在最小粒度上,但读写索引方式保证了块级共享。

这里想多说一句:真正看懂这段代码,不能只看PU的划分逻辑,还要看TComDataCU里那些getMotionVector和setMotionVector函数。它们接收的索引往往不是普通的CU序号,而是按4x4块换算出来的平面索引。这种换算逻辑非常容易在改动代码时踩坑,尤其是当PU尺寸不是4的整数倍时,你在做边界裁剪时必须特别小心,否则读一个运动向量会读到别的PU的数据。

3.2 Merge模式与AMVP模式在传递方式上的差异

Merge模式的特点是全继承:当前PU不产生自己的新MV,而是直接把候选列表里某个候选的完整运动参数(MV、参考索引、预测方向)复制到自己身上。整个传递过程是“纯拷贝”,不经过任何缩放或修正。所以Merge模式特别适合那些处于大平坦区域中的块,运动一致性很高的场景下,压缩效率会非常好。

AMVP模式则不同,它只继承候选MV作为预测基准,最终MV是预测基准加上MVD的结果。这导致一个后果:AMVP模式下候选MV的运动矢量精度必须是已知的,但参考索引可以由当前PU单独选择,不一定和候选块相同。因此在传递流程里,AMVP比Merge多了一步MVD的熵编码和解码,也需要多传递一个MVP索引。

从代码结构来看,HM在预测搜索阶段会先跑一遍Merge候选的评估,然后再评估常规的AMVP模式。你会发现getInterMergeCandidates和xGetMVPIdxCand这两条代码路径走的逻辑很不同:前者会填充完整的候选结构体数组,后者只填充MVP和索引。如果在工程上做优化,这两块往往也是分开维护的,因为Merge的候选结构体还需要保存参考帧列表方向,而AMVP只需要保存一个MVP向量。

3.3 逆搜索过程:解码端如何复现MV

解码端不具备运动估计能力,它只能严格执行标准定义的推导流程。在AMVP模式下,解码器先用当前位置周围已经解码完成的运动信息构造MVP候选列表,然后从码流中读取MVP索引和MVD,把二者相加得到实际MV,再拿这个MV去参考帧中取像素做运动补偿。整个过程的准确度完全依赖“已解码运动信息”的一致性。这就是为什么标准强制要求所有块必须在空间和时间上都保持严格的依赖顺序:你不能在右侧块还没解码时就去读它的MV做预测。

在HM的解码端代码里,TDecCu::xReconInter会通过m_cInterPrediction做运动补偿,而在进入运动补偿之前,TDecCu::decodeCU就已经把MVP和MVD组合好了。这里有一个很值得新手学习的小技巧:解码端构造候选列表和编码端构造候选列表用的是同一套函数,共享代码在工程上极大降低了编解码不一致的风险。如果你自己写编码器,请务必保证这一步绝对复用,不要在解码端“优化”掉任何候选去重或可用性检查。

3.4 传递过程中的数据组织:候选列表构建细节

MVP的传递最终都归结为候选列表的构建。空域候选和第时域候选会先进入同一个池子,然后按照既定顺序排序并去重。HM里有一个细节:对空域候选而言,并不是直接把相邻块的MV塞进池子就完事,还需要记录这个MV是来自哪个参考帧列表、参考索引是多少。因为后续根据候选做运动补偿时,这些信息要一起用。很多标准讲解只讲“MV的预测”,实际上参考索引和列表方向同样是候选的一部分。

去重逻辑也值得单独说。空域五个位置可能存在大量重复运动参数,比如当前PU很大,左下和右下其实处于同一个运动区域,这时候列表里会留下完全相同或者按缩放后相同的候选。HM里的去重判断非常直接:候选的MV、参考索引、预测方向全部相等才算重复。如果你改动参考索引的初始值,导致默认候选是“无参考帧”状态,很可能会跳过去重,然后向列表里塞进一堆无效候选,这个问题在调试时相当隐蔽。

4. MV的存储:参考帧运动场与缓存管理

4.1 为什么必须缓存参考帧的MV:时域MVP的依赖关系

时域MVP是这个环节存在的唯一理由。当前帧做预测时,要去参考帧里找同位块,把这个同位块的MV作为候选。问题是:参考帧的MV在解码完成后,如果只保留重建像素而不保留运动信息,后续帧就无法做时域MVP了。因此,每个参考帧除了要保存像素缓冲区,还必须保存一份运动参数场。这份运动参数场会和像素一起放进参考帧列表DPB中统一管理。

从系统层面看,这个缓存带来的内存开销相当可观。HEVC的MV数量级是很大的,如果按4x4块粒度来存,一帧1080p图像就有大约8万个运动矢量,每个运动矢量如果拆成两个int16分量,再加上参考索引和预测方向,内存开销完全不是小数。也正因如此,标准允许解码器以16x16或更粗的粒度导出运动参数来降低内存需求,而参考软件HM出于精度考虑默认按4x4粒度维护,后续的VTM等实现也同样选择了4x4的MotionBuf结构。

4.2 存储粒度与内存布局

聊到存储粒度,很多初学者不理解为什么标准里对“最小导出单元”有这么多限制。简单解释一下:时域MVP取同位块时,要定位到中心位置,如果运动场粒度太粗,中心位置落入的块可能覆盖很大的面积,导致提取到的运动信息不够精确。粒度过细则内存和带宽压力大,所以在标准层面做了一定的平衡。

HM实现中,每个TComPic拥有一个额外的运动参数缓冲区,它和亮度样本缓冲区尺寸相匹配。读取某个坐标处的运动信息时,会根据坐标位置换算到对应4x4块的行列号。这里如果遇到不在参考帧范围内的坐标,实现会直接返回不可用状态,而不是越界访问。我在自己的开发中就遇到过这样的bug:参考块坐标在分像素精度下做四舍五入后跳到帧边界上,直接读取导致内存越界,加一个边界钳制就稳定了。

4.3 运动参数存储的更新时机与生命周期

运动参数场不是一帧全部重建完成后才一次性生成的,而是随着每个CU的解码过程实时更新的。当一个CU以帧间模式完成重建后,它的运动参数立刻写入当前帧的运动场中。这个设计保证了后续CU做空域MVP时,能读到刚解码完的相邻块信息。标准之所以要求编码顺序和解码顺序一致,这是根本原因之一。

更新时机带来的一个重要工程结论是:你不能在整帧解码完成之后再统一填运动场。否则空域MVP推导会拿到尚未初始化的运动信息,编解码结果就会错乱。在HM代码里,你会看到setMotionField这样的函数在每个CU解码后就被调用。如果做并行解码或瓦片编码,这块缓存的一致性就变得极难维护,这也是很多并行编码器设计里最麻烦的问题之一。

运动场生命周期与DPB绑定。一帧图像的运动场会一直存活,直到它从DPB中被淘汰。如果开的是低延迟编码配置,参考帧数量少,运动场内存还能接受;一旦开B帧层次编码,DPB会缓存多帧参考图像,运动场占用的内存经常能占到整个DPB缓冲区的三分之一左右。在做嵌入式移植时,这往往是被忽略的一项内存开销。

4.4 DPB与内存带宽的权衡

DPB管理不该被当成纯粹的内存问题,它对编码性能同样有直接影响。每次做时域MVP,编码器都要去DPB中的某个参考帧读取运动场,这是一块额外内存访问开销,和运动补偿阶段读像素是并行的数据流。如果设计解码器时只优化像素读取而忽略运动场读取,缓存命中率会很难看。

在实践层面,我建议做代码移植时把运动场从像素缓冲区中分离出来,单独做成一个连续数组,并保证和像素缓冲区使用相同的行数对齐。很多新手会把运动场拼在像素区的尾部,表面省了分配次数,实际上会导致按行读取时缓存行相互干扰,得不偿失。另外,如果芯片本地存储有限,可以考虑把运动场的存储粒度提高到8x8或16x16来换空间。代价是时域MVP的精度会有轻微损失,但这种损失在绝大多数码率下都小于百分之一,是值得做的工程取舍。

5. 实战排查:学习HM代码时常见的几个问题

5.1 空域候选访问越界问题

空域候选推导需要访问当前PU左侧和上方的多个位置,最容易出的问题就是坐标换算导致的越界。尤其是在CU划分不规则的情况下,某些相邻块可能没有完成编码,导致读取到的MV是无意义的初始化值。还有可能是当前块靠图像边界,候选位置本身就不存在。HM代码里有大量的isAvailable类判断,很多新手为了图省事把这些判断删掉,结果编解码结果完全错乱。

我的建议是不要只在逻辑上保留这些判断,还要在DEBUG模式下加入坐标范围的断言。MV传递链路很长,一旦发生越界,错误会延迟到很多个帧之后才在图像上体现出来,排查难度极大。特别是用O2优化编译时,越界读可能不会立刻崩溃,而是读到了之前的旧运动场数据,这种错误几乎是隐形的。

5.2 时域MV缩放精度问题

时域候选的MV缩放是MVP里最容易产生编解码不一致的环节。参考软件里通常使用带精度的定点乘法来实现缩放,比如先左移若干位再乘分子分母,最后右移还原。如果你在移植时把中间运算的位宽或舍入方式改了,哪怕只是差了1,也可能让编码器推导的MVP和解码器推导的MVP不一致,进而导致后续所有MV重建全部错位。

排查这类问题有一个屡试不爽的办法:在编码器端和解码器端各加一个调试开关,把每个块的MVP候选值全部打印出来,逐步比对。尤其是不同参考帧组合下,缩放因子为负值或者分母为零的特殊情况,一定要单独处理。HM里对这种情况会置候选不可用,你在自己的实现里也必须有同样的分支。

5.3 Merge候选列表的去重与长度控制

Merge候选最多五个,这个上限是硬性的。候选池子里可能填不满五个,也可能因为重复过多被去重后明显减少。很多实现为了效率,会在候选数不足时用一些补充手段填满列表,比如直接复制第一个可用候选,或者使用零运动向量。这些补充手段本身不复杂,但必须保证编解码两端严格一致。

我在实际调试中遇到过一个诡异问题:编码器端的候选列表是五个,解码端因为某个去重条件判断里参考索引值不一样,只剩四个,结果从第五个索引解出来的是完全错误的运动信息。最后定位原因,竟然是一个CU在编码过程中被提前标记成了帧内模式,参考索引被重置,导致去重时判定逻辑分叉。这类问题很隐蔽,排查时不要只盯着MVP推导,也要检查当前块和相邻块的模式状态。

5.4 运动场更新与缓存一致性问题

如果你在参考软件上做了并行化改造,最容易被坑的就是运动场更新的时序。帧间预测时,空域候选读取的必须是当前帧已经完成重建的块,时域候选读取的必须是DPB中完整帧的运动场。这两类数据的生命周期完全不同,前者是边解码边写,后者是整帧稳定之后才可读。

一个比较稳妥的做法是给每帧运动场加一个状态标记,标记为“写入中”“已完整”“已被替换”三态。任何时域MVP读取操作都要检查目标帧的状态不是“写入中”。我当时在一个多线程解码器里遇到花屏问题,排查到最后就是某个线程在参考帧运动场还没写完整的时候就去读了,加了一个状态锁之后立刻解决。这个经验不一定适用于所有项目,但它提供了一个排查方向:先怀疑缓存时序,再怀疑计算公式。

编解码这条技术路,向来是细节决定成败。MVP里的MV获取、传递、存储这三件事,单独看都不难,难的是它们之间的耦合关系:ME产生的MV要能精确落入运动场,运动场的存储格式要能让空域和时域候选在毫秒级延迟下读取,而候选列表又要把这些庞杂信息收敛成几个有限的索引值。如果把这条链路吃透了,回头再看HEVC整个帧间预测部分,你会觉得大部分代码都只是这条主链路上的具体实现分支。我自己读HM代码那段时间最大的收获,就是学会了把“标准怎么说”和“代码怎么写”放在一起对照,而不是孤立地啃某一个函数。如果你也在啃这块代码,建议从TComDataCU的MV字段入手,顺着一次完整的帧间CU编码流程把读和写都跟一遍,效果会比只看文档好得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:34:04

奢侈品联名营销策略与影视IP合作案例分析

1. 奢侈品联名营销现象解析最近时尚圈最热的话题莫过于法国顶级珠宝品牌斐登(FRED)与热门剧集《万花世界》的跨界合作。这次联名不仅登上了ELLE杂志封面,更创造了惊人的销售业绩——参与拍摄的几位女星同款单品迅速售罄,直接带动品…

作者头像 李华
网站建设 2026/10/15 13:33:51

STM32驱动双舵机摄像头云台:PWM控制与PID目标追踪实现

简介:STM32摄像头云台综合项目是一份面向嵌入式学习者的完整工程资料,覆盖OV7670图像采集、STM32F103VET6驱动、4.3寸触摸屏显示、MPU9250姿态检测与PID舵机云台控制的主线流程,适合课设、电子竞赛及嵌入式进阶实践。压缩包共含296个文件&…

作者头像 李华
网站建设 2026/10/15 13:33:51

InfiniSynapse智能代理框架:浏览器自动化技术创新

1. 项目背景与技术定位InfiniSynapse作为新一代智能代理框架,其浏览器支持模块在V3架构基础上实现了突破性创新。这个模块最核心的价值在于解决了传统自动化浏览器工具面临的三个行业痛点:页面加载稳定性、动态元素识别准确率以及多模态交互的自然度。我…

作者头像 李华
网站建设 2026/10/15 13:33:48

HyperFrames超帧技术:用光流插帧将连拍照片变为流畅视频

做视频后期的人应该都有过这种经历:手里只有一组连拍照片,或者素材帧率不够,想生成一段顺滑的动态效果,结果一拖进时间线就卡成PPT。前几年遇到这种素材我会直接放弃,改成慢速平移之类的假动作。后来接触到 HyperFrame…

作者头像 李华
网站建设 2026/10/15 13:33:20

SVM人脸分类实战:小样本下的特征工程与可解释决策

1. 为什么不用深度学习,而选SVM做这个人脸分类任务?“人脸分类”四个字一出来,很多人第一反应就是:上ResNet、微调ViT、搭个FaceNet再加个ArcFace损失——这几乎是当前CV领域的肌肉记忆。但我在某高校实验室带一个本科生小组做课程…

作者头像 李华
网站建设 2026/10/15 13:33:12

回溯算法实战:全排列问题解析与Python实现

1. 题目背景与核心需求全排列问题是算法领域最经典的回溯算法练习题之一,在力扣题库中被标记为hot100高频面试题(编号46题,非50题)。给定一个不含重复数字的整数数组nums,要求返回所有可能的排列组合。举个实际例子&am…

作者头像 李华