简介:本资源面向计算机视觉方向的研究者与深度学习实践者,提供一套基于Vision Transformer的图像去雾算法完整实现方案,可用于复现实验、课程设计或二次开发。压缩包共340个文件,约156.35MB,以204个Python源码为核心,辅以39张png与4张jpg结果图、16份yaml配置、12份csv实验记录、9份ipynb笔记本及8份md说明文档,另含gif演示、txt日志与sh脚本等,覆盖训练、推理与评估全流程。内容预览中可见cifar10、cifar100等数据集在ResNet、AlexNet与ViT-Ti等模型上的损失景观与损坏实验记录,便于对比不同骨干网络的去雾表现。目前已有97人学习下载,适合希望深入理解Transformer在底层视觉任务中应用的读者,可据此掌握数据组织、模型搭建与实验分析思路。
1. 从一张灰蒙蒙的照片说起:Vision Transformer 图像去雾到底在做什么
你手里有一张在雾天拍的街景,远处楼宇的轮廓糊成一团,近处的车牌也像蒙了层灰纱。传统做法是上暗通道先验,调几个参数,能出效果,但一到浓雾、天空区域或者大面积白墙就容易翻车,出现光晕和色偏。这几年 Vision Transformer(ViT)被搬进图像去雾任务后,情况有了变化:它靠自注意力机制建模长距离依赖,能把「远处那栋楼该是什么颜色」和「近处路面反射的光」关联起来,去雾结果在细节和色彩上更稳。
这个标题讲的,就是用 Vision Transformer 做图像去雾的完整落地:Python 源码、配套数据集、项目说明打包在一起。它解决的是「我有一堆雾图,想训一个能实际用的去雾模型」这件事。适合两类人:一是刚入门深度学习、想找一个端到端项目练手的同学;二是做过 CNN 去雾、想对比 Transformer 方案到底强在哪的工程师。下面我按「先立住原理、再跑通代码、最后避坑」的顺序,把这条路走一遍。
2. 为什么去雾要从 CNN 换到 Vision Transformer:原理与选型
2.1 去雾任务的本质是一个像素级回归问题
图像去雾在数学上通常写成大气散射模型:I(x) = J(x)·t(x) + A·(1 - t(x))。I 是观测到的雾图,J 是待恢复的清晰图,t 是透射率,A 是大气光。传统方法分两步走,先估 A 和 t,再反解 J。深度学习的方法干脆跳过显式建模,直接让网络学 I 到 J 的映射,把去雾当成端到端的图像到图像回归。
CNN 方案(比如 DehazeNet、AOD-Net、FFA-Net)靠卷积核的局部感受野逐层堆叠来扩大视野。问题是卷积的感受野增长是线性的,要覆盖整张图的全局信息,得堆很深,参数量和显存都吃不消。雾的分布恰恰是全局相关的:一片浓雾会同时影响画面里相距很远的区域,局部卷积很难把这种关联学到位。
2.2 自注意力为什么适合去雾
Vision Transformer 把图像切成固定大小的 patch(常见 16×16 或 8×8),每个 patch 展平后加位置编码,送进多层 Transformer Encoder。核心是自注意力:每个 patch 都能直接和图上任意其他 patch 计算相关性权重。这意味着处理左上角一块浓雾时,网络可以同时参考右下角相对清晰的区域来推断全局大气光,这在 CNN 里要靠很深的网络才能勉强做到。
代价也很直接:自注意力的计算量随 patch 数量平方增长。一张 256×256 的图切成 16×16 的 patch 是 256 个 token,还能接受;切成 8×8 就是 1024 个 token,显存立刻吃紧。所以实际项目里常见两种折中:一是用 Swin Transformer 这类窗口注意力,把全局注意力限制在滑动窗口内,兼顾效率和感受野;二是用混合结构,浅层用卷积提局部细节,深层用 Transformer 建模全局。标题里的方案通常属于后者或 Swin 变体,这也是当前去雾领域比较主流的做法。
2.3 选型对比:CNN、纯 ViT、混合结构怎么选
| 方案 | 全局建模 | 显存占用 | 训练难度 | 适合场景 |
|---|---|---|---|---|
| 纯 CNN(FFA-Net 等) | 弱 | 低 | 低 | 数据少、算力有限 |
| 纯 ViT | 强 | 高 | 高 | 大数据集、充足算力 |
| CNN+ViT 混合 | 较强 | 中 | 中 | 大多数实际项目 |
| Swin Transformer | 强 | 中高 | 中 | 追求 SOTA 效果 |
我一般会建议:如果你手上雾图数据不到几千张,优先混合结构或 Swin,纯 ViT 很容易过拟合;如果数据上万张且有 A100 级别的卡,可以试纯 ViT 冲效果。这个判断比盲目追新架构重要得多。
3. 把项目跑起来:环境、数据、训练三步走
3.1 环境搭建与依赖安装
项目是 Python 写的,主流框架是 PyTorch。先确认显卡驱动和 CUDA 版本匹配,这一步不对后面全是玄学报错。我习惯用 conda 建独立环境,避免和系统里的包打架。
# 创建独立环境,python 版本按项目说明选,一般 3.8 或 3.9 稳妥 conda create -n dehaze_vit python=3.9 -y conda activate dehaze_vit # 安装 PyTorch,CUDA 版本按自己显卡驱动来,这里以 cu118 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装项目其余依赖,通常包含 numpy、opencv、tqdm、tensorboard 等 pip install numpy opencv-python tqdm tensorboard pillow scikit-image逻辑说明:先隔离环境再装框架,是为了让 CUDA 版本和 PyTorch 编译版本严格对应。参数上,python=3.9是兼容性最好的选择,3.10 以上偶尔会遇到某些依赖没预编译轮子。装完务必跑一句python -c "import torch; print(torch.cuda.is_available())",返回 True 才算环境通了。返回 False 就是驱动或 CUDA 版本不匹配,别急着往下走。
3.2 数据集组织与预处理
去雾数据集常见两类:合成数据集(如 RESIDE 的 ITS、OTS,用清晰图加雾生成配对数据)和真实数据集(如 NH-HAZE、Dense-Haze,真实雾天拍摄)。项目一般用配对数据训练,即每张雾图对应一张清晰图。目录结构通常长这样:
dataset/ ├── train/ │ ├── hazy/ # 雾图 │ └── clear/ # 对应清晰图,文件名一一对应 ├── test/ │ ├── hazy/ │ └── clear/预处理的关键是配对不能错位。我见过太多人因为文件名排序不一致,导致雾图和清晰图对不上,训练 loss 死活降不下去,排查半天。下面这段代码做的是读取配对、归一化、转 tensor:
import os import cv2 import torch from torch.utils.data import Dataset class DehazeDataset(Dataset): def __init__(self, root, mode='train', size=256): self.hazy_dir = os.path.join(root, mode, 'hazy') self.clear_dir = os.path.join(root, mode, 'clear') self.size = size # 按文件名排序,保证雾图和清晰图严格对应 self.names = sorted(os.listdir(self.hazy_dir)) def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] hazy = cv2.imread(os.path.join(self.hazy_dir, name)) clear = cv2.imread(os.path.join(self.clear_dir, name)) # BGR 转 RGB,缩放到统一尺寸 hazy = cv2.cvtColor(hazy, cv2.COLOR_BGR2RGB) clear = cv2.cvtColor(clear, cv2.COLOR_BGR2RGB) hazy = cv2.resize(hazy, (self.size, self.size)) clear = cv2.resize(clear, (self.size, self.size)) # 归一化到 [0,1],转成 CHW 格式 hazy = torch.from_numpy(hazy).permute(2, 0, 1).float() / 255.0 clear = torch.from_numpy(clear).permute(2, 0, 1).float() / 255.0 return hazy, clear逻辑说明:sorted保证两个目录读取顺序一致,这是配对数据最容易翻车的地方。size=256是去雾任务的常用输入尺寸,太大显存扛不住,太小细节丢失。归一化到 [0,1] 而不是 [-1,1],是因为多数去雾损失函数(L1、MSE)在 [0,1] 区间数值更稳定。如果你的数据分辨率很高,建议先离线裁剪成 patch 再训练,而不是在__getitem__里随机裁,后者会拖慢数据加载。
3.3 训练脚本与关键参数
训练循环本身不复杂,难的是损失函数和超参。去雾常用 L1 损失打底,再加感知损失(perceptual loss)或 SSIM 损失提升视觉质量。下面是一个精简的训练骨架:
import torch from torch.utils.data import DataLoader from model import DehazeViT # 项目里的模型定义 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = DehazeViT().to(device) dataset = DehazeDataset('dataset', mode='train', size=256) loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4) # 去雾常用组合:L1 主损失 + 感知损失 optimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100) for epoch in range(100): model.train() for hazy, clear in loader: hazy, clear = hazy.to(device), clear.to(device) pred = model(hazy) loss = torch.nn.functional.l1_loss(pred, clear) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(f'epoch {epoch}, loss {loss.item():.4f}')逻辑说明:batch_size=8是 256 分辨率下 12G 显存的稳妥值,显存够可以往上加,但要注意学习率同步调整。AdamW比 Adam 多了正确的权重衰减,Transformer 类模型基本都用它。lr=2e-4是 ViT 微调的常见起点,太大容易震荡,太小收敛慢。CosineAnnealingLR让学习率余弦下降,后期更稳。损失函数先用 L1 跑通,确认能降下来再叠加感知损失,一上来就堆复杂损失,出问题你都不知道是哪一项的锅。
4. 训练不收敛、结果发灰:去雾项目避坑清单
4.1 现象:loss 降到某个值就不动了
原因:多半是学习率过大导致在局部极小值附近震荡,或者数据归一化不一致(雾图归一化了,清晰图没归一化)。解决:先把学习率降到 1e-4 试,同时检查数据加载里两张图的处理流程是否完全一致。我习惯在训练前打印一个 batch 的像素范围,确认都在 [0,1]。
4.2 现象:去雾结果整体发灰、对比度低
原因:L1 损失倾向于输出所有可能值的中间值,导致结果偏保守、发灰。解决:叠加 SSIM 损失或对抗损失,或者在推理后做一次简单的对比度拉伸。另一个常见原因是训练数据里清晰图本身偏暗,模型学到了这个偏差。
4.3 现象:显存溢出(CUDA out of memory)
原因:patch 切得太小导致 token 数量爆炸,或者 batch_size 设太大。解决:把 patch 从 8×8 改成 16×16,token 数直接降到四分之一;或者用梯度累积模拟大 batch。混合精度训练(amp)也能省不少显存,但要注意某些损失函数在 fp16 下会溢出。
4.4 现象:测试集效果好,真实雾图一塌糊涂
原因:合成数据集和真实雾的分布差异大,模型过拟合了合成雾的统计特征。解决:训练时加入真实雾数据做微调,或者用域适应方法。这是去雾领域最经典的坑,别指望一个合成数据训出来的模型能直接上真实场景。
4.5 现象:推理速度慢到没法用
原因:纯 ViT 的注意力计算量大,或者推理时没关掉梯度。解决:推理时加torch.no_grad(),用 ONNX 或 TensorRT 导出加速,或者换 Swin 这类效率更高的结构。如果只是做演示,可以先把输入分辨率降到 128 再放大。
5. 让去雾结果更耐看:验证方法与一个实用技巧
训练完不能只看 loss 曲线,得用客观指标加主观观察双重验证。客观指标常用 PSNR 和 SSIM,但这两个指标和人类视觉感受并不总是一致,PSNR 高的图有时看着反而更糊。我的习惯是:先跑 PSNR/SSIM 筛掉明显崩掉的模型,再挑几张典型图(含天空、白墙、远景)肉眼对比。
import torch from skimage.metrics import peak_signal_noise_ratio as psnr from skimage.metrics import structural_similarity as ssim model.eval() with torch.no_grad(): for hazy, clear in test_loader: hazy, clear = hazy.to(device), clear.to(device) pred = model(hazy).clamp(0, 1) # 推理结果裁剪到合法范围 # 转成 numpy 算指标,注意转成 HWC 格式 pred_np = pred[0].permute(1, 2, 0).cpu().numpy() clear_np = clear[0].permute(1, 2, 0).cpu().numpy() print('PSNR:', psnr(clear_np, pred_np, data_range=1.0)) print('SSIM:', ssim(clear_np, pred_np, channel_axis=2, data_range=1.0))逻辑说明:clamp(0,1)很重要,模型输出可能超出合法范围,不裁剪会让 PSNR 计算失真。channel_axis=2是 skimage 新版本的参数名,老版本用multichannel=True,版本不对会直接报错,这也是个常见坑。
一个实用技巧:推理时对同一张图做水平翻转,两次结果取平均。这个 TTA(测试时增强)操作几乎零成本,通常能把 PSNR 提 0.1 到 0.3 dB,对边缘细节尤其有效。别小看这点提升,在去雾这种像素级任务里,0.2 dB 往往就是肉眼可感的差别。
最后说个我自己的教训:我早期做去雾时,一门心思调网络结构,换了三四个注意力变体,效果提升都不明显。后来发现问题出在数据上——训练集的清晰图有几张本身就有轻微雾感,模型学了个「去一半雾」的中间态。把这几张脏数据清掉重训,效果立刻上了一个台阶。做这类项目,数据质量永远比结构花哨更重要,先把数据洗干净再谈模型。希望帮到你。
本文还有配套的精品资源,点击获取