基于Pytorch的DualGAN图像去雾实战:原理、训练与避坑指南

📅 发布时间:2026/10/11 22:06:43
基于Pytorch的DualGAN图像去雾实战:原理、训练与避坑指南
简介基于PyTorch实现对偶生成对抗网络来完成图像去雾是一份完整可运行的高分毕业设计项目特别适合计算机相关专业正在准备毕设、课程设计或期末大作业的学生也适合希望进行图像修复实战的学习者。整个压缩包共25个文件大小约21.23MB包含10个Python脚本覆盖网络定义、训练、预测、参数解析、日志记录等核心环节另有2个pkl训练好的模型文件可直接加载用于去雾推理并配有5张测试雾图、5张预测结果图、1张loss曲线图及一份项目说明文档目录划分清晰。项目提供判别器与生成器实现、双模型权重以及独立预测脚本读者拿到后既能复现训练流程也能快速调用预训练模型观察去雾结果方便在此基础上改进算法或撰写毕业设计文档。目前已吸引144人学习下载代码结构简洁、注释清楚对入门图像翻译的开发者尤为友好。1. 图像去雾遇上对偶生成对抗网络先搞懂这份资源能帮你省多少事做图像去雾毕设时我一开始用暗通道先验浓雾图一多就会偏色换成pix2pix又得有雾/无雾成对图像标数据标到崩溃。后来我拿到一份基于Pytorch实现对偶生成对抗网络DualGAN做去雾的源码包里面带了训练好的模型和文档说明跑通第一张图只花了半天。这份资源把无监督图像去雾的核心流程全部封装好了生成器和判别器的结构、循环一致性损失、训练脚本、推理脚本以及一版已经收敛的权重。适合正在找Python源码做毕业设计的人也适合想快速验证GAN去雾效果但不想从零搭模型的工程师。它解决的是没有成对数据时的去雾问题只要有若干有雾图像和若干无雾图像DualGAN就能学到两个域之间的映射。不用一张张配标注这恰好是真实场景最需要的。下面我从原理、环境、训练、避坑到进阶按我实际跑这份资源的顺序展开每一步都写好参数和坑点方便你照着复现。2. 对偶生成对抗网络去雾的原理循环一致性为什么比成对监督更省事2.1 DualGAN的双向结构两个生成器和两个判别器各司其职对偶生成对抗网络DualGAN本质是图像到图像翻译框架通常包含两个生成器和两个判别器。生成器G负责把有雾域A域转换到无雾域B域生成器F负责把无雾域转换回有雾域。判别器D_A负责判断输入图像是真实的A域还是F生成出来的假A域D_B负责判断真实的B域还是G生成出来的假B域。G和F可以是相同网络类但权重各自独立不会共享。“对偶”体现在两个方向同时训练。训练时一张有雾图a经过G得到假无雾图G(a)再经过F得到重建图F(G(a))重建图要和原图a尽量接近。反过来无雾图b经过F得到假有雾图F(b)再经过G得到重建图G(F(b))也要和b接近。这个双向重建约束就是循环一致性。在Pytorch源码里生成器常见结构是ResNet残差块组成或者UNet判别器常用PatchGAN输出一个N×N矩阵每个元素判断一个局部区域的真假。PatchGAN对边缘和纹理更敏感去雾任务里能保留更多细节。如果G参数太多显存不够可以把生成器从ResNet9Block换成6Block但感受野变小去雾后的烟雾残留会增加。这是我在调显存时踩过的坑后文避坑部分再细说。2.2 循环一致性损失没有成对样本也能对齐图像域DualGAN的损失函数由三部分组成两个GAN对抗损失和一个循环一致性损失。对抗损失让生成图像在目标域里“看起来真实”循环一致性损失约束结构和内容不丢失。用L2或L1范数计算重建误差通常L1比L2在图像任务中保留边缘更锐利所以源码里多用L1作为循环损失。一个关键点单纯对抗损失不够生成器可能会把所有图都生成成相同风格但内容无关的图。加上循环一致性后G(a)经过F还得能还原成a等于强制要求G保留原始图像的结构信息。去雾场景里有雾图和无雾图共享场景内容只是多了一层雾所以循环一致性天然契合这个任务。实际训练时超参数λ控制循环损失的权重。我看过一些开源实现λ取10比较常见但也有取5的。λ太小生成图像容易有伪影λ太大模型偏向于重建原图去雾不彻底。这个值在训练阶段需要重点观察。2.3 选型理由对比暗通道先验和pix2pix的边界暗通道先验是传统去雾的代表算法假设无雾图像的局部暗通道值趋近零据此估计透射率和全局大气光。它不需要数据训练速度也快但遇到天空、白色区域、浓雾且景深复杂时透射率估计会失效结果发灰或偏色。我用暗通道先验处理逆光雾图天空部分直接变成灰蓝色块。pix2pix是成对监督的GAN效果通常比暗通道好但要求提供逐像素对齐的有雾/无雾对。公开数据集如RESIDE里有合成对但真实场景很难获得。DualGAN只需要两个域的分开图片训练数据容易获取这是它的核心优势。CycleGAN和DualGAN在思路上几乎一致都是双向GAN加循环一致性区别主要在实现细节和损失形式。DualGAN的生成器输入输出会带域标签判别器在输入时也拼接域标签。这套源码大概率参考DualGAN原论文实现训练好的模型权重对一般室外雾图效果不错。选择它而不是CycleGAN多数时候只是习惯问题效果差异不大。3. 环境搭建与源码结构从Pytorch安装到跑通第一张图3.1 Pytorch环境搭建CUDA、cudnn与conda三步到位拿到源码第一步是搭环境。我建议用conda创建独立环境避免和系统Python混在一起。Windows或Linux都适用下面是我的常用安装命令。conda create -n defog python3.8 conda activate defog conda install pytorch torchvision torchaudio cudatoolkit11.3 -c pytorch -c conda-forge pip install opencv-python pillow numpy matplotlib tqdm tensorboard先说版本搭配。Pytorch版本和CUDA对应关系很敏感装错了会出现在import时找不到CUDA或者能import但torch.cuda.is_available()返回False。如果显卡驱动支持CUDA 11.x上面这组命令基本能用如果是30系以后的新卡建议用官方推荐的pip安装方式到Pytorch官网选对应CUDA版本生成安装命令。装完后用小段代码验证。import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))这段代码的作用很简单第一行打印Pytorch版本第二行输出True说明CUDA可用第三行打印显卡型号。如果输出False多半是CUDA工具链版本不匹配不要急着跑训练脚本先把环境问题解决。源码包里如果还有requirements.txt记得在Pytorch装好之后再执行pip install -r requirements.txt不然pip可能会用默认源装出一个CPU版Pytorch后面推理速度慢到你想哭。3.2 源码目录与模型文件布局哪个文件对应哪一步一份规范的源码包通常包含这几个目录和文件路径作用models/生成器、判别器网络定义datasets/数据集加载和预处理utils/工具函数图像变换、损失计算、可视化train.py训练入口test.py推理入口options/参数配置使用argparsecheckpoints/训练好的模型权重文件你拿到的资源里如果还有docs/目录里面应该是环境说明和实验报告。训练好的模型一般以.pth或.pkl保存通常放在checkpoints目录下文件名像latest_net_G.pth和latest_net_D.pth。G是生成器D是判别器。推理时只需要G的权重D不参与生成。拿到源码后第一步先看README或文档说明里的“快速开始”命令。如果文档缺失就直接看test.py开头几行一般会有parse_args函数里面有--checkpoints_dir、--model_path、--input_dir、--output_dir等参数。理解这些参数的含义比盲目点开每个py文件更重要。3.3 用训练好的模型对单张图片去雾推理脚本与参数说明假设源码里test.py接收以下参数输入图片路径、输出路径、生成器权重路径、是否用GPU。运行命令大致如下。python test.py --input images/haze01.png --output results/haze01_defog.png \ --checkpoint checkpoints/latest_net_G.pth --device cuda:0如果你用PyCharm直接跑记得在Run Configuration里配置Working directory为项目根目录不然相对路径找不到。我在这里翻过车明明文件就在当前目录却老报FileNotFoundError。test.py的核心逻辑其实就几步加载模型、读取图片、前向推理、保存结果。用伪代码理解# 加载生成器 netG define_G(input_nc3, output_nc3, ngf64, netGresnet_9blocks) netG.load_state_dict(torch.load(checkpoint_path, map_locationdevice)) # 读取并归一化 img Image.open(input_path).convert(RGB) img_tensor transform(img).unsqueeze(0).to(device) # 前向推理 with torch.no_grad(): fake_img netG(img_tensor) # 反归一化保存 out_img tensor_to_image(fake_img) out_img.save(output_path)这里最容易出错的是归一化。训练时一般把图像缩放到[-1,1]即img/255*2-1推理完成后要把输出再转回[0,255]保存。很多新手在保存结果时忘了反归一化得到一张黑底图或纯色图。另外map_locationcuda:0写死会出问题如果你的机器只有CPU就应该改成map_locationcpu或者在加载前先判断设备。源码里有时会加map_location参数没有的话最好自己补一行。4. 训练自己的去雾模型数据集、参数与收敛判断4.1 数据集格式无成对样本的两个域怎么组织训练DualGAN不需要成对图片但需要两个域各自的图片集。常见组织方式是两个文件夹haze/和clear/。如果你自己用手机拍haze放有雾场景clear放同场景晴天或无雾图数量不需要一一对应但内容尽量覆盖相同的场景类型。我一般把图片处理成256×256或512×512。虽然GAN能处理大图但显存有限分辨率越高batch size就得越小。训练时用中心裁剪和随机翻转做数据增强。源码里datasets文件夹通常有一个读取函数每次迭代从haze和clear各取一张图一起送入模型不需要配对。这里有一条血泪经验两个域的数据量别太悬殊。如果haze有1000张clear只有100张模型很快会把clear域记住生成图像丧失多样性。我遇到过F生成器直接输出训练集里某张样本的翻版G也跟着崩溃。解决办法是把clear做rotate、hflip、色彩抖动增强或者换一个更大的公开数据集。4.2 训练脚本参数解析epoch、batch_size、lambda与学习率训练命令通常长这样python train.py --dataroot ./datasets/defog --name defog_exp1 \ --model dualgan --lambda_L1 10 --lr 0.0002 \ --batch_size 1 --epoch_count 1 --n_epochs 100 --n_epochs_decay 100参数说明一下--dataroot 指向数据集根目录源码里要能看到train/haze和train/clear两个子文件夹。--batch_size 在图像GAN里经常设为1因为要尽量提高单图分辨率。如果你的显卡显存大于12G可以试batch_size2但训练过程容易抖的时候需要调回1。--lambda_L1 是循环一致性损失的权重默认10这个值不建议频繁动除非你发现重建图模糊。--lr 初始学习率Adam优化器常用0.0002。我试过0.001训练很快发散损失直接NaN。--n_epochs 和 --n_epochs_decay 是线性衰减策略前100轮固定学习率后100轮线性降到0。训练过程中你需要留意几个损失D_A_loss, D_B_loss, G_loss, cycle_loss。源码里一般每50 iteration会打印一次也支持tensorboard可视化。4.3 训练过程监控损失曲线与生成器输出采样不只是看loss数字我习惯每500个iteration保存一次生成器输出把有雾图、生成去雾图、重建图并排拼接。人眼看着生成图从模糊到清晰比loss曲线更直观。一个实用的判断点如果cycle_loss在下降但G_loss不变说明循环一致性在起作用但生成器的对抗学习停滞。常见原因是判别器训练得太好梯度消失。这时可以把判别器的学习率调低例如生成器lr0.0002判别器lr0.0001或者给生成器加梯度惩罚项。如果训练到中后期去雾图出现严重伪影可以降低总体epoch或者提前把lambda_L1调大强制保留更多原始结构。每一次训练跑完模型保存在checkpoints目录下通常是最新的netG.pth。为了防止坏权重覆盖好权重我每次在损失最低点手动复制一份文件名带上iteration。这是最简单有效的后悔药。5. 实战避坑从环境报错到模型效果不佳的5个常见问题5.1 CUDA out of memory显存不够时先查这条现象训练或推理时报RuntimeError: CUDA out of memory显示器画面黑一下程序崩溃。原因图像分辨率太高、batch_size过大或生成器网络太深。有时候看起来占用不高但因为Pytorch会为每个卷积层预留workspace显存峰值远高于模型大小。解决优先减小batch_size到1再把输入图片resize到256而不是512。如果还爆把生成器里的resnet_9blocks换成resnet_6blocks或者把判别器改为小PatchGAN。也可以删除代码里所有保存中间feature map的临时变量用del img_tensor释放。再不行就用torch.backends.cudnn.benchmarkFalse减少cudnn显存申请。我最后是通过把输出图缓存到CPU才省下那一口气。5.2 模型输出全黑或全白归一化与反归一化错位现象加载训练好的模型推理结果图是全黑或全白甚至全是噪点。原因训练时图像被归一化到[-1,1]推理代码没有做同样的归一化或者输出后没有做反归一化直接保存。比如输入是[0,255]的整数模型期待[-1,1]的浮点经过卷积后输出值域完全错位。解决严格复制训练脚本里的transform。去雾项目通常这么做transform transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), # [0,255] - [0,1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # [0,1] - [-1,1] ])保存结果时逆操作out (fake_img 1) / 2 * 255 out out.clamp(0, 255).permute(0, 2, 3, 1).cpu().numpy()注意顺序先乘2减1归一化还是减均值除方差不同实现不一样。源码文档里如果写了mean0.5, std0.5就是上面这套。换模型前一定先确认它用的是相同归一化方式。5.3 训练损失一直不降学习率与循环一致性权重现象训练了几百个iterationG_loss和cycle_loss都维持在初始值附近生成图像基本无变化。原因学习率设置太低或者判别器太强导致生成器梯度消失。也有可能是循环一致性权重太大模型陷入“尽量让重建图等于原图”的省力捷径不去学真正的去雾映射。解决先把学习率调到0.0002Adam的betas建议(0.5, 0.999)确认参数配置。如果还不行把lambda_L1从10降到5看看G_loss有没有变化。同时观察判别器loss是否趋近0如果是说明判得太容易可以给判别器输入加高斯噪声或降低判别器学习率。我习惯把D和G的学习率分开设置G保持0.0002D设为0.0001。5.4 去雾后颜色偏色数据分布和预处理不一致现象测试集去雾效果不错但拿到自己拍的图后图像整体泛黄或泛蓝。原因训练集和推理集的色彩分布不一致或者图像在读取时RGB通道顺序错乱。很多源码用PIL读图通道是RGB而opencv读出来是BGR如果两者混用颜色就怪。解决统一用PIL读图或者写一个断言import cv2 img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)另外自己用手机拍的有雾图最好先做白平衡不然训练集都是白天自然光你的图偏暖模型会把偏暖当作雾的一部分输出颜色就有问题。这个不算模型bug是数据分布没对齐。5.5 加载.pth报错state_dict键名与模型定义不匹配现象torch.load能成功但load_state_dict报错提示Missing key(s)或Unexpected key(s)。原因保存的权重来自带“module.”前缀的模型或者在模型定义时用了不同的结构名。最常见原因是训练时用了nn.DataParallel保存的键名变成“module.encoder.0.weight”而推理时模型没有Parallel包装键名是“encoder.0.weight”。解决在加载时先清理前缀state_dict torch.load(checkpoint_path, map_locationcpu) new_state_dict {} for k, v in state_dict.items(): if k.startswith(module.): k k.replace(module., , 1) new_state_dict[k] v netG.load_state_dict(new_state_dict)如果还报缺键就用netG.load_state_dict(new_state_dict, strictFalse)看看到底缺哪些层。一般来说生成器结构一致就没事。我遇到过一次因为Pytorch版本不同state_dict里多出“num_batches_tracked”键strictFalse后不再报错但最好确认不是结构不匹配。玄学一点最好用训练时同样的Pytorch版本加载权重。6. 进阶用法批量评估去雾指标与模型转换技巧6.1 批量计算PSNR和SSIM模型训完了不能只靠眼睛看效果。我写一个脚本对一批无雾参考图和去雾结果图计算PSNR和SSIM。import cv2 import numpy as np from skimage.metrics import structural_similarity as ssim def calc_metrics(pred_path, gt_path): pred cv2.imread(pred_path).astype(np.float64) gt cv2.imread(gt_path).astype(np.float64) psnr cv2.PSNR(pred, gt) ssim_val ssim(gt, pred, channel_axis2, data_range255) return psnr, ssim_val注意skimage版本较新时用channel_axis旧版用multichannelTrue运行前先确认版本。装依赖用pip install scikit-image opencv-python。这个脚本要求去雾结果和参考图尺寸一致否则计算前要先resize。6.2 把Pytorch模型转成ONNX如果你想把去雾功能接到服务端可以转成ONNXimport torch netG.eval() dummy torch.randn(1, 3, 256, 256).to(cuda) torch.onnx.export(netG, dummy, defog.onnx, input_names[input], output_names[output], opset_version11)注意ONNX导出时模型被固定为训练时的输入尺寸如果要求动态尺寸需要设置dynamic_axes否则输入分辨率变了会报错。ONNX输出的图和Pytorch输出通常有微小差异这是浮点运算导致的不影响视觉。从那以后我每次拿到新的去雾源码都会在一开始就把“训练归一化方式”和“加载权重时是否处理module前缀”写在笔记第一行。这两个检查点花不了两分钟却能避开全黑输出和加载崩溃这两个最常见的坑。试错多了你会发现GAN去雾的难点不在网络结构而在数据分布、超参数和那些不起眼的预处理细节。希望这份笔记里的思路能帮你在自己的项目上少走一段弯路。本文还有配套的精品资源点击获取