1. 先搞清楚加雾到底在模拟什么物理过程
加雾不是简简单单地把图像变白、变灰、降低对比度。如果只是这样做,出来的图要么像蒙了一层塑料膜,要么像曝光过度,完全没有雾天那种“空气里有悬浮颗粒”的层次感。在做MATLAB图像合成加雾之前,我建议你先花十分钟想清楚:真实雾天里,相机传感器接收到的光到底经历了什么。
雾天成像可以拆成两部分。第一部分是场景物体本身反射的光,这部分光经过空气中悬浮水滴、尘埃的衰减,到达相机时已经削弱了一部分,对应物理里的“透射衰减”。第二部分是环境光(太阳光、天空光的散射光)直接被空气中微粒散射进相机镜头,这部分光不携带场景细节,只贡献一层叠加的光幕,对应大气散射模型里的“大气光”。最终拍到的带雾图像,是这两部分按一定比例叠加的结果。
这个物理过程用公式表达就是:
I(x) = J(x)·t(x) + A·(1 - t(x))
其中I(x)是带雾图像,J(x)是清晰无雾图像,A是大气光值,t(x)是透射率。t(x)越小,说明衰减越厉害,雾越浓;t(x)越接近1,图像越清晰。这个模型最早是McCartney在1976年提出的,后来被去雾算法广泛应用,但反过来用——给清晰图像“人工加雾”——一样成立,只是我们要主动构造J、A和t,然后把它们合回去。
这里有一个容易搞混的点。加雾不是图像合成完之后的“滤镜”,而是合成过程中的“光照合成”。你在MATLAB里用alpha通道把两张图合成一张,那是空间域的叠加;加雾是亮度域的叠加。空间域决定“哪些像素来自哪张图”,亮度域的叠加决定“这些像素最终看起来受雾影响多少”。如果你先合成再做亮度修正,边缘容易出现脏感,因为两张图的拼接边界在加雾后仍然突兀。更自然的做法是:先把场景合成干净,再基于深度或区域信息赋予不同的透射率,让雾同时影响合成后的整体画面。
我自己最常用的场景是自动驾驶仿真数据集生成。真实雨天雾天数据太难采集,而且不同路段浓度不一样,用MATLAB对合成场景批量加雾,再配合深度图做透射率分布,能快速生成几千张训练图,成本几乎为零。除此之外,遥感图像模拟、监控视频画质评估、无人机航拍仿真也会用到这个思路。
2. 环境准备与核心工具链梳理
2.1 MATLAB版本与工具箱选择
MATLAB做图像处理,最核心的工具箱是Image Processing Toolbox。加雾这件事,理论上只要会用矩阵运算就行,连工具箱都能不用,但有几个函数还是能省不少事:imread、im2double、repmat、imshow、imwrite,这些是基本功;如果要生成深度渐变、做掩膜,还会用到meshgrid、fspecial或imgaussfilt。
版本方面,我建议至少R2020a以上。倒不是说老版本跑不了,而是新版对图像类型处理更友好,比如im2double和im2gray的行为更一致,转uint8再转double不容易出现数据溢出。另外新版imageshow比旧版imshow在交互缩放上更好用,调试局部雾效果时方便很多。
如果你做的是批量数据生成,建议加一个Parallel Computing Toolbox,parfor直接并行处理几千张图,省下来的时间能让你多调好几版参数。这个工具箱不是必须,但一旦图量上来,你会感谢自己装了它。
2.2 关键函数的功能边界
先梳理一下这个项目里最常用的几个函数,别过度依赖,但要知道它们适合干什么。
imread:读图,但要注意它返回的可能是uint8、uint16或logical类型。如果你用旧版MATLAB,README里没强调类型,很容易在后续矩阵运算时炸掉。im2double:把图像归一化到[0,1]区间,同时转为double类型。这是加雾计算的基本盘,所有乘法、指数运算都在这之后进行。repmat:把灰度矩阵复制成三通道,用于和RGB图像做逐像素乘法。很多人喜欢在这用cat(3, t, t, t),效果一样,看个人习惯。meshgrid:生成x、y网格坐标,用于构造深度渐变透射率图。这是实现“近处清楚、远处模糊”的关键工具。imgaussfilt:高斯滤波。有时候直接用rand生成噪声透射率会太碎,先滤波一下,雾的分布会更自然。imwrite:导出图像。注意导出uint8格式时,记得先把double数据im2uint8,直接写double会得到全黑或奇怪的报错。
2.3 数据流设计:从输入到输出的完整链路
加雾处理的代码不长,但数据链路的顺序特别重要。我踩过最蠢的坑是:先对uint8图像直接做乘法,结果非负像素全被截断成255,输出一片白。正确链路是:
- 读入原图(uint8)
- 转double并归一化到[0,1]
- 读取或生成对应的深度图、掩膜、透射率图
- 按公式做矩阵运算,得到带雾图像(double)
- 用imshow查看效果,必要时用
imhist检查直方图 - 调整参数,输出最终图像(转回uint8后imwrite)
这六步看着简单,但每一步都有细节。第2步漏了,后面全错;第4步矩阵维度不匹配,MATLAB会直接报错;第6步不转数据类型,存下来的图会跟预览差别很大。我的习惯是在关键节点用size、class、min/max三个命令检查变量状态,尤其在debug一个莫名其妙的“图怎么变白了”问题时,这招十次里有八次能快速定位。
3. 图像合成加雾的完整代码实现
3.1 基础版:均匀雾效三步走
最简单的加雾效果,整张图同一浓度,适合模拟阴天、霾天气。代码如下:
% 读取图像并归一化 im = imread('scene.jpg'); im = im2double(im); % 设置大气光和透射率 A = [1, 1, 1]; % 白色雾 t = 0.55; % 透射率:数值越小,雾越浓 % 合成加雾图像 J = im .* t + A .* (1 - t); % 显示与保存 imshow(J); imwrite(im2uint8(J), 'scene_fog.jpg');这里A取了[1,1,1],也就是纯白光。如果你在白天薄雾场景拍过照片,会看到远处物体不是单纯变白,而是带一点灰蓝调,那是因为大气光本身也受天空散射影响。想模拟这种效果,可以把A改成[0.95, 0.95, 1]之类的偏蓝值,不同色道的衰减也就会有差异。
这个基础版有个问题:它是全局均匀加雾,没有远近层次。真实雾天里,3米外的树可能还很清晰,50米外的建筑就只剩轮廓了。全局均匀雾只适合做“霾”效果,或者素材本身景深很浅的情况。
3.2 进阶版:用深度图做渐变透射率
要让雾有纵深,关键在于让透射率t随深度变化。公式不变,t变成一张和图像同尺寸的矩阵。比如模拟一条从近到远的道路,远处雾浓:
% 读取图像 im = im2double(imread('road.jpg')); [H, W, ~] = size(im); % 构造深度矩阵:从下到上(或从近到远)线性增大 [x, y] = meshgrid(1:W, 1:H); depth = (y - 1) / (H - 1); % 归一化到[0,1],底部为0,顶部为1 % 透射率随深度指数衰减 beta = 1.2; % 雾气衰减系数 t = exp(-beta * depth); t = repmat(t, [1, 1, 3]); % 扩展成三通道 % 大气光 A = [1, 1, 1]; % 合成 J = im .* t + A .* (1 - t); imshow(J);depth的构造是这个方法的核心。你可以根据实际场景的深度关系改depth的生成方式:如果是无人机下视拍地面,可能中心近、四周远;如果是山景,可能是下方近、上方远。beta值控制衰减快慢,beta越大,远处雾越浓。
这里我补充一个物理逻辑:透射率和深度的关系是t = exp(-βd),不是线性关系。很多人会直觉上觉得线性递减就行,但真实大气散射是指数衰减。用指数,近处雾的变化比较平缓,远处急剧加深,视觉上更接近实拍雾天。
3.3 进阶版二:合成两张图后统一加雾
如果项目叫“图像合成加雾”,大概率不只处理一张图,而是把前景、背景或者其他素材合成到同一个画面,再加雾统一光照氛围。比如你要把一个人物抠出来贴到山区背景,然后模拟“清晨山区薄雾”的效果。
步骤分三段。第一段,前景背景做空间域合成:
% 读取前景和背景 fg = im2double(imread('person.png')); % 带alpha通道的PNG bg = im2double(imread('mountain.jpg')); fg_rgb = fg(:, :, 1:3); fg_alpha = fg(:, :, 4); % alpha通道 % 合成(抠图边缘可能有色差,先高斯羽化alpha边缘) fg_alpha_smooth = imgaussfilt(fg_alpha, 1.5); comp = bg .* (1 - fg_alpha_smooth) + fg_rgb .* fg_alpha_smooth;第二段,构造深度图。背景整体看作远处,前景人物看作近处。深度值需要和画面内容匹配:人物所在区域深度小(雾薄),背景区域深度大(雾浓)。
depth = zeros(size(bg_rgb, 1), size(bg_rgb, 2)); % 实际上是单通道 depth = 0.8; % 默认背景深度0.8 depth = repmat(depth, size(bg_depth, 1), size(bg_depth, 2)); % 然后用alpha掩膜把前景区域深度设小 fg_region = fg_alpha_smooth > 0.5; depth(fg_region) = 0.2;第三段,基于深度和通道做加雾合成:
beta = 0.6; t = exp(-beta * depth); t3 = repmat(t, [1, 1, 3]); A = [0.9, 0.9, 0.95]; % 偏蓝的清晨大气光 J = comp .* t3 + A .* (1 - t3);到这里你会看到,人物和背景不仅空间上拼在了一起,光照上也有了统一的雾面,像是真的站在那里拍的。边缘处因为之前做了羽化,雾的过渡也自然。
3.4 带局部雾效的掩膜法:想去哪儿去哪儿
全局浓度或者简单渐变通常还不够,很多实际需求是“只在某个区域有雾”——比如山间云雾缭绕的带状雾、清晨湖面贴地雾。这时需要一个局部掩膜来控制透射率。
构造掩膜的方法不唯一,最简单的办法是用交互式工具或手绘二值图。比如在MATLAB里可以用drawfreehand交互选区域,也可以直接用代码生成圆形、椭圆渐变:
% 图像尺寸 [H, W, ~] = size(im); [x, y] = meshgrid(1:W, 1:H); % 椭圆区域掩膜 cx = W / 2; cy = H / 2; rx = W * 0.3; ry = H * 0.15; mask = ((x - cx) / rx).^2 + ((y - cy) / ry).^2 <= 1; mask = double(mask); % 对掩膜羽化,避免边缘生硬 mask = imgaussfilt(mask, 20);之后把掩膜映射到透射率上。期望逻辑:掩膜=1的区域浓雾,=0的区域无雾。那么透射率:
t = 1 - 0.7 * mask; % 透射率从1降到0.3再乘一个整体浓度系数,就能控制局部的雾厚。我把mask和t之间的关系理解成“雾的分布图”,而不是“透射率图”,因为这样更直观:你想让雾出现在哪里,就先在mask上画哪里,然后约定mask=1表示雾最浓、透射率最低。
如果你连交互式都不想用,还可以从图像本身提取区域。比如根据亮度:天空区域通常更亮,给天空加雾,用im本身的亮度值做mask就行。这就是为什么图像合成加雾能和语义分割、显著性检测这类前置处理搭在一起。
4. 参数调优与效果对比:雾的浓度、颜色和均匀性
4.1 透射率与浓度:别只调一个系数
很多人调参时只会动t或者beta,结果要么雾太厚,细节全丢;要么雾太薄,变化不明显。关键是搞清楚每个参数的物理含义和交互关系。
透射率t的取值范围是0到1。t=1意味着完全无雾;t=0意味着物体反射光完全被阻挡,图像只剩下大气光A的颜色。实际经验是:
- t在0.8以上,只有轻微薄雾,适合做“清晨轻雾”氛围
- t在0.5~0.8,中等雾,远景轮廓还在,颜色偏灰
- t在0.2~0.5,浓雾,近景勉强能看,远景接近纯色
- t在0.2以下,几乎完全被雾覆盖,除非模拟极端天气,否则不建议
如果用的是深度渐变模型,控制的是β。β的作用是改变“雾随距离增长的速度”。β=0.3,一百米外才明显有雾;β=1.5,十米外就开始发虚。0.6~1.0是我个人最常用的区间,覆盖大部分中等雾场景。
4.2 大气光颜色:决定雾的整体色温
大气光A不再是白色时,雾的观感会发生明显变化。
- [1,1,1]:标准白雾,适用性强
- [0.95,0.95,1]:偏蓝,模拟天空散射光主导的雾
- [1,0.95,0.9]:偏黄,模拟沙尘或日落余晖下的雾
- [0.8,0.85,0.9]:灰冷调,模拟阴雨天气
调整A时要注意一个细节:A的数值会影响图像整体亮度。A=1时,雾最浓的地方亮度接近255;A=0.8时,雾区会偏灰黑,看上去像阴天。如果你的雾天图像整体偏暗,可以适当调高A。
4.3 均匀性、噪声与颗粒感
真实雾天不是绝对均匀的,空气中水汽有湍流,远处雾还会有轻微颗粒感。纯公式算出来的雾过于“干净”,像软件渲染出来的3D雾效。如果你追求照片级真实感,可以在透射率图上加一点低频噪声:
% 给透射率加随机扰动 noise = randn(size(t)); noise = imgaussfilt(noise, 30); % 低频噪声 t = t .* (1 + 0.08 * noise); t = min(max(t, 0), 1);这里有两个关键点:扰动系数不能大,0.05~0.1比较合适,否则雾会出现一块浓一块淡的斑驳;噪声必须高斯滤波,滤掉高频成分,才能模拟大尺度空气湍流而不是感光元件的噪点。
4.4 对比试验:同一张图在不同参数下的结果
我用一张标准测试图(比如住宅街景)做了几步对比,结果如下表:
| 参数组合 | 视觉效果 | 适用场景 |
|---|---|---|
| t=0.85, A=[1,1,1] | 极淡的白薄雾,整体亮度和对比度略微下降 | 阴天镀膜感 |
| t=0.6, A=[1,1,1] | 常见雾天,近景清晰,远景有点灰 | 自动驾驶黄昏数据 |
| t=0.6, A=[0.9,0.9,1] | 中等灰蓝雾,莫名有“寒气” | 冬季清早、山区 |
| depth渐变, beta=1.0 | 近处清,远处白,纵深感强 | 道路监控、航拍 |
| 局部mask, 最大雾厚0.7 | 想去哪去哪,画面叙事感强 | 艺术视角、游戏CG |
从这个表可以看出,参数是组合拳,不能只看单一数值。我调参的顺序一般是:先定透射率下限(雾最浓程度),再定大气光颜色(色温),最后加噪声和边缘处理。反过来调容易陷入局部最优,怎么看都差点意思。
5. 实测中容易踩的坑:完整排查链路
5.1 图像突然全白或全黑的排查思路
这个问题新手遇到概率极高。排查链路如下:
第一步,看变量类型。MATLAB命令行输入class(im),如果是uint8,而你的代码里直接做了im .* 0.5,那结果会被截断成0~255的整数,明亮区域会直接变成127附近,暗部变成0以下直接被钳到0。正确做法是先用im2double。
第二步,检查大气光A的维度。size(A)应该是1×3,如果是单个数,广播的时候MATLAB可能报维度不匹配。
第三步,检查透射率t的维度。如果是全局标量,没问题;如果是矩阵,确保size(t)和size(im(:,:,1))一致。矩阵维度对不上时,MATLAB会报错,但有一种隐蔽情况是t是H×W×1,图像是H×W×3,此时用.*可能会报错,需要用repmat扩展。
第四步,查看中间结果的min/max。如果max(im)>1或min(im)<0,说明在double域的计算出了问题(比如没归一化就做指数运算)。
5.2 边缘色差和光晕怎么处理
做前景背景合成再加雾,人物轮廓边缘常出现两种问题:一是边缘有半透明色边,二是加雾后在轮廓周边形成一圈光晕。
先说色边。前景图如果是JPEG抠图,边缘残留背景颜色,和背景合成后,色边在加雾后会被放大。解决办法是在合成前对alpha通道做轻微侵蚀或羽化,削减半透明边缘:
alpha = im2double(fg(:, :, 4)); alpha_eroded = imerode(alpha, strel('disk', 1)); alpha_smooth = imgaussfilt(alpha_eroded, 1.2);再说光晕。加雾时,透射率t在边缘突变,导致雾的强弱在边缘跳变,看起来像边缘轮廓发光。解决办法是平滑透射率的过渡区域。如果把注意力放在mask上,它和alpha边缘重叠的地方需要用较大标准差的高斯平滑,比如imgaussfilt(mask, 20),让过渡带延展到几十个像素,光晕就消失得无影无踪。
5.3 批量处理时的图形窗口内存溢出
如果你用imshow在循环里预览效果,跑了几百张图后MATLAB可能变慢甚至“Out of memory”。这不是加雾计算的问题,是图形句柄没释放。处理办法:显示后立即close(gcf),或者干脆在批量模式里不预览,只保存到文件。我通常在循环外用figure单独调试一张,确认无误后再全量跑。
5.4 不同格式图像的位深和色彩空间
MATLAB里如果输入图像是16位TIFF或浮点HDR,直接转double后可能超出[0,1]范围。HDR图像可能值远大于1,这时加雾公式里的指数项可能失去意义。统一先做亮度归一化:im = im ./ max(im(:)),再进入加雾流程。
另外注意RGB和YCbCr差异。如果直接在Lab或HSV空间做加雾,效果会有些微妙不同。我的建议是:默认在RGB空间做,因为大气散射模型本身就是基于RGB三通道的独立衰减。想调整色彩氛围,可以在加雾前后增加颜色映射变换,而不要轻易在Lab空间重写公式——除非你明确知道自己在做什么色彩管理工作。
6. 扩展思路与二次开发方向
6.1 把加雾变成数据增强管道
合成加雾最实用的大方向,是给深度学习目标检测或分割模型做训练数据增强。思路很直接:已有清晰无雾的数据集,通过加雾生成带雾样本,增强模型在恶劣天气下的鲁棒性。
具体落地时,有几个经验可以分享:
- 每张原图生成多种雾浓度版本,比如透射率从0.9到0.3均匀采样,不要只输出一种浓度
- 使用随机深度图,避免同一位置雾厚模式固定
- 对mask做随机形态学操作,模拟不同风向、地形造成的雾分布差异
- 配合翻转、旋转做联合变换,保证加雾后的图像和标签一一对应
我在实际项目中用这种方式生成过两万张带雾市政道路图,配合YOLO系列模型训练,雨天雾天的检测准确率提升了将近10个百分点。成本只是几百行MATLAB代码和数据集的整理时间。
6.2 结合真实深度传感器数据做物理仿真
如果你手边有激光雷达或深度相机输出的深度图,加雾质量会再上一个台阶。用真实深度d替换前面代码里的简化depth,然后一步到位:
t = exp(-beta * d_normalized);这个做法能根据场景真实的几何关系,让雾精确地出现在楼后、树后、街道深处,而不是靠手工画mask。做三维重建、城市级数字孪生的朋友,通常会对这个思路感兴趣。
6.3 做去雾算法的“反向验证”
还有一个很少人提但很有意思的场景:加雾可以当作去雾算法的测试工具。你想验证一个新的去雾算法好不好,与其找大量真实雾天图像(很难有Ground Truth),不如自己在清晰图上加雾,再把带雾图交给去雾算法,最后和原始清晰图对比,PSNR、SSIM直接算。这样整个闭环都掌握在自己手里,能够方便地验证算法对不同浓度、不同颜色大气光的适应能力。
顺便说一句,如果你以后想从“加雾”转向“去雾”,MATLAB自带的imreducehaze函数其实就是上面公式的逆过程,但它内部做了不少自动估计大气光和透射率的优化。你自己写的加雾过程正好可以用来设定对照实验,帮助理解imreducehaze内置参数的作用。
6.4 用App Designer做可视化调参工具
最后补一个工程化的思路。如果你经常需要给别人演示或做非编程人员可用的工具,可以考虑用MATLAB App Designer做一个小的加雾工具界面:左边选择图像,中间实时预览,右边拖动滑块控制透射率、大气光RGB、噪声幅度。底层逻辑还是这套公式,只是包了一层交互壳。我自己给实验室学生做过一个,他们拿去生成数据集,反馈说比命令行直观太多,也少了改参数改错行列的烦恼。
核心代码其实没变,只是把每个输入都绑定成控件回调函数里的变量。App Designer关于图像显示的坑,集中在imshow在坐标轴控件里的句柄更新,记得每次刷新前cla清空当前坐标轴。其他的和普通脚本没差别。