3、暗通道先验去雾实现:Python实现暗通道提取、大气光值估计、透射率计算、去雾图像恢复
暗通道先验(Dark Channel Prior, DCP)是去雾领域最经典的算法之一。何恺明在2009年提出这个方法时,我还在学校啃,当时就觉得——这思路真巧妙。说白了,它抓住了一个很朴素的观察:在无雾的户外图像里,大部分局部区域总有一些像素的某个颜色通道值特别低,接近0。雾天就不一样了,雾会把所有通道都抬亮。
这一章,我们就一步步把DCP拆开,用Python完整实现一遍。从暗通道提取开始,到大气光估计,再到透射率计算,最后恢复出清晰图像。我会把我在项目中踩过的坑也一并告诉你。
核心流程速览
输入有雾图像 → 提取暗通道 → 估计大气光值 → 计算粗透射率 → 引导滤波细化 → 图像恢复 → 输出去雾结果
3.1 暗通道提取——找到图像中最暗的区域
暗通道的定义其实很简单。对输入图像 I,取每个像素在RGB三个通道的最小值,然后在一个局部窗口(比如15×15)内再做一次最小值滤波。结果就是暗通道图。
数学表达:
J_dark(x) = min_{y ∈ Ω(x)} ( min_{c ∈ {r,g,b}} J_c(y) )
其中Ω(x)是以像素x为中心的窗口。我习惯用15×15的窗口,太大容易丢失细节,太小又不够鲁棒。
来看代码实现:
import cv2
import numpy as np
def dark_channel(image, patch_size=15):
"""
提取暗通道
:param image: 输入图像 (H, W, 3),值范围0-255
:param patch_size: 窗口大小,默认15
: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
我的经验
窗口大小选15是个不错的起点。如果图像分辨率很高(比如4K),可以适当放大到25或31。我曾经在一个航拍去雾项目里用过7×7的窗口,结果暗通道里全是噪声,透射率估计一塌糊涂。后来改成15×15,效果立马好了。
3.2 大气光值估计——找到雾最浓的地方
大气光值A代表雾的颜色和强度。怎么估计?何恺明的方法很巧妙:
- 从暗通道图中选出最亮的0.1%像素(亮度最高的那些点)
- 在原图中找到这些像素对应的位置
- 取这些位置像素值的最大值作为A
为什么这么做?暗通道最亮的地方,说明雾最浓,那里的像素值最接近大气光。
def estimate_atmospheric_light(image, dark):
"""
估计大气光值
:param image: 原图 (H, W, 3)
:param dark: 暗通道图 (H, W)
:return: 大气光值 (3,) 数组
"""
h, w = dark.shape
num_pixels = h * w
# 取最亮的0.1%像素
num_bright = max(int(num_pixels * 0.001), 1)
flat_dark = dark.ravel()
indices = np.argpartition(flat_dark, -num_bright)[-num_bright:]
# 在原图中找这些像素的最大值
flat_image = image.reshape(-1, 3)
A = np.max(flat_image[indices], axis=0)
return A
注意
大气光值A是一个三维向量(R,G,B),不是标量。不同通道的大气光可能不同,尤其在偏色严重的雾天图像里。我遇到过一张黄昏时拍的雾景,A的R通道明显高于B通道,如果强行用标量,恢复出来的图像颜色就怪怪的。
3.3 透射率计算——雾有多浓?
有了暗通道和大气光,透射率就可以直接算出来了。根据雾天成像模型:
I(x) = J(x) * t(x) + A * (1 - t(x))
两边同时除以A,再取暗通道,整理后得到:
t(x) = 1 - ω * dark_channel(I / A)
其中ω是一个保留少量雾的参数,通常取0.95。为什么留一点雾?完全去雾会让图像看起来不自然,像塑料一样。我一般设0.9到0.95之间。
def estimate_transmission(image, A, patch_size=15, omega=0.95):
"""
计算粗透射率
:param image: 原图 (H, W, 3)
:param A: 大气光值 (3,)
:param patch_size: 窗口大小
:param omega: 去雾强度,0.9-0.95
:return: 粗透射率图 (H, W)
"""
# 归一化图像
normalized = image / A
# 提取暗通道
dark_normalized = dark_channel(normalized, patch_size)
# 计算透射率
transmission = 1 - omega * dark_normalized
# 限制范围,防止过曝
transmission = np.clip(transmission, 0.1, 1.0)
return transmission
透射率的下界
代码里我把透射率限制在0.1以上。为什么?如果透射率接近0,恢复时J = (I - A)/t + A 会爆炸,产生大量噪声。设个下界,至少保留10%的雾,图像更稳定。
3.4 引导滤波细化——去掉块状效应
粗透射率有个明显的问题:块状效应。因为用了最小值滤波,透射率图看起来像马赛克。直接用它恢复图像,边缘会有光晕。
解决办法是用引导滤波(Guided Filter)对透射率做边缘保持平滑。引导图就用原图的灰度版本。
def guided_filter(I, p, r=60, eps=1e-6):
"""
引导滤波
:param I: 引导图像 (H, W),单通道
:param p: 输入图像 (H, W),待滤波
:param r: 窗口半径
:param eps: 正则化参数
:return: 滤波结果 (H, W)
"""
mean_I = cv2.boxFilter(I, cv2.CV_64F, (r, r))
mean_p = cv2.boxFilter(p, cv2.CV_64F, (r, r))
mean_Ip = cv2.boxFilter(I * p, cv2.CV_64F, (r, r))
cov_Ip = mean_Ip - mean_I * mean_p
mean_II = cv2.boxFilter(I * I, cv2.CV_64F, (r, r))
var_I = mean_II - mean_I * mean_I
a = cov_Ip / (var_I + eps)
b = mean_p - a * mean_I
mean_a = cv2.boxFilter(a, cv2.CV_64F, (r, r))
mean_b = cv2.boxFilter(b, cv2.CV_64F, (r, r))
q = mean_a * I + mean_b
return q
参数调优
r=60, eps=1e-6是我常用的参数。r控制平滑范围,越大越平滑;eps控制边缘保留程度,越小边缘越锐利。如果图像噪声大,可以把eps调大到1e-3。记得有一次处理雾霾特别严重的城市街景,r调到80才把块状效应压下去。
3.5 图像恢复——去雾的最后一步
有了细化后的透射率t和大气光A,恢复公式很简单:
J(x) = (I(x) - A) / max(t(x), t0) + A
t0是透射率下界,防止除零。我设0.1。
def dehaze(image, t0=0.1):
"""
完整去雾流程
:param image: 输入有雾图像 (H, W, 3)
:param t0: 透射率下界
:return: 去雾后的图像 (H, W, 3)
"""
image = image.astype(np.float64)
# 1. 提取暗通道
dark = dark_channel(image)
# 2. 估计大气光
A = estimate_atmospheric_light(image, dark)
# 3. 计算粗透射率
t_coarse = estimate_transmission(image, A)
# 4. 引导滤波细化
gray = cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_BGR2GRAY)
gray = gray.astype(np.float64) / 255.0
t_refined = guided_filter(gray, t_coarse)
# 5. 图像恢复
t_refined = np.clip(t_refined, t0, 1.0)
result = np.zeros_like(image)
for c in range(3):
result[:,:,c] = (image[:,:,c] - A[c]) / t_refined + A[c]
# 6. 裁剪到有效范围
result = np.clip(result, 0, 255).astype(np.uint8)
return result
3.6 完整流程与参数对比
我把整个流程的关键参数整理成了一张表,方便你对照调参:
| 步骤 | 关键参数 | 推荐值 | 影响 |
|---|---|---|---|
| 暗通道提取 | patch_size | 15 | 越大暗通道越平滑,但细节丢失 |
| 大气光估计 | top_percentage | 0.1% | 太小可能选到噪声点,太大A偏暗 |
| 透射率计算 | omega | 0.95 | 越大去雾越强,但可能过饱和 |
| 引导滤波 | r / eps | 60 / 1e-6 | r控制平滑,eps控制边缘 |
| 图像恢复 | t0 | 0.1 | 越小去雾越彻底,但噪声越大 |
常见坑点
我曾经在批量处理一批雾天监控视频时,发现有些帧恢复后出现严重偏色。排查了半天,原来是大气光估计时,如果图像中有大面积白色物体(比如白墙、天空),最亮的0.1%像素会被这些区域占据,导致A估计偏高。解决办法是在暗通道中排除天空区域,或者对A做多帧平滑。
3.7 效果评估与调优思路
去雾效果好不好,不能光靠眼睛看。我一般用三个指标:
- 对比度提升:去雾后图像的对比度应该明显增加
- 颜色自然度:不能出现偏色,尤其是天空区域
- 边缘保持:去雾后边缘不能有光晕或模糊
如果发现去雾后图像偏暗,可以适当降低omega(比如0.85)。如果天空区域出现光晕,把引导滤波的r调大。如果图像有噪声放大,把t0从0.1提高到0.2。
一句话总结
暗通道先验去雾的核心就三步:找暗通道、算透射率、恢复图像。引导滤波是让结果变自然的关键。参数调优时,记住一个原则——去雾不是越彻底越好,保留一点雾感反而更真实。