做视觉算法这行,最怕听到的就是“雾天数据不够”。自动驾驶、安防监控、遥感识别,但凡涉及室外场景的项目,迟早都会撞上同一个问题:晴天数据管够,一到雾霾天气模型就翻车。我早年也吃过这个亏。后来慢慢摸索出一套成熟做法:对图像进行加雾化处理,增加训练样本。这个方法不复杂,但坑不少。今天就把我反复踩过、修过的经验整理出来,给正在为雾天数据发愁的朋友一个能直接落地的参考答案。如果你正在做目标检测、语义分割或者图像分类,并且手里的模型上线后会遇到有雾天气,这篇文章应该对口。
1. 为什么需要给训练数据“加雾”:合成雾化背后的逻辑
1.1 真实雾天数据获取成本高
先说最现实的问题:真实雾天数据太难攒了。天气是不可控变量,你不能说今天要拍雾景,立刻就起雾。就算运气好等到一个大雾天,架好设备、开上车,采集回来的数据也不一定能直接用。雾的浓度不均匀、场景覆盖不完整、传感器抖动、光照变化,这些都会让采集成本成倍上升。更重要的是,雾天数据的标注成本极高。目标检测框、语义分割mask、深度图,都需要人工逐一标。雾天图像本身就模糊,标注员看着都费劲,标注质量也很难保证。很多项目组尝试过卫星云图、气象站数据去“匹配”雾天时间段,最终发现能用的样本往往只有几百张,训练一个深度学习模型远远不够。
另一个隐蔽的问题是数据分布失衡。即便你辛辛苦苦攒了一批真实雾图,雾的浓度、天气条件、场景类型也很不均匀。可能大部分是轻雾,少量是中雾,几乎没有大雾。模型在这种数据上训练,遇到训练时没见过的大雾场景,表现依然不稳定。相比之下,合成雾化处理可以精确控制雾的浓度、空间分布、光照条件,批量生成成百上千张样本。只要基础清晰图像够多,加雾就能把训练集扩大好几倍。
1.2 数据增强的核心原则:模拟退化而非随机噪声
很多朋友一听到“加雾”,第一反应是给图像加一层白色蒙版,或者调低对比度、加高斯模糊。这个思路不能说完全错,但离真正的图像退化差得远。雾天图像的形成不是简单的“变白”或“变模糊”,而是场景中的反射光在传播过程中被大气中的粒子散射,同时大气光也混入成像路径,使得原本清晰的信息被一层距离相关的“干扰光”叠加。
数据增强的本质,是让模型学会应对真实世界可能出现的退化。如果我们只是随机加噪声,模型可能只是学会了忽略噪声,并没有理解“不同距离的物体清晰度不同”这个物理规律。合成雾化处理之所以有效,是因为它模拟了真实的物理过程:近处的物体受雾影响小,远处的物体受雾影响大。模型在训练时看到这种空间一致的退化,才能真正学会在雾天条件下提取有用的语义特征,而不是单纯被像素统计骗过去。
1.3 大气散射模型是加雾算法的基础
几乎所有正规的合成加雾方法,都源于大气散射模型。公式很简洁:
I(x) = J(x)t(x) + A(1 - t(x))
其中J是清晰无雾图像,I是生成的雾图,A是大气光,t是透射率。透射率t和场景深度d直接相关:
t(x) = e^{-β·d(x)}
β是散射系数,d是场景深度。这个公式理解起来也不难:每个像素看到的最终亮度,由“清晰图像衰减后的反射光”和“大气光混入的散射光”两部分组成。远近不同的物体,因为光线穿过的大气厚度不同,透射率也不同。
用生活化类比来说:雾不是PS里的白色蒙版,更像每个像素根据距离混入不同程度的“大气光”。远处的山看起来发白,不是因为山本身变白了,而是因为你和它之间隔着太多空气,空气里悬浮的粒子把太阳光散射进了你的视线。合成加雾的核心,就是在已知清晰图像和深度信息的前提下,用这个公式准确还原这种物理过程。
这个模型虽然简单,却给数据增强提供了一个极大的好处:可解释、可控。你调节β就能控制雾的浓度,调节A就能模拟不同的大气光条件。后面实操部分我会详细讲参数怎么选。
2. 主流加雾方案与选型思路
2.1 方案一:全局均匀加雾,适合快速试错
最粗暴但最常见的一种做法是全局均匀加雾。假设整张图像所有像素的深度相同,或者干脆忽略深度差异,直接用同一个透射率t对全图做合成:
I = J·t + A·(1 - t)
这个方案的优点非常直接:不需要深度图,只需要原图和两个参数,计算速度极快,几秒钟就能生成几百张。对于景深变化不大、目标较小的场景,比如车牌识别、某些工业检测,全局均匀加雾也能起到增加训练样本的作用。
但它的缺点也很明显:空间上没有任何远近层次感。真实雾天里,远处的建筑应该比近处的汽车模糊得多,而全局均匀加雾会让整个场景像是镜头前罩了一层纱,看起来比较“假”。模型如果在这种数据上训练,容易学到“整张图变白”这种全局偏移,而不是真正学会感知距离。所以我的建议是:如果你只是先跑通流程、验证想法,可以用这个方案;如果模型最终要部署在真实的室外视觉场景,建议直接用下面第二种。
2.2 方案二:基于深度图的物理感知加雾
这是目前主流且推荐的做法。它的思路很简单:既然大气散射模型需要深度,那我就真的给模型一个深度图。逐像素计算透射率,让雾的浓度随场景深度自然变化。这样生成的雾图,近处清晰、远处模糊,空间过渡真实,非常贴近真实雾天成像。
基于深度图的加雾,通常需要RGB图像和对应的深度图。深度图可以来自RGB-D传感器、激光雷达,或者用单目深度估计模型从普通图像中推断出来。前者精度高,后者胜在数据容易获取。合成公式就是前面提到的大气散射模型:
I = J·t + A·(1 - t),t = e^{-β·d}
这个方案可控性非常好:你能精确控制雾的浓度、空间分布、大气光颜色,并且可以批量生成各种浓度、各种风格的雾图。唯一的门槛是需要解决深度图来源,以及深度图与RGB图的对齐问题。但这些问题都有成熟解决方案,后面我会讲。
2.3 方案三:无监督/生成式加雾,必要时再用
第三个方案更多是用在“没有深度图,也不想估计深度”的场景:用生成对抗网络或者风格迁移模型,学习从清晰图像到雾图的映射。典型套路是把真实雾图作为目标域,清晰图作为源域,训练一个网络做跨域转换。
生成式加雾的好处是真实感可能更强,因为它是直接向真实雾图“看齐”的,不需要显式模拟物理模型。但问题也很明显:网络可能改变图像的语义内容。比如原本清晰的车辆纹理被生成器抹掉,或者多出一个不存在的物体。对于目标检测、语义分割这类对像素级标签敏感的任务,生成式加雾很容易引入标签噪声。此外,训练生成模型本身需要大量数据、算力和调参时间,远没有物理模型直接。我的看法是:除非你的深度图来源实在太差,否则不要优先用生成式加雾。
2.4 方案对比,一张表说清楚
| 方案类型 | 是否需要深度图 | 物理真实感 | 计算成本 | 主要坑点 |
|---|---|---|---|---|
| 全局均匀加雾 | 否 | 中低 | 很低 | 缺少景深层次,容易显得假 |
| 物理感知加雾 | 是 | 高 | 中 | 深度图误差会被放大,需要对齐 |
| 生成式加雾 | 否 | 较高 | 高 | 可能改变语义,标签不可靠 |
选型逻辑其实很简单。先问自己:手头有没有深度数据?如果有,优先物理感知加雾。如果没有,但只是验证想法,可以用全局均匀加雾先顶着。如果项目对真实感要求极高,且有可靠的真实雾图数据做参考,再考虑生成式加雾。大多数实际项目,我都会强烈推荐基于深度图的物理感知加雾,这也是后面要重点展开的内容。
3. 基于深度图的雾化合成实操全流程
3.1 数据准备:图像和深度图的来源
做基于深度图的加雾,首先得让图像和深度图配对。公开的RGB-D数据集是现成的资源,通常已经提供了对齐后的图像和深度图,拿来就能用。如果你手里的数据只有普通RGB图像,那可以用单目深度估计模型来生成深度图。需要注意:单目深度估计出来的深度是相对深度,不是真实的米制距离,但用来做合成雾也足够了。
拿到深度图之后,有几个细节必须处理。
第一是尺寸对齐。深度图和RGB图的分辨率必须完全一致,否则逐像素计算透射率时对不上。如果传感器采集的深度图尺寸较小,要先上采样到和RGB一致。
第二是深度图的归一化。不同数据集的深度范围差别很大,有的以米为单位,有的是相对值0到1。建议统一转换为浮点型数组,并记录好深度范围。后面设计参数的时候,这一步很关键。
第三是深度图的质量。深度图经常有空洞、边缘断裂,直接用来计算透射率,雾的边缘会非常生硬。我一般会用中值滤波或者膨胀操作先对深度图做平滑,避免合成出来的雾图出现明显的“条带感”。
3.2 参数设定:大气光A与散射系数β的选择
参数选择是加雾效果好坏的分水岭。先说大气光A。A代表场景中的大气光颜色,物理上通常是接近白色的亮光,所以我建议把它设成一个接近1的标量,比如(0.9, 0.9, 0.9)或(1.0, 1.0, 1.0)。如果你把A设成随机RGB值,很容易让图像整体偏红或偏蓝,看起来就像加了滤镜,而不是雾。
散射系数β决定雾的浓淡。它的物理单位是“每单位距离的衰减程度”,但实际使用中要看你深度图的尺度。如果你的深度图是米制真实距离,β通常取0.01到0.05之间。举个例子:场景中最远距离是200米,β=0.01时,最远处的透射率t = e^{-2} ≈ 0.135,也就是远处几乎看不清;β=0.002时,远处t≈0.67,只能算轻雾。如果你的深度图是归一化到0~1的相对深度,那β就要放大一个数量级,从0.5开始试,否则透射率几乎等于1,完全看不到雾。
实际操作中,我一般不会固定β,而是每次随机采样。这样可以让训练集覆盖轻雾、中雾、重雾多个浓度区间,模型泛化性更好。A也可以在一个小范围内随机,比如0.85到1.0之间,模拟不同的天气光照条件。
3.3 核心代码实现:加雾函数
下面给出一个可以直接复制的Python函数,基于numpy和OpenCV实现物理感知加雾。代码里做了两件事:先对深度图做归一化和平滑,再按大气散射模型合成雾图。
import numpy as np import cv2 def apply_haze(image, depth, beta=0.02, A=(1.0, 1.0, 1.0), depth_is_metric=False): """ 基于大气散射模型对图像加雾。 image: RGB图像,shape为HxWx3,uint8 depth: 深度图,shape为HxW,float32 beta: 散射系数 A: 大气光,RGB通道,范围0~1 depth_is_metric: 深度图是否为米制真实距离 """ img = image.astype(np.float32) / 255.0 d = depth.astype(np.float32) # 深度图平滑,避免雾的过渡区域出现条带 d = cv2.medianBlur(d, 5) # 根据深度尺度决定beta是否缩放 if not depth_is_metric: # 相对深度范围近似0~1,beta需要放大 d_norm = (d - d.min()) / (d.max() - d.min() + 1e-8) t = np.exp(-beta * 10.0 * d_norm) else: # 米制深度,直接用beta d_norm = d / (d.max() + 1e-8) t = np.exp(-beta * d) t = np.clip(t, 0.05, 1.0) t = t[..., np.newaxis] # 变成HxWx1,方便广播 A = np.array(A, dtype=np.float32).reshape(1, 1, 3) hazy = img * t + A * (1.0 - t) hazy = np.clip(hazy * 255.0, 0, 255).astype(np.uint8) return hazy, t[..., 0]调用时,注意几点。如果深度图是米制真实距离,把beta设为0.02左右,depth_is_metric=True。如果深度图是相对深度,可以用默认的beta=0.02,函数内部会自动放大10倍。实际效果需要根据你的数据集微调,建议先对一张图做快速预览,再决定参数范围。
3.4 批量生成训练样本的脚本设计
有了单个加雾函数,批量生成训练样本就很容易了。我的做法是先把数据集组织成这样的目录结构:
dataset/ images/ img_001.jpg img_002.jpg depths/ img_001.png img_002.png hazy/ img_001_b0.02_A1.00.png img_001_b0.05_A0.95.png对于每一张清晰图,可以生成多张不同浓度的雾图。下面是一个批量生成脚本的骨架:
import os import random import numpy as np import cv2 image_dir = "dataset/images" depth_dir = "dataset/depths" output_dir = "dataset/hazy" os.makedirs(output_dir, exist_ok=True) num_per_image = 5 # 每张原图生成几张雾图 for name in os.listdir(image_dir): if not name.lower().endswith((".jpg", ".png")): continue img_path = os.path.join(image_dir, name) depth_path = os.path.join(depth_dir, name.replace(".jpg", ".png")) if not os.path.exists(depth_path): continue image = cv2.imread(img_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) depth = cv2.imread(depth_path, cv2.IMREAD_UNCHANGED).astype(np.float32) base = os.path.splitext(name)[0] for i in range(num_per_image): # 随机采样散射系数和大气光 beta = random.uniform(0.01, 0.06) A_value = random.uniform(0.85, 1.0) A = (A_value, A_value, A_value) hazy, _ = apply_haze( image, depth, beta=beta, A=A, depth_is_metric=True ) # 保存结果,文件名带上参数,方便溯源 out_name = f"{base}_b{beta:.3f}_A{A_value:.2f}.jpg" out_path = os.path.join(output_dir, out_name) hazy_bgr = cv2.cvtColor(hazy, cv2.COLOR_RGB2BGR) cv2.imwrite(out_path, hazy_bgr)这个脚本可以单线程跑,但数据量大时建议用进程池并行,瓶颈在图像读写和深度图滤波。我通常会把num_per_image设成3到8,根据训练集的规模来定。有一点提醒:加雾生成的样本和原图标签完全一致,因为几何结构没有变,检测框和分割mask都不需要重新标注。所以你可以放心把生成图直接丢进训练集。
3.5 合成效果验证:肉眼检查与数值指标
生成完一批雾图后,千万不要直接拿去训练,先肉眼检查一遍。我会在生成目录里随机抽十几张,重点看三个地方:近处物体是否清晰,远处物体是否模糊,雾的颜色是否自然。如果场景里有一条路,应该看到近处的路缘清晰、远处的路逐渐消失在雾气里,而不是一整张图均匀发白。
除了肉眼,还可以用数值指标辅助判断。你可以把原图、生成雾图和透射率图放在一起可视化,检查透射率的空间分布是否连续。如果透射率图出现了一堆锯齿边缘,说明深度图没处理好。还有一些无参考图像质量指标,但我觉得做数据增强时不需要太依赖指标,最直接的验证方式还是做一次小规模对比实验:把模型分别在“不加雾增强”和“加雾增强”的训练集上跑一遍,再到真实雾天测试集上看效果提升。这个指标比任何合成图像的质量评分都更有说服力。
4. 常见问题与排查技巧实录
4.1 合成雾太假,模型学不到有效特征
这是加雾数据增强的高频问题。生成出来的雾图看起来像“蒙了一层白色塑料布”,模型拿到这种数据学到的往往只是亮度偏移,而不是距离感知。根因一般有三个:β值选得过大,雾浓到完全盖住了场景结构;A值偏离白色,导致严重偏色;深度图质量差,雾的过渡出现断层。
排查方法分两步。第一步,把生成图里的透射率t单独显示出来,看看是不是从前景到背景平滑衰减。如果t图出现一块亮一块暗的斑块,先处理深度图。第二步,把β调小一点,同时把A固定到接近白色,再做一组肉眼对比。不要追求“第一眼就很雾”,只要远处轮廓隐约可见、近处细节保留,这种雾图对模型来说是最有效的训练样本。
4.2 标签需要改吗?浓雾下的标注要不要保留
加雾处理不会改变目标在图像中的位置和大小,所以理论上检测框、分割mask、关键点标注都可以沿用原图。但有一个细节很容易被忽略:当雾浓到某个目标几乎不可见时,原图里那个目标对应的标签是否还有意义?
我的经验是,如果目标区域完全被雾掩盖,人眼都看不出来,留下原始标签会让模型学到“在模糊区域强行输出检测框”,反而增加误检。对于这类极端浓雾样本,建议把置信度极低的目标标签删掉,或者干脆生成时避免使用过大的β值。更稳妥的做法是保留标签,但控制浓雾样本在所有加雾样本中的比例,比如不超过20%。这样模型既见过浓雾情况,又不会过度依赖“盲猜”。
4.3 雾化后图像过暗或发红
合成雾图普遍偏暗或者颜色发黄、发红,多半是大气光A设置出了问题。A代表大气光颜色,应该是场景中“亮白色区域”的反映,而不是一个随意指定的RGB向量。如果你把A设成(0.8, 0.6, 0.6),整张图必然偏红。我在实践中基本只用灰度值相同的大气光,也就是A=(a, a, a),a在0.85到1.0之间。
另一个偏暗原因是透射率相乘之后,图像暗部被进一步压缩。t<1的时候,近景得到的衰减没那么大,暗部还好;但t很小的地方,图像整体趋向于A。如果A是接近1的白色,整体不应该过暗。如果你的雾图明显变暗,检查一下代码里有没有把深度值用反,或者A有没有被归一化到255导致数值错误。
4.4 加了雾之后模型性能反而下降
这最让人头疼。我遇到过几次,原因通常在两个方向:合成分布和真实分布差得太远,或者加雾样本在训练集里占比太高,模型过拟合到了雾噪声上。
处理方法建议如下。第一,不要把所有图片都做成雾图。原始清晰图像和加雾图像混合训练,雾图比例控制在30%到50%以内,能给模型保留足够的清晰特征。第二,在训练过程中增加参数随机化,让每一次生成的雾浓度、大气光都不一样,模型就不会死记一种“雾的风格”。第三,如果训练集里已经有少量真实雾图,可以把它们和合成雾图混合使用,让模型见识到真实的照片细节。第四,如果测试集是真实雾图,模型在合成雾图上学到的东西不一定能迁移,这时候要考虑在特征层面做域自适应,单纯堆数据可能不够。
4.5 避坑清单速查表
| 常见坑 | 表现 | 解决方案 |
|---|---|---|
| 雾图像塑料膜 | 全局均匀白,无层次 | 检查深度图,改用物理感知加雾,β调小 |
| 图像偏红/偏蓝 | 大气光RGB不平衡 | A设置成灰度标量,接近白色 |
| 目标区域完全消失 | 检测性能下降 | 控制β上限,保留浓雾样本但限量 |
| 深度图边缘断裂 | 雾的边缘有明显线条 | 对深度图做中值滤波或形态学处理 |
| 模型在雾天测试集上掉点 | 合成雾和真实雾差距大 | 混合真实雾图,域随机化,控制雾图占比 |
5. 进阶:让合成雾更贴近真实场景
5.1 用单目深度估计弥补深度缺失
很多情况下你没有现成深度图,只有大量普通RGB图像。这时可以用单目深度估计模型先跑一遍,生成相对深度图,再拿去做加雾合成。需要注意的是,单目深度估计结果往往在物体边缘不够锐利,在远处会出现整体偏移。为了不让这些误差直接体现在雾图里,生成深度后我会做一次高斯平滑,再按分位数截断异常值。
即使深度估计不完美,基于它的加雾结果也远好过全局均匀加雾,因为至少有了空间变化趋势。当然,如果你的训练任务极度依赖像素级精确对齐,还是优先找带深度传感器的数据集,或者用已知深度的开放数据集作为辅助训练材料。
5.2 多参数采样与域随机化
想让模型在真实雾天更稳,就不要让训练数据的雾长得千篇一律。我在实际训练时会把β、A、甚至深度图的缩放都做成随机变量。比如β在一个对数空间里随机采样,A在0.85到1.0之间随机,有时候还会给透射率图添加一个小的随机噪声,模拟大气湍流不均匀的感觉。
这种域随机化的思路和Sim2Real的核心逻辑是一样的:训练时见过足够多样的退化方式,模型就不会把注意力放在某种特定的“雾纹”上,而是学会抓住真正稳定的语义特征。实测下来,域随机化比固定一组参数的加雾增强效果好很多,尤其是目标在浓雾边缘的场景。
5.3 与真实雾图混合训练
合成雾再真实,和传感器捕获的真实雾图之间还是有domain gap。最有效的兜底方案,是想办法搞一批真实雾图,哪怕数量少,也混进训练集里。如果真实雾图没有标注,可以用半监督或一致性正则的方法,让模型在合成雾图上学习语义,再在真实雾图上做预测一致性约束。如果真实雾图有标注,那更好,直接作为增强数据的一小部分加入训练即可。
我在项目中一般会让真实雾图占整个训练集的5%到10%。这个比例不算高,但足以让模型在推理阶段更适应真实雾天中传感器噪声、色偏和光线细节。纯合成雾能帮你把基础性能拉起来,真实雾图负责最后一步的微调。
5.4 加雾增强在具体任务上的效果经验
以目标检测为例,模型在晴天数据上训练后直接放到雾天测试,mAP通常会出现明显下滑,尤其是中远距离小目标几乎直接消失。使用加雾增强后,一个明显的变化是模型对模糊边缘的容忍度提高了。语义分割模型会更好地区分“远处的路”和“近处的车”,不会因为整体对比度下降就把大片区域误判成同一类。
我自己比较推荐的做法是,在训练过程中把加雾增强做成在线增强的一部分,而不是全量离线生成。原因很简单:离线生成固定样本会让模型反复看到同一批雾图,在线增强每次随机参数,相当于无穷多的雾图样本。不过,离线生成也有它的价值,比如可以先人工检查质量、方便复现实验,或者在多阶段训练中单独使用。两者结合,通常比单用其中一种效果更好。
最后再分享一个小技巧
在实际操作中,我每次做加雾增强,都会先单独生成一组“透射率图预览”,而不是直接去看雾图。透射率图能让我一眼看出雾的空间分布合不合理:近处透射率接近1,远处平滑下降到0.2以下,说明参数和深度图都没问题。如果透射率图里面有一块块突变,那也不用继续调β了,先去修深度图。
另外,生成雾图的代码最好统一封装成一个模块,输入输出都保持固定格式,这样在不同项目之间切换时,只需要改参数范围,不需要改核心逻辑。加雾只是数据增强的一种手段,不要把精力全花在让雾变“好看”上,关键是让模型在真实雾天掉点更少、泛化更稳。希望这篇总结能帮你在雾天视觉任务上少走一点弯路。