简介:这份资源是面向深度学习研究者与图像处理方向学习者的PyTorch版DehazeNet图像去雾实现,提供从网络结构定义、训练流程到推理演示的完整代码链路,并附带已训练好的预训练权重,可直接加载使用,也便于在现有架构上做扩展实验。压缩包共21个文件,约114KB,以9个Python脚本和4个MATLAB文件为主,前者覆盖训练、验证与预处理等环节,后者用于引导滤波等辅助处理;另有2个pth权重文件、若干备份文件及说明文档,整体结构紧凑、便于快速上手。目前已有86人学习关注。借助其中的室内与室外两套预训练模型、训练与验证脚本以及演示程序,读者能够直接复现去雾效果,并对照代码理解数据准备、模型训练与推理的完整流程,适合具备一定深度学习基础、希望深入掌握去雾算法实现细节的研究人员参考。
1. DehazeNet 去雾到底值不值得做:从一张灰蒙蒙的照片说起
拍过户外照片的人都有体会:明明肉眼看着通透的天气,出片却像蒙了一层灰纱,远处建筑轮廓发白,树叶颜色发闷。这不是相机坏了,是大气散射在作怪——悬浮颗粒把光线散射掉,成像传感器收到的对比度和色彩都被稀释了。传统做法靠暗通道先验、大气光估计那一套,参数调起来玄学得很,换一批图就得重来。DehazeNet 的思路不一样:它用卷积网络直接学「有雾图到无雾图」的映射关系,把去雾当成一个端到端的回归问题。这篇要讲的就是基于 PyTorch 把 DehazeNet 从零搭起来、训练、加载预训练模型推理的完整路径,顺带把环境搭建、参数设置、翻车点都摊开说。适合已经会一点 PyTorch、想拿去雾练手或落地到实际图像增强流程里的同学,新手照着步骤也能跑通,熟手可以重点看后面的边界和坑。
2. DehazeNet 的网络结构与 PyTorch 实现细节
2.1 为什么 DehazeNet 不是简单堆卷积
DehazeNet 的核心设计动机来自大气散射模型:I(x) = J(x)t(x) + A(1 - t(x)),其中 I 是有雾图,J 是无雾图,t 是透射率,A 是大气光。传统方法要分别估计 t 和 A,DehazeNet 则让网络直接预测透射率图,再用大气光做还原。它的结构有几个关键点:第一层用多尺度卷积核(比如 5x5、7x7 并行)提取不同感受野的特征,这比单一尺寸卷积更能捕捉雾的局部浓度变化;中间用 Maxout 激活,而不是 ReLU,因为 Maxout 能拟合更复杂的非线性,对透射率这种连续值回归更友好;最后用 BReLU(双边 ReLU)把输出限制在 [0,1] 区间,因为透射率物理上就在这个范围。这些设计不是拍脑袋,是作者针对去雾任务特性做的取舍。用 PyTorch 实现时,Maxout 没有现成层,得自己写;BReLU 也要自定义。下面先把网络骨架搭出来。
import torch import torch.nn as nn import torch.nn.functional as F class Maxout(nn.Module): """Maxout 激活:把通道分组,每组取最大值""" def __init__(self, in_channels, out_channels, groups=2): super().__init__() self.groups = groups self.linear = nn.Conv2d(in_channels, out_channels * groups, 1) def forward(self, x): out = self.linear(x) # 按 groups 维度分组取 max out = out.view(out.size(0), self.groups, -1, out.size(2), out.size(3)) return out.max(dim=1)[0] class BReLU(nn.Module): """双边 ReLU:下界 0,上界 1""" def forward(self, x): return torch.clamp(x, 0.0, 1.0) class DehazeNet(nn.Module): def __init__(self): super().__init__() # 多尺度特征提取:并行不同卷积核 self.conv1_1 = nn.Conv2d(3, 16, 5, padding=2) self.conv1_2 = nn.Conv2d(3, 16, 7, padding=3) self.conv1_3 = nn.Conv2d(3, 16, 3, padding=1) # Maxout 融合 self.maxout1 = Maxout(48, 16, groups=2) # 中间层 self.conv2 = nn.Conv2d(16, 16, 3, padding=1) self.maxout2 = Maxout(16, 16, groups=2) self.conv3 = nn.Conv2d(16, 16, 3, padding=1) self.maxout3 = Maxout(16, 16, groups=2) # 输出透射率图,单通道 self.conv4 = nn.Conv2d(16, 1, 3, padding=1) self.brelu = BReLU() def forward(self, x): x1 = self.conv1_1(x) x2 = self.conv1_2(x) x3 = self.conv1_3(x) x = torch.cat([x1, x2, x3], dim=1) x = self.maxout1(x) x = self.maxout2(self.conv2(x)) x = self.maxout3(self.conv3(x)) t = self.brelu(self.conv4(x)) return t这段代码里,Maxout 的实现用 1x1 卷积把通道数扩大 groups 倍,再 reshape 后取 max,等价于原论文的 Maxout 操作。参数 groups=2 是常用值,调大能增加非线性但显存和计算量上升。BReLU 用 clamp 实现,简单直接。多尺度卷积并行后 concat,再进 Maxout 融合,这是 DehazeNet 区别于普通 CNN 的关键。注意 conv1 的三个分支输出通道都是 16,concat 后是 48,Maxout 再压回 16。如果显存吃紧,可以把通道数减半,但透射率估计精度会掉一点,这个后面避坑章会细说。
2.2 用 PyTorch 搭训练流程:数据、损失、优化器怎么配
网络有了,接下来是训练。去雾数据集常见的有 RESIDE、NYU-Depth 的合成雾图、或者自己用大气散射模型合成。合成时透射率 t 和大气光 A 是已知的,所以可以直接监督透射率图,损失用 MSE 就行。但实际中更稳的做法是监督最终去雾结果 J,因为透射率的小误差经过还原公式会放大。我一般会两个都加,透射率损失权重 0.5,去雾结果损失权重 1.0。优化器用 Adam,学习率 1e-3,每 20 个 epoch 衰减 0.5。batch size 根据显存来,8G 卡用 8 或 16 都行。下面是一个训练循环的骨架。
from torch.utils.data import DataLoader, Dataset import torch.optim as optim class HazeDataset(Dataset): """假设数据目录下 hazy/ 和 clear/ 成对存放""" def __init__(self, hazy_dir, clear_dir, transform=None): self.hazy_files = sorted(os.listdir(hazy_dir)) self.hazy_dir = hazy_dir self.clear_dir = clear_dir self.transform = transform def __len__(self): return len(self.hazy_files) def __getitem__(self, idx): name = self.hazy_files[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') if self.transform: hazy = self.transform(hazy) clear = self.transform(clear) return hazy, clear # 数据加载 transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), ]) dataset = HazeDataset('data/hazy', 'data/clear', transform) loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4) # 模型、优化器、损失 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DehazeNet().to(device) optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) mse = nn.MSELoss() # 训练循环 for epoch in range(100): model.train() total_loss = 0 for hazy, clear in loader: hazy, clear = hazy.to(device), clear.to(device) t_pred = model(hazy) # 用真实透射率监督(合成数据里 t 已知,这里简化用 1 - 暗通道近似) # 实际训练建议直接用 clear 图监督去雾结果 J_pred = (hazy - 0.8 * (1 - t_pred)) / (t_pred + 1e-6) # A 取 0.8 近似 J_pred = torch.clamp(J_pred, 0, 1) loss = mse(J_pred, clear) + 0.5 * mse(t_pred, torch.ones_like(t_pred) * 0.5) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f'Epoch {epoch}, Loss: {total_loss / len(loader):.4f}')这里有几个参数要说明:大气光 A 在合成数据里通常是已知的,但推理时不知道,常见做法是用暗通道最亮像素估计,或者直接取 0.8~0.95 的经验值。学习率 1e-3 是 Adam 的常用起点,如果 loss 震荡就降到 5e-4。num_workers 在 Windows 下有时会报错,设成 0 就行。损失函数里透射率监督那项,如果数据集没有真实 t,可以去掉,只监督 J。训练 100 epoch 在 RESIDE 子集上大概能收敛,但具体要看数据量。显存不够就减 batch size 或把图像 resize 到 128。
3. 预训练模型怎么用:加载、推理与效果验证
3.1 加载预训练权重并跑通单张图推理
拿到预训练模型后,第一件事是确认权重文件和网络结构对得上。常见坑是 state_dict 的 key 不匹配,比如保存时用了 DataParallel 导致 key 前面多出 module. 前缀。加载时用 strict=False 能跳过不匹配的层,但最好还是手动去掉前缀。下面是一个完整的推理脚本。
import torch from PIL import Image import torchvision.transforms as transforms def load_pretrained(model_path, device): model = DehazeNet().to(device) state_dict = torch.load(model_path, map_location=device) # 处理 DataParallel 保存的权重 new_state_dict = {} for k, v in state_dict.items(): if k.startswith('module.'): new_state_dict[k[7:]] = v else: new_state_dict[k] = v model.load_state_dict(new_state_dict, strict=False) model.eval() return model def dehaze_image(model, img_path, device, A=0.85): img = Image.open(img_path).convert('RGB') transform = transforms.Compose([ transforms.ToTensor(), ]) input_tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): t = model(input_tensor) # 还原公式 hazy = input_tensor J = (hazy - A * (1 - t)) / (t + 1e-6) J = torch.clamp(J, 0, 1) # 转回 PIL out = transforms.ToPILImage()(J.squeeze(0).cpu()) return out device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = load_pretrained('dehazenet_pretrained.pth', device) result = dehaze_image(model, 'test_hazy.jpg', device) result.save('test_clear.jpg')这段代码里,A 取 0.85 是经验值,实际可以根据图像最亮区域自适应估计。t + 1e-6 是防止除零。推理时不需要梯度,用 torch.no_grad() 省显存。如果结果偏暗或偏亮,调 A 就行,A 越大去雾越强但容易过曝。注意输入图像尺寸最好和训练时一致,不一致的话透射率图会有尺度偏差,常见做法是 resize 到 256x256 再还原回去。
3.2 用 PSNR 和 SSIM 验证去雾效果
光看肉眼看不出量化差异,得用指标。PSNR 和 SSIM 是去雾领域最常用的两个。PSNR 衡量像素误差,SSIM 衡量结构相似性。计算时要注意图像范围是 [0,1] 还是 [0,255],别搞混。下面是一个计算脚本。
import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def evaluate(clear_img, dehazed_img): # 转 numpy,范围 [0,1] clear = np.array(clear_img).astype(np.float32) / 255.0 dehazed = np.array(dehazed_img).astype(np.float32) / 255.0 psnr = peak_signal_noise_ratio(clear, dehazed, data_range=1.0) ssim = structural_similarity(clear, dehazed, multichannel=True, data_range=1.0) return psnr, ssim # 假设 clear_img 和 dehazed_img 都是 PIL Image psnr, ssim = evaluate(clear_img, dehazed_img) print(f'PSNR: {psnr:.2f}, SSIM: {ssim:.4f}')参数说明:data_range=1.0 对应 [0,1] 范围,如果图像是 [0,255] 就改成 255。multichannel=True 处理 RGB 三通道。一般去雾任务 PSNR 能到 20 以上、SSIM 到 0.85 以上就算不错,但具体看数据集难度。合成雾图指标会高些,真实雾图因为没有 ground truth,只能靠主观评价或无参考指标。
4. 避坑与排查:DehazeNet 训练推理中的 5 个血泪教训
4.1 现象:训练 loss 不降反升,输出全黑或全白
原因:BReLU 把输出限制在 [0,1],但如果初始化权重太大,透射率一开始就饱和到 0 或 1,梯度传不回去。另外学习率设太高也会导致震荡。
解决:权重初始化用 kaiming_normal_,偏置设 0。学习率从 1e-4 开始试,别一上来就 1e-3。如果已经饱和,加个梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)。
4.2 现象:推理结果颜色失真,偏蓝或偏黄
原因:大气光 A 估计不准。A 取固定值 0.85 对某些图合适,对另一些就不行。偏蓝通常是 A 估低了,偏黄是 A 估高了。
解决:用暗通道先验估计 A:取暗通道最亮 0.1% 像素,对应原图最亮区域均值作为 A。代码里加个自适应估计,别死用固定值。
4.3 现象:GPU 显存溢出,batch size 降到 1 还报错
原因:多尺度卷积并行那层,concat 后通道数 48,中间特征图如果分辨率是 512x512,显存占用是 256x256 的 4 倍。
解决:训练时 resize 到 256x256 或更小,推理时再恢复原尺寸。或者把第一层多尺度分支减到两个,通道数从 16 降到 8。显存 8G 以下建议 batch size 不超过 4。
4.4 现象:加载预训练模型报 key 不匹配,missing keys 一大堆
原因:保存权重时用了 nn.DataParallel,key 前面多了 module. 前缀;或者网络结构改过,层名对不上。
解决:加载时手动去前缀,用 strict=False 先跑通,再打印 missing_keys 和 unexpected_keys 看差在哪。如果是结构改了,要么改回去,要么只加载匹配的层。
4.5 现象:去雾后图像出现网格状伪影或块效应
原因:卷积 padding 方式不对,或者用了 stride 导致棋盘效应。DehazeNet 里如果某层 stride 设成 2,上采样时容易出网格。
解决:所有卷积层 padding 设成 kernel_size//2,保持分辨率不变。别用 stride 下采样,DehazeNet 原设计就是全分辨率。如果非要下采样,用双线性插值上采样,别用转置卷积。
5. 进阶技巧:把 DehazeNet 转成 ONNX 并做推理加速
训练完的 PyTorch 模型部署时往往要转 ONNX,方便在 C++ 或 TensorRT 里跑。转的时候注意动态轴设置,batch 和宽高都设成动态,这样一张图和多张图都能用。下面是一个转换脚本。
import torch.onnx model = DehazeNet().to('cpu') model.load_state_dict(torch.load('dehazenet_pretrained.pth', map_location='cpu')) model.eval() dummy_input = torch.randn(1, 3, 256, 256) torch.onnx.export( model, dummy_input, 'dehazenet.onnx', input_names=['input'], output_names=['transmission'], dynamic_axes={ 'input': {0: 'batch', 2: 'height', 3: 'width'}, 'transmission': {0: 'batch', 2: 'height', 3: 'width'} }, opset_version=11 )opset_version 用 11 比较稳,再高有些推理引擎不支持。dynamic_axes 里把 batch 和空间维度都设成动态,这样 512x512 的图也能直接推理,不用改模型。转完后用 onnxruntime 验证一下输出和 PyTorch 是否一致,误差在 1e-4 以内就算成功。如果要做 TensorRT 加速,注意 Maxout 里的 view 和 max 操作可能被融合,实际测一下速度提升,一般能快 1.5 到 2 倍。我自己的习惯是:每次改完网络结构先跑一遍 ONNX 导出,确认没有不支持的算子,再继续训练。这个后悔药提前吃,比部署时才发现问题强。希望帮到你。
本文还有配套的精品资源,点击获取