简介:面向毕业设计与课程设计的MATLAB图像去噪项目资源,系统覆盖传统算法与深度卷积神经网络DnCNN两条技术路线,适合图像处理、计算机视觉方向的学生和入门开发者参考学习。传统去噪部分包含均值滤波、中值滤波、高斯滤波等基础算法,同时提供BM3D及其变体等经典方法的脚本,可结合不同噪声类型观察参数对细节保留与平滑效果的影响;深度学习部分则完整搭建DnCNN网络,涵盖卷积层、批量归一化、ReLU激活配置以及训练与预测流程,帮助理解从传统滤波到数据驱动学习的差异。压缩包共177个文件,约80.31MB,以.m源码脚本、.mat训练数据、.png示例图片为主,另含跨平台mex编译库和说明文档,便于直接运行、效果对比与二次开发。该项目已有854人学习下载,对需要开展图像去噪实验、算法对比或完成毕设工作的读者,是一份完整且可复现的参考工程。
1. 图像去噪不是滤掉噪声:传统方法与 DnCNN 的建模差异决定代码怎么写
第一次用 MATLAB 做图像去噪大作业时,大多数人会先拖一个 medfilt2 出来,看到画面光滑了就算交差。真正对比后会发现,传统图像去噪算法和小波阈值在低噪声下表现尚可,一旦噪声方差升高,画面要么糊成一片,要么出现振铃。基于深度卷积神经网络的 DnCNN 走的是另一条路:它不是设计一个滤波器模板,而是通过大量带噪图像对训练网络去预测噪声残差,再用原图减去残差得到干净图。两种思路在 MATLAB 里的落地路径完全不同,参数调节方式也几乎没有交集。这篇博文把传统算法和 DnCNN 从实现、参数、评价到自适应批处理逐层拆开,适合正在做图像处理课程设计、毕业设计或工程验证的读者。
2. 在 MATLAB 中实现传统图像去噪算法:均值、中值、维纳与小波阈值
传统图像去噪算法在 MATLAB 里不需要额外工具箱,Image Processing Toolbox 自带的函数足够搭起一套完整对比基线。常见做法是把一张干净图叠加已知噪声,再用不同滤波器处理,最后用 PSNR 和细节保留情况判断好坏。比较时不要只看视觉上干不干净,还要看边缘是否被磨平、弱纹理有没有消失。下面这套代码是图像处理大作业里最常用的一组组合,我一般用它作为深度学习方法上线前的基准线。
2.1 传统去噪算法的核心逻辑与 MATLAB 最小实现
2.1.1 均值与中值滤波:平滑假设下的两种取舍
均值滤波假设噪声是高频信号,图像真实内容在局部区域变化平缓,所以用邻域均值替换中心像素能压低随机波动。中值滤波更像排序统计,它对椒盐噪声特别有效,但在平滑区域会产生细碎灰阶突变。两者共同问题是无差别处理边缘,窗口越大,去噪越强,边缘损失也越明显。
% 读取灰度图并转为 double,值域保持 [0,1] img = im2double(imread('cameraman.tif')); noisy = imnoise(img, 'gaussian', 0, 0.01); % 高斯噪声,均值为 0,方差为 0.01 % 均值滤波:3x3 与 7x7 对比 avg3 = imfilter(noisy, fspecial('average', [3 3]), 'replicate'); avg7 = imfilter(noisy, fspecial('average', [7 7]), 'replicate'); % 中值滤波:同样对比两种邻域尺寸 med3 = medfilt2(noisy, [3 3]); med7 = medfilt2(noisy, [7 7]);代码说明:fspecial('average', [3 3])生成一个归一化平均核,所有元素和为 1,卷积后不会改变图像整体亮度。imfilter的第三参数'replicate'控制边界填充方式,意思是用边界的像素值向外复制,避免黑边进入统计;另一个常用选项是'symmetric',适合预期边界平滑过渡的场景。medfilt2是二维中值滤波,对高密度椒盐噪声建议把窗口开到 5 以上,但对细线、文字这类只有 1 像素宽的结构破坏非常明显。
这两类滤波器的调参思路是单调的:窗口越大,去噪越彻底,PSNR 不一定越高,因为边缘对比度下降会在视觉上产生「塑料感」。如果只用这两种算法,很容易陷入一个困局——为了指标好看不断加大窗口,结果边缘一塌糊涂。
2.1.2 维纳滤波与小波阈值:从统计和频域出发
维纳滤波和小波阈值比前两种多做了一步。维纳滤波器把信号和噪声当作随机过程,在每个局部窗口内估计均值和方差,再对中心像素做自适应收缩;小波阈值则把图像变换到小波域,认为真实边缘对应大幅值系数,噪声分散在小幅值系数里,设定阈值把低于阈值的系数置零或收缩。
% 维纳滤波:开窗 5x5,自动估计局部均值与方差 wn5 = wiener2(noisy, [5 5]); % 小波阈值去噪:三层分解 + 软阈值 [c, s] = wavedec2(noisy, 3, 'db4'); thr = wthrmngr('dw2ddenoLVL', 'sqtwolog', c, s); % 通用阈值规则 cT = wthresh(c, 's', thr); % 's' 表示软阈值 wavDen = waverec2(cT, s, 'db4');代码说明:wiener2默认会在每个窗口内估计噪声方差,也可以手动传入已知噪声方差:wiener2(noisy, [5 5], noiseVar)。当噪声方差已知且均匀时,传参后恢复效果更稳;当噪声是空间变化时,反而会让局部方差估计更合理,这是维纳滤波比固定模板滤波真正自适应的地方。
wthrmngr是小波阈值规则管理器,'sqtwolog'是通用阈值,适合噪声强度未知但有较多系数的场景;噪声较弱时可以换'rigrsure',它基于 Stein 无偏风险估计,PSNR 通常更好;'minimaxi'在两者之间取折中。软阈值wthresh(c, 's', thr)会把大幅值系数也向零收缩,硬阈值'h'则保留原值,视觉效果更锐利但容易放大局部抖动。对小波基的选择,db4计算量小,sym8对称性更好,多层分解后振铃更少。
2.2 传统算法的适用边界与参数调整
| 算法 | 核心参数 | 参数加大后的现象 | 主要适用噪声 |
|---|---|---|---|
| 均值滤波 | 邻域窗口 n | 边缘模糊加重 | 高斯、均匀噪声 |
| 中值滤波 | 窗口 n | 细节丢失、灰阶平台化 | 椒盐噪声 |
| 维纳滤波 | 局部窗口 [m n] | 过度平滑、块状伪影 | 高斯、混合噪声 |
| 小波阈值 | 分解层数、阈值规则 | 振铃、细节稀疏化 | 高斯、低信噪比 |
当测试图像边缘密集时,均值滤波的 PSNR 会开始下降,而中值滤波在低密度椒盐噪声下表现稳定,一旦椒盐比例超过 0.3,窗口内干净像素点不足,排序统计失去意义。维纳滤波的块状伪影通常来自窗口尺寸与图像纹理尺度不匹配:纹理尺度小但窗口开得大,局部均值就把纹理磨掉了;反过来窗口小,方差估计不稳定。
小波阈值最值得调试的是分解层数。三层是常见起点,层数加到五层以上时,低频近似的尺寸变化不大,但高频细节被反复分拆,软阈值收缩量叠加,容易出现横向振铃条纹。另一个容易忽略的点是wavedec2对小波基的边界扩展方式,默认是周期扩展,图像边缘不连续时会在小波系数两端产生大值伪影,这些伪影经过阈值后仍保留。传统方法的调参几乎都是在「保留边缘」和「去除噪声」之间挪动,没有哪个参数能同时解决两者。
提示:在传统算法和 DnCNN 做对比时,传统方法不适合用同一组参数跑全部噪声强度。我一般会把 σ(噪声标准差除以 255)分成 5、15、25、50 四档,每档单独调一次传统算法的窗口或阈值规则,再与深度网络结果比较,这样对 DnCNN 才算公平。
3. DnCNN 网络结构、残差学习与 MATLAB 训练实现
DnCNN 的核心思路是「用深度卷积神经网络学习噪声的分布」,而不是直接学习干净图像。这个选择改变了训练数据构造方式、损失函数写法,以及在 MATLAB 里搭建网络时的输出层设定。
3.1 DnCNN 为什么预测噪声残差而不是直接输出干净图
设带噪图像 Y = X + N,其中 X 是干净图像,N 是加性噪声。传统去噪算法直接估计 X,而 DnCNN 让网络计算 F(Y) ≈ N,推理时得到 X ≈ Y − F(Y)。这个修改在 MATLAB 里影响的是网络最后一层输出和训练响应,网络本身并不复杂。
残差学习的好处有两层。第一,噪声图像 N 的能量远低于干净图像 X 的能量,网络不需要重新学习从输入到输出的恒等映射,深度加深后梯度更容易保持;第二,如果训练时噪声均值接近零,残差图像近似零均值随机场,网络最后一层可以输出负值而不会破坏图像结构。这就是为什么 DnCNN 的最后一层卷积之后不接 ReLU,也不接回归层,直接输出残差图。建筑基块由 3×3 卷积、批量归一化层 BatchNorm 和 ReLU 组成,深度通常取 17 层左右,这部分在 Deep Learning Toolbox 里可以逐层搭出来。
3.2 用 Deep Learning Toolbox 搭建 DnCNN 并准备成对补丁
3.2.1 生成 DnCNN 风格的 layerGraph
function lgraph = dncnnLayerGraph(hiddenLayers, filters, inputSize) % 生成 DnCNN 风格网络的 layerGraph % hiddenLayers: 中间卷积层数量,不包含最后的输出卷积层,常见取 16 % filters: 每层卷积核数量,常用 64 % inputSize: [H W C],例如 [128 128 1] 表示 128x128 灰度图 layers = imageInputLayer(inputSize, 'Name', 'input', ... 'Normalization', 'none'); % 不做输入归一化 layers = [layers; convolution2dLayer(3, filters, 'Padding', 'same', ... 'WeightsInitializer', 'he', 'Name', 'conv_input')]; layers = [layers; reluLayer('Name', 'relu_input')]; for k = 1:hiddenLayers - 1 layers = [layers; convolution2dLayer(3, filters, 'Padding', 'same', ... 'WeightsInitializer', 'he', 'Name', ['conv_' num2str(k)])]; layers = [layers; batchNormalizationLayer('Name', ['bn_' num2str(k)])]; layers = [layers; reluLayer('Name', ['relu_' num2str(k)])]; end layers = [layers; convolution2dLayer(3, inputSize(3), 'Padding', 'same', ... 'WeightsInitializer', 'he', 'Name', 'conv_output')]; lgraph = layerGraph(layers); end参数说明:imageInputLayer里的'Normalization', 'none'很关键,默认的zerocenter会减去训练数据均值,而 DnCNN 的残差学习依赖输入像素的绝对数值,归一化后反而会让残差统计失真。'Padding', 'same'保证每层卷积输出尺寸不变,这样输入输出图像大小一致。he初始化适合后面接 ReLU 的网络,比glorot更容易在训练初期保持激活方差。hiddenLayers取 16 时,加上输入卷积和输出卷积,整个网络共 18 层卷积,灰度图上的参数量大约 60 万,单张消费级 GPU 在视频训练数据上十几分钟能完成一轮。
3.2.2 准备训练数据与训练选项
DnCNN 训练时不能直接把整张图扔进去,常见做法是随机裁剪成 128×128 的小块,并在每个小块上独立添加随机强度的噪声,这样相当于把数据增强和噪声建模合并在一起。
% 用 imageDatastore + transform 生成成对的带噪/残差补丁 imds = imageDatastore(cleanImgFolder, 'FileExtensions', '.png', ... 'ReadFcn', @(x) im2double(imread(x))); dsTrain = transform(imds, @(x) makeNoisyPair(x, [5 25], [128 128])); function out = makeNoisyPair(img, sigmaRange, patchSize) % 随机裁剪 patch h = randi([0, size(img, 1) - patchSize(1)]); w = randi([0, size(img, 2) - patchSize(2)]); cleanPatch = img(h+1:h+patchSize(1), w+1:w+patchSize(2), :); % 从 [5,25] 中随机取一个噪声标准差 sigma = sigmaRange(1) + rand * diff(sigmaRange); noisyPatch = cleanPatch + sigma / 255 * randn(size(cleanPatch)); % trainNetwork 回归任务要求输出 {输入, 响应} out = {noisyPatch, noisyPatch - cleanPatch}; % 响应是噪声残差 end代码说明:响应是noisyPatch - cleanPatch,而不是cleanPatch,这对应 DnCNN 的残差学习目标。如果训练时响应写成干净图,网络就变成了普通卷积回归去噪模型,BatchNorm 对残差的作用会被稀释。随机裁剪不仅增加样本量,还让网络在不同图像结构上都能看到噪声,避免过拟合到固定尺寸。
opts = trainingOptions('adam', ... 'InitialLearnRate', 1e-3, ... 'MiniBatchSize', 32, ... 'MaxEpochs', 30, ... 'L2Regularization', 1e-4, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'ExecutionEnvironment', 'auto'); net = trainNetwork(dsTrain, lgraph, opts);这里的ExecutionEnvironment设为'auto',有 GPU 时自动用 GPU,没有 GPU 时退回 CPU。CPU 上训练 30 个 epoch 会比较慢,建议把MaxEpochs降到 10,或者把 patch 尺寸改到 96×96。
3.3 训练 DnCNN 的关键参数速查
| 参数 | 建议值 | 调整说明 |
|---|---|---|
| InitialLearnRate | 1e-3 | 梯度出现 NaN 时降到 1e-4 |
| MiniBatchSize | 32 | 显存不足时降到 16,但 BatchNorm 统计会抖动 |
| MaxEpochs | 20~50 | 小数据集 30 以内,验证集 PSNR 不再上升即可停 |
| 噪声σ范围 | [5, 25] | 覆盖绝大多数真实噪声场景,训练和测试σ应尽量重叠 |
| PatchSize | 128 | 过小会导致 BatchNorm 批内样本不充分 |
| L2Regularization | 1e-4 | 防止深网络过拟合到训练噪声分布 |
训练完成后,推理代码并不复杂:
noisy = im2double(imread('test.png')); noisy4 = reshape(noisy, size(noisy, 1), size(noisy, 2), 1, 1); residual = predict(net, noisy4); denoised = noisys - squeeze(residual);注意:DnCNN 的输出层没有激活函数,所以它允许输出负数。测试时如果去噪图出现大面积暗斑,先检查是否在最后一层误加了 ReLU;如果残差图整体均值明显偏离 0,说明训练响应的构造有误。
4. 传统图像去噪与 DnCNN 的实验对比:PSNR、SSIM 与运行耗时
对比实验最容易出问题的地方不是算法本身,而是评价指标的使用方式。PSNR 和 SSIM 在 MATLAB 里都是现成函数,但值域不统一会直接得出虚假的高分结果。
4.1 用 MATLAB 自带的 psnr、ssim 计算评价指标
function [p, s] = evalMetrics(denoised, clean) % denoised 和 clean 必须同为 double,且值域都在 [0,1] p = psnr(denoised, clean, 1); % peakval 必须显式传 1 s = ssim(denoised, clean, 'DynamicRange', 1); % 指定动态范围 end % 计算运行耗时:timeit 会执行多次取中位数,比 tic/toc 稳定 f = @() dncnnDenoise(noisyImg); elapsed = timeit(f);参数说明:psnr函数默认把第三参数 peakval 当作 255,如果输入已经是 [0,1] 的 double 而不传峰值,算出来的 PSNR 会普遍虚高约 10 dB。ssim的DynamicRange参数同理。这是对比脚本里最常见的坑,尤其当代码中混用im2double和uint8时最容易现。timeit 要求被测量函数不需要输入参数,所以用匿名函数把 noyisyImg 和 net 捕获进去;如果测量对象是批处理脚本,直接用ts = tic; ...; toc(ts)更直观。
4.2 完整对比脚本与结果解读
sigs = [5 15 25 50]; clean = im2double(imread('cameraman.tif')); for i = 1:numel(sigs) noisy = clean + sigs(i) / 255 * randn(size(clean)); % 均值滤波 avg = imfilter(noisy, fspecial('average', [5 5]), 'replicate'); % 中值滤波 med = medfilt2(noisy, [5 5]); % 维纳滤波 win = wiener2(noisy, [5 5]); % 小波阈值 [c, s] = wavedec2(noisy, 3, 'db4'); cT = wthresh(c, 's', wthrmngr('dw2ddenoLVL', 'sqtwolog', c, s)); wav = waverec2(cT, s, 'db4'); % DnCNN 残差预测 dnc = noisy - squeeze(predict(net, reshape(noisy, ... size(noisy,1), size(noisy,2), 1, 1))); results(i, :) = table(sigs(i), ... psnr(avg, clean, 1), ssim(avg, clean, 'DynamicRange', 1), ... psnr(med, clean, 1), ssim(med, clean, 'DynamicRange', 1), ... psnr(win, clean, 1), ssim(win, clean, 'DynamicRange', 1), ... psnr(wav, clean, 1), ssim(wav, clean, 'DynamicRange', 1), ... psnr(dnc, clean, 1), ssim(dnc, clean, 'DynamicRange', 1)); end| 噪声σ | 更适合的传统方法 | 是否值得切换 DnCNN |
|---|---|---|
| 5~10 | 维纳滤波 | 不值得,DnCNN 提升有限且训练成本高 |
| 15~25 | 小波阈值 | 值得,DnCNN 在高噪区的边缘保持更明显 |
| 30~50 | 几乎无合适选项 | 值得,DnCNN 的优势显著,传统方法已严重失真 |
这只是工程经验判断,实际差异受图像内容影响很大。对于纹理密集的遥感图或者医学影像,DnCNN 在 σ=15 时就能拉开 2~3 dB 差距;对于大片平滑区域占比高的图像,传统维纳滤波反而可能更稳,因为深度网络会把纹理模式误当成噪声去掉。PSNR 反映像素级误差,SSIM 反映结构相似性,两者趋势通常一致,但偶尔会出现 PSNR 高、SSIM 低的情况,说明去噪图在边缘上出现了移位或伪结构,这时应当以 SSIM 为准继续调参。
4.3 对比测试中的三个常见坑
第一,值域不统一。整张图像如果一半是 double [0,1],一半是 uint8 [0,255],计算 PSNR 前必须统一到 [0,1] 并显式传 peakval。
第二,训练噪声强度和测试噪声强度不一致。DnCNN 在 σ=25 上训练,却拿 σ=50 的测试图去推断,残差预测会出现系统性偏差,输出图像反而比传统算法更脏。解决方法是把训练σ范围扩展到测试场景,或者推理前先估计当前图像的噪声等级,再匹配到最近的训练场景。
第三,边界效应。imfilter默认对边界补零,统计指标会把黑色边框算进去,导致边缘像素误差被虚假放大;DnCNN 的卷积层虽然用'Padding', 'same',但深层感受野跨出图像后也会产生边界伪影。对比时最好统一裁掉图像四周 10~15 个像素,再计算 PSNR 和 SSIM,这样对传统算法和深度网络都公平。
5. 用噪声自适应与批处理把 MATLAB 去噪脚本变成可复用工具
实验做通之后,下一步是把它从交互式脚本变成可以自动处理一批图片的工具。落到实际场景里,核心工作是三件事:先估噪声等级,再决定调用哪条路径,最后验证模型没有跑偏。
5.1 用噪声级估计自动选择传统方法还是 DnCNN
function sigmaHat = estimateNoiseLevel(y) % 用拉普拉斯算子提取高频细节,稳健估计高斯噪声标准差 lap = [0 -1 0; -1 4 -1; 0 -1 0]; detail = imfilter(y, lap, 'replicate'); sigmaHat = median(abs(detail(:))) / 0.6745; % 0.6745 是标准正态分布中位数 end原理:真实图像的高频细节在拉普拉斯响应中占比不大,而高斯噪声在逐点变换后近似服从正态分布,因此中位数再除以 0.6745 能估计出噪声标准差。阈值选择一般这样判断:σ 低于 15 时维纳滤波足够,σ 在 15 到 30 之间用小波阈值,σ 高于 30 再调 DnCNN。
function out = smartDenoise(y, net) sigma = estimateNoiseLevel(y); if sigma < 15 out = wiener2(y, [5 5]); elseif sigma < 30 [c, s] = wavedec2(y, 3, 'sym8'); cT = wthresh(c, 's', wthrmngr('dw2ddenoLVL', 'minimaxi', c, s)); out = waverec2(cT, s, 'sym8'); else residual = predict(net, reshape(y, size(y,1), size(y,2), 1, 1)); out = y - squeeze(residual); end end5.2 批量处理文件夹中图像的脚本框架
srcDir = 'D:\project\noise_imgs'; dstDir = 'D:\project\denoised'; if ~exist(dstDir, 'dir'), mkdir(dstDir); end files = dir(fullfile(srcDir, '*.png')); for k = 1:numel(files) im = im2double(imread(fullfile(files(k).folder, files(k).name))); out = smartDenoise(im, net); imwrite(out, fullfile(dstDir, files(k).name)); end脚本说明:smartDenoise每次会先运行estimateNoiseLevel,再决定走哪条分支,这样不需要为每张图手动调参。如果图片总量很大,把 for 换成 parfor 可以并行,但net会作为广播变量复制到每个工作进程,内存占用会上涨,8 核以下的机器建议先用普通 for。
5.3 验证训练后的 DnCNN 是否真正收敛
% 在验证集上检查残差的统计特性 valResidual = valNoisy - valDenoised; % valNoisy 是网络输入,valDenoised 是输出 meanRes = mean(valResidual(:)); stdRes = std(valResidual(:)); fprintf('残差均值 %.4f,标准差 %.4f\n', meanRes, stdRes); % 期望均值接近 0,标准差接近注入的 sigma/255这个验证思路不依赖 PSNR,直接看网络学到了什么。如果残差均值偏离 0 太远,说明网络把一部分图像结构也当成了噪声;如果标准差远小于注入的σ,说明网络欠拟合,需要增大 epoch 或提高学习率。配合训练时的training-progress曲线,可以确认 loss 下降不是靠 LR 衰减假象撑起来的——曲线平滑下降并最终平稳,比单点 PSNR 更有说服力。最后把验证集上收敛时对应的 epoch 记为最终训练轮数,后续再训练同任务数据时直接沿用。
本文还有配套的精品资源,点击获取