雾天出去拍照,回来一看,画面灰蒙蒙的,远山和建筑全藏在白汽里,这种图别说发朋友圈,连喂给识别算法都够呛。图像去雾就是把这个过程反过来:输入一张带雾的观测图,还原出背后清晰的场景。这个方向看着简单,做起来却很有讲究,既要处理光照估计,又要猜透射率,还要保证恢复出来的纹理不是“糊”出来的。我最近完整跑了一个基于图像处理与生成对抗网络的图像去雾项目,从暗通道先验到GAN,从网络设计到训练调参,踩了不少坑,这篇把整个思路和实操过程梳理一遍。如果你正打算做去雾、做图像增强,或者刚开始接触GAN,这套流程可以直接参考,能省掉很多摸索时间。
1. 项目概述与整体设计思路
1.1 先搞懂雾是怎么“糊”住图像的
去雾的第一步不是急着上模型,而是要明白雾图是怎么来的。从物理层面讲,雾天成像可以描述成大气散射模型,这个模型把相机看到的图像拆成两项的叠加:一项是场景原本的辐射经过透射率衰减后到达相机,另一项是大气光在散射作用下形成的背景光。写成公式就是:
I(x) = J(x)·t(x) + A·(1 − t(x))
其中I(x)是观察到的有雾图,J(x)是清晰场景图,t(x)是透射率,A是全局大气光。透射率又可以写成t(x) = e^(−β·d(x)),β是大气散射系数,d(x)是场景点到相机的深度。雾越浓、场景越远,透射率t就越小,图像被大气光淹没得越厉害;近处场景t接近1,画面自然清晰。
这个模型的难点一眼就能看出来:已知的只有I(x),要反推的却是J(x)、t(x)、A三个未知量。一个方程解三个未知数,本身就是典型的病态逆问题,所以去雾本质上是一个“带先验约束的逆问题”。传统方法里最出名的是暗通道先验,它基于一个统计规律:无雾图像在局部小区域内,总有一个颜色通道的亮度趋近于0。利用这个假设可以估算透射率和大气光,进而反推清晰图像。这个思路对大多数室外场景都有效,但一旦遇到天空、白色墙面、光源区域这些不满足假设的地方,就会严重偏色。我一开始先用暗通道先验把整条流程跑通,目的不是跟深度模型比谁更强,而是把它当作物理先验和baseline,后面做深度方案时还能拿来对照。
1.2 为什么最终选了GAN而不是只堆CNN
有了物理模型作参照,深度学习路线就清楚很多。最直接的思路是训练一个CNN,输入有雾图,用均方误差或L1损失监督输出与清晰图的差异。这个方案能出结果,但常见的问题是输出过于平滑。原因在于L1/L2损失会鼓励网络预测所有可能结果的“平均值”,尤其在纹理丰富区域,细节容易被抹成一片模糊。这就像你让一个人猜一堆数字的平均值,他给的答案很稳,但永远不是任何一个具体样本。
GAN恰好能补上这个短板。生成器的任务是“造”出一张看起来像真实清晰影像的图,判别器的任务是分辨这是网络生成的还是数据集的真实清晰图。两者互相对抗,迫使生成器把高频细节、边缘结构、真实纹理重新“生成”出来,而不是做均值平滑。在我这个去雾项目里,最终方案就是把传统图像处理与生成对抗网络结合:一个负责提供物理先验和机制解释,一个负责把纹理和细节做到以假乱真。
顺带说一句,现在很多新方案会直接用扩散模型或者更大规模的预训练模型做去雾,效果也确实好。但如果目标是理解任务本质、可控地迭代方案,GAN这条路线从训练成本、可解释性到工程落地都更友好。我建议第一次做这个方向的人,别一上来就堆最重的模型,先把GAN这套管线完整跑通,跑明白了再考虑架构升级。
2. 网络架构拆解:生成器、判别器与物理先验
2.1 生成器:U-Net加ResBlock,边缘和纹理两头抓
去雾任务对生成器有两个硬性要求:一是要保边缘、保留场景空间位置,二是要有足够大的感受野,把远处被雾完全盖住的区域“找回来”。满足第一个要求最自然的是U-Net结构。编码器逐层下采样提取语义特征,解码器逐层上采样还原分辨率,中间的跳连接把编码器各层的空间细节直接传到解码端对应层,这样输出图像的边缘不会因为多次下采样而丢失。
我的生成器大致是这样:输入256×256×3的RGB有雾图,经过5次下采样,每层用3×3卷积、步长2,通道数从64逐步加到512;在最高分辨率特征图位置叠加6到9个ResBlock,每个ResBlock由两个3×3卷积加残差连接组成,用来扩大有效感受野、加深非线性表达能力;解码端对称做5次上采样,上采样用的是双线性插值再接一个3×3卷积,而不是转置卷积。最后接一个Tanh激活,输出限制在[−1,1],与输入归一化保持一致。
关于为什么不用转置卷积,这里先卖个关子,后面“常见问题”里我会专门说,这是一个非常典型的坑。
2.2 判别器:换成PatchGAN之后,细节明显改善
判别器这块,我最初用的是最朴素的二分类网络,直接把整张图下采样成一个标量,输出真或假。训练了两轮就发现问题:生成图像整体颜色和结构都过得去,但局部纹理看起来别扭,比如墙壁出现奇怪的纹路、树叶区域糊成一片,而判别器却已经判断为“真”。原因在于全局判别器给的是整图一个笼统的评价,它很难约束局部细节。
后来我换成了PatchGAN判别器。PatchGAN的最后一层不是单个数值,而是一个二维矩阵(比如30×30),每个值对应原图一块感受野区域(经典实现约70×70像素)的真伪判断。这样判别器既要看全局结构,又会逐块检查纹理细节,任何一块区域做得不真实都会被拎出来。生成器为了骗过它,就得把每一块纹理都补齐。实测下来,换掉判别器之后,叶子、发丝、路面这类高频区域的效果提升非常明显,主观视觉改善立竿见影。
2.3 把暗通道先验变成生成器的“第四通道”
这里是我觉得整个项目最值得分享的设计:不只在baseline阶段用暗通道先验做对比,而是真正把物理先验融合进深度模型。做法是这样:先用暗通道先验从有雾图估算出一张透射率图,再把这张透射率图作为额外输入通道,与原始RGB图拼接成四通道输入,送给生成器。
为什么要这么做?因为透射率图本身携带了“哪里雾浓、哪里雾淡”的结构化信息。生成器可以从一开始就知道远处要更用力去雾、近处要保持原样,而不是全靠卷积从头学这些规律。这有点像给模型开了一扇物理知识的“外挂窗”。实现上只需要几行图像处理操作,就能把传统方法的先验转化为深度模型的先验,开销极小,收益却可量化。我对比过三通道输入和四通道输入的版本,同样训练轮次下,四通道版本在合成测试集上PSNR能高出约0.8到1.2dB,天空区域的偏色问题也轻了很多。如果项目周期紧,这条融合路径是性价比很高的加分项。
3. 损失函数设计与训练策略
3.1 四路损失各管一摊:L1、感知、SSIM、对抗
去雾模型的训练不能靠单一损失函数。我最后用了四个损失加权组合,每一路都有各自的职责。
第一路是像素损失L1,计算输出与真值的逐像素绝对差。它保证颜色和整体亮度不会偏离真实值太远。相比L2,我的体感是L1更稳,对异常值不敏感,生成的图像不那么“发灰”。L2会把大的像素误差放大,导致网络更偏向保守预测,画面细节会被磨得更平。
第二路是感知损失。把生成图和真值图分别送入VGG19网络,取前面若干层的特征图做L1距离。VGG网络是在大规模分类任务上预训练过的,它的特征空间更接近人类感知的结构、物体边界和语义层次,比像素空间更接近“看起来像不像”这个标准。实践里我取了relu1_2、relu2_2、relu3_2、relu4_2四层,权重按1、1、0.5、0.25递减。
第三路是对抗损失,采用PatchGAN判别器的判别结果作为监督信号,负责高频纹理的真实感。它给生成器提供的是“判断真伪”的压力,而不是逐像素对齐的压力,所以能逼出更锐利的边缘。
第四路是SSIM损失,基于局部窗口的亮度、对比度、结构三项对比建立结构约束。简单理解就是它让输出在边缘和整体结构上和真值“同形”,而不是只盯着单点像素。权重上,我实验时L1权重视为1作为基准,感知损失放0.1到0.2,SSIM放0.3左右,对抗损失放0.05到0.1。一开始我把对抗权重开到0.5,结果训练后期生成器输出色彩饱和度异常地高,因为判别器被“带偏”了,后来降下来才恢复正常。
3.2 训练参数记录与预热策略
训练超参数我踩了很多次坑,最后稳定下来的配置大致如下:
- 生成器优化器:Adam,初始学习率2e-4,betas=(0.5, 0.999)
- 判别器优化器:Adam,初始学习率1e-4,betas=(0.5, 0.999)
- Batch size:8到16,输入图像尺寸256×256
- 训练轮次:100轮起步,看验证集SSIM不再上升就停
- 学习率调度:前50轮固定,之后线性衰减到1e-5
有一个很关键的操作叫“预热”。我先让生成器单独训练大约1000到2000步,只用L1、感知和SSIM损失,判别器保持冻结。等生成器已经把大致去雾效果学出来了,再同时开启判别器,引入对抗损失。这么做的原因是,如果从一开始就让生成器和判别器从零互相对抗,两边都极不稳定,损失曲线像过山车,结果经常是模型收敛到奇怪的伪影上。预热相当于先给生成器一个靠谱的起点,再用对抗来打磨细节。这个策略帮我把稳定收敛的时间缩短了很多。
判别器这边还做了标签平滑,真实标签从1.0改成0.9,让判别器的判断不要那么绝对,对训练稳定性有帮助。这些都是工程实践中很有价值的小细节。
4. 实操过程:从数据准备到模型训练
4.1 成对数据哪来:合成雾图数据集与预处理
去雾训练最理想的数据是有雾图和同一场景清晰图成对出现,但真实拍摄很难做到严格控制变量。实际项目里我用的是公开的合成雾图数据集:用清晰图和对应的深度图,按前面说的大气散射模型,设置不同的散射系数和大气光参数来生成雾图,这样每一对图像都是像素级对齐的。
拿到数据之后,预处理有几个细节比较关键。第一,图像全部裁剪成256×256,训练前做随机翻转、随机旋转90度,增加样本多样性;第二,像素值归一化到[−1,1],生成器输出激活函数用Tanh正好匹配这个范围;第三,如果发现某些样本雾太浓导致监督信号弱,可以按透射率分布做分层采样,保证训练集里中低浓度雾也有足够样本,不然模型很容易只学会“重雾全清”,在轻度雾图上反而把远处细节过度增强。
另外,暗通道透射率图的预处理也要留意。暗通道估算受噪声影响较大,我用导向滤波做了平滑,避免把噪声以额外通道的形式喂给生成器。导向滤波是一种边缘保持的滤波操作,可以平滑掉透射率图中的细小噪点,同时保留深度变化处的强边缘,这样生成器既拿到了可靠的物理信息,又不会被噪声干扰。
4.2 训练流程与关键结果记录
训练时我习惯边训练边存checkpoint,同时在验证集上计算PSNR和SSIM。单步处理的顺序是:读取一张有雾图和对应清晰图,有雾图先计算出暗通道透射率,拼成四通道输入;生成器输出预测清晰图,分别和真值计算各路损失;梯度回传更新生成器(预热阶段),或者交替更新生成器和判别器。硬件条件有限时,batch size可以降到4,配合梯度累积把有效batch size拉到16,效果与显存充足的配置接近,这是我试过很实用的替代方案。
后面这张表是我实验里一组典型的损失权重和对应验证集指标,都是同一个数据集、同样训练轮数下的结果,供参考。
| 损失组合 | L1 | 感知 | SSIM | 对抗 | 验证PSNR |
|---|---|---|---|---|---|
| 只用L1 | 1.0 | 0 | 0 | 0 | 21.6 |
| L1+感知+SSIM | 1.0 | 0.15 | 0.3 | 0 | 23.1 |
| 全损失 | 1.0 | 0.15 | 0.3 | 0.07 | 24.2 |
| 全损失+对抗0.3 | 1.0 | 0.15 | 0.3 | 0.3 | 23.5 |
对抗损失权重不是越大越好。表里最后一行权重0.3时PSNR反而下降,主观细节也没有更好,主要是生成器开始过度“炫技”去造纹理了,属于典型的过拟合到判别器口味上。
4.3 推理部署时的两个提分动作
推理阶段的输入同样要拼上暗通道透射率通道,输出从[−1,1]反归一化回[0,255]。我实际部署时加了一步很廉价的优化:测试时增强(TTA)。对输入分别做原图、水平翻转、垂直翻转、旋转180度四路推理,把四张预测结果反变换后取平均。TTA能明显减少局部伪影的随机性,在PSNR上大概能提升0.3dB左右。推理成本翻了几倍,但如果是离线处理,非常值得。
还有一个容易忽略的点:生成模型的输出偶尔会有极端像素值,直接拉伸显示会刺眼。我在推理后处理里加了一个基于灰度直方图的分位裁剪,把1%和99%分位以外的像素截断,再做线性映射,观感一下就正常了。这一步纯属工程技巧,但对最终交付结果的影响很大。
5. 常见问题与排查技巧实录
5.1 偏色、雾残留这些老毛病怎么查
我最常遇到的问题是输出偏蓝或偏灰。排查顺序一般是这样:先看训练集的清晰图是否有偏色,排除数据问题;再看暗通道透射率估算有没有偏差,如果透射率整体估高了,模型会倾向输出偏暗偏蓝的结果;最后检查损失权重,对抗损失过大容易让输出色彩饱和异常。解决手段按优先级:降低对抗损失权重、在YCbCr空间给色度分量加L1约束、在训练集里多塞一些含天空和白色高光区域的样本。
雾残留通常发生在景深剧烈变化的位置,比如一棵树后面是远景。原因是透射率图在深度边界附近被平滑了,模型没学会精确区分前景和远景。我在暗通道透射率图上改用边缘保持的导向滤波参数,并把原图的梯度信息也拼到输入通道里,这个问题改善很明显。如果你也遇到类似情况,可以按这个顺序排查。
5.2 训练震荡和模式坍塌的急救方法
GAN训练中判别器太强是个经典问题。现象是生成器损失居高不下,输出图反复出现相同的大片色块,几乎每张预测都长得差不多,这是典型的模式坍塌。我当时做了三件事:把判别器学习率从2e-4砍到1e-4,给判别器卷积层加谱归一化,再加标签平滑。三件事叠加之后,训练曲线很快变得平稳。
如果跑了几万步还在震荡,还有个“重启”思路:先停掉判别器更新,只训练生成器的像素和感知损失,缓它几百步再重新打开对抗。这个操作在实践中比调一堆参数更直接,也更容易理解原因——GAN的不稳定通常来源于两个网络强度失衡,适当削弱一方就能恢复平衡。
5.3 棋盘格伪影的根源与替换方案
棋盘格伪影是生成图像里常见的高频瑕疵,形状像一个个小格子。绝大多数情况是用转置卷积做上采样导致的,转置卷积的核在重叠区域会产生周期性的不均匀响应,输出就会出现棋盘状痕迹。解决办法很简单:把转置卷积换成双线性插值再加3×3卷积,或者使用像素重组上采样,棋盘格基本就能消失。
纹理生硬往往来自感知损失权重过高,模型学会了高频分量但不够自然。把感知权重从0.3降到0.1,配合判别器一起作用,效果会更平衡。这个组合我在多个测试场景里验证过,生成的细节既清晰又不发假。
5.4 合成数据到头了,真实雾图怎么办
这是去雾项目绕不开的一堵墙:合成数据训练的模型,到真实雾图上经常出现色彩衰退、块状假纹理。我的经验是两条路结合着走。一是用真实雾图做微调。真实雾图没有成对的清晰图,就换成循环一致性损失或自监督重建任务,让模型在无真值的情况下学会域自适应。二是评估时不要只看PSNR和SSIM,还要用下游任务验证。比如把去雾结果送去跑一次目标检测,看检测召回率和准确率有没有提升。
对我来说,去雾效果好不好,最终标准是下游任务有没有帮上忙,而不是指标刷得多高。这个思维习惯在做图像复原类项目时特别重要。顺便整理一个问题速查表:
| 现象 | 可能原因 | 常用处置 |
|---|---|---|
| 输出偏蓝偏灰 | 透射率估算偏高 | 检查暗通道参数、降低对抗权重 |
| 雾残留明显 | 深度边缘透射率被平滑 | 换导向滤波参数、拼接梯度通道 |
| 训练震荡 | 判别器过强 | 降学习率、谱归一化、标签平滑 |
| 棋盘格伪影 | 转置卷积 | 换双线性插值+卷积或PixelShuffle |
| 色彩饱和异常 | 对抗权重过大 | 对抗损失降到0.05~0.1 |
6. 后续扩展方向与个人心得
做完这个项目,我最大的感受是:图像去雾不是一个单纯的深度学习问题,它同时涉及光学物理建模、传统图像处理和生成模型三个维度。只抓其中一个维度都能做出东西,但要做得好,就得把三个维度拧在一起。GAN在这里的价值尤其明显——它不只是让图变清晰,而是让图“看起来自然”,这在去雾这种强病态逆问题上比任何单一损失都更贴近人的视觉感受。
如果之后要在这个项目上继续扩展,我会优先看这几个方向:把生成器主干换成注意力机制更强的结构,但保留暗通道透射率通道这个融合思路;引入时间维度做视频流去雾,利用相邻帧的时序一致性消除单帧处理的闪烁问题;以及做真正的弱监督训练,用真实雾图加光影一致性约束完成域自适应。
最后分享一个实操里的小技巧:如果你想快速验证一个新想法,不要直接改大模型重训。把生成器冻住,只换判别器或只调损失,在几千步内看验证集曲线变化,效率会高很多。项目后期我几乎都是靠这种方式评估改动,省下的时间和算力非常可观。这套基于图像处理与生成对抗网络的图像去雾流程就梳理到这里,希望对你正在做或者准备做的去雾项目有帮助。