
1. 从“雾里看花”到“拨云见日”图像去雾增强的工程价值做图像处理的朋友估计都遇到过这种头疼事辛辛苦苦采集回来的户外图像因为雾、霾、烟尘的影响整个画面灰蒙蒙一片对比度低色彩失真关键细节完全被“吃掉”了。无论是做自动驾驶的环境感知还是做安防监控的目标识别甚至是无人机航拍的地物分析这种退化图像直接扔给算法效果都得大打折扣。这就像你戴着一副沾满灰尘的眼镜去看世界再好的视力也白搭。图像去雾增强干的就是“擦亮眼镜”的活儿目标是把被大气散射“污染”的图像尽可能地恢复出清晰、真实的场景。为什么说它重要因为这不是简单的调个对比度、拉个曲线就能解决的。雾霾导致图像退化的物理过程是复杂的它让场景的反射光在到达相机传感器之前就被大气中的悬浮颗粒散射和吸收了。这直接导致了两个核心问题一是场景的固有亮度我们真正想看到的被严重衰减二是环境光主要是天空光被大量混入使得整幅图像像是蒙上了一层灰白色的“面纱”并且随着物体距离的增加这种效应越明显。所以一个有效的去雾算法必须能同时估计并去除这层“面纱”我们称之为大气光幕并补偿因衰减而损失的场景反射光。在MATLAB这个强大的数学计算和原型验证平台上实现并深入理解这个过程对于我们掌握图像复原的核心思想具有极高的实践价值。2. 暗通道先验一个反直觉却极其有效的物理洞察提到单幅图像去雾几乎绕不开何恺明博士等人提出的“暗通道先验”Dark Channel Prior。我第一次看到这个理论时感觉非常巧妙甚至有点反直觉。它的核心思想基于一个对大量户外无雾图像的统计观察在绝大多数非天空的局部区域里至少存在一个颜色通道R, G, B的某些像素值非常低接近于0。换句话说一块颜色鲜艳的草地它的红色或蓝色通道里总有些地方很暗一堵红色的墙它的绿色或蓝色通道里也总有暗点。这个“暗”的强度就被称为该区域的暗通道值。为什么这个观察能用于去雾这就要回到大气散射模型了。经典的模型描述如下我们观测到的有雾图像I(x)可以看作是清晰图像J(x)经过大气衰减后再加上大气光A的混合结果。用公式表示就是I(x) J(x) * t(x) A * (1 - t(x))这里x是像素位置t(x)是透射率表示光线能穿透大气到达相机的比例。t(x)越接近1表示该处雾越少越接近0表示雾越浓物体几乎被大气光A淹没。暗通道先验的精妙之处在于它假设对于清晰图像J(x)其暗通道值J_dark(x)趋近于0。把这个假设代入上面的模型经过一番推导这里不展开复杂公式我们就可以从一个非常简单的操作——求取有雾图像I(x)的暗通道图——来估计出透射率t(x)的粗略值。具体操作是对图像中每个像素点取其一个小邻域比如15x15的窗口在这个窗口内分别看R、G、B三个通道的最小值然后再在所有通道的这个最小值里取最小值。这样得到的就是暗通道图像。你会发现这幅图里天空区域和浓雾区域的亮度很高因为三个通道的最小值都不小而物体纹理、边缘区域则很暗。注意暗通道先验在天空区域或大面积白色物体如雪地、白墙上会失效因为这类区域本身的暗通道值就不低算法会误判这里有浓雾。这是该方法的固有局限性在实际应用中必须加以处理。3. 动手实现在MATLAB中一步步构建去雾流程理论说得再多不如动手跑一遍。下面我们就在MATLAB里从零开始实现一个基于暗通道先验的基础去雾流程。我会把每一步的代码意图和关键参数都解释清楚。3.1 环境准备与图像读入首先我们准备好工作环境。确保你的MATLAB路径设置正确将要处理的图像放在当前工作目录或指定路径下。% 1. 清空环境关闭所有窗口确保干净的工作区 clear all; close all; clc; % 2. 读入有雾图像 % 替换 ‘foggy_image.jpg’ 为你的图像文件名 img_hazy imread(‘foggy_image.jpg’); % 将图像转换为双精度浮点数方便后续计算并将值域归一化到[0, 1] img_hazy im2double(img_hazy); figure; imshow(img_hazy); title(‘原始有雾图像’);这里使用im2double转换至关重要。如果直接用uint8类型0-255做运算容易在乘除操作中溢出或损失精度。归一化到[0, 1]区间符合物理模型的数学表达。3.2 计算暗通道图接下来我们实现暗通道的计算函数。这是整个算法的核心步骤之一。function dark_channel get_dark_channel(image, patch_size) % 计算输入图像的暗通道 % 输入 % image: 归一化到[0,1]的RGB图像 (m x n x 3) % patch_size: 局部窗口的尺寸奇数如15 % 输出 % dark_channel: 暗通道图像 (m x n) [height, width, ~] size(image); % 对每个颜色通道取最小值得到一个初步的(m x n)矩阵 min_channel min(image, [], 3); % 为了进行最小值滤波我们需要对图像边界进行填充。 % 这里采用‘replicate’方式复制边缘像素避免边界效应。 pad_size floor(patch_size / 2); min_padded padarray(min_channel, [pad_size, pad_size], ‘replicate’); dark_channel zeros(height, width); % 遍历每个像素在其邻域内取最小值 for i 1:height for j 1:width % 提取以(i,j)为中心的局部窗口 patch min_padded(i:ipatch_size-1, j:jpatch_size-1); % 取窗口内的最小值作为该像素的暗通道值 dark_channel(i, j) min(patch(:)); end end end调用这个函数来计算暗通道% 3. 计算暗通道图 patch_size 15; % 局部窗口大小这是一个关键参数 dark get_dark_channel(img_hazy, patch_size); figure; imshow(dark); title(‘暗通道图像’); colormap(‘jet’); colorbar; % 用颜色条更直观地显示数值大小你会看到一幅灰度图颜色越亮越接近黄色/白色表示该区域暗通道值越高即雾浓度估计越浓。天空区域通常会非常亮。3.3 估计全局大气光A大气光A通常被假设为一个全局常量代表图像中最“白”、雾最浓区域的光强。一种稳健的估计方法是从暗通道图中选取最亮的0.1%的像素然后回到原始有雾图像中找到这些像素位置对应的最亮的像素值通常取RGB三个通道的最大值再取这些值的前几位比如前1%的平均值作为A的估计。更简单直接的方法是取这些像素点的RGB值的平均值或最大值。% 4. 估计全局大气光 A % 将暗通道图拉成列向量 dark_vector dark(:); % 获取暗通道值最高的前0.1%的像素的索引 num_pixels height * width; [~, indices] sort(dark_vector, ‘descend’); top_indices indices(1:round(num_pixels * 0.001)); % 取前0.1% % 初始化大气光A A zeros(1, 3); % 遍历RGB三个通道 for ch 1:3 channel img_hazy(:,:,ch); channel_vector channel(:); % 取这些最亮像素点在原始图像对应通道上的强度并求平均值 A(ch) mean(channel_vector(top_indices)); end % 也可以尝试取最大值A(ch) max(channel_vector(top_indices)); disp([‘估计的大气光 A [‘, num2str(A), ‘]’]);这里选择前0.1%是一个经验值旨在确保选取的点确实来自雾最浓的区域通常是天空。如果图像中没有明显的天空这个方法可能估计不准这是需要根据实际场景调整的地方。3.4 估算透射率图 t(x)根据暗通道先验和推导公式透射率t(x)可以初步估计为t_est(x) 1 - omega * min_{c∈{r,g,b}} ( I^c(x) / A^c )其中omega是一个引入的参数0omega1用于保留少量的雾使得复原后的图像看起来更自然避免在景深很大的场景下产生不真实感。通常取0.95。% 5. 估算粗透射率图 omega 0.95; % 去雾强度参数通常0.9~0.95 % 为了避免除以零将A的最小值设一个下限 A_rep reshape(A, 1, 1, 3); % 将A变为1x1x3以便广播运算 transmission_est 1 - omega * get_dark_channel(img_hazy ./ A_rep, patch_size); figure; imshow(transmission_est); title(‘粗透射率图’); colorbar;注意img_hazy ./ A_rep这一步是对图像进行归一化将大气光的影响部分去除。计算出的transmission_est图亮的地方表示透射率高雾少暗的地方表示透射率低雾浓。3.5 透射率图的精细化与引导滤波上一步得到的透射率图非常粗糙充满了块状效应因为用了15x15的窗口。直接用它去雾会导致复原图像在物体边缘出现光晕halo效应。因此必须对透射率图进行精细化Matting/Refinement。何恺明原文中使用了软抠图Soft Matting但计算量巨大。后来他们又提出了引导滤波Guided Filter在效果和速度上取得了很好的平衡成为目前更常用的方法。幸运的是MATLAB的Image Processing Toolbox从R2014a开始就内置了imguidedfilter函数。我们用它来优化透射率图。% 6. 使用引导滤波精细化透射率图 % 引导图像使用原始有雾图像的灰度图因为它与透射率图在边缘上具有强相关性 guide_image rgb2gray(img_hazy); transmission_refined imguidedfilter(transmission_est, guide_image, ‘NeighborhoodSize’, 40, ‘DegreeOfSmoothing’, 0.001); % 参数说明 % ‘NeighborhoodSize’: 滤波窗口半径影响平滑程度。 % ‘DegreeOfSmoothing’: 平滑参数值越小越保持边缘。 % 这两个参数需要根据图像调整。通常窗口大一些如40平滑参数小一些能在平滑块效应的同时保持边缘。 % 限制透射率在合理范围避免过小导致分母溢出 t0 0.1; % 透射率下限阈值 transmission_refined max(transmission_refined, t0); figure; subplot(1,2,1); imshow(transmission_est); title(‘粗透射率图有块效应’); subplot(1,2,2); imshow(transmission_refined); title(‘引导滤波后的精细透射率图’);经过引导滤波后你会发现透射率图变得平滑了但物体的边缘依然清晰。对比左右两图块状效应基本被消除。3.6 根据大气散射模型复原清晰图像现在我们有了精细化的透射率图t(x)和估计的大气光A就可以直接套用大气散射模型的逆过程来恢复清晰图像J(x)了。公式如下J(x) ( I(x) - A ) / t(x) A注意这里的运算是逐像素、逐通道进行的。% 7. 复原清晰图像 J zeros(size(img_hazy)); for ch 1:3 J(:,:,ch) (img_hazy(:,:,ch) - A(ch)) ./ transmission_refined A(ch); end % 由于计算可能导致数值溢出需要将结果限制在[0,1]范围内 J max(min(J, 1), 0); figure; imshow(J); title(‘去雾复原图像’);到这一步一个基础的去雾流程就完成了。你应该能看到一幅对比度显著提升、细节更清晰的图像。但是事情还没完。4. 效果优化与后处理让结果更上一层楼直接复原的图像有时会显得颜色暗淡或者在某些区域特别是原本雾就很薄的地方出现颜色过饱和。这是因为模型是理想的而实际的大气散射和相机成像过程更复杂。因此我们通常需要一些后处理来优化视觉效果。4.1 自动对比度与色彩拉伸复原后的图像动态范围可能没有被充分利用。我们可以使用imadjust函数或自适应直方图均衡化来增强对比度。% 8.1 全局对比度拉伸 (简单有效) J_contrast imadjust(J, stretchlim(J, 0.01), []); % 拉伸1%的像素到全范围 figure; imshow(J_contrast); title(‘去雾后对比度拉伸’); % 8.2 使用CLAHE限制对比度自适应直方图均衡化处理亮度通道 % 转换到HSV或Lab色彩空间只对亮度通道进行增强避免颜色失真 J_hsv rgb2hsv(J); L J_hsv(:,:,3); % 提取亮度通道Value % 对亮度通道应用CLAHE L_enhanced adapthisteq(L, ‘ClipLimit’, 0.02, ‘NumTiles’, [8 8]); J_hsv(:,:,3) L_enhanced; J_clahe hsv2rgb(J_hsv); figure; imshow(J_clahe); title(‘去雾后CLAHE亮度增强’);stretchlim(J, 0.01)会自动忽略最亮和最暗各0.5%的像素总共1%以其为边界进行线性拉伸能有效抵抗噪声影响。CLAHE则是一种更高级的局部对比度增强方法尤其适合处理光照不均的图像。4.2 处理天空区域的色偏与噪声在天空区域由于透射率估计可能不准确暗通道先验失效复原公式(I-A)/t中的分母t可能非常小导致该区域被过度放大产生严重的颜色失真、色偏和噪声放大。一个常见的补救措施是在天空或高雾浓度区域对复原结果和原始图像进行融合。% 8.3 天空区域保护处理简易版 % 识别天空/浓雾区域透射率很低且亮度很高的区域 is_sky transmission_refined 0.2; % 透射率阈值 % 对识别为天空的区域使用原始图像和复原图像的加权混合 blend_weight 0.7; % 原始图像权重 J_blended J; for ch 1:3 channel_blend blend_weight * img_hazy(:,:,ch) (1-blend_weight) * J(:,:,ch); J_blended(:,:,ch) channel_blend .* is_sky J(:,:,ch) .* (~is_sky); end figure; imshow(J_blended); title(‘去雾后天空区域保护’);这个处理非常直观在算法可能失效的区域我们选择相信一部分原始图像的信息避免出现诡异的颜色块。4.3 锐化与细节增强去雾过程本身会恢复一些细节但有时为了视觉效果可以适当进行锐化。% 8.4 轻度锐化突出边缘细节 J_sharpened imsharpen(J_clahe, ‘Amount’, 0.8, ‘Radius’, 1, ‘Threshold’, 0); figure; imshow(J_sharpened); title(‘去雾CLAHE锐化最终效果’);imsharpen的参数需要谨慎调整。Amount控制强度Radius控制边缘检测的尺度Threshold决定多大反差的边缘才被锐化0表示全部。通常小半径1-2、中等强度0.5-1效果较好避免引入过多噪声。5. 参数调优与效果评估没有银弹只有权衡写到这里你可能已经意识到这个流程里充满了需要手动调整的参数暗通道的patch_size、大气光选取的百分比、去雾强度omega、引导滤波的窗口大小和平滑参数、透射率下限t0、后处理的各项系数等等。没有一组参数能通吃所有图像。调参的过程本质上是在“去雾彻底度”、“细节自然度”、“噪声抑制”和“计算效率”之间做权衡。如何评估去雾效果对于单幅图像没有绝对真实的清晰图作为参考主观视觉评价往往是最直接的。但也有一些客观指标可以参考尽管它们不一定完全可靠信息熵Entropy去雾后的图像应该包含更多信息信息熵通常会增大。平均梯度Average Gradient反映图像的清晰度和纹理丰富程度去雾后应显著提高。对比度Contrast全局或局部对比度应有提升。无参考图像质量评价NR-IQA如BIQI、NIQE等指标可以量化评估图像的自然感。在MATLAB中可以方便地计算前三个指标% 计算并对比原始图与最终结果的信息熵和平均梯度 entropy_original entropy(rgb2gray(img_hazy)); entropy_result entropy(rgb2gray(J_sharpened)); fprintf(‘原始图像信息熵: %.4f\n’, entropy_original); fprintf(‘结果图像信息熵: %.4f\n’, entropy_result); % 计算平均梯度一种简单的清晰度度量 grad_original mean(mean(imgradient(rgb2gray(img_hazy)))); grad_result mean(mean(imgradient(rgb2gray(J_sharpened)))); fprintf(‘原始图像平均梯度: %.4f\n’, grad_original); fprintf(‘结果图像平均梯度: %.4f\n’, grad_result);通常一个成功的去雾entropy_result和grad_result都会大于原始值。但要注意过度增强也会导致指标虚高但图像看起来不自然。所以“看起来舒服”永远是第一标准。6. 实战中的坑与应对策略在我自己实现和调试去雾算法的过程中踩过不少坑这里分享几个最常见的坑一天空区域出现严重色斑或过度增强。现象复原后的天空不是均匀的蓝色或白色而是出现紫红色、绿色等奇怪色块或者噪点极其明显。根因暗通道先验在天空区域失效。天空的暗通道值本来就高算法会误认为这里雾很浓透射率t估计得非常小导致(I-A)/t计算时分母极小将微小的噪声和颜色差异剧烈放大。应对大气光A的准确估计确保选取的像素点确实来自最亮的雾区而不是某个白色物体。可以尝试结合位置信息如优先选取图像上半部分的亮像素。透射率下限t0设置一个合理的下限如0.1防止t值过小。这相当于承认“有些地方的雾是无法完全去除的”虽然保守但能保证稳定性。天空区域融合如4.2节所述对识别出的天空区域将复原结果与原始图像按权重混合。使用更鲁棒的透射率估计方法后期有很多改进算法如基于颜色衰减先验、基于卷积神经网络的方法对天空的处理更好。坑二去雾后图像整体偏暗或发灰。现象雾是去掉了但整张图像蒙了一层灰色不通透。根因大气光A估计偏小或者透射率t(x)整体估计偏大omega太小导致复原公式J (I-A)/t A中加回的大气光A占比过重。应对检查大气光A打印出A的值。对于一幅正常光照的有雾图像A的三个分量通常都比较高例如 0.8。如果A值很低如0.5说明估计可能错误尝试调整选取亮像素的百分比。调整omega参数适当增大omega如从0.95调到0.98让公式中1-omega*...计算出的透射率t更小一些这样去雾力度会更强。后处理对比度拉伸这是最直接有效的方法。使用imadjust或histeq进行全局或自适应对比度调整可以极大改善视觉效果。坑三物体边缘出现白色或黑色光晕Halo Effect。现象在景深突变的地方如树木 against 天空物体轮廓外有一圈不自然的亮边或暗边。根因粗透射率图的块效应block artifacts导致。在计算暗通道时使用的局部窗口如15x15跨越了前景和背景导致前景边缘的透射率被背景的高雾浓度“污染”而估计偏高。应对精细化Matting是关键务必对粗透射率图进行引导滤波或软抠图处理。这是消除块效应和光晕的最重要步骤。调整引导滤波参数减小‘DegreeOfSmoothing’可以更好地保持边缘适当增大‘NeighborhoodSize’可以更好地平滑块内部。需要在平滑和保边之间找到平衡点。尝试更小的patch_size在计算暗通道时使用更小的窗口如5x5或7x7可以减少块效应但可能会使透射率图噪声更大需要更强的后续平滑。坑四算法速度太慢尤其是处理大图时。现象处理一张几百万像素的图片需要几十秒甚至几分钟。根因原始暗通道计算双循环遍历每个像素的邻域和软抠图复杂度很高。引导滤波虽然快很多但对于超大图像仍可能成为瓶颈。应对向量化与预计算暗通道计算可以用形态学腐蚀imerode操作来加速因为取局部最小值等价于用最小值滤波器进行腐蚀。MATLAB中imerode函数经过高度优化。% 加速的暗通道计算 min_channel min(img_hazy, [], 3); se strel(‘square’, patch_size); % 创建结构元素 dark_fast imerode(min_channel, se); % 形态学腐蚀等价于最小值滤波图像下采样可以先对图像进行下采样如缩放到原图的1/2在低分辨率上估计透射率和大气光然后再上采样回原图大小。由于透射率图本身是低频平滑的这样做损失不大但能极大提升速度。使用MATLAB内置优化函数确保使用了Image Processing Toolbox里优化过的函数如imguidedfilter。考虑C/C混合编程或GPU加速对于实时性要求高的应用核心循环部分可以用MEX函数C/C重写或者利用Parallel Computing Toolbox进行GPU并行计算。7. 超越暗通道其他去雾思路与MATLAB资源暗通道先验是里程碑但绝非终点。在实际项目中你需要根据具体场景选择或融合不同方法。1. 基于物理模型优化的方法颜色衰减先验通过观察发现雾的浓度与场景亮度、饱和度之间的差值有某种关系。这种方法有时能更好地处理天空和白色物体。融合多尺度信息在多个尺度上估计透射率然后融合能更好地处理不同大小的纹理和边缘。2. 基于深度学习的方法这是当前的主流研究方向。你可以使用MATLAB的Deep Learning Toolbox来加载和运行预训练的去雾模型如AOD-Net, DehazeNet, FFA-Net等。通常步骤是下载模型权重.mat或.onnx格式使用importONNXFunction或load加载网络然后用predict函数进行推理。深度学习方法在复杂场景下的泛化能力和细节恢复上往往优于传统方法但需要足够的训练数据且模型可解释性较差。3. MATLAB中的现成工具与APPImage Processing Toolbox Functions除了我们手动实现的可以探索imreducehaze函数。这是MATLAB官方提供的去雾函数内部实现了多种算法包括暗通道和物理模型增强使用起来非常方便一键调用效果稳定适合快速验证和集成。Color Thresholder APP对于雾浓度分布不均的图像有时可以先通过颜色阈值分割出浓雾区域进行局部处理。Image Batch Processor如果你有成百上千张图像需要处理可以用这个APP进行批处理自动化整个去雾流程。我个人在项目中的经验是对于快速原型验证和算法理解手动实现暗通道算法非常有价值对于工程部署和追求稳定效率优先考虑imreducehaze或集成成熟的深度学习模型。理解原理能帮助你在现成工具效果不佳时知道该从哪个参数入手进行调整。最后图像去雾没有“完美”的解它是在不完整信息下进行的最优估计。不同的场景浓雾、薄雾、室内烟尘、不同的拍摄设备手机、监控摄像头、卫星都需要微调策略。最好的学习方式就是拿你自己遇到的有雾图片用上面的代码跑一遍然后耐心地调整每一个参数观察图像的变化感受每个参数背后的物理和数学意义。这个过程比你读十篇论文收获都大。