☰
基于GAN的图像去雾算法设计与实战:从暗通道先验到对抗训练
2026/10/20 13:27:55 网站建设 项目流程

雾天图像处理这件事,圈内人都清楚:光线经过大气中悬浮颗粒的散射和吸收,到达传感器时已经损失了大量细节信息。我在这套基于生成对抗网络的图像去雾算法上投入了小半年时间,前前后后对比了传统图像处理路线、纯回归网络路线和GAN路线,最终跑了完整的对比实验。这篇博文就是我把整套项目从问题定义、算法设计、训练调参到评估复盘的过程记录下来,适合正在做图像去雾课题的学生、研发工程师,也适合想了解GAN图像增强实际落地效果的算法从业者。

先说结论:传统图像处理方案(比如暗通道先验)在特定场景下确实有效,但面对真实雾图的多样性和复杂光照时明显力不从心。而引入生成对抗网络之后,去雾图像的自然度和细节保留能力都上了一个台阶,代价是训练难度和计算成本显著上升。下面我会把每一步的逻辑讲清楚,包括为什么这样设计、实验里踩过哪些坑、指标为什么和主观感受不一致。

1. 雾天成像干扰的核心:退化模型决定了后续一切设计

1.1 大气散射模型:去雾问题的数学起点

几乎所有去雾算法都建立在同一个物理模型基础上,就是大气散射模型。我用一句话概括它的思想:相机接收到的像素亮度,等于“景物原本反射光被衰减后的部分”加上“大气环境光散射进镜头的那部分”。用数学表达就是:

I(x) = J(x)t(x) + A(1 - t(x))

其中I(x)是观测到的雾图像素,J(x)是我们要恢复的清晰图像,A是全局大气光(近似天空区域的颜色),t(x)是透射率,范围在0到1之间,表示景物光经过雾层到达相机的比例。t(x)越小,说明这一片区域雾越浓,信息衰减越严重。

这个模型的价值在于,它把去雾问题拆成了两件事:估计透射率t(x),估计全局大气光A,然后反解J(x)。我最早觉得这不过是“套公式”,真正动手才发现难点有两个:第一,I(x)本身是J(x)、t(x)、A三者的耦合结果,一张雾图可以对应无穷多组“清晰图+透射率”的组合,这在数学上是个欠定问题;第二,真实场景中的大气光A通常不均匀,雾的浓度也随距离连续变化,模型本身是理想化的近似。确定了这个基础,后续所有算法路线都围绕“如何在欠定条件下施加合理先验”展开。

1.2 数据集怎么造:合成雾数据与真实雾图的搭配策略

做深度学习去雾绕不开数据集问题。真实场景中想拍到“同一视角的雾图和无雾图”几乎不可能,所以主流做法是用公开的室内外场景深度数据,通过大气散射模型人工合成带有浓雾的图像。我用的方案基本是业界通用思路:从公开深度数据集中取清晰图,配合随机扰动的大气光A和透射率t,合成一批样本,再加上一批真实拍摄的带有雾气的图像用于评测。

实际操作时有一个特别重要的点:合成雾的图像分布和真实雾图存在明显的域差异。合成雾用得太多,模型会在真实雾图上偏色,因为合成雾的A值往往很“干净”,真实大气的A偏灰黄;合成雾用得太少,模型又学不到足够强的去雾映射。我在项目里采用的比例大致是合成雾占七成、真实雾占三成,并且把大气光A的采样范围拉开,模拟早晚阳光、阴天、工业污染等不同色温的雾。这里不要迷信公开数据集本身,你多花一点精力在“数据分布是否贴近你的目标场景”上,后期调参能省一半力气。

2. 先走传统路线:暗通道先验为何是绕不开的基线

2.1 暗通道先验的核心假设和估计流程

做去雾项目不先跑一版经典方法,后面做深度学习对比时根本说不清楚自己提升在哪。传统路线里最有代表性的就是暗通道先验。这个先验基于一个观察:在无雾的清晰图像中,绝大多数局部区域总会有某个颜色通道的亮度值非常低,趋近于零。这些低值主要来自阴影、彩色物体表面或深色背景。

既然清晰图像的“暗通道”很暗,而雾会像一层白纱一样抬高所有通道的亮度,那么直接从雾图的暗通道里,就能反推出雾的浓度。具体流程是:在雾图上取局部区域,对每个像素的RGB三通道取最小值,再做一次最小值滤波,得到暗通道图;取暗通道图中最亮的若干像素所对应的原图像素,估算全局大气光A;然后根据大气散射模型推导出透射率t(x)的粗略估计。得到粗透射率后,还需要用导向滤波之类的方法进行边缘保持平滑,避免去雾后的深度突变区域出现白色光晕。

