拓冰建站拓冰建站
首页 / 资讯中心 / 正文

DnCNN图像去噪实战:TensorFlow实现与残差学习原理详解

简介这是基于深度卷积神经网络DnCNN的图像去噪算法资源面向图像处理初学者、深度学习者及需要去除高斯噪声的研发人员采用Python与TensorFlow实现兼顾理论学习与工程实验。资源包共含45个文件以jpg与png图像样本为主另有4个py源码文件完整网络定义、训练与测试配置、说明文档及许可协议压缩包仅1.39MB结构清晰便于对照学习。基于DnCNN的多层卷积与反卷积结构配合自适应去噪方法可动态调节策略以保留图像细节示例图片直观展示了加噪与去噪前后对比训练结果图则呈现去噪效果随迭代步数的变化。已有1691人学习下载适合用来自行复现模型、调整参数或迁移至其他噪声类型是理解深度图像去噪流程的实用入门资料。1. 深度卷积神经网络图像去噪这份 DnCNN 资源能直接解决什么问题拿到一张夜景照片暗部噪点密密麻麻拿中值滤波一磨噪点是没了纹理也跟着糊成一片。深度学习去噪这几年能替代传统滤波靠的不是把图抹平而是让网络自己学会区分噪声和真实边缘。DnCNNDeep Image Denoising with Convolutional Neural Networks是这类方法里最有代表性的一个这份资源就是它的 TensorFlow 实现Python 环境下直接可用。压缩包里除了完整训练代码 DnCNN.py、网络定义 network.py还带着已经训练好的参数目录 save_para——意味着你不用自己训上几天直接加载参数就能给高斯噪声图像去噪。同时附有 TrainingSet 训练图、TestingSet 测试图和前后对比样张适合正在做图像去噪毕业设计、想快速复现经典论文效果、或者打算在现有项目里接入深度学习去噪的工程师。下面把网络结构、运行步骤和我实际踩过的坑讲清楚。2. DnCNN 网络结构拆解残差学习为什么比直接输出干净图更稳2.1 卷积层、BN 与 ReLU 的分工DnCNN 的基本单元是“卷积 批量归一化 ReLU”三件套这个组合在 network.py 里被反复堆叠。三者的分工很明确卷积层负责提取局部特征把每个像素周围 3x3 区域里的灰度变化模式编码成特征图批量归一化Batch NormalizationBN负责把每层输入拉回均值为 0、方差为 1 的分布这样即使网络加深梯度也不会因为层间分布漂移而消失或爆炸ReLU 则把负值全部置零给网络引入非线性表达能力。没有 ReLU无论堆多少层卷积本质上还是线性变换学不出复杂噪声模式。一个容易被忽略的细节是这三者的顺序。DnCNN 用的是 Conv → BN → ReLU而不是常见的 Conv → ReLU → BN。原因是 BN 要统计当前 batch 的特征分布如果先让 ReLU 把负值截断统计到的分布就是有偏的批量归一化的效果会被削弱。项目里 network.py 和 ops.py 的分工也很清楚ops.py 封装卷积、BN 这些底层算子network.py 负责把它们按顺序拼成完整网络。核心块实现大致是下面这个样子def conv_block(inputs, out_channels, is_training): conv tf.layers.conv2d( inputs, out_channels, kernel_size3, paddingsame, kernel_initializertf.truncated_normal_initializer(stddev0.01)) bn tf.layers.batch_normalization(conv, trainingis_training) relu tf.nn.relu(bn) return relu这段代码的逻辑说明tf.layers.conv2d 做标准二维卷积kernel_size3 意味着每个输出像素只看输入上 3x3 邻域paddingsame 保证输出特征图尺寸和输入一致方便后续做残差减法。kernel_initializer 用截断正态分布初始化权重标准差 0.01 是为了让初始权重尽量小避免网络一开始就输出离谱的大数值这在我第一次训练时省了不少事。BN 层必须显式传入 training 标志训练时用当前 batch 的统计量推理时用训练阶段累积的滑动平均二者混用会直接导致测试结果变差这一点在后面排查篇会专门说。2.2 残差学习让网络去学噪声而不是学图像DnCNN 论文里最核心的改动不是加深网络而是改变学习目标。设干净图像为 x噪声为 n观测到的含噪图像 y x n。大部分传统 CNN 去噪网络学的是 y → x 的映射也就是直接预测干净图。DnCNN 学的是 y → n 的映射预测的是纯噪声残差图然后用 y - n 得到干净结果。这个选择在训练上收益很大噪声 n 的数值范围通常远小于图像内容残差图绝大部分区域接近 0网络要拟合的目标函数简单得多收敛速度和稳定性都更好。而直接回归 x等于同时拟合平坦区域、纹理、边缘和噪声优化路径长还容易把纹理细节抹掉。简单说残差学习把“画一张干净图”这个难任务拆成了“估计一层噪声”这个相对容易的任务。项目中 README 提到的 ADM 自适应降噪思路本质上也是在这个基础上加了一点策略根据图像局部方差动态调整残差强度平坦区域多去一点纹理区域少去一点避免过度平滑。在代码里网络最后一层输出的是单通道残差图灰度图场景干净图像通过减去残差得到# 假设 model 输出的是残差图 residual residual model(noisy_input, is_trainingFalse) clean_output noisy_input - residual逻辑说明noisy_input 是归一化到 [0, 1] 区间的含噪图像residual 是网络预测的噪声估计。两者形状必须完全一致减完后得到的 clean_output 就是去噪结果。这里有个容易翻车的点如果网络输出层带了 ReLU 或者 BN残差值会被强行截断或重新缩放导致减法结果偏色或出现伪影。所以最后一层一般只保留纯卷积不做任何归一化和非线性激活这一点在改代码时要特别留意。2.3 感受野与深度为什么是 17 层而不是 5 层高斯噪声的特点是像素级独立扰动单看一个像素无法判断它是噪声还是真实细节必须借助周围像素的上下文。卷积网络的感受野决定了每个输出像素能“看到”多大范围的输入信息。对于 3x3 卷积每堆一层感受野向外扩 1 个像素n 层之后感受野尺寸是 1 2n。17 层对应 35x35 的感受野也就是网络判断每个像素是否含噪时会综合考虑它周围 35x35 区域内的亮度统计这个范围对高斯噪声的判别已经足够。项目超参数集中在 config.py我一般会先看三个值层数 depth、每层通道数 channel、以及噪声等级 sigma。默认配置下 DnCNN 每层用 64 个特征图通道数越多表达能力越强但显存占用和训练时间同步上涨。如果你只是想验证效果channel64、depth17 是性价比最高的组合不建议在这个基础上盲目加深深度超过 20 层之后 PSNR 提升非常有限训练时间却成倍增加。训练时把大图裁剪成 patch 也是基于同样的考量——让每张输入图的尺寸和感受野适配同时把显存占用控制住。提示判断网络是否用了残差学习最快的方法是看输出层后面有没有一行 y - residual 的减法操作。很多网上流传的“DnCNN 实现”其实只是普通卷积自编码器结构像但学习目标完全不同跑出来的效果也差一个档次。3. 跑通这份 TensorFlow 代码从文件结构到训练与推理3.1 压缩包结构与文件职责先把压缩包里每个文件的作用理清楚避免拿到手里不知道先跑哪个。整个工程的核心文件可以按职责分成几组DnCNN.py 是训练和推理的主入口network.py 负责构建卷积网络结构ops.py 封装了卷积、批量归一化这些底层算子config.py 放着学习率、batch size、噪声等级等全部超参数。save_para 目录是训练完成后保存的模型参数TrainingResults 目录里能看到训练过程中不同迭代步数的输出图像比如 0_1440.jpg 表示第 1440 步时网络对验证图给出的去噪结果。IMAGES 目录装的是去噪前后的对比样张noised1.jpg 是加了噪声的输入denoised1.jpg 是对应的去噪输出。文件清单如下文件/目录职责说明DnCNN.py训练与推理主入口network.py网络结构定义ops.py底层卷积、BN 算子封装config.py超参数配置save_para已训练模型参数TrainingSet训练图像1_20.jpg 等TestingSet测试图像01.png 到 12.pngTrainingResults训练过程输出图像IMAGES去噪前后对比样张这个文件组织的运行顺序很直观先用 config.py 配好超参数然后跑 DnCNN.py 里的训练入口加载 TrainingSet 开始训练边训练边往 TrainingResults 写中间结果。如果想直接验证效果跳过训练从 save_para 加载已保存参数对 TestingSet 里的 01.png 到 12.png 做推理就行。README.md 里写明了基本的运行说明README 的注意事项我建议在动代码前先读一遍有些环境兼容性问题提前知道能省半小时。3.2 训练合成高斯噪声、MSE 损失与 Adam 优化DnCNN 训练阶段用的是合成噪声图像对。从 TrainingSet 里取干净图按指定 sigma 叠加高斯噪声得到 (noisy, noise) 作为监督信号def add_gaussian_noise(clean, sigma): # 输入 clean 为 float32取值范围 [0, 1] noise np.random.randn(*clean.shape).astype(np.float32) * sigma noisy np.clip(clean noise, 0.0, 1.0) return noisy, noise # 训练时的一个 batch batch_clean load_batch_from_training_set(config.TRAIN_PATH, config.BATCH_SIZE) batch_noisy, batch_noise add_gaussian_noise(batch_clean, config.SIGMA)逻辑说明np.random.randn 生成标准正态分布随机数乘以 sigma 后就是符合 N(0, sigma^2) 的高斯噪声。sigma 的单位是像素灰度值如果图像归一化到 [0, 1]sigma 通常取 0.01 到 0.05 之间如果图像范围是 [0, 255]sigma 就取 1 到 15。clip 把越界像素拉回合法区间防止训练数据里出现 NaN。返回的 noise 是干净噪声图之后要作为标签喂给网络不能直接拿 noisy 和 clean 的差值来替代因为 clip 操作会破坏差值的线性关系。有了数据之后训练目标就是让网络输出的残差逼近真实噪声。损失函数用 MSE优化器用 Adamresidual dncnn(batch_noisy, is_trainingTrue) loss tf.reduce_mean(tf.square(residual - batch_noise)) train_op tf.train.AdamOptimizer(learning_rateconfig.LR).minimize(loss)逻辑说明tf.square 计算逐像素误差的平方reduce_mean 取平均得到标量损失。learning_rate 我一般从 1e-3 起步每训练 30 个 epoch 降到原来的一半。Adam 的默认 beta10.9、beta20.999 在 DnCNN 场景下表现稳定不需要额外调整。注意这里的标签是 batch_noise 而不是 batch_clean这是残差学习和普通卷积自编码器最大的区别。如果你看到别人的代码里 loss 是网络输出和 clean 的 MSE那它学的就是 y → x 映射不是严格意义上的 DnCNN。训练时还有个工程细节大图不能整张喂进去因为 3x3 卷积加 64 通道的中间特征图显存占用和分辨率成正比。常见做法是把训练图像随机裁剪成 40x40 到 50x50 的 patch每个 batch 放 16 到 32 个 patch。项目里 TrainingSet 的图像都是几百像素级别的直接整图训练会爆显存。这里我先用一个简单实现示意def extract_patch(image, patch_size50): h, w image.shape[:2] x np.random.randint(0, w - patch_size) y np.random.randint(0, h - patch_size) return image[y:y patch_size, x:x patch_size]逻辑说明x、y 是随机裁剪起点保证 patch 不越界。裁剪的目的有两个一是制造足够多的训练样本二是让每个 batch 里有不同位置的图像内容相当于一种数据增强。patch_size 设太大会让显存吃紧设太小又会让感受野超过图像尺寸模型学不到足够的上下文信息。我在 4G 显存的 GPU 上用 50x50 patch、batch size 16 跑得很稳如果你显存更小就降到 8。3.3 推理加载参数对 TestingSet 输出去噪图推理阶段比训练简单得多关键在于加载参数时保持变量名一致以及输入数据必须做和训练时相同的归一化import tensorflow as tf # 构建网络is_training 必须为 False noisy_ph tf.placeholder(tf.float32, shape[None, None, None, 1]) clean_output dncnn(noisy_ph, is_trainingFalse) saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, tf.train.latest_checkpoint(save_para)) test_image load_and_normalize(TestingSet/01.png) # 已归一化到 [0, 1] result sess.run(clean_output, feed_dict{noisy_ph: test_image}) save_image(result[0], denoised_01.png)逻辑说明restore 之前必须先完整构建网络图因为 TensorFlow 保存的是变量名和变量值图结构对不上会直接报错。test_image 的维度是 [batch, height, width, channels]灰度图 channels 为 1。clean_output 是从占位符进去再从占位符减残差得到的去噪结果feed_dict 里只喂了 noisy_ph 一个输入。推理时把 is_training 固定为 False否则 BN 层会尝试更新滑动平均输出结果不稳定。save_image 保存时记得把结果从 [0, 1] 还原到 [0, 255] 并转成 uint8否则用 PIL 保存会得到一张全黑的图。实际使用中如果你不想自己写这部分代码项目里 save_para 目录下有已经训练好的 checkpoint直接复用 DnCNN.py 里封装好的推理入口就能跑通。我第一次跑的时候没有细看输出层是否带了 ReLU结果去噪图整体偏暗后来检查发现是最后一层多了一个激活函数去掉之后图像亮度才恢复正常。这类问题靠看数值很难发现把去噪前后的直方图列出来对比就一目了然。4. 常见问题排查复现 DnCNN 时我踩过的五个坑4.1 训练 loss 不降反升现象训练前几个 epoch 的 loss 从 0.1 涨到 0.5或者一直在震荡不下降。原因最常见的是学习率设太大Adam 在初始阶段步长过大会跳过最优区域其次是输入图像没有归一化到一致范围导致梯度尺度不稳定不同 batch 之间的更新方向互相抵消。解决把学习率降到 1e-4 起步等 loss 平稳后再调回 1e-3训练前确认 clean 图像、noisy 图像、网络输出三个值范围一致统一先除以 255 归一化到 [0, 1]。如果归一化范围是 [-1, 1]那么 BN 层的统计量也会跟着变化训练曲线表现可能完全不同。4.2 去噪输出全灰或全黑现象训练结束后推理去噪图整体灰蒙蒙或者直接全黑但网络输出的 loss 看起来正常。原因最常见是网络输出层带了 ReLU把负残差值截断成 0减法时 y - residual 变成了 y - 0等于没去噪另一种可能是在预处理时做了去均值操作把图像均值移到 0但没有在输出端把均值加回去导致整体灰蒙蒙。解决检查最后一层卷积后面是否跟了激活函数去掉并确保归一化是线性的除以 255不要做去均值。如果已经去均值了就在输出端把均值加回去。这个问题的排查思路很简单单独打印一张残差图如果残差图里没有任何负值说明激活函数截断了一定存在。4.3 测试 PSNR 反而输给 BM3D现象用训练好的模型去测标准测试图PSNR 比传统 BM3D 算法还低 1~2 dB肉眼看起来也没有明显优势。原因训练时的 sigma 和测试时图像的噪声强度不匹配。如果模型以 sigma25 训练测试图实际噪声 sigma70模型会欠拟合输出模糊。BM3D 在测试时会自己估计噪声等级而 DnCNN 的噪声知识全部固化在权重里固化在权重里的 sigma 和测试图不匹配结果就会很差。解决训练时用混合噪声强度让 sigma 在 10 到 60 之间随机采样推理时对不同强度的噪声都有一定泛化能力或者针对固定噪声场景单独训练专用模型。项目里如果只配了单一 sigma 的预训练参数优先在 config.py 里改成多 sigma 采样再训练。4.4 显存不足OOM现象训练刚开始就报 Resource exhausted: OOM进程直接退出。原因整张大图直接喂进网络或者 batch_size 设置得过大。3x3 卷积加 64 通道第 17 层的中间特征图尺寸和输入一样但通道数是 64 倍一张 500x500 的图走到最后单是一层的特征图就要占超过 60MB 显存十几层叠起来非常可观。解决把训练图像裁剪成 40x40 或 50x50 的 patch 输入batch_size 从 16 起步显存紧张就降到 8。推理时大图可以按滑动窗口分块处理窗口之间留 8~10 像素重叠结果再拼接避免边缘因为感受野缺失而产生接缝。这个办法我在处理 4K 图像时也用过效果稳定。4.5 图像颜色不对或通道错乱现象彩色图去噪后颜色偏色或者输出是黑白但输入是彩色程序没有报任何错误。原因训练时用了单通道灰度图推理时直接喂了 RGB 三通道通道数不一致或者 RGB 图像的通道顺序在预处理时被转置了。TensorFlow 的 conv2d 会按最后一维做卷积如果输入是 [height, width, 3]网络参数却按 1 通道初始化加载 checkpoint 时要么报形状不匹配要么静默出错。解决统一通道数。灰度图是 [height, width, 1]彩色图是 [height, width, 3]。如果预训练模型是灰度训练就把彩色图先转灰度再推理如果要用彩色模型必须用 3 通道数据从头训练。检查代码里 imageio 或 cv2 读图后有没有把通道维度丢掉cv2 读出来是 HWC如果中间插了 tf.image 相关操作通道顺序可能会变成 CHW这也是一个隐蔽的坑。5. 去噪效果怎么看PSNR/SSIM 指标与结果文件对照5.1 两个指标说明什么问题PSNR 全称峰值信噪比是一个纯数值指标计算方式很简单import numpy as np def psnr(clean, denoised, max_val255): mse np.mean((clean.astype(np.float64) - denoised.astype(np.float64)) ** 2) if mse 0: return float(inf) return 10 * np.log10(max_val ** 2 / mse)逻辑说明先算逐像素均方误差再用峰值灰度值的平方除以 MSE取对数乘 10。max_val 是图像可能的峰值灰度值归一化到 [0, 1] 时传 1[0, 255] 时传 255。这个指标的单位是 dB通常超过 30 dB 肉眼就看不太出明显噪声。DnCNN 在 sigma25 的 BSD68 数据集上一般能到 29~31 dB。但 PSNR 有个问题它对结构性细节不敏感图像模糊和噪声被抹平两种错误可能给出相近的 PSNR 值。SSIM 结构相似度则更接近人眼感知它综合比较亮度、对比度和结构三项。计算时会在局部窗口内做统计所以即便整体 PSNR 一样SSIM 高的那张图看起来通常更锐利。两个指标结合使用才能全面地评估去噪质量我自己的习惯是 PSNR 为主、SSIM 为辅两者都报才有说服力。指标反映内容常见值范围PSNR像素级误差25~35 dB越高越好SSIM结构保持程度0.85~0.99越接近 1 越好5.2 文件对照与结果验证项目 IMAGES 目录下有一组 noised*.jpg 和 denoised*.jpg 同名文件比如 noised1.jpg 和 denoised1.jpg 就是同一张图的含噪输入和去噪输出可以直接用眼睛对比也可以跑一次上面的 psnr 函数得到数值。TestingSet 里有 01.png 到 12.png 共 12 张标准测试图加载 save_para 里的参数推理后可以逐张计算 PSNR 和 SSIM和论文报告值做对照。TrainingResults 里 0_1440.jpg、0_1520.jpg、0_1600.jpg 展示了网络在不同训练步数下的去噪能力你会发现 1440 步时还残留部分噪声1600 步时明显干净这也是判断模型有没有训练到位的一个直观依据。我自己验证时习惯在同一个脚本里把 noised、denoised、clean 三张图都打印出来拼成一张对比图放在同一个目里逐张看。只看数值容易忽略模板化的伪影只看图又缺乏量化依据两个同时做才能确认模型是真的学会了去噪还是只学会了降低对比度。测试时记得不要用训练集里的图TensorFlow 模型的记忆能力很强训练图喂进去 PSNR 会虚高这个现象在图像去噪里比分类任务更明显。5.3 和 BM3D 等传统算法对比DnCNN 强在哪里BM3D 这类传统算法需要先估计噪声的统计特性比如假设它是高斯噪声并且给定 sigma然后用块匹配和协同滤波处理。DnCNN 不一样它把噪声的统计特性直接学进了网络权重里训练时如果见过多种 sigma 的噪声推理时就自动适配不需要知道测试图的噪声等级。另外传统方法在纹理密集区域容易把细节磨掉DnCNN 因为有残差学习和足够大的感受野细节保持能力明显强一些。对比项BM3DDnCNN是否需已知噪声等级需要不需要计算耗时中等纯 CPU 可跑需要 GPU纹理保持一般较好可迁移场景换噪声类型需重调参数微调网络即可这个对比不是否定 BM3D。在单一噪声等级、计算资源有限的情况下BM3D 依然是很强的基线而且不需要训练。但如果你面对的是噪声类型不确定的真实图像DnCNN 的优势就体现出来了——省去了噪声估计这一步一张图直接进去干净图直接出来。项目里 IMAGES 目录的 method.jpg 看起来就是这张方法流程的可视化从含噪输入到网络处理再到干净输出一条线走完。6. 进阶改噪声等级、滑动窗口推理与微调技巧如果你已经把上面的流程完整跑通了下一步值得做的是把模型从“固定 sigma 去噪”升级成“多噪声等级自适应去噪”。改动核心在 config.py 里把固定 SIGMA 改成从列表里随机采样比如 [10, 25, 40, 55] 四个等级每个 batch 用不同等级的高斯噪声训练。这样训出来的模型对噪声强度不那么敏感实际部署时不需要知道图像到底被污染到什么程度。我习惯在推理阶段对超过 1K 分辨率的大图做滑动窗口拼接而不是整图直接进网络。窗口取 200x200重叠 10 像素输出后按位置拼回原尺寸重叠区域取平均。这个做法的好处是显存占用恒定坏处是速度慢但对工程应用来说稳定第一。做微调时还有一个细节加载 save_para 的预训练参数后把学习率调到 1e-5 以下只对最后两到三层的权重做更新避免破坏前面层已经学好的特征提取能力。项目里 ADM 自适应降噪的策略也可以在这个阶段结合在推理时对图像的平坦区域和纹理区域分别处理用一个简单的局部方差阈值区分两者能让高处纹理保持得更好。从那次在最后一层多写了一个 ReLU 导致全黑图之后我每次跑 DnCNN 都会强制先走一遍流程确认输入通道数、确认图像归一化范围、确认输出层没有多余的激活函数、确认 is_training 在推理时是 False。四个确认下来百分之九十的坑都在训练前就被排掉了。希望这套流程也对你有帮助。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门