☰
整数DCT变换与量化:视频压缩省算力、防误匹配的关键
2026/11/4 9:34:00 网站建设 项目流程

简介:这份文档面向视频编码学习者、算法实现者与相关工程人员,围绕H.264标准中的整数DCT变换编码与量化过程展开,帮助读者理解4×4块无乘法整数变换的推导、实现及其在压缩流程中的作用,并厘清反变换误匹配等疑点。压缩包内仅含1个doc文件,大小约194KB,以专题文章形式集中呈现,便于阅读与留存。目前已有880人学习。文档结合公式与编码器流程,分析整数DCT正反变换、基于提升结构的无乘法二进制DCT(BinDCT)仅用加法与16位算术移位的实现思路,并说明亮度、色度DC系数分别进行4×4与2×2 Hadamard变换的组织方式,以及量化如何与变换中的点乘合并。读者可据此掌握H.264变换编码的关键环节,理解其降低运算复杂度、避免编解码不匹配并提升压缩效果的原因,适合作为课程学习或算法实现的参考。

1. 一个词先讲明白:整数 DCT 变换到底在视频里干什么

把一段视频拆成一帧帧画面,再拿每一帧和它的“预测版本”相减,剩下的东西叫残差——也就是画面里没被猜中的那部分。这部分数据看着零散,其实高度重复:相邻像素的亮度往往只差一点点。整数 DCT 变换干的事,有点像收拾行李箱:把散落在各个像素上的能量归拢到少数几个格子里,剩下的格子大多是接近零的小数,随手就能丢掉。区别在于,这套变换只用整数加减和移位,不碰浮点乘法。为什么非要这么抠?因为编码器和解码器是两台完全独立的机器,它们必须对同一块数据算出分毫不差的结果,差一位都可能在画面上滚成一片雪花。搞懂这套变换和紧跟其后的量化,视频压缩里“省算力”和“算得准”这两件事就同时说通了。

2. 为什么要跟浮点数过不去:整数 DCT 变换省下的三笔账

早一代的视频编码标准里,变换这一步是老老实实按 DCT 的定义算的:把 8×8 或 4×4 的像素块乘上一个由余弦值构成的变换核。余弦值是实数,实数在计算机里只能近似表示,于是浮点运算就成了绕不开的一环。这套做法在数学上很漂亮,可一旦落到工程里,就变成三笔怎么算都不划算的账。整数 DCT 变换的出现,本质上不是为了追求更“高级”的数学,而是把这笔账重新记了一遍。

2.1 浮点 DCT 的两个硬伤

第一个硬伤是运算复杂度。浮点乘法、浮点加法、浮点舍入,在通用处理器上或许还能忍,放到专用芯片、嵌入式设备、需要实时编码的场合就很要命。一个 4×4 块看着小,可一帧 1080p 画面里这样的块有几十万个,每块都要做正变换、量化、反量化、反变换,乘法的次数被放大到一个惊人的量级。

第二个硬伤更隐蔽,也更致命:编解码不匹配。变换核里全是无理数,而有限精度的浮点数不可能精确表示无理数,运算过程中还会不断引入舍入误差。编码器算出的反变换结果,和解码器算出的反变换结果,可能就差那么一点点。这个差值本身很小,问题是它会进入参考帧,被后续的帧拿去当预测依据,误差于是逐帧累积、慢慢漂移。打个通俗的比方,两个人各拿一把刻度略有差别的尺子去量同一块布,量完再互相对账,永远对不上。这类问题在工程上被称为“误匹配”,一旦发生,画质就是不可逆地一路下滑——它不像参数调错那样还能改回来,属于典型的没有后悔药。

2.2 加减法与移位怎么顶替乘法

整数 DCT 变换的核心一招,是把变换核里的数全部换成 1、2、-1、-2 这种整数。它的一维变换矩阵长这样:

[ 1 1 1 1 ] [ 2 1 -1 -2 ] [ 1 -1 -1 1 ] [ 1 -2 2 -1 ]

矩阵里最大只有 2,乘以 2 就是左移一位,乘以 -1 就是取负,剩下的全是加法。整个变换因此可以用 16 位整数运算完成,一次乘法都不需要。这类结构在文献里被称为基于提升结构的无乘法二进制 DCT,名字很长,说白了就是“把乘法拆成加法和移位”的一种固定套路。

