在前面的步骤,我们已经将图像的亮度与色度分离开来,通过将色度降采样,使得数据量减少了很多,但是还有很大的下降空间。
冗余信息
现实存在的图像,相邻的像素经常是相关的。比如一片蓝天,左边的像素很可能和右边的像素颜色相近。
大部分情况下,我们面对的是这样的序列:
100, 101, 102, 103, 103, 104, 105, 105
而不是:
100, 250, 20, 240, 10, 255, 30, 230
当我们得知一个像素的值时,与它邻近的像素的值很可能也是可以预测的。这就意味着,图像中存在大量的冗余信息。
比如,前面提到的序列:
100, 101, 102, 103, 103, 104, 105, 105
我们可以用一些手段去减少冗余信息,比如:
100, +1, +1, +1, 0, +1, +1, 0
这样除了第一个数需要存储,后续的数都可以用一个比较小的差值来代替,那后续的数就可以用更少的比特数来存储了。当然,JPEG 并不会直接对相邻像素采用这种简单的差分编码,而是先通过离散余弦变换(DCT)重新组织信息。
正如我们前面所了解到的,JPEG 压缩算法通常认为是一种有损压缩算法。虽然 DCT 本身并不会丢失信息,但它把图像信息重新组织到不同频率的系数中,使得后续的量化阶段可以优先保留重要的低频信息,舍弃掉部分高频细节。因此,DCT 是 JPEG 有损压缩能够高效工作的关键基础。
1D DCT
这里的“1D”表示数据只有一个采样方向,与线性代数中向量空间的维数不是同一个概念。例如,一个包含8个样本的一维序列可以表示为一个8维向量,但它仍然是一维信号。
我们先绘制一条普通的余弦曲线 f1(x)=cos(x):
我们将[0,π]区间三等分,并取每个区间的中点,分别为:
f1(6π)f1(2π)f1(65π)=cos(6π)=23=cos(2π)=0=cos(65π)=−23
得到的值可以用一个向量表示:
230−23
同样的做法应用在f0(x)=cos(0⋅x)上,可以得到:
111
应用在f2(x)=cos(2⋅x)上,可以得到:
21−121
如此,我们便得到了一组向量:
111230−2321−121
目前这三个向量的长度还不一致,我们可以在维持向量方向不变的情况下,想办法让它们的长度都变成1,即所谓的归一化:
333333220−2266−3666
可是这样的一组向量有什么用呢?
让我们随便指定一个一维离散信号。它包含3个样本,因此可以表示为一个3维列向量:
v=v1v2v3
我们可以将它表示为3个基向量的线性组合:
v=v1100+v2010+v3001
这里面选用的基向量是:
100010001
但是想要得到v,我们并不一定非要使用这组基向量,我们也可以使用前面归一化后的那组向量作为基向量:
v=u1333333+u2220−22+u366−3666
只不过这样基向量前面的系数u1,u2,u3会和原先的v1,v2,v3不一样。但是把系数从v1,v2,v3变换成u1,u2,u3,到底有什么作用呢?
原先的系数v1,v2,v3分别对应各个采样位置的取值,它们的大小主要反映对应位置的明暗,并没有把信号中的整体趋势与局部细节区分开来。如果想要进行有损压缩,我们就需要进一步判断:哪些信息构成了信号的整体面貌,应该尽量保留;哪些信息只是局部细节,可以用较低的精度表示,甚至直接舍弃。
但是u1,u2,u3却不一样,u1后面跟着的是频率最低的基向量,u2后面跟着的是频率中等的基向量,u3后面跟着的是频率最高的基向量。虽然这些基向量在数学上的地位相同,但对于自然图像,低频分量通常承载整体明暗和缓慢变化,高频分量则更多表现为边缘和细节。因此从图像压缩的角度看,低频分量通常更为重要,其系数往往更大;高频系数则往往更小。这就给我们带来了一些启发,如果我们只是去掉一些占比不大的“细节”,是不是就可以使用更少的存储空间来得到跟原先的v差不多的向量呢?
为了方便,我们可以把刚才得到的新系数拼成一个向量:
u=u1u2u3
我们还可以把刚才得到的3个基向量组合成一个矩阵:
T=333333220−2266−3666
线性代数中有一个技巧,那就是矩阵左乘一个向量,可以理解为是矩阵的各个列向量的线性组合。如此一来,我们就可以用一种更为简洁的方式来表达刚才的过程了:
v=v1v2v3=v1100+v2010+v3001
=u1333333+u2220−22+u366−3666
=333333220−2266−3666u1u2u3=Tu
这个例子中的一维信号可以表示为一个3维向量,因此我们在每条余弦曲线上取3个样本,并构造3个基向量。我们也可以增加一维信号的样本数量。比如,将[0,π]区间八等分,并取每个区间的中点(即16π,163π,165π,…,1615π),在这8个点上分别计算cos(0x),cos(1x),…,cos(7x),得到8个基向量;再像前面一样,把每个基向量都归一化到长度1。这样我们就可以将一个8维向量表示为8个归一化基向量的线性组合:
v=v1v2⋮v8=4242⋮4221cos(161π)21cos(163π)⋮21cos(1615π)21cos(162π)21cos(162⋅3π)⋮21cos(162⋅15π)⋯⋯⋱⋯21cos(167π)21cos(167⋅3π)⋮21cos(167⋅15π)u1u2⋮u8
其中v、u是8维列向量,T是由8个基向量(作为列)拼成的8×8矩阵。这其实就意味着,我们可以用这8个基向量来表示这样排成一行的8个像素点的亮度值,并且我们可以修改高频分量的系数来降低这一行的“细节”,为后续压缩编码做好准备。
但我们的图像通常不会只有一行,有没有办法将这个思路从一行像素扩展到一大块像素呢,比如说,从1×8的像素扩展到8×8的像素?
2D DCT
为了解决这个问题,我们来看看线性代数中另一个技巧:行向量右乘矩阵,可以理解为是矩阵的各个行向量的线性组合。也就是说,我们不仅有:
333333220−2266−3666u1u2u3=u1333333+u2220−22+u366−3666
还可以有:
[u1u2u3]332266330−3633−2266=u1[333333]+u2[220−22]+u3[66−3666]
利用这两个技巧,我们可以分别将图像块的行与列重新用新的基向量表达:
X=TMTT
我们单看后半部分的MTT,它实际上就是遍历M的每一行,把每一行都用新的基向量重新组合一遍,得到了一个中间矩阵M′;然后再用左边的T去遍历这个中间矩阵M′的每一列,把每一列也重新组合一遍,得到最终结果X。
在 1D DCT 中,我们能清晰地看到,v1,v2,v3之所以会变成u1,u2,u3,是因为我们更换了基向量,整个变换过程可以完全视为一个线性映射。可是在 2D DCT 中,虽然我们能猜到X与M之间肯定是有联系的,但一时之间似乎看不太出 X=TMTT到底干了什么。
现在我们将图像块X的各个列拆开,上下拼在一起,形成一个特别长的列向量:
vec(X)=vec(x11x21x31⋮x81x12x22x32⋮x82x13x23x33⋮x83⋯⋯⋯⋱⋯x18x28x38⋮x88)=x11x21x31⋮x81x12x22x32⋮x88
这样写未免有些啰嗦,我们不妨将X的每一列都用一个向量符号表示:
xi=x1ix2i⋮x8i
那刚才的式子就可以写成下面这种更简洁的形式:
vec(X)=vec([x1x2x3⋯x8])=x1x2x3⋮x8
我们再回过头研究一下 2D DCT 中通过系数块重建图像块的过程:
X=TMTT=T[m1m2m3⋯m8]TT
=T[t11m1+t12m2+⋯+t18m8t21m1+t22m2+⋯+t28m8⋯t81m1+t82m2+⋯+t88m8]=[T(t11m1+t12m2+⋯+t18m8)T(t21m1+t22m2+⋯+t28m8)⋯T(t81m1+t82m2+⋯+t88m8)]
如果我们把X的各列堆叠起来,就可以得到:
vec(X)=T(t11m1+t12m2+⋯+t18m8)T(t21m1+t22m2+⋯+t28m8)T(t31m1+t32m2+⋯+t38m8)⋯T(t81m1+t82m2+⋯+t88m8)
=t11Tt21Tt31T⋯t81Tt12Tt22Tt32Tt82T⋯⋯⋯⋯t18Tt28Tt38Tt88Tm1m2m3⋮m8
=t11t11t11t21⋮t11t81⋮t81t11t81t21⋮t81t81t11t12t11t22⋮t11t82⋮t81t12t81t22⋮t81t82⋯⋯⋱⋯⋱⋯⋯⋱⋯t11t18t11t28⋮t11t88⋮t81t18t81t28⋮t81t88⋯⋯⋱⋯⋱⋯⋯⋱⋯t18t11t18t21⋮t18t81⋮t88t11t88t21⋮t88t81t18t12t18t22⋮t18t82⋮t88t12t88t22⋮t88t82⋯⋯⋱⋯⋱⋯⋯⋱⋯t18t18t18t28⋮t18t88⋮t88t18t88t28⋮t88t88vec(M)
其中vec(X)是64维的列向量,vec(M)也是64维的列向量,而中间的
W=t11t11t11t21⋮t11t81⋮t81t11t81t21⋮t81t81t11t12t11t22⋮t11t82⋮t81t12t81t22⋮t81t82⋯⋯⋱⋯⋱⋯⋯⋱⋯t11t18t11t28⋮t11t88⋮t81t18t81t28⋮t81t88⋯⋯⋱⋯⋱⋯⋯⋱⋯t18t11t18t21⋮t18t81⋮t88t11t88t21⋮t88t81t18t12t18t22⋮t18t82⋮t88t12t88t22⋮t88t82⋯⋯⋱⋯⋱⋯⋯⋱⋯t18t18t18t28⋮t18t88⋮t88t18t88t28⋮t88t88
则是一个64×64的矩阵。
原来,2D DCT 和 1D DCT 一样,都是用“可以区分重要程度”的新的基向量,重新表达原来的数据。在上面的式子中,W的每一列就是一个新的基向量,vec(M)中的每个值就是对应基向量前面的系数。
不过这样理解似乎过于抽象了些,我们再往回退一步,将堆叠起来的vec(X)还原回原来的X,而矩阵中的每一列(也就是每个新的基向量),也从堆叠的状态还原到8×8的矩阵状态。
比如,我们将W的第一列还原回8×8的矩阵状态:
w1=t11t11t11t21t11t31t11t41t11t51t11t61t11t71t11t81t21t11t21t21t21t31t21t41t21t51t21t61t21t71t21t81t31t11t31t21t31t31t31t41t31t51t31t61t31t71t31t81t41t11t41t21t41t31t41t41t41t51t41t61t41t71t41t81t51t11t51t21t51t31t51t41t51t51t51t61t51t71t51t81t61t11t61t21t61t31t61t41t61t51t61t61t61t71t61t81t71t11t71t21t71t31t71t41t71t51t71t61t71t71t71t81t81t11t81t21t81t31t81t41t81t51t81t61t81t71t81t81
因为W有64列,这就意味着我们可以得到64个图像块。这就意味着,原本的图像块X,可以用64个图像块的线性组合来表示。
而M中的每个值,对应的就是各个pattern前面的系数。
前面我们也提到了,真实的图像中,低频分量的系数绝对值往往比较大,高频分量的系数绝对值往往比较小。这就造成M的左上角的数值绝对值往往都比较大,而右下角的数值往往都比较小甚至接近于0。这就给我们带来了一个提示,如果我们舍弃掉右下角的那些接近于0的数值,是否就可以用更少的存储空间来表示原本的图像块X呢?关于如何精简系数,我们将在下一节中详细介绍。
目前我们还剩下一个问题。虽然我们已经知道 2D DCT 产生的系数矩阵M的含义,但是我们要怎么才能计算出M呢?
我们回到前面的式子:
X=TMTT
前面构造T时使用的8个离散余弦基向量,两两正交,并且都不是零向量。而一组两两正交的非零向量必然线性无关。因此,T满足可逆的条件。那么我们只需要在等式左边乘以T−1,再在右边乘以(TT)−1,就可以得到:
M=T−1X(TT)−1
可是要如何计算T−1呢?
我们再回忆前面,每个基向量已经都归一化,即T的各个列向量长度都为1。
根据矩阵乘法的定义,TTT的第i行第j列,刚好就是T的第i列与第j列的内积:
(TTT)ij=(column i)⋅(column j)={1,0,i=ji=j
这就说明:
TTT=I
接着在等号两侧同时右乘T−1:
TTTT−1=IT−1⟹TT=T−1
这就带来了一个非常好用的结果:我们只要将T进行转置,就可以轻松得到它的逆矩阵。
所以计算M的公式也变得清晰起来:
M=T−1X(TT)−1=TTXT
回忆一下前面,T 可以表示为:
4242⋮4221cos(161π)21cos(163π)⋮21cos(1615π)21cos(162π)21cos(162⋅3π)⋮21cos(162⋅15π)⋯⋯⋱⋯21cos(167π)21cos(167⋅3π)⋮21cos(167⋅15π)
这样书写未免过于繁琐,实际上 T 的第 i 行第 u 列可以用一个统一的公式表示:
Tiu=21Cucos(16(2i−1)(u−1)π)
其中
Cu={21,1,u=1u=2,3,…,8
类似地,TT 的第 v 行第 j 列为:
(TT)vj=Tjv=21Cvcos(16(2j−1)(v−1)π)
其中
Cv={21,1,v=1v=2,3,…,8
这里其实就能看出,Tiu 和 (TT)vj 的结构实际上是一样的。
如果我们把 M=TTXT 完全展开,M 中第 v 行、第 u 列的系数可以写成:
Mvu=41CuCvi=1∑8j=1∑8Xijcos(16(2i−1)(v−1)π)cos(16(2j−1)(u−1)π)
其中:
Ck={21,1,k=1k=2,3,…,8
这便是将一个8×8图像块X转换为 DCT 系数矩阵M的计算公式。
到这里为止,我们已经完成了 JPEG 实现 DCT 的理论基础。实际上,JPEG 正是将 Y、Cb、Cr 三个分量分别划分成一个个8×8的样本块,然后对每个样本块进行 DCT,得到对应的 DCT 系数矩阵。
另外需要注意的是,按照 JPEG 中的约定,DCT 变换前需要先将输入样本块的每个样本值减去128,使得样本值的范围从[0,255]变为[−128,127],也就是把数据范围的中点定在了0。