图像本质上就是一个或多个矩阵,并且通常具有低秩结构,可以利用SVD分解舍弃低奇异值的部分来实现图片的压缩存储。

简单的 Python 示例实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import numpy as np import matplotlib.pyplot as plt from PIL import Image
def compress_img_first_k(img_array, k): if img_array.ndim == 2: img_array = img_array[:, :, np.newaxis]
m, n, c = img_array.shape compressed_channels = []
for i in range(c): channel = img_array[:, :, i] U, S, VT = np.linalg.svd(channel, full_matrices=False)
channel_k = U[:, :k] @ np.diag(S[:k]) @ VT[:k, :] compressed_channels.append(channel_k)
compressed_img = np.stack(compressed_channels, axis=2) if compressed_img.shape[2] == 1: compressed_img = compressed_img[:, :, 0]
compress_ratio = (m * n) / (m * k + k + k * n)
return compressed_img, k, compress_ratio
|
采用的测试图像如下(用 Lenna 的例子都看吐了,换个 AI 图)

对灰度图像进行压缩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| img = Image.open("code-svd-demo.jpg").convert("L") img_array = np.array(img) print(f"Image shape: {img_array.shape}")
fig, axes = plt.subplots(2,2,figsize=(10, 6))
axes[0][0].imshow(img, cmap='gray') axes[0][0].set_title("Original Image") axes[0][0].axis("off")
k = 100 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[0][1].imshow(np.uint8(np.clip(compress_img, 0, 255)), cmap='gray') axes[0][1].set_title(f"k={k}, ratio={ratio:.2f}") axes[0][1].axis("off")
k = 50 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[1][0].imshow(np.uint8(np.clip(compress_img, 0, 255)), cmap='gray') axes[1][0].set_title(f"k={k}, ratio={ratio:.2f}") axes[1][0].axis("off")
k = 20 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[1][1].imshow(np.uint8(np.clip(compress_img, 0, 255)), cmap='gray') axes[1][1].set_title(f"k={k}, ratio={ratio:.2f}") axes[1][1].axis("off")
plt.show()
|

对彩色图像进行压缩:(每一个通道的矩阵分别进行压缩)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| img = Image.open("code-svd-demo.jpg").convert("RGB") img_array = np.array(img) print(f"Image shape: {img_array.shape}")
fig, axes = plt.subplots(2,2,figsize=(10, 6))
axes[0][0].imshow(img) axes[0][0].set_title("Original Image") axes[0][0].axis("off")
k = 100 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[0][1].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[0][1].set_title(f"k={k}, ratio={ratio:.2f}") axes[0][1].axis("off")
k = 50 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[1][0].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[1][0].set_title(f"k={k}, ratio={ratio:.2f}") axes[1][0].axis("off")
k = 20 compress_img, k, ratio = compress_img_first_k(img_array, k) axes[1][1].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[1][1].set_title(f"k={k}, ratio={ratio:.2f}") axes[1][1].axis("off")
plt.show()
|

值得注意的是,如果不指定保留的秩,而是按照保留矩阵的范数比例进行压缩,可能会得到不好的效果,因为前几个奇异值太大了,这样设置容差会导致选取的秩太小,压缩后的图像效果很差。
还有一个本质上的问题:对于图像来说,两个矩阵的范数接近就意味着图像近似的效果更好吗?矩阵范数可能并不是一个好的“度量”。
指定容差的相关函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| def compress_img_tol(img_array, tol): if img_array.ndim == 2: img_array = img_array[:, :, np.newaxis]
_, _, c = img_array.shape
ks = [] for i in range(c): channel = img_array[:, :, i] S = np.linalg.svd(channel, compute_uv=False)
energy = np.cumsum(S**2) total_energy = energy[-1]
k_i = np.searchsorted(energy, tol * total_energy) + 1 ks.append(k_i)
k = max(ks) return compress_img_first_k(img_array, k)
|
例如取彩色图像进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| img = Image.open("code-svd-demo.jpg").convert("RGB") img_array = np.array(img) print(f"Image shape: {img_array.shape}")
fig, axes = plt.subplots(2, 2, figsize=(10, 6))
axes[0][0].imshow(img) axes[0][0].set_title("Original Image") axes[0][0].axis("off")
tol = 0.999 compress_img, k, ratio = compress_img_tol(img_array, tol) axes[0][1].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[0][1].set_title(f"tol={tol}, k={k}, ratio={ratio:.2f}") axes[0][1].axis("off")
tol = 0.99 compress_img, k, ratio = compress_img_tol(img_array, tol) axes[1][0].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[1][0].set_title(f"tol={tol}, k={k}, ratio={ratio:.2f}") axes[1][0].axis("off")
tol = 0.95 compress_img, k, ratio = compress_img_tol(img_array, tol) axes[1][1].imshow(np.uint8(np.clip(compress_img, 0, 255))) axes[1][1].set_title(f"tol={tol}, k={k}, ratio={ratio:.2f}") axes[1][1].axis("off")
plt.show()
|
