1. 图像压缩到底在解决什么问题1.1 从一张照片的存储困境说起你手机里随手拍的一张照片按 1200 万像素、每个像素 RGB 三通道各 8 位来算原始数据量是 1200万 × 3 字节大约 34MB。如果拍一段 1080p、30 帧每秒、时长一分钟的视频原始数据量更是达到 1080×1920×3×30×60接近 10GB。这个体量放在存储介质里是灾难放在网络传输里更是不可接受。图像压缩要解决的核心矛盾就一句话用尽可能少的比特数尽可能无损地表示原始图像信息。注意这里有两个“尽可能”它们之间是互相拉扯的——压缩率越高失真往往越大要求完全无损压缩率就有天花板。整个第八章的知识体系本质上就是在研究这个拉扯关系里的各种权衡策略。我刚开始接触这块内容的时候总觉得压缩就是“把图片存小一点”后来做实际项目才发现压缩算法选型直接决定了整个系统的架构。比如做实时视频监控你不可能用无损压缩带宽扛不住但做医学影像归档你又不能接受有损压缩诊断细节丢不起。所以理解压缩首先要理解应用场景决定了你能接受多大的失真。1.2 压缩的数学本质去除冗余图像数据之所以能被压缩根本原因是原始表示里存在大量冗余。冗余主要分三类空间冗余同一幅图像中相邻像素的值往往高度相关。一面白墙上的几百个像素值几乎一样逐个存储就是浪费。频谱冗余图像从频域看能量集中在低频部分高频分量幅度很小可以用更少的比特表示。视觉冗余人眼对某些信息不敏感比如对亮度变化敏感、对色度变化迟钝对高频细节的分辨能力也有限。丢掉这些人眼感知不到的信息主观质量几乎不变。压缩算法做的事情就是把这些冗余找出来用更紧凑的方式重新编码。信息论里香农给出的信源编码定理告诉我们一个信源的平均码长存在下界即信源熵。压缩算法的目标就是尽量逼近这个下界。提示理解冗余的三种类型是理解后面所有压缩算法的一把钥匙。每学一个算法你都可以问自己它主要在去除哪一类冗余1.3 有损与无损的分水岭无损压缩的典型代表是 Huffman 编码、算术编码、LZW 编码它们通过统计符号出现概率来分配变长码字解压后能完全恢复原始数据。无损压缩的压缩比通常在 2:1 到 5:1 之间对于自然图像来说很难再高了。有损压缩则允许在编码过程中丢弃部分信息典型代表是 DCT 变换编码、小波变换编码、矢量量化。有损压缩的压缩比可以做到 10:1 甚至 100:1代价是解压后的图像与原始图像存在差异。衡量这个差异的指标常用的有均方误差 MSE、峰值信噪比 PSNR、结构相似度 SSIM。选择有损还是无损不是技术问题而是需求问题。我个人的经验判断标准很简单如果解压后的数据要用于二次计算或法律取证选无损如果只是给人眼看有损通常足够。2. 核心压缩算法的原理拆解2.1 Huffman 编码变长码的经典解法Huffman 编码是 1952 年提出的到现在依然是很多压缩标准里的最后一级熵编码环节。它的核心思想非常朴素出现频率高的符号用短码字出现频率低的符号用长码字。构建 Huffman 树的过程是这样的统计每个符号的出现概率每个符号作为一个叶子节点。每次取出概率最小的两个节点合并成一个新节点新节点概率为两者之和。重复步骤 2直到只剩一个根节点。从根到叶子的路径上左分支记 0右分支记 1得到每个符号的码字。举个例子假设有四个符号 A、B、C、D概率分别是 0.4、0.3、0.2、0.1。合并过程是先合并 C 和 D 得到 0.3 的新节点再合并 B 和这个 0.3 节点得到 0.6最后合并 A 和 0.6 得到根。最终 A 的码字是 01 位B 是 102 位C 是 1103 位D 是 1113 位。平均码长是 0.4×1 0.3×2 0.2×3 0.1×3 1.9 位而熵是 1.846 位非常接近。Huffman 编码有一个重要性质叫前缀码任何一个码字都不是另一个码字的前缀。这保证了解码时不会产生歧义不需要分隔符就能唯一解码。但 Huffman 编码也有局限。它给每个符号分配的码字长度必须是整数位当某个符号概率接近 1 时理论上只需要零点几位但 Huffman 最少给 1 位这就造成了浪费。算术编码正是为了解决这个问题而出现的。2.2 算术编码突破整数位限制算术编码的思路和 Huffman 完全不同。它不把每个符号映射成独立的码字而是把整个符号序列映射成 [0,1) 区间内的一个小数。具体做法是初始区间是 [0,1)。每读入一个符号就按照该符号的概率把当前区间切分然后选择对应符号的子区间作为新的当前区间。读完全部符号后在当前区间内任取一个小数用二进制表示就是编码结果。假设符号 A、B 概率分别是 0.6、0.4要编码序列 “ABA”初始区间 [0, 1)读入 A新区间 [0, 0.6)读入 B在 [0, 0.6) 内按比例切分B 对应 [0.36, 0.6)读入 A在 [0.36, 0.6) 内切分A 对应 [0.36, 0.504)最后在 [0.36, 0.504) 内取一个数比如 0.4用二进制表示即可。这个例子里三个符号理论上只需要约 1.9 位算术编码可以逼近这个极限。算术编码的压缩效率比 Huffman 更高尤其在符号概率极端不均衡时优势明显。但它的实现复杂度也更高而且存在专利历史问题早期部分算术编码算法受专利保护这在一定程度上影响了它的普及后来专利过期后才被广泛采用。2.3 变换编码从空间到频率的转换变换编码是绝大多数有损图像压缩标准的核心。它的基本流程是把图像分成小块对每块做某种数学变换把能量集中到少数系数上然后对系数进行量化和编码。最常用的变换是离散余弦变换 DCT。对于 8×8 的图像块二维 DCT 把空间域的 64 个像素值转换成频率域的 64 个系数。左上角的系数是直流分量 DC代表块的平均亮度其余是交流分量 AC代表不同频率的细节。DCT 之所以好用是因为它有能量集中特性自然图像块的能量大部分集中在低频系数上高频系数往往接近零。这样我们就可以对高频系数进行粗量化甚至直接置零而主观质量损失很小。量化的过程是每个 DCT 系数除以对应的量化步长然后取整。量化步长来自一张量化表这张表是根据人眼视觉特性设计的——低频用小的量化步长保留精度高频用大的量化步长大胆丢弃。JPEG 标准里有一张经典的亮度量化表质量因子就是通过缩放这张表来实现的。量化是有损压缩中唯一真正丢失信息的步骤后面的熵编码是无损的。所以调质量因子本质上就是在调量化步长。2.4 预测编码利用相邻像素的相关性预测编码的思路是不直接编码像素值而是编码当前像素与预测值之间的差值。如果预测足够准差值就会很小分布集中在零附近熵就低压缩就容易。最简单的预测是前值预测用左边相邻像素预测当前像素。更好的做法是用多个相邻像素的线性组合来预测这就是 DPCM差分脉冲编码调制的思路。在无损 JPEG 里预测器有 8 种模式可选编码器会为每个像素行选择最优的预测模式。在有损视频编码里帧内预测和帧间预测都是预测编码的延伸——帧内预测利用同一帧内相邻块的空间相关性帧间预测利用相邻帧之间的时间相关性。预测编码的关键在于预测器的设计。预测器太简单残差不够小预测器太复杂计算量大且可能过拟合。实际标准里用的都是经过大量实验验证的固定预测器组合。3. 完整压缩系统的实操流程3.1 一个典型有损压缩系统的流水线把上面的技术点串起来一个完整的基于 DCT 的有损图像压缩系统大致是这样的流水线颜色空间转换RGB 转 YCbCr把亮度 Y 和色度 Cb、Cr 分离。因为人眼对亮度敏感、对色度迟钝可以对色度分量进行下采样比如 4:2:0色度分辨率减半。分块把每个分量切成 8×8 的小块。DCT 变换对每个块做二维 DCT得到 64 个系数。量化用量化表对系数进行除法取整这是有损的关键步骤。Zigzag 扫描把 8×8 的系数矩阵按之字形顺序排成一维序列让低频系数排在前面高频的零集中到后面。熵编码对量化后的系数做 Huffman 编码或算术编码进一步压缩。这个流水线就是 JPEG 的基本框架。理解了这个框架再看其他标准比如 JPEG2000 用小波变换替代 DCTWebP 用更先进的预测和熵编码就很容易触类旁通。3.2 量化表的设计与质量因子调节量化表的设计直接决定了压缩质量和压缩比的平衡。JPEG 标准给出的基准量化表是基于心理视觉实验得出的但实际使用时通常会根据质量因子 Q 进行缩放。缩放公式一般是如果 Q 50缩放因子 5000 / Q如果 Q ≥ 50缩放因子 200 - 2Q。然后用这个因子去乘基准量化表再取整并限制在 1 到 255 之间。Q 越大量化步长越小质量越高压缩比越低。Q75 通常被认为是质量和体积的较好平衡点。Q90 以上基本看不出差别但文件会大很多。Q50 以下开始出现明显块效应。我在实际项目里做过一组对比测试用同一张 1920×1080 的风景照不同质量因子下的表现大致如下质量因子文件大小PSNR主观评价95约 1.2MB42dB几乎无损85约 600KB38dB优秀75约 350KB35dB良好50约 180KB31dB可接受25约 90KB27dB明显块效应这张表不是绝对标准不同图像内容差异很大但能帮你建立一个量级概念。3.3 用 Python 实操一遍 DCT 压缩光看原理不够动手跑一遍才能真懂。下面用 Python 和 NumPy 实现一个简化版的 DCT 压缩流程不依赖现成的图像库纯手工实现帮你把每一步都看清楚。import numpy as np # 标准 8x8 DCT 变换矩阵 def dct_matrix(n8): mat np.zeros((n, n)) for i in range(n): for j in range(n): if i 0: mat[i, j] np.sqrt(1/n) else: mat[i, j] np.sqrt(2/n) * np.cos((2*j1)*i*np.pi/(2*n)) return mat # 二维 DCT def dct2(block, D): return D block D.T # 二维 IDCT def idct2(coeff, D): return D.T coeff D # 标准亮度量化表 Q np.array([ [16,11,10,16,24,40,51,61], [12,12,14,19,26,58,60,55], [14,13,16,24,40,57,69,56], [14,17,22,29,51,87,80,62], [18,22,37,56,68,109,103,77], [24,35,55,64,81,104,113,92], [49,64,78,87,103,121,120,101], [72,92,95,98,112,100,103,99] ]) # 模拟一个 8x8 图像块 block np.array([ [120,122,125,128,130,132,135,138], [118,120,123,126,129,131,134,137], [115,118,121,124,127,130,133,136], [112,115,119,122,126,129,132,135], [110,113,117,120,124,128,131,134], [108,111,115,119,123,127,130,133], [106,110,114,118,122,126,129,132], [105,109,113,117,121,125,128,131] ], dtypefloat) D dct_matrix(8) coeff dct2(block, D) quantized np.round(coeff / Q) dequantized quantized * Q reconstructed idct2(dequantized, D) print(原始块均值:, block.mean()) print(量化后非零系数个数:, np.count_nonzero(quantized)) print(重建误差 MSE:, np.mean((block - reconstructed)**2))跑完这段代码你会发现64 个系数里量化后非零的往往只有十几个其余全被量化成零了。这就是压缩的来源——后面只需要编码这十几个非零系数和它们的位置信息。注意这段代码是教学用的简化实现没有做 Zigzag 扫描和熵编码。真实 JPEG 编码器还会对 DC 系数做差分编码对 AC 系数做游程编码然后接 Huffman 编码。3.4 从块效应看压缩的代价有损压缩最典型的视觉缺陷就是块效应在低码率下8×8 块的边界会变得可见图像看起来像是由小方块拼成的。产生的原因是每个块独立做 DCT 和量化块与块之间的连续性被破坏了。解决块效应有几种思路重叠变换让相邻块有重叠区域比如 MLTModulated Lapped Transform用平滑窗函数减少边界不连续。后处理滤波解码后对块边界做平滑滤波这是很多视频编码标准里的去块滤波器的思路。换变换用小波变换替代 DCT小波变换是全局的天然没有块边界问题JPEG2000 就是这么做的。我在做图像压缩质量评估时块效应是最容易被人眼察觉的失真类型甚至比同等 MSE 下的模糊更让人不适。所以如果你的应用对主观质量要求高宁可提高一点码率也要把块效应压下去。4. 常见问题与排查技巧实录4.1 压缩后图像出现色带怎么办色带banding通常出现在平滑渐变区域表现为原本连续的渐变变成了明显的阶梯状色块。根本原因是量化步长太大把相近的颜色值量化到了同一个值。排查思路先看是不是色度下采样导致的。4:2:0 下采样在渐变区域容易产生色带可以试试改成 4:4:4 看是否改善。如果确认是量化问题提高质量因子或者对平滑区域使用更精细的量化表。一个实用技巧是在压缩前给图像加一点微小的抖动噪声打散量化台阶这在专业图像处理里叫 dithering。加噪听起来反直觉但确实能改善渐变的视觉平滑度。4.2 压缩比达不到预期怎么排查有时候你会发现明明设了很低的质
阅读完成 · 觉得有帮助?