DnCNN图像去噪模型全解析:从残差学习到PyTorch完整复现

发布时间:2026/9/2 8:07:43
DnCNN图像去噪模型全解析:从残差学习到PyTorch完整复现 简介这是一份面向深度学习初学者与图像处理研究者的PyTorch图像去噪实战资源聚焦DnCNN系列模型的完整复现与工程落地解决真实场景中高斯噪声抑制、模型结构对比与端到端训练测试等核心问题。资源共20个文件包含5个核心Python脚本如main_train.py、main_test.py、data_generator.py、6个预训练.pth模型权重覆盖DnCNN-S/B/3及CDnCNN-B多噪声等级、4个XML配置与IDE工程文件、1个README说明文档及结果可视化PNG图压缩包仅13.2MB轻量易部署。已有2302人学习下载适配Windows/Linux双平台支持开箱即测——无需训练即可调用已收敛模型完成PSNR/SSIM定量评估同时提供数据切块、参数化模型切换、RGB/灰度统一接口等工程细节目录结构模块清晰几乎复现原论文全部实验图表与消融分析是深入理解图像去噪网络设计与训练流程的高质量实践范本。1. 项目概述与核心价值最近在整理图像去噪相关的学习笔记发现DnCNN这个经典模型虽然论文发表已有年头但其简洁的架构和扎实的效果依然是很多同学入门深度学习图像复原领域的绝佳选择。网上能找到的Pytorch实现版本不少但大多只实现了最基础的DnCNN-S模型对于论文中提到的几个重要变体如DnCNN-B、DnCNN-3以及更贴近真实噪声场景的CDnCNN-B要么实现不全要么缺少清晰的训练和测试脚本让想深入复现和对比实验的朋友有点无从下手。这个项目的目的就是基于一个清晰、模块化的DnCNN基础源码完整复现论文《Beyond a Gaussian Denoiser: Residual Learning of Deep CNN for Image Denoising》中的多个核心模型。不仅仅是把网络结构搭出来更重要的是提供一套从数据准备、模型定义、训练循环到测试评估的完整流程并且特别补充了DnCNN-B、CDnCNN-B和DnCNN-3这几个关键变体的实现。无论你是想快速跑通一个基线模型还是想深入研究不同网络深度、不同训练策略对去噪效果的影响这份代码都能提供一个可靠的起点。代码会力求简洁明了避免过度封装让你能清晰地看到每一行代码在做什么方便你进行二次修改和实验。2. 核心模型架构深度解析2.1 DnCNN的核心思想残差学习与批归一化DnCNN的全称是Denoising Convolutional Neural Network它的核心创新点在于将图像去噪问题巧妙地转化为一个残差学习问题。传统思路是让网络直接学习从噪声图像到干净图像的映射即F(y) ≈ x其中y是噪声图x是干净图。而DnCNN则让网络学习噪声残差即R(y) ≈ v其中v y - x 是噪声。最终的去噪图像通过x y - R(y)得到。这么做有几个显著优势。首先学习残差通常比学习原始图像更容易因为残差噪声的分布相对简单网络可以更专注于噪声模式。其次这种结构天然地融入了“恒等映射”identity mapping的思想即当输入图像本身就很干净时网络理想的输出应该是接近0的残差图这有助于训练的稳定性和模型的泛化能力。最后论文中大量使用的批归一化Batch Normalization BN层被证明能极大地加速深度网络在图像复原任务上的训练收敛并带来一定的正则化效果。2.2 模型变体详解DnCNN-S/B/3与CDnCNN-B论文中提出了几个不同配置的模型理解它们的区别对于正确复现至关重要。DnCNN-S这是最基础的版本。“S”代表“shallow”或“specific”通常指层数较浅例如17层且针对特定噪声水平如σ25的高斯噪声训练的模型。它的结构非常规整第一层是ConvReLU中间是若干组ConvBNReLU最后一层是Conv。中间层的卷积核都是3x3通道数固定为64。DnCNN-B“B”代表“blind”。这是本项目复现的重点之一。与S版本针对单一固定噪声水平不同DnCNN-B旨在处理一个噪声水平范围例如σ ∈ [0, 55]。为了实现这一点除了网络结构可能更深例如20层以增加容量外最关键的是训练策略。在训练时每一批batch输入图像的噪声水平σ是从一个均匀分布中随机采样的。这样迫使网络学习适应不同强度的噪声成为一个“盲”去噪器。在代码实现上这意味着我们的数据加载和噪声添加模块需要支持动态的噪声水平。DnCNN-3这个变体是针对三种特定噪声水平σ15, 25, 50分别训练三个独立的DnCNN-S模型。它不属于盲去噪但常用于对比实验以展示单一模型DnCNN-B与多个专用模型DnCNN-3在性能上的权衡。复现它需要管理三个独立的模型实例和对应的训练流程。CDnCNN-B这是另一个关键变体论文中用于处理更为复杂的真实噪声。“C”可能代表“Clip”或“Color”但在上下文中最合理的解释是处理截断Clipped的噪声。在真实图像中由于相机传感器的物理限制和图像存储格式如8-bit的JPEG噪声分布不是完美的高斯分布而是会被“截断”到一定的动态范围如0-255。CDnCNN-B就是在模拟这种条件下训练的DnCNN-B。具体操作是在添加高斯噪声后对噪声图像进行np.clip(y, 0, 255)操作然后再归一化到[0,1]或[-1,1]送入网络。这个细微的差别对处理真实图像噪声至关重要。注意很多复现代码忽略了CDnCNN-B这个细节直接使用未截断的噪声图像训练这会导致模型在处理真实照片时性能下降因为其训练和测试的数据分布不一致。2.3 网络模块的代码级实现要点在Pytorch中实现DnCNN结构清晰是关键。我们通常会定义一个基础的DnCNN模块。import torch import torch.nn as nn class DnCNN(nn.Module): def __init__(self, depth17, n_channels64, image_channels1, use_bnormTrue, kernel_size3): super(DnCNN, self).__init__() padding kernel_size // 2 # 保持特征图尺寸不变 layers [] # 第一层Conv ReLU layers.append(nn.Conv2d(in_channelsimage_channels, out_channelsn_channels, kernel_sizekernel_size, paddingpadding, biasTrue)) layers.append(nn.ReLU(inplaceTrue)) # 中间层(Conv BN ReLU) * (depth-2) for _ in range(depth-2): layers.append(nn.Conv2d(in_channelsn_channels, out_channelsn_channels, kernel_sizekernel_size, paddingpadding, biasFalse)) if use_bnorm: # BN层放在Conv和ReLU之间 layers.append(nn.BatchNorm2d(n_channels, eps0.0001, momentum0.95)) layers.append(nn.ReLU(inplaceTrue)) # 最后一层Conv layers.append(nn.Conv2d(in_channelsn_channels, out_channelsimage_channels, kernel_sizekernel_size, paddingpadding, biasFalse)) self.dncnn nn.Sequential(*layers) self._initialize_weights() def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): # 论文中提到使用He初始化kaiming初始化配合ReLU nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) def forward(self, x): y x out self.dncnn(x) return y - out # 残差学习输出 输入 - 噪声残差关键点解析残差连接在forward函数中我们显式地执行y - out。这比在网络内部用nn.Identity()更清晰强调了DnCNN的残差学习本质。批归一化use_bnorm参数控制是否使用BN。对于极浅的网络或某些实验你可能想关闭它。注意BN层的momentum参数论文中使用了0.95比默认的0.1大这会让运行均值和方差的更新更平滑可能对去噪这种低层视觉任务更友好。权重初始化正确的初始化对收敛很重要。对于使用ReLU的卷积层Kaiming初始化是标准做法。偏置Bias注意第一层和最后一层卷积通常使用偏置而中间层在使用了BN之后卷积可以不再设置偏置biasFalse因为BN本身包含可学习的偏移参数这样可以减少参数数量并可能提升稳定性。3. 数据准备与噪声模拟实战3.1 训练数据集的选择与预处理对于灰度图像去噪最常用的数据集是BSD400或BSD500的灰度版本以及Waterloo Exploration Database。对于彩色图像可以使用CBSD400或DIV2K数据集的训练部分。我们的实现需要支持灰度channel1和彩色channel3。预处理步骤通常包括裁剪Cropping加载图像后随机裁剪出固定大小的块如 50x50, 64x64, 128x128。较小的块可以增加批大小加快训练较大的块能提供更多上下文信息可能提升性能。论文中多使用50x50。数据增强为了增加数据多样性可以对裁剪块进行随机水平翻转和垂直翻转。对于彩色图像还可以考虑轻微的随机色彩抖动但在经典去噪论文中较少使用以避免引入非噪声变化。归一化Normalization将像素值从 [0, 255] 归一化到 [0, 1] 或 [-1, 1]。DnCNN论文中似乎使用的是[0, 1]。我个人的经验是对于使用BN的网络[0,1]或[-1,1]影响不大但整个训练和测试过程必须保持一致。3.2 高斯噪声的精确添加与CDnCNN-B的关键实现这是训练DnCNN的核心环节不同的噪声添加方式直接对应不同的模型变体。import numpy as np import torch def add_gaussian_noise(clean_patches, sigma, clipFalse): 为一批干净图像块添加高斯噪声。 Args: clean_patches: 归一化到[0,1]的干净图像块形状为 [B, C, H, W] sigma: 噪声标准差。可以是一个标量固定噪声也可以是一个列表/数组为每个样本指定不同sigma。 clip: 布尔值。如果为True则模拟CDnCNN-B的训练对加噪后的图像进行截断。 Returns: noisy_patches: 加噪后的图像块值域仍在[0,1]内如果clipTrue则可能因截断导致分布改变。 batch_size clean_patches.shape[0] # 处理sigma如果是标量扩展为与batch_size相同的数组 if np.isscalar(sigma): sigma_array np.full((batch_size, 1, 1, 1), sigma) # 保持维度便于广播 else: # 假设sigma是一个长度为batch_size的列表或数组 sigma_array np.array(sigma).reshape(-1, 1, 1, 1) # 生成高斯噪声 # 注意噪声的标准差是sigma/255.0因为clean_patches在[0,1]范围 noise np.random.randn(*clean_patches.shape) * (sigma_array / 255.0) noisy_patches clean_patches noise if clip: # CDnCNN-B的关键步骤在像素值域模拟0-255截断然后重新归一化 # 先将[0,1]映射到[0,255]截断再映射回[0,1] noisy_patches np.clip(noisy_patches * 255.0, 0, 255) / 255.0 # 理论上即使不clip也可能有极少数点超出[0,1]但影响很小。 # 为了严谨也可以加一个soft clip: noisy_patches np.clip(noisy_patches, 0, 1) return noisy_patches针对不同模型的训练数据流DnCNN-S/DnCNN-3在数据加载器中sigma是一个固定值如25。clip设置为False。DnCNN-B在数据加载器中sigma需要从一个均匀分布中随机采样例如sigma np.random.uniform(0, 55)。clip设置为False。CDnCNN-B与DnCNN-B类似随机采样sigma但clip参数必须设置为True。实操心得在实现DnCNN-B的数据加载时一个常见的错误是在整个数据集级别采样一个sigma然后所有batch都用这个sigma。正确做法应该是在每个batch甚至每个样本级别随机采样sigma。这样能确保在一个epoch内模型能看到所有噪声水平的数据学习到真正的盲去噪能力。我们可以在数据集的__getitem__方法中为每个样本随机生成sigma并将sigma作为额外信息返回给训练循环。3.3 测试数据集与评估指标训练完成后我们需要在标准测试集上评估性能。常用的测试集有Set1212张经典的灰度测试图像。BSD6868张来自BSD数据集的灰度图像。CBSD68BSD68的彩色版本。Kodak2424张高质量彩色照片。评估指标主要使用峰值信噪比PSNR和结构相似性指数SSIM。PSNR计算简单与均方误差MSE直接相关是去噪论文中最常用的指标。SSIM则更符合人眼视觉感知。在测试时通常是对整张图像进行去噪而不是裁剪的块。import cv2 import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def calculate_psnr_ssim(clean_img, denoised_img, data_range255.0): 计算PSNR和SSIM。假设输入是0-255范围的numpy数组。 # 确保图像是float类型 clean_img clean_img.astype(np.float64) denoised_img denoised_img.astype(np.float64) psnr peak_signal_noise_ratio(clean_img, denoised_img, data_rangedata_range) # SSIM对于多通道图像需要指定channel_axis参数 if len(clean_img.shape) 3 and clean_img.shape[2] 3: ssim structural_similarity(clean_img, denoised_img, data_rangedata_range, channel_axis2, win_size11) else: ssim structural_similarity(clean_img, denoised_img, data_rangedata_range, win_size11) return psnr, ssim测试流程注意点边界处理DnCNN是全卷积网络理论上可以处理任意尺寸的输入。但为了避免边界效应可以在测试时对图像进行镜像填充padding后再输入网络输出时再裁剪掉填充部分。论文中似乎没有特别强调这一点对于大小适中的测试图如512x512直接输入影响不大。批处理即使测试时只有一张图也最好构造成一个批量为1的tensor以保持与训练时相同的计算图。模型模式务必使用model.eval()和torch.no_grad()来关闭dropout和BN的训练模式并减少内存消耗。4. 训练策略与超参数配置4.1 损失函数与优化器选择DnCNN论文中使用的是简单的均方误差MSE损失即预测的噪声残差与真实噪声残差之间的MSE。这在Pytorch中对应nn.MSELoss()。对于残差学习这是最直接的选择。优化器选择Adam。论文中使用的初始学习率是1e-3。这是一个比较高的学习率但因为使用了BN层训练通常能稳定收敛。import torch.optim as optim criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3)4.2 学习率调度与训练轮次学习率策略对最终性能影响很大。论文中提到当训练损失在连续几个epoch不再下降时将学习率减半。我们可以用Pytorch的ReduceLROnPlateau调度器来实现。from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5, verboseTrue) # 在每个epoch结束后调用 # val_loss ... # 在验证集上的损失 # scheduler.step(val_loss)关于训练轮次epoch论文中对DnCNN-B的训练使用了约50个epoch。对于DnCNN-S由于任务相对简单可能20-30个epoch就足够了。我建议设置一个较大的epoch数如80并配合早停Early Stopping策略当验证集损失在长时间内如10个epoch不再下降时停止训练以防止过拟合。4.3 DnCNN-B与CDnCNN-B训练的特殊设置这是本项目复现的难点和重点。动态噪声水平在训练循环的每个batch我们需要生成当前batch对应的噪声水平sigma。这需要在数据加载器内部完成。# 在自定义Dataset的__getitem__或collate_fn中 class BlindDenoisingDataset(Dataset): def __init__(self, clean_image_paths, patch_size50, sigma_range(0, 55)): ... self.sigma_range sigma_range def __getitem__(self, index): clean_patch ... # 读取并裁剪干净图像块 # 为每个样本随机采样sigma sigma np.random.uniform(*self.sigma_range) # 添加噪声这里可以传入sigma和clip标志 noisy_patch add_gaussian_noise(clean_patch, sigma, clipself.clip) # 需要返回噪声图、干净图以及sigma如果需要 # 对于DnCNN损失是噪声残差所以标签是 (noisy_patch - clean_patch) noise_residual noisy_patch - clean_patch return noisy_patch, noise_residual, sigma # 返回sigma可用于记录或调整损失权重损失计算的一致性无论sigma如何变化损失函数始终是MSE。网络需要自己学会适应不同强度的噪声输入。有研究尝试过根据sigma对损失进行加权但原始DnCNN论文并未采用我们首先按原论文实现。4.4 训练循环代码框架一个完整的训练循环框架如下def train_epoch(model, dataloader, criterion, optimizer, device, clipFalse): model.train() running_loss 0.0 for batch_idx, (noisy_imgs, residual_labels, _) in enumerate(dataloader): # sigma可能不需要用于计算损失 noisy_imgs noisy_imgs.to(device) residual_labels residual_labels.to(device) # 前向传播 optimizer.zero_grad() predicted_residual model(noisy_imgs) # 我们的模型forward返回的是预测的噪声残差 loss criterion(predicted_residual, residual_labels) # 反向传播与优化 loss.backward() optimizer.step() running_loss loss.item() avg_loss running_loss / len(dataloader) return avg_loss在训练脚本中我们需要根据要训练的模型类型S/B/3/C来实例化不同的数据集和模型。对于DnCNN-3最简单的方法是分别用sigma15, 25, 50训练三个独立的模型。5. 测试、结果分析与可视化5.1 模型测试与性能对比训练完成后在Set12或BSD68测试集上运行测试脚本计算平均PSNR/SSIM。为了公平对比需要确保测试噪声水平对于非盲模型S, 3使用其训练时对应的固定sigma如25生成测试噪声。测试噪声水平对于盲模型B, CB测试时也需要在一个sigma范围内如15, 25, 50分别测试以观察其泛化性能。理想情况下DnCNN-B在它训练所覆盖的范围内0-55各个sigma水平上的性能应该比较均衡且接近于对应sigma的DnCNN-S模型。可以制作一个表格来对比结果模型训练噪声水平 (σ)测试噪声水平 (σ)Set12平均PSNRBSD68平均PSNR参数量DnCNN-S25 (固定)25约 30.40 dB约 29.23 dB~0.56MDnCNN-3 (σ15)15 (固定)15约 33.00 dB约 31.73 dB~0.56MDnCNN-3 (σ25)25 (固定)25约 30.40 dB约 29.23 dB~0.56MDnCNN-3 (σ50)50 (固定)50约 26.95 dB约 26.23 dB~0.56MDnCNN-B[0, 55] (均匀)15约 32.80 dB约 31.60 dB~0.67MDnCNN-B[0, 55] (均匀)25约 30.35 dB约 29.20 dB~0.67MDnCNN-B[0, 55] (均匀)50约 26.90 dB约 26.18 dB~0.67MCDnCNN-B[0, 55] (均匀截断)25 (截断)约 30.10 dB约 29.00 dB~0.67M注以上PSNR值为论文中的近似参考值实际复现结果会因随机种子、训练细节略有浮动。从表格可以看出DnCNN-B作为一个单一模型在多个噪声水平上都能达到与专用模型DnCNN-3非常接近的性能体现了其“盲”去噪的优势。CDnCNN-B在模拟真实噪声截断的条件下训练在该测试条件下性能可能略有下降但这是为了换取对真实图像更好的泛化能力。5.2 结果可视化与问题排查可视化是检验模型效果的直观方式。在测试时可以保存去噪前后的图像对比。def save_denoising_results(clean_path, noisy_img, denoised_img, output_path): 保存干净图、噪声图、去噪图的对比。 # 将 tensor 转换回 numpy array并缩放到0-255 noisy_np (noisy_img.squeeze().cpu().numpy() * 255).clip(0, 255).astype(np.uint8) denoised_np (denoised_img.squeeze().cpu().numpy() * 255).clip(0, 255).astype(np.uint8) # 读取原始干净图用于对比 clean_full cv2.imread(clean_path, cv2.IMREAD_GRAYSCALE) # 注意可能需要根据训练时的裁剪方式对齐干净图的区域 # 这里假设测试是对整图进行的clean_full就是干净图 # 水平拼接三张图 result np.hstack([clean_full, noisy_np, denoised_np]) cv2.imwrite(output_path, result)常见问题与排查训练损失不下降或为NaN检查数据范围确保输入图像数据已正确归一化如除以255.0。未归一化的数据0-255会导致梯度爆炸。检查学习率1e-3对于Adam是常用值但如果损失震荡或爆炸可以尝试调低到5e-4。检查BN层确保模型在训练时处于model.train()模式在测试时处于model.eval()模式。检查损失计算确认你计算的是预测残差与真实残差之间的损失而不是预测图像与干净图像之间的损失。模型去噪效果模糊丢失细节可能过拟合检查训练集和验证集损失曲线。如果训练损失持续下降而验证损失上升说明过拟合。可以尝试增加数据增强如随机旋转、使用更小的模型、或加入早停。学习率过高或训练轮次不足过高的学习率可能导致优化不稳定难以找到好的极小值。训练轮次不足则模型未充分学习。噪声水平不匹配如果你用σ25训练的DnCNN-S去测试σ50的噪声图像效果肯定会差。确保训练和测试的噪声水平一致对于非盲模型。DnCNN-B在所有噪声水平上表现都平庸检查动态噪声生成确保每个batch的sigma是真正随机且覆盖了整个指定范围如[0,55]。可以打印几个batch的sigma值来验证。模型容量DnCNN-B通常比DnCNN-S深20层 vs 17层检查你的模型深度配置是否正确。训练轮次盲去噪任务更难可能需要更多的训练轮次如80-100个epoch才能收敛到好的性能。CDnCNN-B在合成高斯噪声测试集上PSNR反而更低这是正常的。因为CDnCNN-B是在“截断”的噪声数据上训练的这与标准的、未截断的高斯噪声测试条件存在分布差异。它的优势在于处理真实的、被传感器和编码过程限制的噪声。评估CDnCNN-B时应考虑在带有真实噪声的图像数据集如PolyU, Nam等上进行测试。5.3 项目代码结构建议一个清晰的项目结构有助于管理和复现。建议如下dncnn-pytorch-complete/ ├── data/ │ ├── train/ # 存放训练原图 │ ├── test/ # 存放测试原图Set12, BSD68等 │ └── prepare_data.py # 数据预处理脚本裁剪、生成路径列表等 ├── models/ │ ├── __init__.py │ └── dncnn.py # DnCNN, DnCNN-B, CDnCNN-B模型定义 ├── utils/ │ ├── dataset.py # 自定义Dataset类支持不同噪声模式 │ ├── losses.py # 损失函数定义 │ ├── metrics.py # PSNR, SSIM计算函数 │ └── logger.py # 日志和可视化工具 ├── configs/ │ ├── dncnn_s.yaml # 各模型的配置文件超参数、路径 │ ├── dncnn_b.yaml │ └── cdncnn_b.yaml ├── train.py # 主训练脚本根据配置启动不同训练 ├── test.py # 主测试脚本加载模型并在测试集评估 ├── pretrained/ # 存放训练好的模型权重 │ ├── dncnn_s.pth │ ├── dncnn_b.pth │ └── cdncnn_b.pth └── results/ # 存放测试输出图像和日志在train.py中可以通过命令行参数或配置文件来指定要训练的模型类型、噪声参数、是否截断等从而用一个脚本统一管理所有变体的训练。通过这样一套从理论到实践从模块到整体的完整实现你不仅能跑通DnCNN更能深入理解盲去噪、残差学习以及如何针对不同任务调整训练策略的核心思想。这份代码可以作为你进入图像复原领域的一个坚实跳板在此基础上尝试更复杂的模型或应用到实际项目中。本文还有配套的精品资源点击获取