我在复现时遇到一个细节:透射率估计时需要引入一个经验参数w,通常取0.95左右,目的是保留少量薄雾,让去雾后的图像不那么“假”。如果w取1.0,远处景物会发灰发暗,因为透射率被压得太低。这个参数在传统方法里只能靠经验调,到了深度学习方法里其实就对应了网络学习到的去雾强度。

2.2 传统方法的三个天花板:天空偏色、光晕伪影、透射率估计误差

暗通道先验在多数户外场景效果惊艳,但它的假设在特定区域成立得并不好,这也是我坚持先跑基线再上GAN的原因。第一个大问题是天空区域。天空本身亮度很高,不满足“暗通道趋近于零”的假设,结果就是去雾后天空变成灰褐色,出现明显的颜色畸变。为了缓解这个问题,传统方案里要专门检测天空区域并单独处理,非常麻烦。

第二个问题是深度突变处的光晕伪影。暗通道先验假设同一局部区域透射率恒定,但物体边缘前后景深差异大,透射率骤变,最小值滤波会把前景边缘的信息扩散到背景区域,去雾图像边缘出现一圈白色或黑色光晕。导向滤波能改善,但治标不治本。第三个问题是全局大气光A估计对噪声极敏感,一旦误选到白色车辆或白色建筑,整张图的色彩会全面失衡。这三个天花板让我意识到,基于人工先验的方法在“局部信息缺失”和“全局一致性”之间很难兼顾,而神经网络天然擅长在大规模数据中学到更鲁棒的映射关系。

3. 基于GAN的去雾网络设计:我在项目中使用的整体方案

3.1 为什么用GAN而不是更简单的回归网络

有人会问:直接训练一个卷积神经网络,输入雾图输出清晰图,用L1或L2损失约束,不也能去雾吗?能,但效果上限有限。我在对比实验里发现,纯回归网络训练出来的模型倾向于输出“安全但模糊”的结果。因为L1损失鼓励像素平均,模型面对多种可能的清晰恢复时,会选择中庸的解决方案。这就好比让一个人给模糊照片下判断,如果判断错了扣分不多,他会倾向于给出模棱两可的答案。

GAN的核心优势在于引入了一个判别器网络,它的任务是区分生成结果到底像不像清晰的真实图像。生成器要同时骗过判别器,就必须输出清晰锐利、色彩自然的图像。这种“对抗压力”让生成器不再满足于低风险的平均结果。做去雾这类不适定问题,信息缺失本来就严重,GAN给了模型一个从数据分布中“补全”细节的机会。当然,对抗训练也带来了不稳定问题,这部分我在后面专门写一节。

3.2 生成器结构:U-Net加残差块,注意感受野和细节保留

我最终采用的生成器主体是U-Net结构,配合残差模块。这里解释一下U-Net为什么适合图像去雾:它由编码器、解码器和跳跃连接组成,编码器逐层下采样提取高级语义特征,解码器逐层上采样恢复空间分辨率,跳跃连接把编码器每一层的细节特征直接传到解码器对应层。对去雾来说,模型既要判断雾的浓度分布,又要保留原始图像的纹理细节,跳跃连接能有效避免下采样过程中的细节丢失。

在U-Net中间层,我堆叠了多个残差块而不是普通卷积层。残差块可以缓解深层网络的梯度消失问题,让网络在训练初期更容易收敛。我特意做了一组消融实验:去掉残差块换成同等深度的普通卷积,训练同样的轮数,带残差块的模型在SSIM上高了约0.02,而且训练损失曲线更平稳。这可能源于残差结构让网络更容易拟合恒等映射——在雾气较薄的区域,模型只需学习轻微的亮度调整,残差学习比直接重映射要省力得多。

3.3 判别器设计:PatchGAN为什么比全局判别器更稳

判别器结构直接影响去雾图像的视觉效果。我一开始使用了常规的全局判别器,输入整张去雾图,输出一个0到1之间的真假概率值。训练时确实能收敛,但生成出来的图像有明显的“廉价的真实感”:整体色彩分布接近真实图像,但放大后局部纹理不自然,存在水渍感。后来我把判别器换成了PatchGAN,这是GAN图像翻译中常用的做法。

PatchGAN并不对整张图做真假判定,而是把输入图像切分成多个图像块,对每个块分别判断真伪,最终将所有块的结果取平均。我用的判别器是70x70感受野设计,每个输出像素对应原图中的一个70x70区域。这样一个像素点覆盖的范围里包含了足够的纹理信息,能够迫使生成器在局部结构上更精细。用PatchGAN之后,去雾图像的细节明显更扎实,而且判别器参数量变小,训练也更快。

