AVS3参考软件HPM4.0源码阅读指南:从编译到核心算法
发布时间:2026/9/16 1:38:21 作者:尧图编辑部 阅读量:1,286

1. HPM4.0源码包到手先搞清楚这几点再开读如果你正在做AVS3相关的东西早晚会跟HPM这个参考软件打交道。HPM的全称是High Performance Model是AVS3标准配套的参考代码工程相当于H.266/VVC那边的VTM、AV1那边libaom的角色。HPM4.0是其中比较有代表性的一个版本代码量已经不小结构也比早期的AVS3参考软件规范了不少很多高校实验室和编解码芯片团队做算法研究、软件优化都是拿这个版本当底子来改的。不过这玩意儿对第一次接触的人来说确实有一点劝退。我印象很清楚第一次解压HPM4.0的压缩包目录一展开密密麻麻的.cpp和.h文件铺了一屏幕光是找main函数在哪就花了一个下午。更麻烦的是AVS3本身用了很多缩写术语什么QBTT、PDPC、DIMD、BDOF如果你不先对这些概念有点印象代码里随便一个变量的名字都能让你懵半天。这篇东西不是标准文档是我自己啃HPM4.0代码时整理出的一套阅读路线。我默认你至少知道视频编码的基本流程预测、变换、量化、熵编码、环路滤波这五件事。如果你连这个框架都不清楚建议先去看任意一本视频编码教材的前三章不然直接上代码会很痛苦。反过来如果你已经跑过VTM或者x265那HPM4.0对你来说只是“换了一组名字和规则”的编码器上手会快很多。我打算按这个顺序来带你看代码先解决环境编译和整体调用链再把编码器里最重要的数据结构捋清楚然后逐模块走读帧内、帧间、变换量化、熵编码的实现位置最后分享几个真正能提升阅读效率的调试技巧。这样一圈走完你对HPM4.0的代码地图就有了一个完整的概念后面想深挖任何一个算法都知道该去哪找、该从哪里打断点。2. 编译运行和代码地图先把它跑起来再谈读代码2.1 编译HPM4.0的最小准备读代码之前我强烈建议你先把工程编出来跑通一条“原始YUV进码流和重建视频出”的完整流程。原因很简单代码里的任何一个函数你光看逻辑很难判断它到底执行了没有但如果手上有一个能跑起来的程序配合调试器的断点和单步执行函数的真实行为一下就清楚了。HPM4.0的编译方式跟大多数C工程一样Linux下直接进build目录执行cmake和makeWindows下可以用Visual Studio打开sln工程。我个人的经验是Linux下调试和看代码的效率更高因为可以用gdb配合cgdb或者vscode的远程调试查调用栈比Visual Studio直观不少。编译前确认你的机器装了gcc/g 7.0以上版本和cmake 3.10以上否则有些C11甚至C14的语法会报错。汇编优化相关的宏如果编译不过先关掉不影响读代码只是编码速度会慢一些。跑编码器的命令行参数也简单。HPM4.0继承了参考软件一贯的风格通过配置文件传入编码参数可执行文件后面跟一个-cfg配置文件路径配置文件里写输入YUV路径、分辨率、帧数、量化参数QP、编码帧类型等等。我第一次拿到工程的时候直接用了别人踩过坑之后留下的一个最小配置方案输入一个352x288的CIF序列QP设32编码10帧编码器跑完在同一目录下生成码流文件和重建YUV。整个过程不到十秒但这一步跑通之后后面所有代码验证都有了参照物值回票价。2.2 入口函数和一帧编码的主干调用链跑通之后接下来要做的事是找入口和主干。HPM4.0的main函数位置在不同版本里可能有差异但一般不会太深你在工程根目录下搜“int main”就能定位。main函数里做的事情非常直白解析配置文件、创建编码器实例、循环读入一帧YUV、调用编码函数处理一帧、把输出码流写入文件、最后清理资源。整个工程里最值得跟的一条调用链是把“一帧图像变成码流”的主线。以我读过的那版HPM4.0为例大致是这样走的从帧级循环进去之后编码器会先做帧级初始化比如决定当前帧是I帧还是P帧然后进入CTU编码树单元级别的外层循环。AVS3和H.265/HEVC一样先把一帧画面切成一个个固定大小的CTU通常是64x64或者128x128然后逐个CTU独立编码。每个CTU的编码过程又进入一个递归函数这个函数会尝试不同的块划分方式对每个划分出来的子块做帧内或帧间预测算率失真代价最后挑一个代价最小的划分方案把对应的预测残差做变换、量化、熵编码。这条链捋顺之后你再看代码里那些几千行的函数就不会慌了因为你知道当前正在看的这个函数属于“递归分块”还是“帧间搜索”还是“熵编码”它在整个流程里的位置是清晰的。这也是为什么我一直强调不要上来就扎进某个算法的细节里先把主干走一遍后面填枝叶的时候才不会迷路。2.3 用符号搜索快速建立“算法名字到代码位置”的映射HPM4.0的代码整体命名比早期的AVS3参考软件可读性好一些但依然有不少缩写。我自己的习惯是遇到一个不认识的算法缩写先在工程目录下做全局搜索搜的阶段越多对它的实现位置越有感觉。比如你想找DIMDDecoder-side Intra Mode Derivation相关的实现直接搜“DIMD”大概会出现几个文件里的十几个函数名然后挑一个看起来像是主函数的点进去看看它的函数签名和注释很快就能判断这个算法是怎么被调进来的。另外HPM4.0里很多算法是有开关控制的通常是SPS序列参数集级或者Slice级的语法元素代码里常见的写法是“if (xxxEnableFlag)”或者“if (m_pcEncCfg-getUseXXX())”。你读代码时如果看到这种开关不要急着跳过去可以反查一下这个开关是在哪里被设置的、默认值是开还是关这样你就知道当前读的这个功能在默认配置下到底会不会被激活。默认配置下很多高级工具是关闭的因为参考软件要兼顾多配置的测试需求而不是追求单点最高压缩率。3. CTU、CU、PU、TU到底谁套着谁啃数据结构的正确姿势3.1 AVS3块划分QBTT在代码里长什么样做AVS3代码阅读第一关必须过“块划分”这关。AVS3的块划分是在四叉树基础上引入了扩展的多叉树划分所以叫QBTT也就是Quadtree plus Binary/Ternary Tree的缩写。简单理解一个CTU可以先按四叉树切成四块每个子块还可以继续四叉切同时块也允许被竖着一刀切成左右两块或者横着一刀切成上下两块甚至按1:2:1的比例切成三块。这种划分方式比H.265的只有四叉树灵活很多代码实现上的递归逻辑也复杂一截。在HPM4.0的代码里描述这些划分的数据结构核心就是一个编码单元类里面会记录当前块的坐标、宽高、深度、父块指针、子块列表。我读代码时的重要体会是不要一上来就去看那个几千行的划分递归函数而是先把这个类的成员变量过一遍搞清楚每个变量记录的是什么信息。比如坐标是用“块左上角相对CTU的x和y”还是“相对图像原点的x和y”这个搞清楚后面调试时打印日志才不会算错位置。另外一个关键点是AVS3的编码单元里有一个很实用的成员用来标识当前块允许做哪些划分方式。这个成员本质上是几个bool开关比如“允许四叉”“允许水平二叉”“允许垂直三叉”等。因为AVS3规定不同深度、不同大小下允许的划分模式不同代码里会先检查开关再进入对应分支。你读划分函数时重点看那些检查开关和计算划分代价的分支条件基本上就把QBTT的全貌掌握了。3.2 CU、PU、TU三种单元的维度要区分开很多读HPM4.0代码的人会被CU、PU、TU这三类结构绕晕。这里我建议你在脑子里建立一个简单的模型CU编码单元是决策维度负责决定这块区域用帧内还是帧间、用哪些预测工具PU预测单元是执行预测时的实际划分有的模式下一个CU只有一个PU有的模式会拆成多个PUTU变换单元是残差做变换和量化的最小单位CU残差可能会被拆成多个TU分别变换。在HPM4.0代码里这三个概念是由不同的类或者同一个类的不同成员来承载的。你在读帧间预测代码时看到遍历PU列表去搜索运动矢量的逻辑这是正常的在读变换量化代码时看到在TU层面上做残差变换和系数扫描这也是正常的。关键是别把“PU跳过”和“TU跳过”搞混一个是预测阶段决定不单独处理一个是变换阶段决定跳过某些子块。为了验证自己对三者的理解我推荐一个实操方法在递归编码函数入口处打一个断点在断点处打印当前CU的坐标、大小以及它的划分方式。然后单步执行几轮观察父块怎么被切成子块的。切完之后再跟进预测函数看PU是怎么生成的跟进变换函数看TU是怎么生成的。这个过程跑一轮下来你对“谁套着谁”的理解比看十遍类定义文档都管用。3.3 RDO循环编码器为什么“慢”答案全在这还有一件事读HPM4.0的编码代码时一定要理解否则你会对编码器的行为感到非常困惑——就是它为什么大部分时间都花在“试错”上。视频编码器的核心逻辑不是直接算出最优方式而是把所有可能的方式都试一遍然后通过率失真优化Rate-Distortion Optimization, RDO选一个综合代价最小的。在代码里你会反复看到类似“rdCost”这样的变量意思是当前候选模式的总代价。代价的计算公式通常是失真加上拉格朗日系数乘以码率拉格朗日系数跟量化参数相关。对不同候选模式做比较然后保留代价最小的那个这就是整个编码器最核心的“内卷”逻辑。所以你会发现同一个块帧内预测函数算一遍代价帧间预测函数又算一遍代价甚至同一种预测模式下不同的参考行、不同的运动矢量精度都要各算一遍。参考软件默认不追求编码速度所以这些候选都会老老实实全试一遍跑得慢是正常的。读代码的时候看到“xCheckRDCost”这种名字的函数就明白这是在做模式比较。我的经验是先搞清楚某个候选模式的代价是怎么算出来的再决定要不要深入它的每一个细节。很多刚读代码的同学喜欢把每个函数的每一行都看懂结果两周过去了还在帧内预测里出不来。正确做法是先把“候选模式—代价计算—选优”这个框架搭起来细节后面用的时候再拆。4. 核心算法代码去哪找帧内、帧间、变换、熵编码的走读入口4.1 帧内预测多参考行和ISP的代码入口帧内预测在AVS3里的地位非常高因为参考软件在Intra配置下完全靠帧内来压缩。HPM4.0里帧内预测的实现入口通常是一个叫“intra prediction”相关名称的函数深挖下去会看到两个主要分支一个计算预测模式包括传统的角度模式以及DIMD这类从已解码像素推导模式的工具另一个根据选定的模式生成预测像素。AVS3的帧内预测比H.265时代复杂不少一个典型功能是多参考行预测。简单说编码时除了用紧挨着当前块的那一行重建像素做参考还可以用更远的一行或几行代价是需要在码流里传参考行的索引。代码里对应的地方会有一个循环遍历多个参考行候选对每个候选都生成一遍预测块并算代价。我第一次看这段代码时觉得这样反复算也太费了后来才明白这就是RDO的常态。另一个值得关注的是ISP也就是帧内子块划分。它把一个帧内编码的亮度块在预测阶段拆成若干个子块每个子块依次单独预测、编码前面的子块重建像素可以作为后面子块的参考。这种模式对细节丰富的区域压缩效率提升明显。要理解ISP的代码关键是熟悉它的划分方式只支持水平或垂直的等宽划分而且子块数量有上限。代码里对应逻辑会体现在“分割方向”和“分割数量”两个变量的设置上以及递归预测子块时对参考像素的更新。读帧内预测代码的时候有一个细节特别容易让人困惑有些参考像素可能需要做平滑滤波或位置相关的加权比如PDPC这类工具。HPM4.0里会有专门的函数处理参考像素的滤波如果你在预测主流程里看到奇怪的像素值变换别急着跳过那很可能就是PDPC或者参考像素平滑在起作用。4.2 帧间预测从运动估计到高级工具的实现层次帧间预测是视频编码压缩率的最大来源HPM4.0里这块代码量也是最大的。整个帧间预测在代码上可以分三个层次运动估计搜索、运动信息编码、运动补偿预测。运动补偿是相对机械的部分本质就是按照运动矢量去参考帧里取对应的块运动信息编码是把运动矢量差和参考帧索引写进码流真正的大头是运动估计搜索因为编码器要在搜索窗口内试很多个位置比较哪个位置预测残差最小。HPM4.0里运动估计的代码会涉及整像素搜索、分像素插值、以及各种提前终止策略。AVS3的帧间工具很多比如AMVR可以动态选择运动矢量精度整像素、半像素、四分之一像素这个功能体现在运动估计里会用一个循环去测试不同精度仿射运动模型Affine则是在平移运动矢量的基础上增加旋转缩放参数对应的代码结构通常是额外的参数数组和额外的代价估计逻辑。建议你读帧间预测代码时先抓住“运动估计入口—MV搜索循环—候选列表构建—RD cost比较”这几步。很多高级工具本质都是在给这个主流程增加候选。比如UMVE是在现有运动候选的基础上做微小偏移再比较一次代价GPM是把一个块拆成两个几何形状区域分别平移运动补偿OBMC是在运动补偿之后对块边界像素做加权叠加。理解了“候选越多选出来的越优但复杂度越高”这个大原则你就不会被这些看似零散的工具搞乱。4.3 变换量化与熵编码残差压缩链路怎么串起来预测做完之后当前块会生成一个残差块也就是原始像素减预测像素。这个残差块接下来要走变换、量化、熵编码链路。HPM4.0的变换部分核心是多种变换核的选择AVS3支持DCT-8、DST-7等变换核还会根据残差特性和块大小去选择最合适的变换组合。代码里你会看到变换候选列表、变换核索引、以及对应的正反变换实现函数。量化部分相对简单一些参考软件更多是直接做标量量化配合量化矩阵和量化参数来计算步长。但这里有块很关键且容易忽略的代码变换跳过模式。有些块经过RD比较后认为直接对残差做量化比做变换更划算此时会跳过变换直接量化残差值。这个分支在代码里有独立判断逻辑调试时如果发现某个块没有进入变换函数却进了量化函数别以为自己看漏了代码它就是跳过了变换。熵编码在HPM4.0里用的是自适应算术编码代码风格比预测模块更“工程化”充斥着各种上下文模型索引、概率更新表、区间计算。读熵编码代码的性价比其实不高除非你是专门做硬件熵编码器优化或者码流分析。我的建议是非相关方向的同学只需要把“系数扫描顺序”和“上下文模型选择”这两件事搞清楚即可知道一个变换系数块是怎么被映射成二进制串、大致按什么顺序编码就足够支撑你理解码流结构和编码器行为了。4.4 环路滤波重建质量全靠这几刀环路滤波虽然放在编码流程的后段但它对重建图像质量的影响非常大而且AVS3在这块也做了不少增强。HPM4.0里环路滤波至少包括去块滤波、样本自适应偏移SAO、自适应环路滤波ALF这几个大块有的版本还带了针对色度的CCALF。它们的共同特点是都需要在一个CTU甚至一帧图像重建完成之后才能做依赖的是重建像素所以你在看编码主流程时会看到“重建—滤波—输出到参考帧”的一连串调用。去块滤波的代码相对好读因为它高度结构化只处理块的边界按水平和垂直两个方向分别滤波。SAO的代码核心是一个分类和偏移量计算的过程编码器要试探像素分类方式把分类结果和偏移量写进码流。ALF是最复杂的它基于维纳滤波原理设计需要对整帧或CTU级别的像素做统计分析来求滤波器系数代码里相关函数的计算量也非常大。读滤波代码时我的建议是不要把每个数学推导都扣死而是先搞清楚滤波器的输入是什么、输出是什么、影响哪些相邻块的重建像素引用。因为你后面如果改动编码器里的预测模块经常会发现画面出现块边界不连续这个时候如果你知道去块滤波和SAO的触发条件就能快速定位是不是重建像素没做滤波就被当作参考了。5. 调试HPM4.0代码的实操技巧断点、日志、对比三件套5.1 断点该打在哪个函数一种高效定位方案读代码的时候如果只是顺序翻阅效率其实很低。对HPM4.0这种大规模工程我的做法是带着“问题”去打断点。比如你想搞清楚某个CTU最后选了什么样的划分我会在递归划分函数的尾部也就是选出最优模式后准备编码系数之前的位置打一个条件断点条件是当前块的坐标等于我关心的那个位置。这样程序运行到目标块时自动停下来我直接看这个块的子块列表和模式选择结果非常直观。确定“坐标”这个条件怎么写需要回到第3节里说的数据结构成员。你先在断点处打印出当前块信息确认坐标成员的命名比如是否有xPos、yPos或者用其他名字。之后条件断点的写法就是“xPos目标x yPos目标y”。这个方法比在代码里加一堆printf再重编译要高效得多也方便反复验证不同位置的处理差异。条件断点还有一种用法是按帧号过滤。参考软件在编码多帧时前几帧是I帧后面是P帧处理逻辑差异很大。只想关注P帧的帧间预测时可以通过帧号或帧类型变量设置条件跳过I帧的漫长单步省掉大量无用操作。5.2 把中间数据丢出来重建块与预测块的对比验证很多编码算法从代码上是看不出实际效果的所以要把中间结果输出来看。HPM4.0这类参考软件一般都有把重建帧写到磁盘的功能就是配置文件里输出重建YUV的路径。但如果你想单独看某个块的预测值、残差值默认功能往往不够用。我的做法是临时在目标代码段加几句文件写入代码把当前块的预测像素存成一个小的raw文件然后用Python或者matlab读出来显示成图像。看到图像之后你对这个算法到底在做什么的理解会是质的提升。有人可能会觉得在参考软件里加日志代码太粗暴但实际做代码研究时这是最常用也最省事的方式。我一般用“宏开关包住临时日志代码”比如定义一个DEBUG_xxx宏默认关闭需要时打开重编。这样不会影响正常实验又能保留调试能力。唯一要注意的是改完代码后跑出来的结果要和未改动的原版做一次对比确保日志代码本身没有改变编码器的行为。除了肉眼观察更严谨的方式是直接做编码器与解码器的中间数据比对。参考软件通常自带解码器编码出的码流可以用配套解码器解回来。如果编码端和解码端在处理某个工具时出现不一致根本原因是两边用了不同的上下文或中间值。这时候要做的就是在编码器和解码器的对应函数里同时打印中间变量逐项对比。这个过程通常比较磨人但只要咬住“某个语法元素编码端写的是A解码端读的是B”这个差异点就能快速缩小范围。5.3 常见误区和避坑记录读HPM4.0代码时我见过太多人栽在几个同样的坑里这里集中提一下。第一个坑是“默认配置下高级工具没开怎么找都找不到执行路径”。我之前有个学生第一次接触ALF找了整整两天都没看到ALF相关的分支被执行最后发现是配置文件里的ALF开关默认是0。所以读任何功能之前先回配置文件看一眼对应开关的默认值会帮你节省大把时间。第二个坑是“多线程干扰调试”。HPM4.0有些版本开启了多线程编码断点一打下去旁边一堆线程也跟着停单步执行时经常跳到无关代码里。排查功能逻辑时建议先在配置里把线程数设为1让编码走单线程。虽然速度慢一些但调试体验会好很多逻辑也更清晰。第三个坑是“过度纠结失真算法和码率估计算法的每个细节”。率失真代价计算时涉及很多近似和快速计算方法这些细节对提升编码效率有价值但对理解编码流程和算法原理帮助不大。如果一开始陷进去很容易把整个阅读计划拖垮。建议第一遍略过这些细节只认“代价越低越优”这个原则等你要改代价模型或者做快速算法研究时再回头补细节也不迟。还有一点参考软件的版本迭代很快HPM4.0之后还有新的版本发布函数名和目录结构会有调整。如果你看的代码和我描述的细节不完全一致这是正常的。读代码时别依赖“背名字”要把自己训练成理解“结构”和“算法本质”的人这样才能在不同版本的参考软件之间游刃有余。我自己用下来的心得就是多花时间把第3节说的CU/PU/TU关系和第2节的调用链彻底吃透后面换到任何AVS3参考软件版本你都能快速定位自己想要的东西。