☰
CNN图像去噪实战:从训练集测试集构建到DnCNN模型调优
2026/10/9 23:57:48 网站建设 项目流程

简介:这份资源是面向深度学习与图像处理初学者及开发者的CNN图像去噪完整实践包,针对传感器噪声、传输干扰等导致图像质量下降的问题,提供从训练到评估的一站式解决方案。压缩包共428个文件,约60.97MB,以412张png图像构成训练与测试样本,另含5个h5模型权重、3个py训练脚本、4个xml配置及txt说明等,覆盖数据、模型与代码各环节。资源包含训练集与测试集、CNN模型架构、损失函数与优化器选择、训练脚本及PSNR、SSIM等评估指标,可帮助读者理解卷积层、池化层、激活函数与全连接层如何协同完成去噪,并掌握模型训练、验证与调参流程。目前已有374人学习,适合作为入门练手项目,也可作为进一步优化网络结构与学习率策略的复用基础。

1. 图像去噪这件事,为什么卷积神经网络成了默认选项

如果你手头有一批在暗光下拍的、或者高 ISO 硬拉出来的照片,放大一看全是彩色噪点和块状伪影,传统做法无非是高斯滤波、中值滤波、非局部均值(NLM)或者 BM3D。这些方法我都用过,BM3D 在标准测试图上确实能打,但一到真实场景就露馅:纹理被抹平、边缘发糊,参数还得逐张调。卷积神经网络(CNN)做图像去噪,核心思路是让网络从「带噪图 → 干净图」的成对数据里自己学一个映射函数,而不是靠人写死的滤波核。这个标题里的「包括训练集和测试集」,说明它不是一个只给推理脚本的 Demo,而是把数据准备、训练、评估这条链路都摆出来了——这对想复现的人来说才是真正有用的部分。适合谁?适合已经会写 PyTorch 基础训练循环、想搞明白去噪模型从数据到指标怎么闭环的工程师,也适合手里有自己拍的噪声图像、想训一个专用去噪器的人。下面我按「数据怎么造 → 网络怎么搭 → 训练怎么调 → 坑在哪」的顺序,把这条链路拆开讲。

2. 训练集和测试集怎么造:从干净图到成对样本

2.1 为什么去噪任务的数据必须是「成对」的

CNN 去噪是有监督学习,网络输入是带噪图,监督信号是同一场景的干净图。没有干净图,就只能走无监督或自监督路线(比如 Noise2Noise、Noise2Void),那是另一个话题。标题里明确带了训练集和测试集,说明走的是成对监督路线。成对数据的构造方式直接决定模型上限:如果你用合成噪声训练,模型学到的是你合成噪声的分布;拿它去处理真实相机噪声,效果会掉一截。常见做法是两条腿走路——合成噪声用来做大规模预训练和消融实验,真实成对数据(同一场景固定机位、低 ISO 长曝光当干净图、高 ISO 短曝光当带噪图)用来做微调。

合成噪声里最常用的是加性高斯白噪声(AWGN),因为它的数学性质干净、可控性强。但真实传感器噪声不是纯加性的,它包含泊松分量(光子散粒噪声)和读出噪声,所以更接近的模型是异方差高斯:噪声方差随像素亮度变化。我一般会同时准备 AWGN 和异方差高斯两套数据,前者验证网络结构,后者逼近真实。

2.2 用 Python 生成 AWGN 成对数据的最小脚本

import numpy as np import cv2 import os def add_awgn(img, sigma, rng): """img: float32 [0,1] 的干净图; sigma: 噪声标准差(0-255尺度)""" noise = rng.normal(0, sigma / 255.0, img.shape).astype(np.float32) noisy = np.clip(img + noise, 0.0, 1.0) return noisy def build_pairs(clean_dir, out_clean, out_noisy, sigma_list, patch=256, stride=128): rng = np.random.default_rng(42) os.makedirs(out_clean, exist_ok=True) os.makedirs(out_noisy, exist_ok=True) idx = 0 for name in os.listdir(clean_dir): path = os.path.join(clean_dir, name) img = cv2.imread(path, cv2.IMREAD_COLOR) if img is None: continue img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 h, w, _ = img.shape # 滑窗切 patch,边缘不足则丢弃 for y in range(0, h - patch + 1, stride): for x in range(0, w - patch + 1, stride): clean_patch = img[y:y+patch, x:x+patch] sigma = float(rng.choice(sigma_list)) noisy_patch = add_awgn(clean_patch, sigma, rng) cv2.imwrite(os.path.join(out_clean, f"{idx:06d}.png"), cv2.cvtColor((clean_patch*255).astype(np.uint8), cv2.COLOR_RGB2BGR)) cv2.imwrite(os.path.join(out_noisy, f"{idx:06d}.png"), cv2.cvtColor((noisy_patch*255).astype(np.uint8), cv2.COLOR_RGB2BGR)) idx += 1 print(f"生成 {idx} 对样本") if __name__ == "__main__": build_pairs("./clean_images", "./dataset/train/clean", "./dataset/train/noisy", sigma_list=[15, 25, 50], patch=256, stride=128)

