简介:基于Gompertz分布函数的各向异性扩散滤波Matlab代码包,专为图像去噪与边缘保持场景设计,适用于计算机、电子信息工程、数学等专业学生的课程设计、期末大作业和毕业设计。包内共15个文件,含2个m格式主程序、7个tif与6个png格式案例图像,总大小仅1.56MB,资源轻量但结构完整;源码采用参数化编程,扩散参数可方便调整,注释细致,能适配Matlab 2014、2019a及2024a多个版本。算法将Gompertz生长曲线融入扩散过程,凭借其先快后慢的转变特性,根据图像局部亮度变化自动控制扩散强度,在平滑区域强化去噪、在边缘附近保留细节,比常规各向异性扩散更具适应性与灵活性。目前已有65人学习浏览,随包附带可直接运行的案例数据,便于快速验证效果,也适合在此基础上开展改进实验或作为算法对比基准。
1. 基于Gompertz分布函数的各向异性扩散:一个Matlab滤波包能做什么
做图像去噪时最常撞见的两难是:高斯滤波把噪声磨掉了,边缘和纹理也跟着糊掉。Perona-Malik各向异性扩散能按梯度幅值自动切换平滑强度,但经典的指数式扩散系数在低对比度边缘上表现得过于“果断”,该保的边缘没保住,该平滑的纹理又留下斑点。这个Matlab代码包把扩散系数换成了Gompertz分布函数,用两个形状参数控制衰减曲线,让边缘保持有了更细腻的调节余地。适合正在做非线性扩散实验、复现经典算法对比、或者给毕设找图像增强基线的从业者。解压后跑通demo只需要一条命令,但真正能出好效果,得先把扩散系数、梯度方向、时间步长这三个环节对齐。
2. 从Perona-Malik到Gompertz扩散系数:模型原理与代码结构
2.1 PM模型的离散形式:先看懂每个变量在图像上的含义
各向异性扩散的核心方程是:
∂I/∂t = div(c(|∇I|) · ∇I)
其中I是图像灰度,∇I是梯度,c(s)是边缘停止函数。物理含义很直白:梯度小的地方c接近1,扩散通量大,噪声被抹平;梯度大的地方c趋近0,扩散被阻断,边缘保留下来。Matlab里实现这个方程不用碰偏微分方程求解器,直接把每个像素当成一个网格节点,用中心差分近似梯度,用显式欧拉格式往前推时间步。
离散化后每个像素在每一轮的更新量是四个方向的通量之和:
I_new = I_old + dt × (cN·ΔN + cS·ΔS + cE·ΔE + cW·ΔW)
其中ΔN、ΔS、ΔE、ΔW是中心像素与上、下、右、左四个邻居的灰度差,cN等是对应方向的扩散系数。dt是时间步长,控制每轮更新的幅度。经典PM模型里c是梯度幅值|∇I|的单变量函数,而这个代码包的关键在于:c不是直接取|∇I|,而是把Gompertz分布函数套在了梯度幅值上。
2.2 Gompertz函数怎么当扩散系数用:两个形状参数的作用
Gompertz分布函数在生存分析里常用来描述“随着时间推移事件发生概率”的曲线,特点是单调、S形、尾部趋于饱和。拿它构造扩散系数时,常见实现是取其互补形式:
c(s) = exp(-β · (exp(γ · s) - 1))
其中s是归一化后的梯度幅值,β和γ是两个形状参数。当s=0时,c(0)=exp(0)=1,扩散完全开放;s增大时,exp(γ·s)迅速增长,c整体向0收缩,扩散关闭。与经典指数式c(s)=exp(-(s/k)^2)相比,Gompertz形式在中间梯度段有一条更缓的衰减走廊:低对比度边缘在指数模型里很容易被判定为噪声,而在Gompertz模型里,扩散系数还维持在0.3~0.6的量级,给了边缘一个“缓冲期”。
参数β控制起点衰减的快慢,β越大,即使梯度很小扩散系数也压得很低,图像更容易保持细节但噪声去除不干净;γ控制衰减率随梯度变化的急缓,γ越大,曲线越陡,边缘阈值越明确,但参数敏感度也随之上升。我在实际复现时发现,β通常取0.5~2,γ取5~20,具体取值和图像灰度归一化范围强相关,这个在第5章会详细展开。
2.3 解压后的代码结构:先跑demo再改参数
拿到rar解压后,我一般会先确认这几个文件是否存在,文件职责如下:
| 文件 | 职责 |
|---|---|
| gompertz_coeff.m | 扩散系数计算函数,输入梯度幅值和β、γ,输出c |
| central_gradient.m | 中心差分梯度计算,返回水平和垂直方向梯度 |
| gompertz_anisodiff.m | 主迭代函数,包含时间步长推进和早停逻辑 |
| demo_denoise.m | 演示脚本,读图、加噪、调用滤波、展示效果 |
| measure_quality.m | PSNR与SSIM客观指标计算,用于验证滤波效果 |
先把demo_denoise.m在Matlab里直接运行一遍,得到一张含噪图和一张去噪图的对比,再回头逐行改参数。这个顺序比上来就看主函数更高效,因为主函数里的变量名和PM论文里的符号不一定一一对应,先通过demo建立“参数变化→效果变化”的直觉,后面看代码会顺畅很多。
3. 把Gompertz滤波跑起来:梯度计算、扩散系数与迭代更新
3.1 中心差分法计算梯度:为什么不用Sobel
各向异性扩散对梯度的对称性很敏感。Sobel算子在3×3窗口里带权重,会轻微偏向水平或垂直方向,导致扩散在斜向边缘上产生方向性伪影。所以代码包里一般用中心差分,模板是[1 0 -1]/2,它结果无偏,且对像素网格的对称破坏最小。实现如下:
function [dx, dy] = central_gradient(I) % I: 灰度图像,double类型,已归一化到[0,1] dx = imfilter(I, [1 0 -1]/2, 'replicate'); dy = imfilter(I, [1; 0; -1]/2, 'replicate'); end逻辑说明:imfilter的核[1 0 -1]/2对每一行做水平方向中心差分,核[1; 0; -1]/2对每一列做垂直方向中心差分。/2是为了让差分结果保持原始灰度量级,避免梯度值翻倍。边框用'replicate'填充,意思是边界像素在计算差分时复制外侧值,这是第4章会重点提到的边界坑之一。
参数说明:如果图像没有归一化而是以uint8范围0~255参与运算,这里的梯度幅值会整体大几十倍,直接影响Gompertz函数里γ参数的选择。所以统一在进入滤波函数前把图像转成double并除以255。
3.2 Gompertz扩散系数函数:直接用梯度幅值计算c
function c = gompertz_coeff(grad_mag, beta, gamma) % grad_mag: 梯度幅值矩阵,[0,1]范围 % beta: 起始衰减系数,建议0.5~2 % gamma: 衰减速率系数,建议5~20 c = exp(-beta .* (exp(gamma .* grad_mag) - 1)); end逻辑说明:grad_mag是元素介于0和1之间的矩阵。对每个像素计算exp(gamma×grad_mag),梯度为0时该项等于1,括号内减1后为0,整个指数项为exp(0)=1,扩散系数最大。梯度越大,括号内迅速膨胀,exp(-beta×大数)快速收缩到0附近。这里用的是点乘,确保c和grad_mag形状一致。
参数说明:输入grad_mag应当是用sqrt(dx.^2 + dy.^2)算出的梯度幅值,不是梯度向量本身。beta增大相当于把整条c曲线向下压,gamma增大相当于把曲线的下降段变陡。在固定beta=1的情况下,gamma=10时,梯度幅值0.1对应的c约等于0.37,还是一半以上的扩散强度;gamma=20时,同样梯度下c约等于0.05,扩散基本关闭。这个量级判断对后面调参很重要。
3.3 主迭代函数:四方向通量与时间步长推进
function out = gompertz_anisodiff(I, beta, gamma, dt, n_iter) % I: double灰度图,范围[0,1] % beta, gamma: Gompertz形状参数 % dt: 时间步长,建议0.1~0.2 % n_iter: 最大迭代次数 if max(I(:)) == 0 out = I; return; end [rows, cols] = size(I); I_cur = I; for it = 1:n_iter % 扩展边界,方便四邻域差分 Ip = padarray(I_cur, [1 1], 'replicate'); % 四个方向的灰度差 deltaN = Ip(1:end-2, 2:end-1) - Ip(2:end-1, 2:end-1); deltaS = Ip(3:end, 2:end-1) - Ip(2:end-1, 2:end-1); deltaE = Ip(2:end-1, 3:end) - Ip(2:end-1, 2:end-1); deltaW = Ip(2:end-1, 1:end-2) - Ip(2:end-1, 2:end-1); % 梯度幅值近似,用于计算扩散系数 grad_h = 0.5 * (deltaE - deltaW); grad_v = 0.5 * (deltaN - deltaS); grad_mag = sqrt(grad_h.^2 + grad_v.^2); % 四个方向的扩散系数 cN = gompertz_coeff(abs(deltaN), beta, gamma); cS = gompertz_coeff(abs(deltaS), beta, gamma); cE = gompertz_coeff(abs(deltaE), beta, gamma); cW = gompertz_coeff(abs(deltaW), beta, gamma); % 显式欧拉更新 I_new = I_cur + dt * (cN .* deltaN + cS .* deltaS ... + cE .* deltaE + cW .* deltaW); I_cur = I_new; end out = I_cur; end逻辑说明:padarray把图像四周边界各扩大一个像素,填充方式用replicate,这样在计算图像最外圈像素的四方向通量时,边界外的灰度值等于紧邻内侧的灰度值,等效于零梯度边界条件。四个delta变量分别代表上下左右邻居与中心像素的灰度差。这里扩散系数没有统一用梯度幅值,而是对每个方向的差分绝对值单独算一份c,这样在斜向边缘上,沿边缘方向的扩散仍然保留,垂直边缘方向被阻断,更贴合各向异性扩散的本意。
参数说明:dt的合法范围是0到0.25之间,原因在5.3小节会展开。代码里超过这个范围会导致迭代发散,表现为图像出现周期性的黑白条纹或NaN。n_iter这里给了最大迭代次数,实际使用时建议在循环内加一个早停判断:当相邻两次I_cur之差的均方根小于某个阈值(比如1e-4)就直接跳出,避免过度平滑。我在复现时会额外记录每次迭代后的结果,方便后面画残差曲线。
3.4 演示脚本:从加噪到滤波的一条龙验证
% demo_denoise.m clear; clc; close all; I_orig = im2double(imread('your_image.png')); % 替换为你的灰度图 if size(I_orig, 3) == 3 I_orig = rgb2gray(I_orig); end rng(42); I_noisy = imnoise(I_orig, 'gaussian', 0, 0.01); % 高斯噪声,方差0.01 % Gompertz各向异性扩散滤波 beta = 1.0; gamma = 10; dt = 0.15; n_iter = 50; I_den = gompertz_anisodiff(I_noisy, beta, gamma, dt, n_iter); % 显示结果 figure; subplot(1,3,1); imshow(I_orig); title('Original'); subplot(1,3,2); imshow(I_noisy); title('Noisy'); subplot(1,3,3); imshow(I_den); title('Gompertz Anisotropic Diffusion'); % 客观指标 psnr_noisy = measure_quality(I_noisy, I_orig); psnr_den = measure_quality(I_den, I_orig); fprintf('PSNR before: %.2f dB, after: %.2f dB\n', psnr_noisy, psnr_den);逻辑说明:rng(42)固定随机种子,保证每次加噪结果可复现,这是对比实验的基本习惯。imnoise的gaussian参数依次是均值0和方差0.01,对应灰度范围[0,1]下的中等强度噪声。measure_quality函数负责计算PSNR和SSIM,分别从峰值信噪比和结构相似度两个角度衡量去噪是否损伤了边缘结构。
参数说明:如果你的测试图自身纹理特别丰富,建议把方差降到0.005;如果噪声更强,则把gamma适当降低到6~8,让扩散系数曲线变缓,否则会出现“纹理区去噪不干净、光照渐变区被磨平”的极端情况。这段脚本跑通之后,再进主函数改其他细节。
4. 常见问题排查:参数翻车、边界失真与迭代爆炸
4.1 迭代到第20轮图像突然出现黑白交替条纹
现象:前十几轮图像平滑正常,到某一轮开始出现规则的棋盘格状黑白条纹,继续迭代条纹越来越密,最后图像充满NaN。
原因:显式欧拉格式的时间步长超出了数值稳定性边界。四邻域拉普拉斯的CFL条件要求dt×max(c)≤0.25,当dt取0.3而图像某些区域梯度极低、c接近1时,扩散系数乘以差分项后更新量过大,数值振荡逐步放大,最终发散。Gompertz系数在平坦区本身就是近似1,比经典PM模型里exp(-(s/k)^2)在平坦区的导纳更高,所以更容易触发这个边界。
解决:把dt压到0.1~0.2,同时检查输入图像是否确实归一化到[0,1]。如果图像还是0~255的uint8范围,梯度差会放大255倍,即使dt=0.01也照发不误。我在代码里加了保护性判断:进入主函数后自动检查max(I(:)),大于1.5就直接除以255。
4.2 去噪后图像四边框出现一圈亮边或暗边
现象:滤波结果中央区域看起来正常,但图像最外圈5个像素左右明显比周围亮或暗,像是加了一圈描边。
原因:边界填充方式选错了。如果padarray用'zero'填充,图像外一圈的邻居是0,中心像素与“伪黑边”的差分极大,扩散系数把这些假梯度识别成了边缘,扩散在边界处被错误阻断,视觉上就成了亮边。这是所有各向异性扩散实现里最容易翻车的地方。
解决:统一改用'replicate'或'symmetric'填充。replicate复制最外侧像素值,假设图像外是平的;symmetric做镜像翻转,在图像边缘接近对称的真实场景里更稳。两种都能用,但选了就要全程统一,否则梯度场在左上角与右下角不对称,会产生方向性伪影。
4.3 低对比度边缘被当成噪声抹掉了
现象:原图里一条很淡的划痕或暗部细节,滤波后彻底消失,图像看起来“干净过头”,PSNR比含噪图还低。
原因:gamma取太大,扩散系数曲线过陡,低梯度区域c已经压到接近0,扩散在淡边缘两侧都没有充分传递信息;又或者beta取太小,平坦区的噪声没有在早期轮次被压掉,后期曲线又陡,噪声残留下来形成斑点。
解决:用梯度直方图估计gamma初值。我的做法是先把含噪图的梯度幅值算出,统计90%分位数作为特征尺度,gamma设为这个分位数的倒数,保证大部分非边缘区域落在c>0.5的区间,再手动微调。具体代码在第5章。
4.4 滤波到60轮之后图像出现“油画感”
现象:图像局部区域变成几个大块常量色,内部细节全部消失,边界棱角分明但过渡生硬,像水彩颜料干裂。
原因:迭代次数太大,没有早停机制。各向异性扩散是个非线性过程,前期以去噪为主,后期扩散持续推进,即使梯度大的边缘也会被一点点侵蚀,最终整幅图接近分段常数。这种退化在PSNR曲线上表现为先升后降,只看最终结果很容易误判。
解决:在主循环里加早停条件:每10轮计算一次I_new和I_cur的标准差,相对变化小于1e-4就跳出。我在3.3节的代码里只留了n_iter,实际用时更推荐写成while循环,外部传入tol参数,见5.4小节的改进版。
5. 调参经验:梯度阈值、时间步长与Gompertz系数怎么设
5.1 参数之间的关系:β、γ不是独立变量
Gompertz扩散系数曲线里,β和γ共同决定“边缘阈值”。换算成经典PM模型里的k参数来看:对c(s)=0.5的梯度值s_half,有:
s_half = (1/γ) · ln(1 + ln(2)/β)
从这个公式能看出,β增大让s_half向更大梯度方向移动,γ增大让s_half变小。所以“边缘保持更强”有两个方向:把β调大让扩散在总体上都收得更紧,或者把γ调小让曲线整体右移。实际调参时,不会独立把β从1改成2又保持γ不动然后逐轮试,那样维度太高。常见的策略是先固定β=1,只调γ,曲线形状从缓变陡,观察效果;如果γ调到20还是模糊细节,再考虑增大β到2,让扩散系数整体下降一档。
| 参数 | 作用 | 推荐初值 | 调整方向 |
|---|---|---|---|
| β | 起始衰减系数,控制c曲线的整体高度 | 1.0 | 边缘保留不足时增大 |
| γ | 衰减速率系数,控制c曲线的陡度 | 10 | 噪声残留时增大,细节丢失时减小 |
| dt | 时间步长,控制每轮更新幅度 | 0.15 | 发散时减小,收敛慢时最多到0.2 |
| n_iter | 最大迭代轮数 | 50 | 噪声强时适当增加,配合早停使用 |
| tol | 早停阈值,相邻两轮图像标准差变化 | 1e-4 | 要求更严格时取1e-5 |
5.2 自适应梯度阈值估计:用prctile替代手工猜gamma
手工设定gamma的痛点在于:不同图像梯度分布差异极大,暗图像整体梯度小,亮图像整体梯度大,固定gamma=10对其中一张合适,另一张就完全失效。我习惯在滤波前先做一步统计:
% 自适应估算gamma初值 I_gray = im2double(rgb2gray(I_orig)); % 输入任意图像 [dx, dy] = central_gradient(I_gray); grad_mag = sqrt(dx.^2 + dy.^2); grad_p90 = prctile(grad_mag(:), 90); % 90%分位数 gamma_init = 1 / max(grad_p90, 1e-6); fprintf('Suggested gamma: %.2f\n', gamma_init);逻辑说明:prctile取梯度幅值的90%分位数,意味着90%的像素梯度小于这个值。把gamma设为该值的倒数,则这些像素的gamma×s分布在0~1附近,扩散系数c在exp(-beta×(exp(1)-1))量级,不会过早关停;而剩下10%的高梯度边缘像素,gamma×s迅速突破3以上,c逼近0,边缘保护生效。这个初值不需要精确,但能把gamma从“随便填一个”变成“基于图像统计量推导”。
参数说明:grad_p90可能非常小,比如一张平滑图像里90%的像素梯度都小于0.02,gamma_init就会高达50,这种场景下噪声本来就不严重,把gamma限制在5~30之间更合理。所以我在代码里加了min(max(1/grad_p90, 5), 30)的钳制,防止极端值出现。
5.3 时间步长dt的上限推导:为什么是0.25
显式欧拉格式的稳定性条件比像素差分格式更严格。对二维四邻域拉普拉斯算子,谱半径是4(每个像素有4个邻居,每个邻居贡献一个连接项),最大扩散系数c_max=1(梯度为0时),所以要求:
dt × c_max × 4 ≤ 1
即dt ≤ 0.25。这是理论上限。实际使用上,取0.15比较保守,因为Gompertz系数在平坦区虽然接近1但不会严格等于1,留20%的裕量能避免边界像素上的数值振荡。如果dt取0.2,迭代60轮后虽不至于发散,但可能在某些纹理密集区出现轻微的高频抖动,我建议在演示实验里统一用0.15。
5.4 带早停的改进版主循环
function out = gompertz_tuned(I, beta, gamma, dt, n_iter, tol) if nargin < 6, tol = 1e-4; end I_cur = double(I); if max(I_cur(:)) > 1.5 I_cur = I_cur / 255; end for it = 1:n_iter % 四方向差分与扩散系数计算,与3.3节相同 % ... 此处省略重复代码 ... I_new = I_cur + dt * (cN .* deltaN + cS .* deltaS ... + cE .* deltaE + cW .* deltaW); diff_rms = sqrt(mean((I_new(:) - I_cur(:)).^2)); if diff_rms < tol fprintf('Early stop at iteration %d, diff_rms=%.2e\n', it, diff_rms); out = I_new; return; end I_cur = I_new; end out = I_cur; end逻辑说明:diff_rms计算上一轮与当前轮的像素级均方根差。去噪早期每轮变化大,diff_rms在1e-2量级;进入平稳期后快速下降。设置tol=1e-4意味着两轮之间的平均灰度变化小于万分之一时,继续迭代没有意义。这个阈值对多数自然图像适用,如果图像本身细节很丰富,建议改到5e-4,避免早停太晚把纹理也磨掉。
参数说明:n_iter仍然保留作为最大轮数,防止tol设得太小导致循环跑不完。实际运行中,50轮迭代大部分在20~35轮就会触发早停,剩余轮数用于边缘缓蚀,这正是避免“油画感”的关键。
5.5 三组实测参数组合:不同噪声强度下的参考配置
| 噪声方差 | β | γ | dt | n_iter | 预期行为 |
|---|---|---|---|---|---|
| 0.005 | 1.0 | 12 | 0.15 | 30 | 轻微去噪,细节保持好 |
| 0.01 | 1.0 | 10 | 0.15 | 50 | 均衡,PSNR提升2~3dB |
| 0.03 | 0.8 | 6 | 0.15 | 80 | 强去噪,接受部分细节损失 |
第二组是最常遇到的场景。第三组强噪声下beta要降到0.8,让平坦区扩散更充分,gamma降到6避免噪声梯度被误判成边缘。记住这些组合不是公式,是起点。每种图像都值得跑一遍梯度直方图,按5.2小节重新推导gamma。
6. 验证与进阶:SSIM评估、彩色扩展与边缘保持性检查
6.1 客观指标验证:PSNR和SSIM一起看
PSNR只衡量像素级误差,边缘被磨平但整体灰度接近时PSNR可能仍然不低,必须叠加SSIM验证结构保持:
function [p, s] = measure_quality(I_test, I_ref) p = psnr(I_test, I_ref); % 需Image Processing Toolbox s = ssim(I_test, I_ref); end没有工具箱时可以用手动PSNR:10×log10(1/mean((I_test(:)-I_ref(:)).^2)),SSIM实现较繁琐,建议直接装工具箱。评估时注意参考图必须是与含噪图同尺寸的原始干净图,不能拿滤波后的图和含噪图比PSNR,那只能测噪声压制了多少。
6.2 边缘保持性检查:用残差图看问题
我一直觉得SSIM还不够直观。更简单的做法是把原图边缘提取出来,看边缘像素在滤波前后的灰度变化量。用imgradient或edge算子得到边缘掩膜,统计掩膜内像素的平均绝对误差,这个数值小于全图平均误差,说明边缘确实被保护了。如果边缘区误差反而更大,说明扩散系数把边缘当成了噪声在磨。
6.3 彩色图像扩展:分通道小心亮度漂移
把灰度滤波扩到彩色图时,不要直接对RGB三个通道分别跑Gompertz扩散。RGB通道之间相关性高,分开处理会出现颜色边缘毛刺。常见做法是转到Lab空间,只对L通道滤波,保留a和b通道不变,再转回RGB。这样既能用上Gompertz的灰度扩散模型,又不会产生伪彩色。
6.4 一个提醒
我最早复现这个算法时,跳过梯度统计直接拿网上的参数跑,结果一张暗图像被磨成了塑料片。从那以后每次做扩散滤波实验,我都先跑一遍干净图的零噪声死循环验证离散格式稳定,再用含噪图调β和γ,养成这个习惯之后,翻车概率低了很多。希望帮到你。
本文还有配套的精品资源,点击获取