资讯动态

深度卷积神经网络图像去噪:DnCNN原理、复现与避坑指南

发布时间:2026/10/14 14:00:26 来源:尧图企业网站定制
简介这是一份基于深度卷积神经网络DnCNN实现图像去噪的完整代码与示例包面向深度学习图像处理开发者、研究人员及TensorFlow实践者帮助理解并复现高斯噪声去除流程。压缩包共45个文件含4个Python源码网络结构、参数配置、训练及辅助运算脚本、26张jpg与12张png图像训练集、测试集、去噪前后对比图、说明文档与许可证整体约1.39MB。资源从DnCNN网络结构入手覆盖卷积层与反卷积层设计、批量归一化、ReLU激活函数以及按噪声水平自适应处理等关键环节目录按训练集、测试集、训练结果模块组织配套图片可直观对比去噪效果Python脚本便于直接运行或二次修改。目前已有6508人学习下载适合希望快速上手DnCNN并基于TensorFlow开展图像去噪实验的读者也适合课程设计、论文复现或工程预研参考。1. 基于深度卷积神经网络图像去噪DnCNN 资源拆解与复现路径图像去噪问题说大不大说小不小。传统方法调参数调到怀疑人生而基于深度卷积神经网络的 DnCNN 图像去噪算法把这件事变成了端到端的模型训练。这份资源把 DnCNN 从数据集构造、网络定义、训练调度到验证推理完整串了一遍适合想自己复现模型又不想从零啃论文的开发者。它解决的不只是“有没有代码”而是“代码能不能跑、参数该怎么调、卡在什么位置”。下面我按自己拆项目的习惯把这份资源里真正值钱的部分铺开讲。2. DnCNN 网络原理与结构选型为什么输出的是残差而不是干净图2.1 残差学习让网络去学“噪声”而不是“图像”DnCNN 的核心设计是残差学习。给定带噪图像 y x v其中 x 是干净图v 是噪声网络直接拟合一个映射 F(y) ≈ v然后得到干净图 x y - F(y)。这个设计与直接学习 x 的映射完全不同。干净图像内容复杂纹理、边缘、平坦区域混在一起网络要强行记住太多结构信息而噪声 v 在统计上更接近零均值的高斯分布空间上高频且稀疏拟合这样一个目标收敛速度会快很多。残差学习还有一个额外好处网络在 forward 里把输入减去残差后即使网络在训练初期输出近似零模型也不会让图像变得不可用。换句话说模型“什么都不做”时输出等于输入而不是黑屏。这为训练提供了一个很宽松的起点。在资源包里模型定义文件model.py的逻辑是这样的class DnCNN(nn.Module): def __init__(self, depth17, in_channels1, out_channels1, num_filters64): super(DnCNN, self).__init__() layers [] # 第一层卷积 ReLU不接 BN layers.append(nn.Conv2d(in_channels, num_filters, kernel_size3, padding1, biasFalse)) layers.append(nn.ReLU(inplaceTrue)) # 中间 depth-2 层卷积 BN ReLU for _ in range(depth - 2): layers.append(nn.Conv2d(num_filters, num_filters, kernel_size3, padding1, biasFalse)) layers.append(nn.BatchNorm2d(num_filters, eps1e-3)) layers.append(nn.ReLU(inplaceTrue)) # 最后一层卷积输出残差图 layers.append(nn.Conv2d(num_filters, out_channels, kernel_size3, padding1, biasFalse)) self.net nn.Sequential(*layers) def forward(self, x): noise self.net(x) # 网络输出的是噪声估计 return x - noise # 输入减残差得到去噪结果这段代码里输入输出默认是单通道灰度图如果你处理的是彩色图可以把 in_channels 改为 3也可以在预处理时转成 YUV 只对亮度通道去噪。最后一层卷积后面没有 BN 和 ReLU因为残差值可能是负的必须原样输出。调用时返回的是去噪结果而不是残差本身这一点看起来简单实际很多人加载别人代码时会在推理里写错后面避坑章节会再强调。2.2 网络深度与感受野为什么默认是 17 层DnCNN 默认 depth17对应 17 层 3×3 卷积。3×3 卷积叠出来的感受野是 1 2 × 17 35也就是网络每输出一个像素相当于看到了输入图像上 35×35 的区域。这个尺寸能覆盖常见高斯噪声的空间相关性同时参数量控制在 66 万左右单张主流显卡就能训得动。如果深度太少比如 12 层感受野只有 25×25去噪后细节边缘会发虚尤其是对 sigma 大于 30 的噪声效果明显变肉。如果深度加到 20 层感受野到 41理论上去噪能力更强但训练时 BN 统计量会变得不太稳定收敛变慢学习率要相应下调。我的经验是如果没有特殊需求就用 17 层资源包也默认这个值。滤波器数量 num_filters64 是标准配置它决定了网络的宽度。把滤波器降到 32模型轻很多推理速度几乎翻倍但去噪效果通常会掉 0.5 dB 左右。在快速验证方案时我会用 32一旦确定方向立即换回 64。2.3 批归一化的位置与训练稳定性批归一化是 DnCNN 能稳训的关键。它的正确顺序是 conv - BN - ReLU而不是 conv - ReLU - BN。BN 需要在非线性激活之前把卷积输出分布拉回零均值单位方差这样 ReLU 输入就不会因为噪声强度变化而剧烈漂移。资源包的 model.py 已经按这个顺序写好了但如果你自己改网络很容易写成 ReLU 后再 BN属于比较隐蔽的翻车点。BN 的 eps 参数默认值是 1e-5PyTorch 里已经够用资源包设置成 1e-3 是为了在 batch size 较小时数值更稳。注意 BN 在训练和推理阶段的行为不同训练时用当前 batch 的均值和方差推理时用训练阶段累积的全局统计量所以后面必须强调 eval() 模式。3. 数据准备与预处理流程让模型能在自己的图像上复现3.1 训练数据的来源与处理方式DnCNN 训练通常使用灰度图像数据集比如经典的 BSD400包含 400 张自然图像。资源包把图像整理成训练集和验证集并提供了一个数据预处理脚本让你把自定义图像也加进去。常见做法是把一批清晰图像放在一个目录下脚本统一缩放到合适尺寸再按滑动窗口切出小块。切块的目的很简单增加样本量。一张 512×512 的图只有一张样本网络很容易过拟合。切成 40×40 的小块后一张图能产出上百个训练样本。patch 大小要和感受野匹配默认 40×40 刚好略大于 35×35 的感受野如果 patch 小于感受野边缘区域的卷积计算会缺乏有效上下文。下面是资源包中数据预处理脚本的核心逻辑import numpy as np import cv2 def prepare_patches(image_paths, patch_size40, stride20, sigma25, augmentTrue): noisy_list, clean_list [], [] for path in image_paths: img cv2.imread(path, cv2.IMREAD_GRAYSCALE) img img.astype(np.float32) / 255.0 # 归一化到 [0, 1] if augment: # 随机 90 度旋转和水平翻转相当于 8 倍数据增强 k np.random.randint(0, 4) img np.rot90(img, k) if np.random.rand() 0.5: img np.flip(img, axis1) h, w img.shape for i in range(0, h - patch_size 1, stride): for j in range(0, w - patch_size 1, stride): clean_patch img[i:ipatch_size, j:jpatch_size] noise np.random.normal(0, sigma / 255.0, clean_patch.shape) noisy_patch clean_patch noise noisy_list.append(noisy_patch) clean_list.append(clean_patch) return np.stack(noisy_list), np.stack(clean_list)这段代码的关键操作有两个。第一个是像素归一化必须在添加噪声之前完成否则 sigma 表示噪声强度就不一致。第二个是数据增强尽量在切块之前做这样旋转和翻转不会产生黑边也更高效。参数上stride20 会让相邻 patch 有重叠重叠部分相当于隐式数据增强。如果你想减少训练样本数量可以把 stride 调到 patch_size也就是不重叠采样但效果通常会略差。sigma25 是一个中等强度的训练噪声适合快速测试。3.2 噪声模型固定 sigma 还是随机 sigma很多第一次接触的人会问模型能不能泛化到各种噪声强度。答案是训练时如果固定 sigma25模型对 sigma25 附近效果好遇到 sigma50 效果明显衰减。所以资源包提供了两阶段训练方案先固定 sigma 训一个底版模型再用随机 sigma 做微调。训练时的 sigma 采样方式如下def add_noise(clean_patch, fixed_sigmaNone): if fixed_sigma is not None: sigma fixed_sigma / 255.0 else: # 随机采样 0 到 55 之间的噪声强度 sigma np.random.uniform(0, 55) / 255.0 noise np.random.normal(0, sigma, clean_patch.shape) return clean_patch noise为什么下限是 0因为 sigma 为 0 时训练样本等于干净图这会让网络学到恒等映射对低噪声场景有保护作用同时不会干扰高噪声场景的训练。上限 55 是经验值超过 55 的噪声已经严重破坏图像结构模型很难学到有效信息强行加入反而会拉低整体性能。3.3 验证集划分与数据泄漏问题验证集的构造要刻意避开训练集里出现过或者内容高度相似的图像否则验证 PSNR 会虚高。资源包默认把数据集里 380 张做训练、20 张做验证另外保留了 3 张经典 512×512 标准测试图用于跨数据集泛化评估。标准测试图不参与训练也不参与验证专门用来看模型有没有记住训练集内容。划分时还要注意随机种子。资源包在脚本开头固定了 np.random.seed(0) 和 torch.manual_seed(0)这能保证每次划分结果一致。如果你要复现报告里的指标这个种子千万不要改。4. 训练配置与实现细节从损失函数到模型保存4.1 损失函数MSE 与 PSNR 的内在一致性DnCNN 的训练目标是最小化输出与干净图像之间的均方误差MSE。对高斯噪声来说MSE 对应的就是最大似然估计数学上最匹配。用 L1 损失也可以但 L1 对异常值的惩罚更温和训练后期容易出现残差偏大而 PSNR 不变的情况。感知损失虽然对视觉质量有改善但需要额外引入预训练网络训练成本高而且在小 patch 上容易引入特征分布偏移。资源包使用 PyTorch 内置的 MSELoss训练循环里是这样计算损失的criterion nn.MSELoss() for batch_idx, (noisy, clean) in enumerate(train_loader): noisy noisy.to(device) clean clean.to(device) optimizer.zero_grad() output model(noisy) # 模型直接返回去噪结果 loss criterion(output, clean) # 与干净图比较 loss.backward() optimizer.step() if batch_idx % 100 0: print(fStep {batch_idx} Loss: {loss.item():.6f})注意这里比较的是 model(noisy) 的输出也就是 x_hat而不是残差图。因为 forward 里已经做了一次输入减残差所以这个 loss 等效于论文中残差与真实噪声的 MSE。梯度回传时会先经过减操作但减操作是线性变换梯度能正确传到残差分支。4.2 优化器与学习率调度Adam 搭配分段衰减资源包默认使用 Adam 优化器初始学习率 1e-3训练 50 个 epoch。Adam 对超参数不敏感适合图像去噪这种相对平滑的目标函数beta1 和 beta2 保持 PyTorch 默认值即可。训练到第 30 个 epoch 时学习率降到 1e-4到第 45 个 epoch 时再降到 1e-5。该策略对应下面这段代码optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones[30, 45], gamma0.1) for epoch in range(50): train_one_epoch(model, train_loader, optimizer, criterion, device) val_psnr evaluate(model, val_loader, device) scheduler.step() print(fEpoch {epoch1} Val PSNR: {val_psnr:.2f} dB)milestones 是学习率下降的 epoch 节点gamma 是衰减系数。50 个 epoch 里把衰减节点放在 30 和 45能让前期快速探索、后期精细收敛。如果你自己把 epochs 改成 80按比例换算milestones 可以改成 [48, 72]但要注意观察验证 PSNR 是否还在上升如果已经饱和就不用再等了。Batch size 默认是 16。BN 在 batch size 小于 8 时统计量抖动很厉害模型很难收敛大于 64 时显存消耗大而且假设每个 batch 的分布与整体分布一致过大的 batch 反而可能让 BN 的统计量过于平滑。16 是一个比较舒适的区域。4.3 训练监控与模型保存策略训练时如果 loss 从 0.2 慢慢降到 0.01 以下说明网络在正常学习。但 loss 下降幅度不能直接反映去噪质量所以资源包在每个 epoch 结束后都会在验证集上计算一次 PSNR。保存模型时不是保存最后一个 epoch 的权重而是保存验证 PSNR 最高的那个 epoch。这个细节能帮你避免学习率衰减后期出现过拟合导致 PSNR 回退的情况。保存逻辑如下best_psnr 0.0 for epoch in range(50): train_one_epoch(...) val_psnr evaluate(...) if val_psnr best_psnr: best_psnr val_psnr torch.save(model.state_dict(), best_model.pth) print(fSave best model at epoch {epoch1}, PSNR {best_psnr:.2f} dB)这里有一个容易忽略的点验证 PSNR 是在模型处于 eval 模式下计算的所以 evaluate 函数开头一定要调用 model.eval()并且用 with torch.no_grad() 包裹推理过程否则 BN 会按训练模式更新统计量验证结果就不准了。5. DnCNN 训练与推理避坑指南五个典型翻车现场5.1 loss 完全不下降卡在初始值附近现象训练了十几个 epoch打印的 loss 始终在 0.2 上下浮动几乎看不到下降趋势。原因最常见的是数据归一化不一致。输入图像还是 0-255 的整数网络权重初始化时默认输入分布在 0-1 附近大数值输入会让梯度计算出现异常BN 也无法把这么宽的分布拉回来。另一个原因可能是学习率太大导致 loss 震荡但不下降。解决检查训练脚本里图像是否已经 astype(np.float32) / 255.0噪声的 sigma 是否也同步除以了 255。如果确认归一化没问题再把初始学习率从 1e-3 降到 1e-4 试跑 5 个 epoch看 loss 是否开始下降。5.2 输出图像整体偏灰或偏白现象测试时模型输出像一层雾盖在上面去噪结果的平均亮度与原始图像明显不同PSNR 反而低于直接看带噪图。原因把网络输出的残差直接当成了去噪结果没有做输入减残差。也就是说你保存或者打印的是 model(x) 中间层的 noise而不是 forward 最终返回的 x - noise。解决检查推理脚本里是否使用了完整模型而不是模型里某一层单独输出。如果你是自己手写的加载代码确认 model(x) 返回的是去噪图如果返回的是残差就手动执行 clean noisy - residual。5.3 BN 在单张推理时输出异常出现局部黑白噪点现象验证集上 PSNR 正常但拿一张新图去测结果图某些区域出现斑驳的黑白噪点整体质感不对。原因模型处于训练模式。训练模式下 BN 会使用当前输入 batch 的均值和方差对于单张图像来说这个统计量抖动极大甚至与训练阶段的全局统计量完全偏离造成输出异常。解决推理前必须调用 model.eval()。这是最简单也最容易被忽略的一步资源包的所有测试脚本里都加了这一行但如果你复制到自己的工程里很容易漏掉。5.4 彩色图像直接三通道输入导致色斑现象把彩色图按照三通道灰度图直接喂给网络模型输出出现伪彩色或色斑边缘颜色互相渗透。原因DnCNN 默认是单通道模型训练时每个通道的分布是一样的。如果直接把三通道堆叠输入等于是强行让网络学习一个三通道到三通道的映射但通道之间的相关性没有被建模结果自然混乱。解决资源包里的测试脚本提供两种模式。第一种是把三通道 YUV 转换只对 Y 通道去噪然后合并 UV 通道第二种是逐通道独立去噪每个通道单独走一次模型。YUV 模式效果好因为人眼对亮度噪声更敏感推荐默认用这个。5.5 实验结果不可复现两次训练 PSNR 差 0.2 dB 以上现象同一次训练流程隔几天再跑得到的 PSNR 和 loss 曲线不完全一致。原因GPU 算子本身有随机性PyTorch 的 DataLoader 在切 patch 时也没有固定顺序。如果脚本没有固定随机种子数据和网络初始化都会变化。解决在脚本最开头设置 np.random.seed(0)、torch.manual_seed(0)并关掉 cudnn 的 benchmark。如果还要更严格可以设置 torch.cuda.manual_seed_all(0)并把 torch.backends.cudnn.deterministic 设为 True。这样基本能保证每次跑出来的数字一致。6. DnCNN 的推理验证PSNR 边界处理与真实场景中的最后一步6.1 PSNR 计算流程与边界处理拿到训练好的模型先别急着上线用标准测试图跑一遍量化指标。PSNR 的计算看起来简单但边界处理和像素区间转换常常让结果出现误差。资源包里的推理脚本是这样做的import cv2 import numpy as np import torch def calculate_psnr(img1, img2, max_val255.0): img1 img1.astype(np.float64) img2 img2.astype(np.float64) mse np.mean((img1 - img2) ** 2) if mse 0: return 100.0 return 10 * np.log10((max_val ** 2) / mse) # 推理 model.eval() with torch.no_grad(): noisy_tensor torch.from_numpy(noisy_im).permute(2, 0, 1).unsqueeze(0).float() clean_tensor model(noisy_tensor) clean_im clean_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy() # 关键先 clip 再转回 0-255 范围 clean_im np.clip(clean_im, 0.0, 1.0) clean_im (clean_im * 255.0).round().astype(np.uint8)这里有两个细节会直接影响 PSNR。第一个是 clip 必须做模型输出的像素可能略小于 0 或略大于 1如果不 clipMSE 会被这些越界值拉大PSNR 看起来会偏低。第二个是计算 PSNR 时建议去掉图像四周至少 15 个像素再算因为卷积零填充会在边缘产生一圈假像素这部分不能代表模型的真实去噪能力。实际测试时我习惯用 sigma15、25、50 三组噪声分别测记录 PSNR 和单张推理时间。如果低 sigma 效果好而高 sigma 效果崩说明模型容量对强噪声的拟合不足可以先用随机 sigma 微调模型而不是直接改网络结构。6.2 权重加载与后续微调训练结束后保存的是 state_dict而不是整个模型对象。推理时先创建模型实例再加载权重model DnCNN(depth17, in_channels1, out_channels1, num_filters64) state_dict torch.load(best_model.pth, map_locationcpu) model.load_state_dict(state_dict) model.eval()注意加载后一定要 model.eval()这一点无论说多少次都有人会犯。如果你想把模型用到自己的数据集上可以在原权重基础上用更小的学习率微调。比如固定前 10 层只训练最后几层或者把初始学习率降到 1e-5配合你自己的图像数据再训 10 个 epoch。这种微调方式比从头训练稳定得多也快得多。关于这个 DnCNN 项目我还想分享一个习惯每次做完推理不管结果好不好我都会强制走一遍四步检查——设置随机种子、加载权重、调用 model.eval()、对输出做 clip。这四个动作几乎覆盖了我过去踩过的大半坑从那以后每次新数据集拿来测试我都能很快判断是模型问题还是数据问题。希望这篇文章能帮你把 DnCNN 从“看论文觉得会了”变成“真正跑通且知道边界在哪”也希望这份资源能在你自己的图像数据集上派上用场。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