VTM编码单元CU划分递归逻辑与代码调试实战
1. 从零啃VTM代码CU划分到底在做什么如果你正在读VTM的代码大概率已经翻过几遍CompressGOP或者xCompressCU这类函数然后被里面层层嵌套的递归、各种try/split分支搞得头大。我刚开始看的时候也一样一个xCompressCU函数几百行里面又是RdCost又是PartSplit看完只觉得“好像懂了”但真要自己改点东西完全不知道从哪下手。这篇就专门聊CU划分这一块。CUCoding Unit编码单元是VTM里做预测、变换、量化的基本单位。你可以把它理解成“编码时的最小决策块”——一个CTU进来先决定怎么切成CU切完之后每个CU再决定用什么预测模式、怎么变换。所以CU划分是整个编码流程的骨架骨架搭错了后面再优化都是白搭。VTM的CU划分核心逻辑集中在xCompressCU这个递归函数里。它做的事情说白了就一句话给定一个CU尝试各种划分方式算出每种方式的最优代价选最小的那个。听起来简单但里面涉及几个关键问题划分方式有哪几种、递归什么时候停、代价怎么算、快速算法怎么剪枝。这几个问题搞清楚了CU划分的代码基本就通了。这篇文章适合两类人一是刚接触VTM、想搞清楚编码流程的在校学生或者转行做视频编码的工程师二是已经在用VTM做实验、但想深入改划分逻辑发论文或者做优化的研究者。我会从整体设计思路讲到具体代码细节再给出手把手的调试方法最后分享几个我踩过的坑。代码基于VTM的通用版本不同版本函数名可能略有差异但核心逻辑一致。2. CU划分的整体设计与递归框架2.1 为什么VTM要用递归划分而不是固定分块早期编码标准比如H.264宏块大小固定16x16划分方式也就那么几种实现起来简单直接。但视频内容差异太大了——平坦区域用大块编码效率高纹理复杂区域必须用小块才能保证质量。固定分块要么浪费码率要么质量上不去。VTM继承HEVC的 quadtree 思路并做了扩展CTU最大可以到128x128然后通过递归的方式往下切。递归的好处是自适应编码器可以根据当前块的率失真代价自己决定切不切、怎么切。平坦区域可能一个128x128的CU就够了复杂区域可能一路切到8x8甚至4x4。但递归的代价是计算量爆炸。一个128x128的CTU如果每种划分都试一遍组合数是天文数字。所以VTM里既有“全搜索”的暴力逻辑也有大量快速算法来剪枝。理解这个矛盾是读懂CU划分代码的关键。2.2 划分模式的全景从四叉到二叉树再到扩展四叉VTM的CU划分不是简单的四叉树。它支持多种划分模式这些模式定义在PartSplit枚举里。我整理了一下常见的几种划分模式含义适用场景CTU_QUAD_SPLIT四叉划分切成4个等大子块通用最基础CTU_HOR_SPLIT水平二分切成上下两块水平纹理或运动边界CTU_VER_SPLIT垂直二分切成左右两块垂直纹理或运动边界CTU_HOR_TR_SPLIT水平三分按1:2:1切特殊纹理分布CTU_VER_TR_SPLIT垂直三分按1:2:1切特殊纹理分布CTU_QUAD_SPLIT_HOR扩展四叉先水平二分再各自二分VTM新增灵活度更高CTU_QUAD_SPLIT_VER扩展四叉先垂直二分再各自二分同上这里要注意不是所有划分模式在所有层级都可用。比如128x128的CU不能直接做三分因为最小子块会小于4x4。代码里有一堆isSplitAllowed之类的判断就是在做这些约束。提示看代码时先找到PartSplit的定义把所有划分模式列出来然后对照xCompressCU里的switch分支这样不会迷路。2.3 递归的终止条件什么时候不再往下切递归不能无限往下必须有终止条件。VTM里主要有几个约束最小CU尺寸由SPS里的minCUWidth和minCUHeight决定通常是4x4或8x8。到了这个尺寸就不能再切了。最大划分深度maxMTTDepth和maxBTDepth分别控制二叉树和多类型树的深度。超过深度限制就不能再用对应的划分模式。图像边界如果CU超出图像边界某些划分方式会导致子块完全在图像外这种划分直接跳过。SPS配置有些划分模式可以通过配置开关关闭比如sps_btt_flag控制二叉树是否启用。这些条件在代码里分散在各处我建议你画一张决策流程图把每个判断条件标上去这样递归逻辑就一目了然了。2.4 代价函数RD Cost是怎么算出来的CU划分的核心决策依据是率失真代价也就是RD Cost。公式很简单RD Cost Distortion Lambda * RateDistortion是重建图像和原始图像的失真通常用SSE或SAD。Rate是编码这个CU需要的比特数包括预测模式、残差、划分标志等。Lambda是拉格朗日乘子控制码率和失真的权衡。但实际代码里RD Cost的计算分好几个层次。在划分阶段VTM通常先用SATD或者简化代价做快速筛选只有候选者才会做完整的RD计算。这就是为什么你看代码时会发现calcRdCost被调用的次数远少于xCompressCU的递归次数。注意不同版本的VTM在代价计算上差异很大尤其是VTM 10之后引入了很多基于神经网络的快速算法。如果你看的是老版本逻辑会相对单纯一些。3. 核心代码细节xCompressCU逐层拆解3.1 函数入口与初始化进来先干什么xCompressCU的签名大概长这样void EncCu::xCompressCU( CodingStructure *tempCS, CodingStructure *bestCS, Partitioner partitioner, const EncTestMode encTestMode )参数里几个关键对象tempCS当前尝试的编码结构每次尝试划分时会重置bestCS目前为止最优的编码结构递归返回时会把最优结果存进去partitioner划分器管理当前CU的位置、尺寸、深度等信息encTestMode编码测试模式包含当前是做什么类型的决策函数一进来先做一堆初始化设置当前CU的预测模式候选、初始化代价为最大值、检查各种跳过条件。这部分代码比较琐碎但有几个点值得注意partitioner.currArea()返回当前CU的区域信息后面所有操作都基于这个encTestMode里的optimalMode决定当前是尝试帧内还是帧间如果当前CU尺寸已经等于最小CU直接进入预测模式决策不再尝试划分我刚开始看的时候经常搞混tempCS和bestCS后来想明白了tempCS是草稿纸每次尝试新划分时在上面算bestCS是最终答案只有比当前最优更好时才会更新。3.2 尝试不划分先算当前CU的最优代价递归的第一步是先假设不划分直接对当前CU做完整的预测、变换、量化算出一个RD Cost。这个代价作为“基准”后面所有划分尝试都要跟它比。这部分代码通常在xCheckRDCostIntra或者xCheckRDCostInter里。帧内的话会遍历所有帧内预测模式帧间的话会做运动估计和运动补偿。算完之后得到一个bestCS的初始值。这里有个细节VTM里帧内和帧间的尝试顺序会影响最终结果因为有些快速算法会基于已有结果做剪枝。但理论上如果全搜索顺序不影响最优性。实操心得如果你想验证代码逻辑可以在这里加个断点看看一个平坦区域的CU是不是直接就不划分了而复杂区域会继续往下递归。这是最直观的理解方式。3.3 遍历所有划分模式switch分支里的秘密不划分的代价算完之后就进入划分尝试的循环。代码结构大概是for( auto split : splits ) { if( !isSplitAllowed( split ) ) continue; // 保存当前状态 // 执行划分 // 递归调用xCompressCU // 比较代价更新最优 // 恢复状态 }splits是一个PartSplit的列表包含了当前CU允许的所有划分模式。这个列表不是固定的会根据CU尺寸、深度、SPS配置动态生成。每个划分模式的处理逻辑类似但细节不同。以四叉划分为例调用partitioner.splitCurrArea把当前CU切成4个子块对每个子块递归调用xCompressCU4个子块的代价加起来加上划分标志的比特数得到总代价如果总代价小于当前最优更新bestCS二叉树划分类似只是切成2块。三分划分切成3块比例是1:2:1。这里最容易出错的地方是状态保存和恢复。递归之前要保存当前tempCS的状态递归之后要恢复否则会影响后续划分模式的尝试。VTM里用CodingStructure的save和restore方法来做这件事代码里到处是tempCS-save()和tempCS-restore()。3.4 递归调用与代价比较最优解是怎么选出来的递归调用的核心逻辑可以用伪代码表示bestCost cost of no split for each split mode: if not allowed: continue split current CU totalCost 0 for each sub-CU: subCost xCompressCU(sub-CU) totalCost subCost totalCost split flag bits if totalCost bestCost: bestCost totalCost bestCS tempCS restore state return bestCost看起来简单但实际代码里有很多优化。比如提前终止如果当前累加的代价已经超过bestCost后面的子块就不用算了直接跳过。这叫“代价剪枝”。快速筛选先用简化的代价函数比如SATD筛一遍只有有希望的划分才做完整RD计算。上下文继承子块的某些编码参数可以从父块继承减少计算量。这些优化是VTM性能的关键但也是代码最难懂的地方。我的建议是先把主逻辑理清楚再去看这些优化不然容易被细节淹没。3.5 特殊处理边界CU和最小CU边界CU的处理是个坑。如果CU部分超出图像边界VTM会用padding的方式填充但划分的时候要特别小心。比如一个64x64的CU只有右下角32x32在图像内那四叉划分之后有些子块完全在图像外这种划分就没意义。代码里通常用partitioner.isSplitAllowed()来统一判断但不同版本的实现不一样。有的版本会在xCompressCU入口就检查边界有的版本在划分循环里检查。最小CU的处理相对简单到了最小尺寸直接做预测模式决策不再尝试划分。但要注意最小CU的尺寸不一定是4x4取决于SPS配置。有些配置下最小CU是8x8这时候4x4的划分根本不会出现。4. 实操手把手调试CU划分过程4.1 环境准备与编译配置要调试VTM首先得能编译。VTM用CMake构建基本流程mkdir build cd build cmake .. -DCMAKE_BUILD_TYPEDebug make -j8Debug模式很重要因为你要加断点、看变量。Release模式编译快但调试信息不全。编译完之后用测试序列跑一下./bin/EncoderApp -c cfg/encoder_intra_vtm.cfg -i test.yuv -wdt 416 -hgt 240 -f 1配置文件里可以控制很多参数比如MaxCUWidth、MaxCUHeight、MaxPartitionDepth等。调试CU划分时建议先用小分辨率、少帧数不然跑一次太慢。4.2 加日志打印每次划分决策最直接的调试方法是在xCompressCU里加日志。我通常会在函数入口打印当前CU的位置和尺寸std::cout xCompressCU: pos( partitioner.currArea().blocks[0].pos().x , partitioner.currArea().blocks[0].pos().y ) size partitioner.currArea().blocks[0].size().width x partitioner.currArea().blocks[0].size().height depth partitioner.currArea().depth std::endl;然后在划分决策的地方打印最终选了哪种划分std::cout best split: bestCS-bestPartition cost bestCost std::endl;跑一个小序列日志会很多但能清楚看到递归的整个过程。我第一次跑的时候看到平坦区域确实很快就停了复杂区域一路切到8x8跟理论完全对得上那种“原来如此”的感觉很爽。4.3 用调试器观察递归栈日志适合看整体流程但要看具体变量值还是得用GDB或者Visual Studio的调试器。在xCompressCU入口加断点然后单步执行观察partitioner.currArea()的变化tempCS和bestCS的指针地址bestCost的更新过程递归调用的深度我建议用一个非常小的序列比如64x64的一帧这样递归次数少容易跟踪。你可以手动记录每次递归的输入和输出画成一棵树这样对整个流程的理解会深刻很多。注意VTM的递归很深调试器里栈帧可能几十层别被吓到。从最外层开始一层一层往下看重点关注划分决策发生的那几层。4.4 修改划分逻辑做实验理解代码之后可以试着改点东西。比如禁用某种划分在splits列表里去掉CTU_HOR_SPLIT看看码率和质量怎么变修改代价函数把Lambda调大或调小观察划分倾向的变化加自定义剪枝比如“如果当前CU的SATD代价已经很低直接跳过划分”这些实验能帮你真正理解每个参数和逻辑的作用。我当初为了搞懂三分划分专门写了个配置只允许三分跑出来的结果跟预期一致那种验证感比看十遍代码都强。5. 常见问题与排查技巧实录5.1 递归不终止或者栈溢出这是新手最容易遇到的问题。原因通常是终止条件没写对或者状态恢复有问题导致无限递归。排查步骤检查partitioner的深度是否正确递增检查最小CU尺寸的判断是否生效检查isSplitAllowed是否在所有路径上都返回了正确结果我遇到过一次是因为改了maxMTTDepth但忘了同步改maxBTDepth导致二叉树划分一直往下切最后栈溢出。这种问题看日志最直接递归深度异常增长一眼就能看出来。5.2 划分结果跟预期不符有时候你会发现某个区域明明很平坦却切得很碎或者复杂区域反而没怎么切。可能的原因现象可能原因排查方法平坦区域过度划分Lambda太小码率权重低检查Lambda计算复杂区域不划分快速算法剪枝太激进关闭快速算法对比划分模式单一SPS配置限制了划分类型检查sps_btt_flag等边界CU异常padding或边界判断有bug打印边界CU的划分过程我建议先用全搜索模式跑一遍作为基准然后再开快速算法对比差异。这样能快速定位是算法问题还是配置问题。5.3 代价计算异常RD Cost出现负数或者异常大的值通常是Distortion计算溢出Rate计算包含了不该包含的比特Lambda为0或者未初始化这类问题在Debug模式下用断言最容易发现。VTM里有很多CHECK宏如果编译时开了断言异常值会直接报错。5.4 性能太慢跑不动全搜索的CU划分计算量巨大跑一个1080p序列可能要几个小时。加速方法用--fast相关的配置选项减小搜索范围比如限制最大CU尺寸只跑几帧不要跑完整序列用Profile工具找到热点函数针对性优化我通常调试时用416x240的序列跑10帧以内这样一次实验几分钟就能出结果。5.5 版本差异导致的代码对不上VTM版本更新很快不同版本的xCompressCU差异可能很大。比如VTM 5和VTM 10的划分逻辑就有明显区别。如果你看的教程和你的代码对不上先确认版本号。代码里的VTM_VERSION宏可以查。我的建议是选定一个版本就不要再换除非有明确需求。不同版本的函数名、参数、甚至整体架构都可能变来回切换只会增加困惑。6. 我个人在实际操作中的体会啃VTM代码这件事最忌讳的就是“从头到尾读一遍”。我试过读到一半就忘了前面效率极低。后来我换了个方法先跑起来再加日志再改代码最后才去读细节。先让编码器跑起来看它实际怎么划分然后加日志把递归过程打印出来然后试着改一个参数看结果怎么变最后才去读代码里对应的逻辑。这样每一步都有反馈理解得特别扎实。CU划分这块核心就是递归加代价比较。把xCompressCU的框架搞懂剩下的都是细节。细节可以慢慢补但框架一定要先建立起来。另外别怕改代码VTM是开源的改坏了重新编译就行。我当初为了搞懂三分划分把相关代码注释掉又加回来折腾了好几次但每次都有新收获。最后分享一个小技巧如果你觉得递归太绕可以自己用Python写一个简化版的CU划分模拟器把划分模式、代价函数、递归逻辑都实现一遍。不用考虑编码细节只关注划分决策。写完之后再回头看VTM的代码会发现清晰很多。这个模拟器我到现在还留着改算法的时候先用它验证思路没问题了再往VTM里移植省了很多时间。

相关新闻

VKL280段码液晶驱动选型与低功耗防静电设计实战

VKL280段码液晶驱动选型与低功耗防静电设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:41:11 阅读更多 →
Java集成Kettle数据同步:空字符串变NULL的根因与解法

Java集成Kettle数据同步:空字符串变NULL的根因与解法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:41:11 阅读更多 →
参数估计实战:从数学公式到工程落地的七道关卡

参数估计实战:从数学公式到工程落地的七道关卡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:40:10 阅读更多 →

最新新闻

LaTeX 2025环境配置:texlive + TeXstudio

LaTeX 2025环境配置:texlive + TeXstudio

文章目录Latex2025环境配置一、LaTeX 简介1.1 什么是 LaTeX?1.2 为什么使用 LaTeX?1.3 如何开始使用 LaTeX?二、texlive下载安装2.1 texlive下载2.2 texlive安装2.3 常见问题2.3.1 用管理员权限打开install-tl-windows.bat 报错2.3.2 Latex安…

2026/10/14 2:20:23 阅读更多 →
从工勘图到资源库:提交 nVisual Studio 与机房设计全生命周期的闭环

从工勘图到资源库:提交 nVisual Studio 与机房设计全生命周期的闭环

从工勘图到资源库:提交 nVisual Studio 与机房设计全生命周期的闭环摘要:工勘图纸的价值在「进入可检索、可复用资源库」之后才被放大——本地文件解决现场与交付,平台解决长期资产。DCDesigner 的「提交 nVisual Studio」把工勘 JSON 一键回…

2026/10/14 2:20:23 阅读更多 →
因果推断教材实战:从AB实验到双重机器学习与策略学习

因果推断教材实战:从AB实验到双重机器学习与策略学习

简介:这份资源是斯坦福大学Stefan Wager于2024年9月更新的《Causal Inference: A Statistical Learning Approach》教科书草稿,面向数据科学、统计学、经济学与公共政策领域的学习者和研究者,帮助其系统掌握基于统计学习方法的因果推断理论与…

2026/10/14 2:20:23 阅读更多 →
游集美Day1:逛完学村走老巷,被这几家街坊常来的小店拦住了脚步

游集美Day1:逛完学村走老巷,被这几家街坊常来的小店拦住了脚步

很多人来集美,打卡完嘉庚建筑、龙舟池,拍几张照片就匆匆返程。 但集美真正动人的烟火,不在网红打卡点,藏在黄昏的社区老巷。 这条路线我按地理位置理顺,不跨区乱跑,骑行顺路,慢逛无赶路压力&…

2026/10/14 2:20:23 阅读更多 →
系统辨识实战教案:M序列设计、RLS实现与工业白化检验

系统辨识实战教案:M序列设计、RLS实现与工业白化检验

简介:本资源是一份面向自动化专业本科生与研究生的《系统辨识》课程教案,聚焦工程建模核心能力培养,解决从实际系统数据中构建可用数学模型的关键问题。教案覆盖实体与模型本质辨析、Zadeh三要素(数据/模型类/准则)理论…

2026/10/14 2:20:23 阅读更多 →
AI 辅助科研溯源闸门:输入·命令·决策·证据四类记录的失败含义与当天最小实验

AI 辅助科研溯源闸门:输入·命令·决策·证据四类记录的失败含义与当天最小实验

千笔-AIWritePaper https://www.aiwritepaper.com Agent 能很快从原始表跑出一张宇宙学风格的斜率图,不等于结果可审查。缺了输入校验和,文件可以被静默替换;命令行事后改参,YAML 里仍写着旧参数;决策条目没有理由&am…

2026/10/14 2:19:23 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →