
简介一个基于Python实现的图像去雨Deraining项目面向图像处理与计算机视觉学习者旨在去除照片中的雨滴干扰提升恶劣天气下拍摄图像的清晰度。项目围绕预处理、特征提取、雨滴建模与背景恢复等关键步骤展开并可能借助OpenCV、scikit-image或卷积神经网络等常见技术来识别和分离雨滴。压缩包共4个文件包含2篇PDF学术论文、1份Python源代码和1份说明文档整体大小仅3.72MB体量轻巧便于下载后快速阅读和实践。已有199人学习下载适合希望系统理解去雨算法原理并动手复现的初学者或研究人员。通过这份资源读者可以对照论文掌握ICONIP等前沿去雨方法参考源码理解CNN在雨滴分割与图像重建中的具体应用同时借助说明文档快速跑通流程为后续开展相关实验或项目打下基础。 Derain这个词圈内人一看就知道是single image deraining单张图像去雨的简称。用一个神经网络把附着在画面上的雨线、雨雾去掉还原出干净的背景内容这在自动驾驶、安防监控、户外摄影这些场景里都是刚需能力。我这次做的就是一个从零搭建的Derain项目从雨图合成、网络设计到训练推理全流程跑通整个过程踩了不少坑也沉淀了比较完整的实操经验这篇就把它完整拆开方便想入手图像复原方向的朋友直接参考。1. 项目定位Derain到底要解决什么问题1.1 去雨任务的数学表达图像去雨任务本质上是一个病态逆问题。我们观察到带雨图像O希望恢复出无雨的清晰背景B。经典雨图模型可以写成O B S其中S代表雨线层。但这只是一个非常原始的线性叠加实际雨图还要复杂得多——雨线有方向性、有透明度变化、有运动模糊造成的拖影远处还有雨雾导致的对比度衰减。更贴近物理的写法是O α(B S) (1 - α)B这个公式的意思是在雨线覆盖的区域观测值由背景和雨线混合而成混合系数α控制雨的密度和强度。之所以要把这部分讲清楚是因为合成数据的质量直接决定了模型的上限。网络结构再复杂数据里的雨纹不符合物理规律训练出来也就是个花架子。1.2 为什么去雨比去噪更难很多人会拿去雨和去噪做对比实际上两者的难度不在一个量级。噪声是随机的、各向同性的网络很容易学习到“平滑”这个策略但雨线是结构化的——它有方向、有明暗过渡、有空间分布规律。更麻烦的是雨线的尺度跨度极大近处的雨滴粗大清晰远处的雨丝细如发丝甚至还有雨雾产生的全局亮度影响。这就是为什么当时这个项目做技术选型时我特意避开了单纯的CNN堆叠而是采用了残差学习配合多尺度特征融合的思路。网络不去直接预测清晰图像而是先预测“雨纹残差”再用输入减掉残差得到清晰图。这个设计能让网络把注意力集中在结构化的雨线上而不是花大量参数去重建背景纹理。这个思路在PReNet、RESCAN这些经典去雨论文里都被验证过属于经过实战检验的可靠方向。2. 数据先行雨图是“造”出来的2.1 数据合成细节去雨领域和超分、去噪一样最头疼的还是训练数据。目前业界并没有大规模、高质量的真实成对雨图数据集主流的做法都是在清晰图上合成雨纹。我实测下来这套合成方案的效果足够支撑一个演示级甚至准产品级的模型训练。雨线合成的核心是用带方向的运动核生成不同角度、不同长度的雨线条纹import cv2 import numpy as np def generate_rain_streak(h, w, angle_deg70, length30, thickness1.5, blur_sigma1.2): 生成单条雨线angle_deg为雨滴下落方向角 返回归一化的雨线强度图 mask np.zeros((h, w), dtypenp.float32) # 随机选择一个起点沿着指定角度绘制直线 x0 np.random.randint(0, w) y0 np.random.randint(0, h) dx int(np.sin(np.deg2rad(angle_deg)) * length) dy int(np.cos(np.deg2rad(angle_deg)) * length) cv2.line(mask, (x0, y0), (x0 dx, y0 dy), 1.0, int(thickness), cv2.LINE_AA) # 高斯模糊模拟相机运动导致的拖影效果 mask cv2.GaussianBlur(mask, (0, 0), blur_sigma) return mask def synthesize_rainy_image(clean_img, streak_num30, alpha_range(0.4, 0.8)): h, w clean_img.shape[:2] streak_layer np.zeros_like(clean_img, dtypenp.float32) for _ in range(streak_num): angle np.random.uniform(-15, 15) # 雨线角度有一定随机性 length np.random.uniform(15, 60) # 雨线长度 thickness np.random.uniform(1, 2) # 雨线粗细 streak generate_rain_streak(h, w, angle_degangle, lengthlength, thicknessthickness) alpha np.random.uniform(*alpha_range) # 雨线强度 streak_layer[..., 0] np.maximum(streak_layer[..., 0], streak * alpha) streak_layer[..., 1] np.maximum(streak_layer[..., 1], streak * alpha) streak_layer[..., 2] np.maximum(streak_layer[..., 2], streak * alpha) rainy_img clean_img.astype(np.float32) streak_layer * 255.0 rainy_img np.clip(rainy_img, 0, 255).astype(np.uint8) return rainy_img, streak_layer这里有几个关键细节值得展开。第一雨线方向不能太集中在同一个角度否则训练出来的模型只会对特定雨强有效换个角度就失效了所以angle在-15到15度之间随机采样。第二雨线是逐条用max叠加而不是直接求和这样能模拟真实雨场景中雨线交叠、遮挡的视觉效果。第三雨线做高斯模糊非常关键真实的雨在曝光时间内是有运动轨迹的边缘不会锐利得像刀割模糊后的雨线能让模型学会处理更真实的雨痕。2.2 训练集组织和增强策略数据集我采用的是公开的BSD400、Rain100L这些图像库做底图再加上一部分自己收集的街景高清图统一缩放到256x256分辨率作为训练patch。一批样本里混合了小雨、中雨、大雨三种强度比例大概在3:4:3。这样做的原因是如果只训练大雨样本模型会倾向于把整张图压暗因为它在数学上找到了一个“更省力”的解小雨区域的残差方向反而学偏了。数据增强方面我用了随机水平翻转、随机90度旋转、随机色彩抖动亮度正负10%、饱和度和对比度轻度扰动。值得强调的是色彩抖动去雨网络很容易把色彩和纹理一起“滤掉”加入色彩扰动可以强迫网络关注结构信息而不是依赖颜色恒常性来做判断。实际效果表明加上这一步之后输出图像的颜色保留度明显变好。3. 模型构建与训练残差U-Net方案3.1 网络结构设计网络结构我采用的是残差U-Net变体整体架构由编码器、中间残差块、解码器三部分组成并引入跳层连接。编码器做3次下采样通道数从32逐级翻倍到256每层由两个卷积块组成激活函数用LeakyReLU斜率0.2归一化层选择GroupNorm而不是BatchNorm——原因后面会细说。中间部分串联4个残差块每个残差块内部包含两个3x3卷积。解码器用转置卷积做3次上采样并和编码器同尺度的特征图拼接最后输出一个和输入尺寸相同的通道数为1的残差雨纹图。这里有一个我不太建议新手踩的坑BatchNorm在图像复原任务中会引入严重的batch内部统计信息依赖当batch size较小或者验证时输入尺寸变化较大时模型效果会出现明显抖动。GroupNorm不依赖batch维度每组自己归一化稳定性好很多。实际训练中GroupNorm版本在验证集上的PSNR比BatchNorm版高大约0.4dB左右而且收敛更平滑。import torch import torch.nn as nn class ResBlock(nn.Module): def __init__(self, channels): super().__init__() self.conv1 nn.Conv2d(channels, channels, 3, 1, 1) self.gn1 nn.GroupNorm(8, channels) self.act nn.LeakyReLU(0.2) self.conv2 nn.Conv2d(channels, channels, 3, 1, 1) self.gn2 nn.GroupNorm(8, channels) def forward(self, x): out self.act(self.gn1(self.conv1(x))) out self.gn2(self.conv2(out)) return self.act(out x) class DerainNet(nn.Module): def __init__(self, in_channels3): super().__init__() self.enc1 nn.Sequential(nn.Conv2d(in_channels, 32, 3, 1, 1), nn.GroupNorm(8, 32), nn.LeakyReLU(0.2)) self.enc2 self._make_block(32, 64, downsampleTrue) self.enc3 self._make_block(64, 128, downsampleTrue) self.enc4 self._make_block(128, 256, downsampleTrue) self.resblocks nn.Sequential(*[ResBlock(256) for _ in range(4)]) self.dec3 self._make_block(256 128, 128, upsampleTrue) # 与enc3拼接 self.dec2 self._make_block(128 64, 64, upsampleTrue) # 与enc2拼接 self.dec1 self._make_block(64 32, 32, upsampleTrue) # 与enc1拼接 self.out_conv nn.Conv2d(32, in_channels, 3, 1, 1) def _make_block(self, in_ch, out_ch, downsampleFalse, upsampleFalse): layers [] if downsample: layers.append(nn.Conv2d(in_ch, out_ch, 3, 2, 1)) # 步长为2下采样 elif upsample: layers.append(nn.ConvTranspose2d(in_ch, out_ch, 3, 2, 1, output_padding1)) else: layers.append(nn.Conv2d(in_ch, out_ch, 3, 1, 1)) layers.append(nn.GroupNorm(8, out_ch)) layers.append(nn.LeakyReLU(0.2)) return nn.Sequential(*layers) def forward(self, x): e1 self.enc1(x) e2 self.enc2(e1) e3 self.enc3(e2) e4 self.enc4(e3) m self.resblocks(e4) d3 self.dec3(torch.cat([m, e3], dim1)) d2 self.dec2(torch.cat([d3, e2], dim1)) d1 self.dec1(torch.cat([d2, e1], dim1)) rain_residual self.out_conv(d1) return x - rain_residual # 残差学习输出 输入 - 预测雨纹3.2 损失函数组合项目没有只用单一的L1损失而是采用“像素损失 感知损失 结构损失”的三合一组合。L1损失保证逐像素强度的一致性感知损失借助VGG19的relu2_2和relu3_3层特征在更语义化的层面拉近生成结果和干净图像的距离——这一步能显著减少雨雾残留导致的“糊感”结构损失SSIM Loss则保护边缘和纹理不被过度平滑。总损失函数为L L1 0.1 * L_percep 0.05 * L_ssim权重分配是我多次实验试出来的。感知损失如果超过0.1生成图像会出现明显的色彩偏移和细节“油画感”这是因为VGG特征空间中的差异不完全等价于视觉质量的差异。SSIM损失权重不宜太高否则优化过程会变得不稳定SSIM的梯度在局部区域容易震荡。三者的比例调到这个位置既保证了去雨强度又保留了纹理细节。3.3 训练超参数与评估指标训练配置如下输入patch大小为128x128batch size为16优化器选用Adam初始学习率2e-4每30个epoch衰减为原来的一半总共训练120个epoch。学习率这块我吃过亏最初直接按1e-3起跑前20个epoch损失确实下降快但到达谷底后一直在震荡无法收敛到更优的极小值点。后来改为2e-4起步虽然前期慢一点但后半程稳定得多最终PSNR反而更高。评估指标采用图像复原领域的两个标准PSNR峰值信噪比和SSIM结构相似性。PSNR本质上是基于MSE的对数变换数值越高说明像素层面越接近SSIM则是从亮度、对比度、结构三个维度综合度量更贴近人眼感知。只盯着PSNR调参是一个很常见的误区PSNR高但视觉上纹理怪异的情况我遇到太多次了所以每次epoch结束我都是两个指标一起看以SSIM为主做早停判断。4. 训练过程与效果复盘训练日志记录得比较详细。模型从第5个epoch开始就能目视看出雨纹明显衰减第20个epoch时背景纹理开始恢复第60个epoch后PSNR提升趋于平缓SSIM还有缓慢上升。最终在合成测试集上的效果为PSNR从带雨图的22.1dB提升到31.8dBSSIM从0.72提升到0.95。一个值得注意的现象是模型在小角度单调雨线场景下的去雨非常干净实测能接近99%去除率但在多角度交叉雨线场景下会有少量残留。原因很好理解——合成数据里交叉雨线出现的频率明显低于单方向雨线模型对这类样本的拟合不充分。后来我在合成数据里人为提高了多角度雨线的比例这个问题得到了明显改善。推理阶段的速度同样关键。这个模型在单张RTX 3090上处理一张512x512的图片大约需要45ms在CPU上则大约1.2秒。如果要对视频流做实时去雨建议把转置卷积替换成PixelShuffle上采样再叠加半精度FP16推理整体延迟可以压到20ms以内。但要注意模型修剪之后需要重新微调直接砍层数会导致去雨能力断崖式下降。5. 常见问题与排查技巧实录5.1 典型故障速查表我把训练过程中遇到的高频问题整理成了一张表照着排查能省下大量试错时间。现象可能原因解决方案训练Loss震荡不下降学习率过大、batch size太小学习率降到1e-4batch size提升到16以上去雨后图像灰蒙蒙一片雨纹残差估计有整体偏移检查归一化层尝试给残差加零均值约束输出图像出现网格伪影转置卷积重叠导致棋盘效应改用PixelShuffle上采样或加FFT频域损失细节被过度平滑感知损失权重过高降低感知损失权重改用浅层VGG特征验证集效果差但训练集好合成数据分布单一增大雨纹角度、密度、尺度变化范围输入尺寸变化后效果变差BN依赖batch统计信息切换到GroupNorm或InstanceNorm5.2 从合成数据到真实雨图的泛化问题合成数据和真实雨图之间存在明显的domain gap这个项目做到最后也不得不面对它。真实雨图里的雨往往带有复杂的雾气散射效应和镜片反光远不是“背景加几条雨线”能完全覆盖的。解决思路有几个方向我实测推荐的方式是在合成阶段加入雨雾衰减模型把大气散射模型同时加进来让合成雨图在全局对比度和亮度上也向真实场景靠拢。另一个方向是引入少量弱监督的真实雨图做域自适应微调不需要像素级标注只要图像级标签通过GAN的域判别器让模型在特征层面拉近合成域和真实域的分布。这个项目做完后我最大的体会是图像复原任务里数据工程和模型结构拼的是运气数据拼的是耐心合成数据的多样性直接决定模型的天花板。另外损失函数的设计不是照搬论文就能奏效的最好在验证集上观察具体是雨纹残留、细节丢失还是色彩失真再反向调整每一项损失的权重。最后再分享一个实在的小技巧每次epoch结束把当前模型处理同一张固定测试图的效果也存下来自己肉眼扫一遍比只盯指标曲线更能发现问题——因为指标会骗人眼睛不会。本文还有配套的精品资源点击获取