一、实验目的
掌握图像压缩的必要性;
掌握常见的图像压缩标准;
掌握常见的图像压缩方法分类;
掌握常见的图像压缩方法原理与实现(包括哈夫曼编码、算术编码、行程编码方法等);
了解我国音视频编解码标准的发展。
二、实验内容
- 学习图像压缩章节内容。
- 读取灰度图像cameraman.jpg和barbara.jpg,从以下方法中选择两种实现对图像的压缩,并显示压缩比和压缩前后对比图像,试分析所选算法的优缺点。
A.哈夫曼编码; B. 算术编码; C. 行程编码;D. 小波图像编码。
三、完整实验程序、结果与分析
- 图像压缩是通过减少图像数据的冗余性来实现高效存储与传输的关键技术,其核心目标是在保证视觉质量的前提下降低数据量。根据压缩后信息是否完整保留,可分为无损压缩和有损压缩两类。
无损压缩基于统计冗余(如哈夫曼编码、行程编码),通过构建最优编码表将高频像素值用短码表示,实现精确还原但压缩比有限(通常2:1~3:1),适用于医学影像、工程制图等对精度要求严格的场景。
有损压缩则利用人类视觉特性(如小波编码、JPEG),通过傅里叶变换、小波变换等频域分析分离高频细节与低频主体,结合量化和阈值处理舍弃次要信息,可获得10:1以上的高压缩比,广泛用于网络图像传输和视频流媒体。
其中,哈夫曼编码通过贪心算法构建最优前缀码树,实现信息熵极限压缩,但需额外存储码表;小波编码通过多分辨率分析实现图像分层压缩,在保留主体特征的同时显著减少数据量,但会引入振铃效应等失真。不同算法在压缩效率、重建质量和计算复杂度上存在显著差异,实际应用需根据场景需求权衡选择。 - 选择A.哈夫曼编码和D. 小波图像编码
代码
显示压缩比和压缩前后对比图像,试分析所选算法的优缺点。
哈夫曼编码
压缩比: 通常较低(约1.2-1.5倍),因为自然图像像素分布均匀,且需存储编码表。
优点: 无损压缩,精确还原图像。
缺点: 压缩比低,不适合高压缩需求场景,存储编码表增加额外开销。
小波编码
压缩比: 较高(可达5-10倍),通过去除高频细节实现。
优点: 高压缩比,保留主要视觉特征。
缺点: 有损压缩,高频细节丢失,图像可能出现模糊。
对比结论
哈夫曼编码适用于需要无损压缩的场景(如医学图像),但压缩效率有限。
小波编码适用于允许有损压缩的高效存储(如网络传输),但会损失部分细节。
四、问题及心得
在本次实验中,主要遇到图像读取失败及压缩算法实现问题。读取barbara.jpg
时因路径格式错误导致加载失败,经排查发现路径需使用原始字符串(r"..."
)避免转义,并通过os.path.exists
验证路径存在性。此外,文件实际格式与扩展名不符时,OpenCV无法读取,改用PIL库并添加文件头校验后解决。压缩算法方面,哈夫曼编码因像素分布均匀导致压缩比低(约1.3倍),且编码表存储开销大;小波编码通过阈值量化高频分量实现高压缩比(约5倍),但重构图像出现边缘模糊。实验表明,无损压缩(哈夫曼)适合精度要求高的场景,而有损压缩(小波)更适用于存储优化。此次实践深化了对路径处理、文件格式验证及压缩算法特性的理解,同时认识到错误隔离和模块化测试的重要性。
完整代码
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pywt
from heapq import heappush, heappop, heapify
import os
from PIL import Image
class HuffmanCoder:
def __init__(self):
self.codes = {}
def _build_heap(self, freq):
heap = [[weight, [pixel, ""]] for pixel, weight in freq.items()]
heapify(heap)
while len(heap) > 1:
lo = heappop(heap)
hi = heappop(heap)
for pair in lo[1:]:
pair[1] = '0' + pair[1]
for pair in hi[1:]:
pair[1] = '1' + pair[1]
heappush(heap, [lo[0] + hi[0]] + lo[1:] + hi[1:])
return heap[0] if heap else []
def _build_codes(self, heap_entry):
codes = {}
for pair in heap_entry[1:]:
pixel, code = pair
codes[pixel] = code
return codes
def encode(self, data):
freq = {}
for pixel in data.flatten():
freq[pixel] = freq.get(pixel, 0) + 1
if not freq:
return ""
heap_entry = self._build_heap(freq)
if not heap_entry:
return ""
self.codes = self._build_codes(heap_entry)
encoded_data = ''.join([self.codes[pixel] for pixel in data.flatten()])
return encoded_data
def decode(self, encoded_data, shape):
inv_codes = {v: k for k, v in self.codes.items()}
current_code = ""
decoded_pixels = []
for bit in encoded_data:
current_code += bit
if current_code in inv_codes:
decoded_pixels.append(inv_codes[current_code])
current_code = ""
return np.array(decoded_pixels, dtype=np.uint8).reshape(shape)
def compress_huffman(img):
coder = HuffmanCoder()
encoded_data = coder.encode(img)
data_bits = len(encoded_data)
table_bits = 0
for pixel, code in coder.codes.items():
table_bits += 8 + 4 + len(code)
total_bits = data_bits + table_bits
decoded_img = coder.decode(encoded_data, img.shape)
return decoded_img, total_bits
def compress_wavelet(img, threshold=10.0, quant_factor=20):
coeffs = pywt.wavedec2(img, 'haar', level=1)
coeff_arr, coeff_slices = pywt.coeffs_to_array(coeffs)
coeff_arr_thresh = coeff_arr.copy()
coeff_arr_thresh[np.abs(coeff_arr_thresh) < threshold] = 0
quantized = np.round(coeff_arr_thresh * quant_factor).astype(np.int32)
non_zero = quantized != 0
non_zero_coords = np.transpose(np.nonzero(non_zero))
non_zero_values = quantized[non_zero]
num_non_zero = len(non_zero_values)
total_bits = num_non_zero * (16 + 32)
dequantized = non_zero_values.astype(np.float32) / quant_factor
recon_coeff_arr = np.zeros_like(coeff_arr, dtype=np.float32)
recon_coeff_arr[non_zero] = dequantized
recon_coeffs = pywt.array_to_coeffs(recon_coeff_arr, coeff_slices, output_format='wavedec2')
reconstructed = pywt.waverec2(recon_coeffs, 'haar')
reconstructed = np.clip(reconstructed, 0, 255).astype(np.uint8)
return reconstructed, total_bits
def plot_images(original, reconstructed, title, compression_ratio):
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(original, cmap='gray')
plt.title('Original Image')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(reconstructed, cmap='gray')
plt.title(f'{title}\nCR: {compression_ratio:.2f}')
plt.axis('off')
plt.show()
def safe_imread(path):
# 基础路径验证
if not os.path.exists(path):
print(f"❌ 路径不存在: {path}")
print("可能原因:")
print(f"1. 请检查D盘是否存在")
print(f"2. 确认完整路径:{os.path.abspath(path)}")
return None
# 文件权限检查
if not os.access(path, os.R_OK):
print(f"⛔ 无读取权限: {path}")
return None
# 文件大小检查
file_size = os.path.getsize(path)
if file_size < 1024:
print(f"⚠️ 文件过小({file_size}字节),可能已损坏: {path}")
# 尝试用OpenCV读取
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
if img is not None:
return img
try:
print(f"⚠️ OpenCV读取失败,尝试PIL读取: {path}")
pil_img = Image.open(path).convert('L')
return np.array(pil_img, dtype=np.uint8)
except Exception as e:
print(f"❌ 双重读取均失败: {path}")
print(f"错误详情: {str(e)}")
# 文件头验证
with open(path, 'rb') as f:
header = f.read(4).hex()
print(f"📄 文件头(十六进制): {header}")
print("正常JPEG应以 ffd8ff 开头")
return None
def main():
img_paths = [
r"D:\tuxiang\cameraman.jpg", # 使用原始字符串
r"D:\tuxiang\barbara.jpg",
]
for path in img_paths:
print(f"\n{'=' * 40}\n处理图像: {path}")
# 安全读取图像
img = safe_imread(path)
if img is None:
print("跳过处理...")
continue
# 显示基本信息
print(f"✅ 成功读取 | 尺寸: {img.shape} | 数据类型: {img.dtype}")
# 哈夫曼编码
try:
huff_img, huff_bits = compress_huffman(img)
cr_huff = (img.size * 8) / huff_bits if huff_bits != 0 else 0
plot_images(img, huff_img, 'Huffman Encoding', cr_huff)
except Exception as e:
print(f"哈夫曼编码错误: {str(e)}")
# 小波编码
try:
wavelet_img, wavelet_bits = compress_wavelet(img)
cr_wavelet = (img.size * 8) / wavelet_bits if wavelet_bits != 0 else 0
plot_images(img, wavelet_img, 'Wavelet Encoding', cr_wavelet)
except Exception as e:
print(f"小波编码错误: {str(e)}")
if __name__ == "__main__":
main()