ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AOD-Net图像去雾实战:从大气散射模型到端到端PyTorch实现

AOD-Net图像去雾实战:从大气散射模型到端到端PyTorch实现 简介面向图像去雾初学者的实战资源包包含基于暗原色先验与传统优化思路、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 NetworkCVPR 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是无参数无需训练室外场景好DehazeNetCNN估计t再配A是较大透射率估计更准MSCNN多尺度CNN估计t是较大多尺度特征丰富AOD-NetCNN直接回归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, beta1.0, A0.8): # 输入图像和深度图范围都归一化到 [0, 1] img np.array(image, dtypenp.float32) / 255.0 dep np.array(depth, dtypenp.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 Size8显存不够可以降到4优化器Adam默认β10.9β20.999初始学习率1e-4不需要预热直接从头训学习率策略阶梯下降每30轮乘0.5后期收敛更稳定训练轮数100数据集小时50轮也能看效果数据增强随机裁剪、翻转、颜色抖动必选训练的核心代码也比较直接import torch import torch.nn as nn criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-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]区间再进网络这样重建公式里的常数b1才自然合理。如果输入是[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_size1, stride1, padding0) self.conv2 nn.Conv2d(6, 3, kernel_size3, stride1, padding1) self.conv3 nn.Conv2d(9, 3, kernel_size5, stride1, padding2) self.conv4 nn.Conv2d(12, 3, kernel_size7, stride1, padding3) self.conv5 nn.Conv2d(15, 3, kernel_size3, stride1, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): # 第一层1x1卷积调整通道 x1 self.relu(self.conv1(x)) cat1 torch.cat([x, x1], dim1) # 6 通道 x2 self.relu(self.conv2(cat1)) cat2 torch.cat([x, x1, x2], dim1) # 9 通道 x3 self.relu(self.conv3(cat2)) cat3 torch.cat([x, x1, x2, x3], dim1) # 12 通道 x4 self.relu(self.conv4(cat3)) cat4 torch.cat([x, x1, x2, x3, x4], dim1) # 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用padding03x3用padding15x5用padding27x7用padding3这样才能保证所有特征图尺寸不变。二是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_locationcpu)) 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_range1.0) ssim_val ssim(clear_img, dehazed_img, channel_axis-1, data_range1.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然后用验证集挑最优别省那点磁盘空间。本文还有配套的精品资源点击获取
返回列表