ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Matlab K-means图像分割实战:特征构造、K值选择与避坑指南

Matlab K-means图像分割实战:特征构造、K值选择与避坑指南 简介这份资料包面向图像处理入门者与Matlab实践者围绕K-means聚类算法在图像特征分割中的应用展开帮助读者理解无监督聚类如何将像素按RGB或灰度特征划分为若干类别并观察不同K值与初始质心对分割结果的影响。包内共10个文件以7个m脚本为核心涵盖主流程、初始质心选取、距离计算与聚类验证等环节另附2个png与1个jpg示例图像整体约487KB便于直接运行与对照实验。目前已有1380人学习下载说明该案例在同类资源中具有一定参考价值。通过阅读与调试这些代码读者可掌握数据预处理、质心迭代更新、分割结果可视化等关键步骤并借助示例图像比较不同参数下的分割效果为后续在目标识别、区域提取等任务中应用K-means打下实践基础。1. 从一张过曝的街景图说起K-means 图像分割到底在切什么一张在正午拍下的街景天空过曝成一片死白路面阴影又黑得看不清纹理。你想把「天空」「建筑」「路面」「植被」这几类区域分开用阈值法试了三遍不是天空和白色墙面被归成一类就是阴影里的树和路面糊在一起。这种场景下K-meansK 均值聚类做图像特征分割往往比手工调阈值更省心——它不关心你定义的阈值是多少只关心像素在特征空间里离哪个中心更近。这个标题讲的事情很具体把图像里每个像素或每个超像素块映射成一组特征向量通常是 RGB 三通道值也可以是 Lab、HSV 或加了空间坐标的五维向量然后用 K-means 把这堆向量聚成 K 类每一类对应图像里的一块区域。它解决的是「无监督地把图像切成若干语义相近区域」的问题适合做预处理——比如给分割网络做伪标签、给遥感影像做地物粗分、给工业质检做缺陷区域定位。适合谁手上已经有 Matlab 环境、想快速验证分割思路、又不想一上来就训深度模型的人。Matlab 的kmeans函数和图像处理工具箱把矩阵运算和可视化都包好了几十行就能跑通一个可看的版本。但这里有个反直觉的点K-means 在图像上跑得「看起来还行」不代表它真的理解了图像。它只是在颜色空间里画了几个球球的大小和位置由你给的 K 和初始中心决定。所以后面几章我会把重点放在「怎么选特征、怎么定 K、怎么让结果稳定」上而不是只贴一段调用kmeans的代码。2. 把图像塞进 K-means 之前特征向量怎么构造才不翻车2.1 为什么直接拿 RGB 三通道做聚类经常出问题RGB 三个通道在数值上是相关的而且欧氏距离在 RGB 空间里和人眼感知的差异并不一致。两个颜色在 RGB 空间距离很近人眼看起来可能差很多反过来人眼觉得相近的两个颜色RGB 距离可能不小。常见做法是转到 Lab 空间L 管亮度a 和 b 管色度欧氏距离更接近感知差异。Matlab 里用rgb2lab一行就能转但要注意输入必须是double类型且归一化到[0,1]否则rgb2lab会按uint8的尺度去解释结果偏得离谱。另一个坑是空间信息。纯颜色聚类会把图像里所有相似颜色的区域归成一类哪怕它们隔得很远。比如蓝天和蓝色招牌会被聚到一起。解决办法是在特征向量里加上归一化后的像素坐标(x, y)构成五维向量[L, a, b, x, y]。但坐标的权重要调坐标权重太大分割结果会变成规整的网格权重太小又退化成纯颜色聚类。我一般先把坐标归一化到[0,1]然后乘一个spatialWeight从 0.3 开始试。2.2 用 Matlab 构造五维特征矩阵的完整步骤下面这段代码把一张 RGB 图像转成 Lab 加空间坐标的五维特征矩阵并做归一化。每一步后面都有说明。% 读取图像并转 double img imread(street.jpg); img im2double(img); % 归一化到 [0,1]避免 rgb2lab 尺度错误 % 转 Lab 色彩空间 lab rgb2lab(img); % 输出 L 在 [0,100]a/b 在 [-128,127] % 获取图像尺寸 [H, W, ~] size(img); % 构造空间坐标网格并归一化到 [0,1] [X, Y] meshgrid(1:W, 1:H); X (X - 1) / (W - 1); Y (Y - 1) / (H - 1); % 把 Lab 三个通道拉成列向量 L reshape(lab(:,:,1), [], 1); A reshape(lab(:,:,2), [], 1); B reshape(lab(:,:,3), [], 1); % 空间坐标也拉成列向量 Xc reshape(X, [], 1); Yc reshape(Y, [], 1); % 空间权重控制坐标对聚类的影响 spatialWeight 0.5; % 拼接成 N x 5 的特征矩阵 features [L, A, B, spatialWeight * Xc, spatialWeight * Yc]; % 对每一维做 z-score 归一化让各维量纲一致 features (features - mean(features)) ./ std(features);逻辑说明im2double把uint8转成[0,1]的double这是rgb2lab正确工作的前提。meshgrid生成每个像素的列号和行号归一化到[0,1]是为了让空间坐标和 Lab 数值在同一个量级附近。spatialWeight是唯一需要手动调的参数它决定空间邻近性对聚类结果的影响程度。最后一步 z-score 归一化很关键Lab 的 L 通道范围是[0,100]a/b 是[-128,127]如果不归一化L 通道会主导距离计算a/b 几乎不起作用。参数说明spatialWeight建议从 0.3 到 0.8 之间试图像纹理越细、区域越碎权重越小图像区域越大、越连续权重可以适当加大。features矩阵的行数是H*W列数是 5如果图像是 1000x1000这个矩阵就是 100 万行 5 列kmeans跑起来内存和速度都要留意。2.3 超像素预处理把百万像素降到几千个块直接对 100 万像素跑 K-means即使 Matlab 的kmeans用了 k-means 初始化迭代几十轮也要等一会儿。更实际的做法是先做超像素分割比如用superpixels函数把图像切成 2000 到 5000 个超像素块然后对每个块取平均 Lab 和块中心坐标作为特征再聚类。这样特征矩阵从百万行降到几千行速度提升两个数量级而且分割结果在空间上更连续不会出现孤立的噪点。% 用 SLIC 超像素分割目标块数 3000 numSuperpixels 3000; [labels_sp, numActual] superpixels(img, numSuperpixels); % 计算每个超像素的平均 Lab 和中心坐标 idx label2idx(labels_sp); featSp zeros(numActual, 5); for k 1:numActual pixelIdx idx{k}; featSp(k, 1) mean(L(pixelIdx)); featSp(k, 2) mean(A(pixelIdx)); featSp(k, 3) mean(B(pixelIdx)); [rows, cols] ind2sub([H, W], pixelIdx); featSp(k, 4) mean(cols) / W; featSp(k, 5) mean(rows) / H; end % 同样做 z-score 归一化 featSp (featSp - mean(featSp)) ./ std(featSp);逻辑说明superpixels返回的labels_sp是每个像素所属的超像素编号label2idx把编号转成像素索引列表。循环里对每个超像素取 Lab 均值坐标取块内像素行列号的均值再归一化。这样每个超像素就是一个样本点后续kmeans的输入从百万级降到几千级。参数说明numSuperpixels设 3000 是个折中设太小比如 500区域边界会很粗糙设太大比如 10000又接近逐像素聚类失去加速意义。superpixels的Compactness参数默认是 10调大让超像素更规整调小让边界更贴合图像边缘做分割一般保持默认或稍微调小到 5 到 8。3. K 值怎么定肘部法、轮廓系数和实际图像的三方拉扯3.1 肘部法在图像分割里的真实表现肘部法算的是不同 K 值下的簇内平方和WCSS然后找曲线拐点。Matlab 里可以循环调用kmeans并取sumd输出。K_range 2:10; wcss zeros(size(K_range)); for i 1:length(K_range) K K_range(i); [~, ~, sumd] kmeans(featSp, K, Replicates, 3); wcss(i) sum(sumd); end % 画肘部图 figure; plot(K_range, wcss, -o); xlabel(K); ylabel(WCSS); title(Elbow Method); grid on;逻辑说明sumd是每个簇内点到簇中心距离的平方和sum(sumd)就是总的 WCSS。Replicates设为 3 表示用不同初始中心跑 3 次取最优减少局部最优的影响。参数说明Replicates在图像数据上建议至少 3如果特征维度高或数据量大可以加到 5。但肘部法在图像上经常不给面子——曲线可能平滑下降没有明显拐点。这时候不要硬找拐点结合轮廓系数一起看。3.2 轮廓系数怎么算、怎么读轮廓系数衡量每个点与自身簇的紧密度和与最近邻簇的分离度范围[-1,1]越大越好。Matlab 没有内置的silhouette对kmeans输出的直接封装但可以手算也可以用evalclusters函数。% 用 evalclusters 自动算多个 K 的轮廓系数 eva evalclusters(featSp, kmeans, silhouette, KList, 2:10); figure; plot(eva); title(Silhouette Score vs K);逻辑说明evalclusters会内部调用kmeans并计算每个 K 的轮廓系数返回的eva对象可以直接plot。OptimalK属性给出推荐值。参数说明轮廓系数在超像素特征上通常比逐像素更稳定因为超像素本身已经平滑了噪声。但要注意如果图像里有一块区域特别大、其他区域很小轮廓系数会偏向大区域推荐值可能偏小。我一般把肘部法和轮廓系数都跑一遍如果两者推荐一致就用不一致就取轮廓系数推荐值加 1 到 2因为图像分割里稍微多分几类通常比少分更安全。3.3 实际图像里 K 值的经验范围做了几十张不同场景的图之后我总结了一个粗略的经验自然场景天空、地面、建筑、植被通常 4 到 6 类够用遥感影像地物分类 5 到 8 类工业质检里缺陷和背景分离 2 到 3 类就够医学图像比如 MRI 脑组织分割灰质、白质、脑脊液加背景4 类左右。如果 K 超过 10聚类结果往往开始把同一块区域打碎看起来像过分割。这时候要么降 K要么检查特征里是不是混入了噪声维度。提示K 值不是越多越好。每多一类就多一个需要解释的区域后续做区域合并或分类时工作量翻倍。4. 避坑与排查K-means 图像分割最常见的 5 个翻车现场4.1 现象分割结果每次跑都不一样原因kmeans默认只跑一次初始化初始中心随机选容易落到局部最优。图像数据里如果某类像素特别多随机初始化很可能把多个中心都扔到那一类里。解决把Replicates设到 3 以上并且用Start参数指定plus做 k-means 初始化。Matlab 的kmeans默认Start是plus但Replicates默认是 1所以关键是加Replicates。[labels, centers] kmeans(featSp, K, Replicates, 5, Start, plus);4.2 现象分割结果里出现大量孤立小区域原因纯颜色聚类没有空间约束颜色相近但位置远的像素被聚到一起或者噪声像素被单独聚成一小类。解决在特征里加空间坐标第 2 章的做法或者聚类后做形态学后处理用bwareaopen去掉小于一定面积的区域再用imfill填洞。% 假设 labels 是逐像素的聚类标签先转成二值图处理某一类 mask (labels 1); mask bwareaopen(mask, 50); % 去掉面积小于 50 像素的连通域 mask imfill(mask, holes);4.3 现象Lab 转换后结果全黑或全白原因rgb2lab要求输入是double且范围[0,1]如果直接传uint8的imread结果Matlab 会按uint8解释L 通道可能全是 0 或 100。解决img im2double(imread(xxx.jpg));然后再rgb2lab。如果图像本身是uint16先im2double再检查范围。4.4 现象K-means 跑得特别慢内存爆掉原因逐像素特征矩阵太大比如 4000x3000 的图像有 1200 万行kmeans每次迭代都要算距离矩阵内存和时间都吃不消。解决用超像素预处理第 2.3 节把样本数降到几千。如果必须逐像素可以先用imresize把图像缩小到长边 500 像素左右聚类完再把标签上采样回原尺寸。% 缩小图像加速聚类 scale 0.25; imgSmall imresize(img, scale); % ... 对 imgSmall 做特征构造和聚类 ... % 把标签上采样回原尺寸 labelsFull imresize(labelsSmall, [H, W], nearest);4.5 现象分割边界锯齿严重区域边缘不贴合原因逐像素聚类没有考虑边缘信息超像素的Compactness参数太大也会让超像素边界过于规整。解决调小superpixels的Compactness让超像素边界更贴合图像梯度。或者聚类后用引导滤波imguidedfilter对标签图做平滑但要注意引导滤波对标签图这种离散值效果有限更稳妥的是用imsegfmm或图割做后处理。注意后处理能改善视觉效果但不会改变聚类本身的错误。如果某一类从根上就分错了后处理只是把错误区域修得好看一点。5. 从聚类标签到可用结果可视化、评估和一个加速技巧5.1 把标签图变成人眼能看懂的分割图kmeans输出的labels是每个超像素的类别编号要变成彩色分割图需要给每个类分配一个颜色然后填回像素。% 给每个类分配颜色 colors lines(K); % 生成 K 种区分度较高的颜色 % 把超像素标签映射到逐像素标签 labelsPixel zeros(H, W); for k 1:numActual labelsPixel(idx{k}) labels(k); end % 生成彩色分割图 segImg zeros(H, W, 3); for k 1:K mask (labelsPixel k); for c 1:3 channel segImg(:,:,c); channel(mask) colors(k, c); segImg(:,:,c) channel; end end % 叠加原图看边界 figure; imshow(img); hold on; h imshow(segImg); set(h, AlphaData, 0.4); % 半透明叠加 title(Segmentation Overlay);逻辑说明lines(K)生成 K 种颜色循环把每个超像素的标签填回像素级labelsPixel再按类上色。最后用AlphaData做半透明叠加方便看分割边界和原图的对应关系。参数说明AlphaData设 0.3 到 0.5 之间比较合适太低看不清分割区域太高盖住原图细节。5.2 没有标注数据时怎么判断分割好坏图像分割的评估通常需要 ground truth但实际项目里往往没有。这时候可以用几个间接指标一是区域内部方差好的分割应该让每个区域内部的颜色方差尽量小二是区域间边界梯度分割边界应该落在原图梯度大的地方。Matlab 里可以算每个类的 Lab 方差和边界处的平均梯度。% 算每个类的内部方差 for k 1:K mask (labelsPixel k); varL var(L(mask)); varA var(A(mask)); varB var(B(mask)); fprintf(Class %d: varL%.2f, varA%.2f, varB%.2f\n, k, varL, varA, varB); end如果某一类的方差明显大于其他类说明这一类里混了不同颜色的区域可能需要增加 K 或者调整特征权重。5.3 一个让 K-means 快 3 倍的技巧先用小样本估中心如果数据量实在大又不想做超像素可以先用随机采样的 10% 像素跑一次kmeans得到中心再把这个中心作为Start传给全量数据的kmeans。这样全量数据只需要少量迭代就能收敛。% 随机采样 10% 的样本 n size(featSp, 1); sampleIdx randperm(n, round(0.1 * n)); featSample featSp(sampleIdx, :); % 在小样本上跑 kmeans 得到初始中心 [~, centersInit] kmeans(featSample, K, Replicates, 3); % 用这些中心作为全量数据的起点 [labels, centers] kmeans(featSp, K, Start, centersInit);逻辑说明小样本上的聚类结果虽然不精确但中心位置已经接近最终解全量数据从这些中心出发迭代次数大幅减少。Replicates在小样本上可以设 3因为小样本跑得快。参数说明采样比例 10% 是个经验值数据量越大可以采得越少但不要低于 5%否则中心估计偏差太大。如果图像内容特别复杂采样比例提到 20%。我自己的习惯是拿到一张新图先缩到长边 400 像素跑一遍看 K 值和特征权重确认分割区域合理了再上全尺寸加超像素跑最终结果。这样比一上来就全量跑省时间也少了很多「跑完发现参数不对重来」的后悔药时刻。希望帮到你。本文还有配套的精品资源点击获取
返回列表