1. 从“猜硬币”到视频压缩为什么需要CABAC想象一下你和朋友在玩一个猜硬币正反面的游戏。如果这枚硬币是绝对公平的正反面概率各50%那么你每次猜对后能获得的信息量是固定的。但如果我告诉你这枚硬币被动了手脚出现正面的概率高达80%反面只有20%。这时当你猜“正面”并且猜对时这件事带给你的“惊喜感”或信息量就变小了因为它本来就很容易发生。信息论中的香农熵告诉我们一个事件发生的概率越高它所携带的信息量就越低。视频数据本质上就是一长串由“0”和“1”组成的比特流。在未经压缩的原始视频里每个像素值、每个运动矢量出现的概率看起来都差不多信息分布比较“均匀”。但当我们用上各种视频编码技术比如H.264/AVC、H.265/HEVC后情况就大不一样了。经过预测、变换、量化等一系列操作我们得到的残差系数、运动矢量差值等语法元素它们的数值分布会呈现出极强的偏斜性。简单说就是某些值比如0或者接近0的小数值出现的概率极高而其他大数值出现的概率极低。这时候如果还用那种给所有符号分配固定长度码字的传统编码方式比如哈夫曼编码就太“浪费”了。这就好比用同样大小的箱子去装一颗钻石和一车棉花效率极低。我们需要一种能“察言观色”的编码器它能根据当前编码符号的实际概率动态分配最短的码字。这就是二进制算术编码Binary Arithmetic Coding的核心思想概率高的符号用更短的区间表示概率低的符号用稍长的区间表示。最终整个数据流被编码成一个很短的小数区间实现极高的压缩效率。而CABACContext-based Adaptive Binary Arithmetic Coding即基于上下文的自适应二进制算术编码就是将这个思想发挥到极致的产物。它不仅仅是算术编码更关键的是“基于上下文”和“自适应”。在视频编码这个复杂战场上CABAC就像一位经验丰富的老兵不仅能根据全局统计调整策略更能根据眼前的“战况”上下文信息实时调整对下一个“敌人”语法元素出现概率的判断从而实现近乎极限的压缩。接下来我们就一层层剥开CABAC这颗“洋葱”看看它到底是如何工作的。2. 庖丁解牛CABAC的三大核心组件CABAC的高效并非魔法而是建立在三个环环相扣的精密组件之上二值化、上下文建模和二进制算术编码。理解这三者的协作是弄懂CABAC的关键。2.1 第一步万物皆可“0/1”——二值化视频编码中需要压缩的语法元素五花八门有表示“这个宏块是否跳过”的mb_skip_flag一个0或1有表示预测模式的mb_type可能有20多种类型还有变换后的残差系数可以是-1024到1023之间的任何整数。二进制算术编码器是个“偏食”的家伙它只吃“0”和“1”这两种“食物”。所以我们的第一步就是要把所有非二进制的语法元素通过一套规则转换成由“0”和“1”组成的二进制串这个过程就叫二值化Binarization。CABAC中常用的二值化方案有好几种针对不同类型的语法元素择优使用一元码Unary Code适合小数值、概率递减的元素。比如数字3一元码是“1110”三个1后跟一个终止0。数值越大码字越长。截断一元码TU在一元码基础上对超过最大值的数进行截断处理更高效。指数哥伦布码EGk非常适合对概率分布不太确定的大数值进行编码比如残差系数中那些不常见但可能很大的数值。它由前缀和后缀组成能自适应地调整码长。定长码FL当语法元素所有取值概率几乎相同时使用比如某些特定的标识位。K阶指数哥伦布码这个在HEVC里用得非常多是EG码的泛化通过参数k调整能更好地匹配不同概率分布的数据。我刚开始接触时总想为什么不直接用算术编码处理整数后来在实战中才明白二值化这一步不仅仅是格式转换它已经根据语法元素的统计特性进行了一次“预压缩”为后续自适应编码打下了最优的基础。选择哪种二值化方案本身就是对数据概率分布的一种先验知识应用。2.2 第二步猜猜下一个是什么——上下文建模这是CABAC的灵魂所在也是“Context-based”一词的由来。二值化后我们得到了一串比特流。对于每一个即将被编码的比特称为binCABAC不会盲目地用一个固定的概率去编码它而是会先看看“周围的情况”来预测这个比特是0还是1的概率更大。这个“周围的情况”就是上下文Context。它主要包括语法元素本身的类型是运动矢量差值还是残差系数不同类型的元素0/1的分布规律天差地别。该比特在二值化串中的位置是第一个比特决策性最强的比特还是后面的后缀比特通常前缀比特如一元码的前几个1具有更强的相关性。相邻已编码块的信息在视频中空间和时间上的相关性极强。当前块上方和左侧的块是否编码它们的编码结果是什么这些信息对于预测当前块的信息非常有效。例如如果左边和上边的块都采用了帧内预测那么当前块也很可能采用帧内预测。CABAC会综合这些上下文信息计算出一个上下文索引ctxIdx。这个索引值就是去查一张庞大的概率模型查找表Look-up Table的地址。这张表里为每一个可能的上下文状态都预先存储或动态维护着一对概率值MPSMost Probable Symbol大概率符号可能是0或1的概率以及LPSLeast Probable Symbol小概率符号的概率。举个例子在编码一个帧间预测块的mvd_x运动矢量水平分量差值的第一个bin时CABAC可能会根据左边块和上边块的mvd_x是否为零来决定查表的地址。如果相邻块都没运动那么当前块没运动的概率也高此时“该差值为0”对应的MPS概率就会被设置得很高。编码器就用这个动态获取的概率去进行算术编码精度远高于使用全局平均概率。2.3 第三步精打细算的区间分配——二进制算术编码引擎这是执行压缩的“肌肉”。拿到了当前bin的MPS概率比如0.9和LPS概率0.1后算术编码器就开始工作了。它维护着一个当前编码区间[L, LR)其中L是区间下界R是区间长度初始为1.0或一个整数值如510。编码过程可以想象成不断细分一条长度为R的线段如果当前要编码的bin是MPS那么新的区间就取原区间靠前的MPS概率 * R那么长的一段。如果编码的是LPS就取靠后的LPS概率 * R那么长的一段。每编码一个bin区间R就会缩小到原来的MPS或LPS概率那么大。随着编码的进行区间R会变得越来越小精度要求越来越高。为了用有限精度的整数进行高效运算避免浮点数CABAC采用了一种巧妙的区间归一化Renormalization技术。当区间R小到一定程度比如小于256编码器就会将其扩大一倍左移一位同时检查区间下界L是否“溢出”。如果L也超过了表示范围就会输出相应的比特到码流中。这个过程保证了整个编码过程在固定精度的整数运算下稳定进行同时一边编码一边输出比特无需等待全部编完。解码器是编码器的镜像。它同样维护一个区间并从码流中读取一个偏移值Offset。根据当前上下文查到的概率模型它判断Offset落在当前区间的MPS子区间还是LPS子区间从而解码出一个bin。然后它执行和编码器完全相同的区间细分和归一化操作保持同步。正是这种精妙的对称性使得解码得以准确进行。3. CABAC为何高效深入对比与实战优势理解了原理我们再来看看CABAC在实际视频编码标准如H.264/AVC和H.265/HEVC中相比之前的熵编码方案如CAVLC或普通的算术编码到底强在哪里。3.1 与CAVLC的对比从“查字典”到“自适应造句”在H.264的Baseline Profile里熵编码主要使用CAVLC上下文自适应变长编码。CAVLC本质上是一种改进的变长编码它根据周围块的非零系数个数、拖尾系数情况等上下文为当前的残差系数块从几张预定义的码表中选一张最合适的来查表编码。你可以把CAVLC想象成一个拥有多本“短语词典”的压缩方式。根据上下文选对词典就能用较短的码字表示常见的系数组合模式。但它有两个固有局限码表是静态的虽然有多张表可选但这些表是在标准制定时基于典型序列统计设计好的无法适应千变万化的实际视频内容。编码单元是“组合”它是对一组系数如游程-电平对进行联合编码虽然考虑了相关性但灵活性不如对每个二进制决策单独编码。而CABAC则像是“自适应造句”。它不查固定短语词典而是从“字母”0/1比特开始根据刚刚写过的“上文”上下文动态决定下一个“字母”该用什么最短的“笔画”区间来表示。这种对概率模型的实时、细粒度自适应让CABAC能更紧密地贴合视频数据的实际统计特性从而获得比CAVLC高10%-20%的压缩效率提升。这也是为什么H.264的Main和High Profile支持CABAC比Baseline Profile压缩率高出不少的关键原因。3.2 自适应性的威力概率模型的更新CABAC的自适应不仅体现在上下文选择上更体现在概率模型的持续更新上。每个上下文模型由ctxIdx指向不仅存储着当前的MPS值和LPS概率估计值还会在编码/解码每一个bin后根据这个bin的实际值来更新这个概率估计。如果编码了一个LPS那么下次再遇到相同上下文时LPS的概率估计值就会被稍微调高一点但通常不会超过0.5否则MPS和LPS就会交换角色。这个更新过程通常使用一个有限状态机FSM概率值被离散化为几十个或几百个状态更新就是在这些状态间转移。这种机制使得CABAC能快速跟踪视频局部统计特性的变化。比如从一个平滑背景区域切换到复杂纹理区域残差系数的概率分布会迅速变化CABAC的概率模型能在几十个bin内就完成调整始终以接近最优的概率进行编码。这种“学习”能力是静态码表完全不具备的。3.3 整数运算与硬件友好设计你可能注意到前面的描述提到了区间用[0, 510]这样的整数表示。CABAC的整个算术编码核心被设计为使用整数运算避免浮点运算这对于在嵌入式设备、DSP或专用硬件ASIC上实现至关重要。区间R和概率pLPS都用查表或移位、加法等简单操作来近似在保证编码精度的同时极大提升了运算速度。此外CABAC的编码流程具有很强的顺序性和确定性。编码下一个bin所需的上下文完全取决于之前已编码的语法元素和bin值。这种设计虽然不利于并行化这是CABAC的一个缺点也是后来一些研究试图优化的方向但使得编解码器的状态非常清晰易于实现和调试。在硬件流水线设计中可以通过精心安排来缓解串行依赖带来的吞吐量问题。4. 在H.264与H.265/HEVC中的演进与实战解析CABAC并非一成不变从H.264到H.265/HEVC再到更新的VVC其设计也在不断演进以追求更高的压缩效率和实现友好性。4.1 H.264中的CABAC实现在H.264中CABAC是作为高级熵编码工具引入的。它对绝大多数语法元素进行编码包括片头信息slice header宏块类型mb_type预测模式pred_mode参考帧索引ref_idx运动矢量差值mvd残差系数coeffH.264 CABAC的设计已经非常成熟。它定义了数百个不同的上下文模型ctxIdx。初始化时这些模型的初始概率状态MPS是0还是1LPS概率状态索引会根据一个slice_qp量化参数查表获得因为QP反映了图像的粗略质量与系数分布有一定相关性。在实际编码一个I帧帧内编码和P帧帧间编码时CABAC的行为差异很大。I帧的残差系数多且经过帧内预测后系数分布有特定规律如DC系数大AC系数衰减快其上下文模型的设计会利用这些规律。P帧则会有大量的运动矢量差值和mb_skip_flag等语法元素这些元素的概率分布在场景静止或运动剧烈时差异巨大CABAC的自适应性在这里大显神威。4.2 H.265/HEVC中的增强与优化H.265/HEVC在H.264的基础上对CABAC进行了多方面的强化以支持更灵活的块划分CTU最大到64x64和更多的预测模式。更多的上下文模型HEVC的上下文模型数量大幅增加超过300个对语法元素的划分更细致。例如对变换系数的编码会根据系数在块中的位置是否在低频或高频区域、相邻系数的状态等使用更多不同的上下文使得概率估计更精准。初始化过程的简化HEVC摒弃了H.264中依赖QP的复杂初始化表改为使用一个基于切片类型I/P/B和模型类型的固定初始化值。这简化了实现同时通过更精细的上下文模型设计弥补了适应性。概率状态转移表的优化HEVC使用了新的、更平滑的概率更新状态转移表使得概率估计的收敛速度和对统计特性变化的跟踪能力更加平衡。针对并行处理的改进虽然CABAC本质串行但HEVC在语法结构上做了努力比如引入波前并行处理WPP。它将一帧图像分成多个编码树单元CTU行每一行可以独立初始化CABAC引擎基于上一行第二个CTU的上下文从而实现行级并行提升了多核CPU上的编码速度。我曾参与过一个基于HEVC的嵌入式编码器优化项目。最初使用CAVLC时码率控制波动较大。切换到CABAC后在相同主观质量下码率平均下降了约15%。但挑战也随之而来CABAC的计算复杂度更高尤其是上下文模型的存取和更新成了性能瓶颈。我们通过将频繁访问的概率模型表放置在芯片的紧耦合内存TCM中并优化状态更新逻辑最终将CABAC模块的耗时降低了近40%。这个经历让我深刻体会到CABAC的高效率是以计算复杂度为代价的在工程实现上需要精心优化。5. 超越理论CABAC的工程实现考量与“踩坑”经验看懂原理是一回事把它高效、稳定地实现出来是另一回事。在工程实践中围绕CABAC有几个需要特别注意的“坑”。5.1 精度与溢出整数运算的边界守护CABAC使用有限精度的整数区间如9比特表示区间R范围[256, 510]。在区间细分和归一化过程中所有运算都必须保证在整数域内精确且不发生溢出。例如计算R_LPS (pLPS * R) 精度时pLPS是查表得到的近似概率值这个乘法运算的中间结果可能需要更高位宽的临时变量来存储。解码端的“Offset”同样需要小心维护。在归一化过程中不仅R要左移Offset也要左移并从码流中补入新的比特。这里必须确保比特流的读取与编码端的输出严格同步任何一位的错误都会导致后续所有解码结果“雪崩式”错误。在调试时我们经常逐比特比对编码输出和解码器的Offset值这是定位问题的关键。5.2 上下文模型的存取与同步编解码双方必须保持上下文模型的完全同步。这意味着上下文索引ctxIdx的计算规则必须绝对一致。任何依赖于平台或编译器的未定义行为如移位、取模都必须消除。概率模型的初始化值必须相同。概率更新状态机FSM的转移必须完全一致。通常标准会提供一个确定的查表更新法必须严格遵循。在分布式编码或容错应用中如果发生数据包丢失解码器必须知道如何重置或重新同步上下文模型否则错误会持续扩散。通常在每一个片Slice的开头所有上下文模型都会被重置为初始状态这就是一个天然的同步点。因此在实时通信中更频繁地插入I帧或独立片有助于提高抗误码能力但会牺牲一些压缩效率。5.3 性能优化实战技巧CABAC是视频编码器中的热点模块优化其性能至关重要。查表优化概率状态pLPS和下一个状态的转移TransIdxLPS,TransIdxMPS都可以预先计算成表。将这些表放在高速缓存Cache友好的内存区域能极大提升访问速度。有时甚至可以将几个相关的小表合并成一个大表通过一次内存访问获取多个所需值。分支预测优化CABAC编码循环中有大量的if-else判断判断bin是MPS还是LPS判断区间R是否需要归一化等。这些分支如果预测失败对CPU流水线性能影响很大。可以通过概率统计将MPS路径发生概率高作为快速路径进行优化或者尝试用条件移动CMOV等指令减少分支。比特流写入优化归一化过程中的比特输出不要每次输出一个比特就调用一次写比特流函数这样开销巨大。通常的做法是维护一个字节Byte缓冲攒够8个比特再一次性写入。这需要仔细处理缓冲区的边界和最后的刷新Flush操作。并行化探索如前所述CABAC本质串行但可以在更高层级并行。除了HEVC的WPP还可以考虑片级并行每个Slice独立编码或者使用“旁路Bypass模式”。对于某些概率接近0.5、编码增益不高的binCABAC可以切换到旁路模式直接用等概率0.5进行算术编码无需上下文建模和更新。旁路模式的bin之间没有依赖可以批量处理为SIMD指令优化提供了可能。在我做过的一个移动端编码器项目中我们发现CABAC模块占用了超过30%的编码时间。通过将关键的概率模型表从DDR内存搬到片上SRAM并重写比特流输出缓冲逻辑我们最终在ARM Cortex-A系列处理器上获得了近一倍的编码速度提升。这让我明白对于CABAC这样的核心算法算法层面的理解必须结合硬件架构的特性才能发挥其最大威力。它就像一台精密的机械钟表每一个齿轮模块都必须严丝合缝并且选用合适的材料硬件优化才能走得准、走得久。