简介:面向图像去雾初学者的实战资源包,包含基于暗原色先验与传统优化思路、AOD卷积网络两套去雾实现,适合学习经典算法与深度学习方法对比提升的Python/MATLAB开发者。资源包共76个文件,主要涵盖28张JPG原图/结果图、13张PNG、8个MATLAB脚本与6个Python脚本,同时提供pyc编译文件、PyQt界面设计源码、说明文档与PDF实验报告,压缩包整体约19.88MB。现有505人学习下载。内容按AOD神经网络、暗原色先验、可执行程序等模块组织,既包含改进的暗原色先验算法(MATLAB GUI可操作),也包含基于PyQt的AOD去雾界面;由于AOD神经网络可执行程序过大,单独提供清华网盘下载指引,并配有README与PDF报告。报告中记录了暗原色先验的速度/效果改进思路,以及通过数据集处理增强AOD网络鲁棒性的实验细节,便于复现训练、观察去雾效果和对比算法误差。 从拿到这个项目标题到把整套流程跑通,前前后后花了我大概两个周末。AOD-Net这个网络在图像去雾这个方向里算一个很有意思的存在:它不像传统方法那样把透射率和大气光分开估,而是通过卷积网络直接回归一个融合参数K(x),再用一条重建公式把清晰图像算出来。我用一张自带雾气的测试图跑了一下,单张推理在普通笔记本上也就是几十毫秒,效果虽然谈不上完美,但作为快速落地的去雾方案,性价比相当高。这篇博文就把我从原理、数据、训练到推理踩坑的完整过程都写出来,适合刚接触低层视觉、或者想在项目里快速集成一个去雾模块的朋友参考。
1. 项目原理拆解:AOD-Net为什么能直接回归K(x)
1.1 从大气散射模型说起:去雾问题的数学底座
图像去雾的经典物理模型叫大气散射模型,公式长这样:
[ I(x) = J(x)\cdot t(x) + A\cdot(1 - t(x)) ]
I(x)是带雾图像,J(x)是我们要恢复的清晰图像,t(x)是透射率,A是全局大气光。t(x)又跟景深强相关,通常写成 t(x) = e^{-βd(x)},β是散射系数,d是场景深度。雾越浓、场景越远,t越小,图像退化越严重。
传统去雾算法的思路是“分步估计”:先想办法估计出t(x),再估计A,最后用公式反解J(x)。这个思路看起来合理,但实际操作中有个很致命的问题——误差会累积。如果t估计偏了一点,后面的自然场景颜色甚至整张图的亮度都会跟着偏。暗通道先验(DCP)是这类方法的代表,它在室外晴天场景下效果惊艳,但一旦遇到大面积天空、白色物体或者偏色场景,暗通道假设失效,结果往往出现明显的色斑和光晕。
分步估计的另一个隐患是,每一步都可能在优化目标上“自欺欺人”。比如估计透射率的时候,网络只保证了t的数值接近真值,但并没有保证最终恢复出来的J在感知上正确。这就是为什么后来的深度学习去雾方法开始尝试端到端训练,直接从带雾图映射到清晰图。
1.2 把t和A合成K(x):AOD-Net的核心破局点
AOD-Net这篇论文(All-in-One Dehazing Network,CVPR 2017)最大的贡献,是它对大气散射模型做了一次等价变形。它不单独估计t和A,而是把两者合并成一个变量K(x),定义如下:
[ K(x) = \frac{\frac{1}{t(x)} \cdot (A - 1)}{I(x) - A} + 1 ]
这个式子看起来复杂,但它的意义很直观:K(x)里面同时包含了透射率和大气光的信息。然后原模型就可以被改写成:
[ J(x) = K(x) \cdot I(x) - K(x) + b ]
b可以设为一个常数(通常取1)。也就是说,网络不再需要知道t和A分别是什么,只需要回归出K(x),再用这一条公式重建清晰图像。
这个变形的厉害之处在于,它把“两步误差累积”变成了“一步端到端优化”。K(x)本质上是一个像素级的空间变化参数,卷积网络天生就擅长学习这种局部映射关系。而且K(x)的物理意义是模糊的——网络不需要精确理解雾的物理参数,只需要学到“什么样的输入对应什么样的K能恢复出清晰图”,这让整个任务变得更像一个通用的图像回归问题,实现起来简单,训练也稳定。
和同期其他深度学习方法相比,AOD-Net的优势也很明显。我用一个表格直接给它们做个对比:
| 方法 | 估计方式 | 是否分步 | 参数量级 | 主要优势 |
|---|---|---|---|---|
| 暗通道先验 | 手工先验估计t和A | 是 | 无参数 | 无需训练,室外场景好 |
| DehazeNet | CNN估计t,再配A | 是 | 较大 | 透射率估计更准 |
| MSCNN | 多尺度CNN估计t | 是 | 较大 | 多尺度特征丰富 |
| AOD-Net | CNN直接回归K | 否 | 很小 | 端到端,轻量,易嵌入其他任务 |
我在实际跑的时候体会特别深:AOD-Net在参数量只有几KB级别的网络里,就能达到接近当时大规模去雾网络的效果。后面很多做自动驾驶感知的研究,会把AOD-Net当作一个即插即用的去雾前端,直接接在检测或分割网络前面,因为它在GPU上几乎不占资源。
2. 数据准备与训练流程:让网络学会去雾
2.1 训练数据怎么来:合成雾图的生成思路
深度学习去雾训练有个绕不开的老大难问题:真实的“带雾图-清晰图”配对数据几乎没有。雾天场景下拍一张清晰图再拍一张雾图,这种数据的采集成本太高,环境和光照也很难完全对齐。
所以主流做法是用合成雾图。思路很简单:基于大气散射模型,拿一张清晰图和对应的深度图,人为设定β和A,就能生成“仿真雾图”。NYU-Depth-v2数据集是最常用的选择,它包含上千张室内RGB图像和对应的深度图,虽然场景是室内,但在训练去雾网络时依然能提供足够的深度多样性。
我用一段简单的Python代码来演示合成雾图的生成:
import numpy as np from PIL import Image def make_hazy(image, depth, beta=1.0, A=0.8): # 输入图像和深度图范围都归一化到 [0, 1] img = np.array(image, dtype=np.float32) / 255.0 dep = np.array(depth, dtype=np.float32) / 255.0 # 透射率随深度指数衰减 t = np.exp(-beta * dep) t = t[..., np.newaxis] # 大气散射模型合成雾图 hazy = img * t + (1 - t) * A hazy = np.clip(hazy, 0.0, 1.0) return (hazy * 255).astype(np.uint8)这里的参数选择直接决定训练集的雾浓度分布。beta太小,雾太淡,网络学不到东西;beta太大,图几乎全白,也没法训练。我一般把beta范围设在0.4到1.6之间随机采样,大气光A在0.6到1.0之间随机取值,这样生成的雾图既有薄雾也有浓雾,网络在不同浓度下都能保持一定的鲁棒性。
数据增强方面,我建议至少做随机裁剪(256x256左右)、水平翻转、随机颜色抖动。AOD-Net虽然轻量,但对训练数据的多样性还是敏感的。我刚开始只用原始分辨率训练,结果模型在验证集上PSNR都很高,一到真实雾图就发灰,后来加了随机裁剪和颜色扰动,泛化能力明显改善。
2.2 损失函数与训练参数:让网络收敛的关键
AOD-Net原文用的损失函数很简单,就是均方误差(MSE),也就是L2损失。但如果你只优化L2,训练出来的图像往往偏模糊,这是L2的天生缺陷——它倾向于把输出拉向所有候选值的平均,导致高频细节丢失。
我的做法是L2加上结构相似性损失(SSIM)的组合:
[ Loss = |J_{pred} - J_{gt}|2^2 + \lambda \cdot (1 - SSIM(J{pred}, J_{gt})) ]
λ通常取0.1到0.3。SSIM损失会强制网络保留局部结构和边缘信息,弥补L2只度量像素差而忽略感知结构的不足。这个改动在肉眼对比下非常明显,尤其是物体边缘的清晰度。
训练参数我整理了一个可以直接参考的配置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入尺寸 | 256x256 | 太大影响速度,太小细节不够 |
| Batch Size | 8 | 显存不够可以降到4 |
| 优化器 | Adam | 默认β1=0.9,β2=0.999 |
| 初始学习率 | 1e-4 | 不需要预热,直接从头训 |
| 学习率策略 | 阶梯下降,每30轮乘0.5 | 后期收敛更稳定 |
| 训练轮数 | 100 | 数据集小时50轮也能看效果 |
| 数据增强 | 随机裁剪、翻转、颜色抖动 | 必选 |
训练的核心代码也比较直接:
import torch import torch.nn as nn criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) for epoch in range(100): for hazy, clear in train_loader: hazy, clear = hazy.to(device), clear.to(device) K = model(hazy) # 网络输出K dehazed = K * hazy - K + 1 # 重建清晰图 loss = criterion(dehazed, clear) # 计算损失 optimizer.zero_grad() loss.backward() optimizer.step()这里有个容易踩的坑:输入图像和标签的数值范围一定要对齐。我习惯将图像归一化到[0, 1]区间再进网络,这样重建公式里的常数b=1才自然合理。如果输入是[0, 255]的整数张量,重建出来的图值域会完全不对,K的输出也会被拉到一个很诡异的范围。
3. 网络结构解析与PyTorch实现
3.1 卷积层的拼接设计:5层卷积如何做到轻量又有效
AOD-Net的内部结构比我现在想象的还要简洁。它由5个卷积层组成,每个卷积核尺寸依次是1x1、3x3、5x5、7x7、3x3,每层输出通道都是3。也就是从第一层开始,它就在不断利用不同感受野尺寸的特征信息,同时把前序层的结果通过concat拼起来,形成一个类似稠密连接的短接结构。
这种结构的好处有两个。第一,多尺度感受野让网络能同时感知局部细节和全局光照变化。1x1卷积捕捉像素级颜色变换,3x3和5x5捕捉局部纹理,7x7能覆盖更大的区域,对于估计光照变化比较平稳的区域非常重要。第二,特征拼接把每一层的中间结果都保留下来,梯度传播路径更短,网络即使层数不多,也能在训练时保持稳定的梯度流。
很多人第一眼看到AOD-Net会觉得“就这么点层,真能去雾吗”。实际上,去雾任务和分类任务不同,它不要求网络提取高级语义特征,而是更依赖局部像素之间的映射关系。AOD-Net的目标只是拟合一个空间变化的K,这个映射相对简单,所以不需要很深的网络。论文里甚至提到,AOD-Net的参数量只有大约千级别,在当年动辄几百万参数的深度学习模型里算是非常异类了。
值得一提的是,AOD-Net原文还引入了一个全局特征模块——通过全局平均池化把整张图压缩成一个向量,再上采样回原尺寸,最后和前面的特征拼接。这样网络在估计K的时候,不只看局部patch,还能感知整张图的全局光照水平。我在复现时发现这个模块对浓雾场景的效果提升比较明显,所以完整版建议保留。
3.2 核心代码实现:AOD-Net完整的PyTorch前向过程
我复现的AOD-Net PyTorch代码如下,结构非常干净:
import torch import torch.nn as nn class AODNet(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 3, kernel_size=1, stride=1, padding=0) self.conv2 = nn.Conv2d(6, 3, kernel_size=3, stride=1, padding=1) self.conv3 = nn.Conv2d(9, 3, kernel_size=5, stride=1, padding=2) self.conv4 = nn.Conv2d(12, 3, kernel_size=7, stride=1, padding=3) self.conv5 = nn.Conv2d(15, 3, kernel_size=3, stride=1, padding=1) self.relu = nn.ReLU(inplace=True) def forward(self, x): # 第一层:1x1卷积,调整通道 x1 = self.relu(self.conv1(x)) cat1 = torch.cat([x, x1], dim=1) # 6 通道 x2 = self.relu(self.conv2(cat1)) cat2 = torch.cat([x, x1, x2], dim=1) # 9 通道 x3 = self.relu(self.conv3(cat2)) cat3 = torch.cat([x, x1, x2, x3], dim=1) # 12 通道 x4 = self.relu(self.conv4(cat3)) cat4 = torch.cat([x, x1, x2, x3, x4], dim=1) # 15 通道 K = self.conv5(cat4) # 输出为 3 通道的 K return Kforward里每层都把自己和原始输入以及之前所有层的输出拼接,输入通道数因此逐级增加:6、9、12、15。最后一层卷积输出3通道的K,表示每个像素位置上RGB三个通道的融合系数。
拿到K之后,重建清晰图的操作在训练和推理中保持一致:
# 重建公式:J = K * I - K + b, b = 1 dehazed = K * x - K + 1这段代码有几个细节需要注意。一是卷积层的padding要配对好,1x1用padding=0,3x3用padding=1,5x5用padding=2,7x7用padding=3,这样才能保证所有特征图尺寸不变。二是ReLU放在每层卷积之后,最后一层conv5没有接ReLU,因为K需要保留负值,如果对K做ReLU,会把一部分中间状态硬截断,导致去雾能力下降。
4. 推理实战与效果评估
4.1 从单张雾图到清晰图:推理流程怎么写
训练好模型之后,推理流程和训练大同小异。关键是每个步骤都要严格保持一致的前处理和后处理。我一般这样写推理代码:
import torch from PIL import Image import torchvision.transforms as T # 加载模型权重 model = AODNet() model.load_state_dict(torch.load('aodnet.pth', map_location='cpu')) model.eval() # 读图并转为张量 img = Image.open('hazy.jpg').convert('RGB') transform = T.Compose([T.ToTensor()]) # 转张量并归一化到 [0, 1] x = transform(img).unsqueeze(0) # [1, 3, H, W] with torch.no_grad(): K = model(x) dehazed = K * x - K + 1 dehazed = torch.clamp(dehazed, 0, 1) # 防止越界 out = T.ToPILImage()(dehazed.squeeze(0)) out.save('dehazed.jpg')这里有一个很容易被忽略的点:必须要加torch.clamp。因为K是通过网络学习出来的,不能保证重建后的图像一定落在[0,1]范围内。有时候K的值偏大,重建结果会出现过白或过黑的情况,clip一下可以避免输出图片的像素越界。如果你不想牺牲极端区域的细节,也可以不用clamp直接存储,但大多数情况下加clamp更安全。
真实雾图的推理效果,我实测过一个浓雾街道场景。用AOD-Net处理后,整体对比度提高了一截,远处建筑的轮廓能看清了,但颜色会轻微偏灰。这个偏灰问题可以通过后处理来缓解:对输出图像做个自适应直方图均衡化,或者把亮度通道单独拉伸一下,肉眼观感会好很多。
4.2 效果评估与调参心得:不只要看PSNR
去雾效果评估最常用的两个指标是PSNR(峰值信噪比)和SSIM(结构相似性),计算方式直接调库就行:
from skimage.metrics import peak_signal_noise_ratio as psnr from skimage.metrics import structural_similarity as ssim psnr_val = psnr(clear_img, dehazed_img, data_range=1.0) ssim_val = ssim(clear_img, dehazed_img, channel_axis=-1, data_range=1.0)但我想说的是,指标只能做参考,真正的效果判断要看人眼。有时候PSNR很高,但图里有一块明显的伪影,这个指标是体现不出来的。我见过很多论文里PSNR刷得好看,放到真实雾图上却崩得一塌糊涂,原因就是合成雾和真实雾的分布差异太大。
训练过程中,我会每个epoch保存一次验证集上的去雾结果图,每隔10个epoch手动看一遍。这样能直观感受模型在“去雾强度”和“颜色保真”之间的平衡。如果发现去雾强度过猛,图发暗发灰,可以适当调低b,或者对K加一个轻微的正则项,让K的变化更平滑。
5. 常见问题与避坑指南
5.1 训练期最容易踩的三个坑
第一个坑是Loss不下降或反向传播发散。大多数情况是学习率设置过大。AOD-Net因为参数量小,对学习率很敏感,我用过1e-3直接训,结果Loss在第5个epoch就炸了,最后把学习率降到1e-4才恢复正常。如果你用的是Adam,建议初始学习率就按1e-4来,别贪快。
第二个坑是颜色偏灰甚至全部变成灰白色。这种问题往往出自数据预处理。合成雾图生成时,如果A值设得太接近1.0,或者输入图像的归一化方式不对,模型学到的K就会普遍偏大,推理时整张图被强行提亮。排查方法很简单,把训练数据的直方图打出来,确认分布正常后再送入网络。
第三个坑是训练集不小,但验证集PSNR低。这通常是数据增强不够导致的过拟合。AOD-Net虽然轻量,但在小数据集上还是容易记住训练分布,我加了随机裁剪和颜色抖动之后,验证集PSNR直接从24dB升到了27dB左右,效果立竿见影。
我把这些问题整理成了一个速查表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| Loss爆炸 | 学习率过大 | 降低lr到1e-4或1e-5 |
| 输出整体发白 | A值或b值设置不当 | 调整合成参数、降低b |
| 验证集PSNR低 | 过拟合 | 增加数据增强、加SSIM损失 |
| 边缘模糊 | 只用了L2损失 | 加入SSIM或感知损失 |
| 天空区域偏色 | 全图共享K过平滑 | 改进全局特征或后处理 |
5.2 真实场景部署的独有难题
如果你打算把去雾模型放到真实场景里用,那就得提前做好心理准备:合成雾和真实雾之间有一道明显的distribution gap。我在真实雾天拍的照片上测试,模型去雾效果能打60分——立体感出来了,但颜色和细节总觉得“差点意思”。这个问题不能说无解,但需要额外手段来缓解。
一个可行思路是用真实雾图做无监督微调,或者用CycleGAN这类方法生成更接近真实雾分布的训练样本。另一个思路是让AOD-Net和其他任务联合训练,比如在目标检测网络前面接AOD-Net,让检测loss反过来约束去雾模型的参数,这样模型会主动学习对下游任务更有用的去雾特征,比单独追求去雾指标要实用得多。
部署时还容易遇到推理速度问题。AOD-Net本身够轻量,在GPU上的单张推理速度几乎可以忽略不计,但在CPU上处理2K分辨率图还是需要优化。我建议先把图像缩放或者分块处理,用OpenCV的DNN模块或者ONNX Runtime做加速,推完再拼回原图。这样在嵌入式设备上也能跑得动。
我个人跑完整个流程的体会是:AOD-Net不是一个性能天花板级别的模型,但它非常像一个理想的“技术练手项目”——结构简单到一眼就能看完,数学原理却不浅,加上训练和部署链路完整,你能在很短的时间里把图像去雾从原理到落地过一遍。如果你接下来想深入低层视觉,完全可以把AOD-Net当跳板,再去尝试FFA-Net、DEA-Net这类更复杂的网络。至少我做完这个项目之后,再回头看大气散射模型和非均匀雾的paper,理解难度直接降了一档。
最后再分享一个小技巧:训练AOD-Net时,不要只保留最后一个epoch的权重。我在实验中经常发现,验证集PSNR最高的点往往出现在某个中间epoch,而不是最后一个。所以每轮结束都存一个checkpoint,然后用验证集挑最优,别省那点磁盘空间。
本文还有配套的精品资源,点击获取