☰
Derain去雨实战:从合成数据到深度学习模型部署
2026/10/11 12:20:56 网站建设 项目流程

简介:Derain 是一份面向图像处理与计算机视觉学习者的 Python 去雨项目资源,聚焦于消除照片中的雨滴干扰,提升恶劣天气下图像的清晰度与可用性,适合具备一定 Python 基础、希望了解去雨算法实现思路的开发者参考。压缩包共 4 个文件,约 3.72MB,包含 2 个 pdf 论文资料、1 个 py 主程序脚本和 1 个 txt 说明文档,分别对应算法理论参考、核心代码实现与使用说明。资源围绕预处理、特征提取、雨滴建模与背景恢复等环节展开,涉及 OpenCV、scikit-image 等常用库,并可能结合卷积神经网络等深度学习方法完成雨滴分割与图像补全。已有 199 人学习,读者可借此了解去雨任务的完整流程,参考论文与代码对照理解算法细节,并在此基础上进行复现与二次开发。

1. Derain:从一张雨滴糊住的行车记录图说起

雨天开车,前挡玻璃上挂满水珠,行车记录仪拍出来的画面几乎没法看——车牌糊成一团、车道线断断续续、远处红绿灯只剩一个色块。这不是镜头脏了,是雨滴在光学路径上做了两件事:折射和散射。折射让背景纹理发生位移,散射让局部对比度骤降。Derain(去雨)要解决的,就是从这种被雨滴污染的单帧图像里,把雨滴层剥离出来,还原出接近无雨状态下的清晰背景。

这个方向不是学术玩具。安防监控在梅雨季的可用率、自动驾驶视觉感知在暴雨天的鲁棒性、户外摄影的后期修复,都直接吃这个能力。做 Derain 的人通常分两拨:一拨走传统图像处理路线,靠频域滤波和稀疏编码;另一拨走深度学习路线,用 CNN 或 Transformer 做端到端映射。两条路各有各的翻车点,后面会逐一拆开讲。如果你手头正好有一批雨天图像要处理,或者想在这个方向做点能落地的东西,下面这套从数据到推理的路径可以直接抄。

2. Derain 的两条技术路线:选型逻辑与最小可跑方案

2.1 传统方法为什么在真实雨图上容易翻车

传统 Derain 的核心假设是:雨滴在频域上表现为高频分量,背景是低频。于是有人用低通滤波把高频砍掉,有人用稀疏编码把雨滴字典和背景字典分开学。这套逻辑在合成雨图上跑得通,因为合成雨滴的形态、方向、透明度都是可控的。但真实雨滴不是这样——它受风速影响会拉成斜线,受焦距影响会变成大小不一的椭圆,受背景亮度影响会呈现半透明到全反射的连续变化。

我早期用导向滤波做过一版,参数调了三天,结果是把雨滴和纹理一起抹掉了。树叶边缘、建筑窗框、路面标线,全被当成高频噪声处理。后来换成稀疏编码,字典训练了 2000 张合成雨图,在真实雨图上重建出来的背景带着明显的块效应。血泪经验是:传统方法不是不能用,而是它的先验假设太强,强到只能处理特定形态的雨。如果你手头的数据是固定场景、固定焦距、雨滴形态单一,传统方法还能凑合;一旦场景切换,泛化能力断崖式下跌。

2.2 深度学习方案的最小闭环:数据、模型、损失

深度学习做 Derain,最小闭环就三件事:配对数据、编码器-解码器结构、像素级损失加感知损失。配对数据是最大的门槛——真实场景下你不可能同时拍到有雨和无雨的两张图。常见做法是用合成雨图训练,再用真实雨图做微调。合成雨图的生成方式直接决定模型的上限。

下面这段代码用 Python 生成一批基础合成雨图,核心是控制雨滴的方向、长度、密度和透明度四个参数。这不是什么官方脚本,是我自己反复调出来的一套参数范围,能覆盖大部分中雨到大雨的场景。

import cv2 import numpy as np import random def generate_rain_layer(h, w, num_drops=800, angle_range=(-30, 30), length_range=(15, 40), thickness_range=(1, 3), alpha_range=(0.3, 0.7)): """ 生成单帧雨滴层 h, w: 图像高宽 num_drops: 雨滴数量,控制密度 angle_range: 雨滴倾斜角度范围,模拟风速影响 length_range: 雨滴长度范围,模拟焦距和运动模糊 thickness_range: 雨滴粗细 alpha_range: 透明度范围,模拟背景亮度影响 """ rain_layer = np.zeros((h, w, 3), dtype=np.float32) for _ in range(num_drops): x = random.randint(0, w - 1) y = random.randint(0, h - 1) angle = random.uniform(*angle_range) length = random.randint(*length_range) thickness = random.randint(*thickness_range) alpha = random.uniform(*alpha_range) # 计算雨滴终点 rad = np.deg2rad(angle) x2 = int(x + length * np.sin(rad)) y2 = int(y + length * np.cos(rad)) # 画线模拟雨滴,颜色偏白 color = (255, 255, 255) cv2.line(rain_layer, (x, y), (x2, y2), color, thickness) # 按透明度混合 mask = np.zeros((h, w), dtype=np.float32) cv2.line(mask, (x, y), (x2, y2), 1.0, thickness) rain_layer = rain_layer * (1 - mask[..., None] * alpha) + \ rain_layer * mask[..., None] * alpha return rain_layer def blend_rain(clean_img, rain_layer, beta=0.8): """ 将雨滴层叠加到干净图像上 beta: 背景保留系数,越小雨越重 """ rain_layer = rain_layer.astype(np.float32) / 255.0 clean = clean_img.astype(np.float32) / 255.0 rainy = clean * beta + rain_layer * (1 - beta) rainy = np.clip(rainy * 255, 0, 255).astype(np.uint8) return rainy # 使用示例 clean = cv2.imread("clean.jpg") h, w = clean.shape[:2] rain = generate_rain_layer(h, w, num_drops=1200, angle_range=(-25, 25)) rainy = blend_rain(clean, rain, beta=0.75) cv2.imwrite("rainy.jpg", rainy)

这段代码的逻辑很直白:先随机撒线模拟雨滴,再按透明度混合到干净图上。参数说明几个关键点。num_drops控制密度,800 到 1500 对应中雨到大雨,超过 2000 会变成暴雨,背景几乎不可见。angle_range模拟风向,真实场景下风速越大角度偏移越明显,一般设在正负 30 度以内。beta是背景保留系数,0.75 到 0.85 之间比较接近真实雨图的对比度下降程度,低于 0.7 会显得太假。这套合成数据训练出来的模型,在真实雨图上做微调时,学习率要降到初始值的十分之一,否则会把合成数据的分布特征带进去,导致真实雨滴被过度平滑。

模型结构上,我一般用 U-Net 变体,编码器用 ResNet 的前几层做特征提取,解码器加跳连。损失函数用 L1 加 VGG 感知损失,权重比 1:0.01。L1 保证像素级对齐,感知损失防止过度平滑。训练时 batch size 设 8,学习率 1e-4,Adam 优化器,大概 50 个 epoch 能看到收敛趋势。这些参数不是金科玉律,但能让你在半天内跑出一个能看的 baseline。

3. 把 Derain 跑起来:数据准备、训练配置与推理部署

3.1 数据集的三个来源与清洗规则

Derain 的数据集来源无非三种:合成雨图、真实配对雨图、非配对真实雨图。合成雨图用上一节的脚本批量生成,注意控制场景多样性——室内、室外、白天、夜晚、近景、远景都要覆盖,否则模型会过拟合到某类背景。真实配对雨图非常稀缺,公开的几套数据集规模都不大,常见做法是拿合成数据预训练,再用少量真实配对数据微调。非配对真实雨图最多,但需要配合 CycleGAN 这类非配对框架使用,训练难度和调参成本都更高。

清洗规则我踩过坑。合成雨图里,如果雨滴层叠加时背景本身就有噪声,模型会学到噪声和雨滴的混合模式,推理时把噪声也当成雨滴处理。所以合成前要对干净图做一次降噪,用非局部均值或者 BM3D 都行。真实雨图这边,要剔除雨滴完全遮挡主体的样本,比如雨滴正好糊在车牌上、人脸上,这种样本对训练没好处,反而会让模型学到错误的映射关系。我一般用简单的人工抽检,抽 10% 看一眼,把明显不合格的删掉。

3.2 训练配置:学习率、批大小与损失权重的实操取值

训练 Derain 模型,学习率是最容易翻车的参数。初始学习率设 1e-4 是安全区,设 1e-3 大概率震荡,设 1e-5 收敛太慢。用余弦退火调度,每 10 个 epoch 降一次,降到 1e-6 左右停。批大小受显存限制,8 到 16 之间比较稳,太小梯度噪声大,太大泛化差。损失权重方面,L1 和感知损失的比值我试过 1:0.1、1:0.01、1:0.001,最后发现 1:0.01 在视觉质量和像素指标之间平衡最好。比值太高,输出偏模糊;比值太低,输出会有伪影。

下面这段是训练循环的核心配置,用 PyTorch 写的,关键参数都标了注释。

import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR # 假设 model 是定义好的 Derain 网络 model = DerainNet().cuda() criterion_l1 = nn.L1Loss() criterion_perceptual = PerceptualLoss().cuda() # 基于 VGG 的感知损失 # 学习率 1e-4,Adam 优化器,权重衰减 1e-5 防止过拟合 optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6) # 损失权重:L1 为主,感知损失为辅 lambda_l1 = 1.0 lambda_perceptual = 0.01 for epoch in range(50): model.train() for rainy, clean in dataloader: rainy, clean = rainy.cuda(), clean.cuda() output = model(rainy) loss_l1 = criterion_l1(output, clean) loss_perceptual = criterion_perceptual(output, clean) loss = lambda_l1 * loss_l1 + lambda_perceptual * loss_perceptual optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 每 5 个 epoch 存一次权重 if (epoch + 1) % 5 == 0: torch.save(model.state_dict(), f"derain_epoch_{epoch+1}.pth")

逻辑说明:前向传播得到去雨输出,分别算 L1 损失和感知损失,加权求和后反向传播。参数说明:T_max=50对应 50 个 epoch 的余弦周期,eta_min=1e-6是学习率下限。权重衰减1e-5是为了防止模型记住训练集的雨滴模式。存权重的频率设 5 个 epoch 一次,方便回滚到效果最好的版本。如果显存不够,把批大小降到 4,同时把学习率降到 5e-5,否则梯度更新幅度太大,容易跳过最优解。

3.3 推理阶段的滑动窗口与边缘融合

推理时如果图像分辨率超过训练时的输入尺寸,直接缩放会丢失细节,常见做法是滑动窗口切块推理再拼接。切块大小设 256×256,步长设 128,重叠区域用余弦窗加权融合。这样能避免块与块之间的拼接缝。下面这段是推理和拼接的代码。

import torch import numpy as np import cv2 def infer_with_sliding_window(model, img, patch_size=256, stride=128): """ 滑动窗口推理,余弦窗融合 img: 输入雨图,HWC 格式,RGB patch_size: 切块大小 stride: 滑动步长 """ model.eval() h, w, c = img.shape output = np.zeros((h, w, c), dtype=np.float32) weight = np.zeros((h, w, c), dtype=np.float32) # 生成余弦窗 cos_window = np.outer(np.hanning(patch_size), np.hanning(patch_size)) cos_window = cos_window[..., None] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patch = img[y:y+patch_size, x:x+patch_size, :] patch_tensor = torch.from_numpy(patch).permute(2, 0, 1).float().unsqueeze(0).cuda() / 255.0 with torch.no_grad(): pred = model(patch_tensor).squeeze(0).permute(1, 2, 0).cpu().numpy() pred = pred * 255.0 output[y:y+patch_size, x:x+patch_size, :] += pred * cos_window weight[y:y+patch_size, x:x+patch_size, :] += cos_window # 处理边缘未覆盖区域 output = output / np.maximum(weight, 1e-6) output = np.clip(output, 0, 255).astype(np.uint8) return output

逻辑说明:对每个切块做推理,用余弦窗加权累加,最后除以权重和。参数说明:patch_size和训练时保持一致,否则分布不匹配。stride设 patch_size 的一半,保证重叠区域足够融合。余弦窗的作用是让块中心权重高、边缘权重低,拼接时过渡自然。如果图像尺寸小于 patch_size,直接整图推理,不用切块。推理完记得把输出转成 uint8 再保存,否则 OpenCV 写出来的图会全黑。

4. Derain 避坑指南:五个真实翻车现场

4.1 合成数据训练,真实雨图推理,雨滴变成糊状

现象:模型在合成验证集上 PSNR 跑到 32dB,拿到真实雨图上跑,雨滴是没了,但背景也糊了,树叶变成一团绿色,建筑边缘像水彩画。

原因:合成雨滴的透明度和形态分布和真实雨滴差距太大。合成雨滴是均匀的白色线条,真实雨滴有高光、有暗边、有半透明过渡。模型学到的是“白色线条等于雨滴”,遇到真实雨滴时,把雨滴和背景的过渡区域也当成雨滴抹掉了。

解决:在合成数据里加入真实雨滴的纹理特征。具体做法是收集一批真实雨滴的局部 patch,用泊松融合贴到干净图上,再训练。或者用非配对框架做域适应,把合成域的特征对齐到真实域。我一般会留 20% 的真实雨图做验证,不参与训练,只看指标趋势,一旦验证集 PSNR 开始下降就停。

4.2 损失函数权重没调好,输出出现棋盘伪影

现象:推理结果放大看,有规律的网格状纹理,像棋盘格。

原因:感知损失的权重设得太高,VGG 特征图的池化操作引入了棋盘效应。或者解码器用了转置卷积,步长和卷积核不匹配。

解决:把感知损失权重降到 0.001 以下,或者改用 L1 加 SSIM 的组合。解码器上采样换成最近邻插值加卷积,别用转置卷积。如果伪影已经出现,在推理后加一道非局部均值降噪,能压下去一部分,但治标不治本。

4.3 滑动窗口步长太大,拼接缝像刀切

现象:大图推理后,块与块之间有明显的直线接缝,像拼图没拼好。

