简介:这是一套基于K-means聚类算法实现彩色图像分割的MATLAB项目源码,主要面向图像处理初学者及具备一定开发经验的研究人员,适用于理解聚类特征提取、像素分类与区域分割的完整流程。压缩包共包含55个文件,以19个.m脚本为算法核心,辅以25张JPG测试图像、2个.mat数据文件及一份算法说明文档,整体打包大小仅869KB,轻量易部署。源码目录按图像、脚本、字典等模块组织,便于对照学习。已有1207人浏览学习。通过该资源,读者可以获得完整的K-means分割实现思路,包括金字塔特征构建、聚类合并、区域统计等关键步骤,配合多张自然图像进行测试,可直观观察不同聚类数对分割效果的影响,适合课程设计或算法复现。
1. 彩色图像分割不是颜色分类,k-means其实解决的是“距离”问题
一张彩色照片里,树叶的亮部、暗部和背景的阴影在 RGB 通道上可能相差很远,但人眼能无意识地归为同一片叶子。直接用固定阈值做彩色图像分割,几乎必然把同一物体的受光面和背光面切成两块。k-means 聚类之所以是这一类场景的默认起点,因为它不关心颜色绝对值,只关心样本点在特征空间里的相对距离。它把每个像素当作一个点,点的坐标来自颜色通道,目标是让同一个簇内的点在距离度量下尽量靠拢。这里的内容面向已经在用 MATLAB 处理图像、但对聚类细节还没吃透的工程师,从原理讲到可运行代码,再讲参数设置的坑,最后给出接进实际流程的验证方法。
2. 彩色图像分割的聚类原理:从RGB到Lab*,k-means到底在算什么
2.1 为什么RGB不是k-means的最佳输入
直接用 RGB 作为聚类特征,问题不在通道本身,而在距离度量。RGB 三个通道是强相关的,而且对光照变化极其敏感。想象同一个物体在阴影下的像素值(12, 35, 20)和阳光下的(200, 210, 180),这两个点在 RGB 空间里的欧氏距离非常大,k-means 很自然把它们分到两个簇。可是对彩色图像分割任务来说,这通常意味着同一个物体被切开。常见做法是先做色彩空间转换,把亮度信息从色度信息里剥离出来。
Lab* 色彩空间被设计成感知均匀空间,欧氏距离的差值与人的感知差异更接近。MATLAB 里可以用rgb2lab把图像从 8 位 RGB 转到 Lab*。转换以后,a* 和 b* 两个通道表达颜色对立关系,L* 通道单独承载亮度。很多分割任务只取 a* 和 b* 做聚类,故意丢掉 L*,来降低阴影影响。这样做的代价是原本靠亮度区分的区域会融合,所以具体取舍要看对象。
像素点还需要考虑空间坐标。如果只把颜色通道作为特征,那么远处和近处两块相同颜色的区域会被归为一类,这符合颜色聚类的语义。但如果目标是切出某个连通物体,通常还要把 (x, y) 坐标拼进特征向量。这样做的副作用是会产生“空间正则”的簇,但 k 值要更大,因为颜色维度被稀释。MATLAB 的imsegkmeans在较新版本支持SpatialInput参数,但不少工程师会先试纯颜色版本,再用形态学后处理修复碎片化。
2.2 标准k-means的目标函数与迭代流程
k-means 的目标是把 N 个像素点分成 k 个集合,使类内平方和(WCSS,Within-Cluster Sum of Squares)最小。目标函数是 J = Σ_{i=1}^{k} Σ_{x∈C_i} ||x - μ_i||²,意义是所有样本到各自质心距离平方之和。迭代流程四步:初始化质心、分配样本到最近质心、重新计算质心、重复直到质心变化小于容差。其中分配步骤的计算量最大,是影响整体运行时间的关键。
k-means 保证收敛到局部最优,不保证全局最优。这是初学者最容易忽略的:同样的 k、同样的数据,每次运行结果可能不同,因为初始化不同。MATLAB 的kmeans函数默认使用 k-means++ 初始化,imsegkmeans也吸收了类似策略。理解了这一点,再看后面Replicates参数为什么不是可选项而是必调项,就顺理成章了。
特征矩阵在 MATLAB 里的存储和kmeans期望的输入格式不一致,这也是新手容易卡住的地方。图像是 H×W×D,聚类希望 N×D,N 是像素总数。下面两行是标准预处理:
% 图像特征 H x W x D 到 N x D 的标准转换 a = lab(:,:,2); b = lab(:,:,3); features = double(cat(3, a, b)); % h x w x 2 featuresVec = reshape(features, [], 2); % N x 2,N = h * wreshape按列优先顺序展开,这意味着第 n 行对应像素位于ceil(n / w)行、mod(n-1, w)+1列。只要在恢复图像时用同一个尺寸做reshape,行列关系就不会错。这样做是为了避免直接在kmeans里传 3 维数组,因为kmeans会把 3 维数组当成 N 行 × (h*w) 列的特征矩阵,逻辑完全不一样。
2.3 kmeansClusters 中的距离度量与初始化选择
这里讨论一种常见做法:自己写一个名为kmeansClusters的函数,把“距离度量”和“初始化”暴露成参数。这个函数不是某工具箱里的现成函数名,而是项目里对 k-means 封装层的统称,后面给出的自定义函数就叫这个名字。距离度量常见有'sqeuclidean'(平方欧氏)、'cosine'(余弦)、'cityblock'(曼哈顿)。图像分割里几乎只用'sqeuclidean',因为 Lab* 空间下欧氏距离有明确的感知含义。但如果你把 RGB 值直接丢进去,'sqeuclidean'会被高亮度区域主导,所以要么先转色彩空间,要么改用余弦距离。
初始化方面,Start参数可选'plus'(k-means++)、'sample'(随机抽样)、'cluster'(用层次聚类预分组)。k-means++ 是最稳的选择,它让初始质心尽量分散,能明显降低陷入糟糕局部最优的概率。Replicates参数控制重复运行次数,保留最优 WCSS 的一次。重复 3 到 5 次通常够了,图像数据量大时,重复次数要跟运行时间做权衡。
提示:
imsegkmeans是统计工具箱和图像处理工具箱里更上层的封装,内部就是kmeans,但会帮你处理好图像尺寸、颜色通道、迭代结束条件,输出是像素标签矩阵。带SpatialInput时,还把坐标拼接进特征。
| 色彩空间 | 通道含义 | 对k-means的影响 | MATLAB转换函数 |
|---|---|---|---|
| RGB | R/G/B强度,相关性高 | 受亮度影响大,距离无感知意义 | 直接使用 |
| Lab* | L亮度,a红绿,b蓝黄 | 感知均匀,常用特征 | rgb2lab |
| HSV | H色相,S饱和度,V亮度 | H有环形问题,0和360距离近但欧氏距离远 | rgb2hsv |
HSV 的 H 通道是环形的,直接丢进 k-means 会出错,除非做角度转换或把 H 拆成 sin/cos 两个特征。这也是很多人踩过的坑。图像尺寸对 k-means 的影响很直接:一张 1920×1080 的图有约 200 万个像素,特征维度如果是 3,double 矩阵占 48 MB;加入坐标变成 5 维,就是 80 MB。MATLAB 的kmeans默认用 double 计算,内存和速度都会翻倍。常见做法是把特征转成 single 再聚类,分割任务里精度损失几乎看不出来。另一个做法是先下采样聚类,再把标签图用imresize放大回原尺寸,速度提升明显。
3. 用 MATLAB 实现 k-means 彩色图像分割的可运行代码
3.1 最小可复现脚本:imsegkmeans 一行拿到分割结果
先给官方路线,假设图像I是 RGB 格式,已安装 Image Processing Toolbox 和 Statistics and Machine Learning Toolbox。较新的 MATLAB 版本(R2023b 以后)对这些函数支持很完整,如果你还停留在旧版本,建议先更新到有imsegkmeans的发布。脚本如下:
% 读取并转换色彩空间 I = imread('peppers.png'); % RGB 图像,uint8 lab = rgb2lab(I); % 转 L*a*b* % 取 a* b* 两通道做聚类,k=4 numClusters = 4; [L, centers] = imsegkmeans(lab(:,:,2:3), numClusters, ... 'NumAttempts', 3); % 重复3次取最优 % 可视化分割结果 figure; imshow(label2rgb(L)); % 标签矩阵转伪彩色imsegkmeans的第一个输入可以是 M×N×P 的图像,也可以是 M×N×D 的特征图。这里传入lab(:,:,2:3),大小和原图一样,但只有两个通道。输出L是 h×w 的标签矩阵,每个像素的值是 1 到 k 的整数;centers是 k×2 的质心矩阵,顺序对应L中的标签值。NumAttempts对应kmeans的Replicates,默认是 1,设成 3 能在几乎不增加多少时间的情况下降低局部最优风险。
这里要注意rgb2lab的输出是 double,L* 值域大约 [0,100],a* 和 b* 大约在 [-100,100]。如果直接把完整的lab传进去,L* 的数值范围会主导距离,导致实际上只看亮度聚类。这也是为什么只取 a* 和 b*,或者先对每个通道做标准化。imsegkmeans不会自动标准化,这一步必须自己做。
imsegkmeans和kmeans的关系可以看这张表:
| 函数 | 输入格式 | 输出 | 适用场景 |
|---|---|---|---|
kmeans | N×P 矩阵 | N×1 标签、k×P 质心 | 需要自定义特征或预处理 |
imsegkmeans | H×W×D 特征图 | H×W 标签、k×D 质心 | 快速原型、图像分割专用 |
imsegkmeans内部会做 reshape,并自动把标签恢复成图像大小。它解决的是便利性问题,不是正确性问题;如果你需要跨版本兼容或者要对特征做更复杂的标准化,用kmeans自控反而更清晰。
3.2 不依赖 imsegkmeans 的 kmeansClusters 封装
如果需要控制更多细节,常见做法是用 MATLAB 自带的kmeans自己封装。下面是一个可运行的函数,命名就是kmeansClusters,输入是特征矩阵,输出标签、质心和类内平方和。
function [clusterIdx, centers, withinClusterSum] = ... kmeansClusters(features, k, opts) % KMEANSCLUSTERS 对特征矩阵应用 k-means 聚类 % 输入: % features - NxP 矩阵,N 为像素数,P 为特征维度 % k - 聚类数 % opts - 可选参数结构体,可带 Distance, Start, MaxIter % 输出: % clusterIdx - Nx1 标签向量 % centers - kxP 质心矩阵 % withinClusterSum - 标量,类内平方和 arguments features (:,:) double k (1,1) {mustBeInteger, mustBePositive} opts.Distance (1,1) string = "sqeuclidean" opts.Start (1,1) string = "plus" opts.MaxIter (1,1) {mustBeInteger, mustBePositive} = 100 end [clusterIdx, centers, sumd] = kmeans(features, k, ... 'Distance', opts.Distance, ... 'Start', opts.Start, ... 'MaxIter', opts.MaxIter, ... 'Replicates', 3, ... 'Options', statset('UseParallel', true)); withinClusterSum = sum(sumd); end调用方式:
I = imread('peppers.png'); lab = rgb2lab(I); labAB = lab(:,:,2:3); nPixels = size(labAB,1) * size(labAB,2); features = reshape(labAB, nPixels, 2); % 每行一个像素 [clusterIdx, centers, wcss] = kmeansClusters(features, 4); % 恢复成图像布局 L = reshape(clusterIdx, size(labAB,1), size(labAB,2)); figure; imshow(label2rgb(L));reshape保持行的顺序,所以clusterIdx恢复成二维时行列关系不变。statset('UseParallel', true)让工具箱用并行池加速;如果没开并行池,第一次调用会启动,时间较长。数据量小时不建议开,因为启动开销可能大于省下来的时间。arguments块是 R2019b 引入的语法,如果你的 MATLAB 更旧,需要改成传统的varargin或直接写固定参数。使用varargin的兼容版本在老旧环境里更稳妥,但可读性稍差。
3.3 从分割掩膜到可视化:label2rgb 和边界叠加
标签矩阵本身不是最终交付物。分割结果通常要转成两类产品:一类是每个簇的掩膜,另一类是轮廓叠加在原图上。生成掩膜:
for c = 1:numClusters mask = (L == c); % 保存或进一步处理 end把每个簇轮廓叠到原图上:
% 找标签之间的边界:膨胀后和原标签不一致的位置 boundary = L ~= imdilate(L, ones(3,3)); overlay = I; for c = 1:3 channel = overlay(:,:,c); channel(boundary) = 255 * (c == 1); % 红通道置255 overlay(:,:,c) = channel; end figure; imshow(overlay);ones(3,3)是 3×3 结构元素,膨胀后与原始标签比较,能找出所有紧邻不同类别的像素。红色通道置 255,绿蓝置 0,就形成红色轮廓。imdilate来自图像处理工具箱,如果没有这个工具箱,可以用circshift手写邻域判断,但没必要重复造轮子。这个做法比edge算子更准确,因为直接比较标签而不是梯度,不会受颜色渐变影响。
对于超大图,不建议一次性把整张图喂进 k-means。常见做法是滑动窗口或先做超像素。超像素(SLIC)产生的块数量远小于像素数,把每个块的均值颜色作为聚类样本,聚类后把标签映射回块内所有像素。MATLAB 的superpixels函数在图像处理工具箱里,500 像素块大小的设置能保持边界质量,运行时间从分钟级降到秒级。这和 k-means 结合是彩色图像分割里很成熟的一套流程。
4. k 值、初始化、距离度量:这些参数才是分割质量的瓶颈
4.1 确定 k 的三种方法:肘部法则、轮廓系数、任务约束
k 是 k-means 唯一必须人工指定的超参数。彩色图像分割里,k 往往就是前景/背景类别数再加干扰项。比如一张图里有人、车、道路、建筑、天空,k=5;如果光照变化大,阴影区域需要单分一类,k 可能还要加 1。数据驱动的方法有三类。
肘部法则:对不同 k 计算 WCSS,画出折线图,找拐点。MATLAB 实现:
kList = 1:8; wcss = zeros(size(kList)); for i = 1:numel(kList) [~, ~, sumd] = kmeans(features, kList(i), ... 'Replicates', 3, 'MaxIter', 100); wcss(i) = sum(sumd); end plot(kList, wcss, '-o');拐点通常是主观判断,所以这种方法并不总可靠,但能筛掉离谱的 k。轮廓系数计算每个点相对自己簇和其他簇的紧密度,平均值越接近 1 越好。MATLAB 的evalclusters可以直接做:
clustEval = evalclusters(features, 'kmeans', 'silhouette', 'KList', 1:8); plot(clustEval);注意轮廓系数计算复杂度接近 O(N^2),200 万像素根本算不动。常见做法是从features里随机抽样几千个点来评估,再对抽样点做聚类,而不是全图。第三种方法是任务约束,根据实际需求定 k。例如工业检测里只区分良品和缺陷,k=2;医学图像想分离组织、肿瘤、背景,k=3。这是最实用也是我默认的方法。
4.2 Distance、Start、MaxIter 的推荐设置
| 参数 | 默认值 | 推荐值或场景 | 影响 |
|---|---|---|---|
| Distance | sqeuclidean | Lab* 下用默认;RGB 下可试 cosine | 决定相似性定义,sqeuclidean 对离群点敏感 |
| Start | plus | plus 一般足够 | 影响初始质心,进而影响局部最优 |
| MaxIter | 100 | 图像大时 200-300 | 迭代上限,达到容差会提前停止 |
| Replicates | 1 | 3 或 5 | 重复次数越多,越可能跳出局部最优 |
| Options | 默认 | UseParallel 酌情开启 | 多核加速,小数据反而慢 |
MaxIter常被误解为必须迭代到这么多轮;实际上只要质心移动小于Options.TolFun设定的容差,迭代会提前终止。如果设置太小(比如 10),聚类可能没收敛就停了,标签结果会很碎。我一般设 200 足够,TolFun保持默认的 1e-4。Start为'plus'时,k-means++ 的时间开销与特征维度成正比,对图像这种大样本影响不大。还有'uniform'和'cluster',一般不用。
4.3 常见误区和修正方式
局部最优:即使 k-means++ 也无法完全避免。解决方法是增大Replicates,并检查每次随机的 WCSS 是否波动很大。如果波动大,说明数据有多个明显分区,k 可能太小或太大。可以用statset('Display','iter')看重复次数的收敛过程。
颜色污染:被摄物体有反光或渐变色。比如红色车身上有一条高光,高光区域在 a* 上接近白色,会被单独分出来。处理办法是在特征里加入位置信息,让空间邻域趋向于同一个簇。imsegkmeans在较新版本支持'SpatialInput', true,会拼接像素坐标;代价是分割边界会偏向规则的块,运行时间变长。
噪声干扰:单像素噪声会在标签图上形成椒盐状碎片。解决办法常常不是改 k-means,而是后处理:用medfilt2对标签图或原图做中值滤波。不过直接对标签矩阵滤波可能抹掉小目标;更安全的是用bwareaopen去掉面积小于阈值的连通区域。也可以对聚类前的特征图做高斯滤波,效果更平滑但会丢失细节。
分割质量不能只看视觉。如果有一批人工标注的 ground truth,可以计算像素准确率和 IoU。像素准确率简单但容易被背景类主导,IoU 对类别不平衡更鲁棒。MATLAB 可以用confusionmat得到混淆矩阵再算 IoU。如果没标注,只能靠 WCSS 和轮廓系数做相对判断。注意不要只盯着 WCSS,k 越大 WCSS 一定越小,必须结合 k 的语义。
提示:k-means 对离群点敏感。如果图像里有大量透明区域或纯黑边,这些像素在 Lab* 下可能聚集并干扰质心。要么先做掩膜排除,要么专门分一个类给它们。
5. 进阶:把 kmeansClusters 接到真实图像流程中的验证技巧
5.1 用面积阈值和连通域清理分割结果
分割标签图上总有小碎块。常见做法是逐类检查连通分量,用bwareaopen删除小于阈值的区域。比如想保留至少 500 像素的区域:
cleanLabels = L; for c = 1:numClusters mask = (L == c); mask = bwareaopen(mask, 500); % 删除面积小于500的连通域 cleanLabels(L == c & ~mask) = 0; % 0表示未分配 end % 重新分配未标记像素:取最近质心 unassigned = (cleanLabels == 0); [~, assignedCluster] = min(pdist2(features(unassigned,:), centers), [], 2); cleanLabels(unassigned) = assignedCluster;这里的关键是用bwareaopen删除小连通域后,图中会出现未标记像素,不能直接留 0,否则后面统计会比预期少一类。通过pdist2计算每个未分配像素到所有质心的距离,取最近质心重新分配,这与 k-means 的收敛逻辑一致。pdist2需要统计和机器学习工具箱;如果没有,可以用循环加vecnorm代替,但要注意内存。
如果想把小区域按标签合并回去,而不是重新分配,可以先用bwlabel给每个连通域编号,然后统计每个连通域面积和所属标签。面积小于阈值的连通域,用其邻域中出现最多的标签填充。这个做法的好处是能保持原有分割边界,坏处是如果相邻区域全是被删除的小块,填充结果可能偏向噪声。
5.2 自动生成调色板和伪彩色可视化
分割的最终呈现一般要回答“每个区域是什么颜色”。常见做法是把 k 个簇的质心颜色转回 RGB,然后给每个标签上色。但质心来自 a* b* 通道,没有 L*,所以需要固定一个亮度值。
fixedL = 70; % 固定亮度,避免亮暗主导 centersAB = centers; % 来自 kmeans 的 a* b* 质心 centersLab = [ones(size(centersAB,1),1)*fixedL, centersAB]; centersRGB = lab2rgb(centersLab); % 转回 sRGB pseudo = zeros(size(L,1), size(L,2), 3); for c = 1:numClusters mask = (L == c); pseudo = pseudo + double(mask) .* reshape(centersRGB(c,:), 1,1,3); end figure; imshow(pseudo);固定 L*=70 让不同簇在感知亮度上一致,视觉上更像“调色板分类图”,不会因为某一个簇恰好特别暗或特别亮而掩盖其他区域。这个技巧在写报告和做演示时很实用。lab2rgb输出范围在 [0,1],imshow会直接按 double 显示,不需要再转换。
5.3 可重复性验证:固定随机种子和稳定性检查
k-means 每次运行结果可能不同,在论文或验收时要能复现。常见做法是在脚本开头设置随机种子:
rng(42);因为kmeans的Start='plus'依赖全局随机流。固定种子后,同一输入会得到同一输出。另一个验证是稳定性:重复运行 10 次,统计标签结果两两之间一致的比例。
numRuns = 10; labelsRuns = zeros(numel(L), numRuns); for r = 1:numRuns [idx, ~, ~] = kmeans(features, numClusters, ... 'Start', 'plus', 'Replicates', 1); labelsRuns(:, r) = idx; end % 以第一次运行为基准,计算像素一致率 agreement = mean(all(labelsRuns == labelsRuns(:,1), 2));all(...)逐行判断所有列是否与第一次相同。这个一致性比例算不上标准指标,但能快速判断 k-means 是否稳定。如果两次运行有 5% 的像素归属翻转,说明数据本就没有清晰的簇边界,k 可能需要调大或换特征。更严格的做法是算调整兰德指数(ARI),但手写实现较长;对彩色图像分割来说,固定种子加像素一致率已经能挡住绝大多数可重复性质疑。
本文还有配套的精品资源,点击获取