
简介一套基于深度学习的图像去雨配套资源面向有一定TensorFlow基础的图像处理学习者和研究者系统展示如何利用卷积网络将雨滴从背景中分离解决雨天图像清晰度下降问题。包内共37个文件以Python脚本训练、测试及导向滤波辅助代码、TensorFlow模型检查点与权重数据index、meta、data分片、JPG样本图片为主另附算法原理PDF、说明文档与数据目录压缩包整体约53.79MB结构清晰便于按模块检索。内容覆盖雨滴合成数据集构建、多阶段CNN结构设计、残差学习与U-Net跳跃连接、MSE/SSIM损失函数选择等完整环节并提供可运行的训练与测试脚本读者既能从零训练去雨模型也可加载现成权重直接预测测试图像。已有3811人学习下载适合希望快速上手图像去雨实践并进一步拓展到去噪、去雾等低层次视觉任务的开发者。1. 图像去雨为什么难雨纹不是简单的噪声下雨天抓拍的照片总蒙着一层或密或疏的雨丝人脸、车牌、监控画面全被糊住。图像去雨Image Deraining不是简单的高斯去噪因为雨纹在空间上是局部的、在形态上是条状的、在语义上还遮挡了背景纹理。传统方法用稀疏编码或低秩模型手工设计雨纹先验遇到真实雨景往往力不从心。基于深度学习的方案把这个问题转成“从雨图映射到干净图”的监督学习只要有成对的训练数据就能端到端训练。这份资源是一套基于TensorFlow的完整去雨项目代码覆盖了合成雨图数据准备、网络结构选型、训练调参到评估部署的全过程每一步都有可运行的代码和参数说明。适合正在入门底层视觉深度学习的开发者也适合想在监控、自动驾驶、影像存档场景里快速落地去雨能力的人新手能照着跑通熟手能直接改网络和损失函数。2. 去雨网络怎么搭从物理模型到CNN与残差架构搞深度学习的都知道数据决定上限模型决定下限。去雨也不例外但这里的数据和模型之间还有一个隐藏的桥梁——雨纹的物理模型。先把这层关系弄清再谈网络结构才不会拍脑袋。2.1 雨图合成与数据准备合成雨图的真实性从哪来去雨任务常见做法是用合成数据训练因为真实世界很难同时拍到“有雨”和“无雨”的同场景对照图。合成雨图的基本假设是叠加模型O B RO 是观测到的雨图B 是干净背景R 是雨纹层。这个模型虽然简单但足够支撑大多数CNN方案。实际合成时还要考虑雨纹的多样性雨滴大小、方向、密度、模糊程度都不一样。如果只用几种固定雨纹模型很快过拟合换到真实雨天就翻车。我一般会用多组参数来合成雨图。关键是让雨纹的位置随机、长度随机、透明度随机并且叠加两次以上模拟不同景深下的雨滴。代码包里提供了一个合成脚本核心逻辑是生成随机方向的运动模糊线条再以不同alpha叠加到干净图上。需要注意合成时最好把雨纹像素值控制在0到255之间避免叠加后溢出导致背景发白。数据准备阶段还有两个容易被忽略的点。一是训练集和验证集要按场景分开比如室内、户外、夜晚各留一部分不能随机切分否则验证集里可能出现和训练集高度相似的雨纹指标虚高。二是要做在线数据增强随机裁剪、水平翻转、旋转10度以内让模型对雨纹位置不敏感。裁剪尺寸我一般用128×128太小背景信息不足太大训练速度会明显变慢。这套代码里用的是TFRecord格式打包数据虽然写起来比直接读文件夹麻烦但训练时IO瓶颈会小很多。尤其是几百GB的合成雨图直接读JPEG会让GPU等CPU训练效率大打折扣。TFRecord的解析函数里要注意图片解码后的形状对齐通道数统一为3像素归一化到0-1之间再输入网络。2.2 网络结构选型残差学习、多尺度特征与空洞卷积去雨网络五花八门从早期的DeRainNet到后来的RESCAN、PReNet核心都在回答一个问题怎么从雨纹中恢复出干净的背景纹理。直接让网络回归干净图往往会把背景细节磨掉输出过度平滑。更聪明的做法是让网络去学“雨纹残差”也就是输入雨图减去输出雨纹得到干净图。这样网络只需要关注雨纹那一部分背景纹理可以原样保留。这套代码里用的就是一个残差学习框架输入雨图经过若干残差块输出雨纹图Rain Map最终干净图用输入减去雨纹图得到。残差块的结构借鉴了ResNet的思想每个块包含两组卷积、批归一化、ReLU再通过一个shortcut连接。shortcut的意义在于网络在训练初期可以直接把输入拷贝到输出雨纹预测为零比从头学一个恒等映射容易得多。实际训练中我也验证过有残差连接的训练曲线比无残差的稳定很多PSNR能高1dB以上。多尺度特征也是去雨的关键。雨纹有大有小粗雨丝和细雨滴在尺度上差异很大。代码里的网络在中间层加入了不同空洞率dilation rate的卷积相当于在不增加参数的前提下扩大感受野。比如一个3×3卷积dilation rate2时感受野变成5×5rate4时变成9×9。用三个并行分支分别处理不同感受野最后concat起来能让网络同时捕捉雨纹边缘和背景纹理。我在调参时发现空洞率设置过大反而会引入棋盘格伪影一般取1、2、4比较稳。除了网络结构激活函数也值得说道。早期去雨网络喜欢用ReLU但ReLU会把负响应直接截断而雨纹残差有时是负值比如暗背景上叠了亮雨丝。我建议把最后一个预测雨纹的卷积层前的激活换成LeakyReLU或者干脆去掉激活让输出可以取负值。代码里默认的做法是最后一层不加激活配合残差学习效果比ReLU收敛快。2.3 损失函数组合L1、感知损失与边缘损失的配比损失函数是去雨模型的指挥棒。最常见的做法是直接用L2损失MSE但L2会让网络倾向于生成模糊的平均结果因为它在像素级上惩罚的是欧氏距离的平方对离群点雨纹边缘过度敏感。L1损失在去噪任务里通常比L2好它只惩罚绝对差生成的图像更锐利。这套代码里默认用的是L1损失实验对比中PSNR比L2高0.3dB左右SSIM高0.02。不过纯L1不够它只看像素值不管感知结构。两个图片像素差相同但一个纹理清晰一个糊成一团L1是分不出来的。所以在代码里我加了一个感知损失Perceptual Loss做法是把预测图和干净图同时送进一个预训练好的VGG16网络取某个中间层的特征图算L1距离。感知损失能让网络在语义层面更接近干净图但不是所有任务都需要它如果目标只是跑通流程可以先不加等训练稳定后再加。边缘损失也是一个常用辅助。因为雨纹边缘和背景边缘在梯度域有明显差异可以用Sobel算子计算水平、垂直梯度然后对预测图与干净图的梯度差做L1惩罚。边缘损失的权重不宜过大否则模型会过度增强边缘产生振铃效应。代码里的配比是主损失L1权重1.0感知损失权重0.1边缘损失权重0.05。这个配比不是我凭空拍的是在一个模拟项目X上调过几轮的结果换数据集后可以按实际情况微调。还有一个小细节计算损失时不要把整张图都算进去。输入网络前会做一个特定尺寸的random crop在裁剪区域上计算损失即可不用全图。因为合成雨图里雨纹分布不均匀全图损失会被大片无雨区域稀释网络学不到雨纹的精细结构。3. TensorFlow实现去雨模型数据管道、训练循环与参数设置理论聊完进入动手环节。这套代码基于TensorFlow 2.x用tf.keras搭建模型支持TensorFlow 2.6以上版本。环境建议装好CUDA和cuDNNGPU显存至少6GB才能跑128×128的batch size 16。下面逐个模块拆开讲。3.1 数据管道构建tf.data读图、增强与批处理用tf.data建管道是TensorFlow的标准姿势。先写一个解析TFRecord的函数把序列化的图片字节解码成张量def parse_tfrecord(example): feature_desc { rainy: tf.io.FixedLenFeature([], tf.string), clean: tf.io.FixedLenFeature([], tf.string), } parsed tf.io.parse_single_example(example, feature_desc) rainy tf.image.decode_jpeg(parsed[rainy], channels3) clean tf.image.decode_jpeg(parsed[clean], channels3) rainy tf.cast(rainy, tf.float32) / 255.0 clean tf.cast(clean, tf.float32) / 255.0 return rainy, clean这段代码把JPEG字节流解码成三维张量然后转float32并归一化到0-1区间。注意decode_jpeg的channels参数必须显式写成3否则灰度图会被读成单通道后面模型输入形状就错了。再写一个训练用的增强函数随机裁剪和翻转def augment(rainy, clean): combined tf.concat([rainy, clean], axis-1) combined tf.image.random_crop(combined, (128, 128, 6)) combined tf.image.random_flip_left_right(combined) rainy combined[..., :3] clean combined[..., 3:] return rainy, clean这里把雨图和干净图在通道维度拼接一起做随机裁剪保证裁剪位置一致。如果不拼接两个图各自随机裁剪的位置对不上训练就废了。random_crop的尺寸参数是(128, 128, 6)6是两图的通道数之和。翻转只做水平翻转垂直翻转会让雨纹方向颠倒不合常理。构建训练集和验证集def build_dataset(tfrecord_paths, batch_size16, is_trainingTrue): dataset tf.data.TFRecordDataset(tfrecord_paths) dataset dataset.map(parse_tfrecord, num_parallel_callstf.data.AUTOTUNE) if is_training: dataset dataset.map(augment, num_parallel_callstf.data.AUTOTUNE) dataset dataset.shuffle(buffer_size1000).repeat() dataset dataset.batch(batch_size) dataset dataset.prefetch(buffer_sizetf.data.AUTOTUNE) return datasetshuffle的buffer_size不要设太大1000张够用了太大占内存。repeat()用于无限循环配合训练步数控制epoch。验证集不需要repeat也不需要shuffle。3.2 模型定义从输入到输出残差块的实现细节模型代码分两个部分残差块和整体网络。先看残差块def residual_block(x, filters32, dilation_rate1): shortcut x x tf.keras.layers.Conv2D(filters, 3, paddingsame, dilation_ratedilation_rate, kernel_initializerhe_normal)(x) x tf.keras.layers.BatchNormalization()(x) x tf.keras.layers.LeakyReLU(alpha0.2)(x) x tf.keras.layers.Conv2D(filters, 3, paddingsame, dilation_ratedilation_rate, kernel_initializerhe_normal)(x) x tf.keras.layers.BatchNormalization()(x) x tf.keras.layers.LeakyReLU(alpha0.2)(x) return x shortcutconv层的paddingsame保证特征图尺寸不变。dilation_rate参数是前面说的空洞卷积这里把它暴露出来方便在下面调用时给不同分支设置不同的膨胀率。kernel_initializer用he_normal是因为后面跟LeakyReLU适合ReLU族的激活。整体网络是几条分支的拼接def derain_net(input_shape(128, 128, 3)): inputs tf.keras.Input(shapeinput_shape) x tf.keras.layers.Conv2D(32, 3, paddingsame)(inputs) x tf.keras.layers.LeakyReLU(alpha0.2)(x) branch1 residual_block(x, 32, dilation_rate1) branch2 residual_block(x, 32, dilation_rate2) branch3 residual_block(x, 32, dilation_rate4) fused tf.keras.layers.Concatenate()([branch1, branch2, branch3]) fused tf.keras.layers.Conv2D(32, 1, paddingsame)(fused) rain_map tf.keras.layers.Conv2D(3, 3, paddingsame, namerain_map)(fused) outputs tf.keras.layers.Subtract()([inputs, rain_map]) model tf.keras.Model(inputs, outputs) return model输入先经过一个卷积提特征然后分成三个并行残差块每个块用不同的dilation_rate。Concatenate之后用1×1卷积做特征融合最后直接预测雨纹图从输入中减去。这里Subtract层就实现了残差学习。注意最后预测雨纹的卷积层没有激活函数因为雨纹数值可能有正有负。3.3 训练循环优化器、学习率调度和检查点保存模型定义好以后训练循环可以直接用tf.keras的Model.fit但我更喜欢自定义训练循环因为可以更灵活地控制梯度累积和损失打印。先定义优化器和学习率initial_lr 1e-3 lr_schedule tf.keras.optimizers.schedules.CosineDecay( initial_learning_rateinitial_lr, decay_steps10000, alpha1e-6 ) optimizer tf.keras.optimizers.Adam(learning_ratelr_schedule)CosineDecay让学习率从1e-3余弦退火到1e-6一共10000步。这个衰减策略比固定学习率稳尤其训练后期不容易在局部震荡。用Adam是因为自适应学习率能让收敛速度更快省去手动调学习率的痛苦。自定义一步训练代码tf.function def train_step(x_batch, y_batch): with tf.GradientTape() as tape: y_pred derain_model(x_batch, trainingTrue) loss tf.reduce_mean(tf.abs(y_pred - y_batch)) grads tape.gradient(loss, derain_model.trainable_variables) optimizer.apply_gradients(zip(grads, derain_model.trainable_variables)) return loss必须用tf.function包装否则TensorFlow会以贪婪模式执行每个操作都有Python开销训练速度会慢5倍以上。GradientTape记录前向传播过程tape.gradient计算损失对所有可训练变量的梯度apply_gradients更新参数。损失这里直接用了L1如果想加感知损失可以在loss上再加一个分量。检查点保存同样重要不然训到一半断电就白跑了。TensorFlow的Checkpoint机制比手动save_model更灵活ckpt tf.train.Checkpoint(modelderain_model, optimizeroptimizer) manager tf.train.CheckpointManager(ckpt, ./ckpts, max_to_keep3)每训练500步存一次max_to_keep保留最近3个。恢复训练时只需要ckpt.restore(manager.latest_checkpoint)会自动恢复模型权重和优化器状态包括学习率位置。这个细节很关键很多同学只保存了权重恢复后发现学习率又从初始值开始训练直接乱套。4. 训练与评估PSNR/SSIM指标、验证集划分和模型选择模型能跑起来只是第一步怎么知道它训练得好不好这是很多新手最迷茫的部分。去雨任务最常见的评估指标是PSNR和SSIM但这两个指标也有自己的脾气用不对会得出错误结论。4.1 评估指标计算PSNR和SSIM的正确用法PSNR峰值信噪比基于像素级MSE计算公式为import numpy as np def psnr(pred, clean, max_val1.0): mse np.mean((pred - clean) ** 2) if mse 0: return float(inf) return 10 * np.log10(max_val**2 / mse)这里max_val是像素最大值。如果图片归一化到0-1max_val就是1.0如果不归一化直接用0-255整数max_val就是255.0。最容易翻车的是pred和clean的dtype不一致一个float32一个uint8减出来的结果全乱套。评估前一定先转成float32再算。SSIM结构相似性考虑了亮度、对比度和结构三个维度比PSNR更符合人眼感知。推荐用scikit-image的实现from skimage.metrics import structural_similarity as ssim ssim_val ssim(pred, clean, channel_axis-1, data_range1.0)注意skimage的SSIM输入必须是无符号的0-1 float且data_range要正确。如果输入是0-255data_range就要设成255。还有一个坑skimage 0.19以上版本把参数名从multichannel改成了channel_axis老代码直接跑不起来。评估时建议不要只算单张图而是对整个验证集做平均。验证集至少放100张图每张图在中心裁剪一个固定尺寸比如128×128不要用随机裁剪否则每次评估结果不一样没法横向对比。代码里评估流程是加载模型权重取验证集batch前向传播对每张图计算PSNR和SSIM最后取平均。4.2 训练曲线分析loss下降但指标不动时怎么排查训练过程中我习惯同时记录两类信息训练loss和验证集PSNR。最常见的现象是loss稳定下降但验证集PSNR迟迟不动。这种情况先别急着调模型按下面的步骤排查。第一步看验证集划分是否合理。如果验证集里的雨图是从同一批合成参数里生成的雨纹分布和训练集高度重叠PSNR可能从第一步就虚高但随着训练升高不明显。这时候换一个不同雨纹参数的验证集指标立刻原形毕露。第二步看损失函数是否主导了指标。L1损失下降不代表PSNR上升因为L1在像素值接近0的区域贡献小而PSNR对绝对误差敏感。可以在验证时额外打印L1 loss如果一个epoch内L1 loss下降而PSNR不变说明网络在大部分无雨区域拟合得很好但雨纹区域还原不到位。这时候需要加大雨纹区的损失权重或者用SSIM损失辅助。第三步看模型是否欠拟合。把batch size降低到4学习率调成5e-4先过拟合一个小数据集比如32张图。如果小数据集上PSNR能涨到30以上说明模型容量没问题问题出在训练配置或数据多样性如果小数据集也上不去那就是网络结构或损失函数的问题需要回头改网络。训练曲线里还有一个玄学现象PSNR会周期性波动每几百步突然掉下去又拉回来。这是batch normalization在切换训练和验证模式时导致的正常。如果波动幅度超过2dB可以考虑把BN的momentum调大一点默认0.99可以调到0.995让batch统计更稳定。4.3 模型选择只看PSNR还是结合主观效果PSNR高不代表去雨效果好。之前在某图像处理Demo上跑过一个实验A模型PSNR比B模型高0.5dB但A模型把雨纹周围的背景细节全部抹平了人眼一看就是“塑料感”。B模型PSNR稍低但背景纹理保留完整视觉上明显更好。所以我建议在选择模型时除了PSNR/SSIM还要看三个主观维度雨纹残留量放大看是否还有白色条纹、背景模糊程度边缘是否清晰、过度增强伪影有没有发亮的光晕。可以把几张典型结果保存成对比图分别展示输入、预测、干净图拼在一起做人眼盲评。代码里也提供了一个可视化脚本用matplotlib保存对比图到当前目录。如果你的项目是给下游任务用比如目标检测或语义分割强烈建议直接去雨后的图跑一遍下游模型看IoU或mAP有没有提升。这比PSNR更有说服力。有时候去雨反而会破坏检测框边缘这种时候就要权衡到底要不要用深度去雨还是用传统滤波预处理。5. 避坑指南图像去雨项目中的常见问题与排查训练去雨模型路上坑比想象中的多。这一章把我自己踩过的以及身边同事踩过的坑整理出来每条都是血泪经验按“现象→原因→解决”写清楚。5.1 雨纹残留或过度平滑现象预测图里雨纹没去干净或者背景被抹得像磨皮一样一点纹理都没有。原因有两个。第一个是网络容量不够只有两三个残差块学不完复杂雨纹。第二个是损失函数权重配比不对L1权重太高模型倾向于输出一个模糊的平均图来稳住L1 loss。解决先增加残差块数量从3个加到5个检查PSNR是否上升。如果上升说明是容量问题如果不动就调整损失。把感知损失权重从0.1提到0.3边缘损失从0.05提到0.1同时把L1权重降到0.8。我一般会做一组小实验每次只改一个权重记录验证集PSNR和主观效果找到最佳配比后再合并调整。5.2 训练不收敛或loss震荡现象loss曲线忽高忽低甚至变成NaN。原因学习率太高是最常见的。另一个原因是数据没有归一化输入的像素范围不对还有可能是批归一化在batch size太小时统计不稳定。解决先把学习率降到1e-4试试。如果还震荡检查输入数据是不是被归一化到0-1再检查batch size是否至少8。我用batch size4训练时就遇到过BN的moving mean一直乱跳loss像过山车。另外如果用了混合精度训练记得把loss scale打开否则梯度下溢也会造成NaN。5.3 模型对真实雨天图像失效现象在合成雨图上PSNR很高一放到真实雨景照片上雨纹去除不干净还出现伪影。原因合成雨图与现实雨景存在domain gap。真实雨图有相机成像的噪声、不同焦距的散焦模糊、雨滴反光等这些在合成数据里都没模拟到。解决最直接的办法是在合成数据里加入更多退化模拟高斯噪声、运动模糊、随机亮度调整。还可以收集少量真实雨景图用训练好的模型去预测把预测结果当作伪标签再用这些伪标签做二次训练。这套流程叫自训练虽然效果有限但能让模型对真实雨图更鲁棒。如果项目允许也可以用无监督域适应方法但复杂度会高很多。5.4 显存不足和训练速度慢现象训练到一半报ResourceExhaustedError或者GPU利用率只有30%训练一天才跑了几个epoch。原因显存不足通常是batch size太大或输入分辨率太高。利用率低则可能是数据管道慢GPU在等CPU准备数据。解决把batch size减半或者输入裁剪从128×128改成96×96。数据管道方面检查是否忘记prefetch或者TFRecord解码是不是占用了太多CPU。用tf.data的profiling工具可以查看瓶颈。我习惯先跑一个profiling确认数据准备时间是否超过GPU计算时间再决定是否增加并行解析的线程数。5.5 模型复现不一致现象同一份代码换台机器训练最终PSNR差1dB以上。原因框架版本不一致、随机种子没固定、GPU浮点运算差异都可能导致。解决代码里固定随机种子用tf.random.set_seed(42)和np.random.seed(42)。另外核对TensorFlow版本和CUDA版本记录在requirements.txt里。如果两个机器都按同一份环境文件安装通常在可接受范围。6. 进阶玩法把去雨模型部署到实际场景中的验证技巧模型训练完最后一步是让它真正在项目里干活。部署前我会做三次验证每次都能筛掉一批不靠谱的模型。第一次验证是真实雨图测试。准备一组真实雨天照片不管分辨率、不管拍摄设备直接扔进模型。如果模型输入是128×128就先把大图切成128×128的patch逐一去雨后再拼回去。拼接时注意patch重叠区域要取平均不然接缝处会出现明显的亮度突变。代码里可以这样处理def predict_patches(model, image, patch_size128, stride64): h, w, c image.shape output np.zeros_like(image) count np.zeros((h, w, 1)) for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch image[y:ypatch_size, x:xpatch_size] pred model(patch[None, ...])[0] output[y:ypatch_size, x:xpatch_size] pred count[y:ypatch_size, x:xpatch_size] 1 output / np.maximum(count, 1) return outputstride比patch_size小重叠区域会被多次预测取平均能消除边界伪影。第二次验证是和经典方法对比。拿同一张雨图分别用滤波方法和训练好的模型处理对比PSNR。但要注意经典滤波器在合成数据上PSNR可能比深度模型低很多可在真实照片上滤波器的稳定性和计算速度反而更实用。如果深度模型在真实照片上跑出来的结果还有明显伪影那就需要回头补数据。第三次验证是轻量化。如果你的项目要部署到嵌入式设备模型参数量和推理速度很重要。量化和剪枝是两条常用路线。TensorFlow的Post-Training Quantization可以用几行代码把float32模型转成int8模型大小缩到四分之一推理速度提升3到5倍。但量化对PSNR的影响通常在0.1dB以内可以接受。如果精度下降明显就要做量化感知训练在训练过程中模拟量化误差。我从这个项目里养成的一个习惯是每次训练结束都会把模型权重、训练曲线、评估结果、测试代码打包成一个固定目录结构命名带上日期和数据集标识。这样过了两个月再翻出来还能知道这个模型当时是拿什么数据训的、指标是多少、踩过哪些坑。最后检查一遍代码里的硬编码路径确认没有指向本地临时目录避免别人下载后跑不起来。希望这份记录和代码能帮你在图像去雨这条路上少走几步弯路也希望你训练出来的模型能真正在雨天的摄像头前面扛住一轮又一轮的考验。本文还有配套的精品资源点击获取