换PatchGAN还有一个隐藏的好处:降低判别器过度强大的风险。全局判别器很容易通过整张图的色彩统计就分辨出真假,导致生成器只需要把颜色调对就能骗过判别器,反而忽略了真实的结构信息。PatchGAN迫使判别器关注局部patch的高频细节,生成器不得不尽力恢复纹理和边缘。

3.4 损失函数组合:对抗损失、感知损失与L1的配比实验

单一对抗损失训练出来的生成器容易产生诡异的纹理,我最终使用的损失函数是三个损失的加权组合。首先是对抗损失,负责让输出图像的整体分布逼近真实清晰图像;其次是感知损失,它用预训练分类网络的中间特征来衡量两张图像在语义层面的差异,鼓励生成图在高层特征上和清晰图对齐;最后是L1损失,负责像素级约束,防止生成器输出偏离原图太远。

我在实验里固定总损失权重为1,逐步调整三个分项的配比。最终采用的比例是:对抗损失权重0.2,感知损失权重0.6,L1损失权重1.0(这里说明一下,L1权重不是1.0,而是作为基准权重1,另外两项在训练时相对调整)。感知损失权重不能过大,否则生成结果会沿袭分类网络偏好的纹理样式;对抗损失权重过大会导致训练开始阶段振荡剧烈。这部分我强烈建议使用曲线可视化工具实时盯着损失变化,而不是每训练完一轮才记录一下,否则很难定位是哪一项损失在“拖后腿”。

4. 训练细节与效果评估:从指标到主观感受

4.1 训练策略:分阶段训练是稳定GAN去雾的关键

直接端到端训练生成器和判别器会非常不稳定,我的做法是分两个阶段。第一阶段,只使用L1损失和感知损失训练生成器,不启用判别器。这一步的目的是让生成器先学会一个“基础去雾能力”,生成质量已经明显优于随机初始化的状态。此时图像虽然可能偏平滑,但整体轮廓和色彩趋势是对的。第一阶段大概占整个训练轮数的百分之三十到四十。

第二阶段才加入判别器,启用完整的对抗损失,让生成器在“基础去雾能力”之上细化纹理。这样做的好处是,对抗训练开始时生成器已经离真实清晰图像不远,判别器面对的主要是“好图里的局部瑕疵”,而不是和一张乱七八糟的图对抗,梯度信号更有指导性。我在第一阶段结束后单独保存了一份模型权重,后续调参失败时能随时回退到这个基线上重新开始对抗训练,这个习惯帮我节省了大量时间。

4.2 评估指标怎么看:PSNR、SSIM与色差指标的取舍

去雾效果的评估比一般图像增强更微妙。PSNR和SSIM是最常用的两条客观指标,但我在实验中发现它们的判断和人的主观感受经常不一致。PSNR对像素误差敏感,一张整体偏暗但边缘锐利的去雾图可能PSNR很低,人眼看着却不错;SSIM更关注结构相似性,但它对亮度偏移和色彩偏移不敏感。举个具体例子:某个模型输出了严重的灰蓝色偏图,SSIM反而比另一张色彩正常的图更高,因为结构没变,只是整体色调偏移了。

所以我额外引入了两个指标:CIEDE2000色差公式和UCIQE(水下/雾天图像质量评价的常用指标之一)。CIEDE2000是人类感知色差的国际标准,能直接反映去雾后颜色偏移的严重程度;UCIQE则综合了色度、对比度和饱和度信息,更贴近雾天图像质量的主观感受。我的项目报告里同时列了这四项指标,避免单看某一项做错结论。

4.3 实验结果对比:传统方法、纯L1回归、GAN方案的差异

我在测试集上做了三个方案的对比:暗通道先验处理、纯L1损失的U-Net回归、完整GAN方案。单看PSNR,纯L1回归甚至略高于GAN方案,但这完全是假象。把三张结果图放在一起看,暗通道先验在天空部分明显发灰发蓝,纯L1回归的输出边缘模糊、远处细节黏成一团,GAN方案的图像纹理更锐利,天空色彩也更自然。CIEDE2000色差指标上,GAN方案比纯L1回归低了约百分之十八,比暗通道先验低了约三分之一,这和人眼观察的方向一致。

这个对比非常能说明问题:客观指标必须结合视觉评测,而且指标之间要互相校正。我后来在报告里写结论时,特意用“视觉自然度优先,PSNR/SSIM作为参考,CIEDE2000与UCIQE作为色彩和对比度补充”的方式来呈现,而不是只贴一堆数字。

5. 实战中踩过的坑与排查思路

5.1 生成器输出发灰:判别器失衡与BatchNorm使用问题

训练到某个阶段后,我发现生成器的输出整体发灰,像是蒙了一层半透明的灰纱。最初我怀疑是雾没去干净,于是加大L1损失权重,结果更灰了。后来查看训练日志才发现,此时判别器的loss已经降到接近零,而生成器的对抗loss一直在高位震荡。这说明判别器已经“太强了”,生成器无论如何输出都无法骗过它,梯度信号失去指导意义,生成器只能原地踏步。