这段脚本做了三件事:读干净图并归一化到 [0,1];按固定步长切 256×256 的 patch;对每个 patch 随机抽一个 sigma 加噪后分别存盘。参数说明:sigma_list控制噪声强度范围,训练时随机采样能让模型适应多个噪声水平,这叫盲去噪(blind denoising);patch取 256 是显存和感受野的折中,太小则上下文不足,太大则 batch size 上不去;stride取 patch 的一半,保证相邻 patch 有重叠,增加样本量同时减少边界效应。注意存盘时用 PNG 而不是 JPEG,JPEG 的有损压缩会引入额外伪影,污染监督信号。

2.3 训练集和测试集的划分原则

划分不能随机打散 patch,否则同一张原图的 patch 会同时出现在训练和测试里,造成数据泄漏,指标虚高。正确做法是按「原图」划分:先把干净图分成 train/val/test 三个不相交的集合,再各自切 patch。测试集我一般留 5~10 张有代表性的图,覆盖平滑区域、强纹理、暗部、高光四类场景。如果做真实噪声,测试集必须是真实成对数据,不能用合成噪声凑数,否则评估结果没有参考价值。

提示:合成噪声的 sigma 建议覆盖 15、25、50 三档,这是去噪文献里最常对比的噪声水平,方便和已有方法横向比较。

3. 网络结构选型:从 DnCNN 到残差密集连接

3.1 为什么去噪网络普遍预测「残差」而不是直接输出干净图

直接让网络输出干净图,等于让它从零重建每一个像素,任务难度大、收敛慢。去噪领域一个被反复验证的结论是:网络预测噪声残差(noisy - clean),推理时用输入减去预测残差得到干净图,效果更好。原因是噪声相对于图像内容是稀疏且高频的,残差映射的学习目标更简单,梯度也更稳定。DnCNN 就是靠这个思路加上 batch normalization 和残差学习,在 AWGN 去噪上打出了当时的最好成绩。我自己的经验是,哪怕换成更深的网络,残差学习这个设计也基本不要动。

3.2 一个可复现的 DnCNN 变体实现

import torch import torch.nn as nn class DnCNN(nn.Module): def __init__(self, channels=3, num_layers=17, features=64): super().__init__() layers = [] # 第一层:无 BN,保留原始噪声统计 layers.append(nn.Conv2d(channels, features, 3, padding=1, bias=False)) layers.append(nn.ReLU(inplace=True)) # 中间层:Conv + BN + ReLU for _ in range(num_layers - 2): layers.append(nn.Conv2d(features, features, 3, padding=1, bias=False)) layers.append(nn.BatchNorm2d(features)) layers.append(nn.ReLU(inplace=True)) # 最后一层:输出残差,无激活 layers.append(nn.Conv2d(features, channels, 3, padding=1, bias=False)) self.body = nn.Sequential(*layers) def forward(self, x): residual = self.body(x) return x - residual # 推理时直接得到干净图

结构上有几个点值得说清楚。第一层不加 BN,是因为 BN 会归一化输入,破坏噪声的绝对强度信息,而噪声强度恰恰是去噪需要感知的关键线索。中间层加 BN 是为了加速收敛、稳定训练。最后一层不加激活函数,因为残差可正可负,ReLU 会把负残差截断。num_layers=17、features=64是 DnCNN 的经典配置,参数量约 55 万,单张 256×256 图在主流显卡上推理只要几毫秒。如果你的数据是灰度图,把channels改成 1 即可。