提示:这里的“整数 DCT”已经不是严格意义上的 DCT 了,它只是由 DCT 推导出来的一个近似。标准里仍然沿用它名字里的 DCT,主要是为了跟另一个变换(Hadamard 变换)区分开,两者在同一个编码流程里都会用到。

一维变换写成代码是这样的:

# H.264 一维整数变换的蝶形实现,输入输出都是 16 位整数 def butterfly_1d(x0, x1, x2, x3): a0 = x0 + x3 # 中间结果先存下来,后面两个输出都要用 a1 = x1 + x2 a2 = x1 - x2 a3 = x0 - x3 y0 = a0 + a1 # 对应矩阵第一行 [1, 1, 1, 1] y1 = (a2 << 1) + a3 # 乘 2 用左移,对应 [2, 1, -1, -2] y2 = a0 - a1 # 对应 [1, -1, -1, 1] y3 = a3 - (a2 << 1) # 对应 [1, -2, 2, -1] return y0, y1, y2, y3

这段函数做的事,就是把四个输入按上面的矩阵线性组合成四个输出。<< 1是左移一位,等价于乘 2;a0到a3这四个中间量被反复复用,所以整个函数只用了 8 次加法和 2 次移位,没有一次乘法。参数x0到x3是一行或一列的四个残差像素值,返回值是对应的四个变换系数。之所以强调“16 位”,是因为残差本身的范围有限,这套运算在设计上保证中间结果不会溢出 16 位整数。

2.3 把归一化甩给量化:尺度 DCT 的思路

光把乘法换掉还不够。严格的 DCT 是正交变换,算完要乘一个归一化系数,让能量守恒。整数版本如果老老实实做归一化,又会引入 1/2 这样的小数,前面的努力就白费了。

标准的处理办法很巧妙:把 1/2 提到矩阵外面,跟右边的点乘操作合并,一起丢给后面的量化环节。变换本身不做任何归一化,只负责把能量重新分布;缩放这件事,交给量化去一次性完成。这种做法通常叫尺度 DCT 思想,听起来玄,其实就是“该算的账不赖账,只是换个地方结”。

反变换那头也一样。反变换公式里需要乘 1/2,标准直接用右移一位来实现。有人会担心右移会不会截断出误差——这里不会,因为反量化之后的数据量级已经足够大,右移丢掉的那一位远小于有效精度,落不到画面上。正变换把归一化推给量化,反变换用移位把归一化补回来,一进一出,账目两清。

3. 一个 4×4 小块里的完整走位:从残差到系数

知道了“为什么用整数”,接下来看“具体怎么走”。一个 4×4 的残差块从进入变换到产出系数,中间要经过一维拆分、蝶形复用、直流系数单独处理这几道工序。每一步都在省算力,每一步也都在保证结果可复现——这两件事本来就是同一件事的两面。

3.1 蝶形算法:为什么加法次数能从 12 次降到 8 次

二维变换没必要硬算。标准采用的做法是把它拆成两次一维变换:先对每一列做一次一维变换,再对结果的每一行做一次一维变换;顺序反过来,先行后列,结果一样。这样二维问题就退化成了若干个一维问题,代码好写,硬件也好排流水线。

真正省算力的地方在单个一维变换内部。如果直接照公式展开,每个输出都要把四个输入各乘一遍再相加,四次输出合计 12 次加法、4 次乘法。而仔细观察会发现大量重复:x0 + x3这个和,同时被第一个输出和第三个输出的计算需要;x1 + x2也是同理。把这些公共部分提前算好存起来,加法次数就降到 8 次,乘法降到 2 次。这种利用运算冗余的结构就是蝶形算法,名字来自它画出来的信号流图形状。

def transform_4x4(block): # 第一步:逐列做一维变换 tmp = [[0] * 4 for _ in range(4)] for c in range(4): col = [block[r][c] for r in range(4)] out = butterfly_1d(*col) for r in range(4): tmp[r][c] = out[r] # 第二步:对中间结果的每一行再做一维变换 res = [[0] * 4 for _ in range(4)] for r in range(4): res[r] = list(butterfly_1d(*tmp[r])) return res

block是一个 4×4 的二维列表,元素是预测残差;tmp存的是列变换之后的中间结果;res才是最终的 4×4 变换系数。两个循环的顺序可以互换,先横后竖也完全成立,这一点在实现时给了硬件很大的自由度——哪边访存方便就先做哪边。整段代码没有浮点、没有除法,跑在什么样的设备上,结果都是同一份整数。

