KMeans聚类实战:用Python给杂乱无章的图片颜色做“减法”,5步实现图像压缩
KMeans聚类实战:用Python给杂乱无章的图片颜色做“减法”,5步实现图像压缩
你是否曾经遇到过这样的场景:一张精美的风景照片因为体积过大无法快速上传,或是设计素材库中成千上万的图片占用了大量存储空间?这背后其实隐藏着一个有趣的数学问题——如何用更少的颜色尽可能真实地还原图像。本文将带你用Python和KMeans算法,像魔术师一样为图片颜色做"减法",仅用16色或64色就能呈现出令人满意的视觉效果。
这种技术被称为色彩量化(Color Quantization),是KMeans聚类在图像处理中的经典应用。不同于传统的图像压缩算法,它从数据本质出发,通过机器学习找到最具代表性的颜色组合。下面我们将从零开始,一步步实现这个既有趣又实用的项目。
1. 环境准备与数据理解
在开始编码前,我们需要准备好Python环境和必要的库,同时理解图像在计算机中的表示方式。
必备工具安装:
pip install numpy opencv-python matplotlib scikit-learn
图像数据本质上是一个三维数组:
- 对于RGB图像,形状为(高度, 宽度, 3)
- 每个像素由红(R)、绿(G)、蓝(B)三个通道的值组成
- 每个颜色通道的取值范围通常是0-255
让我们加载一张示例图片并查看其数据结构:
import cv2
import matplotlib.pyplot as plt
image = cv2.imread('sample.jpg')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # OpenCV默认BGR需转为RGB
print(f"图像尺寸:{image.shape}") # 输出如(800, 600, 3)
plt.imshow(image)
plt.show()
提示:实验时建议使用尺寸适中的图片(800×600左右),过大的图片会显著增加计算时间。
2. 数据预处理:从图像到特征矩阵
KMeans算法的输入是一个二维特征矩阵,而我们的图像是三维数组,需要进行适当的转换。
关键转换步骤:
- 将图像从(height, width, 3)重塑为(height×width, 3)
- 将像素值从0-255的整数转换为0-1的浮点数
- 可选:对数据进行标准化处理
import numpy as np
# 将图像数据转换为适合KMeans的格式
pixels = image.reshape(-1, 3).astype(np.float32)
pixels /= 255.0 # 归一化到[0,1]范围
print(f"转换后的特征矩阵形状:{pixels.shape}") # 如(480000, 3)
为什么需要归一化?因为:
- KMeans基于距离度量,不同尺度的特征会影响聚类结果
- 图像RGB通道天然具有相同尺度,归一化主要是为了数值稳定性
3. 应用KMeans进行色彩聚类
现在到了最核心的部分——使用KMeans算法找出最具代表性的颜色。我们需要决定使用多少种颜色(K值)来代表原始图像。
K值选择经验法则:
- 网页/移动应用:16-64色
- 艺术效果:4-8色
- 高质量压缩:128-256色
from sklearn.cluster import KMeans
n_colors = 16 # 尝试修改这个值观察效果
kmeans = KMeans(n_clusters=n_colors, random_state=42)
kmeans.fit(pixels)
理解KMeans的输出:
cluster_centers_: 各簇的中心点,即我们找出的代表性颜色labels_: 每个像素点所属的簇索引
让我们查看找到的16种主要颜色:
dominant_colors = kmeans.cluster_centers_
print("代表性颜色(RGB):")
print(dominant_colors)
4. 重构压缩后的图像
有了代表性颜色和每个像素对应的颜色索引,我们可以重建压缩后的图像。
重构步骤:
- 将每个像素替换为其所属簇的中心颜色
- 将数据形状恢复为原始图像尺寸
- 将颜色值从[0,1]范围转换回[0,255]
# 使用簇中心替换每个像素
compressed_pixels = dominant_colors[kmeans.labels_]
# 重塑回原始图像形状
compressed_image = compressed_pixels.reshape(image.shape)
# 转换回0-255范围并转为整数
compressed_image = (compressed_image * 255).astype(np.uint8)
# 显示原始和压缩后的图像对比
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.title("原始图像")
plt.imshow(image)
plt.subplot(1, 2, 2)
plt.title(f"压缩后({n_colors}色)")
plt.imshow(compressed_image)
plt.show()
5. 效果评估与优化
如何判断压缩效果的好坏?除了肉眼观察,我们可以计算一些量化指标。
常用评估指标:
- 均方误差(MSE):衡量压缩前后像素值的平均差异
- 峰值信噪比(PSNR):衡量图像质量的常用指标
- 文件大小对比:实际存储节省的空间
def calculate_mse(original, compressed):
return np.mean((original - compressed) ** 2)
mse = calculate_mse(image, compressed_image)
psnr = 10 * np.log10(255**2 / mse)
print(f"MSE: {mse:.2f}")
print(f"PSNR: {psnr:.2f} dB")
优化方向:
- 尝试不同的K值,寻找质量与压缩率的平衡点
- 使用KMeans++初始化方法(默认已使用)改善聚类效果
- 对特定颜色空间(如HSV)进行聚类可能获得更好的视觉效果
# 使用不同K值比较
for k in [4, 8, 16, 32, 64]:
kmeans = KMeans(n_clusters=k, random_state=42).fit(pixels)
compressed = kmeans.cluster_centers_[kmeans.labels_].reshape(image.shape)
mse = calculate_mse(image, compressed)
print(f"K={k}: MSE={mse:.2f}")
6. 高级应用与扩展
色彩量化只是KMeans在图像处理中的一个应用,这种思想可以扩展到许多有趣的方向:
1. 图像分割 将KMeans应用于像素位置+颜色特征,可以实现简单的图像分割:
# 添加像素坐标作为特征
height, width = image.shape[:2]
xx, yy = np.meshgrid(np.arange(width), np.arange(height))
pixel_locs = np.column_stack((xx.ravel(), yy.ravel()))
features = np.hstack((pixels, pixel_locs / [width, height])) # 归一化坐标
# 使用KMeans进行分割
kmeans_seg = KMeans(n_clusters=5).fit(features)
segmented = kmeans_seg.cluster_centers_[:, :3][kmeans_seg.labels_].reshape(image.shape)
2. 调色板生成 设计师可以提取图像的主色调创建协调的配色方案:
def plot_colors(colors):
plt.figure(figsize=(len(colors), 1))
for i, color in enumerate(colors):
plt.fill_between([i, i+1], 0, 1, color=color)
plt.xlim(0, len(colors))
plt.axis('off')
plot_colors(dominant_colors)
3. 视频压缩 将每帧图像量化为有限的颜色集,可以显著减少视频存储空间,这是早期视频编码的基础技术之一。
在实际项目中,我发现对于风景照片,K=16通常就能达到不错的效果,而人像照片可能需要K=64才能保持皮肤色调的自然过渡。一个实用的技巧是先用小尺寸图像确定最佳K值,再应用到全分辨率图像上,这可以大幅减少计算时间。
更多推荐

所有评论(0)