原因:步长等于 patch_size,没有重叠区域,或者重叠区域太小,余弦窗融合不够。

解决:步长设 patch_size 的一半,余弦窗的尺寸和 patch_size 一致。如果接缝还在,把步长再缩小到 patch_size 的三分之一,代价是推理时间翻倍。另一个容易忽略的点是图像边缘——滑动窗口可能覆盖不到最右边和最下边,需要单独处理,或者把图像 padding 到 patch_size 的整数倍再推理,最后裁掉 padding。

4.4 学习率设太大,损失震荡不收敛

现象:训练 loss 在 0.05 到 0.15 之间来回跳,降不下去。

原因:初始学习率 1e-3 对 Derain 这种像素级回归任务来说太大了,梯度更新幅度超过最优解的范围。

解决:初始学习率降到 1e-4,加余弦退火。如果还震荡,降到 5e-5,同时把批大小翻倍,用梯度累积模拟大批量。另外检查一下数据归一化,输入图像要除以 255 归到 0 到 1 之间,别直接拿 0 到 255 的像素值训练,否则梯度量级完全不对。

4.5 显存不够,批大小降到 1,BN 层统计量失真

现象:训练时 loss 正常下降,推理时效果差很多,尤其在小批量样本上。

原因:批大小太小,BatchNorm 层的均值和方差估计不准,训练和推理的分布不一致。

解决:把 BatchNorm 换成 InstanceNorm 或 GroupNorm,这两个对批大小不敏感。或者用梯度累积,攒 4 个 batch 再更新一次参数,等效批大小变成 4。如果显存实在紧张,把图像裁成 128×128 的小块训练,推理时再滑窗拼接,但要注意小块训练时感受野受限,雨滴长度超过 40 像素的样本可能学不好。

5. 进阶技巧:用半监督微调把真实雨图利用率拉满

真实配对雨图难搞,但真实雨图本身不难搞。半监督微调的思路是:先用合成数据训练一个 baseline,再用少量配对真实数据做有监督微调,最后用大量非配对真实数据做自监督约束。自监督的损失怎么设计?常见做法是让模型对同一张雨图做两次不同增强(比如随机裁剪、随机翻转),然后约束两次输出的去雨结果一致。这叫一致性正则,不需要干净标签。

具体操作上,我一般分三个阶段。第一阶段,合成数据训练 50 个 epoch,学习率 1e-4。第二阶段,配对真实数据微调 20 个 epoch,学习率降到 1e-5,损失只用 L1。第三阶段,非配对真实数据做一致性正则,学习率 1e-6,损失是 L1 加一致性损失,权重比 1:0.1。一致性损失用 MSE,约束两次增强输出的差异。下面这段是第三阶段的训练逻辑。

# 第三阶段:半监督一致性正则 model.train() for rainy in unpaired_dataloader: rainy = rainy.cuda() # 两次不同增强 aug1 = random_augment(rainy) # 随机裁剪+翻转 aug2 = random_augment(rainy) out1 = model(aug1) out2 = model(aug2) # 一致性损失:两次输出应该一致 loss_consistency = nn.MSELoss()(out1, out2) # 如果手头有少量配对数据,也可以混进来算 L1 loss = lambda_consistency * loss_consistency optimizer.zero_grad() loss.backward() optimizer.step()

参数说明:lambda_consistency设 0.1 到 0.5 之间,太高会让模型输出趋于平均,失去细节;太低起不到正则作用。增强方式用随机裁剪到 256×256,随机水平翻转,别用颜色抖动,颜色变化会破坏雨滴的物理特征。这个阶段不需要太多 epoch,10 到 15 个就够,多了容易过拟合到非配对数据的噪声上。

验证方法上,我习惯留三张真实雨图不参与任何训练,每完成一个阶段就跑一次推理,肉眼对比。指标方面,PSNR 和 SSIM 在真实雨图上参考价值有限,因为干净标签本身是估计出来的。更靠谱的是看下游任务的表现——比如把去雨后的图送进目标检测器,看检测框的召回率有没有提升。如果去雨后检测器反而漏检更多,说明去雨过程把目标特征也抹掉了,得回头调损失权重。

这套流程跑下来,从数据准备到推理部署,大概需要两到三天。硬件门槛不高,单卡 8GB 显存就能跑。值不值得做,取决于你的场景对雨天图像质量的敏感程度。如果只是偶尔几张图,传统方法凑合能用;如果是批量处理或者集成到实时系统里,深度学习方案的上限明显更高。我自己踩过的最大坑是急于求成,合成数据没调好就上真实数据,结果返工重训了三次。后来养成习惯,每换一个数据集,先跑 5 个 epoch 看 loss 曲线,曲线不健康就停下来查数据,别硬训。希望帮到你。

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

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

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

立即咨询