简介基于MOSS网络与CR损失的图像去雨Python项目面向计算机视觉、图像处理方向的学生与研究者可用于毕业设计、课程设计及算法复现。项目源码完整包含网络结构定义、CR损失实现、训练与测试脚本以及PSNR/SSIM评估模块可直接运行测试。压缩包共21个文件以9个.py源码文件和2个.pth训练好的模型为核心附带10个.pyc编译版本整体约60.38MB便于快速部署与二次开发。同时内置数据加载和特征可视化等工具适合在此基础上扩展新的去雨思路。目前已有156人学习下载资源由专业团队维护并提供远程技术指导可降低复现门槛是入门图像去雨或开展相关实验的实用参考。1. 图像去雨不是玄学MOSS网络CR损失这套源码能做什么单张图像去雨很多人以为难点在网络结构真正拆过这套基于MOSS网络CR损失的图像去雨python源码之后你会发现难的是让网络在特征层面区分雨纹和背景纹理。MOSS网络负责多尺度特征交互与背景重建CR损失在特征空间把预测结果拉向真图、推离雨图输入两者配合才避免去雨后图像发糊。这份zip最实用的地方在于result目录下放着训练好的model_best.pth和model_latest.pth不用从头训练就能先把推理流程跑通。它适合三类人做毕设需要去雨基线的学生、课程设计要交可运行项目的从业者以及想研究对比正则化损失怎么落到具体任务上的读者。如果你已经装好Python 3.6到3.8环境按下面的流程把依赖补上用best权重跑test.py很快就能看到第一张去雨效果图。2. 先摸清三个关键文件networks.py、CR.py与train.py的职责分工资源目录里文件不少但真正决定能不能跑通的只有三块。utils下放的是data_RGB.py、dataset_RGB.py、PSNR.py、SSIM.py这些辅助模块networks.py和CR.py在model目录里train.py、test.py在根目录test/featuremap.py负责可视化result目录下两个.pth才是训练产物。拆源码的顺序我建议反着来先看模型和损失再看训练循环最后看数据加载。去雨这类低层视觉任务数据加载环节最容易出问题放到后面排查反而高效。2.1 networks.pyMOSS结构在代码里长什么样MOSS网络在去雨任务里走的是多尺度特征交互加分阶段重建的路线。它和普通U-Net最大的区别在于不同尺度分支之间不是单向skip connection而是互相传递特征。浅层分支保留雨纹的纹理信息深层分支负责全局亮度与背景结构最后把多尺度结果融合重建。在代码层面networks.py通常导出一个MOSS类构造函数接收输入通道、输出通道和基础通道数这几个参数。from model.networks import MOSS model MOSS(in_channels3, out_channels3, base_channels32, scale3) rain torch.rand(1, 3, 256, 256) pred, feats model(rain) # 部分版本只返回pred print(pred.shape) # torch.Size([1, 3, 256, 256]) print(len(feats)) # 中间特征个数一般等于scale这段实例化代码里in_channels和out_channels都为3因为输入输出都是RGB三通道图像base_channels是每层特征的基础宽度32是省显存的起点配置显存有富余可以换成64参数量和细节恢复能力都会上来scale3表示三个尺度分支对应网络里的特征金字塔层数。如果在你手头这个版本里MOSS只返回pred那CR损失一般是在train.py内部通过提取模型中间层特征来计算的不影响整体逻辑。加载预训练权重有一个值得注意的细节MOSS类名和state_dict里的key必须严格一致。如果自己改了base_channels或者scale训练好的model_best.pth大概率会报missing key因为权重shape对不上。所以第一次复现时别急着魔改网络参数先把原配置跑通再考虑扩展。2.2 CR.pyCR损失的实现套路与权重建议CR损失在这套代码里的定位是特征层面的对比正则化。去雨本质是回归任务单用L1损失只在像素维度约束输出模型容易学出保守的平滑结果。加上CR损失后预测图被约束在特征空间里靠近GT、同时远离输入雨图。这个远离输入的约束很关键它逼着网络真的把雨纹当作要删除的东西而不是把输入抄一遍。class CRLoss(nn.Module): def __init__(self, tau0.1, margin0.2): super().__init__() self.tau tau # 负样本惩罚缩放 self.margin margin # 相似度容忍阈值 def forward(self, f_pred, f_gt, f_in): pos F.cosine_similarity(f_pred, f_gt, dim1).mean() neg F.cosine_similarity(f_pred, f_in, dim1).mean() return -pos self.tau * F.relu(neg - self.margin)这个简化实现里cosine_similarity在通道维度上计算特征向量的夹角余弦pos越高代表预测与GT越接近所以loss第一项取负。neg代表预测与雨图输入的相似度relu(neg - margin)只惩罚超过容忍阈值的部分意思是允许保留一点原始信息但不许把雨纹也留下。tau是负样本项的缩放系数训练前期给0.1等主损失降到平台期再调成0.05。margin取0.2左右比较稳太大去雨不彻底太小网络会过度重构背景细节产生伪影。在train.py里CR损失不会单独出现常见组合是rec_loss L1(pred, gt) w_cr * CR(...)w_cr建议从0.1起步。一个很常见的现象是training loss一路下降但验证PSNR不动这时候先别怀疑网络把w_cr从0.2降到0.05再跑一轮大概率是负样本惩罚压过头了模型为了降低CR那项把所有高频信息都抹掉了。2.3 train.py训练主循环与模型选择逻辑train.py是整套代码的调度中心看懂它的命令行参数就相当于拿到了调参说明书。常见参数包括数据目录、batch_size、crop_size、学习率、训练轮数、CR损失权重、验证间隔和保存目录。下面这条命令是我在8G显存机器上比较稳的启动方式。python train.py --data_dir ./datasets/Rain100H \ --batch_size 8 --crop_size 128 --lr 1e-4 \ --epochs 200 --loss_cr 0.1 --val_interval 5 \ --save_dir ./resultbatch_size和crop_size共同决定显存占用8G显存下8和128这对组合比较稳。lr用1e-4配合Adam是去雨训练的常态起点如果验证损失震荡明显优先把lr降到5e-5而不是动batch_size。loss_cr对应CR损失权重val_interval5表示每5个epoch评估一次验证集间隔太大会让你在模型已经退化之后才发现问题。for epoch in range(start_epoch, args.epochs): model.train() for rain, clean in train_loader: pred model(rain) l_rec F.l1_loss(pred, clean) l_cr cr_loss(*get_feats(pred, clean, rain)) loss l_rec args.loss_cr * l_cr optimizer.zero_grad() loss.backward() optimizer.step() if epoch % val_interval 0: psnr evaluate(model, val_loader) if psnr best_psnr: torch.save(model.state_dict(), f{save_dir}/model_best.pth)这段主循环有三个容易忽视的点。第一重建损失用L1而不是L2去雨领域L1对边缘更友好L2天然偏向平滑会让去雨结果显得干净但细节尽失。第二每次迭代都执行optimizer.zero_grad()清空梯度漏掉这一步梯度会跨batch累加训练曲线出现锯齿状跳动。第三挑选best模型依据的是验证集PSNR而非训练loss因为训练loss里混着CR正则项不能直接代表去雨质量。3. 把训练跑起来数据集准备、参数设定与断点续训的完整姿势模型和损失看明白了接下来面对的是会拦住大多数人的第一道坎数据。去雨训练需要成对的雨图和干净图dataset_RGB.py负责把这对图片读进来并做增强。很多人一上来就拿着自己的图片往目录里丢结果train.py报出一堆FileNotFoundError。开始之前提醒一句如果你还没把Python环境顺好建议直接用vscode或pycharm建一个3.6到3.8的解释器再装torch和opencv后面跟着这套流程会顺手很多。3.1 数据集目录约定配对文件怎么摆dataset_RGB.py这类数据模块内部通常是一个按文件名配对扫描的逻辑。我见过最多的是两类一类是rain和clean两个子目录同名文件一一对应另一类是单目录下带_t和_gt后缀的文件对。这套源码从目录结构推断更接近前者按rain/{name}.png和clean/{name}.png来配对。建议的目录结构如下datasets/Rain100H/ rain/ 0001.png 0002.png ... clean/ 0001.png 0002.png ...配对逻辑只认文件名rain子目录里的0001.png对应clean子目录里的0001.png任何一边缺文件都会在训练中途抛出读取错误。如果你有自己的数据集建议先写一段脚本统一改名保证两端文件名完全一致再给图片编号。我刚上手时习惯直接复制一份Rain100H的目录结构把图片替换掉省得改数据模块的代码。如果你发现代码里预期的是单目录加后缀的配对方式也就是同一文件夹下rain_0001.png和clean_0001.png这样的命名那就把图片重命名放进同一个目录。判断依据很简单看dataset_RGB.py里是用glob扫描两个子目录还是扫描一个目录后split文件名。这一步值得花十分钟确认比跑挂了再排查高效。3.2 train.py命令行参数改哪些、不建议改哪些数据就位后启动训练前把参数过一遍。需要改的只有四类数据路径、训练预算、损失权重和保存路径。显存不够时优先动crop_size而不是batch_sizecrop_size从128降到96显存占用直接降到原来一半左右。batch_size低于2会影响BatchNorm统计量如果网络里用了BN层建议保持batch至少4。数据增强里常见的有随机翻转和随机裁剪crop_size就是裁剪这一项的参数。不建议改的是网络结构相关参数和初始学习率组合。第一次训练就用默认base_channels32和lr1e-4跑完200个epoch再说。很多人一上来把lr调到2e-4追求速度结果训练前几十个epoch loss就出现NaN排查半天发现是学习率过大导致梯度爆炸。这时候不是调参的问题是训练策略的问题正确做法是加梯度裁剪。python train.py --data_dir ./datasets/Rain100H --batch_size 4 \ --crop_size 96 --lr 1e-4 --epochs 200 --loss_cr 0.1 \ --grad_clip 0.5 --save_dir ./resultgrad_clip设为0.5表示把梯度的L2范数裁到0.5以内这是防止NaN的兜底手段。前面那个NaN案例里加上grad_clip再配合lr降到1e-4训练马上恢复正常。这个参数在训练代码里通常是一行if语句找到torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)放进去即可。3.3 从model_best.pth到model_latest.pth断点续训的正确姿势result目录下两个模型文件分工完全不同。model_latest.pth是每个epoch结束时保存的最近状态里面除了模型权重通常还带着optimizer状态和当前epoch数专门给断点续训用。model_best.pth是验证集PSNR最高的那次保存只存权重但对应的是当前数据下效果最好的模型。这两个文件不能混用。# 续训从latest恢复会接着原来的epoch数继续 python train.py --data_dir ./datasets/Rain100H --resume ./result/model_latest.pth # 推理/微调加载best的权重但不恢复优化器状态 python test.py --weights ./result/model_best.pth --data_dir ./datasets/Rain100H/test续训用--resume关键是恢复optimizer状态和学习率调度器这样继续训练时学习率修正不会乱。微调用load_state_dict加载best权重此时网络参数被覆盖但优化器重新初始化。不少人在模型跑了一半中断后用best的pth接回去训练结果学习率从头开始前几个epoch把已经收敛的权重又冲乱了。这是典型的加载方式选错翻车现场。另外注意如果训练代码本身有更新比如networks.py里加了新模块旧权重加载会报missing key。这种情况不要硬来把新模块对应的权重初始化代码补上用load_state_dict(..., strictFalse)加载再在日志里确认缺的key确实只是新模块。4. 测试与特征可视化test.py、featuremap.py把模型效果落到实处训练或者直接使用训练好的模型最终都要落到测试这一步。test.py和featuremap.py两个脚本一个负责出数值结果一个负责出可视化结果。很多人只会跑test.py拿PSNR却忽略了featuremap.py实际上后者才是判断模型是否真正学到去雨逻辑的关键。这章把两条路径都走一遍顺带说清楚PSNR和SSIM怎么读数才有意义。4.1 test.py单张推理与批量评估两条路test.py通常支持两种模式给一张图做单张推理或者给一个目录做批量评估。批量评估时会自动调用PSNR.py和SSIM.py计算指标所以测试数据必须保持rain和clean成对。单张推理只需要一张雨图输出是去雨后的png。常见启动方式如下python test.py --data_dir ./datasets/Rain100H/test \ --weights ./result/model_best.pth --save_dir ./outputweights参数指向训练好的权重建议用model_best.pth。data_dir指向测试集脚本会按训练时相同的配对逻辑读取。save_dir是输出目录跑完以后output目录下每个样本会同时存预测图和记录指标的txt或csv。如果单张推理看不出效果我一般会并排对比三张图输入雨图、输出去雨图、干净GT叠在一起看细节差异。test.py里最容易出错的是归一化方向。训练时图像被缩放到[0,1]区间测试时如果读进来还是0到255的整数网络输出就会整体偏移最后保存的图发灰发暗。遇到这种情况先检查预处理有没有除以255以及保存前有没有clip到[0,1]再乘255。这个坑在测试环节出现频率非常高。4.2 featuremap.py打开去雨网络的黑匣子featuremap.py的价值在于把网络的中间特征可视化出来。去雨网络不是一个完全的黑匣子浅层特征图通常对应雨纹的位置和方向深层特征图对应背景结构与亮度信息。通过可视化你能直接判断模型是在删除雨纹还是在把整张图模糊掉。前者特征图里雨纹区域高亮后者特征图整个是平滑的训练早期就能发现不用等200个epoch跑完。import cv2 rain load_image(sample_rain.png) pred, feats model(rain) # feats是中间特征列表 for i, f in enumerate(feats): vis f.detach().squeeze(0).mean(0) # 通道维平均 vis (vis - vis.min()) / (vis.max() - vis.min() 1e-6) cv2.imwrite(ffeat_{i}.png, (vis * 255).astype(uint8))这段代码把每个尺度的特征图在通道维度取平均再归一化到0到255保存成png。mean(0)是为了把多个通道压缩成一张可读图。加1e-6防止特征图全为常数时除零。detach()是因为特征图带梯度直接转numpy会报错。如果某个尺度的特征图全黑或全白先检查有没有做归一化再看是不是该层响应本身趋近零。通常第一个尺度的特征图最能反映雨纹提取质量。我用这个脚本排查过很多次PSNR高但视觉差的情况。有一次模型在benchmark上PSNR到了32dB但特征图显示第二尺度分支几乎没有激活等于网络绕过了中间分支直接走捷径重建视觉上雨纹是没了但背景纹理也丢了。逼着我把那层分支的权重重新初始化再微调效果才正常。4.3 PSNR与SSIM的正确读数口径去雨的论文和复现代码之间最常见的争议就是分数对不上。PSNR和SSIM的数值高度依赖评估口径直接比较不同代码跑出来的数字经常得出错误结论。一个规范的评估流程通常要统一三点计算通道、边界裁剪、数值范围。我一般在评估代码里显式控制这三个条件避免数据集自带的评估脚本偷偷用不同口径。口径项常见做法备注评估通道YCbCr的Y通道RGB三通道全算会偏低边界裁剪裁掉边缘4像素边缘伪影不计入数值范围统一[0,1]或[0,255]与GT保持同一范围指标定义PSNR取MSE均值的log值不要混用Y通道和RGB这四条里最容易踩的是边界裁剪。很多网络对边缘处理时会产生伪影如果评估时边缘像素也算进去PSNR会掉0.5到1dB看起来就像你的实现比论文差一截。SSIM同理在Y通道上算窗口大小7x7或11x11并不敏感但RGB直接算SSIM会明显偏低。跑test.py之前先确认评估脚本里有没有这四项里的任何一项与你的预期不一致。5. 避坑图像去雨复现里最常见的五个翻车现场去雨代码本身不算复杂真正消耗时间的是各种环境、数据、训练层面的隐性故障。以下五条是我自己复现、帮别人调试时反复遇到的按现象到原因到解决的顺序写每一条都能单独对应一次真实的调试经历。如果你第一次跑这套源码建议把这章当成排查手册用。5.1 数据与显存相关的坑第一个坑是训练刚开始就CUDA out of memory。现象是loss还没打印几个batch就报显存不足进程直接崩掉。原因是默认参数在大分辨率或大batch下超出了显卡显存特别是8G显存的机器跑256x256加batch8很容易爆。解决方法是把crop_size降到128或96、batch_size降到2到4这两个参数直接决定显存占用的大头。如果还报错再检查有没有把num_workers临时设成0排除数据加载线程的额外显存占用。第二个坑是数据配对错位导致loss异常偏高。现象是训练初期loss就比正常值高一个量级偶尔伴随图像内容完全不相关的错误样本。原因是rain目录和clean目录文件名字典序不一致比如rain里是1.png、10.pngclean里是1.png、2.png配对逻辑按sorted顺序就会错位。解决方法是先把两个目录里的文件名统一成定长编号比如0001.png这种格式再在数据集类里加一段debug打印输出前三个配对的文件路径确认对应关系。这一步十分钟能搞定能省后面几十个小时的困惑。5.2 训练收敛与模型加载相关的坑第三个坑是训练loss下降但验证PSNR不涨。现象是L1和CR的组合loss从0.05降到0.01可验证集PSNR始终在同一个水平徘徊。原因是CR损失权重过大模型为了降低负样本相似度把高频细节整体抹平PSNR在这种平滑但无纹理的输出上提不上去。解决方法是把loss_cr从0.2降到0.05并且主损失增加L1与SSIM的组合形式SSIM项对结构保持更敏感能拉着模型往有纹理的方向走。调整后通常几个epoch内就能看到PSNR回升。第四个坑是加载预训练权重报missing key或unexpected key。现象是torch.load后load_state_dict直接抛异常或者strictFalse下静默丢掉了部分层的权重。原因是网络配置和训练权重不匹配最常见的是base_channels被改过、scale变了或者MOSS类名在refactor时换了模块名。解决办法是先用torch.load打印state_dict里的key清单和当前模型的named_parameters逐项比对不一致的点基本都集中在第一个卷积层和最后的重建层。如果只是改了输入通道数可以直接改第一层卷积的权重shape把预训练权重加载进去而不是放弃预训练。第五个坑是复现结果比论文低2到3dB。现象是同样的数据集、同样的网络跑出来的PSNR就是和论文对不上。原因是评估口径不一致比如论文在Y通道裁边4像素下评估你的脚本在RGB全图上评估。解决方法是严格统一评估标准按4.3节的四项目口径来Y通道、裁边4像素、数值范围与GT一致。先拿官方预训练模型在自己的评估脚本下跑一遍如果还差很多再排查测试集的裁剪操作是否和训练一致很多测试图里有边框或黑边这些区域会显著拉低分数。这个坑我吃过一次大的之后任何去雨项目第一件事就是固定评估脚本。6. 在自有数据上微调先固定浅层再放开深层的小习惯把训练好的MOSS网络用到自己的场景里最常见的需求是在一批新数据上微调而不是从零训练。我的习惯是分两步走第一步固定浅层只更新深层和重建层跑10到20个epoch第二步再放开全部层用更小的学习率训练到收敛。浅层学的是底层纹理和边缘跨数据集时相对通用直接冻结可以防止新数据把通用特征带偏。深层学的是雨纹分布和背景结构的组合方式你的数据如果和原训练集差别大主要靠这一段来适配。for name, param in model.named_parameters(): param.requires_grad not name.startswith((stage1, stage2)) optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr5e-5)这段代码里stage1、stage2对应网络前两个尺度的模块名如果你的MOSS实现用了别的命名用print(name)把模块名打印出来再填进去。requires_grad设为False的层optimizer里用filter过滤掉就不会给它更新。小lr取5e-5比从头训练低一倍避免把预训练权重冲乱。CR损失的权重在这个阶段建议保持在0.05因为新数据上特征分布已经在漂移正则项再大容易限制适配能力。我自己在这个习惯上栽过跟头。有一回拿着监控场景的雨图直接全量微调浅层也在更新跑了一整晚第二天验证PSNR比预训练模型还低特征图里第一尺度分支全乱了。从那以后我每次拿到新数据集都会先按上面这套冻结浅层的流程跑10个epoch看loss和验证PSNR的趋势再决定要不要放开浅层这个习惯让微调的翻车率降了大半。如果你也是拿这套源码做自己的数据建议从固定浅层开始成本很低但能省掉很多盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取