3.2 直流系数单独拎出来:亮度 4×4 与色度 2×2 的 Hadamard

变换完之后,每个 4×4 块都有一个直流系数,也就是左上角那个代表整块平均亮度的值。这个值有个特点:相邻块的直流系数往往非常接近,因为一帧画面里大面积区域的亮度是渐变的。既然它们彼此这么像,就还有压缩空间。

标准的做法是把它们抽出来再变换一次。对一个 16×16 的亮度宏块来说,它包含 16 个 4×4 块,把这 16 个块的直流系数收集起来,正好组成一个新的 4×4 矩阵,对它做一次 4×4 的 Hadamard 变换。色度分量是 8×8,包含 4 个 4×4 块,抽出来的直流系数组成一个 2×2 矩阵,做 2×2 的 Hadamard 变换。

Hadamard 变换的核里只有 1 和 -1,正变换和反变换的形式完全一样,运算量比整数 DCT 还低。它只处理直流系数,不碰任何交流系数。于是整个编码流程里就有了三种变换方式,按残差数据的类型来选择:普通的交流残差走 4×4 整数 DCT,亮度直流系数走 4×4 Hadamard,色度直流系数走 2×2 Hadamard。

解码端要把这个顺序倒过来,而且有个容易踩坑的细节:对于直流系数,解码器是先做反变换、再做反量化,跟普通系数的顺序正好相反。反变换完成后,还要把这些直流系数按原来的位置装回各个 4×4 块,和已有的交流系数拼成完整的块,才能进入后续的反 DCT 步骤。这个“装配”动作在流程框图里通常单独画一个模块,看着不起眼,位置错了整帧就花了。

3.3 反变换那头:整数运算根治误匹配

前面说过浮点 DCT 最大的问题是编解码不匹配。整数变换从根上解决了它:变换核是精确的整数,正变换和反变换都是确定的整数运算,同一个输入,在任何一台设备上算出来的结果必然一致。不存在“你的舍入和我的舍入不一样”这回事。

反变换里那个乘 1/2 的操作由右移实现,前面解释过为什么不产生截断误差。这一步看着像个小技巧,实际上是整套设计能闭环的关键——如果反变换必须做浮点缩放,前面省下来的整数优势就全废了,误匹配也会重新出现。

这也解释了为什么整数变换对硬件特别友好。整个链路里没有浮点单元什么事,加减法、移位、比较,都是最基础的逻辑门能干的活。低端处理器上省下的是实打实的功耗和面积,高端处理器上省下的是流水线可以被填得更满。同样是保证精度,用整数保证和用高精度浮点保证,成本差着一个量级。

4. 量化这把刀怎么下:52 档步长与 12.25% 的节奏

变换只是把能量挪了个位置,真正让数据变少的是量化。前面所有步骤——整数化、蝶形、尺度合并——都是在为这一步做铺垫:让量化能用整数乘加完成,一次除法都不做。理解量化,重点不在公式本身,而在“步长怎么选”和“缩放怎么并进去”这两件事上。

4.1 等比例标量量化在做什么

量化最朴素的形式是等比例标量量化:拿变换后的系数除以一个步长,四舍五入取整。公式写出来就一行,Z = round(W / step),其中W是变换系数,step是量化步长,Z是量化后的整数。

拿尺子打比方:原来是一把精确到毫米的尺子,每个系数都能读出小数点后好几位;换成只有厘米刻度的尺子,读数只剩整数,误差变大,但记录一个数占的地方小了。步长越大,刻度越粗,压缩比越高,画质损失也越明显。这是视频压缩里唯一真正丢信息的一步,前面那些变换、移位、系数重排,只要不量化,理论上都能原样还原。

也正因为如此,码率控制的核心抓手就是量化步长。编码器想在某个码率下拿到最好的画质,调的基本就是这一个参数。

4.2 QP 每加 6 步长翻倍的道理

标准里量化步长一共有 52 档,由量化参数 QP 决定,每个宏块自己带一个 QP 值。这 52 档不是等间隔排的,规律是:QP 每增加 1,步长增加约 12.25%;每增加 6,步长正好翻一倍。

