尧图精选

深度卷积神经网络图像去噪实战:DnCNN原理与PyTorch实现

🕒 发布时间:2026/10/1 17:37:17 📁 来源:尧图网络
简介基于深度卷积神经网络的图像去噪算法是一套采用Python和TensorFlow实现的完整工程主要面向图像处理学习者、算法研究者以及有去噪需求的开发者用于解决图像噪声干扰导致的画质下降问题。资源共四十五个文件包括四个源码文件、二十六张训练图片、十二张测试图片以及说明文档和许可信息压缩包仅一点三九兆体积轻便。已有一千六百九十一人学习可用来快速复现去噪流程并调整参数。模型以卷积层提取图像特征再由反卷积层重建清晰图像并引入自适应去噪策略能针对高斯噪声等常见噪声取得良好效果。压缩包内划分了训练集、测试集和训练结果文件夹图片清晰展示噪声输入与去噪输出的对比便于理解算法细节源码结构完整稍作修改即可迁移到其他噪声场景或实际项目。1. 图像去噪不只是磨皮深度卷积神经网络把退化模型变成了可训练的参数基于深度卷积神经网络图像去噪算法没有再靠中值、高斯这种手工核去猜噪声而是把“从带噪图到干净图”的映射丢给卷积层去拟合。Python 让这件事从论文复现到工程落地都变得很轻——PyTorch 里几十行代码就能定义出一个能跑通的去噪网络数据准备、训练、评估也都能在同一个生态里完成。我第一次用 DnCNN 做实验时一个 17 层的小网络在噪声标准差 25 的高斯噪声下PSNR 能到 28.5dB 以上比 BM3D 还要高 0.6dB 左右这个差距不是调参调出来的是结构带来的。适合被 OpenCV 传统滤波磨掉细节、又不愿意用 Matlab 工具箱的工程师也适合想在低层视觉方向快速拿到一个可复现基准的研究者。下面从网络结构开始一直讲到数据合成、训练、翻车案例和验收。2. 深度卷积神经网络凭什么能去噪从退化模型到 DnCNN 的残差学习2.1 退化模型与噪声类型高斯、椒盐、泊松先搞清楚你的对手图像去噪的核心退化模型是 y x n其中 y 是传感器输出的带噪图x 是潜在干净图n 是噪声。几乎所有深度卷积神经网络去噪算法都是围绕这个加法模型做文章训练网络 g(y) 去逼近 x或者逼近 n 再让 x y - g(y)。常见的噪声源有三种。高斯噪声来自传感器热电子和放大器形态上每个像素独立加一个服从正态分布的随机数标准差 sigma 越大画面越花这是去噪论文最常用的合成噪声。椒盐噪声来自坏像素和传输错误表现为随机的白点或黑点一般用中值滤波就能压下去。泊松噪声来自光子计数的统计涨落信号越暗噪声相对越强常见于夜景和医学影像。如果目标只是做一个能用的去噪网络我一般建议先用高斯噪声切入原因有三个一是合成高斯噪声只需一行 numpy 代码可控性强二是 DnCNN 这类公开结构本来就是以高斯噪声为基准调出来的三是真实传感器噪声在高光区域往往接近高斯低光段再用后续的噪声估计网络去补偿。把对手定义清楚了后面的网络结构、损失函数和评估指标才能对得上。很多翻车案例的起点就是把椒盐噪声的图拿给高斯去噪网络跑PSNR 自然一塌糊涂。2.2 DnCNN 的核心残差学习和批量归一化为什么收敛更快DnCNN 是深度卷积神经网络图像去噪里那个“新算法”的经典代表2017 年提出后几乎成了低层视觉的必跑基线。它比早年的 VDSR、SRCNN 多做了一件事让网络预测残差而不是直接预测干净图。原因很直接干净图像自身是强结构化的像素之间存在大量低频冗余直接回归 x 网络要同时记住纹理和亮度而噪声 n 在空间上是近似独立的分布更均匀回归残差的优化面更平滑收敛快最终效果也好。另一个关键组件是批量归一化BN。去噪网络里的 BN 不是简单加速训练它让每一层输出的分布保持稳定使网络能堆到 17 层甚至更深而不梯度爆炸。DnCNN 结构非常规整第一层是 ConvReLU中间 15 层是 ConvBNReLU最后一层是单 Conv 输出残差。卷积核统一用 3x3padding 为 1保证输入输出分辨率不变。这里有个容易忽略的细节最后一层没有 BN也没有 ReLU因为残差的像素值有正有负不能让激活函数截断。网络宽度上特征图数量 n_feats 一般取 64深度取 17这两个参数是作者在 PSNR 和显存占用之间反复权衡后的结果。2.3 用 Python 搭建一个可运行的深度卷积去噪网络PyTorch 代码骨架下面这段代码是我会写进自己项目里的最小 DnCNN 骨架依赖只有 torch不需要额外库。你可以把它当作免费 Python 源码的起点直接复制到模型定义文件里。import torch import torch.nn as nn class ConvBlock(nn.Module): # 卷积块bn和act都可选方便第一层和最后一层去BN/去ReLU def __init__(self, in_ch, out_ch, bnTrue, actTrue): super().__init__() self.conv nn.Conv2d(in_ch, out_ch, kernel_size3, padding1) self.bn nn.BatchNorm2d(out_ch) if bn else None self.act nn.ReLU(inplaceTrue) if act else None def forward(self, x): x self.conv(x) if self.bn is not None: x self.bn(x) if self.act is not None: x self.act(x) return x class DnCNN(nn.Module): # 输入一张带噪图输出是干净图。网络内部学的是残差最后用原图减残差 def __init__(self, depth17, n_feats64, num_channels1): super().__init__() self.head ConvBlock(num_channels, n_feats, bnFalse, actTrue) self.body nn.Sequential(*[ ConvBlock(n_feats, n_feats, bnTrue, actTrue) for _ in range(depth - 2) ]) self.tail nn.Conv2d(n_feats, num_channels, kernel_size3, padding1) def forward(self, x): y self.head(x) y self.body(y) y self.tail(y) return x - y # 关键公式x - 残差 干净图这段代码里最需要盯住的是 forward 里的 return x - y。DnCNN 输出的 y 是网络预测的噪声残差干净图必须用输入 x 减去残差得到。这个减法决定了损失函数的目标值如果你把 return x - y 误写成 return y训练目标也要从干净图换成噪声否则模型会一直学一个负的恒等映射loss 永远降不动。depth 控制网络层数加深可以提升感受野但会增加显存和推理延迟n_feats 控制每层通道数64 是经验值内存紧张时可以先降到 32 验证通路出效果再调回 64。num_channels 输入灰度图填 1RGB 图填 3但要注意彩色去噪需要把 RGB 数据按通道归一化而且通常拆成 YCbCr 后只在亮度通道训练这样可以省显存颜色通道交给简单滤波。如果你是从 python 环境配置开始的新手这段代码在 CPU 上也能跑前向只是训练会很慢。建议至少准备一块 8G 显存的 GPUbatch size 才能放到合理档位。3. 造出能训练去噪网络的数据Python 从少量图像合成大批量噪声对3.1 为什么不能直接拿真实噪声图训练真值缺失与配对难题真实世界的噪声图到处都是但去噪网络训练需要成对的“干净图 噪声图”。白天手持相机拍两张同场景照片噪声不一样但内容也有位移固定机位拍一张长曝光和一张短曝光亮度一致了但长曝光可能有热噪、行人模糊。想要从真实数据里拿到像素级对齐的干净参考成本极高所以学术界的通用做法是用干净图合成噪声。这里有个常见误解有人觉得合成数据训练的网络在真实照片上会不好用其实不然只要合成噪声的分布覆盖了真实噪声的主要部分网络学到的是“去噪”这个操作而不是死记某张图的纹理。图像去噪算法在 BSD 这类标准测试集上的排名基本都是合成噪声训练出来的模型落地时再用真实噪声微调。3.2 用 Numpy 与 OpenCV 在 Python 里合成高斯噪声图像对合成噪声对可以用几行完成但要写出能训练的数据集还得处理图像读取路径、灰度/彩色、数值范围归一化这些细节。下面是我最常用的一段基础函数。import numpy as np import cv2 def add_gaussian_noise(img, sigma): # img 是 float32 且取值范围为 [0, 255] noise np.random.randn(*img.shape).astype(np.float32) * sigma return np.clip(img noise, 0.0, 255.0)参数 sigma 是高斯噪声的标准差单位是像素灰度级。sigma25 是去噪论文的默认强度对应在 0-255 范围内零点几的归一化方差如果你把图像归一化到 0-1 再训练加噪声时要把 sigma 同步除以 255。注意 np.clip 会把超出 255 的像素截断这会让“噪声”不再是严格的高斯但对网络训练几乎无影响论文里也这么干。这段代码可以写成一个公共工具函数后面 Dataset 里反复调用。3.3 Dataset 与数据增强把有限的图像变成无限的训练资源很多公开数据集只给 300 到 1000 张干净图直接全图训练几轮就过拟合了。常见做法是随机裁 patch配合翻转旋转把有限的图变成几百万个训练样本。这里我一般会继承 torch 的 Dataset把随机裁剪和随机 sigma 都放进去。import torch from torch.utils.data import Dataset class DenoiseDataset(Dataset): def __init__(self, clean_paths, patch_size64, sigma_range(0, 50)): self.clean_paths clean_paths self.patch_size patch_size self.sigma_range sigma_range def __getitem__(self, index): img cv2.imread(self.clean_paths[index], cv2.IMREAD_GRAYSCALE) img img.astype(np.float32) h, w img.shape # 随机裁出 patch_size x patch_size 的区域 top np.random.randint(0, h - self.patch_size 1) left np.random.randint(0, w - self.patch_size 1) clean img[top:topself.patch_size, left:leftself.patch_size] # 随机翻转做数据增强不用额外写增强库 if np.random.rand() 0.5: clean clean[:, ::-1] if np.random.rand() 0.5: clean clean[::-1, :] # 每次随机抽一个sigma网络学会“盲去噪” sigma np.random.uniform(*self.sigma_range).astype(np.float32) noisy add_gaussian_noise(clean, sigma) # 归一化到 [0,1] 并增加通道维 clean_t torch.from_numpy(clean / 255.0).unsqueeze(0) noisy_t torch.from_numpy(noisy / 255.0).unsqueeze(0) return noisy_t, clean_t, torch.tensor(sigma) def __len__(self): return len(self.clean_paths)Dataset 里的关键参数是 patch_size 和 sigma_range。patch_size 取 64 能在“上下文信息”和“样本数”之间取得平衡太小了模型感受野塞不下太大一个 batch 显存爆表。sigma_range 设成 (0, 50) 后网络不再只为一个噪声强度工作在 0-50 之间的未知强度上都有不错表现这就是所谓的盲去噪。如果你的目标场景是安防摄像头那种固定传感器sigma_range 可以收窄到 (15, 35)训练难度显著下降输出更锐利。返回的 sigma 张量虽然当前网络没用但如果你接下来换 FFDNet 这类需要输入噪声水平图的网络数据集不用改直接拿它喂进辅助分支就行。这段 Dataset 代码没有在init里预读所有图片每个 epoch 都会重新从硬盘读图。如果机器磁盘是机械硬盘建议在初始化时就以数组形式读进内存训练速度能快一倍。我一般会先看数据集总大小200 张 512x512 图片全读进内存不过 200MB 量级完全值得。4. 训练一个能用的去噪模型损失函数、优化器与三个必调参数4.1 损失函数选 L2 还是 L1PSNR 导向与视觉质量的拉锯深度卷积神经网络去噪的损失函数最常见的是 MSE也就是 L2 距离。PSNR 的定义里只有一项和网络输出相关MSEMSE 越小 PSNR 必然越高。所以如果你用提交指标做评比L2 是稳的选择。但 L2 有个毛病它对大像素误差非常敏感会把网络逼到“平均”多个可能解输出偏平滑丢失高频纹理。L1 对这个惩罚更温和保留的细节多一些但收敛速度比 L2 慢早期 loss 下降像爬坡。我自己的习惯是前 20 个 epoch 用 MSE 把结构拉起来之后切到 L1 微调两个损失切换时不需要动学习率模型已经在谷底附近切过去不会震荡。4.2 学习率、batch size、epoch先跑通再调优的基准配置下面这组参数是 DnCNN 在 64 patch 下比较稳的基准配置先跑通再调优不要一开始就追求论文里的精确数字。参数推荐值说明优化器Adam比 SGD 省心beta 默认初始学习率1e-3过大不收敛过小原地踏步batch size1288G 显存上限24G 可以到 256patch size64太大显存不够太小上下文不足epoch60配合学习率衰减学习率衰减step 30, gamma 0.130 轮后降到 1e-4batch size 和 patch size 的乘积决定了每步的图片像素量显存不足时优先减 patch再减 batch。因为去噪网络对 batch size 的敏感度不高从 128 减到 64PSNR 的下降通常不到 0.1dB而 patch 从 64 减到 32 会因为上下文不足掉 0.5dB 左右所以优先保 patch。每轮训练时长取决于数据集大小600 张图每个 patch 64、batch 128一张 3060 显卡 40 分钟能跑完一轮60 轮要一晚上这是正常节奏别急着提前终止。4.3 训练脚本日志记录、模型保存与中断恢复训练脚本里我固定会写三个模块模型定义、数据装载、训练循环。完整训练代码可以浓缩成下面这段直接运行前需要补上 clean_paths 的文件列表。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model DnCNN(depth17, n_feats64, num_channels1).to(device) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) criterion nn.MSELoss() loader DataLoader(dataset, batch_size128, shuffleTrue, num_workers4) for epoch in range(60): model.train() total_loss 0.0 for noisy, clean, sigma in loader: noisy noisy.to(device) clean clean.to(device) pred model(noisy) # pred 已经是干净图 loss criterion(pred, clean) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * noisy.size(0) scheduler.step() avg_loss total_loss / len(loader.dataset) torch.save(model.state_dict(), fdncnn_{epoch1}.pth)两个参数在这里很关键。num_workers4 能同时读图但如果你的电脑在 Windows 上跑 PyTorchnum_workers 大于 0 可能报 DataLoader worker 异常改成 0 就能跑通代价是读取慢一点。另一个是 scheduler.step() 的位置必须在每个 epoch 末尾调用放在 batch 循环里会让学习率在每个 batch 都变模型会乱掉。logging 我习惯用 torch.save 存完整 checkpoint包括 model、optimizer、epoch这样中断后能恢复训练不然两个小时后进程被 OOM 杀掉就只能从头再来。# 断点恢复写一个 load 函数 checkpoint torch.load(last.pth) model.load_state_dict(checkpoint[model]) optimizer.load_state_dict(checkpoint[optimizer]) start_epoch checkpoint[epoch] 1如果你要彻底复现论文里的 DnCNN记得也要固定 torch.manual_seed、np.random.seed 和 DataLoader 的随机种子否则每次跑出来的 PSNR 会有一点波动不是模型问题而是采样问题。5. 去噪网络训练避坑5 个最常见的翻车现场与排查方案5.1 现象loss 降了但输出还是一片糊训练曲线正常下降验证时把模型输出保存下来发现图像比输入更糊像是做了均值滤波。原因网络 forward 里返回的到底是残差还是干净图和损失函数的目标没对齐。如果你在代码里写了 return x - y那么输出已经是干净图损失目标必须是 clean如果你写了 return y但损失目标也是 clean网络只能学到一个几乎为零的 y输出约等于 x看起来就是没去噪。还有另一个隐蔽原因测试时没写 model.eval()BN 层还在用每个 batch 的统计量输出有随机扰动。解决打印 forward 输出的均值残差应该接近 0干净图应该与输入结构相同。训练循环里先确认 pred 的形状和数值范围再跑 loss。测试脚本里一行 model.eval()有时能让 PSNR 提升 0.5dB。5.2 现象训练集 PSNR 高但验证集纹路全被磨平模型在训练用的合成噪声上效果很好换成真实照片或不同 sigma 的验证图纹理像被磨皮。原因训练时 sigma 固定为 25模型只是在记忆 25 强度下的去噪函数没学会泛化或者 patch 太小模型没见过大尺度纹理。解决训练数据里随机抽取 sigma如 (0, 50)把 patch size 提到 64 以上检验结果时用多种 sigma 的合成图评估而不是只测一个点。真实照片上还要留意传感器噪声不是纯高斯可以先把输入做一次 3x3 小核模糊降低高频差异这是工程临时手段。5.3 现象显存溢出发生在第一轮epoch 0 第一个 batch 直接 OutOfMemoryError。常见原因patch 64、batch 128 在 8G 卡上其实已经很紧如果再加上 num_workers 4 的预读缓存显存会被数据集占用一部分导致爆掉。解决先减 batch 到 64 试试不行就把 patch 降到 48再不行用 torch.utils.checkpoint 对中间激活做检查点用时间换空间。还可以把 DataLoader 的 pin_memory 设为 False许多时候能省出几百兆显存。5.4 现象验证 PSNR 忽高忽低波动超过 1dB同一套测试集这次跑 29.2dB下次变成 28.1dB。原因验证集图片太少或者评估时没有固定随机种子翻转/裁剪数据增强活在验证阶段起作用裁剪位置不同导致计算出的指标不同。解决验证阶段不要用任何随机增强固定 torch.manual_seed并且把整图作为输入而不是随机 patch如果显存不够放整图就滑窗计算再拼接而且窗口之间要有重叠重叠区域取平均避免接缝伪影影响指标。5.5 现象图像边缘出现方块或伪影模型在拼接滑窗推理时边缘有可见的棋盘格或接缝。原因滑窗没有 overlap相邻窗口的 BN 统计量不同边缘像素预测不一致另一个原因是 DnCNN 里的 BN 在推理时对全图统计偏差大它本来是为固定尺寸 patch 训练的。解决滑窗推理时窗口重叠 8 到 16 像素最后在重叠区线性加权平均另一个更干脆的方案是去掉 BN改用残差缩放和可选的噪声水平图输入也就是 FFDNet 的做法速度还更快。如果你坚持用 DnCNN推理时也可以把整图切成 256x256 的 patch保证每个 patch 足够大BN 统计更稳定。6. 验证你的去噪模型没白练PSNR、SSIM 计算与基于 PyTorch 的快速复现技巧模型训练完别急着肉眼对比。肉眼会被亮度差异骗过还是得用指标说话。这里给一段评估代码计算 PSNR 和 SSIM。import os import torch import cv2 import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def evaluate(model, clean_dir, noisy_dir): model.eval() psnr_list, ssim_list [], [] for name in os.listdir(clean_dir): clean cv2.imread(os.path.join(clean_dir, name), cv2.IMREAD_GRAYSCALE).astype(np.float32) / 255.0 noisy cv2.imread(os.path.join(noisy_dir, name), cv2.IMREAD_GRAYSCALE).astype(np.float32) / 255.0 noisy_t torch.from_numpy(noisy).unsqueeze(0).unsqueeze(0).cuda() with torch.no_grad(): pred model(noisy_t).squeeze(0).squeeze(0).cpu().numpy() psnr_list.append(peak_signal_noise_ratio(clean, pred, data_range1.0)) ssim_list.append(structural_similarity(clean, pred, data_range1.0)) return float(np.mean(psnr_list)), float(np.mean(ssim_list))data_range 要根据图像范围填上面对图像做了 /255.0所以是 1.0如果你直接读 0-255 的 uint8这里要填 255。skimage 的 PSNR 对像素范围非常敏感填错了差很多。评估时固定测试集建议用 BSD68 或 Set68 这类公开数据集别人论文里的数值都能对得上。快速复现技巧DnCNN 在 PyTorch 里跑通只需要三要素——残差公式、随机 sigma 数据增强、固定随机种子。测试时加上水平翻转的 TTApred (model(x) model(x.flip(-1)).flip(-1)) / 2PSNR 能稳定高 0.10.2dB。想要更高指标可以考虑 FFDNet把噪声水平图作为额外输入可以用同一个 Dataset 里返回的 sigma 直接拼成单通道特征图再往上就是 CBDNet 这种噪声估计 去噪联合网络但训练复杂度翻倍。我做这个方向的经验是别把 PSNR 当成唯一真理每次保存模型后在真实照片上看低反差纹理比如墙面、头发丝如果纹理变成了塑料就是过拟合了噪声假设。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →