☰
基于卷积神经网络的图像去噪:从DnCNN到U-Net的实战指南
2026/10/10 20:14:56 网站建设 项目流程

简介:本资源为深度学习图像去噪方向的项目源码包,面向计算机视觉学习者、课程大作业与毕业设计需求者,解决去噪算法实现与效果对比的完整实验问题。项目以卷积神经网络DnCNN为核心,同时实现均值滤波、中值滤波、非局部均值NLM与三维块匹配BM3D四种传统算法作为对照组,并采用峰值信噪比PSNR与结构相似性SSIM量化评价去噪效果,五种算法分目录组织,对Set12数据集运行即可查看去噪前后图像对比。压缩包共177个文件,以m脚本、mat数据、png图像及多平台mexw64、mexa64等编译文件为主,整体约80.31MB,目录结构清晰便于按算法模块检索。目前已有691人学习下载,适合希望快速复现经典去噪流程、对比深度学习与传统方法差异并完成高分大作业的读者参考。

1. 从一份「高分大作业」说起:卷积神经网络做图像去噪到底在做什么

你手里如果正躺着一个名为「深度学习基于卷积神经网络的图像去噪算法研究项目源码」的压缩包,大概率是课程设计、毕设或者大作业的交付物。它要解决的问题很具体:给一张被高斯噪声、椒盐噪声或者真实相机噪声污染的图,用训练好的 CNN 模型把干净图还原出来。这件事在深度学习里属于底层视觉任务,和分类、检测不同,它的输出不是标签而是整张图,所以对网络结构、损失函数、训练数据的要求都不一样。

很多人第一次接触图像去噪,会以为「不就是个滤波吗」,高斯滤波、中值滤波、双边滤波确实能去噪,但它们在去噪的同时把边缘和纹理一起抹掉了。CNN 的价值在于它可以从大量「噪声图—干净图」配对样本里学到噪声的统计规律,而不是靠固定核。这个方向适合两类人:一类是要交大作业、需要能跑通并讲清楚原理的学生;另一类是想把去噪当作底层预处理,接到自己检测、分割、超分流程里的工程师。接下来我按「网络怎么选、数据怎么造、代码怎么写、参数怎么调、坑在哪」的顺序,把这份源码背后真正能复现的东西讲透。

2. 卷积神经网络去噪的原理与选型:为什么不是随便堆几层就行

2.1 去噪网络的输入输出关系与残差学习

图像去噪任务的形式化定义很直接:给定观测图 ( y = x + n ),其中 ( x ) 是干净图,( n ) 是噪声,目标是学一个映射 ( F(y) \approx x )。最朴素的做法是让网络直接输出干净图,但实际训练时收敛慢、容易把低频结构也改掉。常见做法是改成残差学习,让网络只预测噪声 ( \hat{n} = F(y) ),再用 ( \hat{x} = y - \hat{n} ) 得到结果。这样网络的学习目标变成噪声图,噪声通常接近零均值,分布更简单,训练更稳。

这个思路最早在 DnCNN 里被系统化,它用堆叠的卷积层加 BN 和 ReLU,把噪声残差学出来。你在源码里如果看到最后一层没有激活函数、损失用 MSE,基本就是这个套路。残差学习的好处是:当噪声很弱时,网络只需要输出接近 0 的残差,不会强行改变原图;当噪声强时,残差项承担主要修正量。对新手来说,记住一句话:去噪网络不是重建整张图,而是估计「哪里被污染了、污染了多少」。

2.2 常见网络结构对比:DnCNN、REDNet、U-Net 怎么选

大作业源码里最常见的三种结构是 DnCNN、REDNet(编码解码+跳跃连接)和 U-Net 变体。它们不是随便选的,对应不同的噪声类型和算力预算。

结构核心特点适合噪声参数量级训练难度
DnCNN纯卷积堆叠+BN+残差高斯噪声、盲去噪约 0.6M低,容易复现
REDNet编码器-解码器+跳跃连接高斯、真实噪声约 1~3M中,需要调学习率
U-Net多尺度下采样+上采样+跳跃混合噪声、医学图像约 7~30M高,吃显存

如果你只是要交作业、机器只有一张消费级显卡,DnCNN 是最稳的选择,17 层左右就能出效果。如果题目要求「研究」并且允许更大模型,REDNet 或 U-Net 能写出更多对比实验。选型时不要只看 PSNR,还要看推理速度:DnCNN 在 256×256 图上通常几毫秒,U-Net 可能几十毫秒,如果后续要接实时流程,这个差距很关键。

2.3 损失函数与评价指标:MSE 之外还要看什么

训练去噪网络最常用的是 MSE(均方误差),它对应 PSNR 指标,优化起来稳定。但 MSE 有个已知问题:它倾向于产生过度平滑的结果,边缘细节会丢。所以很多改进工作会加入 SSIM 损失或者感知损失。源码里如果只写了 MSE,你可以自己加一项 SSIM 项做对比实验,这在作业里是加分点。

评价指标方面,PSNR 和 SSIM 是标配。PSNR 单位是 dB,越高越好,但超过一定值后人眼几乎看不出差别;SSIM 取值 0~1,更贴近结构相似性。实际写报告时,建议同时给出这两个指标,并且附上视觉对比图。注意:PSNR 对亮度偏移很敏感,如果测试时忘了把图像裁剪到 [0,1] 或 [0,255] 一致范围,指标会莫名其妙掉几个 dB,这是血泪经验。

3. 数据准备与训练环境:把「噪声图—干净图」配对造出来

3.1 数据集选择与噪声合成方式

公开数据集里,BSD68、Set12、Urban100 常被用来做去噪评测,但它们主要是干净图,需要你自己加噪声。训练集常用 BSD400 或者 DIV2K 的 patch。合成高斯噪声的公式是 ( y = x + \sigma \cdot \epsilon ),其中 ( \epsilon \sim N(0,1) ),( \sigma ) 控制噪声强度,常见取值 15、25、50。

代码里合成噪声时要注意:图像先归一化到 [0,1],再加噪声,最后裁剪到 [0,1]。如果先加噪声再归一化,噪声强度会被缩放,训练和测试不一致。下面是一个标准的数据对生成片段:

import numpy as np import cv2 def add_gaussian_noise(img, sigma): # img: float32, range [0,1] noise = np.random.randn(*img.shape) * (sigma / 255.0) noisy = img + noise return np.clip(noisy, 0.0, 1.0) # 读取并归一化 clean = cv2.imread('clean.png', cv2.IMREAD_COLOR) clean = cv2.cvtColor(clean, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 noisy = add_gaussian_noise(clean, sigma=25)

这里sigma/255.0是把以像素值为单位的噪声强度换算到 [0,1] 区间。如果你用 0~255 范围训练,就不要再除 255,但整个流程必须统一。参数sigma决定任务难度,25 是论文里最常报告的设置,50 更难,适合展示模型上限。

3.2 训练集 patch 切分与数据增强

整图训练对显存要求高,通常切成 40×40 或 64×64 的 patch。切分时要有重叠,避免边界效应。数据增强可以用随机翻转和旋转,但注意:去噪任务里不要用颜色抖动,因为颜色变化会改变噪声分布假设。

def extract_patches(img, patch_size=40, stride=20): patches = [] h, w, _ = img.shape for i in range(0, h - patch_size + 1, stride): for j in range(0, w - patch_size + 1, stride): patches.append(img[i:i+patch_size, j:j+patch_size]) return np.array(patches) def augment(patches): # 随机水平翻转和旋转90度 out = [] for p in patches: if np.random.rand() > 0.5: p = np.fliplr(p) k = np.random.randint(0, 4) p = np.rot90(p, k) out.append(p) return np.array(out)

patch_size越大,感受野覆盖越完整,但 batch 里能放的样本数变少。stride越小,样本越多,但相邻 patch 高度相关,容易过拟合。我一般用 patch_size=40、stride=20,训练集控制在几万 patch 量级。

3.3 环境配置与依赖版本

源码里通常会带一个 requirements.txt,但版本冲突是常见翻车点。PyTorch 1.10 以上对 CUDA 版本有要求,如果你机器是 CUDA 11.3,就装对应轮子。下面是一个可用的最小环境:

conda create -n denoise python=3.8 conda activate denoise pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html pip install opencv-python numpy scikit-image tqdm

注意torch和torchvision版本要匹配,否则 import 时报 undefined symbol。如果不想折腾 CUDA,用 CPU 也能跑通小模型,只是训练时间从几十分钟变成几小时。验证环境是否正常,跑一句torch.cuda.is_available()看返回。

4. 模型实现与训练脚本:从网络定义到 loss 反向传播

4.1 DnCNN 网络结构的 PyTorch 实现

下面是一个可直接抄的 DnCNN 简化版,17 层,每层 64 通道,去掉 BN 的版本更适合小数据集:

import torch import torch.nn as nn class DnCNN(nn.Module): def __init__(self, depth=17, n_channels=64, image_channels=3): super(DnCNN, self).__init__() layers = [] # 第一层:输入到特征 layers.append(nn.Conv2d(image_channels, n_channels, kernel_size=3, padding=1, bias=False)) layers.append(nn.ReLU(inplace=True)) # 中间层:特征到特征 for _ in range(depth - 2): layers.append(nn.Conv2d(n_channels, n_channels, kernel_size=3, padding=1, bias=False)) layers.append(nn.BatchNorm2d(n_channels)) layers.append(nn.ReLU(inplace=True)) # 最后一层:特征到噪声残差 layers.append(nn.Conv2d(n_channels, image_channels, kernel_size=3, padding=1, bias=False)) self.dncnn = nn.Sequential(*layers) def forward(self, x): noise = self.dncnn(x) return x - noise # 输出去噪图

关键点:padding=1保证卷积后尺寸不变;最后一层没有激活,因为残差可正可负;forward里返回的是x - noise,如果你想让网络直接输出干净图,把最后一行改成return noise并调整 loss 即可。depth和n_channels是主要调节参数,显存不够就减通道数。

4.2 训练循环与学习率调度

训练脚本的核心是:每个 batch 取噪声 patch 和干净 patch,前向算输出,MSE 算 loss,反向更新。下面是一个最小训练循环:

import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DnCNN().to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5) # 假设 noisy_patches, clean_patches 已准备好,形状 [N,3,40,40] dataset = TensorDataset(torch.from_numpy(noisy_patches).float(), torch.from_numpy(clean_patches).float()) loader = DataLoader(dataset, batch_size=64, shuffle=True) for epoch in range(100): model.train() total_loss = 0 for noisy, clean in loader: 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() print(f'Epoch {epoch}, Loss {total_loss/len(loader):.6f}')

lr=1e-3是 Adam 的常用起点,如果 loss 震荡就降到 1e-4。StepLR每 30 个 epoch 减半,防止后期在最优解附近跳。batch_size 受显存限制,64 是 40×40 patch 的常见值。注意:如果 loss 降到 1e-4 以下还在降,可能是过拟合,要拿验证集看 PSNR 是否同步提升。

4.3 验证与模型保存策略

每个 epoch 结束后在验证集上算 PSNR,保存 PSNR 最高的模型,而不是最后一个 epoch 的模型。这是很多人忽略的点:训练 loss 最低不代表去噪效果最好。

def compute_psnr(pred, target): mse = torch.mean((pred - target) ** 2) return 10 * torch.log10(1.0 / mse) best_psnr = 0 for epoch in range(100): # ... 训练代码 ... model.eval() with torch.no_grad(): val_out = model(val_noisy) psnr = compute_psnr(val_out, val_clean) if psnr > best_psnr: best_psnr = psnr torch.save(model.state_dict(), 'best_dncnn.pth')

compute_psnr里假设数据范围是 [0,1],如果你的数据是 [0,255],把1.0换成255.0**2。保存state_dict而不是整个模型,加载时先实例化结构再load_state_dict,这样源码迁移更方便。

5. 避坑与排查:训练不收敛、指标虚高、显存爆炸的常见原因

5.1 现象:loss 一直停在 0.1 不下降

原因通常是数据范围不一致。比如干净图是 [0,255],噪声图是 [0,1],MSE 会被大数值主导,梯度方向混乱。解决:在数据加载后统一除以 255,或者统一不除,但训练和验证必须一致。检查方法:打印一个 batch 的 min/max,确认都在同一区间。

5.2 现象:PSNR 很高但视觉上全是噪点

这是典型的「指标虚高」翻车。原因可能是测试时用了训练集里的图,或者噪声合成时 sigma 设得太小。解决:严格划分训练/验证/测试集,测试集图像不参与任何训练;报告指标时注明 sigma 值。如果 PSNR 超过 40dB 但图还是花的,检查是不是把干净图直接当预测结果输出了。

5.3 现象:训练到一半 CUDA out of memory

原因:patch 太大、batch 太大、模型通道数太多,或者忘了torch.no_grad()导致验证时也建计算图。解决:先把 batch_size 减半,再把 patch 从 64 降到 40;验证和推理时用with torch.no_grad():;如果还不够,把n_channels从 64 降到 32。显存爆炸时不要盲目加 swap,会拖慢训练。

5.4 现象:验证集 PSNR 波动很大,时高时低

原因:验证集太小,或者 BN 层在 batch 很小时统计量不稳定。解决:验证集至少放 10 张以上图;如果 batch_size 小于 8,考虑把 BN 换成 InstanceNorm 或者去掉 BN。DnCNN 原论文用 BN 是因为 batch 够大,小数据集上去掉 BN 反而更稳。

5.5 现象:加载源码里的预训练权重报 key 不匹配

原因:源码作者可能改了层名或者用了 DataParallel 保存,权重 key 带module.前缀。解决:用torch.load后手动去掉前缀,或者用model.load_state_dict(state_dict, strict=False)先跑通,再打印缺失的 key 逐个补。不要直接改源码结构去迁就权重,容易引入新 bug。

6. 进阶技巧:把去噪模型用到真实噪声和盲去噪上

合成高斯噪声的模型放到真实照片上,效果通常会掉一截,因为真实噪声不是加性高斯的,还跟 ISO、曝光时间有关。一个实用技巧是:在合成噪声之外,加入少量真实噪声配对数据做微调。如果没有真实配对,可以用「噪声估计 + 盲去噪」的思路,先估出噪声水平图,再让网络以噪声水平为条件输入。

具体做法是在 DnCNN 的输入里多拼一个通道,表示每个像素的噪声估计值。噪声估计可以用小波变换或者 PCA 方法先算一个粗略图。代码上只需要把image_channels从 3 改成 4,第一层卷积对应改一下,训练时把噪声图拼上去。这个改动不大,但在作业报告里可以写成「盲去噪扩展实验」,是很好的加分项。

另一个技巧是测试时增强(TTA):对同一张噪声图做 8 种翻转旋转,分别推理后再逆变换平均。这样 PSNR 通常能涨 0.1~0.3dB,代价是推理时间乘 8。如果只是交作业,这个技巧性价比很高,代码也好写:

def tta_denoise(model, noisy): outputs = [] for k in range(4): for flip in [False, True]: x = np.rot90(noisy, k) if flip: x = np.fliplr(x) x_t = torch.from_numpy(x.copy()).permute(2,0,1).unsqueeze(0).float() with torch.no_grad(): out = model(x_t).squeeze(0).permute(1,2,0).numpy() if flip: out = np.fliplr(out) out = np.rot90(out, -k) outputs.append(out) return np.mean(outputs, axis=0)

注意np.rot90后要copy(),否则负 stride 会让 PyTorch 报错。这个函数里 8 次推理可以改成 batch 一次前向,速度更快。最后平均时用np.mean而不是加权,因为各变换等价。

我自己做这类项目时,习惯先把 DnCNN 在 sigma=25 上跑通,确认 PSNR 能到 29dB 左右,再去改结构和加技巧。如果基线都没跑通就上 U-Net,最后连问题出在数据还是模型都分不清。希望帮到你。

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

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

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

立即咨询