简介:基于MATLAB实现的局部模糊c均值聚类(FLICM)代码包,面向图像分割、聚类分析领域的研究生、科研人员及工程开发者,用于解决传统FCM算法对噪声敏感、分割不稳定的问题。压缩包共7个文件、容量87KB,包含2个MATLAB脚本(算法实现与测试)、1个C辅助源文件、2张脑部MRI测试图像(原始图与加噪图)以及TXT/Markdown说明文档,结构简洁,功能层次分明,便于对照代码理解局部空间信息引入聚类的实现细节并调整参数。已有110人浏览学习,代码在MATLAB 2020b下验证可运行,可直接复现分割结果,替换自己的图像数据同样适用。该实现通过局部空间信息约束聚类过程,可有效提升含噪图像的分割鲁棒性,适合作为医学影像分割实验的对比基线、深度学习预处理步骤或课堂演示项目。
1. 局部模糊 c 均值:图像分割场景下比 K-means 更稳的模糊聚类方案
在 MATLAB 里做图像分割,很多人第一步会用 kmeans 聚类算法,灰度图上看着效果还行,一旦换成带噪声的工业图、细胞荧光图,分割结果立刻冒出一堆孤立碎块。局部模糊 c 均值聚类算法(Local Fuzzy C-Means,LFCM)和普通模糊 c 均值的关键差别,是每一轮迭代不只看像素自身的灰度,还要把它所在邻域窗口内的局部统计量一起带进隶属度计算,让分割结果在空间上连续、对噪声更钝感。这个改动听起来不大,但目标函数、更新公式、参数调节和 MATLAB 矩阵化写法全都要跟着调整。下面直接按「建模—实现—调参—打包」这条线往下走,新手能照着跑通,老手可以重点看局部项权重和邻域窗口的边界条件。
2. 局部模糊 c 均值的数学模型与隶属度更新公式
2.1 从硬聚类到模糊隶属度:K-means 在图像噪声面前的短板
K-means 对每个样本只给一个硬标签,聚类结果完全由最近中心决定。这个特性在表格数据上问题不大,在图像数据上就很吃亏:噪声像素和周围像素灰度差异大,硬分类会把这种差异放大成一个独立的小簇,于是分割图出现大量椒盐状碎点。MATLAB 里跑一遍idx = kmeans(X(:), C)很快,但这类结果放到后续定量分析里基本不可用。
模糊 c 均值把硬标签换成隶属度u_ik,取值范围在 0 到 1 之间,并对每个像素保持归一化约束Σ_k u_ik = 1。一个像素不再被强行分给某个簇,而是同时持有多个簇的归属程度,目标函数写成加权距离和:
J = Σ_i Σ_k u_ik^m * ||x_i - v_k||^2其中m > 1是模糊指数,v_k是第 k 个聚类中心。这个目标函数比 K-means 平滑,但仍只看像素自身灰度。一个孤立噪声点的x_i离所有中心都远,隶属度会被拉得很散,分割出来照样是碎块。局部模糊 c 均值要解决的,就是把这个「只看自己」的缺陷补上。
2.2 局部约束怎么进目标函数:加一个邻域距离项
要让分割结果在空间上连续,最直接的想法是让像素在聚类时也参考邻域信息。常见做法有两类:一类是先把图像做局部均值平滑,再把平滑后的灰度喂给普通 FCM,等于在预处理阶段滤波;另一类是把局部信息以正则项的形式直接写进目标函数。前者的缺点是边缘会被一并抹掉,后者保留更多细节,也更接近「局部模糊 c 均值聚类算法」这个名称的通常含义。
我一般在目标函数里加一个带权重λ的邻域距离项:
J = Σ_i Σ_k u_ik^m * ||x_i - v_k||^2 + λ Σ_i Σ_k u_ik^m * ||x̄_i - v_k||^2x̄_i是像素 i 在邻域窗口内的灰度均值,λ ≥ 0控制局部约束的强度。这个式子可以这样理解:一个像素既要离聚类中心近,它所在邻域的代表值也要离中心近。λ = 0时退化为经典 FCM;λ越大,分割结果越偏向邻域一致性,边缘保留能力随之下降。各符号的维度关系如下:
| 符号 | 含义 | 维度 |
|---|---|---|
x_i | 像素 i 的特征(灰度值或颜色向量) | N×1 或 N×D |
x̄_i | 像素 i 的邻域局部均值 | N×1 或 N×D |
u_ik | 像素 i 属于簇 k 的隶属度 | N×C |
v_k | 第 k 个聚类中心 | C×1 或 C×D |
m | 模糊指数,控制隶属度平滑程度 | 标量 |
λ | 局部项权重 | 标量 |
N | 像素总数 | 标量 |
C | 聚类簇数 | 标量 |
这个建模方式的好处是解析解容易推,MATLAB 实现不依赖额外工具箱,调λ一个数就能控制局部约束强度。
2.3 聚类中心与隶属度的迭代推导
目标函数带约束Σ_k u_ik = 1,用拉格朗日乘子法对v_k和u_ik分别求导。先对v_k求偏导并令其为零:
v_k = Σ_i u_ik^m * (x_i + λ * x̄_i) / ((1 + λ) * Σ_i u_ik^m)再对u_ik求偏导,结合归一化约束得:
u_ik = (d_ik + λ * d̄_ik)^(-1/(m-1)) / Σ_j (d_ij + λ * d̄_ij)^(-1/(m-1))其中d_ik = ||x_i - v_k||²,d̄_ik = ||x̄_i - v_k||²。两个公式交替迭代:先用当前隶属度更新中心,再用新中心更新距离和隶属度,直到隶属度变化小于阈值。这里有一个实现上必须处理的细节:m接近 1 时指数-1/(m-1)趋于无穷,隶属度会变成 one-hot,和硬聚类没区别;m太大则所有隶属度趋于均匀,分割失去意义。m = 2是最常用的取值,后面调参部分再展开。
3. MATLAB 实现局部模糊 c 均值聚类:矩阵化代码与主函数
3.1 预处理:灰度归一化、局部均值与颜色空间选择
局部模糊 c 均值的第一步是把图像转成特征矩阵。灰度图直接im2double并归一化到 0 到 1,彩色图有两种常见处理:转 LAB 色域后只取 L 通道,或者把 RGB 三通道并列成 N×3 的特征矩阵。LAB 的优势是亮度与颜色解耦,对光照不均更稳;RGB 的好处是代码不用分通道处理。如果图像本身亮度分布不均,可以先用localcontrast或在预处理里做亮度平衡,否则λ项会把亮度梯度当成真实边缘。
局部均值x̄_i的计算用卷积一次完成,不要写双重 for 循环:
img = im2double(imread('cameraman.tif')); r = 1; % 邻域半径,3x3 窗口 k = ones(2*r+1) / (2*r+1)^2; % 归一化卷积核 Xbar = conv2(img, k, 'same'); % 局部均值图像conv2的'same'返回与输入同尺寸的结果;边界像素按零填充处理,对图像边缘会有轻微衰减。条件允许时也可以在预处理阶段对边界做replicate填充,避免局部均值在四角偏低。
3.2 隶属度矩阵初始化与主迭代骨架
初始化只要保证每行和为 1,并且不出现全零列即可:
U = rand(N, C) + 0.05; % 加小偏移防止某列为全零 U = U ./ sum(U, 2); % 每行归一化,满足 Sigma_k u_ik = 1主迭代里最需要注意的是用矩阵运算替代循环。MATLAB R2016b 之后支持隐式扩展,X - V.'可以直接把 N×1 的向量和 1×C 的向量扩展成 N×C 的距离矩阵,不再需要bsxfun。各矩阵的维度关系如下:
| 变量 | 维度 | 作用 |
|---|---|---|
X | N×1 | 像素灰度矩阵 |
Xbar | N×1 | 局部均值矩阵 |
U | N×C | 隶属度矩阵,每行和为 1 |
V | C×1 | 聚类中心 |
D | N×C | 像素到中心的欧氏距离平方 |
W | N×C | 局部加权后的综合距离 |
3.3 主函数与 demo 脚本
完整的实现可以收敛到一个函数里。输入输出结构按「先参数、后矩阵」组织,方便别人一眼看懂。
function [U, V, labels, hist_J] = lfcm_segmentation(img, C, m, lambda, win, tol, maxiter) % LFCM_SEGMENTATION 基于局部模糊 c 均值的图像分割 % 输入: % img - 灰度图像,double 型,取值 [0,1] % C - 聚类簇数 % m - 模糊指数,常用 2 % lambda - 局部项权重,0 时退化为 FCM % win - 邻域窗口,如 [3 3] % tol - 隶属度最大变化阈值,默认 1e-4 % maxiter - 最大迭代次数,默认 100 % 输出: % U - N*C 隶属度矩阵 % V - C*1 聚类中心 % labels - N*1 硬标签(取最大隶属度) % hist_J - 每轮目标函数值 if nargin < 7, maxiter = 100; end if nargin < 6, tol = 1e-4; end if nargin < 5, win = [3 3]; end if nargin < 4, lambda = 0.6; end if nargin < 3, m = 2; end [H, W] = size(img); X = img(:); % N*1,N 为像素总数 N = numel(X); X = X ./ max(X(:)); % 全局归一化 % 邻域均值,统一用 2D 卷积实现 r = floor(win(1) / 2); k = ones(2*r+1) / (2*r+1)^2; Xbar = conv2(reshape(X, H, W), k, 'same'); Xbar = Xbar(:); % 随机初始化隶属度矩阵 U = rand(N, C) + 0.05; U = U ./ sum(U, 2); V = zeros(C, 1); hist_J = zeros(maxiter, 1); for it = 1:maxiter % 1) 更新聚类中心,公式见 2.3 Um = U .^ m; V = (Um' * (X + lambda * Xbar)) ./ ((1 + lambda) * sum(Um, 1)'); % 2) 计算加权距离矩阵 D = (X - V') .^ 2; % N*C,隐式扩展 Dbar = (Xbar - V') .^ 2; W = D + lambda * Dbar; % 3) 更新隶属度,加 eps 防止除零 tmp = W .^ (-1 / (m - 1)); Unew = tmp ./ sum(tmp, 2); % 4) 记录目标函数,按像素数归一化便于观察收敛 hist_J(it) = sum(Um .* W, 'all') / N; % 5) 收敛判断 if norm(Unew - U, Inf) < tol U = Unew; hist_J = hist_J(1:it); break; end U = Unew; end [~, labels] = max(U, [], 2); end关于代码有几点说明。V = (Um' * (X + lambda * Xbar)) ./ ((1 + lambda) * sum(Um, 1)')里的分母维度是 C×1,与分子的 C×1 逐元素相除,正好对应v_k的更新公式。W .^ (-1/(m-1))对整张距离矩阵统一做幂运算,比在两层 for 循环里逐像素更新快一个量级。sum(tmp, 2)是对同一像素的所有簇求和,保证u_ik的归一化约束每轮都不被破坏。像素数多时W是 N×C 的稠密矩阵,内存占用的量级相当于几张原图,普通 500 万像素以内图像不需要特殊处理。
配合一个简单的 demo 脚本能快速看到效果:
%% demo_run_lfcm.m clear; clc; close all; img = im2double(imread('cameraman.tif')); rng(1); img_n = max(min(img + 0.05 * randn(size(img)), 1), 0); [C, m, lambda, win, tol, maxiter] = deal(3, 2, 0.6, [3 3], 1e-4, 60); [U, V, labels, hist_J] = lfcm_segmentation(img_n, C, m, lambda, win, tol, maxiter); labels_img = reshape(labels, size(img)); figure; subplot(1, 2, 1); imshow(img_n); title('Noisy Image'); subplot(1, 2, 2); imagesc(labels_img); colormap(parula(C)); axis image; title('LFCM Result'); figure; plot(hist_J, '-o'); xlabel('Iteration'); ylabel('Objective');3.4 查看收敛:目标函数画图与硬标签生成
收敛情况不能只看分割图,目标函数曲线的形状更说明问题。正常迭代下hist_J前几轮快速下降,后面进入平缓区;如果曲线反复震荡,先怀疑m是否接近 1,再看lambda是否过大导致两个距离项互相拉扯。聚类结果最终通过max(U, [], 2)取每个像素隶属度最大的簇作为硬标签,这个操作放在循环外,不参与迭代收敛判据,避免人为提前终止迭代。
4. 局部模糊 c 均值聚类算法的参数设置与调参实践
4.1 需要暴露的参数清单
局部模糊 c 均值的参数比普通 FCM 多一个λ,调参顺序应该固定在「先定 C 和 m,再调 λ,最后缩窗口」上。各项参数的推荐范围如下:
| 参数 | 推荐范围 | 过大/过小的影响 |
|---|---|---|
C簇数 | 2~10 | 过大产生过分割,过小合并本应分开的区域 |
m模糊指数 | 1.5~2.5,常用 2 | 接近 1 退化为硬聚类;过大则隶属度过于平均 |
λ局部权重 | 0.2~0.8 | 过小失去局部约束;过大抹掉边缘细节 |
r邻域半径 | 1~2(对应 3x3、5x5) | 噪声大取 2;窗口过大细节丢失 |
maxiter | 60~150 | 过小未收敛;过大浪费算力 |
tol | 1e-4 ~ 1e-5 | 1e-5 通常足够精确 |
4.2 模糊指数 m:取 2 的默认与退化边界
m是模糊 c 均值里最容易被忽视的参数。m = 1时目标函数退化为硬 c 均值,代码里的-1/(m-1)直接除零,这也是新手最常遇到的 NaN 来源之一。m = 2是文献和工程中的默认值,此时隶属度与平方距离成反比,几何意义直观。如果觉得分割结果太平滑,可以把m降到 1.5 到 1.7,让靠近中心的像素归属更明确;相反如果结果碎块多,说明模糊程度不够,把m调到 2.2 到 2.5。可以用一个小脚本快速对比:
m_list = [1.5 1.8 2.0 2.5]; for i = 1:numel(m_list) [~, ~, labels_i, hist_J_i] = lfcm_segmentation(img_n, 3, m_list(i), 0.6, [3 3], 1e-4, 60); fprintf('m=%.1f, final J=%.4f\n', m_list(i), hist_J_i(end)); end只看hist_J的终值不够,还要注意曲线是否单调下降。m设置不合适时目标函数会在某个区间抖动,这就是退化的信号。
4.3 局部权重 λ 与邻域半径 r:噪声强度决定窗口
λ控制的是「邻域说话的分量」。我的经验是从0.6起步,先跑一遍看分割图;碎块仍然多就把λ加到 0.8,边缘被过度平滑就降到 0.3 左右。λ = 0时算法就是 FCM,可以用来做对照实验,确认局部项带来的增益到底有多大。r决定局部均值的计算范围,3x3 窗口适合轻度噪声,5x5 窗口对中高强度噪声更稳,但边缘会明显变粗。亮度不均的图像在调λ前先做亮度平衡,否则λ会把阴影过渡区误判为类别边界。
4.4 初始化方式与多起点策略
局部模糊 c 均值的目标函数是非凸的,随机初始化容易落入局部极小值。我一般会做多起点初始化:随机跑 5 次,取最终目标函数最小的一次作为结果。也可以用 K-means 的中心做初始化,让算法从更合理的起点出发,但这样做的代价是初始化本身也要时间。如果C不确定,先用evalclusters跑一遍 K-means 看轮廓系数,再在 LFCM 上用固定C精调。多起点脚本如下:
best_J = inf; for trial = 1:5 [U, V, labels, hist_J] = lfcm_segmentation(img_n, 3, 2, 0.6, [3 3], 1e-4, 60); if hist_J(end) < best_J best_J = hist_J(end); best_labels = labels; end end5. 把 MATLAB 代码和说明文档打包成可复用的分割项目
5.1 说明文档应该写哪几块
一个 zip 包里的使用说明文档,核心价值是让拿到文件的人在三分钟内跑通。按 MATLAB 教程里项目文档的常见结构,建议按这个顺序组织:
1. 算法简介(两段话,说明与 FCM 的区别) 2. 运行环境(MATLAB R2016b 以上,无需额外工具箱) 3. 文件清单(主函数、demo 脚本、说明文档) 4. 快速开始(demo_run_lfcm.m 的直接运行方式) 5. 参数说明表(与第 4 章表格一致) 6. 输出文件说明(U、labels、hist_J 的含义) 7. 常见问题(NaN、全图一簇、收敛慢)快速开始要放在参数说明前面,因为大多数人拿到压缩包的第一反应是找能不能直接跑的脚本,而不是先读公式。常见问题里建议把「全图只有一个簇」解释清楚:通常不是代码 bug,而是C=1或m过大导致隶属度趋同。
5.2 定量验证:用 Dice 和 IoU 而不是只看分割图
分割图好看不等于算法正确,如果手里有标注好的金标准,应该补充一个定量评价。IoU 的 MATLAB 实现很短:
function iou = calc_iou(A, B, k) % 计算第 k 类的 IoU,A/B 为标签图 A = (A == k); B = (B == k); iou = sum(A(:) & B(:)) / (sum(A(:) | B(:)) + eps); end对每一类算完 IoU 后再平均,就得到 mIoU。分割结果里如果残留少量小碎块,可以在后处理用形态学开闭运算去除,常见的做法是对标签图做imclose:
se = strel('disk', 2); labels_clean = imclose(labels_img, se);开闭运算的半径按目标尺寸的 1/10 左右设置即可,半径过大会把细长结构一并吞掉。至此,从目标函数、MATLAB 实现到参数调优和结果验证就形成了一条完整的闭环,后续要接入批量处理或多通道特征时,只需要把lfcm_segmentation的输入从灰度向量换成 N×D 的特征矩阵即可。
本文还有配套的精品资源,点击获取