简介:何凯明去雾算法的MATLAB实现包,面向图像处理入门者、计算机视觉研究者及课程设计人员,解决雾天图像清晰化与算法复现问题。资源基于大气散射模型,覆盖核心去雾算法、引导滤波细化及盒滤波快速处理等关键环节,并配有GUI交互界面与多张不同雾度测试图,方便直观调整参数、对比恢复效果。压缩包共12个文件,以4个M脚本(含主程序、引导滤波、盒滤波、界面逻辑)为主,辅以JPG/PNG测试图、MATLAB FIG界面文件和PPT讲解文稿,整体15.42MB,结构清晰。已有763人学习,适合作为理解何凯明算法、开展图像去雾实验或完成相关课程设计的参考工具。凭借这套完整程序包,读者不仅能复现经典流程,还能借助配套界面与演示文稿快速掌握思路修改与效果评估方法。
1. 暗通道先验为什么是去雾算法的默认起点:何凯明图像去雾的破局点
在 matlab 图像处理大作业和各类视觉项目的预处理环节,何凯明图像去雾算法几乎是绕不开的复现对象。它发布于 2009 年,核心主张简单到反直觉:无雾图像的局部暗通道亮度趋近于零,而雾度会线性抬高这一数值。于是,透射率不再需要多帧或深度信息,直接从单张图就能估算出来。这个算法真正的贡献并不是提出了某个新滤波器,而是把一个欠定物理模型用一条统计规律压出了可解性。这里按从业者验证算法的路径来写:先梳理大气散射模型和暗通道先验的推导,再给出可运行的 MATLAB 实现,接着讨论参数调节与先验失效边界,最后讲怎么验证效果。适合需要用 MATLAB 快速出结果、又要能说清原理的人。
2. 大气散射模型与暗通道先验:何凯明去雾公式怎么一步步落地
2.1 大气散射模型:一张雾图等于场景衰减加环境光
去雾算法普遍建立的物理基础是大气散射模型:
$I(x) = J(x) \cdot t(x) + A(1 - t(x))$
其中 $I(x)$ 是相机接收到的有雾像素,$J(x)$ 是场景原本的辐射亮度,$t(x)$ 是透射率,$A$ 是全局大气光。直观理解:光线从物体表面到相机,一部分被空气中粒子散射掉,强度衰减为 $J \cdot t$;同时粒子散射的环境光叠加进来,构成 $A(1-t)$。透射率与景深满足 $t(x)=e^{-\beta d(x)}$,$\beta$ 是散射系数,$d$ 是深度,所以远处场景通常更雾。
把 $A$ 当成全局常量是一种简化,严格说并不总是成立,尤其在室内彩色光源场景;但常量假设带来的求解稳定性明显超过误差,所以几乎所有工程实现都保留这一假设。参数含义值得先固化下来,调试时对照着看:
| 符号 | 含义 | 取值范围 | 在图像上的表现 |
|---|---|---|---|
| $I$ | 观测到的有雾图像 | $[0,1]$ | 整体灰白、对比度低 |
| $J$ | 无雾场景辐射 | $[0,1]$ | 待恢复的清晰图 |
| $t$ | 透射率 | $(0,1]$ | 越小雾越浓 |
| $A$ | 大气光 | 常量 | 决定雾的基础色板 |
只要把 $J$、$t$、$A$ 求出来,问题就算解决。但 $I$ 已知时,每像素有 3 个通道方程、却要解 $J$ 的 3 个通道加 $t$ 共 4 个未知数,天然欠定。所以算法真正的分水岭在于:用什么额外约束把这个方程封闭。
2.2 暗通道先验:一句关于彩色图像的统计规律
何凯明的破局点是暗通道先验。对任意无雾自然图像,定义暗通道为最小颜色通道再做局部最小值滤波:
$J^{dark}(x) = \min_{c \in {r,g,b}} \left( \min_{y \in \Omega(x)} J_c(y) \right)$
统计结果显示,对于非天空区域,$J^{dark}(x)$ 几乎处处趋近于 0。原因很好理解:彩色物体总存在某个通道反射率低;阴影区域所有通道都暗;暗色表面整体就暗。换句话说,在每个局部窗口里,RGB 三通道至少有一个出现极小值的概率非常高。用 MATLAB 很容易验证:
img = im2double(imread('clear.png')); % 读一张无雾图 min_rgb = min(img, [], 3); % 逐像素取RGB最小值 patch = 15; dark_clear = ordfilt2(min_rgb, 1, ones(patch)); % 局部最小值滤波 histogram(dark_clear(:)); % 检查数值分布代码说明:min(img, [], 3)沿第三维取最小值,得到单通道图;ordfilt2做顺序统计滤波,第 2 个参数设为 1 表示取窗口内最小值,等价于滑动窗口最小值滤波。运行后如果直方图峰值压在 0 附近,就说明先验对这张图成立。雾气会引入白光叠加,直接抬高暗通道数值,这正是后续估计透射率的依据。
这个验证步骤建议保留在工程里,换数据集时先跑一次,确认先验是否成立。如果暗通道直方图峰值已经在 0.2 以上,说明画面本身有大量灰白区域,后续直接套用原公式很容易出事。注意,暗通道先验的统计基础依赖彩色自然图像;灰度图、红外图、水下偏色严重的图,通道数量退化或响应失衡,暗通道不再趋近零,公式推导就塌了。在 matlab 图像处理实践里,有人把监控灰度视频直接套暗通道,结果要么过曝要么出现伪彩色,第 4 章会专门谈失效边界。
2.3 透射率推导:把先验塞回模型
假设局部窗口内透射率恒定,对模型两侧同时取暗通道。因为 $A$ 是常量向量,做逐通道除法后可得:
$\min_{c}\min_{\Omega} \frac{I_c}{A_c} = 1 - t(x) \cdot \left(1 - \min_{c}\min_{\Omega} \frac{J_c}{A_c}\right)$
在暗通道先验成立的区域,右侧第二个因子趋近 1,于是得到透射率估计:
$t(x) = 1 - \min_{c}\min_{\Omega} \frac{I_c(x)}{A_c}$
实际操作中会给 $t$ 乘一个系数 $\omega$,论文默认 $\omega=0.95$。原因是完全去除所有雾会让图像塑料感过重,保留 5% 的薄雾能让景深感更自然。大气光 $A$ 的估计也依赖暗通道:先取暗通道最亮的前 0.1% 像素,再到原图这些位置里找亮度最大值作为 $A$,避免直接取全图最亮像素时选中白色车或灯。到这里,模型、先验和估计路径已经闭环,下一步就是把这三个步骤翻译成 MATLAB 函数。
3. 在MATLAB实现暗通道去雾:从暗通道算到透射率恢复的五个函数
3.1 主函数与参数结构
写一个入口函数把流程串起来,参数用结构体传入,方便后续批量调参。各模块职责先列出来:
| 函数名 | 输入 | 输出 | 职责 |
|---|---|---|---|
dehaze_dark_channel | 雾图、prm | 去雾图 | 主流程编排 |
dark_channel | 图像、patch | 暗通道 | 透射率与大气光的共同基础 |
estimate_atmospheric_light | 图像、暗通道 | A(1×3) | 大气光估计 |
guided_filter | 引导图、透射率、r、eps | 细化透射率 | 边缘保持平滑 |
recover_scene | 图像、t、A、t0 | 去雾图 | 模型反解 |
function J = dehaze_dark_channel(img, prm) if nargin < 2 prm = struct('patch', 15, 'omega', 0.95, 't0', 0.1, ... 'r', 40, 'eps', 1e-3); end img = im2double(img); % uint8转double[0,1] dark = dark_channel(img, prm.patch); % 步骤1:暗通道 A = estimate_atmospheric_light(img, dark); % 步骤2:大气光 A = reshape(A, 1, 1, 3); % 变成1x1x3便于按通道除法 t = 1 - prm.omega * dark_channel(img ./ A, prm.patch); % 步骤3:透射率 t_refined = guided_filter(rgb2gray(img), t, prm.r, prm.eps); % 步骤4:细化 J = recover_scene(img, t_refined, A, prm.t0); % 步骤5:恢复 end代码说明:im2double把 uint8 转到 double 的 $[0,1]$ 区间,这一步不做,后面的减法、除法会直接截断或被放大;reshape(A,1,1,3)让矩阵除法沿通道广播;rgb2gray是把灰度图作为导向滤波的引导图,边缘结构与原始图像一致。
3.2 暗通道:最小值滤波是核心
function dark = dark_channel(img, patch) if mod(patch, 2) == 0 patch = patch + 1; % 保证窗口中心像素存在 end min_rgb = min(img, [], 3); % RGB三通道逐像素取最小 dark = ordfilt2(min_rgb, 1, ones(patch)); % 滑动窗口最小值滤波 end代码说明:ordfilt2第二个参数是顺序,1 表示最小值。窗口默认补零,暗通道在图像边缘会偏低一点,但它影响的只是前几像素,对其他区域的透射率排序影响很小;如果要严格处理,可先用padarray复制边缘再滤波。窗口大小patch建议取奇数,默认 15 在 0.5K 到 2K 分辨率的图片上都比较稳。
3.3 大气光估计:暗通道前 0.1% 像素
function A = estimate_atmospheric_light(img, dark) [rows, cols] = size(dark); n = max(1, floor(rows * cols * 0.001)); % 最亮0.1%像素数 flat_dark = dark(:); [~, idx] = maxk(flat_dark, n); % 前n个最大值下标 [r_idx, c_idx] = ind2sub([rows, cols], idx); A = zeros(1, 3); for ch = 1:3 values = img(sub2ind([rows, cols], r_idx, c_idx) + (ch-1)*rows*cols); A(ch) = max(values); % 原图对应位置取最亮 end end代码说明:这里取的是原图对应暗通道高亮位置的通道最大值,而不是亮度的均值,符合原始论文取最亮大气光的思路。maxk在较新的 MATLAB 版本中可用,老版本可以先用sort(flat_dark, 'descend')截取前 n 个下标替换。如果大气光估出来偏红或偏蓝,通常是因为暗通道最亮像素落在彩色光源上,可以对前 0.1% 像素的通道值求均值,鲁棒性更好。
3.4 透射率估计与导向滤波
透射率初估计带有窗口状方块,直接恢复会在景深边缘形成光晕,所以要做边缘保持平滑。原论文用导向滤波,MATLAB 里一般自己实现一个简化版:
function q = guided_filter(I, p, r, eps) win = fspecial('average', [2*r+1, 2*r+1]); % 平均窗口 mean_I = imfilter(I, win, 'replicate'); mean_p = imfilter(p, win, 'replicate'); corr_I = imfilter(I .* I, win, 'replicate'); corr_Ip = imfilter(I .* p, win, 'replicate'); var_I = corr_I - mean_I .* mean_I; cov_Ip = corr_Ip - mean_I .* mean_p; a = cov_Ip ./ (var_I + eps); % 边缘保持系数 b = mean_p - a .* mean_I; q = imfilter(a, win, 'replicate') .* I + imfilter(b, win, 'replicate'); end代码说明:这段代码把导向滤波化简为局部线性模型的均值滤波近似。假设在窗口内输出是引导图的线性变换,a、b是线性系数,取邻域均值后得到最终的q。严格版本会用积分图做盒式滤波,这里的imfilter+fspecial在效率上略低,但一对 800×600 图像完全够用。如果你的 MATLAB 里 Image Processing Toolbox 版本较新,也可以直接查imguidedfilter的文档,参数语义对应这里的r和eps。
透射率估计与细化这样串:
A = reshape(A, 1, 1, 3); t = 1 - prm.omega * dark_channel(img ./ A, prm.patch); % 初始透射率 t_refined = guided_filter(rgb2gray(img), t, prm.r, prm.eps); % 细化代码说明:把img逐通道除以大气光,再做暗通道,得到了透射率初值;omega=0.95时实际保留 5% 薄雾,后续恢复结果不会显得过于生硬。用灰度原图做引导,透射率图的边缘与物体轮廓对齐,天空这类平坦区域则被平滑。
3.5 恢复场景与完整调用
function J = recover_scene(img, t, A, t0) t = max(t, t0); % 防止除零和噪声放大 J = zeros(size(img)); for ch = 1:3 J(:,:,ch) = (img(:,:,ch) - A(ch)) ./ t + A(ch); end J = min(max(J, 0), 1); % 截断到[0,1] end模型反解就是J = (I - A)/t + A。t0论文里默认 0.1,对应雾最浓、透射率最低的区域也不能完全信 0,否则除以 0 会把噪声放大成雪花。最后min(max(J,0),1)把结果压回有效范围,这行建议保留,因为t下界虽然挡住了除零,数值误差仍然可能让个别像素越界。
调用示例:
I = im2double(imread('haze.jpg')); prm = struct('patch', 15, 'omega', 0.95, 't0', 0.1, 'r', 40, 'eps', 1e-3); J = dehaze_dark_channel(I, prm); imshow([I, J]);3.6 参数结构为什么用 struct
所有中间量理论上都能写成嵌套脚本,但用 struct 的好处是批量实验时可以在循环里改字段,而不用改函数签名。比如针对一张低分辨率图:
prm.patch = 9; prm.omega = 0.97; J = dehaze_dark_channel(I, prm);代码说明:patch从 15 降到 9,暗通道窗口变小,透射率细节更锐利;omega提高则去雾更彻底。这样改参不会破坏主函数接口,第 4 章的参数表也能逐字段对应上。
4. 参数怎么设、什么时候失效:暗通道去雾的三个坑位与边界
4.1 五个参数的对照表与调节方向
去雾效果的好坏,80% 集中在下面五个数字上:
| 参数 | 默认值 | 调大效果 | 调小效果 | 适用场景 |
|---|---|---|---|---|
patch | 15 | 透射率图更平滑,边缘细节丢失 | 细节更锐利,容易出现块状halo | 高分辨率图可到21,低分辨率用9 |
omega | 0.95 | 去雾更彻底,灰白感更弱 | 保留更多雾,更自然 | 薄雾0.85~0.95,浓雾0.97~1.0 |
t0 | 0.1 | 雾浓处更暗更稳 | 恢复值更亮但噪声放大 | 天空区域过曝时提高到0.15~0.25 |
r | 40 | 透射率更平滑 | 保留更多细节 | 图像越大,r 建议越大 |
eps | 1e-3 | 平滑更强 | 边缘保持更强 | 噪声图上调到1e-2 |
实际调试建议先固定patch=15, omega=0.95, t0=0.1,只动r和eps;这两个参数组合决定透射率图是否干净。r调大后eps也要跟着调大,否则平滑区域会出现亮度波纹。
比如一张分辨率 1920×1080 的雾图,可以用patch=21, omega=0.97, t0=0.12, r=60, eps=1e-3;天空占比大时把t0改到0.18。而手机拍的 400×600 缩略图,patch=9就明显比 15 干净。另一个容易忽视的点是:去雾不是把omega推到 1 就最强——许多城市雾霾图中,完全去掉雾会把远处楼体边缘压出黑边,保留 3% 到 5% 的雾反而让结果更像真景。
4.2 天空区域过曝与先验失效
暗通道先验的成立条件是局部窗口内至少一个通道有暗值。天空、白色墙面、白车身这类区域不满足先验,暗通道偏亮,导致透射率被低估,恢复时补偿过度,结果就是天空发灰、发暗甚至偏色。处理手段按顺序做:优先把t0从 0.1 提到 0.15 以上,强制限制最大增强幅度;其次是修改大气光的选取,避免 $A$ 落在霓虹灯或彩色反光面;如果天空面积很大,建议对天空区域单独做 mask,透射率不再用暗通道推导,而是直接平滑插值。
提示:判断是不是先验失效,把透射率可视化出来看,天空区域出现明显的凹坑或方块状突变,基本就是暗通道高估了雾浓度。
4.3 halo 边缘光晕的成因与三个缓解方向
halo 来自窗口跨过深度突变边缘:窗口内同时包含近处清晰物体和远处浓雾背景,最小值滤波被背景低值污染,透射率在边缘附近估计不准。减少 halo 的方向有三条:一是缩小patch,让窗口更难跨深度边界;二是对透射率做强平滑,代价是边缘细节损失;三是调整 omega,不完全恢复,视觉上掩盖过度补偿。工程上通常先减patch再调r,三步就能压掉大部分光晕。
调试顺序建议:先停到t_refined生成后看透射率图,再停到恢复后看J。如果透射率正常但结果发灰,问题在A;如果透射率本身就是块状,问题在patch或导向滤波;如果透射率呈现整体偏移,检查是否对img./A用错了矩阵除号。/和./在这里差别很大,前者会尝试求解线性方程组而不是逐通道除法,报错信息通常是维度不匹配。
4.4 常见误用与排错清单
- 对 uint8 数据直接做
img - A,负数被截断为零,恢复图整体发黑。先im2double,恢复完成后再转回 uint8 保存。 - 大气光估计用了全图最亮像素而不是暗通道前 0.1% 像素,白色车身会被当成大气光,整体色温严重偏移。
- 恢复时
A是 1×3 向量,直接和 H×W×3 矩阵做./会报维度不匹配,必须先reshape(A,1,1,3)。 - 忘记截断
t的下界,浓雾区域出现大量彩色噪点。 - 透射率可视化时没做归一化,输出一片白,误以为是算法失败。
这一套查下来,九成报错集中在类型和维度上,先看工作区变量大小再执行。
5. 透射率可视化与无参考验证:让去雾结果可解释的三个技巧
5.1 把透射率画出来,比盯结果图更容易发现问题
t_show = mat2gray(t_refined); % 映射到[0,1] figure; subplot(1,2,1); imshow(t_show); title('transmittance'); colormap(jet); colorbar; subplot(1,2,2); imshow([img, J]);代码说明:mat2gray会把透射率线性拉伸,方便观察相对结构。合格的透射率图应该和景深结构一致:近处亮、远处暗,边缘过渡连续。如果图上出现方块边界,说明patch太大或导向滤波没有真正生效,先确认guided_filter的输出变量是否被后续代码使用。
5.2 无参考质量指标:梯度与熵的组合
没有无雾真值图时,用梯度均值和信息熵给结果做客观描述。有雾图细节被压缩,去雾后梯度通常会上升;但噪声放大也会拉高梯度,所以要和熵一起看:熵太低说明细节仍然不足或过度平滑,太高说明噪声被一起放大了。
gI = imgradient(rgb2gray(img)); gJ = imgradient(rgb2gray(J)); fprintf('grad before: %.3f / after: %.3f\n', mean(gI(:)), mean(gJ(:))); fprintf('entropy before: %.3f / after: %.3f\n', ... entropy(rgb2gray(img)), entropy(rgb2gray(J)));代码说明:entropy是图像处理工具箱函数,对 double 输入会先转 uint8 再算直方图,所以两张图要同条件对比;两张图来自同一曝光设置时结论才可靠,自动曝光会改变直方图基线,跨批次比较没有意义。
5.3 批量回归测试:固定参数跑完一组图再调参
单张图调参容易过拟合到一张样本上。建议把去雾函数存成独立 m 文件,然后批量跑目录下所有雾图:
files = dir(fullfile('haze_set', '*.jpg')); prm = struct('patch', 15, 'omega', 0.95, 't0', 0.1, 'r', 40, 'eps', 1e-3); for i = 1:numel(files) I = im2double(imread(fullfile(files(i).folder, files(i).name))); J = dehaze_dark_channel(I, prm); imwrite(J, fullfile('output', files(i).name)); end跑完在缩略图模式下扫一遍,重点看三个位置:天空、远处物体边缘、暗部区域。如果大多数图透射率细节平滑、颜色偏移只出现在天空,就继续调t0;如果边缘光晕普遍存在,先减小patch。这套流程同样适合把暗通道去雾作为预处理步骤接到深度学习 matlab 工作流里,比如用结果透射率当伪标签训练一个轻量估计网络,算是老算法衔接新方向比较自然的一种扩展。
本文还有配套的精品资源,点击获取