【图像处理基石】如何入门图像压缩编码技术?

一、先搞懂“痛点”:为什么需要图像压缩?
我们平时拍的一张4000×3000分辨率的RGB888格式照片,原始体积约34.3MB。10张图就超300MB,视频更是“存储杀手”——这就是压缩编码的意义:在尽量保留视觉质量的前提下,减少数据量,解决“存不下、传不动”的问题。
二、压缩的根本:图像里的3类“冗余”
图像能被压缩,核心是存在大量冗余信息,去掉后不影响视觉效果:
- 空间冗余:相邻像素颜色相似(比如天空、墙面);
- 视觉冗余:人眼对高频细节、部分颜色不敏感;
- 信息熵冗余:部分像素值出现频率极高,用等长编码浪费空间。
三、核心分类:无损压缩 vs 有损压缩
| 类型 | 核心特点 | 适用场景 | 典型例子 |
|---|---|---|---|
| 无损压缩 | 解压后与原图完全一致 | 截图、图标、医学图像 | PNG、GIF、DEFLATE算法 |
| 有损压缩 | 牺牲少量画质换高压缩比 | 照片、网页图、视频帧 | JPEG、WebP(有损模式) |
接下来,直接上代码实战!
四、实战1:OpenCV实现常见图像格式压缩(最常用)
这是开发中最频繁的操作——将图像保存为JPEG/PNG/WebP,调整压缩参数,对比效果和体积。
环境准备
先安装依赖库:
pip install opencv-python pillow # opencv处理图像,pillow辅助格式转换
代码实现:格式转换+压缩参数调整
import cv2
import os
from PIL import Imagedef image_compress_demo(input_path, output_dir):# 1. 读取图像(OpenCV默认BGR格式,后续保存会自动转RGB)img = cv2.imread(input_path)if img is None:print(f"无法读取图像:{input_path}")return# 创建输出目录os.makedirs(output_dir, exist_ok=True)filename = os.path.splitext(os.path.basename(input_path))[0]# 2. JPEG有损压缩(质量0-100,越高画质越好、体积越大)jpeg_quality = 70 # 常用中间值,平衡画质和体积jpeg_output = os.path.join(output_dir, f"{filename}_jpeg_q{jpeg_quality}.jpg")cv2.imwrite(jpeg_output, img, [int(cv2.IMWRITE_JPEG_QUALITY), jpeg_quality])# 3. PNG无损压缩(压缩级别0-9,越高压缩率越好、速度越慢)png_level = 6 # 默认推荐级别png_output = os.path.join(output_dir, f"{filename}_png_l{png_level}.png")cv2.imwrite(png_output, img, [int(cv2.IMWRITE_PNG_COMPRESSION), png_level])# 4. WebP压缩(支持有损/无损,质量0-100)webp_quality = 70 # 有损模式webp_output = os.path.join(output_dir, f"{filename}_webp_q{webp_quality}.webp")# OpenCV部分版本不直接支持WebP,用Pillow兼容img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR转RGBImage.fromarray(img_rgb).save(webp_output, "WebP", quality=webp_quality)# 5. 计算并打印各文件体积print("压缩完成,文件体积对比:")for file in [jpeg_output, png_output, webp_output]:size = os.path.getsize(file) / 1024 # 转KBprint(f"{os.path.basename(file)}: {size:.2f} KB")# 运行示例
if __name__ == "__main__":input_image = "test.jpg" # 你的输入图像路径output_folder = "compressed_images"image_compress_demo(input_image, output_folder)
关键说明
- JPEG:
IMWRITE_JPEG_QUALITY控制质量,推荐50-80,适合照片; - PNG:
IMWRITE_PNG_COMPRESSION控制压缩级别,无损模式下,级别越高体积越小(但速度变慢); - WebP:相同画质下,体积比JPEG小30%左右,比PNG小50%,是网页/APP的优选。
五、实战2:简化版哈夫曼编码(无损压缩核心)
哈夫曼编码是无损压缩的基础,核心是“频率高的字符用短码”。下面实现一个简化版,用于理解原理(实际工程中用现成库即可)。
代码实现:哈夫曼编码与解码
import heapq
from collections import defaultdict# 1. 构建哈夫曼树节点
class HuffmanNode:def __init__(self, value, freq):self.value = value # 像素值(或字符)self.freq = freq # 出现频率self.left = None # 左子树self.right = None # 右子树# 重载比较运算符,用于堆排序def __lt__(self, other):return self.freq < other.freq# 2. 生成哈夫曼编码表
def build_huffman_code(img_flat):# 统计每个像素值的频率freq_dict = defaultdict(int)for pixel in img_flat:freq_dict[pixel] += 1# 构建最小堆(优先队列)heap = [HuffmanNode(val, freq) for val, freq in freq_dict.items()]heapq.heapify(heap)# 合并节点构建哈夫曼树while len(heap) > 1:left = heapq.heappop(heap)right = heapq.heappop(heap)# 新节点频率为左右子节点之和,值设为None(内部节点)merged = HuffmanNode(None, left.freq + right.freq)merged.left = leftmerged.right = rightheapq.heappush(heap, merged)# 生成编码表(递归遍历哈夫曼树)huffman_code = {}def traverse(node, current_code):if node is None:return# 叶子节点(对应像素值)if node.value is not None:huffman_code[node.value] = current_codereturntraverse(node.left, current_code + "0")traverse(node.right, current_code + "1")root = heapq.heappop(heap)traverse(root, "")return huffman_code# 3. 编码与解码
def huffman_compress(img_flat, huffman_code):# 编码:将像素序列转为二进制字符串compressed_bits = "".join([huffman_code[pixel] for pixel in img_flat])return compressed_bitsdef huffman_decompress(compressed_bits, huffman_code):# 解码:二进制字符串转回像素序列reverse_code = {code: val for val, code in huffman_code.items()}current_code = ""decompressed = []for bit in compressed_bits:current_code += bitif current_code in reverse_code:decompressed.append(reverse_code[current_code])current_code = ""return decompressed# 4. 测试:用灰度图验证
if __name__ == "__main__":# 读取灰度图(简化计算,单通道像素值0-255)img = cv2.imread("test.jpg", cv2.IMREAD_GRAYSCALE)img_flat = img.flatten() # 转为一维数组(方便处理)# 编码huffman_code = build_huffman_code(img_flat)compressed = huffman_compress(img_flat, huffman_code)# 解码decompressed = huffman_decompress(compressed, huffman_code)decompressed_img = np.array(decompressed).reshape(img.shape)# 验证:解压后与原图是否一致(无损)is_same = np.array_equal(img, decompressed_img)print(f"解压后与原图一致?{is_same}")# 计算压缩比(原始字节数 vs 压缩后比特数/8)original_size = len(img_flat) # 每个像素1字节(灰度图)compressed_size = len(compressed) / 8compression_ratio = compressed_size / original_sizeprint(f"哈夫曼压缩比:{compression_ratio:.2f}(越小越好)")
关键说明
- 实际工程中,哈夫曼编码会结合其他算法(如LZ77)使用(比如PNG的DEFLATE算法);
- 该简化版仅用于理解原理,未处理二进制对齐、文件存储等细节,生产环境建议用
zlib等成熟库。
六、实战3:DCT变换可视化(JPEG核心原理)
JPEG的核心是DCT变换(离散余弦变换),将图像从“空间域”转为“频率域”,再通过量化丢弃高频信息(无损压缩)。下面用代码可视化这个过程。
代码实现:DCT变换+量化
import cv2
import numpy as np
import matplotlib.pyplot as pltdef dct_visualization(input_path):# 1. 读取灰度图并预处理(8×8分块,JPEG标准)img = cv2.imread(input_path, cv2.IMREAD_GRAYSCALE)img = img.astype(np.float32) - 128 # 减去128,使像素值范围为[-128, 127](优化DCT效果)# 2. 定义JPEG标准量化表(高频系数量化步长更大,更容易归0)jpeg_quant_matrix = 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]], dtype=np.float32)# 3. 对每个8×8块执行DCT+量化h, w = img.shapedct_img = np.zeros_like(img)quant_img = np.zeros_like(img)for i in range(0, h, 8):for j in range(0, w, 8):# DCT变换(cv2.dct默认只处理单通道、浮点型)dct_block = cv2.dct(img[i:i+8, j:j+8])# 量化(核心:高频系数被大幅压缩)quant_block = np.round(dct_block / jpeg_quant_matrix)# 逆量化+逆DCT(恢复图像)idct_block = cv2.idct(quant_block * jpeg_quant_matrix)# 保存结果dct_img[i:i+8, j:j+8] = dct_blockquant_img[i:i+8, j:j+8] = idct_block + 128 # 加回128恢复原始亮度范围# 4. 可视化结果(原图 vs DCT频率图 vs 量化后恢复图)plt.figure(figsize=(15, 5))# 原图plt.subplot(1, 3, 1)plt.imshow(img + 128, cmap="gray")plt.title("原图")plt.axis("off")# DCT频率图(取对数增强可视化,高频系数值很小)plt.subplot(1, 3, 2)plt.imshow(np.log1p(np.abs(dct_img)), cmap="gray")plt.title("DCT频率域(亮处为低频)")plt.axis("off")# 量化后恢复图plt.subplot(1, 3, 3)plt.imshow(quant_img.astype(np.uint8), cmap="gray")plt.title("DCT+量化后恢复图")plt.axis("off")plt.tight_layout()plt.show()# 运行可视化
if __name__ == "__main__":dct_visualization("test.jpg")
关键说明
- DCT变换后,矩阵左上角是低频系数(对应图像轮廓),右下角是高频系数(对应细节);
- 量化过程中,高频系数被大的量化步长“抹平”(归0),这是JPEG有损压缩的核心;
- 逆变换后,图像会有轻微损失,但肉眼几乎无法察觉(除非量化步长过大)。
七、实战4:压缩效果量化评估(PSNR+SSIM)
光看主观画质不够,用PSNR(峰值信噪比)和SSIM(结构相似性)客观评估压缩质量。
代码实现:计算PSNR和SSIM
import cv2
import numpy as npdef calculate_psnr(img1, img2):# PSNR:越高越好(>30dB肉眼无明显差异)mse = np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2)if mse == 0:return float("inf") # 完全一致max_pixel = 255.0psnr = 20 * np.log10(max_pixel / np.sqrt(mse))return psnrdef calculate_ssim(img1, img2):# SSIM:越接近1越好(0-1)C1 = (0.01 * 255) ** 2C2 = (0.03 * 255) ** 2img1 = img1.astype(np.float32)img2 = img2.astype(np.float32)mu1 = cv2.GaussianBlur(img1, (11, 11), 1.5)mu2 = cv2.GaussianBlur(img2, (11, 11), 1.5)mu1_sq = mu1 ** 2mu2_sq = mu2 ** 2mu1_mu2 = mu1 * mu2sigma1_sq = cv2.GaussianBlur(img1 ** 2, (11, 11), 1.5) - mu1_sqsigma2_sq = cv2.GaussianBlur(img2 ** 2, (11, 11), 1.5) - mu2_sqsigma12 = cv2.GaussianBlur(img1 * img2, (11, 11), 1.5) - mu1_mu2ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2))return np.mean(ssim_map)# 测试:对比JPEG压缩前后的效果
if __name__ == "__main__":img_original = cv2.imread("test.jpg")# 保存JPEG压缩图cv2.imwrite("compressed_test.jpg", img_original, [int(cv2.IMWRITE_JPEG_QUALITY), 50])img_compressed = cv2.imread("compressed_test.jpg")# 计算指标psnr = calculate_psnr(img_original, img_compressed)ssim = calculate_ssim(img_original, img_compressed)print(f"PSNR: {psnr:.2f} dB")print(f"SSIM: {ssim:.4f}")
关键说明
- PSNR > 30dB:肉眼无法区分压缩图和原图;
- SSIM > 0.9:结构相似性极高,压缩质量优秀;
- 实际开发中,可根据这两个指标调整压缩参数(比如JPEG质量)。
八、总结与进阶建议
- 基础应用:开发中优先用OpenCV/Pillow的现成接口,调整压缩参数即可满足大部分需求;
- 原理理解:哈夫曼编码(无损)、DCT变换(有损)是核心,通过简化代码能快速掌握逻辑;
- 进阶方向:学习新一代编码(VVC/AVIF)、基于深度学习的图像压缩(如TensorFlow/PyTorch实现)。
