Matlab实践CNN图像去噪:残差学习与PSNR评估仿真
简介:一套面向图像处理与深度学习初学者的卷积神经网络去噪仿真资源包,包含可直接运行的MATLAB脚本、训练好的模型数据、结构示意图和操作录屏,帮助读者从零搭建图像去噪流程。资源共6个文件,包括1份MATLAB脚本、1份模型数据、1段视频教程和3张示意图,整体仅8.35MB,轻量紧凑,便于快速下载和本地实践。目前已有240人学习使用,适合MATLAB课程设计、算法复现或入门深度学习图像处理的人群。内容围绕自编码器与U-Net两种常用网络展开,涵盖数据预处理、网络参数设计、模型训练以及峰值信噪比(PSNR)与结构相似度(SSIM)等指标评估;配套录屏演示了从加载数据到应用模型的完整操作,初学者可对照代码和视频边看边练,最终实现自己的去噪模型。
1. 用Matlab跑CNN图像去噪算法:这份仿真资源解决什么问题
做图像去噪仿真,最怕的不是算法看不懂,而是环境配不通、数据喂错、训练出一堆NaN还不知道哪里翻车。这份Matlab资源把基于CNN的图像去噪算法完整落成仿真工程,并附带教程,核心价值在于:你不用重新造轮子,直接在Matlab里把高斯加噪、网络训练、PSNR评估整条链路跑通。适合作课程设计、毕业设计或图像处理大作业的方向,也适合想快速对比DnCNN与传统去噪算法效果的工程师。要拿到结果,先理解这套仿真背后的网络结构和数据处理逻辑。
2. 高斯噪声与残差学习:先把去噪模型的结构选型搞清楚
2.1 噪声模型与加噪数据是怎么来的
CNN去噪任务说白了就是输入一张被高斯白噪声污染的图像,输出一张尽量接近原图的干净图。仿真里用的噪声一般假设为 y = x + n,其中x是干净图像,n是均值为0、标准差为σ的高斯噪声。σ是唯一的关键参数,仿真里常用σ=15/25/50三档,对应轻度、中度、重度噪声。加噪数据在Matlab里一行就能生成:
noise = sigma / 255 * randn(size(I)); % 生成高斯白噪声 I_noisy = I + noise;注意这里sigma/255是把σ从像素灰度值转到[0,1]归一化空间。如果图像直接用im2double处理过,噪声标准差也要对应换算,否则训练数据和测试数据噪声水平不一致,模型效果会打折扣。这块的常见误用是先把噪声加在uint8数据上,再转double,结果归一化后噪声幅度全变了。我一般会在每个脚本开头固定好数据流水线:读图→im2double→加噪→存储,三步顺序不变。
2.2 残差学习为什么比直接学干净图更稳
仿真里的网络结构通常沿用DnCNN的思路。DnCNN核心不是直接回归干净图像,而是让网络去预测噪声残差,最后用加噪图减去残差恢复干净图。为什么要绕这一圈?自然图像本身有复杂的纹理和结构,直接回归干净图需要网络记忆大量图像先验,训练难度大;而噪声残差在统计上是近似零均值的随机场,分布更简单,网络更容易拟合。从优化角度看,残差学习的损失面更平缓,训练更容易收敛,PSNR也更高。
这个资源里的网络层定义,常见版本是一个三到五层卷积的简化网络:
layers = [ imageInputLayer([patchSize patchSize 1], 'Name', 'input', 'Normalization', 'none') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv1') reluLayer('Name', 'relu1') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') convolution2dLayer(3, 1, 'Padding', 'same', 'Name', 'conv_out') regressionLayer('Name', 'mse') ];imageInputLayer的'Normalization'必须设成'none',因为图像已经手动做了[0,1]归一化,不要让工具箱再减均值除方差。卷积核选3×3,Padding设'same'是为了让特征图尺寸不变,网络输出和输入保持同一分辨率,这对图像去噪任务至关重要。最后一层输出通道数是1,对应预测的噪声残差。regressionLayer是回归损失,训练目标是残差图而不是干净图。
2.3 仿真资源里的网络结构与原版DnCNN的差异
原版DnCNN是17层,每层64个3×3卷积核,深网络感受野大,去噪能力强。但17层在个人电脑上用trainNetwork训练,显存和耗时都够呛。这种教程型仿真资源里,网络往往是简化版,层数在3到5层之间,通道数也控制在64以内,它的意义在于演示原理而不是刷榜。把层数从3层加到17层,训练时间不是线性增长而是近似指数级上涨,而且BN层对batch size敏感,batch设太小反而容易崩。
所以拿到资源先看网络定义部分,确认到底几层,再决定用多大batch去训练。深网络并不一定在你那几张图的训练集上表现更好。数据量小的时候,3层网络比10层网络更稳,过拟合概率更低,这个取舍要在动手之前就想清楚。
3. 环境与数据集准备:Matlab深度学习工具箱的软硬件要求
3.1 训练之前先过三个检查
跑trainNetwork之前,有三个东西不检查会浪费时间。一是Matlab版本和工具箱,需要Deep Learning Toolbox和Parallel Computing Toolbox,前者提供trainNetwork和层定义,后者提供GPU加速。如果没有GPU,CPU也能训练,但要把MiniBatchSize调小,训练时间会明显拉长。二是显卡显存,一个40×40×64的中间特征图不占多少显存,但batch size一大,反向传播的中间变量会累积,显存不够时Matlab直接报错。三是训练数据格式,trainNetwork对数据维度要求非常死,输入必须是H×W×C×N,灰度图C=1。
3.2 训练数据生成:裁剪、加噪、归一化的顺序不能乱
真实训练集通常是自己准备的图像文件夹。仿真资源里一般会带一个数据生成脚本,核心步骤如下:
% 训练数据生成脚本(裁剪+加噪+归一化) srcFiles = dir(fullfile('trainData', '*.png')); patchSize = 40; % 裁剪块大小 stride = 10; % 滑动步长 sigma = 25; % 训练噪声等级 cleanPatches = []; % 干净块集合 noisyPatches = []; % 加噪块集合 for idx = 1:numel(srcFiles) img = imread(fullfile(srcFiles(idx).folder, srcFiles(idx).name)); if size(img, 3) == 3 img = rgb2gray(img); % 灰度化 end img = im2double(img); % 转[0,1] for i = 1:stride:size(img,1)-patchSize+1 for j = 1:stride:size(img,2)-patchSize+1 patch = img(i:i+patchSize-1, j:j+patchSize-1); cleanPatches = cat(3, cleanPatches, patch); noisyPatches = cat(3, noisyPatches, patch + sigma/255*randn(patchSize)); end end end % 调整为 [H W C N] 四维 cleanPatches = reshape(cleanPatches, patchSize, patchSize, 1, []); noisyPatches = reshape(noisyPatches, patchSize, patchSize, 1, []);这段代码有几个关键点。stride控制Patch之间的重叠程度,stride=10表示相邻patch有30像素重叠,数据量翻好几倍,但也会引入大量相似样本,训练容易过拟合到重复纹理。sigma=25是中度噪声等级,如果后面要测σ=15或50,最好在数据准备阶段把三档噪声都生成一份。加完噪声没有做clip,让少量像素超出[0,1],保持噪声统计特性,这是DnCNN训练时的常见做法,网络自己会学怎么处理边界值。
如果训练图像只有十几张,这种全量滑动裁剪会产生几十万个patch,全部存进内存会让普通台式机直接卡死。常见做法是加一个数量上限,比如每张图最多取200个patch,或者用matfile写入磁盘分批加载。代码里直接cat到内存是演示用,真实项目里我会加一个计数判断,超过上限就break。
3.3 验证集与测试集怎么划分
验证集不是从训练patch里随机抽的。因为同一张图的不同patch高度相似,随机抽会导致验证集和训练集来自同一张原图,验证曲线看起来很好,实际测试别的图就翻车。我一般按图像粒度切分:比如准备80张图,70张做训练,5张做验证,每张只取少量patch,剩下5张用完整原图做测试。测试集用原图而不是patch,是为了计算整图的PSNR,这样能反映真实去噪能力。仿真资源里的验证逻辑如果只是从patch池里随机抽,建议自己改成图像级切分,这一步能让后续评估可信得多。
提示:训练数据、验证数据、测试数据的归一化方法必须完全一致,否则模型在训练集上表现正常,一上测试图就崩。
4. 训练与评估全流程:从代码到PSNR曲线的每一步
4.1 训练主流程代码
数据准备好之后,训练主脚本是整个资源里最重要的部分。它的逻辑很直接:定义网络、配置trainingOptions、调用trainNetwork。注意第2章的层定义中,regressionLayer的回归目标在实际调用时要传残差图,即加噪图和干净图的差值,方向是“噪声”而不是“干净图”。
% 残差学习的训练目标:网络输出的是噪声 ≈ 加噪图 - 干净图 trainTargets = noisyPatches - cleanPatches; options = trainingOptions('sgdm', ... 'MiniBatchSize', 128, ... 'MaxEpochs', 50, ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.1, ... 'LearnRateDropPeriod', 20, ... 'Shuffle', 'every-epoch', ... 'L2Regularization', 1e-4, ... 'Verbose', true, ... 'Plots', 'training-progress', ... 'ExecutionEnvironment', 'gpu'); net = trainNetwork(noisyPatches, trainTargets, layers, options);这里输入是noisyPatches,输出目标是noisyPatches减去cleanPatches,也就是纯噪声残差。训练完成后推理时,用加噪图减去网络预测的噪声,就得到去噪结果,这个对应关系一定要在脚本里保持一致。InitialLearnRate设1e-3是SGD类优化器比较稳妥的起点,如果发现loss曲线前期下降极慢,可以试着调到5e-3,但超过这个值大概率训练曲线开始震荡。ExecutionEnvironment设'gpu'需要Parallel Computing Toolbox,没装就改为'cpu'。
4.2 核心训练参数详解
下面这个表是这套仿真里最值得调的参数,拿默认值先跑通一遍,再按自己的数据改。
| 参数 | 默认值 | 调整方向 | 典型问题 |
|---|---|---|---|
| MiniBatchSize | 128 | 显存不够就降到32或16;收敛慢就升到256 | batch过小时BN层统计不稳定 |
| InitialLearnRate | 1e-3 | 不收敛降到1e-4;太慢升到5e-3 | 过大会出现NaN或震荡 |
| MaxEpochs | 50 | 数据集小可以降;只看趋势设20 | epoch过多过拟合,loss降PSNR不升 |
| LearnRateDropPeriod | 20 | 配合'piecewise'设15~25 | drop太早模型没学够,太晚浪费轮次 |
| L2Regularization | 1e-4 | 过拟合时升到1e-3 | 过大模型欠拟合 |
MiniBatchSize是训练里最玄学的一个数。它决定每轮权重更新用到多少样本,影响梯度估计的稳定程度。在GPU上,batch size设成2的幂(32/64/128/256)能充分利用显存带宽,这也是默认值是128而不是100的原因。BN层对batch size很敏感,batch=16或更小时,BN的均值和方差估计噪声大,可能直接导致验证loss不降。InitialLearnRate则要看loss曲线走势来判断,如果前几个iteration的loss完全不降,先检查数据有没有归一化,再检查学习率是不是太小。
4.3 测试脚本:PSNR与SSIM计算
训练完成后,网络输出的是噪声残差,恢复干净图要做一次减法:
function [psnrVal, ssimVal] = evaluateDenoiser(net, noisyImg, cleanImg) % 输入noisyImg和cleanImg需要是[0,1]范围的double灰度图 % 尺寸 [H W] 需要先转成 [H W 1 1] inputTensor = reshape(noisyImg, size(noisyImg,1), size(noisyImg,2), 1, 1); predNoise = predict(net, inputTensor); predNoise = squeeze(predNoise); % 去掉单例维度 denoisedImg = noisyImg - predNoise; % 计算PSNR,图像已归一化到[0,1],分子用1 mseVal = mean((denoisedImg(:) - cleanImg(:)).^2); psnrVal = 10 * log10(1 / mseVal); ssimVal = ssim(denoisedImg, cleanImg); end注意predict和trainNetwork的调用接口有差异,predict需要的是4维输入,而且要带通道维和batch维。squeeze之后才能做减法。PSNR公式里分子是1,前提是图像已经归一化到[0,1],如果直接用uint8数据算,分子要改成255^2,很多人就在这里算错导致PSNR偏低十几个dB。SSIM直接用Matlab自带的ssim函数,它对数据范围有要求,传参前确保是[0,1]的double。
4.4 训练曲线的判读方法
trainNetwork训练时会弹出training-progress图,主要看两条曲线:训练loss和验证loss。两条曲线都下降且接近,说明训练正常。验证loss下降而训练loss不降,典型原因是学习率太低或网络太浅、拟合能力不足。训练loss下降但验证loss反弹,说明过拟合,优先增加数据、降低网络层数、加大L2正则。
最需要警惕的情况是loss曲线看起来在降,但最终PSNR却不高。这可能是因为回归目标方向不一致:训练时目标设成了干净图,测试时却仍然按残差方式用加噪图减去预测,等于把干净图当成噪声减掉,结果图像直接变暗、PSNR低到离谱。检查训练脚本里trainTargets到底定义成什么,这是整个资源里最容易翻车的地方之一。
5. 避坑:CNN去噪训练中的几个翻车现场
5.1 训练loss不降或直接出NaN
现象:训练开始后loss卡在某个值附近完全不下降,或者某一步直接变成NaN,后面所有iteration全是NaN。
原因:同类仿真里最常见的两个原因,一是imageInputLayer的'Normalization'没设成'none',工具箱把输入额外标准化,数据分布和预期不一致;二是InitialLearnRate设到0.01以上,梯度直接爆炸。还有一种隐蔽情况是数据里有Inf或NaN,比如裁剪时越界、某个patch是纯黑图像,都会让loss在几十次迭代后崩掉。
解决:先确认imageInputLayer的'Normalization'是'none';把InitialLearnRate降到1e-4跑20个iteration,如果恢复正常再逐步调回去。训练前用min(noisyPatches(:))、max(noisyPatches(:))和any(isnan(noisyPatches(:)))扫一遍数据,确认范围在有限数值内。这个检查习惯能救回大量白费时间的训练任务。
5.2 PSNR偏低:数据范围不一致
现象:训练曲线很好,loss降到很低,但每张测试图算出来的PSNR只有20dB上下,和论文里动辄30dB差一大截。
原因:训练时图像在[0,1]范围,测试时忘了归一化,用0到255的原图直接喂进网络,预测残差还在[0,1]量级上,恢复出来的图要么偏暗要么偏亮。另一种情况是PSNR公式里分子写255^2但图像在[0,1]范围,计算结果虚低。
解决:测试脚本开头统一加一行img = im2double(img),PSNR计算里分子用1。如果非要保持在0到255范围,分子用255^2,二选一,不要在两端各用一套。数据范围不一致这种问题在图像去噪里非常隐蔽,因为loss曲线完全正常,只有PSNR暴露问题。
5.3 显存溢出:batch size和patch size的联动
现象:训练跑到中途,Matlab直接报“Out of memory on device”,或者整个IDE卡死。
原因:MiniBatchSize=128、patchSize=50、网络64通道时,中间激活占显存是按倍数涨的,4GB显存的老卡上这个组合基本必炸。更隐蔽的原因是一次性把所有训练patch加载进内存,数据维度是[50 50 1 300000],光这个数组就几十GB,内存先于显存阵亡。
解决:把patchSize降到32或40,MiniBatchSize降到32,先在CPU上跑5个epoch确认逻辑通了再上GPU。大数据量场景用imageDatastore配合augmentedImageDatastore做流式读取,不要一次性load全量。patchSize从40改成32后网络感受野变小,去噪效果会有轻微下降,需要在清晰度和资源占用之间取舍。
5.4 验证集和训练集混在一起
现象:验证loss一直很低,但拿一张全新图片去测试,PSNR明显低于验证指标。
原因:验证patch是从所有训练图像的patch池里随机抽的,同一张原图的不同patch同时出现在训练和验证数据中,验证指标虚高。演示脚本图省事经常这么干,做课程设计时容易被这个假象误导。
解决:按图像文件粒度划分。40张图分成32张训练、4张验证、4张测试,验证和测试图像绝不进训练列表。在数据生成脚本里直接分开目录:trainData/、valData/、testData/,三个文件夹各出一份数据,从源头上隔离。
5.5 测试时边缘出现黑边或伪影
现象:去噪后图像四周出现深色边框,边缘区域特别模糊,中心区域效果正常。
原因:网络输入是固定尺寸patch,推理时对整张图卷积,Padding='same'在边缘补零,补零区域被网络当成真实像素预测,边缘残差估计不准。另外如果测试图像尺寸小于网络输入的patchSize,predict会直接报维度错误。
解决:测试时先用padarray给图像四周做反射填充,推理完成后再裁剪掉同等宽度:
padSize = 20; paddedImg = padarray(noisyImg, [padSize padSize], 'symmetric'); pred = predict(net, reshape(paddedImg, size(paddedImg,1), size(paddedImg,2), 1, 1)); pred = squeeze(pred); denoised = paddedImg - pred; denoised = denoised(padSize+1:end-padSize, padSize+1:end-padSize);'symmetric'反射填充比补零好得多,能让边缘区域也有足够的上下文信息,基本上能把黑边问题压下去。检查测试代码时优先看有没有这层填充处理,很多仿真资源里没写这步,导致边缘效果很差却不自知。
6. 进阶:把模型用到真实图像上,并看懂网络内部在干什么
6.1 真实照片去噪的预处理差异
训练用的是合成高斯噪声数据,但真实照片的噪声混合了传感器的高斯噪声、泊松噪声和坏点。用这套模型去处理真实照片时,不要直接把原图喂进去。常见做法是先对RGB三通道分别推理再合并,因为网络是单通道输入,逐通道处理比先转灰度再处理能保留更多色彩细节。如果照片是JPG格式,压缩噪声和传感器噪声叠在一起,建议先做一次轻量中值滤波降低异常值,再进CNN,否则个别坏点会被网络当成结构细节保留下来。
6.2 中间特征图可视化
想知道网络到底学到了什么,用activations可以拿到任意层的特征图:
feat = activations(net, reshape(noisyImg, [H, W, 1, 1]), 'relu2'); % feat尺寸 [H W 64 1],montage展示全部通道 montage(mat2gray(feat(:,:,:,1)), 'Size', [8 8]); title('relu2层输出特征图');这一步非常建议做完。如果特征图里能看到清晰的边缘和纹理方向,说明前面几层在提取局部结构;如果看到的基本是噪声图案,说明网络还在死记纹理,没有学到图像先验,继续加层数或加数据才有意义。特征图可视化是判断网络有没有真正“看懂”图像的最直接手段。
6.3 推理加速与模型落地
训练完保存的net对象直接predict速度一般,仿真阶段没必要强行上线部署。在消费级显卡上Matlab的GPU推理速度已经够用,一个立竿见影的小技巧是把预测代码包成函数后用GPU跑:
noisyImg = gpuArray(single(noisyImg)); pred = predict(net, reshape(noisyImg, [H, W, 1, 1])); pred = gather(pred);注意gpuArray要配single类型,double的GPU运算在消费级显卡上慢很多。从那以后我每次训练前都会先跑一遍数据范围检查、加一小块patch的快速推理,确认pred维度对得上再开始正式训练,这个习惯帮我省掉了大量反复调试的时间,希望帮到你。
本文还有配套的精品资源,点击获取