3.3 什么时候该上更复杂的结构

DnCNN 在 AWGN 上够用,但遇到真实噪声、大尺寸图像或者需要保留极细纹理时,它的感受野和特征复用能力就不够了。这时候可以考虑三类改进:一是加宽加深(比如 20 层以上、128 通道),代价是显存和推理时间;二是引入密集连接或残差密集块(RDB),让浅层特征反复被复用,对纹理恢复帮助明显;三是用编码器-解码器结构(类似 U-Net),通过下采样扩大感受野、上采样恢复分辨率,适合噪声空间相关性强的场景。选型原则很简单:先跑通 DnCNN 拿到 baseline 指标,再逐项加改进,每次只动一个变量,看验证集 PSNR 有没有实质提升。盲目堆结构是最容易翻车的地方。

4. 训练循环与关键参数:把 PSNR 从 28 推到 32

4.1 损失函数选什么

去噪最常用的损失是 L2(MSE)和 L1。L2 对大误差惩罚重,优化稳定,但容易产生过度平滑的结果;L1 对异常值更鲁棒,恢复的纹理更锐利,但收敛慢一些。我的习惯是先用 L2 训到收敛,再用 L1 微调若干轮,兼顾稳定性和锐度。如果追求感知质量,可以加一个感知损失(用预训练分类网络提特征算距离),但要注意感知损失会拉低 PSNR,因为 PSNR 衡量的是像素级误差,和人的主观感受不完全一致。

4.2 一个完整的训练脚本骨架

import torch from torch.utils.data import DataLoader from dataset import PairedDenoiseDataset # 自定义 Dataset,返回 (noisy, clean) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = DnCNN(channels=3, num_layers=17, features=64).to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5) criterion = torch.nn.MSELoss() train_loader = DataLoader(PairedDenoiseDataset("./dataset/train"), batch_size=16, shuffle=True, num_workers=4) val_loader = DataLoader(PairedDenoiseDataset("./dataset/val"), batch_size=1, shuffle=False) best_psnr = 0.0 for epoch in range(80): model.train() for noisy, clean in train_loader: noisy, clean = noisy.to(device), clean.to(device) pred = model(noisy) loss = criterion(pred, clean) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() psnr_sum, count = 0.0, 0 with torch.no_grad(): for noisy, clean in val_loader: noisy, clean = noisy.to(device), clean.to(device) pred = model(noisy).clamp(0, 1) mse = torch.mean((pred - clean) ** 2).item() psnr = 10 * torch.log10(torch.tensor(1.0 / max(mse, 1e-10))).item() psnr_sum += psnr count += 1 avg_psnr = psnr_sum / count print(f"Epoch {epoch}: val PSNR = {avg_psnr:.2f} dB") if avg_psnr > best_psnr: best_psnr = avg_psnr torch.save(model.state_dict(), "best_dncnn.pth")

几个参数需要重点解释。lr=1e-3配 Adam 是去噪任务的常见起点,如果训练后期 loss 震荡,降到 1e-4 再跑。StepLR每 30 轮衰减一半,目的是让模型在后期精细收敛。batch_size=16在 256×256 patch、64 通道的 DnCNN 上大约占 6~8 GB 显存,显存不够就降到 8 或 4,同时把学习率按比例调小。验证时用clamp(0,1)把输出截断到合法像素范围,否则 PSNR 计算会被异常值拉偏。保存策略用「验证集最优才存」,避免存下过拟合的最后一轮。

4.3 学习率、batch size 和 patch 大小的联动关系

这三个参数不是独立的。patch 变大,单样本显存占用上升,batch size 就得降;batch size 降了,梯度估计噪声变大,学习率通常也要跟着降,否则训练不稳定。我一般按这个顺序定:先根据显存定 patch 和 batch 的组合,再在这个组合下扫学习率(1e-3、5e-4、1e-4 三档),选验证集收敛最快的那档。另外,如果开了混合精度训练(AMP),显存能省 30%~40%,可以适当把 batch size 提上去,但要注意 AMP 下 BN 的数值稳定性,必要时把 BN 换成 GroupNorm。

5. 避坑与排查:那些让 PSNR 不升反降的细节

5.1 训练 loss 一直降,验证 PSNR 却不动甚至下降

