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 三通道的最小值,再在局部窗口内取最小值。结果就是一张单通道的暗通道图。
2.2 统计规律:为什么暗通道是"暗"的?
你想想看,一张正常的户外图像,为什么暗通道会这么暗?我统计过几百张无雾图像,发现三个规律:
- 阴影——建筑物、树木、车辆的阴影区域,RGB 值都很低
- 彩色物体——比如红色花朵,它的绿色和蓝色通道值很低
- 深色表面——黑色路面、深色岩石,整体亮度低
这些区域加起来,覆盖了图像的大部分。所以暗通道先验说:无雾图像的暗通道值趋近于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 暗通道先验去雾完整流程
我把整个流程整理成了一张图,方便你理解各步骤的关系:
2.6 大气光值 A 的估计方法
大气光 A 怎么取?我见过有人直接取图像最亮的像素,但这样容易出错——比如图像里有白色汽车,它比天空还亮。
何恺明的方法很巧妙:
- 从暗通道图中选出最亮的 0.1% 像素
- 在原图 I 中找到这些像素对应的位置
- 取这些位置像素的最大值作为 A
这样做的好处是:暗通道最亮的像素通常就是雾最浓的区域(也就是天空),而不是白色物体。
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)。