4. 大气散射模型:物理模型推导、大气光与透射率的概念、暗通道先验理论

各位同学,今天我们来啃一块硬骨头——大气散射模型。说实话,我刚接触遥感去雾那会儿,看到这些物理公式也头疼。但后来我发现,搞懂这个模型,就像拿到了去雾算法的“万能钥匙”。你想想看,所有经典去雾方法,几乎都绕不开它。

4.1 大气散射模型的物理推导

我们先从一张图说起。假设你站在卫星上看地面,光线要走这么一段路:

  1. 目标反射光:地面物体反射的阳光,穿过大气层到达传感器。
  2. 大气光:太阳光被大气中的气溶胶、水汽等颗粒散射,直接进入传感器。
  3. 衰减过程:反射光在传播过程中,被大气颗粒吸收和散射,强度逐渐减弱。

把这三条合在一起,就得到了经典的大气散射模型公式:

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 知识结构图

下面这张图,我把本章的核心逻辑串起来了:

大气散射模型与暗通道先验知识结构 大气散射模型与暗通道先验知识结构 大气散射模型 I(x) = J(x)t(x) + A(1-t(x)) 大气光 A 透射率 t(x) A 估计方法 全局最亮 / 暗通道前0.1% / 四叉树 t(x) 估计方法 暗通道先验 → t(x) = 1 - ω·I_dark/A 暗通道先验:J_dark(x) → 0 核心逻辑:已知 I → 估计 A 和 t(x) → 反解 J(x) 实现去雾

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) 的关键