现象:训练集 MSE 从 0.01 降到 0.001,验证集 PSNR 卡在 28 dB 上不去。原因通常是数据泄漏或过拟合。先查划分:如果 patch 是随机打散的,同一张原图的 patch 会跨训练和验证集,验证指标虚高且不再反映泛化。再查模型容量:DnCNN 17 层对几千张 patch 的数据量来说偏大,容易记住训练集。解决办法是按原图重新划分数据集,并加数据增强(随机翻转、旋转 90 度),把有效样本量翻几倍。

5.2 推理结果出现棋盘格伪影

现象:输出图上有规律的网格状纹理。原因多半是转置卷积(deconvolution)的步长和卷积核尺寸不匹配,导致上采样时像素覆盖不均匀。如果你用的是编码器-解码器结构,把转置卷积换成「最近邻上采样 + 普通卷积」,或者把步长设为 2、核尺寸设为 4(能被步长整除),棋盘格基本就消失了。DnCNN 这种全卷积结构不会出现这个问题,因为它没有下采样。

5.3 真实噪声图上效果远差于合成噪声

现象:合成 AWGN 上 PSNR 32 dB,换真实高 ISO 照片只有 26 dB。原因是噪声分布不匹配。合成噪声是空间独立的高斯白噪声,真实噪声在暗部强、亮部弱,还带空间相关性。解决办法是用异方差高斯模型重新合成训练数据,或者直接用真实成对数据微调最后几层。我一般会保留一个在合成数据上预训练的权重,再用少量真实成对数据以 1e-4 的学习率微调 10~20 轮,效果提升比从头训真实数据更稳。

5.4 显存溢出(OOM)的排查顺序

现象:训练到一半报 CUDA out of memory。按这个顺序查:先看 batch size 和 patch 是不是设大了,降一半试;再看 DataLoader 的num_workers是不是开太高导致内存 pinned 占用过多,降到 2~4;然后确认验证阶段有没有用torch.no_grad(),忘了加会导致计算图累积;最后检查是不是在训练循环里无意中保留了中间变量(比如把 loss 存进列表却没 detach)。这四步走完,九成 OOM 都能定位。

5.5 保存的模型加载后输出全黑或全白

现象:训练时正常,重新加载权重推理却输出异常。原因通常是保存和加载的键名不匹配,比如训练时用了nn.DataParallel,保存的 state_dict 带module.前缀,加载时没去掉。解决办法是保存时用model.module.state_dict()(如果是 DataParallel),或者加载时用{k.replace('module.', ''): v for k, v in state.items()}处理键名。另一个可能是推理时忘了把模型切到eval()模式,BN 还在用 batch 统计量,导致输出不稳定。

6. 评估与进阶:PSNR 之外你该看的指标和一个小技巧

PSNR 是最常用的去噪指标,但它有个明显缺陷:对过度平滑不敏感。一个把纹理全抹平、只剩大色块的输出,PSNR 可能很高,但人眼看着很假。所以我评估时一定同时看 SSIM(结构相似性),它从亮度、对比度、结构三个维度衡量,对纹理保留更敏感。如果做的是真实照片去噪,我还会加一个人工盲评:把原图、带噪图、去噪图并排,让不参与项目的人挑哪张最自然。指标是给论文看的,观感是给用户看的,两者都要顾。

进阶方向有两个值得投入。一是把单一噪声水平的模型扩展成盲去噪模型,做法是在训练时随机采样 sigma,让网络自己估计噪声强度,DnCNN 的盲版本就是这么做的,代价是同 sigma 下的 PSNR 会比专用模型低 0.2~0.5 dB。二是引入注意力机制,比如在残差块里加通道注意力(SE 模块),让网络自适应地给不同通道分配权重,对彩色噪声的抑制更精准。我试过在 DnCNN 的每个残差块后加 SE,参数量增加不到 5%,验证集 PSNR 涨了约 0.3 dB,性价比可以。

最后分享一个我踩过坑才养成的习惯:每次改完网络结构或数据管线,先拿 100 对样本跑 5 个 epoch,看 loss 有没有正常下降、输出图有没有明显异常,确认链路通了再上全量数据。这个「小样本快跑」的习惯帮我省下了大量等训练的时间,也避免过好几次跑了一整夜才发现数据路径写错、模型根本没学到东西的翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询