4. 变换与量化:从频域视角看压缩
变换与量化,说白了就是视频编码里「丢信息」最核心的两个环节。很多刚入行的同学总觉得这一步就是算算DCT、除个系数,没什么技术含量。嗯,我当年也这么想,直到第一次调编码器时发现——同样的码率,画面质量能差出一大截。
这一章我们就把DCT变换、整数实现、量化参数和率失真优化这几个点串起来讲。你会发现,它们其实是一套完整的「信息取舍」策略。
4.1 DCT变换原理:为什么是频域?
先问一个问题:为什么非要把像素值转到频域?
答案其实很简单——人眼对高频细节不敏感。图像里大部分能量都集中在低频区域,高频部分往往可以丢掉而不影响主观质量。DCT(离散余弦变换)就是干这个的:把空间域的像素块变成频域的系数块。
以H.264/HEVC里最常用的4x4 DCT为例,变换公式长这样:
Y = A * X * A^T
其中 A 是变换矩阵,A[i][j] = C(i) * cos((j + 0.5) * π * i / N)
C(0) = sqrt(1/N),C(i>0) = sqrt(2/N)
我个人习惯把DCT理解成「能量集中器」。一个平坦的4x4块,经过DCT后,左上角的DC系数占了总能量的90%以上。剩下的AC系数,很多都接近0。
关键结论:DCT本身不压缩数据,它只是把信息重新排列,让后续的量化能更高效地丢掉不重要的部分。
4.2 整数DCT实现:浮点转整数的坑
理论上的DCT是浮点运算,但编码器里不可能用浮点——速度太慢,而且不同平台结果不一致。所以实际编码器都用整数DCT。
整数DCT的核心思路是:把浮点变换矩阵放大、取整,然后在反变换时缩回去。以H.264的4x4整数DCT为例:
// 变换矩阵(放大后取整)
int A[4][4] = {
{1, 1, 1, 1},
{2, 1, -1, -2},
{1, -1, -1, 1},
{1, -2, 2, -1}
};
// 正变换:Y = A * X * A^T
// 反变换:X' = A^T * Y' * A
// 注意:正反变换后需要右移补偿缩放因子
这里有个坑——我曾经在移植编码器到ARM平台时,发现反变换后的像素值总是偏大。查了半天,原来是右移补偿的位数算错了。整数DCT的缩放补偿必须严格匹配,差一个bit画面就花了。
避坑指南:整数DCT的精度取决于变换矩阵的位数和右移策略。H.264的4x4 DCT使用6位右移,HEVC的4x4 DCT使用7位。不要随意改,否则会出现漂移误差。
4.3 量化参数与量化矩阵
量化才是真正丢信息的地方。DCT系数除以量化步长再取整,高频系数被砍成0,这就是压缩的本质。
量化参数QP是编码器里最常用的控制手段。QP每增加6,量化步长翻倍,码率大约减半。但画面质量也会明显下降。
| QP值 | 量化步长 | 典型码率(1080p) | 主观质量 |
|---|---|---|---|
| 0 | 1 | ~50 Mbps | 无损/近无损 |
| 18 | ~4 | ~20 Mbps | 视觉无损 |
| 28 | ~16 | ~8 Mbps | 良好 |
| 38 | ~64 | ~2 Mbps | 可接受 |
| 51 | ~256 | ~0.5 Mbps | 明显块效应 |
量化矩阵则是更精细的控制。不同频率的系数用不同的量化步长——低频用细量化,高频用粗量化。JPEG里就有默认的量化表,视频编码标准里也支持自定义量化矩阵。
我的经验:在低码率场景下(比如视频会议),我习惯把高频量化步长再加大20%-30%。虽然PSNR会掉一点,但主观上画面更干净,没有那种「高频噪声」的颗粒感。
4.4 率失真优化基础:编码器的「经济学」
率失真优化(RDO)是编码器里最「聪明」的部分。它解决的是一个问题:在给定的码率下,如何让失真最小?
说白了,就是做选择题。编码器有无数种方式去编码一个块——不同的预测模式、不同的变换大小、不同的量化参数。RDO帮我们选出「性价比」最高的那个。
经典的率失真代价函数:
J = D + λ * R
其中:
J = 率失真代价(越小越好)
D = 失真(通常用SSD或SAD)
R = 码率(编码该块需要的比特数)
λ = 拉格朗日乘子(控制码率和失真的权衡)
λ的取值很关键。λ越大,编码器越倾向于省码率(哪怕失真大一点);λ越小,编码器越倾向于保质量(哪怕多花点码率)。
我记得在做一个移动端编码器项目时,发现默认的λ公式在低码率下表现很差——画面全是模糊的块。后来我把λ乘以一个0.8的系数,主观质量立刻上来了。代价是码率多了15%,但用户反馈好太多了。
核心思路:RDO不是追求「绝对最优」,而是在编码复杂度可接受的前提下,找到「足够好」的编码方案。实际编码器里,RDO只对部分模式做全搜索,其他模式用快速算法近似。
4.5 变换与量化的整体流程
把上面几个点串起来,一个典型的变换量化流程是这样的:
- 残差生成:原始块减去预测块,得到残差块
- DCT变换:残差块做整数DCT,得到频域系数
- 量化:系数除以量化步长(可能带量化矩阵),取整
- 扫描与熵编码:量化后的系数按Zig-zag顺序扫描,然后做熵编码
- 反量化与反变换:解码端重建残差,用于后续帧的参考
这个流程里,每一步都有调优空间。比如DCT可以用快速算法(蝶形变换),量化可以用死区量化(Dead-zone)来减少小系数的编码开销,扫描顺序可以根据系数分布动态调整。
4.6 实战中的调优建议
最后分享几个我在项目中积累的经验:
- QP不要全局统一:纹理复杂的区域用更细的量化(低QP),平坦区域用更粗的量化(高QP)。这叫自适应量化,几乎所有商用编码器都支持。
- 量化矩阵不是摆设:默认矩阵是通用的,但针对特定内容(比如屏幕内容、监控视频)定制量化矩阵,能省10%-20%的码率。
- RDO的λ要跟码率控制联动:很多编码器把λ设成QP的固定函数,但实际场景下,λ应该根据缓冲区状态动态调整。
- 小心整数精度:整数DCT的反变换必须做饱和处理(clip到像素范围),否则会出现颜色溢出。我曾经见过一个bug,反变换后像素值跑到300多,画面直接花了。
一句话总结:变换和量化是编码器里「性价比」最高的调优点。花一周时间调好量化策略,效果可能比花一个月改运动估计还要明显。