简介:这份资源是面向深度学习与图像处理学习者的卷积神经网络图像去噪完整实践包,适合具备Python基础、希望理解CNN如何从含噪图像中恢复清晰画面的初学者与开发者。压缩包共428个文件,约60.97MB,以412张png图像构成训练与测试样本,另含5个h5模型权重、3个py训练脚本、4个xml配置及txt说明等,覆盖数据、模型与代码多个环节。资源围绕ImageDenoiseCnn展开,包含训练集与测试集、CNN网络结构定义、损失函数与优化器选择、训练脚本以及PSNR、SSIM等评估指标,可帮助读者完整走通从加噪数据到去噪输出的流程,并理解卷积层、池化层、激活函数与全连接层的作用。已有374人学习关注,可作为课程设计、毕业项目或图像处理入门练手的基础,也便于在此基础上调整网络结构与学习率策略,进一步优化去噪效果。
1. 卷积神经网络做图像去噪:从含噪图到干净图的端到端方案
手里有一批带噪点的图,想用卷积神经网络训练一个去噪模型,但卡在数据集怎么组织、网络怎么搭、训练完怎么验证这三个环节上——这是很多做图像去噪的工程师真实的状态。这个标题指向的就是一套完整的端到端方案:用卷积神经网络学习含噪图像到干净图像的映射,配套训练集和测试集,跑通从数据准备到模型推理的全流程。它解决的核心问题是传统去噪方法(比如小波变换图像去噪、非局部均值)在复杂噪声分布下泛化差、参数难调的问题。适合谁看:有基本深度学习卷积神经网络概念、手头有图像数据、想快速搭出一个能用的去噪模型的从业者。下面按数据、网络、训练、验证、避坑的顺序拆开讲。
2. 训练集和测试集怎么组织:从原始图到噪声对
2.1 去噪任务的数据集本质是配对样本
图像去噪和分类任务不一样,分类只需要图片和标签,去噪需要的是「同一场景的干净图和含噪图」这一对。训练集的作用是让网络学会从含噪图还原干净图,测试集的作用是评估它在没见过的噪声上表现如何。常见做法是拿一批高质量干净图作为基准,然后在训练时在线加噪,而不是提前把含噪图存成文件。这样做的好处是每个 epoch 看到的噪声样本都不同,等效于数据增强,能显著降低过拟合。
如果你拿到的压缩包里已经分好了训练集和测试集,先确认两件事:训练集和测试集是否来自不同场景(避免同场景泄漏导致测试指标虚高),以及图片是否已经是配对的含噪/干净对。如果是配对好的,直接按下面的目录结构整理;如果只有干净图,就需要自己写加噪脚本。
2.2 目录结构与数据加载代码
我一般会把数据整理成这样的结构,训练集和测试集各自独立:
dataset/ ├── train/ │ ├── clean/ # 干净图,训练时在线加噪 │ └── noisy/ # 可选,如果已有固定噪声图 └── test/ ├── clean/ └── noisy/ # 测试集建议用固定噪声,保证指标可复现对应的 PyTorch 数据加载代码:
import os import random import numpy as np from PIL import Image import torch from torch.utils.data import Dataset, DataLoader class DenoiseDataset(Dataset): def __init__(self, clean_dir, noise_level=25, patch_size=128, training=True): self.clean_dir = clean_dir self.noise_level = noise_level # 高斯噪声标准差,常用15/25/50 self.patch_size = patch_size # 随机裁剪尺寸,控制显存 self.training = training self.files = [f for f in os.listdir(clean_dir) if f.lower().endswith(('.png', '.jpg', '.bmp'))] def __len__(self): return len(self.files) def __getitem__(self, idx): img = Image.open(os.path.join(self.clean_dir, self.files[idx])).convert('RGB') img = np.array(img).astype(np.float32) / 255.0 if self.training: # 随机裁剪,增加样本多样性 h, w, _ = img.shape top = random.randint(0, h - self.patch_size) left = random.randint(0, w - self.patch_size) img = img[top:top+self.patch_size, left:left+self.patch_size] # 在线加高斯噪声 noise = np.random.normal(0, self.noise_level/255.0, img.shape).astype(np.float32) noisy = np.clip(img + noise, 0.0, 1.0) # 转成 CHW 格式 clean_t = torch.from_numpy(img.transpose(2, 0, 1)) noisy_t = torch.from_numpy(noisy.transpose(2, 0, 1)) return noisy_t, clean_t train_set = DenoiseDataset('dataset/train/clean', noise_level=25, training=True) test_set = DenoiseDataset('dataset/test/clean', noise_level=25, training=False) train_loader = DataLoader(train_set, batch_size=16, shuffle=True, num_workers=4) test_loader = DataLoader(test_set, batch_size=1, shuffle=False)逻辑说明:训练时随机裁剪 patch 是为了控制显存并增加样本多样性,测试时用整图评估才反映真实推理效果。噪声标准差 noise_level 是核心参数,15 对应低噪、25 中等、50 高噪,训练和测试必须用同一个值,否则指标没有意义。batch_size 设 16 是 8GB 显存下的稳妥值,显存大可加到 32。num_workers 根据 CPU 核数调整,一般设成核数的一半。
提示:测试集的噪声建议固定随机种子生成一次后存盘,否则每次评估噪声不同,指标波动会让你误以为模型不稳定。
2.3 数据质量比数量更影响最终效果
去噪模型对训练数据的干净程度极其敏感。如果所谓的「干净图」本身带有压缩伪影或轻微噪声,网络会把这些伪影也当成要保留的内容,去噪结果就会出现残留纹理。我踩过的坑是拿网上下载的 JPEG 图当干净图,训练出来的模型在平坦区域总是有块状痕迹,排查了很久才发现是 JPEG 压缩伪影被学进去了。所以训练集的干净图优先用 PNG 无损格式,或者用高质量相机 RAW 转出的图。数量上,几千张 128×128 的 patch 就能训出一个可用的模型,不必追求几十万张,质量优先。
3. 去噪网络怎么搭:从 DnCNN 到残差学习的选型逻辑
3.1 为什么去噪网络偏爱残差学习
直接让网络输出去噪后的图,等于让它从零重建图像内容,难度大。残差学习的思路是让网络只预测噪声,然后用含噪图减去预测噪声得到干净图。这样网络的学习目标变成了噪声分布,而噪声通常比图像内容简单得多,收敛更快、效果更好。DnCNN 就是这个思路的经典实现,结构不复杂但效果扎实,适合作为第一个跑通的基线模型。
网络结构上,DnCNN 用堆叠的卷积层加 BatchNorm 加 ReLU,最后一层只输出单通道或三通道的噪声估计。层数一般 17 层,感受野约 35×35,对常见噪声尺度够用。卷积核统一 3×3,通道数 64。这个配置在去噪任务里是经过大量验证的,不需要一上来就追求更深或更花哨的结构。
3.2 DnCNN 的 PyTorch 实现
import torch.nn as nn class DnCNN(nn.Module): def __init__(self, channels=3, num_layers=17, features=64): super(DnCNN, self).__init__() layers = [] # 第一层:输入到特征空间 layers.append(nn.Conv2d(channels, features, kernel_size=3, padding=1, bias=False)) layers.append(nn.ReLU(inplace=True)) # 中间层:特征提取,带 BatchNorm for _ in range(num_layers - 2): layers.append(nn.Conv2d(features, features, kernel_size=3, padding=1, bias=False)) layers.append(nn.BatchNorm2d(features)) layers.append(nn.ReLU(inplace=True)) # 最后一层:输出噪声估计 layers.append(nn.Conv2d(features, channels, kernel_size=3, padding=1, bias=False)) self.dncnn = nn.Sequential(*layers) def forward(self, x): noise = self.dncnn(x) # 预测噪声 return x - noise # 含噪图减去噪声 model = DnCNN(channels=3, num_layers=17, features=64)逻辑说明:forward 里返回的是 x - noise,这就是残差学习的体现。num_layers 控制深度,17 是原论文的默认值,减到 10 层也能用但感受野变小,对高噪效果下降。features 是每层通道数,64 是精度和显存的平衡点,降到 32 显存减半但 PSNR 会掉 0.3dB 左右。第一层和最后一层不加 BatchNorm,这是 DnCNN 的细节,加了反而影响噪声估计的精度。
注意:BatchNorm 在 batch_size 很小时统计量不稳定,如果你显存只够跑 batch_size=4,建议把 BatchNorm 换成 InstanceNorm 或者直接去掉,用 GroupNorm 替代。
3.3 损失函数和优化器的选择
去噪任务最常用的损失是 MSE(均方误差),它直接优化 PSNR 指标,收敛稳定。但 MSE 有个已知问题:训出来的图偏平滑,细节纹理容易丢。如果下游任务对纹理敏感,可以换成 L1 损失或者 MSE 和 SSIM 的加权组合。我一般先用 MSE 跑通基线,确认流程没问题后再换 L1 对比。
优化器用 Adam,学习率初始 1e-3,训练到后期用余弦退火降到 1e-5。权重衰减设 1e-4 防止过拟合。这些参数在去噪任务里比较通用,不需要精细调。
import torch.optim as optim criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)4. 训练流程与参数设置:让模型真正收敛
4.1 完整训练循环与关键参数
import torch from tqdm import tqdm device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DnCNN(channels=3).to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) EPOCHS = 50 best_psnr = 0.0 for epoch in range(EPOCHS): model.train() total_loss = 0.0 for noisy, clean in tqdm(train_loader, desc=f'Epoch {epoch}'): noisy, clean = noisy.to(device), clean.to(device) optimizer.zero_grad() output = model(noisy) loss = criterion(output, clean) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() avg_loss = total_loss / len(train_loader) print(f'Epoch {epoch} | Loss: {avg_loss:.6f} | LR: {scheduler.get_last_lr()[0]:.2e}') # 每 5 个 epoch 存一次权重 if (epoch + 1) % 5 == 0: torch.save(model.state_dict(), f'dncnn_epoch{epoch+1}.pth')逻辑说明:训练循环本身不复杂,关键在几个参数的配合。EPOCHS 设 50 是经验值,去噪任务通常 30 到 80 个 epoch 收敛,再多容易过拟合。学习率用余弦退火比固定学习率最终 PSNR 高约 0.2dB。每 5 个 epoch 存权重是为了防止训练中断后从头再来,这个后悔药一定要留。total_loss 累加后取平均,方便观察收敛趋势,如果 loss 震荡不降,先检查数据配对是否正确。
4.2 训练过程中的监控指标
光看 loss 不够,loss 下降不代表视觉效果好。训练时我一般同时监控三个量:训练 loss、验证集 PSNR、以及每隔几个 epoch 存一张验证图的去噪结果。PSNR 的计算要放在验证集上,用整图算,不要用 patch,因为 patch 边界的去噪效果和整图有差异。
import math def calculate_psnr(img1, img2): mse = torch.mean((img1 - img2) ** 2) if mse == 0: return float('inf') return 20 * math.log10(1.0 / math.sqrt(mse.item())) def validate(model, test_loader, device): model.eval() psnr_list = [] with torch.no_grad(): for noisy, clean in test_loader: noisy, clean = noisy.to(device), clean.to(device) output = model(noisy) psnr = calculate_psnr(output, clean) psnr_list.append(psnr) return sum(psnr_list) / len(psnr_list)逻辑说明:calculate_psnr 里像素值范围是 0 到 1,所以 log10 的分子是 1.0。如果你的数据没归一化到 0-1,这个公式要相应改成 255。validate 函数用 model.eval() 切换 BatchNorm 到推理模式,并用 torch.no_grad() 关闭梯度计算节省显存。测试时 batch_size 设 1 是因为整图尺寸不一致,逐张评估最稳妥。
4.3 学习率与 batch_size 的配合关系
这两个参数是联动的。学习率大了,batch_size 小的时候梯度噪声大,容易震荡;学习率小了,batch_size 大的时候收敛慢。经验公式是学习率和 batch_size 的平方根成正比。比如 batch_size=16 用 1e-3,那 batch_size=64 可以用 2e-3。但去噪任务对学习率不敏感,1e-3 到 1e-4 之间都能收敛,不用太纠结。真正影响大的是噪声水平:noise_level=50 时学习率要适当降低,否则早期梯度爆炸。
5. 避坑与排查:去噪模型训练中最容易翻车的五个点
5.1 现象:训练 loss 正常下降但输出全黑或全灰
原因:数据归一化不一致。训练时图片除以了 255,但验证或推理时忘了除,或者反过来。网络学到的映射关系被输入尺度破坏,输出就退化成常数图。
解决:把归一化操作写进 Dataset 类里,训练和推理共用同一个 Dataset 或同一套预处理函数。推理时单独写预处理的话,一定对照训练代码逐行检查。
5.2 现象:测试集 PSNR 很高但视觉上噪点还在
原因:测试集的噪声和训练集噪声分布不一致。比如训练用高斯噪声,测试图是真实相机噪声,两者统计特性不同。或者测试时噪声水平设错了,训练用 25 测试用 15。
解决:训练和测试的 noise_level 必须严格一致。如果要做盲去噪(不知道噪声水平),需要把不同噪声水平的图混在一起训练,或者用噪声估计子网络。真实噪声场景建议用 SIDD 这类真实噪声数据集,不要拿高斯噪声模型硬套。
5.3 现象:BatchNorm 导致小 batch 训练不稳定,loss 剧烈震荡
原因:BatchNorm 依赖 batch 内统计量,batch_size 小于 8 时均值和方差的估计噪声很大,反向传播时梯度不稳定。
解决:把 BatchNorm 换成 InstanceNorm 或 GroupNorm。GroupNorm 的 groups 设 8 或 16 比较稳。或者增大 batch_size,用梯度累积模拟大 batch。
5.4 现象:训练到后期 PSNR 不升反降
原因:过拟合。训练集和测试集场景太接近,网络记住了训练样本的噪声模式,换到测试集就失效。或者学习率没有衰减,后期在最优解附近震荡。
解决:加数据增强(随机翻转、旋转),加权重衰减,用余弦退火或 StepLR 衰减学习率。如果训练集和测试集来自同一批图,务必按场景划分而不是随机划分。
5.5 现象:推理时显存溢出,但训练时正常
原因:推理时输入是整图,尺寸远大于训练的 128×128 patch,中间特征图显存占用随尺寸平方增长。
解决:推理时用滑动窗口分块处理,块大小设 256×256,块之间留 16 像素重叠,最后拼接。或者用 torch.cuda.empty_cache() 清理缓存。如果模型部署到边缘设备,考虑把通道数从 64 降到 32。
6. 验证与进阶:用 PSNR/SSIM 双指标和真实噪声微调收尾
训练完模型,验证不能只看 PSNR 一个数。PSNR 高不代表视觉好,SSIM 衡量结构相似性,两者结合才靠谱。我一般会跑一个对比表,把不同噪声水平下的 PSNR 和 SSIM 都列出来,同时存几张典型图的去噪结果肉眼确认。
| 噪声水平 | PSNR (dB) | SSIM | 说明 |
|---|---|---|---|
| 15 | 32.5 | 0.91 | 低噪,细节保留好 |
| 25 | 29.8 | 0.85 | 中等噪声,通用场景 |
| 50 | 26.2 | 0.74 | 高噪,纹理有平滑 |
SSIM 的计算可以直接用 skimage 库:
from skimage.metrics import structural_similarity as ssim import numpy as np def calculate_ssim(img1, img2): # img1, img2 是 HWC 格式的 numpy 数组,范围 0-1 img1 = img1.transpose(1, 2, 0) img2 = img2.transpose(1, 2, 0) return ssim(img1, img2, channel_axis=2, data_range=1.0)逻辑说明:channel_axis=2 指定通道维度,data_range=1.0 对应归一化后的像素范围。SSIM 对亮度变化和结构变化都敏感,比 PSNR 更贴近人眼判断。
进阶方向有两个。一是用真实噪声数据微调:拿高斯噪声预训练的模型,在真实噪声对上用很小的学习率(1e-5)跑几个 epoch,PSNR 通常能再涨 0.5 到 1dB。二是把 DnCNN 换成带注意力机制的残差网络,比如在残差块里加通道注意力,对纹理丰富的数据集提升明显,但参数量翻倍,推理速度会降。选哪个取决于你的场景是追求极致指标还是追求推理速度。
我自己的习惯是:任何去噪模型上线前,一定拿一批真实场景的含噪图跑一遍,不看指标只看图。指标是给论文看的,图是给用户看的。有次 PSNR 刷到 31dB 的模型,实际跑在手机拍的夜景图上,暗部噪点去干净了但灯光边缘出现了彩色伪影,最后回退到 PSNR 只有 29dB 但视觉更干净的版本。这个教训让我每次都会留一组真实图做最终验证,不迷信测试集数字。希望帮到你。
本文还有配套的精品资源,点击获取