ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

暗通道先验去雾:Python源码解析与工程调参指南

暗通道先验去雾:Python源码解析与工程调参指南 简介资源为基于暗通道先验的图像去雾实现源码以Python语言编写面向图像处理课程设计与期末大作业场景适合具备基础编程语法、希望快速完成高质量项目的学生与入门开发者。项目按经典去雾流程组织通过提取暗通道、估计大气光并优化透射率实现对雾天图像的清晰还原包含核心算法脚本、多张测试图像与说明文档可直接运行并观察去雾效果。压缩包共10个文件主要涵盖py源码、jpg与png/bmp测试图及txt说明整体仅2.71MB轻量易部署便于本地实验与二次修改。当前已有287人学习下载。读者可获得完整可运行的工程包括暗通道估计、大气光求解、透射率优化及引导滤波去雾等关键步骤的精简实现配套室内外、景深差异等不同场景样本便于对照验证同时附有简洁的资源介绍文档有助于梳理原理并扩展复现。1. 暗通道先验去雾拿到Python源码第一件事不是运行而是确认像素统计口径拿到“基于暗通道先验的图像去雾的Python实现项目源码”这个压缩包多数人的第一反应是找 main.py 然后直接运行但暗通道先验这套算法对输入图像的数值范围极其敏感0-255 的 uint8 和 0-1 的浮点图走同一套逻辑得到的结果可能一个偏暗一个发灰。这个项目的核心不是“跑通一个去雾 demo”而是把大气散射模型、暗通道统计、透射率细化这三个环节在 Python 里稳定复现并能在不同模糊程度的图片上给出可解释的参数。适合要用此算法交课程设计、做图像预处理对比实验或者打算把去雾能力集成进自己视觉管线的开发者。先不要急着点运行把代码里的像素统计口径读懂后面所有问题都会好解。2. 暗通道先验与大气散射模型透射率为什么不是“模糊程度”去雾在计算摄影里属于图像复原而非图像增强它先假设一个物理成像模型再反解出原始场景信息。理解这个模型的三个变量是读源码之前必须补的底子否则你只会调滑块不知道每个滑块在动什么。2.1 大气散射模型一张雾图可以被分解成哪三部分现在主流的单幅图像去雾算法几乎都建立在大气散射模型上数学形式是I(x) J(x) · t(x) A · (1 - t(x))I(x) 是相机捕捉到的有雾图像也就是你输入的那张图J(x) 是场景辐射率即无雾状态下的真实图像也就是去雾要恢复的目标A 是全局大气光通常理解为天空中无穷远处的光强t(x) 是介质透射率。这里的 t(x) 与场景深度 d(x) 满足指数关系t(x) e^(-β · d(x))β 是大气散射系数d 是景深。透射率在 0 到 1 之间距离越近t 越接近 1图像越清晰距离越远t 越小雾的影响越大。很多人把透射率理解成“模糊程度”其实不够准确。透射率描述的是物体反射光经过雾层衰减后还剩多少到达相机它是深度方向的衰减因子不是空间上的模糊算子。空间模糊是大气光在传递路径上叠加造成的所以去雾不仅仅要增强对比度还要把叠加的大气光剔除再把衰减的反射光补回来。把上式变形得到恢复公式J(x) (I(x) - A) / t(x) A这意味着只要能估计出全局大气光 A 和每个像素的透射率 t(x)就能逐像素反解出 J。暗通道先验解决的就是这两个未知量的估计问题。2.2 暗通道先验成立的条件与失效场景何恺明 2009 年提出暗通道先验时给出了一个统计结论对户外无雾图像的每个局部区块做最小值滤波取 RGB 三通道中的最小值绝大多数区块的这个最小值会趋近于 0。用数学语言描述为J_dark(x) min_{c∈{R,G,B}} ( min_{y∈Ω(x)} J_c(y) ) ≈ 0其中 Ω(x) 是以 x 为中心的一个局部窗口c 表示颜色通道。这个先验成立的前提是局部窗口内要有足够的色彩变化或阴影。例如一个绿色叶片组成的区块绿色通道值高红色和蓝色通道的值低暗通道接近 0一块彩色积木、一段柏油路、一扇阴影里的窗户也都满足。它本质是在说“彩色自然图像不会在每个局部都存在所有通道的高反射”。失效场景同样明显白色物体、大面积天空、雾浓重到彻底遮蔽色彩的区域暗通道值不趋近 0。这也是为什么暗通道先验去雾普遍会遇到天空区域色彩失真或光晕。源码处理这类区域的手段通常是把大气光 A 采样自暗通道中最亮的像素这些像素往往来自天空或远景雾层相当于把“不满足先验”的区域直接归并入大气光成分避免它们被错误恢复。2.3 从暗通道反推透射率与大气光三个公式的推导逻辑把大气散射模型两边同时除以 A得到I(x)/A J(x)/A · t(x) (1 - t(x))对这个等式两边取暗通道操作。等式右边的 J(x)/A 是真实场景的变换形式暗通道依然趋近于 0因此整项可以消去只剩下t(x) 1 - min_{c∈{R,G,B}} ( min_{y∈Ω(x)} (I_c(y)/A_c) )实际操作里完全去掉雾会引起视觉不自然远景处透射率接近 0 会导致噪声放大到不可接受。所以源码会引入一个保留因子 ω通常取 0.95含义是保留少量雾气让画面更真实t(x) 1 - ω · dark_channel(I / A)对于大气光 A常见做法不是取全图最亮的单个像素那样容易选到白色车顶或路灯。正确的顺序是先计算暗通道在暗通道中取亮度最高的一小块区域通常是总像素数的前 0.1%再回到原始图像 I 中取这些位置对应的像素亮度用最大值或均值作为 A。这段逻辑决定了一个重要性质A 不是全局平均亮度而是雾最浓区域的代表光强。读源码时看到排序或直方图统计多半就是在做这件事。估计变量输入数据统计方式对结果的影响暗通道原图三通道最小值最小值滤波决定透射率整体亮度大气光 A暗通道最亮前 0.1% 像素取这些像素在原图的亮度最值或均值决定去雾后的色偏方向透射率 t暗通道归一化结果1 - ω·暗通道决定去雾强度与景深层次透射率细化这一步在源码里也必须有直接由暗通道算出的透射率图是块状的边缘与物体轮廓不贴合。何恺明原论文用软抠图soft matting求解一个大规模稀疏线性系统Python 项目里更常见的替代方案是导向滤波几十行代码就能获得接近的效果并且速度优势明显。下一章直接按照源码的文件组织方式拆开看。3. 暗通道先验的 Python 实现拆开源码里的关键函数与参数一个结构完整的暗通道先验去雾项目代码组织通常不是单文件堆叠而是按职责拆分成暗通道计算、大气光估计、透射率生成与细化、最终恢复四个模块。这样拆分的好处是你可以在大气光环节换成其他估计算法或者在细化环节替换成深度学习方法而不影响其他模块。3.1 源码目录与核心模块的职责划分拿到压缩包后先看目录结构。典型结构如下dehaze/ ├── main.py # 命令行入口与参数解析 ├── dark_channel_model.py # 暗通道计算 ├── atmosphere_estimate.py # 大气光估计 ├── transmission_estimate.py# 透射率估计与导向滤波 ├── scene_recover.py # 场景辐射恢复 ├── utils.py # 图像读取、数值类型转换与写入 ├── data/ │ ├── input/ # 输入的有雾图像 │ └── output/ # 输出去雾结果 └── requirements.txt读源码的顺序建议是main.py 看整体流程dark_channel_model.py 看统计口径transmission_estimate.py 看细化方式。如果你发现 main.py 里存在 opencv 的 imread 后直接进入计算环节那么大概率有一个强制类型转换的细节需要留意常见坑是 uint8 图像没有归一化到 0-1导致后续除法结果溢出。3.2 暗通道计算的两种写法循环与 OpenCV 腐蚀暗通道定义里包含一个局部窗口的最小值滤波初学者可能写成双层 for 循环扫描每个像素邻域这种写法在 C 语言里能接受在 Python 里处理一张 800×600 的图像要等几十秒。工程化的写法是利用 OpenCV 的腐蚀函数代码量少且速度快import cv2 import numpy as np def calculate_dark_channel(image, patch_size15): # image 必须为 0.0 ~ 1.0 的 float32/BGR 图像 b, g, r cv2.split(image) min_channel cv2.min(cv2.min(b, g), r) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (patch_size, patch_size)) dark_channel cv2.erode(min_channel, kernel) return dark_channel这段代码用三通道两两取 min 得到逐像素最小值再用矩形结构元素做腐蚀操作等价于以每个像素为中心取 patch_size 邻域内的最小值。MORPH_RECT 表示矩形结构元素patch_size 是窗口边长必须为正奇数默认 15 是论文里的推荐值。腐蚀操作本质是在图像上滑动窗口找局部最小值所以不需要手写循环。为什么这里要强调图像是 0-1 的 float32因为大气光 A 和透射率 t 的后续计算里会出现 I/A 这种除法0-255 的数值除出来的结果整体偏大暗通道透射率会偏低最终恢复出的图像偏暗。主流的公开 Python 实现都会在 utils 里预留一个 normalize 函数把 uint8 除以 255。如果你手里的源码没有这一步建议自己在 main.py 的入口处补上。3.3 大气光估计取最亮而不是取最大大气光估计的代码看起来不长但边界条件很多。最常见的工程实现如下def estimate_atmosphere(image, dark_channel, sample_ratio0.001): # 返回大气光向量 Ashape 为 (3,) height, width dark_channel.shape total_pixels height * width sample_count max(1, int(total_pixels * sample_ratio)) flat_dark dark_channel.ravel() indices np.argpartition(flat_dark, -sample_count)[-sample_count:] brightest_value -1 atmosphere np.zeros((3,), dtypenp.float32) for idx in indices: row, col divmod(idx, width) pixel image[row, col] pixel_sum pixel.sum() if pixel_sum brightest_value: brightest_value pixel_sum atmosphere pixel.copy() return atmosphere这段代码先用 np.argpartition 找出暗通道里亮度最高的前 0.1% 像素的索引注意 argpartition 比 argsort 快很多它不需要完整排序只保证前 sample_count 个元素位于数组顶端。然后再遍历这些候选像素在原图里找 RGB 三通道和最大的那个像素作为大气光。取和最大而非取单通道最大是为了避免某一道颜色过亮干扰整体色偏估计。如果源码直接用 np.max(dark_channel) 定位大气光碰到白色高光物体如白色车辆、反光玻璃幕墙时去雾结果会出现明显的偏绿或偏蓝块。遇到这种情况建议将采样方式改成这些像素在原图中的平均值能显著减少随机性。参数方面sample_ratio 通常固定为 0.001对应论文里的前 0.1% 像素对分辨率很高的图像可以适当降低到 0.0005避免把远处的零散亮点纳入统计。3.4 透射率估计与导向滤波从块状到边缘贴合透射率估计公式需要先做一次除法再算暗通道。代码实现def estimate_transmission(image, atmosphere, patch_size15, omega0.95): # 按通道做归一化 normalized np.empty_like(image, dtypenp.float32) for ch in range(3): normalized[:, :, ch] image[:, :, ch] / atmosphere[ch] dark_norm calculate_dark_channel(normalized, patch_size) transmission 1.0 - omega * dark_norm transmission np.clip(transmission, 0.1, 1.0) return transmission除以大气光之后计算暗通道得到的透射率是粗糙的块状图每个 patch_size 方形区域内部透射率相同。直接用它恢复图像会在物体边缘留下白色光晕。细化透射率的常见做法是导向滤波以原始灰度图或亮通道作为导向图def guided_filter(guide, source, radius40, eps1e-4): # guide: 导向图通常用原图灰度图 # source: 待细化的透射率图 mean_g cv2.boxFilter(guide, -1, (radius, radius)) mean_s cv2.boxFilter(source, -1, (radius, radius)) corr_gs cv2.boxFilter(guide * source, -1, (radius, radius)) cov_gs corr_gs - mean_g * mean_s var_g cv2.boxFilter(guide * guide, -1, (radius, radius)) - mean_g * mean_g a cov_gs / (var_g eps) b mean_s - a * mean_g mean_a cv2.boxFilter(a, -1, (radius, radius)) mean_b cv2.boxFilter(b, -1, (radius, radius)) return mean_a * guide mean_b这里的导向滤波用 boxFilter 实现了均值计算核心假设是导向图与输出图像之间存在局部线性关系。当透射率处于边缘两侧时a 的值会出现明显差异从而保留边缘在平坦区域内b 主导输出透射率趋于平滑抑制噪声放大。radius 一般取图像短边的 10% 左右eps 是正则化参数值越小边缘保持能力越强但过小会导致梯度反转伪影。3.5 场景恢复与防噪声下限t0 的作用恢复像素值时透射率过小会让除法结果趋于无穷大噪声被急剧放大。场景恢复函数里常见的处理方式是设置透射率下限def recover_scene(image, transmission, atmosphere, t00.1): transmission np.clip(transmission, t0, 1.0) recovered np.empty_like(image, dtypenp.float32) for ch in range(3): recovered[:, :, ch] ( (image[:, :, ch].astype(np.float32) - atmosphere[ch]) / transmission atmosphere[ch] ) return np.clip(recovered, 0.0, 1.0)t0 默认值 0.1意思是限制透射率最小为 0.1这样即便远景雾层很浓除法运算的分母也不会太小。但 t0 设置过高会让远景区域的雾完全保留失去去雾透感设置过低则天空区域会出现严重噪点和色斑。恢复之后还要做一次 0-1 裁剪防止溢出。到这里核心算法链路已经完整。接下来进入真正调参和跑通源码的环节。4. 去雾源码跑通与调参patch-size、omega、t0 怎么组合这一章解决两个问题如何用最小的命令在本地跑通去雾脚本以及面对不同场景怎么调整参数组合。依赖环境推荐 Python 3.8 以上版本核心库只需要 opencv-python 和 numpy这两个库的安装命令如下pip install opencv-python numpy如果你的环境里同时使用了 conda建议用 conda 新建一个 Python 3.10 环境再安装依赖避免系统级别的 OpenCV 与 numpy 版本冲突。安装完成后在压缩包解压目录下执行python main.py \ --input data/input/haze.jpg \ --output data/output/dehazed.jpg \ --patch-size 15 \ --omega 0.95 \ --t0 0.1 \ --refine guided命令参数的含义需要逐个核对patch-size 决定暗通道窗口大小影响透射率的空间平滑程度omega 是去雾强度的保留因子越接近 1 去雾越猛t0 是透射率下限控制远景噪声refine 参数用来选择透射率细化方式常见值是 guided 表示导向滤波none 表示跳过细化。如果你的 main.py 没有 argparse 参数解析而是一堆全局变量可以直接改文件顶部的常量。4.1 三个必调参数patch-size、omega、t0下表给出主要参数的默认值、调节方向及其在工程中的实际影响参数默认值调大时的效果调小后的效果典型取值区间patch-size15透射率更平滑、无块状但小物体边缘被抹平边缘细节保持好但噪点增多7 ~ 35omega0.95去雾彻底、偏色加重画面自然、雾气残留多0.85 ~ 0.98t00.1远景雾保留、噪声被压住远景恢复更强、噪点明显0.05 ~ 0.3radius40细化后的透射率更平缓、光晕少边缘贴合细、局部不平滑图像短边的 8% ~ 15%对于 patch-size可以把它理解成“物理世界的统计尺度”。同一张 1024×768 的图片patch-size 取 15 统计的是 15 像素见方的区块如果把图片缩放到 512×384再用 15 的窗口覆盖的实际空间范围扩大了一倍统计结果自然不同。一个通用的做法是让 patch-size 等于图像短边除以 50 并取最近的奇数。例如短边 768 像素patch-size 推荐 15短边 1024取 21 或 25。4.2 高分辨率图像的处理先下采样再恢复在 4K 分辨率上直接跑暗通道去雾patch-size 沿用默认 15 会导致暗通道窗口覆盖范围不够透射率分布非常零碎画面出现大量局部色块。工程上的常见解法是拉普拉斯金字塔思想先把输入图像下采样到长边约 1200 像素在这个尺寸上估计透射率中小尺寸的 patch-size 取 21 即可然后通过导向滤波将低分辨率透射率上采样回原尺寸并细化。这样既保证了暗通道统计的上下文又不会让细化环节的计算量过大。实际操作时源码不需要大改只要在预处理阶段做一次 cv2.resize恢复阶段再把透射率图放大。最终恢复仍使用原图透射率只是做空间引导。这样处理完的图像与全分辨率估计相比天空区域的光晕更少整体观感更稳定。4.3 三个容易踩的坑天空偏色、透射率下限过大、通道顺序错误天空偏色是暗通道先验去雾出现频率最高的问题。由于天空区域不满足暗通道先验透射率会被估计过低恢复后天空会变成灰黑色或者出现黄褐色光晕。绕开问题的一个经验做法用阈值把天空区域识别出来对该区域的透射率直接设置一个较大的下限比如 0.3。代码上可以先用阈值生成天空掩膜再将 mask 区域的 transmission 用 np.maximum 与常数值合并这样天空保持明亮其余区域保持原有去雾强度。第二个坑是数据读取时的通道顺序。OpenCV 默认读入 BGR而大多数图像处理框架使用 RGB。如果你将图像从 file 读入后直接用 cv2.imwrite 输出颜色大概率没太大偏差但如果要做可视化或与其他模型联动BGR 转 RGB 这行代码不能省。正确的转换代码是import cv2 image_bgr cv2.imread(input.jpg) image_rgb cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)第三个坑是 uint8 图像参与除法产生截断。如果图像没有归一化透射率估计中 normalized image / atmosphere 得到的结果仍然在 uint8 范围内1 - omega * dark_norm 这个算式的结果会出现大量负值或者被截断为 0。建议在读取图像后立即执行image image.astype(np.float32) / 255.0所有计算用浮点完成最后再乘以 255 输出。4.4 不同场景的参数推荐组合不同拍摄场景对参数的敏感度差异明显下面给出几组经典型取值可以直接套用场景特征patch-sizeomegat0备注室内容量较小、物体边缘多90.90.1防止细节被抹平室外风景、有山有树150.950.1默认参考组合有天空的远景场景210.920.2需要配合天空掩膜浓雾、图像几乎看不到轮廓350.980.05先看直方图确认信息量足够夜间或低照度不适用不适用不适用暗通道先验在暗光下失效优先改用 RGB 通道增强夜间场景去雾不建议直接套暗通道先验。夜间图像整体暗通道值偏高大气光估计结果往往落在高光路灯附近恢复出的图像会色偏严重且暗部噪声被放大幅度超过可接受范围。这种做法与原始论文的使用边界一致不是源码 bug。5. 用 PSNR/SSIM 和局部对比验证去雾结果先证明有效再谈优化去雾算法不能只用肉眼评估同一张图用不同参数会得到风格差异很大的结果需要量化指标支撑调参决策。但如果手头没有对应的无雾真值图PSNR 和 SSIM 都算不了这时可以用局部对比度和暗通道均值来间接评估。写一个简短的评估脚本对同一张输入图批量测试不同参数组合输出指标对比import cv2 import numpy as np def psnr(original, processed, max_value255.0): mse np.mean((original.astype(np.float64) - processed.astype(np.float64)) ** 2) return 10 * np.log10((max_value ** 2) / (mse 1e-8)) def dark_channel_stats(image, patch_size15): min_channel np.min(image, axis2) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (patch_size, patch_size)) dark cv2.erode(min_channel, kernel) return dark.mean(), dark.std() # 读取原图与去雾结果统计暗通道均值 hazy cv2.imread(hazy.jpg, cv2.IMREAD_COLOR) clear cv2.imread(cleared.jpg, cv2.IMREAD_COLOR) dark_before dark_channel_stats(hazy.astype(np.float64) / 255.0) dark_after dark_channel_stats(clear.astype(np.float64) / 255.0) print(去雾前暗通道均值: {:.4f}, 方差: {:.4f}.format(*dark_before)) print(去雾后暗通道均值: {:.4f}, 方差: {:.4f}.format(*dark_after))这段脚本统计去雾前后整图暗通道的均值和标准差。正常情况下去雾后暗通道均值应显著下降因为雾气被移除后局部区域的最小通道亮度不再被大气光抬升方差上升说明不同景深处的对比度被拉开。如果输出结果的暗通道均值反而升高大概率是像素统计口径出了问题最可能的原因就是 uint8 到 float32 的归一化没有做对。对于有真值图的实验合成雾图PSNR 与 SSIM 的计算建议直接用 scikit-imagefrom skimage.metrics import peak_signal_noise_ratio, structural_similarity import cv2 gt cv2.imread(gt.jpg, cv2.IMREAD_COLOR) result cv2.imread(result.jpg, cv2.IMREAD_COLOR) print(PSNR: {:.2f} dB, SSIM: {:.4f}.format( peak_signal_noise_ratio(gt, result), structural_similarity(gt, result, channel_axis2) ))调参时记录几组数据会发现omega1.0 时 PSNR 往往最高但视觉观感最差色偏严重边缘处出现黑色光晕omega0.9 时 PSNR 略低但视觉更自然。这种情况下优先看暗通道方差和局部边缘保持再决定参数取舍。真正把这套代码用进生产管线时还可以加一步自适应 omega 估计根据输入图像的暗通道均值决定去雾强度暗通道均值越高代表雾越浓omega 就相应调大。这样能避免在不同场景间手动切换参数也让项目源码的鲁棒性上一个台阶。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进