12.25% 这个数字不是随手拍的,它约等于 2 的 6 次方根减去 1。也就是说,QP 是一个以 2 为底的指数刻度,只不过被切成了每 6 步翻一番的细格。这样设计的好处很直接:如果步长按固定增量线性增长,小步长区间太密、大步长区间太疏,码率控制会在高压缩比那头变得非常粗糙;改成指数刻度后,无论处在哪一段,调一格 QP 带来的变化幅度都差不多,码率曲线就平滑得多。

这也和人的视觉特性对得上。人眼对亮度差异的感知本来就接近对数关系,暗处的细微变化容易察觉,亮处的大幅变化反而不明显。QP 的指数刻度恰好把“感知上的等间隔”翻译成了“数值上的等比间隔”。

QP 变化量化步长变化直观影响
+1约 +12.25%码率小幅下降,画质轻微变化
+6翻一倍码率明显下降,细节开始成片丢失
+12变为 4 倍高压缩比,块状感和模糊明显

需要留意的是,QP 只是决定步长,不决定“哪个系数被丢掉”。真正被清零的是那些量化后落在 0 附近的系数,而它们大多是高频细节。所以 QP 调大时,画面先丢的是纹理和边缘的锐度,平坦区域反而撑得久一些。

4.3 变换与量化合并:整数乘加替代除法

到这一步,前面埋的伏笔该收了。变换公式里那个被提到矩阵外面的 1/2,以及变换核点乘产生的系数,全部可以并进量化公式里。合并之后,一个系数的量化值可以写成:

Z(i,j) = round( W(i,j) * E(i,j) / step )

其中W(i,j)是变换结果,E(i,j)是跟位置相关的点乘系数,step是量化步长。标准进一步把E(i,j) / step预先算成一张乘法因子表,按 QP 和系数位置索引。这样运行时只剩下一次整数乘法、一次加法、一次右移。

# 量化:Z = (W * f + 偏移) >> qbits # f 由 QP 和系数在 4×4 块里的位置共同决定,编码前预先算好存表 # qbits 是右移位数,随 QP 分段变化,保证结果落在 16 位范围内 def quantize(w, f, qbits): offset = 1 << (qbits - 1) # 半个最低位的偏移,实现四舍五入 return (w * f + offset) >> qbits

w是变换后的系数,f是从表里查出来的乘法因子,qbits是右移位数。offset这一项容易被忽略,它的作用是让右移表现出四舍五入而不是直接截断,不加的话所有系数都会系统性偏小。整个函数没有除法,没有浮点,一次乘法加一次移位就完事。

反量化那头是同样的思路反过来走:先查表乘一个因子,再左移若干位,把系数放大回原来的量级。放大之后的数值足够大,所以反变换里那个右移一位不会造成精度损失——这两处设计是相互配合的,拆开看都像小技巧,合起来才是一套完整的闭环。

5. 四组最容易混淆的概念:DCT、整数 DCT、Hadamard 与量化

这套东西的名字彼此长得像,流程里又挨得近,初学的时候极容易糊成一团。下面四组是我见过混淆率最高的,每一组都按“容易混在哪、为什么会混、怎么区分”来说。

5.1 混淆一:整数 DCT 就是 DCT 的整数实现

易混点:名字里明明白白带着 DCT,很自然会以为它就是把同一个公式换成整数来算。

为什么会混:两者效果确实接近,标准也一直沿用了 DCT 这个叫法,从命名上根本没打算区分。

怎么区分:看它算的是什么。严格的 DCT 是拿余弦值构成的核去做乘加,数学上是正交变换;整数版本用的是只有 1、2、-1、-2 的近似核,归一化被挪到了量化环节,算出来的系数和理论 DCT 系数差着一个缩放因子。叫它 DCT,主要是为了跟 Hadamard 变换区分开——毕竟一套流程里两个变换都得有名字。判断标准很简单:只要看到变换核里出现了 2 和移位,那就不是教科书上的 DCT。

5.2 混淆二:Hadamard 变换和整数 DCT 是一回事

易混点:都是整数运算,都是 4×4 矩阵,都在同一个编码流程里出现,看起来像同一套东西的两个别名。

为什么会混:两者的实现风格太像了,都是加减法堆出来的,代码写在一起几乎分不清哪段在干哪件事。

