简介本资源为基于PyTorch实现的对偶生成对抗网络图像去雾项目面向计算机相关专业正在做毕业设计的学生以及需要项目实战练习的学习者也可作为课程设计或期末大作业参考。项目包含完整Python源码、训练好的模型权重与文档说明代码经导师指导并认可评审分99分确保可运行零基础也能上手。压缩包共25个文件约21.23MB其中10个py文件涵盖生成器、判别器、训练与预测等核心模块2个pkl为已训练模型权重另有png、jpg示例图片及md说明文档便于快速理解网络结构与实验流程。资源围绕对偶生成对抗网络展开涉及数据加载、参数解析、日志记录与结果可视化等环节读者可据此掌握图像去雾的完整实现思路并在此基础上进行二次开发或对比实验。目前已有144人学习适合作为深度学习入门与毕设实战的参考案例。1. 图像去雾为什么总在天空区域翻车从对偶生成对抗网络说起做过去雾的同行大概都有过这种体验模型在合成数据集上跑出来的指标漂漂亮亮一换到真实雾天照片天空区域就开始出现色块、光晕甚至整片糊成灰白。这不是调参能救的问题而是多数去雾网络把「去雾」当成了一个单向的映射任务——输入雾图输出清晰图学一个端到端的回归。问题在于雾的成因本身是物理的大气散射模型里清晰图像经过透射率和大气光两个变量耦合之后才变成雾图。单向回归丢掉了这个约束网络只能靠数据分布去「猜」遇到训练集里没见过的雾浓度或天空亮度猜错就是必然。对偶生成对抗网络DualGAN的思路正好切中这个痛点。它不学单向映射而是同时学两个方向的变换一个生成器负责去雾另一个生成器负责加雾两个判别器分别在清晰域和雾域上判断真假。去雾生成器输出的结果会被加雾生成器重新「打回」雾图再和原始雾图比对循环一致性。这样一来去雾不再是自由发挥而是被加雾过程反向约束——你去的雾得能被重新加回来且加回来的和原来那张对得上。这个约束在天空这种高频低对比度区域尤其管用因为天空的透射率接近无穷、大气光占比高单向模型容易在这里过拟合出灰白色块而对偶结构会强迫生成器保留可逆的物理关系。这套方案适合谁如果你手头有一批配对或非配对的雾图-清晰图数据想在自己的场景比如监控、无人机航拍、户外巡检上做去雾又不想从头设计物理模型那基于 PyTorch 的对偶 GAN 去雾是一个能落地、可复现、且对数据配对要求相对宽松的选择。下面从网络结构、训练流程、参数设置到避坑一步步拆开讲。2. 对偶生成对抗网络去雾的骨架两个生成器、两个判别器怎么搭2.1 为什么去雾和加雾要共用一套循环结构对偶 GAN 的核心不是「一个生成器多强」而是「两个生成器互相制约」。去雾生成器 G_A 把雾图 x 映射到清晰图 y_hat加雾生成器 G_B 把清晰图 y 映射回雾图 x_hat。判别器 D_A 判断输入是真实清晰图还是 G_A 生成的D_B 判断输入是真实雾图还是 G_B 生成的。训练时G_A 的损失由三部分组成对抗损失骗过 D_A、循环一致性损失G_B(G_A(x)) 要接近 x、以及可选的感知损失。G_B 同理。这个结构的关键在于循环一致性不是可选项而是去雾质量的主要来源。没有它G_A 可以输出任意「看起来清晰」的图哪怕内容已经偏离原图。有了它G_A 必须保留足够的信息让 G_B 能重建出原始雾图。这相当于给去雾过程加了一个信息瓶颈逼着网络只去掉雾而不是改内容。2.2 生成器选型U-Net 还是 ResNet 堆叠在 PyTorch 里实现时生成器常见两种选择。U-Net 结构适合去雾因为雾的分布是全局的但细节恢复需要跳跃连接把浅层特征传到深层。ResNet 堆叠比如 9 个残差块参数更少训练更稳但在浓雾区域容易丢失纹理。我一般用「浅层 U-Net 残差块」的混合结构编码器 3 层下采样中间接 6 个残差块解码器 3 层上采样跳跃连接保留边缘信息。判别器用 PatchGAN输出 70x70 的感受野而不是整图一个标量。这样能更好地捕捉局部雾的分布训练也更稳定。下面是一个可直接复现的生成器定义import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, channels): super().__init__() self.block nn.Sequential( nn.Conv2d(channels, channels, 3, 1, 1), nn.InstanceNorm2d(channels), nn.ReLU(inplaceTrue), nn.Conv2d(channels, channels, 3, 1, 1), nn.InstanceNorm2d(channels) ) def forward(self, x): return x self.block(x) # 残差连接避免深层梯度消失 class DehazeGenerator(nn.Module): def __init__(self, base64, res_blocks6): super().__init__() # 编码器3层下采样通道数 64-128-256 self.enc nn.Sequential( nn.Conv2d(3, base, 7, 1, 3), nn.InstanceNorm2d(base), nn.ReLU(inplaceTrue), nn.Conv2d(base, base*2, 3, 2, 1), nn.InstanceNorm2d(base*2), nn.ReLU(inplaceTrue), nn.Conv2d(base*2, base*4, 3, 2, 1), nn.InstanceNorm2d(base*4), nn.ReLU(inplaceTrue) ) # 中间残差块 self.res nn.Sequential(*[ResidualBlock(base*4) for _ in range(res_blocks)]) # 解码器上采样 跳跃连接 self.dec nn.Sequential( nn.ConvTranspose2d(base*4, base*2, 3, 2, 1, output_padding1), nn.InstanceNorm2d(base*2), nn.ReLU(inplaceTrue), nn.ConvTranspose2d(base*2, base, 3, 2, 1, output_padding1), nn.InstanceNorm2d(base), nn.ReLU(inplaceTrue), nn.Conv2d(base, 3, 7, 1, 3), nn.Tanh() # 输出归一化到 [-1,1] ) def forward(self, x): e self.enc(x) r self.res(e) return self.dec(r)这段代码里InstanceNorm2d比BatchNorm2d更适合去雾任务因为去雾的雾浓度和图像内容强相关批统计量会引入不稳定。Tanh输出配合训练时把图像归一化到 [-1,1]能加速收敛。残差块数量res_blocks6是我在 256x256 输入下的常用值再大容易过拟合再小浓雾去不干净。2.3 判别器与损失函数PatchGAN 和循环一致性的权重怎么定判别器用 3 层卷积的 PatchGAN每层接 LeakyReLU最后输出一个特征图而不是标量。损失函数用最小二乘 GANLSGAN比原始 GAN 更稳梯度不会消失。循环一致性损失用 L1权重一般设 10。对抗损失权重设 1。如果加感知损失用预训练 VGG 提特征权重设 0.1 左右但会拖慢训练速度建议先不加等基础模型跑通再试。class PatchDiscriminator(nn.Module): def __init__(self, in_ch3, base64): super().__init__() self.net nn.Sequential( nn.Conv2d(in_ch, base, 4, 2, 1), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base, base*2, 4, 2, 1), nn.InstanceNorm2d(base*2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base*2, base*4, 4, 2, 1), nn.InstanceNorm2d(base*4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base*4, 1, 4, 1, 1) # 输出 Patch 得分 ) def forward(self, x): return self.net(x)训练时判别器每步更新一次生成器也每步更新一次。如果判别器 loss 掉到 0.1 以下说明它太强了生成器学不动这时候要把判别器的学习率调低或者给判别器输入加噪声。这是 GAN 训练里的经典平衡问题后面避坑章节会细说。3. 在 PyTorch 里跑通训练数据准备、训练循环与参数设置3.1 雾图数据集怎么组织配对与非配对的取舍对偶 GAN 理论上支持非配对数据但去雾任务里如果有配对数据同一场景的雾图和清晰图训练会快很多循环一致性也更容易收敛。常见做法是用合成雾图拿清晰图按大气散射模型加雾透射率 t 在 [0.5, 1.0] 之间随机大气光 A 取全局最亮 0.1% 像素的均值。这样能生成大量配对数据。真实雾图没有清晰参考只能做非配对训练但收敛慢且需要更仔细地调循环损失权重。数据加载用torch.utils.data.Dataset把雾图和清晰图分别放在两个文件夹文件名一一对应。预处理只做随机裁剪到 256x256 和随机水平翻转不做颜色抖动因为颜色偏移会破坏大气散射的物理关系。from torch.utils.data import Dataset, DataLoader from PIL import Image import os import torchvision.transforms as T class DehazeDataset(Dataset): def __init__(self, hazy_dir, clear_dir, size256): self.hazy_dir hazy_dir self.clear_dir clear_dir self.names sorted(os.listdir(hazy_dir)) self.transform T.Compose([ T.RandomCrop(size), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean[0.5]*3, std[0.5]*3) # 归一化到 [-1,1] ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] hazy Image.open(os.path.join(self.hazy_dir, name)).convert(RGB) clear Image.open(os.path.join(self.clear_dir, name)).convert(RGB) # 对雾图和清晰图做相同的随机变换 seed torch.randint(0, 2**32, (1,)).item() torch.manual_seed(seed) hazy self.transform(hazy) torch.manual_seed(seed) clear self.transform(clear) return hazy, clear这里用torch.manual_seed保证雾图和清晰图做相同的随机裁剪和翻转否则配对就错位了。Normalize到 [-1,1] 是为了配合生成器的Tanh输出。批量大小设 4 或 8取决于显存256x256 输入下 8GB 显存跑 batch4 没问题。3.2 训练循环两个生成器、两个判别器怎么交替更新训练循环里每一步先更新判别器 D_A 和 D_B再更新生成器 G_A 和 G_B。判别器的损失是真实样本和生成样本的 LSGAN 损失之和。生成器的损失包括对抗损失、循环一致性损失和身份损失可选。身份损失是让 G_A 对清晰图输出清晰图G_B 对雾图输出雾图权重设 5 左右能加速早期收敛。import torch.optim as optim # 初始化 G_A DehazeGenerator().cuda() # 去雾 G_B DehazeGenerator().cuda() # 加雾 D_A PatchDiscriminator().cuda() D_B PatchDiscriminator().cuda() opt_G optim.Adam(list(G_A.parameters()) list(G_B.parameters()), lr2e-4, betas(0.5, 0.999)) opt_D optim.Adam(list(D_A.parameters()) list(D_B.parameters()), lr2e-4, betas(0.5, 0.999)) criterion_GAN nn.MSELoss() # LSGAN criterion_cycle nn.L1Loss() criterion_identity nn.L1Loss() lambda_cycle 10.0 lambda_identity 5.0 for epoch in range(200): for hazy, clear in dataloader: hazy, clear hazy.cuda(), clear.cuda() # 更新判别器 opt_D.zero_grad() fake_clear G_A(hazy) pred_real_A D_A(clear) pred_fake_A D_A(fake_clear.detach()) loss_D_A 0.5 * (criterion_GAN(pred_real_A, torch.ones_like(pred_real_A)) criterion_GAN(pred_fake_A, torch.zeros_like(pred_fake_A))) fake_hazy G_B(clear) pred_real_B D_B(hazy) pred_fake_B D_B(fake_hazy.detach()) loss_D_B 0.5 * (criterion_GAN(pred_real_B, torch.ones_like(pred_real_B)) criterion_GAN(pred_fake_B, torch.zeros_like(pred_fake_B))) loss_D loss_D_A loss_D_B loss_D.backward() opt_D.step() # 更新生成器 opt_G.zero_grad() fake_clear G_A(hazy) pred_fake_A D_A(fake_clear) loss_GAN_A criterion_GAN(pred_fake_A, torch.ones_like(pred_fake_A)) rec_hazy G_B(fake_clear) loss_cycle_A criterion_cycle(rec_hazy, hazy) loss_id_A criterion_identity(G_A(clear), clear) fake_hazy G_B(clear) pred_fake_B D_B(fake_hazy) loss_GAN_B criterion_GAN(pred_fake_B, torch.ones_like(pred_fake_B)) rec_clear G_A(fake_hazy) loss_cycle_B criterion_cycle(rec_clear, clear) loss_id_B criterion_identity(G_B(hazy), hazy) loss_G (loss_GAN_A loss_GAN_B lambda_cycle * (loss_cycle_A loss_cycle_B) lambda_identity * (loss_id_A loss_id_B)) loss_G.backward() opt_G.step()这段循环里判别器更新时对生成样本用了.detach()防止梯度传回生成器。生成器更新时对抗损失、循环损失、身份损失一起回传。lambda_cycle10是 CycleGAN 论文里的经典值去雾任务里可以调到 15 让循环约束更强但太高会导致去雾不彻底。lambda_identity5在配对数据下有用非配对数据下可以设 0。3.3 学习率与迭代次数什么时候该停学习率用 2e-4前 100 个 epoch 保持不变后 100 个 epoch 线性衰减到 0。这是 CycleGAN 的标配。迭代次数取决于数据集大小一般 200 个 epoch 足够。判断什么时候停不要只看 loss要看验证集上的 PSNR 和 SSIM。如果 PSNR 连续 20 个 epoch 不升就可以停了。另外生成器输出的图如果开始出现网格状伪影说明判别器太强要提前停或者调低判别器学习率。4. 去雾效果上不去的排查清单从颜色偏移到模式崩溃4.1 输出图整体偏灰或偏蓝现象去雾结果整体蒙了一层灰或者天空区域偏蓝。原因通常是循环一致性损失权重太低生成器没有保留足够的颜色信息或者训练数据里雾图的颜色分布和清晰图差异太大。解决把lambda_cycle从 10 提到 15同时在数据预处理里检查雾图是否做了和白平衡相关的增强如果有去掉。另外判别器的感受野如果太小会忽略全局颜色分布可以把 PatchGAN 最后一层改成 5x5 卷积。4.2 浓雾区域去不干净残留白色斑块现象近处浓雾区域去雾后仍有白色雾团。原因是对偶 GAN 的循环一致性在浓雾区域容易失效因为加雾生成器 G_B 很难从清晰图重建出浓雾导致 G_A 在浓雾区域缺乏约束。解决在损失里加一个暗通道先验损失对去雾结果的暗通道做约束权重设 0.5。或者用多尺度训练把浓雾区域裁剪出来单独训练一个子模型。4.3 训练中期判别器 loss 骤降生成器输出崩坏现象训练到 50 个 epoch 左右判别器 loss 掉到 0.01生成器输出变成纯色或重复纹理。原因是判别器太强生成器梯度消失。解决把判别器学习率降到 1e-4或者给判别器输入加高斯噪声标准差 0.1。另一个办法是使用谱归一化Spectral Normalization在判别器每层卷积后加nn.utils.spectral_norm能显著稳定训练。4.4 验证集 PSNR 高但视觉质量差现象PSNR 到 28dB 以上但人眼看还是有雾感或细节模糊。原因是 PSNR 对亮度敏感对结构不敏感。解决加感知损失用预训练 VGG16 的 relu3_3 层提特征权重 0.1。同时看 SSIM如果 SSIM 低于 0.85说明结构恢复不够要增加生成器的残差块数量或跳跃连接。4.5 显存溢出或训练速度过慢现象batch4 就 OOM或者一个 epoch 跑半小时。原因是生成器通道数太大或输入分辨率太高。解决把base从 64 降到 32残差块从 6 降到 4输入从 256 降到 192。如果还慢把InstanceNorm2d换成nn.BatchNorm2d并冻结统计量能省一点显存但会牺牲稳定性。另外用torch.cuda.amp混合精度训练显存能省 30%速度提升 20%。5. 从能跑到好用推理部署与效果验证的实操技巧训练完的模型要落地推理阶段有几个细节决定最终效果。第一推理时不要用训练时的随机裁剪而是整图输入但如果显存不够用滑动窗口加重叠拼接重叠区域取平均。第二生成器的Tanh输出要反归一化回 [0,255]再转成 uint8 保存。第三如果部署到边缘设备把生成器导出成 ONNX 或 TorchScript用torch.jit.trace时注意输入尺寸固定动态尺寸会失败。验证去雾效果除了 PSNR 和 SSIM我习惯加一个「雾浓度估计」指标用暗通道先验算去雾前后图像的暗通道均值均值越低说明雾越少。这个指标和视觉感受更一致。下面是一个推理和验证的脚本片段import torch from PIL import Image import numpy as np def infer(model, img_path, size256): model.eval() img Image.open(img_path).convert(RGB) w, h img.size # 填充到 size 的倍数 pad_w (size - w % size) % size pad_h (size - h % size) % size img_pad Image.new(RGB, (wpad_w, hpad_h), (0,0,0)) img_pad.paste(img, (0,0)) tensor T.ToTensor()(img_pad).unsqueeze(0).cuda() tensor (tensor - 0.5) / 0.5 with torch.no_grad(): out model(tensor) out (out * 0.5 0.5).clamp(0,1) out out.squeeze(0).cpu().permute(1,2,0).numpy() out (out * 255).astype(np.uint8) return Image.fromarray(out).crop((0,0,w,h)) def dark_channel(img, size15): # 暗通道先验用于估计雾浓度 img_np np.array(img).astype(np.float32) / 255.0 min_ch np.min(img_np, axis2) kernel np.ones((size, size)) / (size*size) from scipy.ndimage import minimum_filter dark minimum_filter(min_ch, sizesize) return dark.mean()推理时model.eval()和torch.no_grad()是必须的否则InstanceNorm2d会用当前批的统计量单张图推理会出错。暗通道均值在去雾后应该明显下降如果没降说明模型没学到去雾要回去检查训练数据或损失权重。最后说一个我踩过的坑有次训练集里清晰图是 JPEG 压缩过的雾图是 PNG结果模型学到了 JPEG 的块效应去雾后出现方块伪影。后来统一用 PNG 无损格式问题消失。数据格式的一致性比想象中重要。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?