排查和修复思路分两步。第一,把判别器的学习率降到生成器的三分之一左右,人为限制判别器的更新速度;第二,检查生成器中的BatchNorm层行为,发现训练和测试时的批次统计差异很大,测试时输出偏灰。我把生成器中间层的BatchNorm换成InstanceNorm,问题明显缓解。图像生成任务里InstanceNorm每个通道独立归一化,对单张图像的色彩分布更敏感,也更适应不同雾浓度的输入。

5.2 天空区域偏色的根因:透射率上界和色偏惩罚

另一类典型问题是模型对天空区域的处理不稳定,有时把天空变成淡紫色,有时变成深灰色。这个现象在暗通道先验里也常见,但GAN方案里的表现形式更隐蔽。最初我怀疑是训练数据里天空区域占比太大,导致模型对天空的“先验”过度学习,但调整数据比例后改善有限。

后来我在一张测试图上逐通道检查输出,发现偏色的主要来源是透射率估计在天空区域接近零,生成器为了压低透射率,把RGB三个通道做了不对称压缩。修复方式是双管齐下:一是给生成器的最后一层输出增加一个可学习的全局色彩校正层,让模型学到色彩上的全局一致性;二是把透射率图在网络中间层表示时设置一个下限,比如0.1左右,防止出现透射率为零的物理不可能区域。加了这两个约束之后,天空偏色问题基本从“经常出现”变成了“偶尔轻微”。

5.3 伪影与网格状噪声:上采样方式和卷积核的调整

第三个坑是训练后期去雾图像上出现了周期性网格伪影,尤其在均匀纹理的区域非常明显。我用肉眼初步判断是转置卷积带来的棋盘效应——转置卷积的核大小如果不能被步长整除,会产生不均匀的重叠,形成规律性的亮暗条纹。排查时我把上采样层逐个替换成“双线性插值+普通卷积”的组合,再用测试图对比,最终确认问题就出在转置卷积上。

这里分享一个小技巧:排查伪影时不要只看最终输出,要逐层可视化生成器中间特征图。棋盘状噪声在深层特征里就已经存在,只是到输出时才体现出来。我把所有转置卷积替换掉之后,网格伪影彻底消失,但模型参数增加了百分之几,训练速度略降。这个代价是值得的,毕竟输出图像的质量才是最终目的。

6. 训练成本与工程化注意事项

6.1 软硬件环境与训练时长参考

整个项目跑下来,计算资源消耗不小。我用了一块中端消费级GPU,训练集大约一万两千张图像,图像分辨率统一到256x256。分阶段训练的总轮数是两百轮,第一阶段八十轮,第二阶段一百二十轮。第一阶段每轮约三分钟,第二阶段每轮约七分钟,总训练时长大约二十小时出头。如果分辨率提高到512x512,训练时间会翻近四倍,显存占用也会翻倍,GPU显存不够时需要配合梯度累积来模拟更大的批次。

显存不足是很多同学常遇到的问题。我的经验是:优先把batch size降到4或2,其次降低输入分辨率,最后才考虑模型剪枝。因为去雾任务对细节要求高,模型容量太小的话,去雾能力会明显下滑。对了,PyTorch自带的自动混合精度训练在这个项目里能省约百分之三十的显存,训练速度也有提升,值得一开始就开。

6.2 工程化部署的一个小坑:输入尺寸和预处理差异

模型从离线实验到部署上线还有一个容易犯的错:预处理流程没对齐。我在训练时对图像做了随机裁剪、翻转、亮度抖动,但预测接口直接用了原始前缀代码里的resize逻辑,结果同一张图在离线评测和在线调用时输出差异很大。后来专门写了一份预处理一致性对照测试,把离线路径和线上路径跑同一批样本,对比PSNR是否一致,才定位到是resize插值方式不同。

这一类问题看似无关紧要,但在真实项目里非常影响交付体验。如果读者将来要把去雾模型接到应用里,我建议从第一天就把预处理逻辑封装成独立模块,训练、验证、测试、上线都调用同一份代码,不要每个环节各写一份。

做完整个项目,我最大的体会是:图像去雾本质上是从一张退化图像反推场景辐射率的病态问题,任何算法都是在“信息不完整”的前提下做合理的假设与补全。传统图像处理提供了清晰的物理直觉,GAN提供了数据驱动的补全能力,两者结合才是我目前看到比较合理的路线。如果让我重新做一遍,我会更早引入物理模型的约束——比如把大气散射模型的某些参数作为网络结构的先验,而不是完全交给网络黑箱学习。这个方向也是我后续准备继续尝试的,届时再和大家分享新的实验记录。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询