做图像分割时,灰度不均匀一直是让人头疼的问题。手头有一张模拟医学图像,背景亮度从左到右逐渐变化,目标区域和背景的灰度范围大面积重叠,用最经典的Chan-Vese这类全局活动轮廓模型去跑,结果常常是曲线在某个局部边界绕不进去,或者直接把整片暗区当成了目标。最近我把LGDF(Local Gaussian Distribution Fitting,局部高斯分布拟合能量驱动活动轮廓)这套基于区域的变分水平集模型从头到尾重新整理了一遍,从能量构建到MATLAB实现都做了完整复现。这篇文章就把整个过程的逻辑和踩坑点写出来,适合正在做图像分割、尤其是需要处理灰度不均匀或弱边界图像的研究者和工程师参考。
1. 灰度不均匀图像为什么难分割:从全局模型说起
1.1 全局统计模型的局限
图像分割里的活动轮廓模型大致分两类:基于边缘的和基于区域的。基于边缘的模型靠图像梯度驱动,曲线被拉向强的边缘,可一旦边缘不明显或者被噪声破坏,曲线就会穿透边界继续跑。基于区域的模型不看边缘,而是假设目标区域和背景区域满足某种灰度统计规律,通过拟合统计量来分割。Chan-Vese模型是其中最出名的一个,它假设图像被曲线划分成的内部和外部各自灰度近似均匀,通过最小化内部与外部灰度均值的偏差来完成分割。
这个假设在脑部MRI、眼底血管、工业零件这类图像上会遇到一个非常现实的问题:图像灰度并不是均匀的。光照变化、组织噪声、传感器响应都会让同一类区域的亮度缓慢漂移,甚至出现明显梯度。全局的灰度均值在统计上会把这种局部变化平均掉,结果内部均值可能落在背景和目标的灰度重叠区间里,分割边界变得极其不稳定。我见过不少同学用CV模型跑合成渐变背景图,目标明明是一个很规则的圆,分割结果却是一块不规则的月牙,原因就是全局统计把局部灰度变化当成了类别差异。
真正需要的是一个能感知局部灰度波动的模型。也就是说,在曲线附近的每个小邻域内,我们假定目标和背景都服从某种分布,并且这个分布的参数会随着位置变化而变化。这样在图像左半部分暗、右半部分亮的时候,模型依然能在每个局部范围内区分目标和背景,而不是被整体统计带偏。
1.2 LGDF模型的思路:把高斯拟合放到局部
LGDF模型做的事情其实很直观:在图像上取一个像素点,围着一个以它为中心的局部窗口,假设窗口内落在曲线内部的灰度服从一个高斯分布,落在曲线外部的灰度服从另一个高斯分布。这两个高斯分布的均值和方差都不固定,会随着窗口中心位置的移动而变化。于是整个分割问题变成了:找一条曲线,使得在每一个局部窗口内,曲线内外区域的灰度都能被各自的高斯分布最好地解释。
这里的“高斯分布”不是简单套一个统计公式。LGDF用了局部高斯拟合来独立估计每个点的均值和方差,并且通过变分水平集方法求解,把曲线演化问题转化成一个偏微分方程的迭代求解问题。相比Chan-Vese,它相当于把“全局两个高斯”换成了“每个像素邻域两个高斯”。代价是计算量变大,收益则是能够处理灰度不均匀、光照渐变、背景纹理复杂等场景。
在实际使用中,LGDF对初始化的敏感度比全局模型略高,但只要能大致把目标包住或者放在目标内部,大多数情况下都能收敛到合理边界。这一点对工程实践很重要:算法不是玩具,要能在不太完美的初始化条件下稳定出结果。
2. 能量函数设计:局部高斯拟合项、正则项与权重的配合
2.1 数据项:内外区域的拟合误差怎么算
要把LGDF讲清楚,能量函数是绕不开的。假设图像为 (I(x)),(x) 表示像素坐标。曲线把图像域 (\Omega) 分成曲线内部和外部。对任意一个像素位置 (y),我们考察以 (y) 为中心、以高斯核 (K_\rho(y-x)) 加权的邻域。如果用 (u_1(y)) 和 (u_2(y)) 表示曲线内外在 y 邻域中的局部灰度均值,(\sigma_1^2(y)) 和 (\sigma_2^2(y)) 表示局部方差,则数据项可以写成:
[ E_{\text{data}} = \int_\Omega \sum_{i=1}^2 \left[ \int_\Omega K_\rho(y-x) \left( \log \sigma_i(y) + \frac{(I(x) - u_i(y))^2}{2\sigma_i^2(y)} \right) dy \right] M_i(\phi(x)) dx ]
其中 (M_1(\phi(x)) = H(\phi(x))) 表示曲线内部的指示函数,(M_2(\phi(x)) = 1 - H(\phi(x))) 表示曲线外部的指示函数,(H) 是Heaviside阶跃函数。高斯核的作用是限制局部范围,(\rho) 控制局部窗口的大小。
这个公式看起来复杂,理解起来却很简单:对数项相当于对局部方差取对数惩罚,二次项相当于标准高斯分布的负对数似然。曲线的内部被高斯 (N(u_1, \sigma_1^2)) 拟合,外部被高斯 (N(u_2, \sigma_2^2)) 拟合。拟合得越差,能量越高;曲线往正确边界移动时,能量会下降。关键在于,这里的均值和方差都随位置 y 变化,因此能表达空间变化的灰度分布。
2.2 正则项:长度约束和水平集平滑
只靠数据项是不够的。如果没有正则约束,曲线会倾向于在图像噪声上钻出无数小突起,把每个孤立噪点都当成一个目标。LGDF通常在数据项后加上长度正则项:
[ E_{\text{reg}} = \mu \int_\Omega \left| \nabla H(\phi(x)) \right| dx ]
这一项等价于对曲线周长做正则。(\mu) 越大,分割边界越光滑,但太大的 (\mu) 会让曲线无法贴合细节边界;太小则边界会有毛刺和空洞。工程上一般先固定 (\mu) 在一个中等值跑一版,看边界光滑度和细节保留是否平衡,再微调。
还有一个可选项是面积正则项 (\nu \int_\Omega H(\phi(x)) dx)。它的作用是给曲线内部加上面积惩罚或者奖励驱动:(\nu) 为正时抑制内部区域扩大,为负时加速内部区域扩大。有些场景初始化曲线完全在目标外部时,可以把 (\nu) 设成负值,让曲线更快收缩进去。
2.3 三个关键假设与适用边界
LGDF能处理灰度不均匀,不代表它万能。它有几个隐含假设,使用时必须心里有数。
第一个假设是局部高斯性。每个小邻域内的灰度分布近似高斯。自然图像中许多纹理区域并不满足,例如常见纹理可能有多个峰,但多数医学图像、工业成像在一定窗口内基本符合高斯假设。第二个假设是窗口尺度 (\rho) 要与灰度不均匀的变化尺度匹配。如果 (\rho) 太小,窗口内样本不足,均值和方差的估计噪声很大;如果 (\rho) 太大,又会退化成全局模型,失去处理不均匀性的能力。第三个假设是图像由两个区域构成。LGDF最原始的形式是二相分割,多相需要引入多个水平集函数或耦合模型。
我自己的经验是,如果图像中灰度不均匀的尺度大概是十几到几十个像素宽,那么核半径取5到10个像素就能获得不错结果。如果图像有强纹理或严重噪声,则需要先用适当的平滑预处理,否则方差的估计会被噪声拉高。
3. 变分推导与数值实现:从能量到水平集演化
3.1 欧拉-拉格朗日方程与演化方向
把能量函数写成水平集形式后,最小化能量通常采用变分法。固定局部均值 (u_i) 和方差 (\sigma_i^2),对水平集函数 (\phi) 求一阶变分,可以得到梯度下降演化方程。标准LGDF论文给出的结果可以整理为:
[ \frac{\partial \phi}{\partial t} = \delta_\epsilon(\phi) (e_2 - e_1) + \mu , \text{div} \left( \frac{\nabla \phi}{|\nabla \phi|} \right) ]
其中 (\delta_\epsilon(\phi)) 是平滑化的Dirac函数,(e_i) 是每个像素上针对曲线内外分布的拟合误差:
[ e_i(x) = \int_\Omega K_\rho(y-x) \left[ \log \sigma_i(y) + \frac{(I(x) - u_i(y))^2}{2\sigma_i^2(y)} \right] dy ]
这里符号需要仔细理解。(e_i(x)) 表示在像素 (x) 处,用曲线内或曲线外的局部高斯模型解释该像素灰度时产生的误差。如果像素 (x) 在曲线外部,但其灰度更适合用 (e_1)(内部模型)解释,则外部拟合误差更大,曲线就会向外扩展,从而把该像素纳入内部。所以方程中 ((e_2 - e_1)) 的符号是合理的:当外部误差大于内部误差时,(\phi) 增加,曲线向外扩张。配合长度正则项的曲率项,曲线演化会平滑地进行。
3.2 局部均值与方差的迭代更新
演化方程中使用的 (u_i) 和 (\sigma_i^2) 并不是常数,需要在每次迭代后重新估计。它们的最小化更新公式是加权平均:
[ u_i(x) = \frac{\int_\Omega K_\rho(y-x) I(y) M_i(\phi(y)) dy}{\int_\Omega K_\rho(y-x) M_i(\phi(y)) dy} ]
[ \sigma_i^2(x) = \frac{\int_\Omega K_\rho(y-x) (I(y) - u_i(x))^2 M_i(\phi(y)) dy}{\int_\Omega K_\rho(y-x) M_i(\phi(y)) dy} ]
这两个公式的物理意义很清晰:在点 (x) 的邻域内,把曲线内部或外部的像素按照高斯核加权,加权平均得到局部均值,加权偏差平方摊销得到局部方差。在MATLAB中,这可以通过目标图像 (I) 与指示函数 (M_i) 的逐点乘积再与高斯核卷积来实现。卷积操作对应公式中的积分,权重就是高斯核。
注意方差更新时要用 (I(y) - u_i(x)),这里的 (u_i(x)) 是当前点 (x) 处的局部均值,不是 (y) 处的均值。有些实现会误用 (u_i(y)),结果会导致方差估计出现空间错位,收敛速度变慢,边界位置也容易偏移。
3.3 离散化细节:Heaviside平滑、曲率和时间步长
实际代码中,水平集函数是二维数组,演化方程是离散偏微分方程。Heaviside函数必须平滑化,常用:
[ H_\epsilon(z) = \frac{1}{2} \left( 1 + \frac{2}{\pi} \arctan\left( \frac{z}{\epsilon} \right) \right) ]
[ \delta_\epsilon(z) = \frac{1}{\pi} \cdot \frac{\epsilon}{z^2 + \epsilon^2} ]
(\epsilon) 一般取1到2。如果取太小,数值不稳定;取太大,边界会变得模糊。
曲率项 (\text{div}(\nabla \phi / |\nabla \phi|)) 需要用中心差分计算。先求 (\phi) 的梯度分量,再归一化,最后对归一化向量求散度。实现时注意分母加一个极小量防止除零。
时间步长 (\Delta t) 是另一个关键参数。显式欧拉迭代下,(\Delta t) 过大会导致演化震荡甚至发散;过小则收敛极慢。我常用的范围是0.1到2。如果图像尺寸是256×256,强度范围在0到255,取 (\Delta t=1) 配合 (\mu) 在5到20之间,能稳定跑完几百次迭代。如果图像强度归一化到0到1,则时间步长和正则系数都要相应缩小很多。
4. MATLAB代码骨架与参数设置
4.1 高斯核与Heaviside的准备
在MATLAB中,生成高斯核最简单的是fspecial('gaussian', hsize, rho),但实际使用中我喜欢自己写一个归一化窗口,确保窗口内和是1。窗口大小一般取2*ceil(3*rho)+1,这样才能覆盖高斯核的主要支撑区域。如果取太小,核被截断得厉害,局部加权窗口相当于丢掉了外围信息。
Heaviside和Dirac函数写成两个匿名函数就很方便:
epsilon = 1.5; H_eps = @(phi) 0.5 * (1 + (2/pi) * atan(phi / epsilon)); d_eps = @(phi) (1/pi) * (epsilon ./ (phi.^2 + epsilon^2));初始化水平集时,我习惯用半径可调的圆形符号距离函数,也可以用矩形。关键是初始曲线要包含一定数量的背景和目标像素,否则第一次均值方差估计可能不准确,后续演化容易跑到错误方向。
4.2 主循环代码骨架
下面是LGDF迭代主循环的核心骨架。这个版本没有加入太重的高级优化,只保留最直接的能量最小化流程,便于逐行理解。
% 输入: I 灰度图像, 归一化到 [0, 255] % 参数 rho = 5; % 局部窗口核尺度 mu = 10; % 长度正则权重 nu = 0; % 面积正则权重, 可不加 dt = 1.0; % 时间步长 iterNum = 500; % 初始化水平集 [m, n] = size(I); [X, Y] = meshgrid(1:n, 1:m); centerX = n/2; centerY = m/2; phi = sqrt((X - centerX).^2 + (Y - centerY).^2) - 40; % 高斯核 hsize = 2 * ceil(3 * rho) + 1; K = fspecial('gaussian', hsize, rho); for iter = 1:iterNum H = H_eps(phi); delta = d_eps(phi); M1 = H; M2 = 1 - H; % 更新局部均值与方差 num1 = imfilter(I .* M1, K, 'replicate'); den1 = imfilter(M1, K, 'replicate'); u1 = num1 ./ (den1 + eps); sigma1 = imfilter((I - u1).^2 .* M1, K, 'replicate') ./ (den1 + eps); num2 = imfilter(I .* M2, K, 'replicate'); den2 = imfilter(M2, K, 'replicate'); u2 = num2 ./ (den2 + eps); sigma2 = imfilter((I - u2).^2 .* M2, K, 'replicate') ./ (den2 + eps); % 计算拟合误差 tmp1 = log(sigma1 + eps) + (I - u1).^2 ./ (2 * (sigma1 + eps)); tmp2 = log(sigma2 + eps) + (I - u2).^2 ./ (2 * (sigma2 + eps)); e1 = imfilter(tmp1, K, 'replicate'); e2 = imfilter(tmp2, K, 'replicate'); % 曲率 [phi_y, phi_x] = gradient(phi); dphi = sqrt(phi_x.^2 + phi_y.^2); nx = phi_x ./ (dphi + eps); ny = phi_y ./ (dphi + eps); [~, nxx] = gradient(nx); [nyy, ~] = gradient(ny); curvature = nxx + nyy; % 演化 dphi = delta .* (e2 - e1) + mu .* curvature + nu .* delta; phi = phi + dt * dphi; % 每隔一定次数重新初始化为符号距离函数 if mod(iter, 100) == 0 phi = sign(phi) .* max(abs(phi) - 1, 0); end end % 提取分割结果 seg = phi < 0;imfilter的'replicate'边界处理很关键。不处理边界的话,图像四周的卷积结果会偏小,导致局部均值方差估计在边界处失真,曲线靠近图像边缘时容易失控。
4.3 几个容易被忽略的实现细节
第一个细节是收敛判断。如果只固定迭代次数,会发现有时早早就稳定了,白烧时间去算几百次空转。可以在主循环里记录能量变化,或者记录水平集函数变化的绝对值,当连续若干次变化小于阈值时停止。能量函数包含 log 项和方差项,计算时要加小常数,否则遇到零方差会得到负无穷。
第二个细节是重新初始化周期。水平集演化久了,梯度会变得过大或过小,导致数值不稳定。每隔50到100步做一次符号距离重初始化,能让曲率和梯度计算更加稳定,但也不能每步都做,太频繁会引入额外误差。
第三个细节是图像预处理。LGDF虽然能容忍灰度不均匀,但极高的椒盐噪声会严重影响局部方差估计。我通常先用中值滤波或高斯滤波做一次轻量平滑,强度以不破坏边界细节为限。这样局部高斯分布拟合会稳定很多。
5. 实验效果、参数调节与典型坑点
5.1 实例一:合成灰度不均匀图像
我构造了一张256×256的合成图,背景灰度从左侧的0到右侧的255线性渐变,中间放一个灰度100左右的圆形目标。目标在左侧时与背景对比明显,在右侧时几乎要被渐变背景吞没。用Chan-Vese硬跑,结果把右侧背景和左侧暗背景割到了一起。换成LGDF后,取 (\rho=5),(\mu=10),初始化一个半径40的圆,500次迭代后目标边界被完整分割出来。这是因为每个局部窗口内,目标与背景的灰度差始终存在,局部统计能抓住这个差异。
这个例子再次说明一个道理:对于灰度不均匀图像,模型能不能成功不取决于整体对比度,而取决于局部对比度。LGDF本质上是把“局部对比度可区分”这一点用高斯拟合的形式显式建模了。
5.2 实例二:弱边界模拟图像
另一个常见头疼点是弱边界。我模拟了一张背景灰度渐变、目标内部灰度也渐变、目标和背景之间只有很窄的过渡带的图像。目标灰度和周围灰度在交界处只差10个灰度级。单纯用局部均值拟合的模型,例如LBF/RSF,在噪声稍微大一点时,曲线容易越过弱边界。LGDF因为额外拟合了局部方差,模型对边界两侧灰度分布差异的敏感度更高,即使均值差异小,如果方差分布不同,也能识别边界。
当然,方差不是越大越好。如果局部窗口内噪声太大,方差会被噪声主导,反而把噪声当成需要分割的区域。所以遇到强噪声时,应该先平滑,或者适当增大 (\rho),让窗口内统计更稳定。
5.3 常见问题排查对照表
在实际运行中,我总结过一张LGDF调参排查对照表,非常实用:
| 现象 | 可能原因 | 调整方式 |
|---|---|---|
| 分割边界一直在原地不动 | 时间步长太小,演化太慢 | 增大 dt,或检查 e1 与 e2 差值是否过小 |
| 曲线反复震荡、边界抖动 | dt 过大或 mu 过小 | 减小 dt,增大 mu |
| 边界过度光滑,细节丢失 | mu 太大,或 rho 太大 | 减小 mu 和 rho |
| 边界出现大量毛刺和孤立点 | mu 太小,或图像噪声大 | 增大 mu,先做轻量平滑 |
| 曲线钻出目标跑到背景中去 | 初始化太靠近边界,或弱边缘处拟合误差接近 | 重新初始化,增大 mu,适当增加 nu 惩罚 |
| 分割结果永远停在局部极小 | 初始化位置严重不合理 | 尝试目标内部和外部分别初始化,或使用多尺度初始化 |
5.4 和LBF等局部均值模型怎么选
很多人在实际项目里会拿LGDF与LBF/RSF做对比。LBF/RSF这类局部二值拟合模型只估计局部均值,认为局部区域灰度能被分段常数近似。LGDF把分段常数扩展成分段高斯,多估计了一个方差。这意味着它适合区分均值相近但纹理或噪声幅度不同的区域。
代价是计算量。LGDF每次迭代需要多次卷积,包括均值分子分母、方差分子分母以及误差项的卷积,一共六个卷积左右。图像大到512×512以上时,速度会明显下降。如果实时性要求高,LBF/RSF更快;如果分割精度和鲁棒性优先,LGDF更值得。我在自己的测试中还会比较初始化敏感性,LGDF通常能容忍更大范围的初始化位置偏移,但前提是局部窗口尺度与目标尺度匹配。
6. 实际使用建议与后续扩展
6.1 LGDF适合解决什么问题
LGDF最有价值的场景是灰度不均匀、弱边界、背景与目标灰度范围重叠的二维图像分割。医学影像中尤其常见,例如脑部MRI的灰度不均匀、眼底血管造影中的光照不均、皮肤镜图像中的渐变阴影等。工业检测中,带曲面反光的产品表面图像,也经常出现目标区域亮度从中心到边缘逐渐衰减的情况,这类图像用二值化或全局分割很难处理。
如果你面对的是光照完全均匀、目标与背景灰度对比强烈的图像,完全可以继续用Chan-Vese这类更简单的模型,没必要上LGDF。算法选型要匹配问题难度,这本身就是经验。
6.2 多水平集与三维扩展
LGDF原始模型是二相分割,但实际需求经常是多区域分割。一种方式是使用多个水平集函数,例如两个水平集可以把图像域分成四个区域,每个区域用独立的局部高斯分布拟合。代价是能量函数和更新方程变得更复杂,滤波次数成倍增加。
三维扩展相对直接:把二维高斯核换成三维高斯核,二维曲率换成一阶/二阶差分公式的三维版本,水平集函数从二维数组变成三维体数据。医学三维图像分割中,LGDF这类局部统计模型在分割脑结构、血管等组织时表现不错,主要难点是内存和计算速度,建议先用二维切面验证参数,再扩展到三维。
6.3 个人经验总结
我用LGDF跑了大量灰度不均匀图像之后,最深的体会是:许多分割的“不收敛”并不是算法的问题,而是没有把局部统计的尺度和图像的灰度不均匀尺度对齐。窗口太大,模型退化成全局统计;窗口太小,统计估计不可靠。拿到一张图,先估计一下灰度波动的空间周期,再定 (\rho),往往能少走很多弯路。
另一个习惯是盯着能量曲线和分割结果同时看。迭代过程中如果能量一直下降但边界没有明显更新,多半是正则项主导;如果边界快速更新但能量变化不大,可能是初始化位置已经很好,但局部细节还在微调。不要盲目追求大迭代次数,500次以内基本能收敛,再往后变化很小。
最后建议读者在动手复现时,先从一个合成渐变背景图开始,把每一步涉及的高斯核、均值和方差可视化出来。看到局部均值图与原始图像的结构对应关系后,对模型的理解会完全不一样。理解到位了,换到真实图像上处理灰度不均匀问题,自然就有底气了。