怎么区分:看处理对象。整数 DCT 处理的是残差本身,一个 4×4 块的 16 个系数全部由它产出;Hadamard 只处理从各个块里抽出来的直流系数——亮度是 16 个块的直流凑成一个 4×4 再变换,色度是 4 个块的直流凑成一个 2×2 再变换。换句话说,Hadamard 是“对直流系数的二次加工”,交流系数从头到尾不经过它。另外,Hadamard 的正变换和反变换形式完全一致,这一点整数 DCT 做不到。

5.3 混淆三:变换会丢信息,量化只是把数据压小

易混点:两个步骤都让数据变小了,很容易把“丢信息”的账算到变换头上。

为什么会混:从表现上看,一块残差经过变换再量化,确实变得面目全非,很难说清是哪一步造成的。

怎么区分:做个思想实验就清楚了。把量化这一步拿掉,只做正变换再立刻做反变换,结果能精确还原成原始残差——因为整数运算全程无误差,也不存在舍入。所以变换是可逆的能量重排,它只是把数值挪到不同的格子里,一个比特都没丢。量化才是不可逆的那一刀,四舍五入一旦发生,小数部分就永远回不来了。这也解释了为什么调画质要调 QP:QP 控制的正是那一刀下得多深,而变换本身没有“深浅”可调。

5.4 混淆四:量化步长是均匀的,QP 每加 1 就线性变大

易混点:QP 是个整数编号,从 0 数到 51,看起来像一把等刻度的尺子。

为什么会混:整数序列天然给人“等间隔”的暗示,而且平时调参数见惯的也多是线性刻度。

怎么区分:去看步长表就明白了。QP 每加 1,步长涨 12.25%;每加 6,步长翻倍。这是一条指数曲线,不是直线。所以在 QP 较小的时候,加一格带来的变化很小;QP 已经很大时,同样加一格,绝对步长的变化幅度其实更大。理解了这一点,调码率时就不会指望“QP 加个五六档画质只掉一点点”——从 20 加到 26,步长已经翻倍了,画面细节会有肉眼可见的差别。

6. 什么时候该用这套整数变换,什么时候别硬套

判断标准其实就一条:你是不是处在“算力紧、要实时、两端必须严格一致”的场景里。

该用的场合很明确。实时编码,比如监控摄像头、视频会议终端,帧率摆在那儿,每帧的处理时间有硬上限,整数运算省下的每一个周期都直接换成能跑更高分辨率或者更低功耗。低端硬件和嵌入式设备更是如此,没有浮点单元或者浮点性能很弱,整数变换几乎是唯一选择。还有一类场景容易被忽略:需要长期驻留参考帧的编码。误差一旦进入参考帧就会逐帧传播,整数变换从机制上排除了这种累积,属于花小钱办大事。

不该硬套的场合也得说清楚。如果你做的是离线压缩,时间不敏感,追求的是同等画质下码率再低一点,那么 4×4 这个块尺寸本身就是个约束——块越小,能量集中的程度越有限,压缩效率的天花板摆在那儿,后来的编码方案改用更大的变换块正是出于这个考虑。另外,如果你只是做普通的图像存档,不需要实时,也不需要硬件实现,浮点 DCT 在软件里跑得也挺好,没必要为了“整数”两个字去迁就近似核带来的那一点点性能损失。

还有一种情况属于误用:以为用了整数变换就没有精度问题了。不是的,量化该丢的信息一点没少丢。整数只保证了“两端算得一样”,没保证“算得准”。QP 调大,画面照样糊,块效应照样出,这跟变换是不是整数没有关系。

场景特征建议原因
实时编码、帧率有硬约束用整数加减移位,运算量低且稳定
无浮点单元的低端硬件用全链路可纯整数实现
参考帧需长期驻留用精确整数,杜绝误差累积
离线压缩、追求极致码率慎重4×4 块的能量集中能力有限
软件内单次处理、时间不敏感可用可不用浮点方案实现更直观

有个血泪教训值得说一下。早些年我调一个编码参数,为了把码率压下去,直接把 QP 从二十几拉到三十几,跑完看码率曲线漂亮得很,等解码出来才发现整片画面的纹理全糊成了色块,边缘还带着一圈一圈的振铃。后来才想明白,我把“步长翻倍”当成了“加几档”这么简单的事。从那以后我每次动 QP 之前都强制自己先在小片段上跑一遍,按 QP 加 1、加 3、加 6 各出一版对比,确认画质掉在我能接受的范围里,再往完整片子上套。这套笨办法救过我好几次,也让我彻底记住了 12.25% 这个数字的分量。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询