
1. 从“雾天数据集凑不齐”说起为什么要做图像加雾增强做工业视觉、自动驾驶感知、遥感识别或者户外监控项目的朋友十有八九都遇到过同一个尴尬晴天、白天、光线充足的数据攒了一堆可一到雨雾天气模型表现就断崖式下跌。原因不用多说——训练的时候压根没见过雾图推理时突然来一张带雾的输入特征分布整个乱掉。有两条路摆在面前一是去真实场景采集雾天数据二是自己去“造”雾。真实采集的成本有多高不用细说雾天可遇不可求要凑齐不同浓度、不同光照、不同景深分布的数据没有几个月打底根本做不完。就算等到了雾天采集设备和标注人力又是一笔开销。相比之下对现有图像做加雾化处理人为扩充带雾样本是性价比高得多的一条路。我当时做某户外检测项目时就是这么干的。项目里拿来训练的图像大部分来自晴好天气下的历史数据但交付现场经常是清晨、雨后、近水区域动不动就是轻雾或浓雾。一开始直接从真实场景重新采集试了两周发现进度根本走不动。后来改成对训练集做加雾增强模型准确率提了十几个百分点整个项目周期也压缩下来了。这篇文章就把我在这个方向上的完整思路、常用方案、具体写法和踩过的坑都整理一遍。如果你正要扩充带雾训练集或者做的项目经常面对低能见度场景这篇内容能帮你少走不少弯路。2. 先搞清楚加雾背后的物理模型大气散射模型的由来与数学形式2.1 雾是怎么“遮”住图像的在做图像加雾之前最好先把雾的本质搞清楚。雾天拍照传感器接收到的光信号实际是两部分一部分是目标物体反射的光穿过大气到达相机另一部分是大气中悬浮颗粒对光源太阳光、环境光的散射光直接进入相机。这两部分叠加起来就是最终成像。所以雾图并不是简单地“原图变白了”“对比度降低了”它还有一个很隐蔽的影响景深越远的目标被衰减得越厉害同时叠加进来的大气散射光也越多。这也是为什么近处的物体在雾天还能看清远处的山或者楼几乎融进背景里。如果加雾时没有考虑深度信息把所有区域均匀地“刷白”那生成的雾图就很容易被模型“识破”因为真实雾图的雾浓度在空间上是有梯度的。2.2 大气散射模型的数学表达计算机图形学和计算机视觉领域里最常用的大气散射模型可以写成下面这个形式I(x) J(x) * t(x) A * (1 - t(x))其中I(x)是观测到的带雾图像即最终合成结果J(x)是无雾清晰图像即原始干净图像A是全局大气光值可以理解为无限远处大气散射光的强度t(x)是透射率图取值范围在 0 到 1 之间表示物体反射光经过大气衰减后到达相机的比例。透射率t(x)通常建模为随距离指数衰减t(x) exp(-β * d(x))其中β是大气散射系数控制雾的浓度β越大雾越浓d(x)是场景深度表示每个像素对应的物体离相机的距离。把两个公式合起来就得到了完整的合成模型I(x) J(x) * exp(-β * d(x)) A * (1 - exp(-β * d(x)))也就是说只要有一张清晰的输入图J一份对应场景的深度图d再设定好大气光A和散射系数β就能通过逐像素计算合成出一张带有物理意义的雾图。2.3 为什么这个模型适合训练数据增强这个模型之所以在工程中被广泛使用因为它解决了一个非常实际的问题雾的浓度和分布是由场景深度决定的。真实雾天里远近物体的清晰度是不同的这给模型提供了非常强的监督信号。如果模型在训练时见过大量“深度相关”的雾图它学到的特征就不只是“图像对比度低”这么简单而是隐含了“远处的物体本来就不该太清楚”这种先验。我之前做过一个小实验用同一批清晰图分别用“均匀高斯雾化”和“模型化加雾”扩充训练集来训练同一套检测模型在真实雾天测试集上后者的准确率明显更高。原因是均匀高斯雾化只是降低了图像对比度没有改变远近物体的相对清晰度关系模型学到的东西和真实雾天的成像规律不匹配。2.4 深度图从哪来问题来了上面的模型需要深度图d(x)但一般的训练图像是普通彩色照片没有深度信息。解决这个问题有几条路使用双目或深度相机采集的数据某些数据集本身自带深度图比如自动驾驶领域的公开数据集直接拿深度通道来用。单目深度估计模型用训练好的单目深度估计网络后文会细说给普通图像估出一张深度图。这是我用下来最顺手的方案。简化场景假设如果图像内容比较简单比如纯路面、天空占比较高的场景可以直接构造一张“近处亮、远处暗”的近似深度图也能达到够用的效果。3. 工程上更实用的加雾方案从纯颜色变换到模型化合成3.1 方案一基于深度估计结果的模型化合成这是目前质量最高也最接近真实雾天效果的方式整体流程分三步先用深度估计网络给清晰图生成深度图再使用大气散射模型逐像素合成最后做颜色空间微调。平时我用得最多的深度估计网络是 MiDaS这个模型对纯静态图像就能输出不错的相对深度不需要额外训练。拿到深度图后先做归一化把深度值压到0到1区间然后代入上面那个公式就行。深度图归一化之后透射率图的取值范围会直接影响雾的视觉效果。t越接近0的地方雾越浓t越接近1的地方越接近原图。如果是轻雾场景β可以取值0.1~0.5如果是浓雾β可以拉到1.0~2.0。这个参数的设置没什么绝对标准最好根据自己数据中物体远近的分布来做调整。3.2 方案二不依赖深度图的快速近似雾化有的项目里没有合适的深度估计模型或者图像内容比较单一比如很多工业质检图像都是固定机位、固定背景这时候也可以走近似路线最简单的做法是构建一张“中心比边缘清晰”的距离权重图。假设图像中心的物体通常离相机更近四周更远那么深度图就可以粗略地表示为一个二维线性渐变或者径向渐变。虽然不完全准确但对于某些固定场景已经够用了。还有一种做法是直接对亮度层做高斯模糊和叠加。把图像从 RGB 转到 HSV 或者 LAB 空间提高V通道或L通道的整体亮度再叠加不同尺度的高斯模糊也会产生类似雾的视觉效果。不过这个方法合成的图远处和近处的雾浓度差距不明显视觉上“假”一点但胜在实现简单、速度极快。3.3 方案三基于对抗生成网络的雾图风格迁移再进阶一些的方案是用风格迁移或者对抗生成网络把真实雾天图像的特征“迁移”到清晰图像上。这种方案的优势在于不依赖物理模型生成的雾在整个图像上的纹理细节更自然。缺点也很明显需要大量真实雾图和清晰图的配对数据或者至少需要足够的真实雾图来做对抗训练这个数据本身的成本就把前面积累的优势抵消了。在工程量产项目中我通常不会把它作为首选。如果手头已经有大量真实雾天图像资源倒是可以拿来和模型化合成的结果做混合训练效果会更好。3.4 方案对比怎么选用表格把这些路线做一个直观对比方便不同项目背景的人快速决策方案是否需要深度图合成效果实现成本适用场景模型化合成深度图驱动需要接近真实雾图有空间浓度梯度中自动驾驶、遥感、室外监控近似雾化渐变权重不需要可用但细节不够自然低固定机位、室内、简单背景对抗生成风格迁移不需要纹理自然高已有大量真实雾图资源时纯色阶变换/模糊不需要快速但“假”极低轻量实验、快速验证我自己的经验法则是能拿到深度图就优先用模型化合成拿不到深度图但场景固定就用近似雾化项目周期特别紧、只为了快速看看模型会不会崩那就先随便糊一层色阶变换将就一下回头再补高质量的。4. 动手实现一套完整可复现的加雾增强处理流程4.1 整体流程设计从工程落地角度建议把加雾增强做成一个独立模块不要和训练代码搅在一起。理由很朴素训练时你随时可能调整雾浓度、雾的形态、数据比例把这些参数收敛到一个独立模块改起来不用动主流程。我自己在项目里习惯上的一个做法是离线矿图先把原始清晰图跑一遍脚本生成一批雾图文件存成和原图同目录结构的新目录然后训练时直接把这两个目录分别喂给数据加载器按比例采样做 on-the-fly 的随机选择。这里不建议每轮训练都现场生成雾图因为深度估计这一步本身就挺耗时放在训练循环里会拖慢整个速度。离线扩充一遍后面训练效率高很多。4.2 关键依赖与版本代码用了 Python 3 环境主要依赖这几个库torch和torchvision用来跑深度估计模型PIL或opencv-python图像读写和基础处理numpy数组运算不需要特别新的版本我用的是常规稳定版本组合只要保证 torch 和 torchvision 能加载模型权重就行。深度估计模型我用的 MiDaS 官方权重模型本身对输入尺寸不敏感处理不同分辨率的图很方便。4.3 核心代码实现模型化雾图合成下面的代码就是主流程的实际实现我在项目里跑过的版本基础上精简出来的import cv2 import numpy as np import torch from PIL import Image def load_depth_model(): 加载单目深度估计模型这里以 MiDaS 为例 model_type DPT_Hybrid midas torch.hub.load(intel-isl/MiDaS, model_type) transform torch.hub.load(intel-isl/MiDaS, transforms).dpt_transform device torch.device(cuda if torch.cuda.is_available() else cpu) midas.to(device).eval() return midas, transform, device def get_depth_map(image, model, transform, device): 从单张彩色图像估计深度图 img_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) input_batch transform(img_rgb).to(device) with torch.no_grad(): depth model(input_batch) depth torch.nn.functional.interpolate( depth.unsqueeze(1), sizeimg_rgb.shape[:2], modebicubic, align_cornersFalse, ).squeeze() depth depth.cpu().numpy() depth (depth - depth.min()) / (depth.max() - depth.min()) return depth def synthesize_fog(image, depth, beta1.0, A0.8): 基于大气散射模型的雾图合成 image: BGR 图像值范围 0~255 depth: 归一化后的深度图值范围 0~1 beta: 散射系数越大雾越浓 A: 大气光强度0~1通常取 0.7~1.0 img image.astype(np.float32) / 255.0 depth depth.astype(np.float32) t np.exp(-beta * depth) t t[..., np.newaxis] # 扩展为单通道 fogged img * t A * (1.0 - t) fogged np.clip(fogged * 255.0, 0, 255).astype(np.uint8) return fogged def augment_with_fog(src_dir, dst_dir, beta_range(0.3, 1.5), A_range(0.7, 1.0)): 对目录下所有图像批量生成雾图 import os os.makedirs(dst_dir, exist_okTrue) model, transform, device load_depth_model() for name in os.listdir(src_dir): if not name.lower().endswith((.jpg, .png, .jpeg)): continue path os.path.join(src_dir, name) image cv2.imread(path) if image is None: continue depth get_depth_map(image, model, transform, device) beta np.random.uniform(*beta_range) A np.random.uniform(*A_range) fogged synthesize_fog(image, depth, betabeta, AA) cv2.imwrite(os.path.join(dst_dir, name), fogged) print(fprocessed: {name} | beta{beta:.2f} | A{A:.2f})这个脚本干的事情很直白读取一张图估计深度随机选择beta和A合成带雾图写进目标目录。beta_range和A_range可以按实际需求调如果希望生成多样化程度更高可以把beta_range拉开一些。4.4 参数解读beta和A到底该怎么调这两个参数是雾效果最直接的控制旋钮分别控制“雾的浓度”和“整体亮度基底”。beta是散射系数直接决定透射率t的衰减速度。同一个深度图上beta0.3的时候远处的物体还能看出来轮廓beta拉到 1.5 时远处的部分基本要和背景融为一体了。做数据增强的时候不建议把beta固定成一个值最好在一定范围内随机采样这样模型能看到不同浓度的雾泛化性更好。A是大气光强度决定雾的底色偏亮还是偏暗。真实雾天的底色通常不是纯白有时偏灰蓝有时偏灰黄。“雾”在视觉上的色调差异大体来自这个值。如果想让数据多样一点可以把A的 RGB 分量拆开分别设定比如A (0.8, 0.82, 0.85)这样每个通道有一点微小的偏移合成出来的雾色更有真实感。from 一段实际项目的调试经验来看beta范围设在0.3~1.5是比较舒服的区间A值设在0.7~1.0之间比较合理。A小于 0.7 时合成图的整体亮度和“雾感”都会显得有点脏A大于 1.0 会直接超出像素值范围导致高光区域被截断丢失细节。4.5 提升多样性的几个“变种”操作固定模型合成虽然好但一直用同一个规则也有问题生成的雾图在纹理细节上可能会呈现出某种“风格统一性”这会让模型记住合成模式而不是学会泛化。为了打破这种统一性可以在基本流程上增加几种变形一是给透射率图增加空间噪声。正常生成出来的t是相对平滑的真实雾天的透射率图多少会有一些由于空气湍流导致的局部波动。做法很简单在t上叠加一层高斯噪声然后做一次模糊noise np.random.normal(0, 0.03, depth.shape) t_noisy t noise t_noisy cv2.GaussianBlur(t_noisy.astype(np.float32), (15, 15), 0) t_noisy np.clip(t_noisy, 0.05, 1.0)这样生成的雾在边缘和局部区域会有轻微的浓淡变化看起来更真实。注意噪声的振幅不能太大太大会让画面出现斑点状的“脏感”。二是给A值加色调偏移。真实雾天的色调受环境光影响很大早上的雾偏暖黄样子偏冷白。可以在A的三个通道上分别乘以不同的系数形成色调上的偏置。这样生成的雾图不再是千篇一律的灰白而是有冷暖变化的。三是生成多档雾浓度版本。如果一个目标需要模型在不同雾浓度下都能识别就不要只生成一张雾图可以给同一张原图生成beta0.3、beta0.8、beta1.5三张不同浓度的图三张都放进训练集。这样训练时的采样流程按照“原图/轻雾/中雾/浓雾”的分布模型对浓度变化的鲁棒性会有明显提升。4.6 工程建议先跑通小规模再全量扩展第一次使用这套流程时最好先拿一个小文件夹做实验比如拿 20~30 张不同场景的图跑一遍脚本打开输出图检查一下视觉效果。重点检查几个地方远处物体是不是被雾盖住了而近处还保留清晰度天空区域是不是还能看出层次有没有出现原图高光区域被 A 值拉爆导致的一片死白如果效果没问题再对全量数据集扩展。批量跑的时候建议开 GPU深度估计模型在 GPU 上跑和 CPU 上跑的速度差个 10 倍以上。CPU 也能跑就是慢一个上千张的标注数据集可能要跑几个小时GPU 能在十几分钟到半小时内搞定。5. 加雾增强的工程化实施数据配比、目录管理与训练集成5.1 训练集里雾图的比例怎么定扩充训练样本不是简单地把雾图全塞进训练集就行比例失衡会让模型产生新的偏见。如果雾图占比太高模型在晴天场景下反而会掉点占比太低加雾增强又起不到明显效果。我实际测试下来的一个经验是以目标应用场景为准。如果实际部署环境中“雾天出现率”大概在 20%~30%那训练集里雾图比例最好设置在 15%~30% 之间完全复刻真实分布即可。如果实际部署很多雾天比如某个区域野外环境那比例可以升到 50% 甚至更多但不要超过 70%否则模型对清晰无雾输入的处理能力会被严重削弱。还有一个容易忽略的细节不要让同一个目标的所有雾图全部集中在某一个类或者某一段目标大小区间。扩充的时候注意按类别的原始分布去采样避免某个小类别突然在雾图里被放大了权重。5.2 目录结构和加载器的设计如果离线生成了雾图目录结构建议这样组织train/ clear/ img001.jpg img002.jpg fog/ img001.jpg img002.jpg两张名字一样的图分别代表同一张原图的清晰版和雾图版。这样在做数据加载时非常方便按训练迭代随机选择clear或fog目录剩下逻辑都不用改。如果同一样本存在多档雾浓度可以在fog目录下再分子目录或者文件名加后缀比如img001_beta0.3.jpg。训练时数据加载器每取到一张图按既定概率决定取清晰图还是雾图。如果做随机增强时想用 online 方式把synthesize_fog函数组装到数据加载器里也是可以的但前面说过深度估计这一步太慢不建议放在实时加载流程里。5.3 验证集和测试集要不要加雾这个问题遇到过不少同行来问。我的建议非常明确验证集和测试集不要用合成的雾图尽量使用真实雾天图像哪怕数量少也要保证来源是真实的。理由是模型在训练过程中见过大量合成雾图它在合成雾分布上的表现不一定能代表真实雾天场景下的表现。如果验证集也用同一套合成方法就会得到一个“自嗨”的评估指标看起来涨点了实际落地一测又被打回原形。如果实在搞不到真实雾图做验证退而求其次的做法是用另一套完全不相关的合成方法来做验证集雾图保证合成机制有差异这样评估结果相对可信一些。我当时项目里就是同时用了两种不同策略来做训练增广和验证集雾化效果比用同一套方法好不少。6. 加雾增强后模型训练效果如何评估不看单一指标看多维度对比6.1 评估指标的选择加了雾图扩充之后怎么判断增强到底有没有用不能只看在雾天测试集的准确率涨了没有还需要看模型在原本清晰图上的表现有没有退化。用一张表来列一下我常用的评估维度评估维度指标示例期望变化清晰测试集表现mAP、准确率基本保持或略有下降不超过 2%雾天测试集表现mAP、准确率明显提升提升 5% 以上视为有效雾浓度鲁棒性不同 beta 下连续测试随浓度增加性能下降幅度平缓边界框定位稳定性IoU、中心点偏移雾天下的 IoU 下降幅度收窄在实际项目里那个“雾浓度鲁棒性”维度很值得关注。把测试集中加入不同合成浓度的雾图依次评估看模型性能随浓度增加的衰减曲线。如果增强有效曲线整体会变得更加平缓。光看一个平均准确率有时候看不出来模型是不是只在轻雾下好用、一到浓雾又崩了。6.2 一个实际项目的效果变化参考我之前做的那个室外检测项目加雾增强前后有这么一组数字在真实雾天测试集中增强前的 mAP 大约是0.41加雾增强训练之后涨到0.56提升了约 15 个百分点同一时间清晰测试集合的 mAP 从0.78微降到了0.76完全可以接受。项目里那几张出现频率最高的误检类目——远处目标漏检和低对比度下误检也有明显改善。如果把雾浓度分成轻、中、浓三档单独统计增强前浓雾档的 mAP 只有0.25增强后到了0.49翻了一倍这基本能直观说明模型对重度雾天泛化能力的变化。6.3 搞清楚涨点到底来自“雾”还是来自“数据量”做对比实验时有一个容易忽略的干扰因素加雾图等于增加了训练集的总数据量性能提升可能仅仅来自数据量的增加而不是加雾这一操作本身的贡献。要排除这个干扰需要设计一个对照实验从清晰图中随机抽取相同数量的样本复制一份作为扩充数据加回去和加雾扩充做对比。如果“复制清晰图”的那一组性能提升明显小于“加雾扩充”那一组才能说明雾图增强真正有效。这类对照组实验不一定每次项目都要做但如果要考虑发布决策最好还是先把这个结论验证扎实。7. 实际操作中的坑与排查实录7.1 坑一深度图估计错乱导致雾图出现“光晕假象”第一次跑通模型化合成时我遇到一个很典型的问题某些图像里物体边缘出现了一圈明显的光晕尤其是天空和高楼交界的地方合成出来的雾图边缘发亮发虚和真实雾图差得很远。排查之后定位到问题根源单目深度估计模型在天空和前景物体的深度边界处会生成不连续的深度突变这个突变透射到大气散射模型里就形成了肉眼可见的边缘伪影。解决办法是对深度图做一次导向滤波或者快速双边滤波在保留大结构的同时平滑掉边缘附近的小噪点。实际操作时我直接用了cv2.bilateralFilter(depth, 15, 0.2, 0.2)效果也够用。7.2 坑二A 值统一导致雾色发灰白适配不了暖色场景合成雾图的底色完全由A决定。如果所有样本都用同一个A你会发现整批雾图的色调非常统一尤其是这个场景本身偏暖色调时生成的灰白雾和场景原色调格格不入。这个问题在视觉美观上还算小事对模型来说就是让模型在特征层面偷懒地依赖“灰色调”来识别雾天而不是依赖真正的物理特征。解决方式前面提过随机化A分量并对 RGB 三个通道做独立微调。再进阶一点可以基于场景本身的平均色调来设定A使雾的颜色接近场景环境光的自然颜色而不是凭空给一个固定白灰色。7.3 坑三浓雾样本过拟合到“过曝”区域A值接近 1.0 的时候很多暗部区域的像素值会被整体拉高图像中原本明亮的部分会直接顶到 255 变成死白。模型在训练中见到太多这种过曝样本之后遇到真实雾天有时会把正常偏亮的区域也误判成过曝导致漏检。这个问题的缓解手段是在合成之后做一个轻微的高光压缩或者把A的天花板值控制在 0.92 以下别太贪极致效果。想让雾看起来更浓优先增大beta而不是把A顶到上限。7.4 坑四CPU 推理深度估计太慢成为扩充瓶颈这一点在前面提过一次但值得展开单张图像用 MiDaS 在 CPU 上估计深度1080p 分辨率大约要 2~4 秒1000 张图就是 40 分钟到 1 个多小时。如果项目数据量到几万张这个时间是完全不能接受的。替代方案有两个方向一是给整批图像做等比例下采样后再估深度反正深度图最后都要 resize 回原尺寸二是只用轻量级深度估计模型不需要那么高的精确度速度快很多。我后来在其中一个项目上把输入图下采样到半分辨率深度估计时间降了一半还多合成雾图的视觉质量几乎没差。7.5 常见问题速查表现象可能原因处理办法边缘出现光晕伪影深度图边界不平滑对深度图做双边滤波或导向滤波雾色太灰白和场景不搭大气光 A 固定为灰白色随机化 A 并对 RGB 通道微调高光区域死白A 值太大或 beta 太小A 控制在 0.92 以下优先增大 beta合成雾图整体偏暗A 值太小或原图本身亮度低适当提高 A或对输入图做亮度归一化批量扩充太慢CPU 推理深度估计下采样输入图像或换用轻量级模型模型在清晰图上掉点明显雾图比例过高降低雾图占比做对照实验确定合理比例8. 再补充一个务实的小技巧把“加雾增强”和“去雾”组合起来用如果你已经能做到把清晰图变成雾图那反方向的套利空间也很大雾图加回清晰图可以做图像去雾任务的数据集。也就是说同一套大气散射模型、同一批深度图既能给检测模型做增强也能用来训练一个去雾模块。有些项目实际的做法是在推理时增加了一个图像预处理阶段先用一个轻量级去雾网络把输入图“还原”一下再喂给检测网络效果比直接让检测网络硬抗雾天要好。这个预处理的训练数据恰好就是上面这套流程生成的配对数据——原图是标签合成雾图是输入。一次工程投入两边都能用很划算。我之前手头有个项目就是这么做的先离线圈生成几千张雾图对训练了一个小型去雾网络推理时先做一次去雾预处理后续检测精度在真实雾天测试集上又涨了一截。这个技巧不一定适合所有场景如果你的检测网络本身就是对雾天做过专门增强的再加去雾模块可能反而画蛇添足。9. 基于个人经验的三点心得式收尾这套加雾增强流程从最初用最简单的色阶变换到后来用深度图驱动的物理模型再到配合训练比例和评估机制做调优我前后用了好几轮才把效果稳定住。过程中最大的感受是合成雾图的“物理真实性”比“视觉效果”更重要。很多人一开始只看雾图好不好看、雾浓不浓却没意识到真正的衡量标准是合成图和真实雾图的成像规律是否一致。远近物体的清晰度差异、雾的色调环境、边缘过渡的自然度这些才是模型能泛化的关键。第二点心得是关于“增强力度”的克制。数据增强有一个边际效应递减的规律加雾图的占比不是越高越好浓度分布也不是越极端越好。模型见惯了极端浓雾到了真实场景反而可能矫枉过正。把增强控制在和真实分布相近的范围内往往测试指标最优。第三点也是我很想提醒大家的不要被合成图上的漂亮指标冲昏头脑。训练集的雾是造出来的验证集最好用真实的雾天数据来检验。如果连真实雾图都拿不到至少换一种合成机制去交叉验证。否则模型到底是在泛化雾天能力还是在背合成公式这一点都搞不清楚的话后续上线风险是相当大的。这些经验不一定每个场景都成立但在这个方向上它们确实是实打实摸出来的心得希望对你有参考价值。