
简介一套面向图像去噪算法学习与实践的 MATLAB 项目资料完整覆盖传统算法与基于深度卷积神经网络的 DnCNN 方法适配期末大作业、课程设计与本科毕设等场景代码注释拉满结构清晰新手也能按注释快速理解、复现并改造。压缩包共 177 个文件约 80.38MB主体为 35 个 .m 源码、33 个 .mat 数据文件与 26 张 .png 效果图另含多平台 mex 动态库、辅助 .p 文件及文档便于在不同环境中直接运行并对比去噪结果。包内可看到 BM3D、VBM3D、CBM3D、BM3DDEB 等传统算法实现也提供 DnCNN 的测试脚本实验链路完整输出图表直观方便横向比较不同方法的效果差异。已有 361 人学习下载特别适合正在准备图像处理课程设计或毕业设计的学生下载后简单部署即可用于算法演示、指标评估与结果展示能有效支撑高分作业的完成。1. 在 MATLAB 里同时跑传统图像去噪算法和 DnCNN先把两套路线的边界画清楚MATLAB 做图像去噪实验的最大优势是矩阵即图像传统图像去噪算法从均值滤波到小波阈值基本都收敛成几个工具箱函数的组合而基于深度卷积神经网络的 DnCNN 要落地到 MATLAB则需要把数据流、网络结构、训练选项和评价指标全部重新组织一遍。很多人在一份源码资料里看到这两套东西并排放着第一反应是想对比谁 PSNR 高但真正值得先做的是搞清楚它们的适用边界传统方法参数少、可解释、CPU 几秒出结果DnCNN 效果好、跨场景泛化强但需要 GPU、需要训练数据对噪声模型的假设同样敏感。下面按噪声建模—传统滤波器—DnCNN 结构—训练部署—质量验证的顺序展开代码基于 MATLAB 的 Image Processing Toolbox 和 Deep Learning Toolbox改一下数据路径就能直接运行。2. 传统图像去噪算法的 MATLAB 实现噪声建模、滤波函数与参数语义2.1 用 imnoise 和 randn 精确控制高斯噪声与椒盐噪声对比实验的第一道工序是让噪声可复现。常见的做法是用rng固定随机种子再按25/255这类行业惯例换算噪声强度。这里的 25 指灰度域 0255 下的标准差而 MATLAB 里 double 图像的数值范围是 01所以要除以 255 才是imnoise中真正要传的方差参数。rng(42); % 固定随机种子保证多次实验可复现 img im2double(imread(cameraman.tif)); % 经典灰度测试图 sigma 25 / 255; % 灰度域 sigma25 - [0,1] 域 gaussNoisy imnoise(img, gaussian, 0, sigma^2); % 均值0方差sigma^2 spNoisy imnoise(img, salt pepper, 0.05); % 5%像素被随机置0或1imnoise的高斯参数第三位是均值第四位是方差不是标准差这是最容易踩偏的地方salt pepper的第三个参数是噪声密度表示被污染像素占总像素的百分比。需要说明的是imnoise输出的噪声是就地生成的拿不到独立的噪声残差量所以后面训练 DnCNN 时要自己用randn生成噪声张量把干净图、噪声图、残差图保存成三元组。另一点是数据类型先im2double再加噪得到的仍是 double一旦中途有人转成 uint8噪声会被量化截断残差图就不再是零均值白噪声了。2.2 空间域滤波均值、高斯、中值和维纳的参数怎么定空间域滤波是传统方法的主干。MATLAB 里均值和高斯滤波用imfilter配合fspecial生成的卷积核中值滤波直接用medfilt2维纳滤波有自带的wiener2图像处理工具箱全部覆盖。下面这段代码把五种常见滤波器一次性跑完全部作用在同一张加了 sigma25 高斯噪声的图上avgKernel fspecial(average, [3 3]); % 3x3 均值核 avgOut imfilter(gaussNoisy, avgKernel, replicate); gKernel fspecial(gaussian, [5 5], 1.5); % 5x5 核sigma1.5 gaussOut imfilter(gaussNoisy, gKernel, replicate); gaussOut2 imgaussfilt(gaussNoisy, 1.5); % 上面两行的等价写法 medOut medfilt2(gaussNoisy, [3 3]); % 3x3 中值窗口 wienerOut wiener2(gaussNoisy, [5 5]); % 5x5 局部窗口维纳滤波imfilter的第三个参数replicate指定边界填充方式表示边缘像素向外复制比默认的零填充少了黑边伪影。fspecial(gaussian, [5 5], 1.5)里的 1.5 是高斯核标准差核尺寸与标准差要配套经验上取2*ceil(2*sigma)1左右的核宽。中值滤波对椒盐噪声极有效但窗口从 3 加到 5 之后细线、角点这类结构信息会明显丢失。维纳滤波假设噪声与信号独立且噪声方差已知它对平坦区做较强平滑对纹理区自动降低平滑强度在低照度图像上比固定核滤波更体面。我把这五个函数的适用场景和关键参数列成一张表做方法对比时按表取参、按表扫参方法MATLAB 函数关键参数擅长噪声主要问题均值滤波imfilterfspecial(average)核大小 3/5高斯边缘全部糊掉高斯滤波imgaussfilt/fspecial(gaussian)sigma 12高斯对边缘和纹理同等模糊中值滤波medfilt2窗口 3/5椒盐窗大后细节破碎维纳滤波wiener2局部窗口 37高斯纹理强区域效果有限导向滤波imguidedfilterNeighborhoodSize, DegreeOfSmoothing高斯强边缘会出现光晕表里的关键参数其实都是可以扫的不确定时先固定噪声种子把参数按 1、3、5 三个档位扫一遍用 PSNR 选档比凭感觉定参可靠得多。MATLAB 的psnr和ssim都属于 Image Processing Toolbox第一个参数传恢复图第二个传干净参考图即可。2.3 变换域与非局部方法小波阈值、非局部均值以及 BM3D 的位置空间滤波只利用局部邻域信息而噪声是全局叠加在图像上的于是有了两类更强的传统思路一是把图像变换到小波域让信号能量集中在少数大系数上、噪声均匀散布在小系数上阈值掉小系数再反变换回去二是突破邻域限制在全图范围内找相似块做加权平均这就是非局部均值。% 小波阈值去噪ddencmp 自动算阈值wdencmp 做全局阈值收缩 [thr, sorh, keepapp] ddencmp(den, wv, gaussNoisy); waveOut wdencmp(gbl, gaussNoisy, sym8, 4, thr, sorh, keepapp); % 新版封装wdenoise2 自动选阈值规则适合高斯噪声 waveOut2 wdenoise2(gaussNoisy, 4, Wavelet, sym8, ... DenoisingMethod, SURE, ThresholdRule, Soft); % 非局部均值以块为单位计算相似度加权 nlmOut imnlmfilt(gaussNoisy, DegreeOfSmoothing, sigma, ... SearchWindowSize, 21, ComparisonWindowSize, 5);wdencmp里的sym8是小波基4 是分解层数sorh取s表示软阈值软阈值对平稳信号更稳硬阈值保留更多边缘但也更容易残留振铃。wdenoise2的SURE是无偏风险估计在噪声标准差未知时能自适应选阈值比统一阈值更稳。imnlmfilt的三个参数中DegreeOfSmoothing直接控制平滑强度一般按噪声标准差同量级取值并在 0.010.1 之间微调SearchWindowSize是搜索窗口ComparisonWindowSize是块大小窗口越大计算越慢。提示BM3D 至今仍是传统方法里 PSNR 的上界但 MATLAB 没有内置实现需要额外下载作者发布的工具箱不在本方案的基础依赖范围内。真要放进对比表把它当作传统方法的天花板即可。传统方法到这里就齐了。它们的共同弱点是滤波器参数和噪声模型是绑定的换一张噪声特性不同的图参数就要重新调。DnCNN 想解决的就是这个参数绑定问题——把去噪重构成一个端到端的回归任务。3. DnCNN 网络结构拆解17 层卷积、残差学习与噪声标准差3.1 从浅层滤波器到深度卷积神经网络35×35 感受野是怎么算出来的DnCNN 的核心假设是去噪不该由人指定核参数而应该由数据学出一组逐层非线性变换。网络主体由 17 层卷积构成每层 64 个 3×3 卷积核卷积之后紧跟批归一化和 ReLU最后一层只做卷积不激活输出一个单通道残差图。为什么没有池化层因为去噪任务要求输入输出尺寸一致池化会丢失像素级位置信息DnCNN 全程用Padding, 1保持特征图尺寸不变。感受野按公式1 L*(k-1)计算L 是层数k 是核宽17 层 3×3 卷积就是1 17*2 35即每预测一个像素网络实际上看到了它周围 35×35 范围内的信息。之所以选 3×3 而不是 5×5是因为两层 3×3 的感受野相当于一层 5×5但参数量只有2*9 18对25且中间多一次非线性变换表达力更强。也就是说17 层这个数字不是拍脑袋定的更浅的网络比如 7 层感受野不足去噪后残存结构噪声更深的网络收益递减训练难度反而上升。在中等噪声水平sigma25下17 层是 PSNR 和训练成本的一个常见平衡点。3.2 残差学习与批归一化为什么网络输出的是一张噪声图DnCNN 与普通端到端去噪网络最大的区别是它训练时让网络学残差映射R(x) ≈ noise推理时再算denoised x - R(x)而不是直接让网络输出干净图。原因有两层。第一干净图像的像素值分布复杂、结构性强直接回归到它相当于同时学 identity 和多尺度细节优化困难而高斯噪声近似零均值随机场分布简单、集中在 0 附近回归残差的损失面平滑得多。第二配合残差学习批归一化把每一层输入重新拉回零均值单位方差对找噪声这种逐像素回归任务收敛极快——这也是 DnCNN 相比 VDSR 这类早期深度去噪网络的关键改进。逐层配置如下表所示这个表也是后面 MATLAB 建图的直接依据网络区段层数配置输出通道作用第一层1Conv 3×3 ReLU64提取底层特征中间层15Conv 3×3 BN ReLU64特征变换与噪声分离最后一层1Conv 3×31输出残差图注意中间层的顺序必须是卷积 → BN → ReLUBN 放在激活之前。如果在实现时把 BN 放到 ReLU 后面训练曲线会明显震荡ReLU 之后的输出分布被截断了再去归一化一个非对称分布等于白做。另外 BN 在训练时用当前 batch 的统计量推理时才用滑动平均的全局统计量所以训练 batch 不能太小常见下限是 64我一般取 128。用 MATLAB 的layerGraph把上述结构落地代码比想象中的短因为每一层的配置高度重复patchSize 40; numFilters 64; numLayers 17; layers imageInputLayer([patchSize patchSize 1], Name, input, ... Normalization, none); layers [layers; convolution2dLayer(3, numFilters, Padding, 1, ... WeightsInitializer, he); reluLayer()]; % 中间 15 层Conv BN ReLU for k 2:numLayers-1 layers [layers; convolution2dLayer(3, numFilters, Padding, 1, ... WeightsInitializer, he); batchNormalizationLayer(); reluLayer()]; end % 最后一层单通道输出不加 ReLU layers [layers; convolution2dLayer(3, 1, Padding, 1, ... WeightsInitializer, he); regressionLayer()]; lgraph layerGraph(layers);WeightsInitializer选he是为 ReLU 配套的初始化方式如果用默认的 Glorot深层网络在前几个 epoch 会明显收敛更慢。Normalization, none表示不在输入层做额外标准化因为图像加噪后的值域已经在 [0,1]且残差监督需要保留噪声幅值信息输入层再做归一化会把噪声幅度 scale 掉影响回归目标的量纲。3.3 盲去噪与非盲去噪sigma 如何进入训练流程DnCNN 原文同时给出了两种训练策略非盲模式针对固定 sigma 训练一个模型盲模式把 sigma 看作一个区间内的随机变量训练时每次随机抽一个 sigma 给样本加噪让一个模型覆盖整个区间。用 MATLAB 复现时我的做法是先分别训 sigma15、25、50 三个非盲模型用来做同噪声水平对比再做盲模型时在读取样本的 transform 函数里把 sigma 设成15 40*rand这样同一套数据流就能覆盖两个任务。这里有个容易误解的点sigma 并不是网络的一个输入参数而是隐含在训练数据分布里的。非盲模型推理时不需要告诉网络这张图是 sigma25 加的噪它只是对那一类噪声响应最好盲模型则等价于在更大先验集合上的最优解。后续的 FFDNet、CBDNet以及处理真实相机噪声的 LANLearning to Adapt Noise等思路都是在训练时噪声分布和推理时真实噪声不一致这个痛点上做文章。做对比实验时如果测试图的噪声不是高斯白噪声那么高斯假设带来的偏差会比模型能力差异更大这一点在分析结果时务必先讲清楚。4. 用 MATLAB 端到端训练并部署 DnCNN数据流、训练选项与推理评估4.1 训练数据流从干净图像到带噪输入 干净响应的配对样本训练的第一步是把图像切成小块并实时加噪生成配对样本。常见做法是用randomPatchExtractionDatastore做随机裁剪再用transform在读取时注入高斯噪声。为什么要在线加噪而不是提前存一批带噪图因为在线加噪相当于无限数据增强每个 epoch 看到的噪声实例都不同能显著缓解过拟合也节省磁盘空间。% 1. 读入干净灰度图 cleanDs imageDatastore(data/train, ReadFcn, readGray, ... FileExtensions, {.png, .jpg, .bmp}); function img readGray(f) img im2double(imread(f)); if size(img, 3) 3 img rgb2gray(img); end end % 2. 随机裁剪 40x40 patch同一位置同时作用于配对的两份数据 patchDs randomPatchExtractionDatastore(cleanDs, cleanDs, ... PatchSize, [40 40], PatchesPerImage, 32); % 3. 在线加噪输入变成噪声图响应保持干净图 sigma 25 / 255; trainDs transform(patchDs, (data) addNoise(data, sigma)); function data addNoise(data, sigma) clean data{2}; % 取配对中的干净 patch noisy clean sigma * randn(size(clean)); data {noisy, clean}; % {输入, 响应} endrandomPatchExtractionDatastore(cleanDs, cleanDs, ...)的两个数据源都填同一批干净图表示输入和响应来自同一张图工具箱会保证裁剪位置一致。transform的匿名函数接收一个 cell第一个元素是输入 patch、第二个是响应 patch这里把第二个取出来加噪后重新打包数据流就变成了标准的回归任务配对。训练集规模上20 张图即可训出能跑通的模型但想逼近论文效果建议准备 100 张以上的干净灰度图训练集中图的种类越杂跨场景泛化越好。4.2 训练选项与超参设置学习率、batch 和 epoch 怎么配合DnCNN 的训练设置本身不复杂关键是学习率调度和 batch 大小要和 BN 配合。我的基准配置如下options trainingOptions(sgdm, ... InitialLearnRate, 0.01, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.1, ... LearnRateDropPeriod, 30, ... MiniBatchSize, 128, ... MaxEpochs, 60, ... L2Regularization, 0.0001, ... Shuffle, every-epoch, ... ExecutionEnvironment, gpu, ... Plots, training-progress); net trainNetwork(trainDs, lgraph, options);几个关键参数的解释和调整方向超参数基准值调整方向InitialLearnRate0.01收敛慢时升到 0.03损失震荡时降到 0.003MiniBatchSize128显存不足降为 64但不要低于 32否则 BN 统计量不稳LearnRateDropPeriod30每 30 个 epoch 学习率衰减 10 倍用于末期精调MaxEpochs60小数据集 40 轮即可收敛看训练曲线不再下降就提前停L2Regularization0.0001权重衰减等价于 DnCNN 原文的 weight decay提示如果只有 CPU 环境把ExecutionEnvironment改为auto并把MiniBatchSize降到 3240×40 patch 下单 epoch 耗时大约增加一个数量级训练 20 轮仍能得到可用模型只是效果比 GPU 完整训练差一些。训练过程主要看两点一是训练 loss 是否单调下降并最终稳定下来sigma 越大 loss 量级越大要在同一 sigma 下比较二是验证集 PSNR 是否持续上升。如果 loss 出现周期性的突然跳高多半是LearnRateDropFactor衰减幅度过大把 0.1 改成 0.3 会平滑很多。4.3 推理、模型保存与 PSNR/SSIM 评估训练完成后把模型当成一个普通函数来用。卷积网络对输入尺寸没有限制训练时用 40×40 patch推理时可以直接输入整张图。下面是最小推理脚本同时给出了严格按 DnCNN 残差公式求结果的写法testImg im2double(imread(data/test/lena.png)); if size(testImg, 3) 3 testImg rgb2gray(testImg); end noisy testImg (25/255) * randn(size(testImg)); % 重新加噪 noisy reshape(noisy, [size(noisy) 1]); % 补通道维 % 方案A响应是干净图的模型predict 输出就是去噪结果 denoised predict(net, noisy); % 方案B响应是残差的模型去噪结果要自己做一次减法 % residual predict(net, noisy); % denoised noisy - residual; % 评估指标 fprintf(PSNR %.2f dB, SSIM %.4f\n, ... psnr(denoised, testImg), ssim(denoised, testImg)); % 保存与导出 save(dncnn_sigma25.mat, net); exportONNXNetwork(net, dncnn_sigma25.onnx); % 导出 ONNX 便于其他框架部署这里有两个高频坑。第一个是通道维predict要求输入是 H×W×C 的三维张量测试图经过rgb2gray后是二维矩阵要用reshape补成 H×W×1否则报维度错误。第二个是数据类型predict对 double 输入也能推理但如果直接传 uint8 图输出会按 0255 量纲解释和训练时的 [0,1] 量纲不一致导致结果整个偏暗。统一在入口im2double最省心。对比实验的评估协议我一般这样定固定测试集、固定随机种子、固定噪声生成方式把所有传统方法和 DnCNN 放进同一循环逐图计算 PSNR 和 SSIM 后取平均。PSNR 对像素级误差敏感SSIM 对结构保真敏感两个指标要一起看——有时候 PSNR 高 0.3dB 但 SSIM 反而下降说明网络在压误差和保结构之间产生了新的 trade-off。5. 用残差图和噪声标准差给 DnCNN 去噪结果做体检训练完 DnCNN先别急着说比维纳好。花五分钟看两样东西残差图长什么样估计出的噪声标准差和加噪 sigma 差多少。这两个检查能暴露最常见的两种病欠去噪和过度平滑。residual noisy - denoised; % 带噪图减去年去噪结果 imshow(residual, []); % 显示并自动拉伸对比度 [~, ~, ~, cD] dwt2(noisy, db1); % 一级小波对角细节子带 sigmaEst median(abs(cD(:))) / 0.6745; % Donoho MAD 估计残差图如果还看得到边缘或纹理轮廓说明欠去噪靠近边缘的噪声没被分离干净先检查训练 sigma 和测试噪声是否匹配。残差图如果大片接近纯黑而不是均匀的灰色颗粒说明模型把细节一起抹掉了这通常表现为 PSNR 尚可但 SSIM 明显下降。残差里出现规则的棋盘格或条状伪影则多半是网络结构里混入了上采样层——DnCNN 全程带 Padding 的卷积不会产生这种情况改过结构就要重点排查。dwt2的一级对角细节子带 cD 在纯高斯噪声下近似零均值正态分布median(abs(cD(:))) / 0.6745是 Donoho 提出的鲁棒噪声标准差估计。把这个估计值和加噪 sigma 对比偏差超过 15% 说明噪声模型已经变了比如混入 JPEG 压缩噪声或椒盐噪声此时固定 sigma 的非盲模型和传统方法都会失真应该换盲模型或 LAN 这类噪声自适应方法。批量对比时把 sigma、PSNR、SSIM、sigmaEst 四列合成一张表输出同一数据集、同一噪声种子跑完残差图序列基本就能给每类方法下结论。如果在残差图上看到结构残留先不要怀疑模型用 sigmaEst 对一下噪声档位估计值和训练 sigma 对不上就回到第 4 章的 transform 函数里把加噪区间改成估计值覆盖的范围重新训练——改训练数据分布永远比改网络结构先一步。本文还有配套的精品资源点击获取