ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

去雨实战:残差U-Net与损失函数调优的完整技术路线

去雨实战:残差U-Net与损失函数调优的完整技术路线 简介Derain是一份基于Python的图像去雨工具项目面向计算机视觉初学者与图像处理开发者解决雨天拍摄照片中雨滴干扰导致的清晰度下降问题。项目综合运用OpenCV、PIL等图像处理库以及CNN、CGAN等深度学习方法涵盖预处理、特征提取、雨滴建模、背景恢复等完整流程适用于科研学习与工程落地参考。压缩包共4个文件以PDF论文资料、Python脚本和TXT说明为主整体大小3.72MB其中包含两篇去雨相关论文和主程序代码便于读者对照算法原理与源码进行实践。已有199人学习下载适合希望快速理解去雨技术框架并动手复现的开发者。通过阅读代码与文档还可掌握图像去雨领域的经典方法、深度学习训练思路以及图像后处理技巧为后续研究提供有价值的参考。同时压缩包目录结构简洁便于按模块查找。 做计算机视觉这几年雨天图像处理一直是我又爱又恨的方向。爱是因为问题足够清晰——把雨纹从画面里扣掉给人一个干净的背景恨是因为它远比表面上看起来复杂。“Derain”这个词在项目里就是“去雨”的直译也是这个项目的代号。今天我不讲那种“包装成产品”的Demo只讲一个真实落地过程中会遇到的事数据怎么选、模型怎么搭、损失函数怎么配、训练翻车了怎么查。这篇内容适合手里已经有PyTorch基础、想系统入坑底层视觉复原方向的人也适合那些正在为雨天监控、自动驾驶、户外图像增强项目发愁的工程师。先说结论去雨不是一个“用CNN跑一跑就能出图”的任务它的难点不在于网络有多深而在于雨纹和背景纹理在频域上高度重叠你很难用一个全局规则把两者切开。我见过太多人把去雨做成“磨皮”输出干净是干净了纹理细节全没了。真正靠谱的Derain方案需要在模型结构、损失函数、数据分布三个层面同时下功夫。下面我把整个项目的拆解思路和实操记录完整过一遍。1. Derain项目要解决什么问题雨图退化模型与任务边界1.1 雨天图像的退化模型从线性叠加到雨雾混合去雨任务的第一步是把问题形式化。绝大多数论文里用的都是线性叠加模型O B SO是有雨图像B是干净背景S是雨纹层。也就是说雨纹被当成一种加性噪声网络的目标就是从O里把S估计出来然后用O - S得到B。这个模型简单但它只对“雨丝清晰、背景亮度不变”的情况有效。真实场景里还有另外两个因素不能忽略一是雨滴在成像时会引入散射导致背景整体亮度下降、对比度降低这部分用大气散射模型来描述二是雨滴落在镜头上会形成局部失焦的雨斑跟空中雨丝的统计特性完全不同。所以我在项目里把退化模型扩展成了带权重的混合形式O B * (1 - M) (B S) * M A * (1 - t)M是雨纹掩膜A是大气光t是透射率。这个模型的好处是它把“雨纹去除”和“雨雾去散射”统一到了同一个优化目标里。你不需要在工程里真的分开训练两个模型但理解这个模型能帮你解释一个现象为什么雨图里的背景常常是灰蒙蒙的因为雨滴散射把远处的对比度削掉了单纯减掉雨纹层根本救不回来。1.2 去雨与传统图像增强的本质差别语义判断比像素处理更重要很多人第一次接触去雨会想当然地拿中值滤波、高斯模糊或者直方图均衡化去试。结果基本都一样雨纹变淡了但背景纹理也跟着糊了。原因在于雨纹和背景边缘在频域上都是高频成分线性滤波没法区分它们。去雨真正难的地方在于它要求算法先“理解”图像内容。哪些高频是雨丝哪些高频是树叶轮廓、窗框、电线需要语义级别的判断。这也是为什么传统方法里稀疏编码、字典学习做到后期都带了一个“雨纹检测”前置模块——因为它必须先把雨的位置标出来才敢动手去擦。从这个角度看深度学习做去雨并不是“大力出奇迹”而是用数据驱动的方式隐式建模了“哪里是雨、哪里是细节”这个判别边界。早期End-to-end的DerainNet之所以效果一般就是因为它只有像素回归缺乏对雨纹结构的显式感知。后来JORDER、RESCAN这些模型加入雨纹检测分支或者循环结构本质上是告诉网络你别光顾着重建背景先把雨的位置找出来。1.3 为什么我最终选了数据驱动路线而不是传统优化传统去雨方法不是没有优点它不依赖标注数据可解释性也强。但它在真实场景里的泛化能力太差。我实测过导向滤波加低秩分解的方案在合成雨图上能拿到不错的结构相似度一换到监控实拍画面雨纹残留和背景伪影都很严重。深度学习的路线虽然需要数据但它能自动学到雨纹在不同场景、不同光照、不同距离下的表现形式。拿项目里的实际对比来说同样的雨图传统方法处理一张要2到3秒深度学习模型在GPU上只要几十毫秒这个速度差距对于视频流的实时处理来说是决定性的。所以在2020年之后工业界做去雨基本已经全面转向数据驱动这没什么好纠结的。2. 模型与训练方案选型我为什么选了残差U-Net这条路2.1 主流去雨模型路线对比目前去雨模型大概分四类路线直接回归路线用一个CNN/GAN直接把雨图映射到干净图代表模型是DerainNet、ContextAgg Net。优点是结构简单缺点是容易把背景细节一并抹掉。雨纹估计路线网络先输出雨纹层再用输入减雨纹得到背景代表模型是RESCAN、PReNet。这类方法天然契合线性退化模型训练更稳定。物理引导路线引入雨纹方向、透明度、景深等物理参数作为中间监督代表模型是JORDER、SPDNet。可解释性强但标注成本高复现难度也大。新兴的Transformer路线Uformer、Restormer这类模型在去雨上效果确实好但显存占用和推理延迟都偏大在边缘设备上落地很吃力。我的选择很直接残差U-Net而且走“预测雨纹层”的路线而不是直接预测背景。2.2 残差U-Net的设计逻辑让网络只学“差异”为什么残差结构在这类任务里特别管用因为雨图O和干净图B之间只差一个雨纹层SO和B的绝大部分像素是一样的。如果你的网络直接预测B它需要把已经被雨纹污染的像素值“改回来”这个映射空间很大、很难学。但如果让网络预测S它只需要学“多出来的那部分是什么”输入和输出的共同信息更多优化会轻松很多。这个思路跟ResNet里的恒等映射是同一个道理只不过这里的残差体现在图像域而不是特征域。在我这个项目里U-Net的编码器负责在不同尺度上提取雨纹特征解码器负责把这些特征映射回雨纹层。中间跳跃连接保证了细节不丢失。编码器用四层下采样每层卷积后加BatchNorm和ReLU解码器对应四层上采样最后接一个1x1卷积输出单通道雨纹图。整体参数量控制在3M左右对显存非常友好。注意如果你用的是真实雨图数据集雨纹层的值可能不是纯粹的加性噪声有些数据集的标注方式是“背景干净图”两者相减得到的S可能含有负值。这时候最后的输出层不要用ReLU激活直接线性输出即可。2.3 损失函数组合L1为主Perceptual和SSIM为辅损失函数是去雨模型里最容易翻车的部分。我用过一段时间的纯MSE损失结果训练出来的模型输出特别平滑边缘细节像水彩画一样。原因在于MSEL2对大的像素误差惩罚呈平方级放大网络为了降低损失会把高频细节主动“磨平”。后来我换成了L1损失输出锐利度明显提升训练过程也更稳。最终采用的组合是L1 loss作为主损失计算预测背景与GT像素间的平均绝对误差。Perceptual loss取VGG16中间层的特征计算两者在特征空间的L1距离。权重设为0.1不能太大否则颜色容易漂移。SSIM loss用于约束局部结构权重0.2改善边缘和纹理的重建。我用一个可调节的加权和把这三种损失拼起来总Loss L1 0.1 * Perceptual 0.2 * SSIM。这个配置在Rain100L上训练20个epoch就能看到明显效果继续训练到80个epoch时PSNR能稳定在31dB以上。3. 数据准备与训练策略合成雨和真实雨的博弈3.1 合成数据集怎么选Rain100L、Rain100H与真实数据集的定位数据是Derain项目的命门。公开数据集里最常用的是Rain100L和Rain100H一个是轻量级雨纹一个是重度雨纹两者都提供雨图和对应的干净图适合用来验证模型结构和收敛情况。我推荐先用Rain100L做快速迭代因为这个数据集的雨纹分布相对均匀模型很容易学会基本规律能帮你快速排除代码里的低级错误。等模型结构验证完毕再换Rain100H测试应对复杂雨纹的能力。真实数据集这块SPA-Data规模比较大包含了几万多张带雨实拍图和对应的干净参考图适合做最终微调。但真实数据集的干净图往往是通过多帧对齐或者人工后期得到的本身存在噪声和模糊训练时不要指望它能像合成数据那样完美对齐。我个人的做法是“合成数据预训练真实数据微调”先用Rain100L训练模型到收敛再用SPA-Data以较小的学习率微调30到50个epoch。这样既能保证模型学到清晰的雨纹先验又能让它适应真实场景中的雨雾散射和镜头光晕。3.2 预处理和数据增强的细节去雨任务的数据预处理有一个容易被忽略的点输入图像的归一化方式。很多新手直接把图像除以255送到网络里这没问题但要注意训练和推理时保持一致。我习惯把图像归一化到[-1, 1]区间配合Tanh输出层数值范围更稳定。数据增强方面随机水平翻转、垂直翻转、旋转90度、随机裁剪这四件套就够用了。裁剪尺寸我通常取256x256太大显存吃不消太小又会让模型看不到完整的雨纹走向。另外不建议做随机亮度、对比度增强因为雨纹的对比度本身就是关键学习信号动它会引入额外的分布偏移。实操技巧如果你用的是合成雨图可以额外叠加一个轻量的运动模糊增强模拟雨丝拖影。做法是把雨纹层沿某个方向平移几个像素后与原图叠加这样能让模型对雨丝方向的鲁棒性明显提升我实测PSNR能提升0.3到0.5dB。3.3 训练参数配置与我的调参顺序优化器我用Adam初始学习率2e-4beta1取0.9beta2取0.999。训练前10个epoch用warmup把学习率从1e-5线性升到2e-4之后用余弦退火衰减到1e-6。批次大小取决于显存输入256x256时我一般用batch size 8两张1080Ti可以跑到10到12的batch。调参顺序很关键。我踩过不少坑之后总结出来的步骤是先固定L1损失跑通全流程确认loss曲线能下降、输出图能看出去雨效果然后加入SSIM loss观察边缘是否更清晰最后加入perceptual loss关注颜色是否偏移。一句话一次只改一个变量否则出问题你根本定位不到原因。4. 从零跑通一个轻量级Derain模型完整实操记录4.1 环境准备与数据加载硬件上我的主力环境是一张RTX 3090显存24G系统是Ubuntu 20.04PyTorch 1.13配合CUDA 11.7。其实这个模型对硬件要求不高8G显存也能跑只是batch要调小一点。数据加载部分我写了一个简单的Dataset类逻辑很直白读取雨图路径和干净图路径按相同随机种子做数据增强。核心代码如下import torch from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T import random class DerainDataset(Dataset): def __init__(self, rainy_paths, clean_paths, patch_size256, trainTrue): self.rainy_paths rainy_paths self.clean_paths clean_paths self.patch_size patch_size self.train train def __len__(self): return len(self.rainy_paths) def __getitem__(self, idx): rainy Image.open(self.rainy_paths[idx]).convert(RGB) clean Image.open(self.clean_paths[idx]).convert(RGB) if self.train: # 随机裁剪 w, h rainy.size x random.randint(0, w - self.patch_size) y random.randint(0, h - self.patch_size) rainy rainy.crop((x, y, x self.patch_size, y self.patch_size)) clean clean.crop((x, y, x self.patch_size, y self.patch_size)) # 随机翻转和旋转 if random.random() 0.5: rainy rainy.transpose(Image.FLIP_LEFT_RIGHT) clean clean.transpose(Image.FLIP_LEFT_RIGHT) if random.random() 0.5: rainy rainy.transpose(Image.FLIP_TOP_BOTTOM) clean clean.transpose(Image.FLIP_TOP_BOTTOM) # 归一化到[-1, 1] to_tensor T.ToTensor() rainy to_tensor(rainy) * 2 - 1 clean to_tensor(clean) * 2 - 1 return rainy, clean注意这里的随机裁剪我没用PyTorch内置的RandomCrop而是手动写了crop坐标目的是保证雨图和干净图用完全相同的坐标裁剪。用单独的RandomCrop分别作用于两张图会导致图像错位这是新手最容易犯的错误。训练时把patch_size设成256推理时直接用原始尺寸输入即可U-Net结构是全卷积的不限制输入大小。4.2 残差U-Net核心代码解析模型部分我保留了一个精简但完整的残差U-Net实现。编码器每层由两个3x3卷积组成一个残差块下采样用stride 2的卷积完成而不是池化这样能保留更多雨纹位置信息。解码器用转置卷积上采样。网络输出的是预测的雨纹层。import torch import torch.nn as nn class ResBlock(nn.Module): def __init__(self, ch): super().__init__() self.conv1 nn.Conv2d(ch, ch, 3, padding1) self.bn1 nn.BatchNorm2d(ch) self.conv2 nn.Conv2d(ch, ch, 3, padding1) self.bn2 nn.BatchNorm2d(ch) self.relu nn.ReLU(inplaceTrue) def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) return self.relu(out identity) class SimpleDerainNet(nn.Module): def __init__(self, in_ch3, base_ch32): super().__init__() # 编码器 self.enc1 nn.Sequential(nn.Conv2d(in_ch, base_ch, 3, padding1), nn.ReLU(inplaceTrue), ResBlock(base_ch)) self.down1 nn.Conv2d(base_ch, base_ch*2, 3, stride2, padding1) self.enc2 nn.Sequential(ResBlock(base_ch*2), ResBlock(base_ch*2)) self.down2 nn.Conv2d(base_ch*2, base_ch*4, 3, stride2, padding1) self.enc3 nn.Sequential(ResBlock(base_ch*4), ResBlock(base_ch*4)) # 解码器 self.up2 nn.ConvTranspose2d(base_ch*4, base_ch*2, 2, stride2) self.dec2 nn.Sequential(ResBlock(base_ch*4), ResBlock(base_ch*4)) self.up1 nn.ConvTranspose2d(base_ch*2, base_ch, 2, stride2) self.dec1 nn.Sequential(ResBlock(base_ch*2), ResBlock(base_ch*2)) self.out nn.Conv2d(base_ch*2, in_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.down1(e1)) e3 self.enc3(self.down2(e2)) d2 self.up2(e3) d2 torch.cat([d2, e2], dim1) d2 self.dec2(d2) d1 self.up1(d2) d1 torch.cat([d1, e1], dim1) d1 self.dec1(d1) rain self.out(d1) return x - rain这里我用的是先把输入x和预测的雨纹层做差再返回干净背景。训练时的监督目标是干净图B所以loss是直接比较网络输出和B。你也可以改成让网络输出雨纹层再用x减掉效果差别不大关键是别漏掉这个减法步骤。如果不做残差学习直接让网络输出背景图训练会明显变慢我实测同样epoch下PSNR大概低1.5到2dB。4.3 训练循环与评估指标训练循环本身不复杂但有几个细节值得说。第一个是损失函数的权重我前面提到的组合在代码里是这样实现的import torch.nn.functional as F from torchvision.models import vgg16 class Losses(nn.Module): def __init__(self, vgg_weight0.1, ssim_weight0.2): super().__init__() self.vgg vgg16(pretrainedTrue).features[:16].eval() for p in self.vgg.parameters(): p.requires_grad False self.vgg_weight vgg_weight self.ssim_weight ssim_weight def forward(self, pred, gt): l1 F.l1_loss(pred, gt) perc F.l1_loss(self.vgg(pred), self.vgg(gt)) ssim 1 - compute_ssim(pred, gt) return l1 self.vgg_weight * perc self.ssim_weight * ssimSSIM我这里简单写法是用pytorch_msssim库里的SSIM模块。注意Perceptual loss的VGG特征层用的是前16层也就是到conv4_3为止太深的层对颜色和纹理过于敏感反而不利于像素重建。评估指标我用PSNR和SSIM两个标准指标。PSNR衡量重建图像与GT的像素级逼近程度SSIM衡量结构相似度。测试时要先把网络输出从[-1,1]反归一化到[0,255]再计算这一点容易漏。代码参考def calculate_psnr(img1, img2, data_range255.0): img1 (img1 1) / 2 * 255 img2 (img2 1) / 2 * 255 mse torch.mean((img1 - img2) ** 2) return 10 * torch.log10(data_range**2 / (mse 1e-8))训练过程我用TensorBoard记录loss曲线、PSNR曲线和中间可视化的去雨结果。每训练500个step就把当前模型在验证集上跑一遍保存一张对比图这样能直观看到模型是变好了还是变坏了。模型保存我习惯同时存state_dict和完整模型方便快速部署。5. 踩坑实录训练到上线的9个典型问题5.1 输出发糊、细节丢失这是去雨项目里最典型的失败模式。模型训练时loss一直在降但可视化结果里背景边缘全是糊的。我最初遇到这个问题时先改了损失函数从MSE换到L1输出锐利度立刻改善。后来还发现BatchNorm的batch size太小也会导致这个问题因为在batch较小时BN统计的均值和方差不准影响特征分布表述。如果L1换完还是糊就检查是不是数据增强太少导致过拟合。那种只用一个数据集硬train整个周期的模型最容易把“雨纹”和“背景纹理”一起抹掉。解决办法是增加随机裁剪数量和翻转增强或者降低模型容量让网络没那么容易把高频信息当成噪声滤掉。5.2 雨纹残留和颜色偏差雨纹残留有两种情况一种是细密的雨丝没去干净集中在高频区域这种情况可以给模型加一个通道注意力机制让网络自己学会关注雨纹密集的区域另一种是图像里大面积灰蒙蒙的雨雾残留这其实是散射导致的亮度衰减单纯靠残差U-Net不够需要引入大气散射模型做辅助监督或者用带透射率的混合数据集再微调。颜色偏差通常是因为Perceptual loss权重太高或者输入图像归一化不一致。我试过把Perceptual权重从0.1调高到0.5输出颜色明显偏黄绿。直觉上VGG特征是针对分类任务训练的它对颜色差异不敏感所以权重太大容易让网络在颜色维度上“放飞”。解决办法是降低权重或者给Loss加一个全局均值对齐项强制重建结果在RGB三通道的均值接近GT。5.3 真实场景泛化差这是最让人头疼的问题。在合成雨图上PSNR能到35dB的模型放到监控实拍画面里效果惨不忍睹。根本原因在于合成雨的粒度和真实雨完全不同合成雨像均匀撒盐真实雨有粗细、方向、景深的变化还有镜头造成的局部失真。我的应对方案有三条线第一数据层面让合成雨更贴近真实用运动模糊加随机方向的雨丝生成器而不是简单叠加雨纹贴图第二训练层面合成数据预训练后用SPA-Data真实数据微调把学习率降到5e-5以下防止微调时灾难性遗忘第三推理层面把大图切成重叠块推理再拼接减少雨雾分布在空间上的不均匀带来的影响。5.4 常见问题速查表现象可能原因排查优先级loss不降学习率太大或太小先看lr曲线loss下降但输出模糊用了MSE损失/batch太小换L1调大batch雨纹残留但背景清晰模型容量不足/缺注意力加深网络或加SE模块颜色偏色Perceptual权重过大/归一化不一致降权重检查预处理真实图泛化差合成雨分布和真实差异大用真实数据微调显存溢出patch太大/batch太大降patch到192梯度累积训练抖动剧烈BN统计不稳定/lr过高加warmup降低lr推理时输出有棋盘格转置卷积出现伪影换用亚像素卷积或双线性上采样PSNR高但主观差指标和视觉感知不一致加点GAN loss或perceptual loss5.5 推理部署时的小技巧项目到部署阶段还有一个容易踩的坑——输入尺寸的归一化。如果训练时用的是[-1,1]或[0,1]推理服务里的预处理必须完全一致否则模型输出会偏暗或偏亮。另外模型如果是用RGB训练的而业务方拿过来的输入是BGR比如OpenCV读图排序一旦错了你得到的输出会呈现诡异的蓝色偏色这种问题排查起来非常隐蔽。我建议在推理入口处加一行断言强制检查输入张量的通道均值和训练时的一致。写在最后的个人体会做Derain项目这一年多我最深的感受是去雨任务没有银弹模型效果的三成靠结构七成靠数据和训练策略。结构上别盲目追新U-Net加残差学习这个组合放到今天依然能打数据上合成和真实要两手抓缺了真实数据微调模型永远停留在实验阶段。如果你正准备开始自己的第一个去雨项目我建议不要一上来就追求高指标先在小数据集上把训练链路跑顺再逐步扩大数据规模。训练过程中多保存几个中间模型的输出图隔几天翻一翻你对模型行为模式的直觉比任何论文里的经验和公式都可靠。本文还有配套的精品资源点击获取
返回列表