4. 大气散射模型:物理模型推导、大气光与透射率的概念、暗通道先验理论
各位同学,今天我们来啃一块硬骨头——大气散射模型。说实话,我刚接触遥感去雾那会儿,看到这些物理公式也头疼。但后来我发现,搞懂这个模型,就像拿到了去雾算法的“万能钥匙”。你想想看,所有经典去雾方法,几乎都绕不开它。
4.1 大气散射模型的物理推导
我们先从一张图说起。假设你站在卫星上看地面,光线要走这么一段路:
- 目标反射光:地面物体反射的阳光,穿过大气层到达传感器。
- 大气光:太阳光被大气中的气溶胶、水汽等颗粒散射,直接进入传感器。
- 衰减过程:反射光在传播过程中,被大气颗粒吸收和散射,强度逐渐减弱。
把这三条合在一起,就得到了经典的大气散射模型公式:
I(x) = J(x) * t(x) + A * (1 - t(x))
其中:
- I(x):传感器接收到的图像(你看到的雾图)
- J(x):场景辐射度(无雾时的真实图像)
- t(x):透射率,描述光线穿透大气的比例
- A:全局大气光值
这个公式其实很好理解。第一项 J(x) * t(x) 是衰减后的目标信号,第二项 A * (1 - t(x)) 是叠加的大气光。说白了,你看到的雾图 = 被削弱的目标 + 额外的雾霾。
核心思路:去雾就是已知 I(x),反推 J(x)。但问题在于,t(x) 和 A 都是未知的。一个方程三个未知数,这就是去雾问题的病态性所在。
4.2 大气光与透射率的概念
这两个参数,我建议你分开理解。
4.2.1 大气光 A
大气光,说白了就是雾最浓的那个区域的像素值。在图像上,它通常对应天空区域或者远处的雾霾区域。我记得有一次处理一张海面遥感图,天空部分被云层遮挡,我直接用全图最亮像素取 A,结果去雾后图像偏暗。后来我改用暗通道方法取前 0.1% 最亮像素的平均值,效果就好多了。
常见的 A 值估计方法:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 全局最亮像素 | 取全图最大像素值 | 简单,但易受高亮物体干扰 |
| 暗通道前 0.1% | 取暗通道中最亮像素对应原图位置的平均值 | 鲁棒性好,何恺明经典方法 |
| 四叉树分割 | 递归分割图像,选取最亮区域 | 计算量稍大,但精度高 |
4.2.2 透射率 t(x)
透射率描述的是光线能“穿透”多少雾。t(x) = 1 表示完全无雾,t(x) = 0 表示完全被雾遮挡。在遥感图像中,透射率通常与场景深度有关——远处的山,透射率低;近处的建筑,透射率高。
这里有个坑,我曾经踩过:透射率图如果直接使用,去雾后的图像边缘会有明显的“光晕效应”。为什么?因为透射率在物体边缘变化剧烈,导致去雾过度。解决办法是使用引导滤波对透射率图进行精细化处理,这个我们后面章节会详细讲。
我的经验:透射率的下限通常设为 0.1,避免去雾过度导致图像失真。你可以在代码里加一句 t = np.maximum(t, 0.1)。
4.3 暗通道先验理论
这是去雾领域里程碑式的发现。何恺明在 2009 年提出:在绝大多数非天空区域的局部小块中,至少有一个颜色通道的亮度值非常低,趋近于零。
为什么会这样?你想想看:
- 绿色的树叶,在红色和蓝色通道上反射率很低
- 红色的砖墙,在绿色和蓝色通道上反射率很低
- 阴影区域,所有通道都低
所以,暗通道的定义就是:
J_dark(x) = min_{y ∈ Ω(x)} ( min_{c ∈ {R,G,B}} J_c(y) ) → 0
翻译成人话:先取每个像素 RGB 三个通道的最小值,得到一张灰度图;再对这个灰度图做最小值滤波(窗口大小通常 15×15),得到暗通道图。对于无雾图像,暗通道图几乎全黑。
有了暗通道先验,我们就可以估计透射率了。把大气散射模型两边同时除以 A,再取暗通道:
I_dark(x) / A = t(x) * J_dark(x) / A + (1 - t(x))
因为 J_dark(x) → 0,所以:
t(x) = 1 - ω * I_dark(x) / A
这里的 ω 是一个常数(通常取 0.95),用来保留少量雾感,让图像看起来更自然。
注意:暗通道先验在天空区域会失效。因为天空区域的暗通道值并不趋近于零。所以处理后的天空区域可能会出现颜色失真。我的做法是:先检测天空区域,对这些区域单独处理透射率。
4.4 知识结构图
下面这张图,我把本章的核心逻辑串起来了:
4.5 代码实战:暗通道与透射率估计
光说不练假把式。下面这段 Python 代码,实现了暗通道计算和透射率估计。我建议你亲手跑一遍。
import cv2
import numpy as np
def dark_channel(image, patch_size=15):
"""计算暗通道"""
# 取每个像素 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
def estimate_atmospheric_light(image, dark):
"""估计大气光 A"""
# 取暗通道中最亮的前 0.1% 像素
h, w = dark.shape
num_pixels = h * w
num_top = max(int(num_pixels * 0.001), 1)
# 展平并排序
dark_flat = dark.flatten()
indices = np.argsort(dark_flat)[::-1] # 从大到小
top_indices = indices[:num_top]
# 取这些位置在原图中的像素平均值
image_flat = image.reshape(-1, 3)
A = np.mean(image_flat[top_indices], axis=0)
return A
def estimate_transmission(image, A, omega=0.95, patch_size=15):
"""估计透射率"""
# 归一化图像
normalized = image / A
# 计算暗通道
dark_normalized = dark_channel(normalized, patch_size)
# 透射率公式
t = 1 - omega * dark_normalized
# 限制范围
t = np.clip(t, 0.1, 1.0)
return t
# 使用示例
img = cv2.imread('hazy_image.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0
dark = dark_channel(img)
A = estimate_atmospheric_light(img, dark)
t = estimate_transmission(img, A)
print(f"大气光 A: {A}")
print(f"透射率范围: [{t.min():.3f}, {t.max():.3f}]")
调试技巧:如果去雾后的图像偏暗,可以适当降低 ω 值(比如 0.85);如果去雾不彻底,可以增大 ω 值(比如 0.99)。我一般从 0.95 开始调。
4.6 本章小结
大气散射模型是整个去雾算法的基石。你只要记住三件事:
- 模型公式:I = Jt + A(1-t),去雾就是已知 I 求 J
- 两个参数:大气光 A 和透射率 t(x),一个全局一个局部
- 暗通道先验:无雾图像暗通道趋近于零,这是估计 t(x) 的关键