简介:基于Vision Transformer的图像去雾算法研究与实现源码与文档包,专为计算机视觉方向的学生、科研人员及算法工程师设计,围绕Uformer等Transformer结构在图像去雾任务中的应用展开。资源提供完整的Python工程,包含NH-HAZE数据集预处理模块(可将训练大图切分为256×256小图)、训练主程序My_train.py、超参数配置(nepoch=270、batch_size=32等)及配套说明文档,能够复现从数据准备、模型训练到结果分析的全流程。包体共338个文件,其中204个py源码、16个yaml配置、12个csv训练指标、9个ipynb分析笔记、9个txt说明及png/gif可视化图等,压缩包约156.36MB,目录规划清晰,便于按模块检索与二次修改。目前已有291人学习下载,适合希望快速完成去雾实验、理解Vision Transformer在前向与反向传播中细节的进阶开发者。
1. 基于Vision Transformer的图像去雾算法:用ViT换掉U-Net的编码器,值不值
雾天拍出来的图,对比度低、细节糊成一片。过去几年,大家默认用CNN做去雾:AOD-Net、FFA-Net这些模型在RESIDE数据集上刷榜,落地也稳。但CNN的卷积核视野有限,雾的浓度分布又往往是全局的——远处一片白茫茫,近处还算清楚,这种全局退化用局部卷积去拟合,总有点吃力。Vision Transformer(ViT)把图像切成patch后用全局注意力建模,天然适合处理这种全局退化问题。所以,基于Vision Transformer的图像去雾算法,本质上就是把原来U-Net或残差网络里的卷积主干换成Transformer结构,用自注意力去捕获雾的全局分布,再回归出清晰图像。
这类项目现在很常见,打包成“源码+文档说明”的Python工程,适合正在做毕业设计、课程设计,或者准备把去雾当切入点上手ViT的人。它解决的不是“能不能去雾”的问题,而是“怎么用Transformer把去雾效果做得比CNN更好”的问题。你需要的是一台有NVIDIA显卡的电脑、PyTorch环境,以及一份能跑通的代码。下面我按自己复现这类项目的经验,把原理、训练、评估和踩坑一次性讲清楚。
2. 从CNN到ViT:去雾任务的原理与实际选型理由
2.1 去雾的核心是求解大气散射模型,不是“看图猜清晰图”
图像去雾的物理基础是大气散射模型:I(x) = J(x)t(x) + A(1 − t(x))。I是雾图,J是清晰图,t是透射率,A是全局大气光。模型要做的事,就是根据I反推J。传统方法用暗通道先验估计t和A,再带入公式求解;深度学习方法则是让网络直接回归J,把物理模型的求解过程隐式学进网络参数里。
当网络结构从CNN换成ViT时,变化的核心在于感受野。CNN要看到远处像素,必须靠堆叠卷积层或扩大膨胀率;Transformer的注意力机制一步到位,任何位置的像素之间都能直接建立关联。这对去雾的意义很大——雾带来的退化是跨区域的,近处清晰区域的信息可以辅助恢复远处模糊区域,这种“全局参考”能力正是CNN所欠缺的。
2.2 为什么偏要用Vision Transformer:三个具体的理由
第一个理由是全局上下文建模。去雾质量差的区域往往纹理少、颜色偏灰白,单靠局部邻域很难判断原来的颜色和纹理。ViT的全局注意力让每个patch在恢复时参考整张图的信息,颜色偏移的纠正会更稳。
第二个理由是透射率估计更平滑。透射率图在空间上是大片缓变区域,CNN输出容易产生块状伪影,而Transformer输出的特征图天然带有全局一致性,透射率估计更平滑。
第三个理由是迁移学习的红利。ViT在ImageNet上预训练的权重可以直接拿来初始化去雾模型的编码器,效果比自己从头训练好得多。这是DehazeFormer等模型能跑出好分数的关键原因之一——它用了卷积patch嵌入和相对位置编码,专门适配了图像恢复任务的局部先验需求。
2.3 去雾ViT和标准ViT的差异:不只是把分类头换成回归头
直接用标准ViT做去雾会翻车。标准ViT的patch是16×16,对于去雾这种像素级任务太粗糙;而且ViT的Patch Embedding和位置编码是为分类设计的,没有考虑图像的局部连续性。常见的做法是改成8×8甚至4×4的小patch,或者在网络前半部分用卷积下采样、后半部分用Transformer做全局建模——这叫做卷积嵌入的混合结构。
做去雾的ViT还面临一个分辨率问题:分类任务常用224×224输入,但图像去雾输入一般是256×256或更大。位置编码必须支持插值,否则换输入尺寸就直接报错。我一般会在训练脚本里把位置编码模式设成'interpolate',并且只在尺寸变化不大时用。
2.4 选型对比:直接告诉你怎么选
| 结构方案 | 参数量 | 去雾效果 | 推理速度 | 适用场景 |
|---|---|---|---|---|
| 纯CNN(AOD-Net) | 小 | 一般 | 快 | 嵌入式设备、实时视频 |
| CNN+Transformer混合 | 中等 | 好 | 中 | 通用去雾、学术研究 |
| 纯ViT | 大 | 不稳定 | 慢 | 不建议日常使用 |
我在实际项目中倾向于第三种变体——也就是DehazeFormer那样的设计:用卷积提取底层特征,中间几个stage用Transformer block做全局建模,最后用卷积上采样恢复。结构和U-Net类似,但编码器的核心块换成了Transformer。
3. 落地实操:数据集准备、损失函数与训练配置
3.1 从零跑通训练的最小代码框架
拿到一个去雾项目,第一件事不是改模型,而是先把训练脚本跑通。下面是简化后的最小训练框架,核心是构建数据集、定义损失、设置优化器。
# train_dehaze.py import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms from models import DehazeFormer # 项目自带模型定义 from datasets import HazyDataset # 项目自带数据集类 # 数据增强:随机裁剪 + 水平翻转 + 颜色抖动 transform = transforms.Compose([ transforms.RandomCrop(256), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.1, contrast=0.1), transforms.ToTensor(), ]) train_set = HazyDataset(root='data/RESIDE/train', transform=transform) train_loader = DataLoader(train_set, batch_size=8, shuffle=True, num_workers=4) model = DehazeFormer(embed_dim=96, depths=[1, 2, 8, 4]) model = model.cuda() # 损失组合:L1 + 感知损失,权重各占一半 loss_l1 = nn.L1Loss() loss_perceptual = PerceptualLoss() # 项目里通常用VGG特征 optimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6) for epoch in range(100): model.train() total_loss = 0.0 for hazy, clean in train_loader: hazy, clean = hazy.cuda(), clean.cuda() pred = model(hazy) loss = loss_l1(pred, clean) + 0.5 * loss_perceptual(pred, clean) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.1) optimizer.step() total_loss += loss.item() scheduler.step() torch.save(model.state_dict(), f'checkpoints/dehaze_epoch{epoch+1}.pth') print(f'Epoch {epoch+1}: loss = {total_loss / len(train_loader):.4f}')这里的核心逻辑是:每次迭代读一对雾图和清晰图,模型输出预测的清晰图,分别用L1损失和感知损失计算误差,反向传播更新权重。clip_grad_norm_用来限制梯度范数,防止ViT训练后期梯度爆炸——这是Transformer系模型的常见毛病。
参数说明:embed_dim=96是第一层Transformer的通道数,越小显存占用越少,但表达力也会下降;depths控制每个stage堆叠的Transformer block数量,[1,2,8,4]是一个性价比不错的配置。学习率2e-4是AdamW在ViT系模型上的常见起点,太大容易发散,太小收敛极慢。
3.2 数据集的坑:RESIDE怎么划分,训练集怎么选
去雾领域的事实标准数据集是RESIDE,但要注意它分好几个子集:ITS用于合成雾训练、OTS用于户外场景、SOTS是测试集。做研究一般用ITS的13个场景训练,在SOTS上评估;如果数据量不够,可以合并OTS。但真实雾图是没有配对清晰参考的,只能靠合成雾图来训练。
训练时一个容易被忽略的细节是:合成雾的数据增强参数要在加载时实时生成,不要提前把雾图存好。因为固定存储的雾图会让网络过拟合特定的透射率分布,训练时动态随机生成雾度参数(随机透射率t、随机大气光A),能让模型的泛化能力明显上升。我在数据类里会这样处理:
# datasets.py 中的关键片段 class HazyDataset(Dataset): def __init__(self, root, transform=None): self.clean_paths = glob.glob(root + '/*.png') self.transform = transform def __getitem__(self, idx): clean = cv2.imread(self.clean_paths[idx]) clean = cv2.cvtColor(clean, cv2.COLOR_BGR2RGB) clean = self._random_crop(clean, size=256) # 动态合成雾图:随机透射率和大气光 t = np.random.uniform(0.6, 1.0, size=clean.shape[:2])[..., None] A = np.random.uniform(0.8, 1.0, size=(1, 1, 3)) hazy = clean * t + A * 255 * (1 - t) hazy = np.clip(hazy, 0, 255).astype(np.uint8) if self.transform: hazy = self.transform(hazy) clean = self.transform(clean) return hazy, clean这个数据类的好处是不需要预先生成雾图文件,每次读取清晰图后随机生成透射率和大气光,相当于无限数据增强。注意t的范围取0.6到1.0,太小的透射率意味着浓雾,过拟合浓雾会让模型在轻雾场景下过度增强。
3.3 损失函数怎么搭配:别迷信单一L2
很多初学者直接上MSELoss,训练完发现图片变模糊了——这是L2损失的通病:像素级的均方误差会把所有高频细节磨平。去雾任务的常用搭配是L1损失打底,加上VGG感知损失提升视觉质量,再配合SSIM损失补充结构信息。
感知损失不是自己去实现VGG,而是用预训练好的VGG19提取中间层特征,计算特征图的L1距离。这样网络学习的目标从“像素值和原图一样”变成“特征表达和原图一样”,细节恢复效果明显更好。SSIM损失则强调局部结构一致性,对边缘保持有好处。三者比例的常见起点是1:0.5:0.1,但建议先用前两个训练,等loss降下去再加SSIM损失。
class CombinedLoss(nn.Module): def __init__(self, weights=(1.0, 0.5, 0.1)): super().__init__() self.w_l1, self.w_perc, self.w_ssim = weights self.l1 = nn.L1Loss() self.perc = PerceptualLoss() self.ssim = SSIMLoss() def forward(self, pred, target): l1_loss = self.l1(pred, target) perc_loss = self.perc(pred, target) ssim_loss = self.ssim(pred, target) return self.w_l1 * l1_loss + self.w_perc * perc_loss + self.w_ssim * ssim_loss3.4 优化器、学习率与Batch Size的设置建议
Vision Transformer对超参数比CNN更敏感,这是Transformer家族的共性。优化器建议用AdamW而不是SGD——ViT训练用SGD基本走不动。学习率方面,一段式训练用CosineAnnealing从2e-4降到1e-6比较省心;如果先用小学习率热身50个iteration再升到正常值,训练会更稳,这里直接在一个Epoch内完成热身即可。
Batch Size按显存来:12GB显存跑256×256输入,batch_size=8是极限,如果OOM就把batch_size调到4,同时把torch.backends.cudnn.benchmark设为True加速卷积。注意Batch Size减小后要适当降低学习率,经验公式是学习率按batch size的平方根缩放——从8降到4,学习率大约除以1.4。这个细节不处理,小batch训练容易不收敛。
3.5 训练资源的合理预期
ViT系去雾模型比CNN慢很多。以我的经验,一张12GB的RTX 3060,输入256×256、batch size为4,训练100个epoch大概需要3到5天,具体要看模型深度和数据集规模。如果时间不够,有两个选择:一是加载ImageNet预训练权重,收敛速度能快一倍以上;二是把训练分辨率降到224×224,最后再用256微调十几个epoch。
4. 推理与量化评估:PSNR/SSIM到底怎么算才算数
4.1 用训练好的权重对单张图去雾
训练跑完后,需要把模型导出并进行推理。下面的代码展示了对一张雾图执行去雾推理并保存结果的完整流程。
# inference.py import torch import cv2 import numpy as np from models import DehazeFormer device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DehazeFormer(embed_dim=96, depths=[1, 2, 8, 4]) model.load_state_dict(torch.load('checkpoints/dehaze_epoch100.pth', map_location=device)) model.eval().to(device) def dehaze_image(model, img_path, output_path): img = cv2.imread(img_path) origin_h, origin_w = img.shape[:2] # 为了保证位置编码不失效,先把输入缩放到训练尺寸的整数倍 scale = 256 / min(origin_h, origin_w) new_h, new_w = int(origin_h * scale), int(origin_w * scale) new_h, new_w = new_h // 8 * 8, new_w // 8 * 8 # 8的倍数,兼容patch划分 img_resized = cv2.resize(img, (new_w, new_h)) img_tensor = torch.from_numpy(img_resized.transpose(2, 0, 1)).float().div(255.0) img_tensor = img_tensor.unsqueeze(0).to(device) with torch.no_grad(): dehazed = model(img_tensor) dehazed = dehazed.squeeze(0).cpu().numpy().transpose(1, 2, 0) dehazed = np.clip(dehazed * 255, 0, 255).astype(np.uint8) dehazed = cv2.resize(dehazed, (origin_w, origin_h)) cv2.imwrite(output_path, dehazed) if __name__ == '__main__': dehaze_image(model, 'test/hazy.jpg', 'test/result.jpg')这段代码做了三件关键事:把输入图缩放到训练尺寸的整数倍以满足patch划分要求、前向推理后裁剪到0到255范围、最后缩回原图大小保存。整个过程不需要GT图,任何一张雾图都能跑。
4.2 PSNR和SSIM的计算方式:不要直接调sklearn
PSNR和SSIM需要在有配对清晰图的测试集上计算。PSNR公式里有个常见的细节:像素值的取值范围MaxI取255还是1,计算结果差异很大。很多项目为了刷分把MaxI取1,但图像像素是0到255的uint8,正确做法是MaxI=255,这叫PSNR不能跨实现直接比较的原因——同一张结果图,不同库算出来能差好几个dB。
SSIM的窗口大小也影响结果。默认的11×11高斯窗口是通用选择,但去雾任务里如果要和论文对标,得看作者用的是什么滤波窗口。通常我在报告里会写明用的scikit-image的structural_similarity,并标注win_size=11。评估脚本代码相对简单,但重算一遍才能真正理解数据。
# evaluate.py from skimage.metrics import peak_signal_noise_ratio, structural_similarity def evaluate(gt_dir, pred_dir): psnr_sum, ssim_sum, count = 0.0, 0.0, 0 for name in os.listdir(pred_dir): gt = cv2.imread(os.path.join(gt_dir, name)).astype(np.float64) pred = cv2.imread(os.path.join(pred_dir, name)).astype(np.float64) psnr = peak_signal_noise_ratio(gt, pred, data_range=255) ssim = structural_similarity(gt, pred, channel_axis=-1, data_range=255) psnr_sum += psnr ssim_sum += ssim count += 1 print(f'Average PSNR: {psnr_sum / count:.2f} dB, SSIM: {ssim_sum / count:.4f}')4.3 量化指标的陷阱:PSNR高不代表视觉好
这是去雾领域最典型的翻车现场:PSNR刷到30以上,但看图发现颜色偏灰、细节发糊。原因在于PSNR对整体像素误差敏感,对局部结构不敏感——把整张图稍微调亮或调暗,PSNR就会显著变化,但视觉质量并没有变好。所以评估时必须两个指标一起看,并配上视觉对比图。
我的习惯是跑完测试后,把三张图拼在一起看:雾图原图、模型输出、GT清晰图。肉眼观察三个关键区域——天空区域有没有过曝、远处建筑物边缘是否清晰、近处植被纹理是否自然。如果PSNR高但天空变成了灰白色,说明大气光估计有问题,模型把天空区域错误增强了。
5. 训练与部署避坑:5个值得写进笔记里的踩坑记录
5.1 训练loss一直不降,在0.78附近震荡
现象:训练了十几个epoch,loss基本不动,就像被卡住了一样。
原因:最常见的是学习率设置过大或过小。ViT对学习率很敏感,AdamW在2e-4时正常,调到5e-4就可能发散,降到1e-5则几乎不动。还有一个,输出层没有加Tanh或Sigmoid限制输出范围,导致模型输出像素值溢出到负数或大于1,L1 Loss在边界附近形成梯度震荡。
解决:把学习率拉回2e-4区间,模型输出层确认是否有范围限制。如果是自己的实现,在最后加一层nn.Tanh()把输出压缩到−1到1之间,配合输入归一化到−1到1范围使用。
5.2 显存OOM,batch size=2都跑不动
现象:一开训练就报CUDA out of memory。
原因:ViT的注意力计算量随patch数量呈平方增长,256×256输入切成8×8 patch,序列长度是1024,注意力矩阵就是1024×1024。显存占用大头是注意力矩阵和中间激活值,不是参数本身。
解决:第一个方案是减小patch大小而不是降低分辨率(保持分割块数不变会降低计算量),把embed_dim从96降到64。第二个是开梯度检查点(gradient checkpointing),牺牲少量训练时间换取显存——PyTorch里用torch.utils.checkpoint.checkpoint包住Transformer block即可。第三个是把batch size降到1,配合梯度累积模拟batch size为4的效果。
5.3 PSNR高但视觉结果偏灰,雾已经去了但颜色不对
现象:量化指标不错,但输出图整体蒙了一层灰色,天空区域尤其明显。
原因:训练数据里合成雾的全局大气光A取值偏向白色或亮灰,而真实雾场景的大气光往往带点蓝色或黄色色偏。模型学到了“把像素往灰色推”的倾向,就去雾的同时把色彩饱和度也削弱了。
解决:数据增强中加入大气光颜色的随机扰动,不要让A固定在纯白色。把大气光取值范围设为[0.7, 1.0]并允许每个通道独立随机取值,让模型见过多种色偏的雾。训练后再看结果,色彩的灰蒙感通常会减轻。
5.4 在真实雾图上效果差,训练集PSNR很高
现象:合成雾数据集上PSNR有30+,一拿到真实拍摄的雾图,去雾效果明显退化,远处物体还是糊的。
原因:合成雾图和真实雾图的物理过程不完全一致。合成雾假设透射率均匀分布,真实雾图的透射率和景深强相关,近处透射率高、远处透射率低,且存在多次散射和大气湍流等复杂因素。这就是领域差距问题。
解决:加入真实雾图进行微调。真实雾图没有GT,可以用无参考指标做筛选后,把网络输出周边一致性作为伪标签来微调。更简单的做法是用预训练模型在少量真实雾图上做风格迁移,但效果有限。合理的预期是:在目标域数据量有限时,先用合成雾数据训练做主模型,到了现场再针对性微调。
5.5 推理速度太慢,视频流实时处理跟不上
现象:单张256×256图片推理要200ms,做不到实时处理。
原因:ViT的全局自注意力计算量太大,多尺度结构叠加深了参数量。
解决:从轻量化入手。第一个方案是把分辨率降下来,用160×160输入,但质量会损失。第二个是换掉全局注意力,用窗口注意力或局部注意力减少计算量,这是Swin Transformer的思路。第三个方案是把训练好的ViT蒸馏到一个小型CNN里——教师模型是ViT,学生模型用轻量U-Net,蒸馏后的CNN推理速度能快5到10倍,效果仍然比直接用CNN好。如果只是验证可行性,项目演示时用GPU加速推理并缓存结果就够了,别要求实时。
6. 进阶技巧:视觉结果自检与服务化部署
量化指标算完不算完,落地时还有一个容易忽略的环节:视觉自检。我的习惯是每一轮训练结束,挑3张有代表性的雾图出来跑推理,一张看天空、一张看中景建筑、一张看近景植被。如果近景植被纹理清晰但天空变成灰白,说明大气光估计引入偏置;如果远景恢复出来了但近景出现振铃,说明网络学的是锐化而不是去雾。这类主观检查花不了几分钟,但能在PSNR没变的情况下看出模型的真实表现。
另一个技巧是无参考条件下快速验证去雾效果:对比去雾前后的暗通道均值。去雾算法的目标是让暗通道值恢复到0.1以下,如果输出图的暗通道均值仍然很高,说明雾没有去干净。这个脚本写起来很简单,用opencv取每个像素的RGB最小值,做最小值滤波,再算均值即可。虽然没有GT图这么严谨,但在真实场景里评估模型有没有正常工作,很实用。
如果项目要交付成服务,可以封装成FastAPI接口,输入图片URL,返回去雾后的图片bytes;也可以转成ONNX格式大幅提升加载速度和推理效率。转换时注意把位置编码设成固定buffer,避免动态图和静态图的转换报错。这个技巧值得记录。
作为一个反复复现过这类项目的人,我的教训是:不要迷信源码里的默认参数,也不要一上来就换大模型。先把原始代码用最小数据集跑通,确认loss下降、推理有输出,再去调模型结构、补损失函数、上数据增强。ViT去雾的潜力在于全局建模能力,局限在于计算量和数据需求,理解这两点,项目推进就不会走偏。希望这篇文章帮你少走几步弯路。
本文还有配套的精品资源,点击获取