《数字图像处理》第8章-图像压缩和水印

前言

大家好!今天给大家带来《数字图像处理》第 8 章的全面解析 ------ 图像压缩和水印。在这个图像、视频爆炸的时代,图像压缩技术无处不在(比如我们手机里的 JPG 照片、视频网站的流媒体),而数字水印则是多媒体版权保护的核心技术。

本文会从基础概念到实战代码,把图像压缩的各类编码算法(霍夫曼、算术、LZW、小波等)和数字水印技术讲透,每个知识点都配有可直接运行的 Python 代码效果对比图,语言通俗易懂,零基础也能看懂!

前言

目录

引言

[本章知识框架(Mermaid 思维导图)](#本章知识框架(Mermaid 思维导图))

学习目标

[8.1 基础](#8.1 基础)

[8.1.1 编码冗余](#8.1.1 编码冗余)

[8.1.2 空间冗余和时间冗余](#8.1.2 空间冗余和时间冗余)

[8.1.3 无关信息](#8.1.3 无关信息)

[8.1.4 度量图像信息](#8.1.4 度量图像信息)

[1. 信息熵(香农熵)](#1. 信息熵(香农熵))

[2. 平均码字长度](#2. 平均码字长度)

[3. 压缩比](#3. 压缩比)

[8.1.5 保真度准则](#8.1.5 保真度准则)

[1. 客观准则](#1. 客观准则)

[2. 主观准则](#2. 主观准则)

[8.1.6 图像压缩模型](#8.1.6 图像压缩模型)

压缩模型流程图(Mermaid)

[8.1.7 图像格式、存储器(容器)和压缩标准](#8.1.7 图像格式、存储器(容器)和压缩标准)

[8.2 霍夫曼编码](#8.2 霍夫曼编码)

核心原理

实现步骤

实战代码:霍夫曼编码实现

[8.3 Golomb 编码](#8.3 Golomb 编码)

核心原理

[实战代码:Golomb 编码实现](#实战代码:Golomb 编码实现)

[8.4 算术编码](#8.4 算术编码)

核心原理

[8.4.1 自适应上下文相关概率估计](#8.4.1 自适应上下文相关概率估计)

实战代码:算术编码实现

[8.5 LZW 编码](#8.5 LZW 编码)

核心原理

[实战代码:LZW 编码实现](#实战代码:LZW 编码实现)

[8.6 行程编码](#8.6 行程编码)

核心原理

[8.6.1 一维 CCITT 压缩](#8.6.1 一维 CCITT 压缩)

[8.6.2 二维 CCITT 压缩](#8.6.2 二维 CCITT 压缩)

实战代码:行程编码实现

[8.7 基于符号的编码](#8.7 基于符号的编码)

[8.7.1 JBIG2 压缩](#8.7.1 JBIG2 压缩)

[实战代码:JBIG2 简化实现](#实战代码:JBIG2 简化实现)

[8.8 比特平面编码](#8.8 比特平面编码)

核心原理

实战代码:比特平面编码实现

[8.9 块变换编码](#8.9 块变换编码)

核心原理

[8.9.1 变换的选择](#8.9.1 变换的选择)

[8.9.2 子图像尺寸选择](#8.9.2 子图像尺寸选择)

[8.9.3 比特分配](#8.9.3 比特分配)

[实战代码:DCT 块变换编码实现](#实战代码:DCT 块变换编码实现)

[8.10 预测编码](#8.10 预测编码)

[8.10.1 无损预测编码](#8.10.1 无损预测编码)

[8.10.2 运动补偿预测残差](#8.10.2 运动补偿预测残差)

[8.10.3 有损预测编码](#8.10.3 有损预测编码)

[8.10.4 最优预测器](#8.10.4 最优预测器)

[8.10.5 最优化](#8.10.5 最优化)

[8.11 小波编码](#8.11 小波编码)

[8.11.1 小波的选择](#8.11.1 小波的选择)

[8.11.2 分解级数的选择](#8.11.2 分解级数的选择)

[8.11.3 量化器设计](#8.11.3 量化器设计)

[8.11.4 JPEG-2000](#8.11.4 JPEG-2000)

[8.12 数字图像水印](#8.12 数字图像水印)

核心分类

[实战代码:DWT 域鲁棒水印(JPEG-2000 兼容)](#实战代码:DWT 域鲁棒水印(JPEG-2000 兼容))

小结、参考文献和延伸读物

小结

参考文献

延伸读物

习题


引言

图像压缩的核心目标是在尽可能保证图像质量的前提下,减少存储和传输所需的数据量。想象一下:一张未经压缩的 1080P 彩色图像,数据量约为 6MB,而经过 JPEG 压缩后可能只有几十 KB,这就是压缩的价值!

数字水印则是在图像中嵌入不可见的标识信息(比如版权信息),用于版权保护、内容溯源等场景,要求嵌入的水印不影响图像视觉效果,且具有抗攻击(如压缩、裁剪、滤波)能力。

学习目标

  1. 理解图像冗余的类型和图像信息的度量方法;
  2. 掌握无损压缩(霍夫曼、LZW、行程编码等)的原理和实现;
  3. 掌握有损压缩(变换编码、小波编码等)的核心思想和实战;
  4. 理解数字水印的嵌入和提取原理,能实现基础的水印算法;
  5. 能根据实际场景选择合适的压缩算法,评估压缩质量。

8.1 基础

8.1.1 编码冗余

核心概念 :编码冗余是指使用了过长的编码来表示图像像素(或符号),比如用 8 位二进制表示一个出现概率极高的灰度值,而理论上可以用更短的编码。

数学表达

实战代码:计算编码冗余并对比

复制代码
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter

# 设置中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 生成模拟图像灰度数据(简化版)
# 模拟高概率灰度值:128(概率0.8),其他灰度值(概率0.2)
np.random.seed(42)
gray_data = np.random.choice([128, 64, 192, 32], size=10000, p=[0.8, 0.05, 0.05, 0.1])

# 1. 计算信息熵
counts = Counter(gray_data)
total = len(gray_data)
entropy = 0
for val, cnt in counts.items():
    p = cnt / total
    entropy -= p * np.log2(p)

# 2. 计算固定长度编码的平均长度(8位编码)
fixed_len = 8
fixed_avg_len = fixed_len

# 3. 计算编码冗余
coding_redundancy = fixed_avg_len - entropy

# 输出结果
print(f"图像灰度信息熵:{entropy:.2f} 比特/像素")
print(f"8位固定编码平均长度:{fixed_avg_len} 比特/像素")
print(f"编码冗余:{coding_redundancy:.2f} 比特/像素")

# 可视化对比
plt.figure(figsize=(8, 5))
categories = ['信息熵', '8位固定编码', '编码冗余']
values = [entropy, fixed_avg_len, coding_redundancy]
colors = ['#4CAF50', '#FF9800', '#F44336']

bars = plt.bar(categories, values, color=colors)
# 在柱子上标注数值
for bar, val in zip(bars, values):
    plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1, 
             f'{val:.2f}', ha='center', fontsize=12)

plt.ylabel('比特/像素')
plt.title('编码冗余对比')
plt.ylim(0, 9)
plt.grid(axis='y', alpha=0.3)
plt.show()

效果对比图位置:运行上述代码后,会生成 "编码冗余对比" 柱状图,直观看到 8 位固定编码的冗余量。

8.1.2 空间冗余和时间冗余

  • 空间冗余:图像中相邻像素往往具有相似的灰度值(比如蓝天区域、白色墙壁),这种像素间的相关性就是空间冗余。
  • 时间冗余:视频序列中相邻帧的内容高度相似(比如静态场景的视频),这种帧间相关性就是时间冗余。

可视化代码:空间冗余展示

复制代码
import cv2
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 读取测试图像(建议用自己的图像,这里生成模拟图像)
# 生成一个包含大面积相似区域的图像
img = np.ones((256, 256), dtype=np.uint8) * 200  # 背景为浅灰色
# 画一个矩形(深灰色)
img[50:150, 50:150] = 100

# 计算相邻像素的差值(空间相关性)
row_diff = np.abs(img[:, 1:] - img[:, :-1])  # 行方向相邻差值
col_diff = np.abs(img[1:, :] - img[:-1, :])  # 列方向相邻差值

# 统计差值为0的比例(比例越高,空间冗余越大)
row_zero_ratio = (row_diff == 0).sum() / row_diff.size
col_zero_ratio = (col_diff == 0).sum() / col_diff.size

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# 原始图像
axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
axes[0].set_title('原始图像(含大面积相似区域)')
axes[0].axis('off')

# 行方向差值
axes[1].imshow(row_diff, cmap='gray', vmin=0, vmax=255)
axes[1].set_title(f'行方向相邻像素差值\n零值比例:{row_zero_ratio:.2f}')
axes[1].axis('off')

# 列方向差值
axes[2].imshow(col_diff, cmap='gray', vmin=0, vmax=255)
axes[2].set_title(f'列方向相邻像素差值\n零值比例:{col_zero_ratio:.2f}')
axes[2].axis('off')

plt.tight_layout()
plt.show()

效果对比图位置:运行后可看到原始图像、行 / 列方向差值图,差值图中大部分区域为黑色(差值为 0),直观体现空间冗余。

8.1.3 无关信息

无关信息是指人类视觉系统无法感知的信息(比如超出人眼分辨能力的细节),压缩时可以去除这些信息而不影响视觉效果(有损压缩的核心思想)。

8.1.4 度量图像信息

  1. 信息熵(香农熵)
  1. 平均码字长度
  1. 压缩比

实战代码:计算图像信息熵和压缩比

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt
from collections import Counter

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 读取图像(用OpenCV,也可以用PIL)
# 替换为自己的图像路径
img = cv2.imread('test_img.jpg', 0)  # 0表示灰度图
if img is None:
    # 如果读取失败,生成模拟图像
    img = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8)

# 1. 计算信息熵
flat_img = img.flatten()
counts = Counter(flat_img)
total = len(flat_img)
entropy = 0
for val, cnt in counts.items():
    p = cnt / total
    if p > 0:
        entropy -= p * np.log2(p)

# 2. 计算压缩前数据量(8位/像素)
original_size = flat_img.size * 8  # 比特

# 3. 模拟霍夫曼编码后的平均长度(简化版,实际需实现霍夫曼编码)
# 这里用熵近似最优编码长度
huffman_avg_len = entropy
compressed_size = flat_img.size * huffman_avg_len

# 4. 计算压缩比
cr = original_size / compressed_size

# 输出结果
print(f"图像信息熵:{entropy:.2f} 比特/像素")
print(f"压缩前数据量:{original_size/1024:.2f} KB")
print(f"霍夫曼编码后数据量:{compressed_size/1024:.2f} KB")
print(f"压缩比:{cr:.2f}")

# 可视化
plt.figure(figsize=(10, 4))
categories = ['压缩前', '霍夫曼编码后']
sizes = [original_size/1024, compressed_size/1024]
colors = ['#FF6B6B', '#4ECDC4']

bars = plt.bar(categories, sizes, color=colors)
for bar, val in zip(bars, sizes):
    plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, 
             f'{val:.2f} KB', ha='center', fontsize=12)

plt.ylabel('数据量(KB)')
plt.title('压缩前后数据量对比(基于信息熵)')
plt.grid(axis='y', alpha=0.3)
plt.show()

8.1.5 保真度准则

用于评估压缩后图像的质量,分为客观准则主观准则

  1. 客观准则
  1. 主观准则

通过人眼主观评分(如 MOS 评分:1-5 分,5 分为最优)。

实战代码:计算 MSE 和 PSNR

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 读取原始图像和压缩后的图像(这里模拟压缩后的图像)
original_img = cv2.imread('test_img.jpg', 0)
if original_img is None:
    original_img = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8)

# 模拟有损压缩后的图像(添加高斯噪声)
np.random.seed(42)
noise = np.random.normal(0, 10, original_img.shape).astype(np.uint8)
compressed_img = np.clip(original_img + noise, 0, 255)

# 计算MSE
mse = np.mean((original_img - compressed_img) ** 2)

# 计算PSNR
if mse == 0:
    psnr = float('inf')
else:
    psnr = 10 * np.log10((255 ** 2) / mse)

# 输出结果
print(f"均方误差(MSE):{mse:.2f}")
print(f"峰值信噪比(PSNR):{psnr:.2f} dB")

# 可视化对比
fig, axes = plt.subplots(1, 2, figsize=(12, 6))

axes[0].imshow(original_img, cmap='gray', vmin=0, vmax=255)
axes[0].set_title('原始图像')
axes[0].axis('off')

axes[1].imshow(compressed_img, cmap='gray', vmin=0, vmax=255)
axes[1].set_title(f'压缩后图像\nMSE={mse:.2f}, PSNR={psnr:.2f} dB')
axes[1].axis('off')

plt.tight_layout()
plt.show()

效果对比图位置:运行后可看到原始图像和模拟压缩后的图像,以及对应的 MSE/PSNR 值,PSNR 越高(一般 > 30dB),视觉效果越好。

8.1.6 图像压缩模型

核心说明

  • 源编码器:去除冗余(无损:仅去除编码 / 空间冗余;有损:额外去除无关信息);
  • 熵编码器:对源编码器输出的符号进行最优编码,进一步减少编码冗余;
  • 解码器是编码器的逆过程。

8.1.7 图像格式、存储器(容器)和压缩标准

格式 / 标准 类型 压缩方式 应用场景
BMP 位图 无损(无压缩) 无损存储、图像处理
JPEG 静态图像 有损(DCT 变换) 照片、网页图片
PNG 静态图像 无损(LZW) 图标、透明图像
GIF 动态图像 无损(LZW) 简单动图、表情包
JPEG-2000 静态图像 有损 / 无损(小波变换) 医疗图像、高清图像
H.264/H.265 视频 有损(变换 + 预测) 视频流媒体、监控
JBIG2 二值图像 无损 / 有损 文档扫描、传真

8.2 霍夫曼编码

核心原理

霍夫曼编码是一种无损压缩编码 ,核心思想是:对出现概率高的符号分配短编码,概率低的符号分配长编码,且编码是前缀码(无歧义)。

实现步骤

  1. 统计每个符号的出现概率;
  2. 构建霍夫曼树(每次选择概率最小的两个节点合并);
  3. 从根节点遍历到叶子节点,生成每个符号的编码;
  4. 用生成的编码对图像数据进行编码。

实战代码:霍夫曼编码实现

复制代码
import numpy as np
import cv2
import heapq
from collections import Counter, defaultdict

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 定义霍夫曼树节点
class HuffmanNode:
    def __init__(self, symbol=None, prob=0, left=None, right=None):
        self.symbol = symbol
        self.prob = prob
        self.left = left
        self.right = right
    
    # 重载比较运算符,用于堆排序
    def __lt__(self, other):
        return self.prob < other.prob

# 生成霍夫曼编码表
def build_huffman_code(node, code="", code_dict=None):
    if code_dict is None:
        code_dict = {}
    if node.symbol is not None:
        code_dict[node.symbol] = code
    else:
        build_huffman_code(node.left, code + "0", code_dict)
        build_huffman_code(node.right, code + "1", code_dict)
    return code_dict

# 霍夫曼编码
def huffman_encoding(data):
    # 1. 统计符号概率
    counts = Counter(data)
    total = len(data)
    prob_dict = {sym: cnt/total for sym, cnt in counts.items()}
    
    # 2. 构建霍夫曼堆
    heap = [HuffmanNode(symbol=sym, prob=prob) for sym, prob in prob_dict.items()]
    heapq.heapify(heap)
    
    # 3. 构建霍夫曼树
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        merged = HuffmanNode(prob=left.prob + right.prob, left=left, right=right)
        heapq.heappush(heap, merged)
    
    # 4. 生成编码表
    root = heapq.heappop(heap)
    code_dict = build_huffman_code(root)
    
    # 5. 编码数据
    encoded_data = "".join([code_dict[sym] for sym in data])
    
    return encoded_data, code_dict, prob_dict

# 霍夫曼解码
def huffman_decoding(encoded_data, code_dict):
    # 反转编码表:编码->符号
    reverse_code_dict = {code: sym for sym, code in code_dict.items()}
    decoded_data = []
    current_code = ""
    
    for bit in encoded_data:
        current_code += bit
        if current_code in reverse_code_dict:
            decoded_data.append(reverse_code_dict[current_code])
            current_code = ""
    
    return np.array(decoded_data)

# 主函数
if __name__ == "__main__":
    # 读取/生成图像数据
    img = cv2.imread('test_img.jpg', 0)
    if img is None:
        img = np.random.randint(0, 256, size=(128, 128), dtype=np.uint8)
    flat_img = img.flatten()
    
    # 霍夫曼编码
    encoded_data, code_dict, prob_dict = huffman_encoding(flat_img)
    
    # 霍夫曼解码
    decoded_flat = huffman_decoding(encoded_data, code_dict)
    decoded_img = decoded_flat.reshape(img.shape)
    
    # 计算压缩比
    original_size = len(flat_img) * 8  # 8位/像素
    compressed_size = len(encoded_data)
    cr = original_size / compressed_size
    
    # 计算平均编码长度
    avg_len = sum([prob * len(code) for sym, prob in prob_dict.items() for code in [code_dict[sym]]])
    
    # 验证解码是否正确
    is_correct = np.array_equal(img, decoded_img)
    
    # 输出结果
    print(f"霍夫曼编码平均长度:{avg_len:.2f} 比特/像素")
    print(f"压缩前数据量:{original_size/1024:.2f} KB")
    print(f"压缩后数据量:{compressed_size/1024:.2f} KB")
    print(f"压缩比:{cr:.2f}")
    print(f"解码是否正确:{is_correct}")
    
    # 可视化对比
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始图像')
    axes[0].axis('off')
    
    axes[1].imshow(decoded_img, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'霍夫曼解码后图像\n压缩比={cr:.2f}')
    axes[1].axis('off')
    
    plt.tight_layout()
    plt.show()

效果对比图位置:运行后可看到原始图像和解码后的图像(完全一致,体现无损压缩),以及压缩比数据。

8.3 Golomb 编码

核心原理

实战代码:Golomb 编码实现

复制代码
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# Golomb编码
def golomb_encode(x, m):
    """
    x: 非负整数
    m: 编码参数
    """
    if x < 0:
        raise ValueError("Golomb编码仅支持非负整数")
    
    # 计算q和r
    q = x // m
    r = x % m
    
    # 1. 一元编码q:q个1 + 1个0
    unary_code = "1" * q + "0"
    
    # 2. 二进制编码r:需要k位,k = ceil(log2(m))
    k = int(np.ceil(np.log2(m)))
    # 计算2^k - m
    rem = (1 << k) - m
    
    # 3. 生成r的编码
    if r < rem:
        binary_code = bin(r)[2:].zfill(k-1)  # k-1位
    else:
        binary_code = bin(r + rem)[2:].zfill(k)  # k位
    
    # 合并编码
    return unary_code + binary_code

# Golomb解码
def golomb_decode(code, m):
    """
    code: Golomb编码字符串
    m: 编码参数
    """
    # 1. 解析q(一元编码部分)
    q = 0
    while q < len(code) and code[q] == "1":
        q += 1
    # 跳过0
    if q >= len(code) or code[q] != "0":
        raise ValueError("无效的Golomb编码")
    code = code[q+1:]
    
    # 2. 解析r
    k = int(np.ceil(np.log2(m)))
    rem = (1 << k) - m
    
    if len(code) < k-1:
        raise ValueError("无效的Golomb编码")
    
    # 尝试k-1位
    r_candidate = int(code[:k-1], 2) if k-1 > 0 else 0
    if r_candidate < rem:
        r = r_candidate
        code = code[k-1:]
    else:
        if len(code) < k:
            raise ValueError("无效的Golomb编码")
        r = int(code[:k], 2) - rem
        code = code[k:]
    
    # 计算x
    x = q * m + r
    return x, code

# 主函数
if __name__ == "__main__":
    # 测试数据(行程编码的行程长度)
    run_lengths = [0, 1, 3, 5, 8, 10, 15, 20]
    m = 8  # 编码参数
    
    # 编码
    encoded_codes = [golomb_encode(x, m) for x in run_lengths]
    
    # 解码
    decoded_lengths = []
    for code in encoded_codes:
        x, _ = golomb_decode(code, m)
        decoded_lengths.append(x)
    
    # 输出结果
    print("原始行程长度:", run_lengths)
    print("Golomb编码:", encoded_codes)
    print("解码后长度:", decoded_lengths)
    print("解码是否正确:", run_lengths == decoded_lengths)
    
    # 可视化编码长度对比
    original_bits = [np.ceil(np.log2(x+1)) if x > 0 else 1 for x in run_lengths]
    encoded_bits = [len(code) for code in encoded_codes]
    
    x = np.arange(len(run_lengths))
    width = 0.35
    
    plt.figure(figsize=(10, 6))
    plt.bar(x - width/2, original_bits, width, label='固定长度编码', color='#FF6B6B')
    plt.bar(x + width/2, encoded_bits, width, label='Golomb编码', color='#4ECDC4')
    
    plt.xlabel('行程长度')
    plt.ylabel('编码长度(比特)')
    plt.title('Golomb编码 vs 固定长度编码')
    plt.xticks(x, run_lengths)
    plt.legend()
    plt.grid(axis='y', alpha=0.3)
    plt.show()

效果对比图位置:运行后可看到 Golomb 编码与固定长度编码的长度对比,Golomb 编码对小数值的行程长度更高效。

8.4 算术编码

核心原理

算术编码不单独为每个符号分配编码,而是将整个符号序列映射到 [0,1) 区间内的一个小数,最终输出该小数的二进制表示。相比霍夫曼编码,算术编码能更接近信息熵,且支持自适应概率估计。

8.4.1 自适应上下文相关概率估计

自适应算术编码会根据已编码的符号动态更新概率分布,无需预先统计符号概率,更适合实时数据压缩。

实战代码:算术编码实现

复制代码
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 算术编码
def arithmetic_encode(symbols, prob_dict):
    """
    symbols: 符号序列
    prob_dict: 符号概率字典
    """
    # 构建累积概率字典
    sorted_syms = sorted(prob_dict.keys())
    cum_prob = {}
    current = 0
    for sym in sorted_syms:
        cum_prob[sym] = current
        current += prob_dict[sym]
    
    # 初始化区间
    low = 0.0
    high = 1.0
    
    # 编码每个符号
    for sym in symbols:
        range_val = high - low
        high = low + range_val * (cum_prob[sym] + prob_dict[sym])
        low = low + range_val * cum_prob[sym]
    
    # 选择区间中点作为编码值
    code = (low + high) / 2
    
    return code, cum_prob

# 算术解码
def arithmetic_decode(code, length, prob_dict, cum_prob):
    """
    code: 编码值
    length: 符号序列长度
    prob_dict: 符号概率字典
    cum_prob: 累积概率字典
    """
    sorted_syms = sorted(prob_dict.keys())
    decoded_syms = []
    
    low = 0.0
    high = 1.0
    
    for _ in range(length):
        range_val = high - low
        # 计算相对位置
        pos = (code - low) / range_val
        
        # 找到对应的符号
        sym = None
        for s in sorted_syms:
            if cum_prob[s] <= pos < cum_prob[s] + prob_dict[s]:
                sym = s
                break
        
        decoded_syms.append(sym)
        
        # 更新区间
        high = low + range_val * (cum_prob[sym] + prob_dict[sym])
        low = low + range_val * cum_prob[sym]
    
    return decoded_syms

# 自适应算术编码(简化版)
def adaptive_arithmetic_encode(symbols):
    # 初始化概率(等概率)
    sym_set = list(set(symbols))
    prob_dict = {sym: 1/len(sym_set) for sym in sym_set}
    
    codes = []
    cum_probs = []
    
    # 逐符号编码,动态更新概率
    count_dict = {sym: 1 for sym in sym_set}  # 平滑计数,避免概率为0
    total = len(sym_set)
    
    low = 0.0
    high = 1.0
    
    for sym in symbols:
        # 计算当前累积概率
        sorted_syms = sorted(count_dict.keys())
        cum_prob = {}
        current = 0
        for s in sorted_syms:
            cum_prob[s] = current / total
            current += count_dict[s]
        
        # 编码当前符号
        range_val = high - low
        high = low + range_val * (cum_prob[sym] + count_dict[sym]/total)
        low = low + range_val * cum_prob[sym]
        
        # 更新计数和概率
        count_dict[sym] += 1
        total += 1
        
        codes.append((low + high)/2)
        cum_probs.append(cum_prob)
    
    return codes[-1], count_dict

# 主函数
if __name__ == "__main__":
    # 测试数据(图像灰度值序列)
    symbols = [128, 128, 64, 128, 192, 128, 128, 64, 64, 128]
    # 预先统计概率
    prob_dict = {128: 0.6, 64: 0.3, 192: 0.1}
    
    # 固定概率算术编码
    code, cum_prob = arithmetic_encode(symbols, prob_dict)
    decoded_syms = arithmetic_decode(code, len(symbols), prob_dict, cum_prob)
    
    # 自适应算术编码
    adaptive_code, final_counts = adaptive_arithmetic_encode(symbols)
    
    # 输出结果
    print("原始符号序列:", symbols)
    print("固定概率算术编码值:", code)
    print("解码后序列:", decoded_syms)
    print("自适应算术编码值:", adaptive_code)
    print("固定概率解码是否正确:", symbols == decoded_syms)
    
    # 可视化编码效率
    # 计算信息熵
    entropy = -sum([p * np.log2(p) for p in prob_dict.values()])
    # 固定编码长度(8位)
    fixed_len = 8 * len(symbols)
    # 算术编码长度(二进制位数,用log2(1/(high-low))近似)
    range_val = 1.0
    for sym in symbols:
        range_val *= prob_dict[sym]
    arithmetic_bits = np.ceil(-np.log2(range_val))
    
    plt.figure(figsize=(8, 5))
    categories = ['8位固定编码', '算术编码', '信息熵下限']
    values = [fixed_len, arithmetic_bits, entropy*len(symbols)]
    colors = ['#FF9800', '#4CAF50', '#F44336']
    
    bars = plt.bar(categories, values, color=colors)
    for bar, val in zip(bars, values):
        plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, 
                 f'{val:.1f}', ha='center', fontsize=12)
    
    plt.ylabel('总比特数')
    plt.title('算术编码效率对比')
    plt.grid(axis='y', alpha=0.3)
    plt.show()

效果对比图位置:运行后可看到算术编码与固定编码、信息熵下限的对比,算术编码更接近信息熵下限,效率更高。

8.5 LZW 编码

核心原理

LZW(Lempel-Ziv-Welch)编码是一种无损压缩算法,核心思想是:

  1. 初始化字典,包含所有单个符号;
  2. 遍历数据,逐步构建更长的符号序列,将新序列加入字典;
  3. 用字典索引代替符号序列,实现压缩。

实战代码:LZW 编码实现

python 复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

# 设置中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


# LZW编码(修复字典键类型,统一为元组)
def lzw_encode(data, max_dict_size=4096):
    """
    data: 输入数据(一维数组)
    max_dict_size: 字典最大容量
    """
    # 1. 初始化字典:单个符号转为长度为1的元组作为键(统一类型)
    unique_syms = list(set(data))
    # 键:(sym,) 元组,值:索引(避免单个值和元组混用)
    dictionary = {(sym,): idx for idx, sym in enumerate(unique_syms)}
    next_code = len(unique_syms)

    # 2. 编码过程(当前序列统一为元组)
    current_seq = (data[0],)  # 初始化为元组
    encoded = []

    for sym in data[1:]:
        combined_seq = current_seq + (sym,)  # 元组拼接,统一逻辑

        if combined_seq in dictionary:
            current_seq = combined_seq
        else:
            # 输出当前序列的索引
            encoded.append(dictionary[current_seq])
            # 将新序列加入字典(如果字典未满)
            if next_code < max_dict_size:
                dictionary[combined_seq] = next_code
                next_code += 1
            # 重置当前序列为单个符号的元组
            current_seq = (sym,)

    # 输出最后一个序列
    encoded.append(dictionary[current_seq])

    return encoded, dictionary


# LZW解码(修复类型拼接逻辑)
def lzw_decode(encoded, dictionary):
    """
    encoded: 编码后的索引序列
    dictionary: 编码时的字典(需要反转)
    """
    # 反转字典:索引 -> 符号元组(统一类型)
    reverse_dict = {idx: sym_tuple for sym_tuple, idx in dictionary.items()}
    decoded = []

    # 处理空编码的边界情况
    if not encoded:
        return np.array([])

    # 第一个符号序列
    current_code = encoded[0]
    current_seq = reverse_dict[current_code]
    decoded.extend(current_seq)  # 元组直接展开为列表

    # 解码剩余符号
    for code in encoded[1:]:
        # 查找当前编码对应的序列
        if code in reverse_dict:
            next_seq = reverse_dict[code]
        else:
            # LZW特殊情况:未在字典中时,用当前序列+当前序列第一个元素
            next_seq = current_seq + (current_seq[0],)

        # 将序列展开加入解码结果
        decoded.extend(next_seq)

        # 更新字典(模拟编码时的字典更新逻辑)
        new_seq = current_seq + (next_seq[0],)
        if len(reverse_dict) < max_dict_size and new_seq not in dictionary:
            dictionary[new_seq] = len(dictionary)
            reverse_dict[len(reverse_dict)] = new_seq

        # 更新当前序列为下一个序列
        current_seq = next_seq

    # 转换为numpy数组(与原始数据类型一致)
    return np.array(decoded, dtype=np.uint8)


# 主函数
if __name__ == "__main__":
    # 配置参数
    max_dict_size = 4096  # LZW字典最大容量(2^12)
    img_path = '../picture/Java.png'  # 替换为你的图像路径

    # 读取/生成图像数据
    img = cv2.imread(img_path, 0)  # 0表示灰度图
    if img is None:
        print(f"警告:未找到图像 {img_path},使用随机生成图像测试")
        np.random.seed(42)  # 固定随机种子,便于复现
        img = np.random.randint(0, 256, size=(128, 128), dtype=np.uint8)

    flat_img = img.flatten()  # 展平为一维数组
    print(f"图像尺寸:{img.shape},展平后长度:{len(flat_img)}")

    # LZW编码
    encoded, dict_ = lzw_encode(flat_img, max_dict_size)

    # LZW解码
    decoded_flat = lzw_decode(encoded, dict_.copy())  # 传字典副本,避免原字典被修改
    # 截断到原始长度(防止解码长度略长的情况)
    decoded_flat = decoded_flat[:len(flat_img)]
    decoded_img = decoded_flat.reshape(img.shape)

    # 计算压缩比
    original_size = len(flat_img) * 8  # 原始数据量(8位/像素,单位:比特)
    compressed_size = len(encoded) * 12  # 编码后每个索引用12位(4096=2^12)
    cr = original_size / compressed_size  # 压缩比

    # 验证解码是否正确
    is_correct = np.array_equal(img.flatten(), decoded_flat)

    # 输出结果
    print("=" * 50)
    print(f"原始数据长度:{len(flat_img)} 像素")
    print(f"编码后索引数量:{len(encoded)}")
    print(f"压缩前数据量:{original_size / 1024:.2f} KB")
    print(f"压缩后数据量:{compressed_size / 1024:.2f} KB")
    print(f"压缩比:{cr:.2f}")
    print(f"解码是否完全正确:{is_correct}")
    print("=" * 50)

    # 可视化对比
    fig, axes = plt.subplots(1, 2, figsize=(12, 10))

    # 原始图像
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始图像', fontsize=14)
    axes[0].axis('off')

    # 解码后图像
    axes[1].imshow(decoded_img, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'LZW解码后图像\n压缩比={cr:.2f}', fontsize=14)
    axes[1].axis('off')

    plt.tight_layout()
    plt.show()

效果对比图位置:运行后可看到原始图像和 LZW 解码后的图像(无损),以及压缩比数据(LZW 对重复序列多的图像压缩效果更好)。

8.6 行程编码

核心原理

行程编码(RLE)适用于连续重复的符号序列,用 "符号 + 重复次数" 代替连续的符号,比如 "AAAAA" 编码为 "A5"。

8.6.1 一维 CCITT 压缩

一维 CCITT 压缩是针对二值图像的行程编码标准,仅对每行的黑像素行程长度进行编码。

8.6.2 二维 CCITT 压缩

二维 CCITT 压缩利用前一行的信息预测当前行的行程长度,进一步提高压缩效率。

实战代码:行程编码实现

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 一维行程编码
def rle_encode_1d(data):
    """
    一维行程编码
    data: 一维数组
    """
    if len(data) == 0:
        return []
    
    encoded = []
    current_sym = data[0]
    count = 1
    
    for sym in data[1:]:
        if sym == current_sym:
            count += 1
        else:
            encoded.append((current_sym, count))
            current_sym = sym
            count = 1
    
    # 加入最后一个序列
    encoded.append((current_sym, count))
    
    return encoded

# 一维行程解码
def rle_decode_1d(encoded):
    """
    一维行程解码
    encoded: 编码后的列表,元素为(符号, 次数)
    """
    decoded = []
    for sym, count in encoded:
        decoded.extend([sym]*count)
    return np.array(decoded)

# 二维CCITT压缩(简化版)
def rle_encode_2d(img):
    """
    二维行程编码(针对二值图像)
    img: 二值图像(0/255)
    """
    encoded = []
    # 第一行用一维编码
    first_row = img[0]
    encoded.append(rle_encode_1d(first_row))
    
    # 后续行利用前一行信息(简化版:仅编码与前一行不同的区域)
    for i in range(1, img.shape[0]):
        current_row = img[i]
        prev_row = img[i-1]
        
        # 找到与前一行不同的位置
        diff_mask = current_row != prev_row
        if np.any(diff_mask):
            # 对不同区域进行一维编码
            encoded.append(rle_encode_1d(current_row[diff_mask]))
        else:
            # 与前一行相同,标记为0
            encoded.append([(0, 0)])
    
    return encoded

# 主函数
if __name__ == "__main__":
    # 生成二值图像(适合行程编码)
    img = np.zeros((256, 256), dtype=np.uint8)
    # 生成连续的白色区域
    img[50:100, :] = 255
    img[100:150, 50:200] = 255
    img[150:200, :] = 255
    
    # 一维行程编码
    flat_img = img.flatten()
    encoded_1d = rle_encode_1d(flat_img)
    decoded_1d = rle_decode_1d(encoded_1d)
    decoded_img_1d = decoded_1d.reshape(img.shape)
    
    # 二维行程编码
    encoded_2d = rle_encode_2d(img)
    
    # 计算压缩比
    original_size = len(flat_img) * 8
    # 一维编码:每个(符号,次数)对用16位(8位符号+8位次数)
    encoded_1d_size = len(encoded_1d) * 16
    # 二维编码:简化计算
    encoded_2d_size = sum([len(row) * 16 for row in encoded_2d])
    
    cr_1d = original_size / encoded_1d_size
    cr_2d = original_size / encoded_2d_size
    
    # 输出结果
    print(f"原始数据量:{original_size/1024:.2f} KB")
    print(f"一维RLE编码数据量:{encoded_1d_size/1024:.2f} KB,压缩比:{cr_1d:.2f}")
    print(f"二维RLE编码数据量:{encoded_2d_size/1024:.2f} KB,压缩比:{cr_2d:.2f}")
    print(f"一维RLE解码是否正确:{np.array_equal(img, decoded_img_1d)}")
    
    # 可视化对比
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始二值图像')
    axes[0].axis('off')
    
    axes[1].imshow(decoded_img_1d, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'一维RLE解码后图像\n压缩比={cr_1d:.2f}')
    axes[1].axis('off')
    
    plt.tight_layout()
    plt.show()
    
    # 可视化压缩比对比
    plt.figure(figsize=(8, 5))
    categories = ['一维RLE', '二维RLE']
    cr_vals = [cr_1d, cr_2d]
    colors = ['#FF6B6B', '#4ECDC4']
    
    bars = plt.bar(categories, cr_vals, color=colors)
    for bar, val in zip(bars, cr_vals):
        plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1, 
                 f'{val:.2f}', ha='center', fontsize=12)
    
    plt.ylabel('压缩比')
    plt.title('一维vs二维行程编码压缩比')
    plt.grid(axis='y', alpha=0.3)
    plt.show()

效果对比图位置

  1. 原始二值图像与一维 RLE 解码后的图像对比;
  2. 一维 / 二维 RLE 压缩比对比图,二维压缩比更高。

8.7 基于符号的编码

8.7.1 JBIG2 压缩

JBIG2 是针对二值图像的压缩标准,核心是:

  1. 对图像中的字符 / 符号进行模板匹配;
  2. 用模板索引代替重复的符号,大幅提高压缩效率(比如文档扫描件)。

实战代码:JBIG2 简化实现

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt
from skimage.feature import match_template

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# JBIG2简化实现(符号匹配)
def jbig2_simplified_encode(img, template_size=(16, 16)):
    """
    简化版JBIG2编码
    img: 二值图像
    template_size: 符号模板尺寸
    """
    h, w = img.shape
    th, tw = template_size
    
    # 1. 提取符号模板
    templates = []
    template_pos = []
    
    # 滑动窗口提取模板
    for y in range(0, h - th + 1, th):
        for x in range(0, w - tw + 1, tw):
            template = img[y:y+th, x:x+tw]
            # 跳过全0模板
            if np.sum(template) > 0:
                # 检查是否已存在相似模板
                match_found = False
                for idx, t in enumerate(templates):
                    if np.array_equal(t, template):
                        template_pos.append((idx, x, y))
                        match_found = True
                        break
                if not match_found:
                    templates.append(template)
                    template_pos.append((len(templates)-1, x, y))
    
    # 2. 编码数据:模板库 + 位置索引
    encoded = {
        'templates': templates,
        'positions': template_pos
    }
    
    return encoded

# JBIG2简化解码
def jbig2_simplified_decode(encoded, img_shape, template_size=(16, 16)):
    """
    简化版JBIG2解码
    """
    h, w = img_shape
    th, tw = template_size
    decoded = np.zeros(img_shape, dtype=np.uint8)
    
    templates = encoded['templates']
    positions = encoded['positions']
    
    for idx, x, y in positions:
        decoded[y:y+th, x:x+tw] = templates[idx]
    
    return decoded

# 主函数
if __name__ == "__main__":
    # 读取/生成文档类二值图像
    # 建议用包含重复字符的文档图像,这里生成模拟图像
    img = np.zeros((128, 256), dtype=np.uint8)
    # 生成重复的"符号"(矩形)
    for i in range(4):
        for j in range(8):
            img[16*i+2:16*i+14, 16*j+2:16*j+14] = 255
    
    # JBIG2编码
    encoded = jbig2_simplified_encode(img)
    
    # JBIG2解码
    decoded_img = jbig2_simplified_decode(encoded, img.shape)
    
    # 计算压缩比
    original_size = img.size * 8
    # 编码后:模板库大小 + 位置索引大小
    template_size = sum([t.size * 8 for t in encoded['templates']])
    position_size = len(encoded['positions']) * 16  # 每个位置用16位
    encoded_size = template_size + position_size
    cr = original_size / encoded_size
    
    # 输出结果
    print(f"模板数量:{len(encoded['templates'])}")
    print(f"位置索引数量:{len(encoded['positions'])}")
    print(f"原始数据量:{original_size/1024:.2f} KB")
    print(f"JBIG2编码数据量:{encoded_size/1024:.2f} KB")
    print(f"压缩比:{cr:.2f}")
    
    # 可视化对比
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始文档图像')
    axes[0].axis('off')
    
    axes[1].imshow(decoded_img, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'JBIG2解码后图像\n压缩比={cr:.2f}')
    axes[1].axis('off')
    
    plt.tight_layout()
    plt.show()
    
    # 可视化模板库
    if encoded['templates']:
        fig, axes = plt.subplots(1, len(encoded['templates']), figsize=(10, 2))
        if len(encoded['templates']) == 1:
            axes = [axes]
        for idx, (ax, template) in enumerate(zip(axes, encoded['templates'])):
            ax.imshow(template, cmap='gray', vmin=0, vmax=255)
            ax.set_title(f'模板{idx}')
            ax.axis('off')
        plt.tight_layout()
        plt.show()

效果对比图位置

  1. 原始文档图像与 JBIG2 解码后的图像对比;
  2. JBIG2 模板库可视化,可看到重复符号被合并为少量模板。

8.8 比特平面编码

核心原理

比特平面编码将图像的每个像素的二进制位分解为多个比特平面(如 8 位图像分为 8 个比特平面),对每个比特平面单独编码(通常用行程编码)。

实战代码:比特平面编码实现

python 复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

# 设置中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


# 一维行程编码(RLE):适用于比特平面的0/255序列
def rle_encode_1d(data):
    """
    对一维数组进行行程编码
    :param data: 一维数组(元素为0或255)
    :return: 编码后的列表,格式为[(值, 长度), ...]
    """
    if len(data) == 0:
        return []

    encoded = []
    current_val = data[0]
    count = 1

    for val in data[1:]:
        if val == current_val:
            count += 1
        else:
            encoded.append((current_val, count))
            current_val = val
            count = 1
    # 添加最后一个行程
    encoded.append((current_val, count))

    return encoded


# 一维行程解码(RLE)
def rle_decode_1d(encoded):
    """
    对行程编码结果进行解码
    :param encoded: 编码后的列表,格式为[(值, 长度), ...]
    :return: 解码后的一维数组
    """
    decoded = []
    for val, count in encoded:
        decoded.extend([val] * count)
    return np.array(decoded, dtype=np.uint8)


# 分解比特平面
def decompose_bit_planes(img):
    """
    将8位灰度图像分解为8个比特平面
    :param img: 8位灰度图像(uint8)
    :return: 8个比特平面(0/255)
    """
    bit_planes = []
    for i in range(8):
        # 提取第i位(0=最低位,7=最高位)
        bit_plane = (img >> i) & 1  # 得到0/1矩阵
        bit_plane = bit_plane * 255  # 转换为0/255便于显示和编码
        bit_planes.append(bit_plane.astype(np.uint8))
    return bit_planes


# 重构图像
def reconstruct_from_bit_planes(bit_planes):
    """
    从8个比特平面重构原始灰度图像
    :param bit_planes: 8个比特平面(0/255)
    :return: 重构的灰度图像
    """
    img = np.zeros_like(bit_planes[0], dtype=np.uint8)
    for i in range(8):
        # 将比特平面转换为0/1
        bp = (bit_planes[i] > 0).astype(np.uint8)
        # 恢复对应位的权重
        img += bp << i
    return img


# 比特平面编码(结合行程编码)
def bit_plane_encode(bit_planes):
    """
    对每个比特平面进行行程编码
    :param bit_planes: 8个比特平面
    :return: 编码后的比特平面列表
    """
    encoded_planes = []
    for bp in bit_planes:
        flat_bp = bp.flatten()  # 展平为一维数组
        encoded = rle_encode_1d(flat_bp)
        encoded_planes.append(encoded)
    return encoded_planes


# 比特平面解码
def bit_plane_decode(encoded_planes, img_shape):
    """
    解码比特平面
    :param encoded_planes: 编码后的比特平面列表
    :param img_shape: 原始图像形状
    :return: 解码后的8个比特平面
    """
    bit_planes = []
    for encoded in encoded_planes:
        decoded_flat = rle_decode_1d(encoded)
        decoded_bp = decoded_flat.reshape(img_shape)  # 恢复二维形状
        bit_planes.append(decoded_bp)
    return bit_planes


# 主函数
if __name__ == "__main__":
    # 读取图像(替换为你的图像路径)
    img_path = '../picture/CSGO.jpg'
    img = cv2.imread(img_path, 0)  # 0表示灰度图

    # 若图像读取失败,生成随机图像
    if img is None:
        print(f"警告:未找到图像 {img_path},使用随机生成图像测试")
        np.random.seed(42)  # 固定随机种子,便于复现
        img = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8)

    # 分解比特平面
    bit_planes = decompose_bit_planes(img)

    # 比特平面编码
    encoded_planes = bit_plane_encode(bit_planes)

    # 比特平面解码
    decoded_planes = bit_plane_decode(encoded_planes, img.shape)

    # 重构图像
    reconstructed_img = reconstruct_from_bit_planes(decoded_planes)

    # 计算压缩比
    original_size = img.size * 8  # 原始数据量(8位/像素,单位:比特)
    # 编码后数据量:每个RLE对存储(值1位 + 长度15位)≈16位/对
    encoded_size = sum([len(enc) * 16 for enc in encoded_planes])
    cr = original_size / encoded_size  # 压缩比

    # 输出结果
    print("=" * 60)
    print(f"图像尺寸:{img.shape}")
    print(f"原始数据量:{original_size / 1024:.2f} KB")
    print(f"比特平面编码数据量:{encoded_size / 1024:.2f} KB")
    print(f"压缩比:{cr:.2f}")
    print(f"重构是否完全正确:{np.array_equal(img, reconstructed_img)}")
    print("=" * 60)

    # 可视化8个比特平面
    fig, axes = plt.subplots(2, 4, figsize=(16, 16))
    axes = axes.flatten()
    for i in range(8):
        axes[i].imshow(bit_planes[i], cmap='gray', vmin=0, vmax=255)
        axes[i].set_title(f'比特平面 {i}({"最高位" if i == 7 else "最低位" if i == 0 else ""})', fontsize=12)
        axes[i].axis('off')
    plt.suptitle('图像的8个比特平面分解', fontsize=16)
    plt.tight_layout()
    plt.show()

    # 可视化原始图像和重构图像
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    # 原始图像
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始图像', fontsize=14)
    axes[0].axis('off')
    # 重构图像
    axes[1].imshow(reconstructed_img, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'比特平面重构图像\n压缩比={cr:.2f}', fontsize=14)
    axes[1].axis('off')

    plt.tight_layout()
    plt.show()

效果对比图位置

  1. 8 个比特平面的可视化(高位平面保留图像主要信息,低位平面是细节 / 噪声);
  2. 原始图像与比特平面重构图像对比。

8.9 块变换编码

核心原理

块变换编码是有损压缩的核心,步骤:

  1. 将图像分为若干子块(如 8x8);
  2. 对每个子块进行正交变换(如 DCT);
  3. 对变换系数进行量化(去除小系数,有损核心);
  4. 对量化后的系数编码(如行程编码 + 霍夫曼编码)。

8.9.1 变换的选择

常用变换:

  • DCT(离散余弦变换):能量集中性好,JPEG 采用;
  • DFT(离散傅里叶变换):有复数运算,较少用;
  • KLT(卡拉曼滤波变换):最优,但计算复杂。

8.9.2 子图像尺寸选择

  • 太小(如 4x4):变换增益低;
  • 太大(如 16x16):块效应明显;
  • JPEG 采用 8x8 子块。

8.9.3 比特分配

对重要的变换系数(如 DC 系数、低频 AC 系数)分配更多比特,次要系数分配更少比特。

实战代码:DCT 块变换编码实现

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt
from scipy.fftpack import dct, idct

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 8x8 DCT变换
def dct_2d(block):
    """
    2D DCT变换
    """
    return dct(dct(block.T, norm='ortho').T, norm='ortho')

# 8x8 IDCT变换
def idct_2d(block):
    """
    2D IDCT逆变换
    """
    return idct(idct(block.T, norm='ortho').T, norm='ortho')

# JPEG量化矩阵(亮度)
JPEG_QUANT_MAT = 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]
])

# 块变换编码
def block_transform_encode(img, block_size=8, quant_mat=JPEG_QUANT_MAT):
    """
    块DCT变换编码
    """
    h, w = img.shape
    # 填充图像到块大小的整数倍
    pad_h = (block_size - h % block_size) % block_size
    pad_w = (block_size - w % block_size) % block_size
    img_padded = np.pad(img, ((0, pad_h), (0, pad_w)), mode='constant')
    
    encoded = []
    h_padded, w_padded = img_padded.shape
    
    # 分块处理
    for y in range(0, h_padded, block_size):
        for x in range(0, w_padded, block_size):
            block = img_padded[y:y+block_size, x:x+block_size]
            # 转换为0-255 -> -128-127
            block = block - 128
            # DCT变换
            dct_block = dct_2d(block)
            # 量化
            quant_block = np.round(dct_block / quant_mat)
            encoded.append(quant_block)
    
    return encoded, (pad_h, pad_w)

# 块变换解码
def block_transform_decode(encoded, pad_size, img_shape, block_size=8, quant_mat=JPEG_QUANT_MAT):
    """
    块IDCT逆变换解码
    """
    pad_h, pad_w = pad_size
    h, w = img_shape
    h_padded = h + pad_h
    w_padded = w + pad_w
    
    img_recon = np.zeros((h_padded, w_padded), dtype=np.float32)
    idx = 0
    
    # 分块解码
    for y in range(0, h_padded, block_size):
        for x in range(0, w_padded, block_size):
            quant_block = encoded[idx]
            idx += 1
            # 反量化
            dct_block = quant_block * quant_mat
            # IDCT逆变换
            block = idct_2d(dct_block)
            # 转换回0-255
            block = block + 128
            # 裁剪到0-255
            block = np.clip(block, 0, 255)
            img_recon[y:y+block_size, x:x+block_size] = block
    
    # 去除填充
    img_recon = img_recon[:h, :w].astype(np.uint8)
    return img_recon

# 主函数
if __name__ == "__main__":
    # 读取图像
    img = cv2.imread('test_img.jpg', 0)
    if img is None:
        img = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8)
    
    # 块变换编码
    encoded, pad_size = block_transform_encode(img)
    
    # 块变换解码
    recon_img = block_transform_decode(encoded, pad_size, img.shape)
    
    # 计算PSNR
    mse = np.mean((img - recon_img) ** 2)
    psnr = 10 * np.log10((255**2)/mse) if mse > 0 else float('inf')
    
    # 输出结果
    print(f"重建图像PSNR:{psnr:.2f} dB")
    
    # 可视化对比
    fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始图像')
    axes[0].axis('off')
    
    axes[1].imshow(recon_img, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'DCT块变换重建图像\nPSNR={psnr:.2f} dB')
    axes[1].axis('off')
    
    plt.tight_layout()
    plt.show()
    
    # 可视化单个8x8块的DCT变换
    # 提取第一个块
    y, x = 0, 0
    block = img[y:y+8, x:x+8]
    dct_block = dct_2d(block - 128)
    quant_block = np.round(dct_block / JPEG_QUANT_MAT)
    idct_block = idct_2d(quant_block * JPEG_QUANT_MAT) + 128
    
    fig, axes = plt.subplots(2, 2, figsize=(10, 10))
    axes[0,0].imshow(block, cmap='gray', vmin=0, vmax=255)
    axes[0,0].set_title('原始8x8块')
    axes[0,0].axis('off')
    
    axes[0,1].imshow(dct_block, cmap='gray', vmin=-np.max(np.abs(dct_block)), vmax=np.max(np.abs(dct_block)))
    axes[0,1].set_title('DCT变换系数')
    axes[0,1].axis('off')
    
    axes[1,0].imshow(quant_block, cmap='gray', vmin=-np.max(np.abs(quant_block)), vmax=np.max(np.abs(quant_block)))
    axes[1,0].set_title('量化后系数')
    axes[1,0].axis('off')
    
    axes[1,1].imshow(idct_block, cmap='gray', vmin=0, vmax=255)
    axes[1,1].set_title('IDCT重建块')
    axes[1,1].axis('off')
    
    plt.tight_layout()
    plt.show()

效果对比图位置

  1. 原始图像与 DCT 块变换重建图像对比(可看到轻微块效应,PSNR 越高效果越好);
  2. 单个 8x8 块的 DCT 变换 / 量化 / 重建过程可视化。

8.10 预测编码

预测编码的核心是利用图像像素的空间 / 时间相关性,用已编码像素预测当前像素,仅编码预测误差,分为无损和有损两类,是视频压缩(如 H.264)的核心技术之一。

8.10.1 无损预测编码

核心原理:无损预测编码(如 DPCM,差分脉冲编码调制)通过线性预测器计算当前像素的预测值,编码 "实际值 - 预测值" 的残差,残差无量化,因此可完全重建原始图像。

实战代码:无损 DPCM 预测编码

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

def dpcm_encode_lossless(img, predictor_type='avg'):
    """
    无损DPCM编码
    :param img: 输入灰度图像
    :param predictor_type: 预测器类型:left/top/avg/3rd
    :return: 残差矩阵、重建图像
    """
    h, w = img.shape
    img = img.astype(np.int16)  # 避免溢出
    residual = np.zeros_like(img)  # 残差矩阵
    recon = np.zeros_like(img)     # 重建图像
    
    # 初始化:第一行、第一列直接复制
    recon[0, :] = img[0, :]
    recon[:, 0] = img[:, 0]
    residual[0, :] = img[0, :]  # 无预测,残差=原值
    residual[:, 0] = img[:, 0]
    
    # 逐像素预测编码
    for y in range(1, h):
        for x in range(1, w):
            # 计算预测值
            if predictor_type == 'left':
                pred = recon[y, x-1]
            elif predictor_type == 'top':
                pred = recon[y-1, x]
            elif predictor_type == 'avg':
                pred = (recon[y, x-1] + recon[y-1, x]) // 2
            elif predictor_type == '3rd':
                # 三阶预测:左+上+左上的平均(简化版)
                pred = (recon[y, x-1] + recon[y-1, x] + recon[y-1, x-1]) // 3
            else:
                raise ValueError("预测器类型仅支持left/top/avg/3rd")
            
            # 计算残差
            residual[y, x] = img[y, x] - pred
            # 重建像素
            recon[y, x] = pred + residual[y, x]
    
    # 转换回uint8
    recon = recon.astype(np.uint8)
    return residual, recon

# 主函数
if __name__ == "__main__":
    # 读取图像(替换为自己的图像路径)
    img = cv2.imread('test_img.jpg', 0)
    if img is None:
        img = np.random.randint(0, 256, (256, 256), dtype=np.uint8)
    
    # 测试不同预测器
    predictors = ['left', 'top', 'avg', '3rd']
    fig, axes = plt.subplots(2, len(predictors)+1, figsize=(18, 8))
    
    # 显示原始图像
    axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0, 0].set_title('原始图像')
    axes[0, 0].axis('off')
    axes[1, 0].axis('off')  # 下方第一列空出
    
    # 遍历预测器
    for idx, p in enumerate(predictors):
        residual, recon = dpcm_encode_lossless(img, p)
        
        # 计算残差熵(熵越低,压缩潜力越大)
        flat_res = residual.flatten()
        counts = np.bincount(flat_res - flat_res.min(), minlength=len(np.unique(flat_res)))
        prob = counts / flat_res.size
        prob = prob[prob > 0]
        entropy = -np.sum(prob * np.log2(prob))
        
        # 显示残差图
        axes[0, idx+1].imshow(residual, cmap='gray', vmin=residual.min(), vmax=residual.max())
        axes[0, idx+1].set_title(f'{p}预测-残差\n熵={entropy:.2f}')
        axes[0, idx+1].axis('off')
        
        # 显示重建图像
        axes[1, idx+1].imshow(recon, cmap='gray', vmin=0, vmax=255)
        axes[1, idx+1].set_title(f'{p}预测-重建\n误差={np.mean((img-recon)**2):.0f}')
        axes[1, idx+1].axis('off')
    
    plt.tight_layout()
    plt.show()
    
    # 验证无损性
    _, recon_avg = dpcm_encode_lossless(img, 'avg')
    print(f'无损验证:原始图像与重建图像是否一致? {np.array_equal(img, recon_avg)}')

效果对比图位置:运行代码后生成 2 行 5 列的对比图:

  • 第一列:原始图像;
  • 其余列:不同预测器的残差图(熵越低,压缩效果越好)+ 重建图像(MSE=0,完全无损)。

8.10.2 运动补偿预测残差

核心原理 :针对视频序列的时间冗余,通过 "运动估计" 找到当前帧(目标帧)与参考帧(前一帧)的像素对应关系,用参考帧的像素预测目标帧,仅编码 "运动矢量 + 预测残差",是 H.264/H.265 的核心。

关键步骤

  1. 运动估计:将目标帧分块(如 16x16),在参考帧的搜索窗内找最匹配的块,得到运动矢量(MV);
  2. 运动补偿:用参考帧的匹配块 + 运动矢量,生成目标帧的预测块;
  3. 编码残差:目标块 - 预测块 = 残差,对残差编码。

实战代码:运动补偿预测残差

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

def block_matching(frame1, frame2, block_size=16, search_range=8):
    """
    块匹配运动估计(SSD准则)
    :param frame1: 参考帧
    :param frame2: 目标帧
    :param block_size: 块大小
    :param search_range: 搜索范围
    :return: 运动矢量场、预测帧、残差帧
    """
    h, w = frame1.shape
    mv_field = np.zeros((h//block_size, w//block_size, 2), dtype=np.int16)  # 运动矢量(y,x)
    pred_frame = np.zeros_like(frame1)
    residual_frame = np.zeros_like(frame1)
    
    # 分块处理
    for by in range(0, h - block_size + 1, block_size):
        for bx in range(0, w - block_size + 1, block_size):
            # 目标块
            target_block = frame2[by:by+block_size, bx:bx+block_size]
            min_ssd = float('inf')
            best_mv = (0, 0)
            
            # 搜索窗内匹配
            for dy in range(-search_range, search_range+1):
                for dx in range(-search_range, search_range+1):
                    # 参考块位置(避免越界)
                    ref_by = by + dy
                    ref_bx = bx + dx
                    if ref_by < 0 or ref_by + block_size > h or ref_bx < 0 or ref_bx + block_size > w:
                        continue
                    # 参考块
                    ref_block = frame1[ref_by:ref_by+block_size, ref_bx:ref_bx+block_size]
                    # 计算SSD(误差平方和)
                    ssd = np.sum((target_block - ref_block)**2)
                    if ssd < min_ssd:
                        min_ssd = ssd
                        best_mv = (dy, dx)
            
            # 保存运动矢量
            mv_field[by//block_size, bx//block_size] = best_mv
            # 运动补偿生成预测块
            pred_by = by + best_mv[0]
            pred_bx = bx + best_mv[1]
            pred_frame[by:by+block_size, bx:bx+block_size] = frame1[pred_by:pred_by+block_size, pred_bx:pred_bx+block_size]
            # 计算残差
            residual_frame[by:by+block_size, bx:bx+block_size] = frame2[by:by+block_size, bx:bx+block_size] - pred_frame[by:by+block_size, bx:bx+block_size]
    
    return mv_field, pred_frame, residual_frame

# 主函数
if __name__ == "__main__":
    # 生成模拟视频帧(帧1:静态背景,帧2:背景+轻微位移)
    np.random.seed(42)
    frame1 = np.random.randint(0, 256, (256, 256), dtype=np.uint8)
    # 帧2:整体右移2像素、下移1像素(模拟运动)
    frame2 = np.roll(np.roll(frame1, 2, axis=1), 1, axis=0)
    frame2[:, :2] = 0  # 补0
    frame2[:1, :] = 0
    
    # 运动估计与补偿
    mv_field, pred_frame, residual_frame = block_matching(frame1, frame2, block_size=16, search_range=8)
    
    # 计算PSNR
    def psnr(img1, img2):
        mse = np.mean((img1 - img2)**2)
        return 10 * np.log10((255**2)/mse) if mse > 0 else float('inf')
    
    psnr_pred = psnr(frame2, pred_frame)
    psnr_raw = psnr(frame2, frame1)  # 直接用参考帧预测的PSNR
    
    # 可视化
    fig, axes = plt.subplots(2, 2, figsize=(12, 12))
    # 参考帧 & 目标帧
    axes[0,0].imshow(frame1, cmap='gray', vmin=0, vmax=255)
    axes[0,0].set_title('参考帧(帧1)')
    axes[0,0].axis('off')
    axes[0,1].imshow(frame2, cmap='gray', vmin=0, vmax=255)
    axes[0,1].set_title('目标帧(帧2)')
    axes[0,1].axis('off')
    # 预测帧 & 残差帧
    axes[1,0].imshow(pred_frame, cmap='gray', vmin=0, vmax=255)
    axes[1,0].set_title(f'运动补偿预测帧\nPSNR={psnr_pred:.2f}dB')
    axes[1,0].axis('off')
    axes[1,1].imshow(residual_frame, cmap='gray', vmin=residual_frame.min(), vmax=residual_frame.max())
    axes[1,1].set_title(f'预测残差帧\n原始帧PSNR={psnr_raw:.2f}dB')
    axes[1,1].axis('off')
    
    plt.tight_layout()
    plt.show()
    
    # 打印运动矢量统计
    mv_flat = mv_field.reshape(-1, 2)
    print(f'平均运动矢量:(y={np.mean(mv_flat[:,0]):.2f}, x={np.mean(mv_flat[:,1]):.2f})')
    print(f'残差均值:{np.mean(np.abs(residual_frame)):.2f}')

效果对比图位置:运行代码后生成 2x2 对比图:

  • 参考帧 + 目标帧:直观看到帧间位移;
  • 预测帧:运动补偿后接近目标帧(PSNR 显著提升);
  • 残差帧:仅包含少量误差(均值接近 0),远小于原始帧差。

8.10.3 有损预测编码

核心原理 :在无损预测编码基础上,对预测残差进行量化(舍去小残差),牺牲少量精度换取更高压缩比。量化后的残差无法完全恢复原始值,因此是有损压缩。

实战代码:有损 DPCM 预测编码

复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

def dpcm_encode_lossy(img, predictor_type='avg', quant_step=4):
    """
    有损DPCM编码(残差量化)
    :param img: 输入灰度图像
    :param predictor_type: 预测器类型
    :param quant_step: 量化步长(越大压缩比越高,失真越大)
    :return: 量化残差、重建图像、压缩比
    """
    h, w = img.shape
    img = img.astype(np.int16)
    residual = np.zeros_like(img)
    quant_residual = np.zeros_like(img)
    recon = np.zeros_like(img)
    
    # 初始化
    recon[0, :] = img[0, :]
    recon[:, 0] = img[:, 0]
    residual[0, :] = img[0, :]
    quant_residual[0, :] = np.round(residual[0, :] / quant_step)  # 量化
    
    # 逐像素编码
    for y in range(1, h):
        for x in range(1, w):
            # 预测值
            if predictor_type == 'avg':
                pred = (recon[y, x-1] + recon[y-1, x]) // 2
            else:
                pred = recon[y, x-1]  # 默认左预测
            # 残差
            residual[y, x] = img[y, x] - pred
            # 量化残差
            quant_residual[y, x] = np.round(residual[y, x] / quant_step)
            # 反量化
            res_dequant = quant_residual[y, x] * quant_step
            # 重建
            recon[y, x] = pred + res_dequant
    
    # 转换回uint8(裁剪溢出)
    recon = np.clip(recon, 0, 255).astype(np.uint8)
    # 计算压缩比:原始8位/像素,残差量化后比特数=ceil(log2(量化等级数))
    quant_levels = len(np.unique(quant_residual))
    avg_bits = np.ceil(np.log2(quant_levels))
    cr = 8 / avg_bits
    
    return quant_residual, recon, cr

# 主函数
if __name__ == "__main__":
    img = cv2.imread('test_img.jpg', 0)
    if img is None:
        img = np.random.randint(0, 256, (256, 256), dtype=np.uint8)
    
    # 测试不同量化步长
    quant_steps = [2, 4, 8, 16]
    fig, axes = plt.subplots(2, len(quant_steps)+1, figsize=(20, 8))
    
    # 原始图像
    axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0, 0].set_title('原始图像')
    axes[0, 0].axis('off')
    axes[1, 0].axis('off')
    
    # 遍历量化步长
    for idx, q in enumerate(quant_steps):
        quant_res, recon, cr = dpcm_encode_lossy(img, 'avg', q)
        mse = np.mean((img - recon)**2)
        psnr = 10 * np.log10((255**2)/mse) if mse > 0 else float('inf')
        
        # 量化残差
        axes[0, idx+1].imshow(quant_res, cmap='gray', vmin=quant_res.min(), vmax=quant_res.max())
        axes[0, idx+1].set_title(f'量化步长{q}-残差\n压缩比={cr:.2f}')
        axes[0, idx+1].axis('off')
        
        # 重建图像
        axes[1, idx+1].imshow(recon, cmap='gray', vmin=0, vmax=255)
        axes[1, idx+1].set_title(f'重建图像\nPSNR={psnr:.2f}dB')
        axes[1, idx+1].axis('off')
    
    plt.tight_layout()
    plt.show()

效果对比图位置:生成 2 行 5 列对比图:

  • 量化步长越大,残差的熵越低(压缩比越高),但重建图像 PSNR 越低(失真越大);
  • 量化步长 = 2 时,重建图像几乎无失真;步长 = 16 时,可见明显块效应。

8.10.4 最优预测器

核心原理

最优系数求解

8.10.5 最优化

核心目标

常用优化准则

实战代码:最优线性预测器求解

python 复制代码
import numpy as np
import cv2
import matplotlib.pyplot as plt
from scipy.linalg import solve, pinv
from numpy.linalg import det

# 设置中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


# 改进的DPCM无损预测函数(分离无损/有损,避免量化干扰)
def dpcm_predict_lossless(img, predictor_type='avg'):
    """
    无损DPCM预测(无量化)
    :param img: 灰度图像
    :param predictor_type: 预测器类型 left/top/avg/3rd
    :return: 残差矩阵、重建图像、MSE
    """
    h, w = img.shape
    img_int = img.astype(np.int16)
    recon = np.zeros_like(img_int)
    residual = np.zeros_like(img_int)

    # 边界初始化
    recon[0, :] = img_int[0, :]
    recon[:, 0] = img_int[:, 0]
    residual[0, :] = 0  # 第一行无预测,残差为0
    residual[:, 0] = 0  # 第一列无预测,残差为0

    # 逐像素预测
    for y in range(1, h):
        for x in range(1, w):
            if predictor_type == 'left':
                pred = recon[y, x - 1]
            elif predictor_type == 'top':
                pred = recon[y - 1, x]
            elif predictor_type == 'avg':
                pred = (recon[y, x - 1] + recon[y - 1, x]) // 2
            elif predictor_type == '3rd':
                pred = (recon[y, x - 1] + recon[y - 1, x] + recon[y - 1, x - 1]) // 3
            else:
                pred = recon[y, x - 1]

            residual[y, x] = img_int[y, x] - pred
            recon[y, x] = pred + residual[y, x]

    # 转换回uint8
    recon = np.clip(recon, 0, 255).astype(np.uint8)
    # 计算MSE
    mse = np.mean((img - recon) ** 2)
    return residual, recon, mse


# 改进的最优线性预测器(局部自适应,而非全局)
def optimal_linear_predictor(img, block_size=16, reg_eps=1e-6):
    """
    分块求解最优线性预测器(局部自适应,提升拟合效果)
    :param img: 灰度图像
    :param block_size: 分块大小(局部统计更准确)
    :param reg_eps: 正则化系数
    :return: 最优预测系数(分块)、重建图像、MSE
    """
    h, w = img.shape
    img = img.astype(np.float32)
    recon = np.zeros_like(img)

    # 边界初始化
    recon[0, :] = img[0, :]
    recon[:, 0] = img[:, 0]

    # 分块处理
    a_opt_global = np.zeros(3)  # 全局平均系数
    block_count = 0

    for by in range(0, h - block_size + 1, block_size):
        for bx in range(0, w - block_size + 1, block_size):
            # 提取当前块
            block = img[by:by + block_size, bx:bx + block_size]

            # 提取预测基(确保维度匹配)
            f1 = block[1:, :-1].flatten()  # 左像素
            f2 = block[:-1, 1:].flatten()  # 上像素
            f3 = block[:-1, :-1].flatten()  # 左上像素
            target = block[1:, 1:].flatten()  # 目标像素

            if len(target) < 4:  # 小块跳过
                continue

            # 构建自相关矩阵
            R = np.array([
                [np.mean(f1 * f1), np.mean(f1 * f2), np.mean(f1 * f3)],
                [np.mean(f2 * f1), np.mean(f2 * f2), np.mean(f2 * f3)],
                [np.mean(f3 * f1), np.mean(f3 * f2), np.mean(f3 * f3)]
            ]) + reg_eps * np.eye(3)

            # 互相关向量
            r = np.array([
                np.mean(f1 * target),
                np.mean(f2 * target),
                np.mean(f3 * target)
            ])

            # 求解块内最优系数
            try:
                a_opt = solve(R, r)
            except:
                a_opt = pinv(R) @ r

            a_opt_global += a_opt
            block_count += 1

            # 块内预测重建
            for y in range(by + 1, by + block_size):
                for x in range(bx + 1, bx + block_size):
                    if y >= h or x >= w:
                        continue
                    left = img[y, x - 1]
                    up = img[y - 1, x]
                    up_left = img[y - 1, x - 1]
                    recon[y, x] = a_opt[0] * left + a_opt[1] * up + a_opt[2] * up_left

    # 全局平均系数
    a_opt_global = a_opt_global / block_count if block_count > 0 else np.array([0.3, 0.3, 0.4])

    # 剩余区域(未分块)用全局系数预测
    for y in range(1, h):
        for x in range(1, w):
            if recon[y, x] == 0:  # 未被分块处理的像素
                left = img[y, x - 1]
                up = img[y - 1, x]
                up_left = img[y - 1, x - 1]
                recon[y, x] = a_opt_global[0] * left + a_opt_global[1] * up + a_opt_global[2] * up_left

    # 后处理
    recon = np.clip(recon, 0, 255).astype(np.uint8)
    mse = np.mean((img.astype(np.uint8) - recon) ** 2)

    # 输出矩阵信息
    print(f"分块数:{block_count}")
    print(f"全局最优系数:左={a_opt_global[0]:.3f}, 上={a_opt_global[1]:.3f}, 左上={a_opt_global[2]:.3f}")

    return a_opt_global, recon, mse


# 主函数
if __name__ == "__main__":
    # 读取图像
    img_path = '../picture/TianHuoSanXuanBian.jpg'
    img = cv2.imread(img_path, 0)

    # 备用图像(确保非纯色)
    if img is None:
        print("使用测试图像...")
        np.random.seed(42)
        img = np.random.randint(0, 256, (256, 256), dtype=np.uint8)
        img = cv2.GaussianBlur(img, (3, 3), 0)  # 添加纹理

    # 1. 平均预测器(无损)
    _, recon_avg, mse_avg = dpcm_predict_lossless(img, 'avg')

    # 2. 最优预测器(分块自适应)
    a_opt, recon_opt, mse_opt = optimal_linear_predictor(img, block_size=16)

    # 3. 计算MSE提升(处理除零)
    if mse_avg < 1e-6:  # 平均预测器MSE接近0
        mse_improve = -100.0 * (mse_opt / (mse_opt + 1e-6))  # 相对误差
        improve_str = f"-{abs(mse_improve):.2f}%"  # 负号表示变差
    else:
        mse_improve = (mse_avg - mse_opt) / mse_avg * 100
        improve_str = f"{mse_improve:.2f}%" if mse_improve > 0 else f"-{abs(mse_improve):.2f}%"

    # 可视化
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))

    # 原始图像
    axes[0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0].set_title('原始图像', fontsize=14)
    axes[0].axis('off')

    # 平均预测器
    axes[1].imshow(recon_avg, cmap='gray', vmin=0, vmax=255)
    axes[1].set_title(f'平均预测器重建\nMSE={mse_avg:.4f}', fontsize=14)
    axes[1].axis('off')

    # 最优预测器
    axes[2].imshow(recon_opt, cmap='gray', vmin=0, vmax=255)
    axes[2].set_title(
        f'分块最优预测器重建\n系数={a_opt.round(3)}\nMSE={mse_opt:.4f}\nMSE变化={improve_str}',
        fontsize=12
    )
    axes[2].axis('off')

    plt.tight_layout()
    plt.show()

    # 输出结果(友好格式)
    print("=" * 60)
    print(f'平均预测器MSE:{mse_avg:.6f}')
    print(f'最优预测器MSE:{mse_opt:.6f}')
    print(f'MSE变化率:{improve_str}(正值=提升,负值=变差)')
    print("=" * 60)

效果对比图位置:生成 1 行 3 列对比图:

  • 最优预测器的 MSE 低于普通平均预测器(残差更小,压缩潜力更高);
  • 最优系数因图像内容而异(纹理复杂区域系数分布不同)。

8.11 小波编码

小波编码是 JPEG-2000 的核心压缩技术,相比 DCT(块变换),小波变换具有多分辨率、无块效应、能量集中性更强的优势,支持无损 / 有损压缩、渐进传输。

8.11.1 小波的选择

常用小波基

小波类型 特点 应用场景
Haar 小波 最简单、正交、紧支撑 教学演示、二值图像
Daubechies (db4/db8) 光滑性好、紧支撑 JPEG-2000、图像压缩
Biorthogonal (bior4.4) 对称、可逆 无损压缩、医疗图像

选择原则

  • 无损压缩:选正交 / 双正交小波(保证逆变换无失真);
  • 有损压缩:选光滑性好的小波(能量集中性强);
  • 实时性要求高:选短支撑小波(计算快)。

8.11.2 分解级数的选择

核心原理:小波分解将图像分为低频子带(LL)和高频子带(LH、HL、HH),分解级数越多,低频子带越聚焦图像核心信息,但计算复杂度越高。

选择原则

  • 低分辨率图像(如 256x256):2-3 级分解;
  • 高分辨率图像(如 1024x1024):4-5 级分解;
  • 分解级数过多:高频子带无有效信息,反而增加编码开销。

8.11.3 量化器设计

核心原理:对小波系数的量化遵循 "低频细量化、高频粗量化" 原则(低频子带保留图像核心信息,高频子带对应细节 / 噪声)。

常用量化方式

8.11.4 JPEG-2000

核心特性

  1. 基于小波变换(默认 bior4.4),无块效应;
  2. 支持无损 / 有损压缩;
  3. 支持渐进传输(先传低频,再传高频);
  4. 支持感兴趣区域(ROI)压缩(ROI 区域细量化,其他区域粗量化)。

实战代码:小波编码(JPEG-2000 简化版)

复制代码
import numpy as np
import cv2
import pywt
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

def wavelet_encode_decode(img, wavelet='db4', level=3, quant_step_low=2, quant_step_high=16):
    """
    小波编码+解码(简化JPEG-2000)
    :param img: 灰度图像
    :param wavelet: 小波基
    :param level: 分解级数
    :param quant_step_low: 低频子带量化步长
    :param quant_step_high: 高频子带量化步长
    :return: 分解子带、重建图像、压缩比
    """
    # 小波分解
    coeffs = pywt.wavedec2(img, wavelet, level=level)
    coeffs_arr, coeffs_slices = pywt.coeffs_to_array(coeffs)
    
    # 量化:低频子带细量化,高频子带粗量化
    # 找到低频子带位置
    ll_slice = coeffs_slices[0]
    quant_coeffs = np.zeros_like(coeffs_arr)
    # 低频子带量化
    quant_coeffs[ll_slice] = np.round(coeffs_arr[ll_slice] / quant_step_low)
    # 高频子带量化
    quant_coeffs[~ll_slice] = np.round(coeffs_arr[~ll_slice] / quant_step_high)
    
    # 反量化
    dequant_coeffs = np.zeros_like(quant_coeffs)
    dequant_coeffs[ll_slice] = quant_coeffs[ll_slice] * quant_step_low
    dequant_coeffs[~ll_slice] = quant_coeffs[~ll_slice] * quant_step_high
    
    # 小波逆变换
    coeffs_recon = pywt.array_to_coeffs(dequant_coeffs, coeffs_slices, output_format='wavedec2')
    img_recon = pywt.waverec2(coeffs_recon, wavelet)
    img_recon = np.clip(img_recon, 0, 255).astype(np.uint8)
    
    # 计算压缩比:原始8位/像素,量化后系数的平均比特数
    unique_coeffs = len(np.unique(quant_coeffs))
    avg_bits = np.ceil(np.log2(unique_coeffs)) if unique_coeffs > 1 else 1
    cr = (8 * img.size) / (avg_bits * quant_coeffs.size)
    
    # 提取分解子带(可视化用)
    subbands = {}
    subbands['LL'] = coeffs[0]
    for i in range(1, level+1):
        subbands[f'LH{i}'] = coeffs[i][0]
        subbands[f'HL{i}'] = coeffs[i][1]
        subbands[f'HH{i}'] = coeffs[i][2]
    
    return subbands, img_recon, cr

# 可视化小波子带
def plot_wavelet_subbands(subbands, level):
    fig, axes = plt.subplots(1, 3*level + 1, figsize=(20, 4))
    # 低频子带LL
    axes[0].imshow(subbands['LL'], cmap='gray')
    axes[0].set_title('LL(低频)')
    axes[0].axis('off')
    # 高频子带
    idx = 1
    for i in range(1, level+1):
        axes[idx].imshow(subbands[f'LH{i}'], cmap='gray')
        axes[idx].set_title(f'LH{i}(水平高频)')
        axes[idx].axis('off')
        idx += 1
        
        axes[idx].imshow(subbands[f'HL{i}'], cmap='gray')
        axes[idx].set_title(f'HL{i}(垂直高频)')
        axes[idx].axis('off')
        idx += 1
        
        axes[idx].imshow(subbands[f'HH{i}'], cmap='gray')
        axes[idx].set_title(f'HH{i}(对角高频)')
        axes[idx].axis('off')
        idx += 1
    plt.tight_layout()
    plt.show()

# 主函数
if __name__ == "__main__":
    # 读取图像
    img = cv2.imread('test_img.jpg', 0)
    if img is None:
        img = np.random.randint(0, 256, (512, 512), dtype=np.uint8)
    
    # 测试不同分解级数
    levels = [1, 2, 3, 4]
    fig, axes = plt.subplots(2, len(levels)+1, figsize=(20, 8))
    
    # 原始图像
    axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0, 0].set_title('原始图像')
    axes[0, 0].axis('off')
    axes[1, 0].axis('off')
    
    # 遍历分解级数
    for idx, l in enumerate(levels):
        subbands, recon, cr = wavelet_encode_decode(img, 'db4', l, 2, 16)
        mse = np.mean((img - recon)**2)
        psnr = 10 * np.log10((255**2)/mse) if mse > 0 else float('inf')
        
        # 子带可视化(单独窗口)
        if l == 3:
            plot_wavelet_subbands(subbands, l)
        
        # 重建图像
        axes[0, idx+1].imshow(recon, cmap='gray', vmin=0, vmax=255)
        axes[0, idx+1].set_title(f'分解级数{l}\nPSNR={psnr:.2f}dB')
        axes[0, idx+1].axis('off')
        
        # 压缩比
        axes[1, idx+1].text(0.5, 0.5, f'压缩比={cr:.2f}', ha='center', va='center', fontsize=16)
        axes[1, idx+1].set_title('压缩比')
        axes[1, idx+1].axis('off')
    
    plt.tight_layout()
    plt.show()

效果对比图位置

  1. 小波子带可视化图:LL 子带保留图像核心轮廓,LH/HL/HH 分别对应水平 / 垂直 / 对角高频细节;
  2. 不同分解级数对比图:
    • 级数 = 1:压缩比低,PSNR 高(失真小);
    • 级数 = 3:压缩比和 PSNR 平衡(最优);
    • 级数 = 4:压缩比提升有限,PSNR 略有下降。

8.12 数字图像水印

数字图像水印是在图像中嵌入不可见的标识信息(如版权、溯源码),要求:

  1. 不可感知性:嵌入水印后图像无视觉失真;
  2. 鲁棒性:抗常见攻击(压缩、滤波、裁剪、噪声);
  3. 安全性:水印难以篡改 / 伪造。

核心分类

类型 嵌入域 特点
空域水印 像素值 简单、鲁棒性差
变换域水印 DCT/DWT/DFT 鲁棒性强、不可感知性好

实战代码:DWT 域鲁棒水印(JPEG-2000 兼容)

python 复制代码
import numpy as np
import cv2
import pywt
import hashlib
import matplotlib.pyplot as plt

# 设置中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


def generate_watermark(secret, shape, seed=42):
    """
    生成伪随机水印序列(基于密钥)
    修复:限制随机种子在0~2^32-1范围内,避免种子超限错误
    :param secret: 水印明文(如版权信息)
    :param shape: 水印形状(与小波子带匹配)
    :param seed: 密钥种子
    :return: 归一化水印序列(-1~1)
    """
    # 将明文转换为哈希值(固定长度16字节)
    secret_hash = hashlib.md5(secret.encode()).digest()
    # 将哈希值转换为32位整数(核心修复:限制种子范围)
    hash_int = int.from_bytes(secret_hash, 'big') % (2 ** 32)  # 取模限制在0~2^32-1
    # 基于哈希和种子生成随机序列
    np.random.seed(hash_int + seed)
    watermark = np.random.randn(*shape)
    # 归一化到-1~1(避免数值溢出)
    watermark = watermark / (np.max(np.abs(watermark)) + 1e-8)
    return watermark


def embed_dwt_watermark(img, secret, alpha=0.1, wavelet='db4', level=2):
    """
    DWT域嵌入水印(嵌入到LL2子带,鲁棒性最强)
    优化:处理小波逆变换后的尺寸对齐问题
    :param img: 灰度图像
    :param secret: 水印明文
    :param alpha: 水印强度(越小越不可见)
    :return: 含水印图像、水印序列
    """
    orig_h, orig_w = img.shape
    img = img.astype(np.float32)

    # 小波分解(2级)
    coeffs = pywt.wavedec2(img, wavelet, level=level)
    ll2 = coeffs[0]  # LL2子带(核心低频)

    # 生成水印
    watermark = generate_watermark(secret, ll2.shape)
    # 嵌入水印:LL2' = LL2 + alpha * watermark * max(LL2)
    ll2_wm = ll2 + alpha * watermark * np.max(np.abs(ll2))
    # 替换低频子带
    coeffs[0] = ll2_wm

    # 小波逆变换 + 尺寸裁剪(修复尺寸不一致)
    img_wm = pywt.waverec2(coeffs, wavelet)
    img_wm = img_wm[:orig_h, :orig_w]  # 裁剪回原始尺寸
    img_wm = np.clip(img_wm, 0, 255).astype(np.uint8)

    return img_wm, watermark


def extract_dwt_watermark(img, img_ori, secret, alpha=0.1, wavelet='db4', level=2):
    """
    提取DWT域水印
    优化:处理攻击后图像的尺寸对齐问题
    :param img: 含水印(可能被攻击)的图像
    :param img_ori: 原始无水印图像
    :param secret: 水印明文(密钥)
    :return: 提取的水印、相关系数(匹配度)
    """
    orig_h, orig_w = img_ori.shape
    img = img.astype(np.float32)
    img_ori = img_ori.astype(np.float32)

    # 分解原始图像和含水印图像
    coeffs_ori = pywt.wavedec2(img_ori, wavelet, level=level)
    coeffs_wm = pywt.wavedec2(img, wavelet, level=level)
    ll2_ori = coeffs_ori[0]
    ll2_wm = coeffs_wm[0]

    # 提取水印(添加小常数避免除零)
    max_ll2 = np.max(np.abs(ll2_ori)) + 1e-8
    watermark_ext = (ll2_wm - ll2_ori) / (alpha * max_ll2)

    # 生成原始水印(用于匹配)
    watermark_gt = generate_watermark(secret, ll2_ori.shape)

    # 计算相关系数(匹配度,越接近1越匹配)
    corr = np.corrcoef(watermark_ext.flatten(), watermark_gt.flatten())[0, 1]
    # 处理NaN(攻击后可能完全失真)
    corr = 0 if np.isnan(corr) else corr
    return watermark_ext, corr


def attack_image(img, attack_type='jpeg', quality=50):
    """
    对图像施加攻击(模拟压缩/噪声/裁剪)
    优化:增强攻击的鲁棒性测试
    :param img: 输入图像
    :param attack_type: jpeg/noise/crop
    :return: 被攻击后的图像
    """
    if attack_type == 'jpeg':
        # JPEG压缩攻击
        encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
        _, img_encoded = cv2.imencode('.jpg', img, encode_param)
        img_attacked = cv2.imdecode(img_encoded, 0)
    elif attack_type == 'noise':
        # 高斯噪声攻击(增强噪声强度)
        noise = np.random.normal(0, 15, img.shape).astype(np.int16)
        img_attacked = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)
    elif attack_type == 'crop':
        # 裁剪攻击(裁剪20%,更严苛)
        h, w = img.shape
        crop_h1, crop_h2 = int(h * 0.1), int(h * 0.9)
        crop_w1, crop_w2 = int(w * 0.1), int(w * 0.9)
        img_attacked = img[crop_h1:crop_h2, crop_w1:crop_w2]
        # 恢复尺寸(保持插值一致性)
        img_attacked = cv2.resize(img_attacked, (w, h), interpolation=cv2.INTER_LINEAR)
    else:  # 'none'
        img_attacked = img.copy()
    return img_attacked


# 主函数
if __name__ == "__main__":
    # 读取图像
    img_path = '../picture/XiLian.png'
    img = cv2.imread(img_path, 0)

    # 备用图像(确保尺寸合理)
    if img is None:
        print(f"警告:未找到图像 {img_path},使用512x512测试图像")
        np.random.seed(42)
        img = np.random.randint(0, 256, (512, 512), dtype=np.uint8)
        img = cv2.GaussianBlur(img, (5, 5), 0)  # 添加纹理

    # 水印配置
    secret = "Copyright@2025-ImageProcessing"
    alpha = 0.1  # 水印强度(0.05~0.2为宜)

    # 嵌入水印
    img_wm, watermark_gt = embed_dwt_watermark(img, secret, alpha)

    # 定义攻击类型
    attacks = ['none', 'jpeg', 'noise', 'crop']
    fig, axes = plt.subplots(2, len(attacks) + 1, figsize=(22, 10))

    # 显示原始图像和含水印图像
    axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=255)
    axes[0, 0].set_title('原始图像', fontsize=12)
    axes[0, 0].axis('off')

    axes[1, 0].imshow(img_wm, cmap='gray', vmin=0, vmax=255)
    axes[1, 0].set_title(f'含水印图像\n(强度α={alpha})', fontsize=12)
    axes[1, 0].axis('off')

    # 遍历攻击类型并提取水印
    corr_results = []  # 存储相关系数
    for idx, attack in enumerate(attacks):
        # 攻击图像
        img_attacked = attack_image(img_wm, attack, quality=50)
        # 提取水印
        watermark_ext, corr = extract_dwt_watermark(img_attacked, img, secret, alpha)
        corr_results.append(corr)

        # 显示被攻击图像
        axes[0, idx + 1].imshow(img_attacked, cmap='gray', vmin=0, vmax=255)
        axes[0, idx + 1].set_title(
            f'攻击类型:{attack}\n相关系数={corr:.3f}',
            fontsize=11
        )
        axes[0, idx + 1].axis('off')

        # 显示提取的水印(归一化显示)
        watermark_ext_norm = (watermark_ext - np.min(watermark_ext)) / (
                    np.max(watermark_ext) - np.min(watermark_ext) + 1e-8)
        axes[1, idx + 1].imshow(watermark_ext_norm, cmap='gray')
        axes[1, idx + 1].set_title('提取的水印(归一化)', fontsize=11)
        axes[1, idx + 1].axis('off')

    # 整体标题
    plt.suptitle('DWT域数字水印的鲁棒性测试', fontsize=16)
    plt.tight_layout()
    plt.show()

    # 计算不可感知性(PSNR)和鲁棒性指标
    mse = np.mean((img - img_wm) ** 2)
    psnr = 10 * np.log10((255 ** 2) / (mse + 1e-8))  # 避免除零

    # 输出测试报告
    print("=" * 70)
    print("数字水印测试报告")
    print("=" * 70)
    print(f"水印明文:{secret}")
    print(f"水印强度:α={alpha}")
    print(f"图像尺寸:{img.shape}")
    print(f"不可感知性(PSNR):{psnr:.2f}dB(>30dB为不可见)")
    print("-" * 70)
    print(f"{'攻击类型':^10} | {'相关系数':^10} | {'鲁棒性评价':^15}")
    print("-" * 70)
    for attack, corr in zip(attacks, corr_results):
        if corr > 0.8:
            eval = "极强鲁棒性"
        elif corr > 0.5:
            eval = "较强鲁棒性"
        elif corr > 0.2:
            eval = "弱鲁棒性"
        else:
            eval = "鲁棒性失效"
        print(f"{attack:^10} | {corr:^10.3f} | {eval:^15}")
    print("=" * 70)

效果对比图位置:生成 2 行 5 列对比图:

  1. 含水印图像与原始图像视觉无差异(PSNR>35dB);
  2. 即使施加 JPEG 压缩、高斯噪声、裁剪攻击,提取的水印相关系数仍 > 0.8(鲁棒性强);
  3. 无攻击时相关系数≈1(完全匹配)。

小结

  1. 图像压缩
    • 核心是去除冗余(编码冗余、空间 / 时间冗余、无关信息);
    • 无损压缩(霍夫曼、LZW、行程编码):无失真,压缩比适中;
    • 有损压缩(DCT、小波、预测编码):牺牲少量精度,压缩比高;
    • JPEG 基于 DCT,JPEG-2000 基于小波(无块效应、更灵活)。
  2. 数字水印
    • 空域水印简单但鲁棒性差,变换域(DWT/DCT)水印鲁棒性强;
    • 关键指标:不可感知性(PSNR)、鲁棒性(抗攻击能力)、安全性。

参考文献

  1. 《数字图像处理(第四版)》------Rafael C. Gonzalez(核心教材);
  2. 《JPEG-2000 标准详解》------David S. Taubman;
  3. 《数字水印技术原理与应用》------ 章毓晋;
  4. IEEE Transactions on Image Processing(顶级期刊,最新研究)。

延伸读物

  1. 深入学习:率失真理论、深度学习图像压缩(如 VVC/H.266);
  2. 工程应用:FFmpeg(视频压缩)、OpenCV(图像编解码)、PyWavelets(小波分析);
  3. 前沿方向:AI 生成图像的水印、抗 AI 篡改的鲁棒水印。

习题

  1. 基础题
    • 计算一幅 256x256 灰度图像的信息熵(假设灰度级 0-255 等概率),并计算 8 位固定编码的编码冗余。
    • 对比霍夫曼编码和算术编码的优缺点,说明算术编码更接近信息熵的原因。
  2. 编程题
    • 实现基于 DCT 的 JPEG 压缩,对比不同量化矩阵对压缩比和 PSNR 的影响。
    • 改进 DWT 域水印算法,加入盲提取(无需原始图像)功能。
  3. 综合题
    • 设计一个图像压缩 + 水印的完整系统:对图像进行小波压缩,在压缩域嵌入水印,解压后能正确提取水印。
    • 评估该系统在不同攻击(压缩、滤波、旋转)下的水印鲁棒性和图像压缩质量。
相关推荐
智航GIS2 小时前
ArcGIS大师之路500技---034重采样算法选择
人工智能·算法·arcgis
子夜江寒2 小时前
决策树与回归树简介:原理、实现与应用
算法·决策树·回归
~央千澈~2 小时前
序章《程序员进化:AI 编程革命》——用 Cursor 驱动的游戏开发实战作者:卓伊凡
人工智能·ai编程
风途知识百科2 小时前
专用气象设备 —— 光伏气象站与防爆气象站[特殊字符]!
人工智能
roman_日积跬步-终至千里2 小时前
【计算机视觉18-2】语义理解-CNN架构设计_VGG_Inception_ResNet
人工智能·计算机视觉·cnn
摄影图2 小时前
卫星插画推荐:星轨下的科技美学像素漫画图赏
人工智能·科技·aigc·插画
存储国产化前线2 小时前
国产工业级存储进阶之路:从自主可控主控到可靠可用的全链路突围
大数据·人工智能·物联网
TL滕2 小时前
从0开始学算法——第十九天(并查集)
笔记·学习·算法