3. H.264编码原理(下):变换与量化、熵编码(CAVLC/CABAC)、去块效应滤波
好,我们接着往下聊。上一节我们把预测和帧内帧间搞清楚了,说白了就是“怎么猜像素”。但猜完了总得有误差吧?这个误差怎么处理,才是今天要聊的重头戏。
我个人习惯把编码器的后半段看作一个“压缩漏斗”——变换、量化、熵编码,一步比一步狠。你想想看,预测残差里还藏着大量空间冗余,不收拾它,码流根本压不下去。
3.1 变换与量化:把能量集中起来
预测做完之后,我们拿到的是残差块。这个块里数值有大有小,但有个特点——相邻像素的残差往往差不多。换句话说,能量分散在整个块里。
变换的目的,就是把能量从空间域转到频率域。低频分量集中到左上角,高频分量散到右下角。这样后面量化的时候,一刀切掉高频,人眼还不太敏感。
3.1.1 整数DCT变换
H.264用的是4×4整数DCT,不是传统的浮点DCT。为什么?因为浮点运算在不同平台上结果不一致,解码端和编码端对不上,那就乱套了。
整数DCT的好处是:
- 无漂移:正反变换完全匹配,不会累积误差
- 低复杂度:只用加减法和移位,没有乘法
- 块效应更小:4×4块比8×8块更贴合图像边缘
我记得第一次在ARM平台上移植H.264编码器时,最头疼的就是浮点精度问题。换成整数DCT后,问题迎刃而解。嗯,这里要注意:整数DCT的系数是固定的,查表就能拿到,不用实时算。
核心公式(简化版):
Y = Cf * X * Cf^T
其中 Cf 是整数变换矩阵,X 是4×4残差块
变换后的系数Y,左上角是DC分量,右下角是高频细节。
3.1.2 量化:真正的“有损”环节
量化才是真正丢信息的地方。变换后的系数除以量化步长(QP),再取整。QP越大,丢得越狠,码率越低,画质越差。
H.264的量化有个巧妙设计——它把除法和乘法结合到一起,用查表和移位来实现。52个QP值对应不同的量化步长,QP每增加6,步长翻倍。
| QP值 | 量化步长 | 典型应用场景 |
|---|---|---|
| 0 | 0.625 | 无损/近无损 |
| 12 | 2.5 | 广播级质量 |
| 24 | 10 | 高清视频通话 |
| 36 | 40 | 移动端短视频 |
| 51 | 224 | 极低码率监控 |
我曾经踩过的坑:量化矩阵(Scaling Matrix)在H.264里是可选的,但很多编码器默认不用。如果你做的是专业级编码器,建议加上——它能针对不同频率的系数做差异化量化,主观质量能提升一截。
3.2 熵编码:把符号变成比特流
量化之后,我们得到了一堆系数。这些系数里有很多0,尤其是高频区域。怎么把这些0和少数非0系数高效地写成二进制?这就是熵编码的事。
H.264支持两种熵编码方式:CAVLC和CABAC。选哪个,直接影响压缩率和复杂度。
3.2.1 CAVLC:基于上下文的自适应变长编码
CAVLC是H.264的基线方案。它专门为量化后的4×4块设计,利用了三个特点:
- 非零系数集中在低频区(左上角)
- 非零系数绝对值大多为1
- 拖尾系数(±1)的数量有规律
编码过程分五步:
- 编码非零系数个数和拖尾系数个数(查表)
- 编码每个拖尾系数的符号(0正1负)
- 编码剩余非零系数的幅值(Level)
- 编码最后一个非零系数前0的个数(TotalZeros)
- 编码每个非零系数前的0的个数(RunBefore)
说实话,CAVLC的查表逻辑有点绕。我建议你直接看标准里的Table 9-5到9-7,对着码流走一遍就明白了。
小技巧:CAVLC的码表是固定的,但上下文会自适应——比如相邻块的非零系数个数会影响当前块的码表选择。这就是“上下文自适应”的含义。
3.2.2 CABAC:基于上下文的自适应二进制算术编码
CABAC比CAVLC复杂得多,但压缩率能提升10%~15%。它不再是查表编码,而是用算术编码器把符号压缩到小数比特里。
CABAC的核心三要素:
- 二进制化:把非二进制语法元素转成二进制串(如指数哥伦布码)
- 上下文建模:每个bin(二进制位)都有一个概率模型,根据相邻信息动态更新
- 算术编码:把整个序列压缩成一个区间,区间越小,比特越少
我刚开始接触CABAC时,觉得这东西太玄乎了。后来自己手写了一个简化版算术编码器,才明白它本质上就是“不断缩小区间”的过程。你想想看,概率高的符号占的区间大,需要的比特就少——这不就是信息论嘛。
CAVLC vs CABAC 对比:
| 特性 | CAVLC | CABAC |
|---|---|---|
| 压缩率 | 基准 | 高10%~15% |
| 复杂度 | 低 | 高(约2倍) |
| 适用Profile | Baseline/Extended | Main/High |
| 硬件实现 | 简单 | 需要流水线优化 |
3.3 去块效应滤波:让画面更“干净”
块效应是块编码的“原罪”。每个4×4或16×16块独立编码,边界上的像素因为量化误差不一致,就会出现肉眼可见的方块边界。
去块效应滤波(Deblocking Filter)就是在解码后、显示前,对这些边界做平滑处理。H.264的滤波器是自适应的——它根据块类型、QP、边界强度来决定要不要滤波、滤多强。
3.3.1 滤波边界与强度
滤波器只处理4×4块边界。每个边界有一个边界强度(Bs),取值范围0~4:
- Bs=0:不滤波(比如两个块都用帧内预测且QP很小)
- Bs=1~3:弱滤波(运动补偿残差边界)
- Bs=4:强滤波(帧内预测块边界)
滤波时,会判断边界两侧像素的差异是否超过阈值。如果差异太大,说明是真实边缘,不滤;如果差异小,说明是块效应,滤掉。
我曾经踩过的坑:去块效应滤波是解码器里最耗计算量的模块之一。如果你在做实时解码,千万别对每一帧都全图滤波。可以只对QP较大的区域滤波,或者用SIMD指令并行处理。我当年在DSP上优化时,把滤波循环展开后,速度提升了3倍。
3.4 本章知识体系总览
下面这张图把变换、量化、熵编码、去块效应滤波的流程串起来了。你可以把它当作编码器后半段的“地图”。
从图上你能看到,整个流程是串行的。残差块经过变换、量化、扫描后,进入熵编码生成码流。同时,量化后的系数还要反量化、反变换,加上预测值,再经过去块效应滤波,才能作为下一帧的参考图像。
这个环路设计是H.264的精髓——编码器和解码器必须保持完全一致的参考图像,否则误差会越滚越大。我在做码流分析工具时,就遇到过编码器和解码器滤波边界强度计算不一致导致的漂移问题,查了整整两天才找到原因。
好了,这一章的内容就到这儿。变换量化、熵编码、去块效应滤波,这三块是H.264编码效率的关键。理解了它们,你就掌握了H.264压缩的“核心引擎”。