基于k-means的图像分割
-
- 一、核心思想:把图像分割问题转化为数据聚类问题
- 二、关键概念与步骤
-
- 1. 特征向量的构建
- 2. K-means 算法的基本步骤(应用于图像)
- 三、K值的选择与影响
- 四、特征设计的变体
- 五、优缺点总结
-
- 优点
- 缺点
- 六、直观例子
- 七、代码实现
-
- 数据准备
- 训练数据
- 语义分割标签的可视化函数
- 显示分割结果
一、核心思想:把图像分割问题转化为数据聚类问题
图像即数据点集合
- 想象一张彩色图片,每个像素都有颜色(如RGB值)和位置(x, y坐标)。
- 我们可以把每个像素看作一个数据点,其属性可以是颜色、位置等。
聚类的目标
- 目标是将所有像素点(数据点)分成 K 个组(簇)。
- 同一组内的像素在颜色/空间上尽可能相似,不同组之间尽可能不同。
- 最终,每个组在图像上就表现为一个分割区域。
二、关键概念与步骤
1. 特征向量的构建
要进行聚类,首先要定义每个像素的“特征”。常用方法:
-
纯颜色特征:只使用颜色信息(如 RGB 或 Lab 值)。 例如:一个像素的红色=200,绿色=100,蓝色=50,则其特征向量为 [200, 100, 50]。
-
颜色 + 空间特征:为了得到空间上连续的分割区域,可以把像素坐标也作为特征的一部分。 例如:[R, G, B, x, y]。此时相似性既考虑颜色相近,也考虑位置靠近。
2. K-means 算法的基本步骤(应用于图像)
步骤 1:初始化
- 设定要分成 K 个簇(比如 K=3,代表希望分割成3个主色/区域)。
- 随机选择 K 个像素的特征向量作为初始簇中心(也称为质心)。
步骤 2:分配阶段
- 遍历每个像素,计算其特征向量到 K 个簇中心 的距离(通常用欧氏距离)。
- 将该像素分配给距离最近的那个簇中心所在的簇。
- 完成后,所有像素都被标记为属于某一个簇(1到K之间的一个标签)。
步骤 3:更新阶段
- 对每个簇,重新计算其簇中心:取该簇内所有像素特征向量的平均值。
- 新的簇中心可能不再是某个实际像素的特征,而是一个平均值向量。
步骤 4:迭代
- 重复 步骤 2 和 步骤 3,直到满足停止条件:
- 簇中心不再显著变化(收敛)。
- 达到最大迭代次数。
步骤 5:输出分割结果
- 每个像素都有一个簇标签(1到K)。
- 将同一标签的像素在图像上显示为同一种颜色(如用该簇中心的颜色替换),就得到了分割后的图像。
三、K值的选择与影响
K 的含义 K 代表期望的分割区域数量。例如:
- K=2:将图像分为前景和背景。
- K=5-10:可能分离出不同的物体或颜色区域。
K 的影响
- K 太小:分割粗糙,不同物体可能被合并。
- K 太大:分割过细,同一物体可能被分成多块,且计算量增加。
如何选择 K
- 通常需要预先设定,这是 K-means 的主要缺点之一。
- 可尝试不同的 K 值,根据结果选择最合理的。
四、特征设计的变体
Lab 颜色空间 由于 Lab 比 RGB 更符合人眼感知差异,在 Lab 空间聚类往往比 RGB 效果更好。
加权空间坐标 在特征 [L, a, b, x, y] 中,对 x, y 乘以一个权重系数,以控制空间连续性对分割影响的强弱:
- 权重=0:只按颜色聚类,空间上可能不连续。
- 权重较大:强烈要求空间上靠近的像素分在同一簇,分割块更紧凑。
五、优缺点总结
优点
- 原理简单,易于理解和实现。
- 计算效率相对较高,适合快速原型。
- 无需训练数据,是无监督方法。
缺点
- 需要预先指定 K 值,且 K 的选择对结果影响大。
- 初始质心随机选择,可能导致每次结果略有不同。
- 倾向于产生凸形、大小相似的簇,对复杂形状分割效果有限。
- 仅考虑特征距离,缺乏对图像边缘、纹理等高层语义的利用。
六、直观例子
想象一张风景图:蓝天、白云、绿树。
- 簇1 → 所有蓝色像素(天空)
- 簇2 → 所有白色/浅灰像素(云)
- 簇3 → 所有绿色像素(树和草地)
七、代码实现
本次代码直接使用sklearn中的k-means方法。如果想要进一步了解k-means的原理,我们提供k-menas从零开始的编程实现。
数据准备
from sklearn.cluster import KMeans
from matplotlib.image import imread
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
image = imread('segmentation.jpeg')[:,:,:3]
# 将RGB值统一到0-1内
if np.max(image)>1:
image = image / 255
X = image.reshape(–1, image.shape[2])
这段代码使用 scikit-learn 的 KMeans 聚类算法对图像进行颜色聚类(图像分割的前期步骤)
from sklearn.cluster import KMeans # K均值聚类算法
from matplotlib.image import imread # 读取图像
import matplotlib.pyplot as plt # 绘图
import numpy as np # 数值计算
from PIL import Image # 图像处理(这里未使用)
image = imread('segmentation.jpeg')[:,:,:3]
- imread():读取名为 'segmentation.jpeg' 的图像
- [:,:,:3]:切片操作,只取前3个通道(RGB)
- 作用:如果图像有RGBA(带透明度)通道,只保留RGB,忽略alpha通道
if np.max(image) > 1:
image = image / 255
- 检查图像像素值的最大值
- 如果最大值 > 1(通常是0-255的整数),则除以255归一化到[0,1]区间
- 目的:许多机器学习算法对输入数据的尺度敏感,归一化可以:
- 加速算法收敛
- 提高数值稳定性
- 使不同特征(R、G、B通道)具有相同的重要性
X = image.reshape(–1, image.shape[2])
- image.shape:原始图像的维度,例如 (height, width, 3)
- reshape(-1, image.shape[2]):
- -1:自动计算该维度的大小(= height × width)
- image.shape[2]:通道数(3,对应RGB)
- 结果:将三维数组 (h, w, 3) 转换为二维数组 (h*w, 3)
- 每个像素变成一行数据,包含3个特征:[R, G, B]值
数据转换示例 假设原图是 100×100 像素:
- 转换前:image.shape = (100, 100, 3),共10000个像素点
- 转换后:X.shape = (10000, 3),10000行数据,每行是一个像素的RGB值
训练数据
# 利用k-means算法进行聚类
segmented_imgs = []
# 设定聚类中心个数
n_cluster= 4
kmeans = KMeans(n_clusters=n_cluster, random_state=42).fit(X)
print(np.unique(kmeans.labels_))
输出
[0 1 2 3]
语义分割标签的可视化函数
# 为每一个类别赋予一个对应的颜色,用于展示
def decode_segmap(label_mask, plot=False):
label_colours = np.asarray([[79, 103, 67], [143, 146, 126],
[129, 94, 64], [52, 53, 55],
[96, 84, 70], [164, 149, 129]])
r = label_mask.copy()
g = label_mask.copy()
b = label_mask.copy()
# 为每个类别赋予对应的R、G、B值
for ll in range(0, 6):
r[label_mask == ll] = label_colours[ll, 0]
g[label_mask == ll] = label_colours[ll, 1]
b[label_mask == ll] = label_colours[ll, 2]
rgb = np.zeros((label_mask.shape[0], label_mask.shape[1], 3))
rgb[:, :, 0] = r
rgb[:, :, 1] = g
rgb[:, :, 2] = b
return rgb
这段代码实现了一个语义分割标签的可视化函数,将分类标签映射为对应的RGB颜色图像。将包含类别索引(0-5)的标签掩码转换为彩色图像,每个类别有固定的颜色表示。
label_colours = np.asarray([[79, 103, 67], [143, 146, 126],
[129, 94, 64], [52, 53, 55],
[96, 84, 70], [164, 149, 129]])
- 定义了6个类别的RGB颜色值(每个类别一个颜色)
- 颜色值范围:0-255
- 对应关系:
- 类别0 → RGB(79, 103, 67) # 偏绿色
- 类别1 → RGB(143, 146, 126) # 偏灰色
- …依此类推
r = label_mask.copy() # 红色通道
g = label_mask.copy() # 绿色通道
b = label_mask.copy() # 蓝色通道
- 创建三个与输入标签相同形状的数组
- 初始值就是类别索引(0-5)
for ll in range(0, 6):
r[label_mask == ll] = label_colours[ll, 0] # 红色分量
g[label_mask == ll] = label_colours[ll, 1] # 绿色分量
b[label_mask == ll] = label_colours[ll, 2] # 蓝色分量
- 遍历6个类别
- 将标签中等于当前类别ll的所有位置
- 在r通道中设置为该类别颜色的R值
- 在g通道中设置为该类别颜色的G值
- 在b通道中设置为该类别颜色的B值
rgb = np.zeros((label_mask.shape[0], label_mask.shape[1], 3))
rgb[:, :, 0] = r # 红色通道
rgb[:, :, 1] = g # 绿色通道
rgb[:, :, 2] = b # 蓝色通道
- 创建三维数组(高度×宽度×3通道)
- 分别填充R、G、B三个通道
return rgb
- 返回一个形状为(H, W, 3)的NumPy数组
- 可以直接用matplotlib显示或保存为图像
显示分割结果
# 获得预测的标签
segmented_img = kmeans.cluster_centers_[kmeans.labels_]
segmented_imgs = decode_segmap(kmeans.labels_.reshape(
image.shape[0],image.shape[1]))
# 展示结果
plt.imshow(image[:,:,:3])
plt.title('Original image')
plt.axis('off')
plt.show()
plt.imshow(segmented_imgs.astype(np.uint8))
plt.title('{} center'.format(n_cluster))
plt.axis('off')
plt.show()
这段代码使用K-means聚类对图像进行分割,并将分割结果显示出来。
segmented_img = kmeans.cluster_centers_[kmeans.labels_]
- kmeans.labels_:每个像素点所属的聚类标签(0到n_cluster-1)
- kmeans.cluster_centers_:每个聚类的中心颜色值(RGB值)
- segmented_img:用聚类中心颜色替换每个像素的原始颜色,得到一个颜色简化的图像
segmented_imgs = decode_segmap(kmeans.labels_.reshape(
image.shape[0],image.shape[1]))
- kmeans.labels_.reshape(image.shape[0], image.shape[1]):将一维的标签数组重新变回二维图像形状
- decode_segmap():调用之前定义的颜色映射函数,将类别标签转换为预设的固定颜色
- segmented_imgs:得到用固定颜色表示每个类别区域的彩色分割图
plt.imshow(image[:,:,:3])
plt.title('Original image')
plt.axis('off')
plt.show()
- image[:,:,:3]:显示图像的前三个通道(通常是RGB,如果是RGBA图像则忽略Alpha通道)
- plt.axis('off'):隐藏坐标轴
- 显示原始彩色图像
plt.imshow(segmented_imgs.astype(np.uint8))
plt.title('{} center'.format(n_cluster))
plt.axis('off')
plt.show()
- segmented_imgs.astype(np.uint8):将图像转换为uint8类型(matplotlib显示需要)
- '{} center'.format(n_cluster):标题显示使用了多少个聚类中心
- 显示K-means聚类后的分割结果
segmented_img vs segmented_imgs:
- segmented_img:用聚类中心颜色显示,颜色来自图像本身
- segmented_imgs:用固定颜色显示,每个类别有固定颜色,便于识别不同区域
输出 






