基于k-means的彩色图像分割:MATLAB实现与参数调优
发布时间:2026/9/15 22:51:10来源:尧图网络
简介这是一套基于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 个集合使类内平方和WCSSWithin-Cluster Sum of Squares最小。目标函数是 J Σ_{i1}^{k} Σ_{x∈C_i} ||x - μ_i||²意义是所有样本到各自质心距离平方之和。迭代流程四步初始化质心、分配样本到最近质心、重新计算质心、重复直到质心变化小于容差。其中分配步骤的计算量最大是影响整体运行时间的关键。k-means 保证收敛到局部最优不保证全局最优。这是初学者最容易忽略的同样的 k、同样的数据每次运行结果可能不同因为初始化不同。MATLAB 的kmeans函数默认使用 k-means 初始化imsegkmeans也吸收了类似策略。理解了这一点再看后面Replicates参数为什么不是可选项而是必调项就顺理成章了。特征矩阵在 MATLAB 里的存储和kmeans期望的输入格式不一致这也是新手容易卡住的地方。图像是 H×W×D聚类希望 N×DN 是像素总数。下面两行是标准预处理% 图像特征 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 2N 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参数可选plusk-means、sample随机抽样、cluster用层次聚类预分组。k-means 是最稳的选择它让初始质心尽量分散能明显降低陷入糟糕局部最优的概率。Replicates参数控制重复运行次数保留最优 WCSS 的一次。重复 3 到 5 次通常够了图像数据量大时重复次数要跟运行时间做权衡。提示imsegkmeans是统计工具箱和图像处理工具箱里更上层的封装内部就是kmeans但会帮你处理好图像尺寸、颜色通道、迭代结束条件输出是像素标签矩阵。带SpatialInput时还把坐标拼接进特征。色彩空间通道含义对k-means的影响MATLAB转换函数RGBR/G/B强度相关性高受亮度影响大距离无感知意义直接使用Lab*L亮度a红绿b蓝黄感知均匀常用特征rgb2labHSVH色相S饱和度V亮度H有环形问题0和360距离近但欧氏距离远rgb2hsvHSV 的 H 通道是环形的直接丢进 k-means 会出错除非做角度转换或把 H 拆成 sin/cos 两个特征。这也是很多人踩过的坑。图像尺寸对 k-means 的影响很直接一张 1920×1080 的图有约 200 万个像素特征维度如果是 3double 矩阵占 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* 两通道做聚类k4 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的输出是 doubleL* 值域大约 [0,100]a* 和 b* 大约在 [-100,100]。如果直接把完整的lab传进去L* 的数值范围会主导距离导致实际上只看亮度聚类。这也是为什么只取 a* 和 b*或者先对每个通道做标准化。imsegkmeans不会自动标准化这一步必须自己做。imsegkmeans和kmeans的关系可以看这张表函数输入格式输出适用场景kmeansN×P 矩阵N×1 标签、k×P 质心需要自定义特征或预处理imsegkmeansH×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 往往就是前景/背景类别数再加干扰项。比如一张图里有人、车、道路、建筑、天空k5如果光照变化大阴影区域需要单分一类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。例如工业检测里只区分良品和缺陷k2医学图像想分离组织、肿瘤、背景k3。这是最实用也是我默认的方法。4.2 Distance、Start、MaxIter 的推荐设置参数默认值推荐值或场景影响DistancesqeuclideanLab* 下用默认RGB 下可试 cosine决定相似性定义sqeuclidean 对离群点敏感Startplusplus 一般足够影响初始质心进而影响局部最优MaxIter100图像大时 200-300迭代上限达到容差会提前停止Replicates13 或 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 和轮廓系数做相对判断。注意不要只盯着 WCSSk 越大 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的Startplus依赖全局随机流。固定种子后同一输入会得到同一输出。另一个验证是稳定性重复运行 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但手写实现较长对彩色图像分割来说固定种子加像素一致率已经能挡住绝大多数可重复性质疑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网