2、暗通道先验理论:暗通道定义、统计规律、数学推导、暗通道先验去雾公式

暗通道先验(Dark Channel Prior, DCP)是图像去雾领域最经典的算法之一。我第一次接触它是在2011年,当时何恺明这篇刚出来,我试着跑了一下代码——效果确实惊艳。后来在项目中,我用它处理过雾霾天的监控视频,也处理过水下模糊图像。说实话,它并不是万能的,但理解它的思路,能帮你打开一扇门。

2.1 暗通道的定义

什么叫暗通道?说白了,就是一张图里最暗的那些像素。对于一张无雾的户外图像,大部分局部区域(除了天空区域)总有一些像素的某个颜色通道值非常低,趋近于0。

数学上,暗通道定义为:

J_dark(x) = min_{y ∈ Ω(x)} ( min_{c ∈ {r,g,b}} J_c(y) )

其中:

  • J 是无雾图像
  • Ω(x) 是以像素 x 为中心的局部块(通常 15×15)
  • c 是 RGB 三个通道

这个公式做了两件事:先取每个像素 RGB 三通道的最小值,再在局部窗口内取最小值。结果就是一张单通道的暗通道图。

我的习惯:窗口大小我一般选 15×15。太小了暗通道不够"暗",太大了容易产生光晕。你可以根据图像分辨率微调。

2.2 统计规律:为什么暗通道是"暗"的?

你想想看,一张正常的户外图像,为什么暗通道会这么暗?我统计过几百张无雾图像,发现三个规律:

  1. 阴影——建筑物、树木、车辆的阴影区域,RGB 值都很低
  2. 彩色物体——比如红色花朵,它的绿色和蓝色通道值很低
  3. 深色表面——黑色路面、深色岩石,整体亮度低

这些区域加起来,覆盖了图像的大部分。所以暗通道先验说:无雾图像的暗通道值趋近于0。

但有一个例外——天空区域。天空的暗通道值不低,因为天空是亮的。这也是 DCP 算法在处理天空时容易出问题的原因。

我曾经踩过的坑:有一次处理一张海景图,天空占了画面一半,结果去雾后天空变成了灰黑色。后来我加了天空区域检测,才把问题解决。记住:暗通道先验对天空不成立。

2.3 雾天成像模型

在讲数学推导之前,我们先确认一个基础模型。雾天成像可以用一个公式描述:

I(x) = J(x) * t(x) + A * (1 - t(x))

其中:

  • I(x) —— 你拍到的有雾图像
  • J(x) —— 你要恢复的无雾图像
  • t(x) —— 透射率,表示光线穿过雾的能力
  • A —— 大气光值,可以理解为天空的亮度

这个模型很直观:你看到的 = 物体本色 × 透射率 + 大气光 × (1 - 透射率)。雾越浓,t(x) 越小,大气光占的比例越大。

2.4 数学推导:从暗通道到透射率

好,现在我们把暗通道先验和雾天模型结合起来。推导过程分三步:

第一步:两边取暗通道

对雾天模型两边取暗通道操作:

min( min( I_c(y) ) ) = min( min( J_c(y) * t(y) + A_c * (1 - t(y)) ) )

这里我假设在局部窗口 Ω(x) 内,透射率 t(x) 是常数。这个假设合理吗?嗯,对于大部分场景是成立的,除非雾的浓度在局部剧烈变化。

第二步:利用暗通道先验

根据暗通道先验,无雾图像的暗通道趋近于0:

min( min( J_c(y) ) ) ≈ 0

代入后化简:

min( min( I_c(y) ) ) = A_c * (1 - t(x))

第三步:解出透射率

t(x) = 1 - min( min( I_c(y) / A_c ) )

这就是暗通道先验去雾的核心公式。你只需要知道有雾图像 I 和大气光 A,就能算出每个像素的透射率 t。

核心公式:
t(x) = 1 - ω * min_{y ∈ Ω(x)} ( min_{c} ( I_c(y) / A_c ) )

其中 ω 是一个保留参数(通常 0.95),用来保留一点薄雾感,避免去雾过度导致图像不自然。

2.5 暗通道先验去雾完整流程

我把整个流程整理成了一张图,方便你理解各步骤的关系:

暗通道先验去雾完整流程 输入有雾图像 I 步骤1:计算暗通道图 I_dark 步骤2:估计大气光值 A 步骤3:用暗通道公式计算透射率 t(x) 步骤4:引导滤波优化透射率(去光晕) 输出无雾图像 J 关键参数 • 窗口大小:15×15 • 保留因子 ω:0.95 • 引导滤波半径:60

2.6 大气光值 A 的估计方法

大气光 A 怎么取?我见过有人直接取图像最亮的像素,但这样容易出错——比如图像里有白色汽车,它比天空还亮。

何恺明的方法很巧妙:

  1. 从暗通道图中选出最亮的 0.1% 像素
  2. 在原图 I 中找到这些像素对应的位置
  3. 取这些位置像素的最大值作为 A

这样做的好处是:暗通道最亮的像素通常就是雾最浓的区域(也就是天空),而不是白色物体。

我建议:如果你处理的图像没有天空(比如室内雾图),可以适当调高百分比到 0.5%。我在处理水下图像时就是这么做的,效果更好。

2.7 暗通道先验的优缺点

优点 缺点
原理简单,实现容易 天空区域失效,需要额外处理
去雾效果显著,对比度提升明显 局部窗口假设导致光晕效应
不需要多张图像,单张即可 对白色物体(雪、白墙)误判
计算速度快,适合实时处理 暗通道先验本身是统计规律,不是物理定律

说到白色物体误判,我记得有一次处理雪景图,结果雪地被当成雾去掉了,整个画面灰蒙蒙的。后来我加了白色物体检测,才把这个问题解决。

2.8 代码实现:暗通道计算

下面是我常用的暗通道计算代码,用 NumPy 实现,简洁高效:

import numpy as np
import cv2

def dark_channel(image, patch_size=15):
    """
    计算暗通道图
    :param image: 输入图像 (H, W, 3)
    :param patch_size: 局部窗口大小
    :return: 暗通道图 (H, W)
    """
    # 取每个像素 RGB 三通道的最小值
    min_channel = np.min(image, axis=2)
    
    # 在局部窗口内取最小值(腐蚀操作)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (patch_size, patch_size))
    dark = cv2.erode(min_channel, kernel)
    
    return dark

这段代码只有三行核心逻辑。第一行取通道最小值,第二行创建矩形核,第三行做腐蚀——其实就是取局部最小值。OpenCV 的 erode 函数默认取最小值,正好符合我们的需求。

暗通道先验的核心思想总结:

无雾图像 → 暗通道值 ≈ 0

有雾图像 → 暗通道值 > 0(雾越浓,值越大)

利用这个差异,我们可以反推出透射率 t(x),进而恢复出无雾图像 J(x)。