4. 云阴影几何投影掩膜生成

几何投影的基本关系

云阴影的位置由“云体遮挡直射光”这一几何事实决定:若把云层近似为高度 H 的水平薄层,太阳方位角为 Az(自正北顺时针)、太阳高度角为 El,则云在像元 (x, y) 处的阴影会沿太阳入射方向的反方向落到地面,水平位移量为:

L = H / tan(El),位移方向为太阳方位角的反方向(即背向太阳)。

在影像坐标系中(x 向东、y 向北),阴影相对云的偏移为:

dx = -L · sin(Az),dy = -L · cos(Az)

注意符号约定:方位角自正北顺时针度量,因此东向分量为 sin(Az),北向分量为 cos(Az);若影像行号向下增长(常见栅格约定),需将 dy 取反后再换算为行偏移。本节统一采用“x 向东、y 向北”的数学坐标,最后再转换到行列号。

从云掩膜到阴影候选区

实际操作中不需要逐像元求交,而是把云掩膜整体平移 (dx, dy) 得到阴影候选区。关键点有三:

  • 平移量以像元为单位,需用像元大小 res(米/像元)把 L 换算成像素数:L_px = L / res。
  • 平移后超出影像边界的部分应裁剪,边缘处阴影可能落在影像外,属于正常缺失而非错误。
  • 云掩膜本身可能包含薄云、碎云,投影前建议先做小半径闭运算,避免阴影区出现大量孔洞。

云高误差对阴影偏移的敏感性

由 L = H / tan(El) 可知,阴影位移对云高是线性敏感的:云高误差 ΔH 引起的位移误差为 ΔL = ΔH / tan(El)。太阳高度角越低,同样的云高误差被放大得越厉害。例如 El = 30° 时 1/tan(El) ≈ 1.73,而 El = 60° 时仅约 0.58。这意味着低太阳高度角条件下,云高估计不准会直接导致阴影掩膜整体错位,与真实阴影的重合度显著下降。

几何剖面:云高 H 与太阳高度角 El 决定水平位移 L 地面 云体 H(云高) 太阳入射 El 阴影 L = H / tan(El) 平面偏移方向与云高误差放大 东 x 北 y 云像元 阴影候选 (dx, dy) Az ΔL / ΔH = 1 / tan(El) El(°) 倍率 El=30° → 1.73 El=60° → 0.58 低太阳高度角下,云高误差被显著放大,阴影掩膜易整体错位

可执行的投影实现

下面给出一个不依赖 GIS 库的纯 Python 实现,输入为云掩膜(二维布尔数组)、太阳方位角、高度角、云高与像元大小,输出阴影候选掩膜。为便于验证,同时给出一个合成小例子。

import numpy as np

def cloud_shadow_projection(cloud_mask, sun_az_deg, sun_el_deg,
                            cloud_height_m, pixel_size_m,
                            row_increases_south=True):
    """
    cloud_mask: 2D bool, True=云
    sun_az_deg: 太阳方位角,自正北顺时针
    sun_el_deg: 太阳高度角
    cloud_height_m: 云高估计值(米)
    pixel_size_m: 像元大小(米/像元)
    row_increases_south: 影像行号是否向南增长(常见栅格为 True)
    返回:阴影候选掩膜(bool),与输入同尺寸
    """
    if sun_el_deg <= 0:
        raise ValueError("太阳高度角必须为正")

    # 水平位移(米)与像素数
    L_m = cloud_height_m / np.tan(np.deg2rad(sun_el_deg))
    L_px = L_m / pixel_size_m

    az = np.deg2rad(sun_az_deg)
    # 数学坐标:x 向东,y 向北;阴影沿太阳反方向
    dx = -L_px * np.sin(az)          # 列方向(东为正)
    dy_north = -L_px * np.cos(az)    # 北向为正

    # 转换为行列偏移
    col_shift = int(round(dx))
    if row_increases_south:
        row_shift = int(round(-dy_north))   # 北向为正 → 行号向南增长需取反
    else:
        row_shift = int(round(dy_north))

    # 平移云掩膜
    shadow = np.zeros_like(cloud_mask, dtype=bool)
    rows, cols = cloud_mask.shape
    r_src0 = max(0, -row_shift); r_src1 = min(rows, rows - row_shift)
    c_src0 = max(0, -col_shift); c_src1 = min(cols, cols - col_shift)
    if r_src1 > r_src0 and c_src1 > c_src0:
        shadow[r_src0 + row_shift:r_src1 + row_shift,
               c_src0 + col_shift:c_src1 + col_shift] = \
            cloud_mask[r_src0:r_src1, c_src0:c_src1]
    return shadow, (row_shift, col_shift), L_px

# 合成示例:一块 5x5 云位于影像中部
cloud = np.zeros((40, 40), dtype=bool)
cloud[15:20, 15:20] = True

shadow, shift, L_px = cloud_shadow_projection(
    cloud, sun_az_deg=135, sun_el_deg=35,
    cloud_height_m=2000, pixel_size_m=10,
    row_increases_south=True)

print("位移像素数 L_px =", round(L_px, 2))
print("行列偏移 (row, col) =", shift)
print("阴影像元数 =", shadow.sum())

示例参数下 L = 2000 / tan(35°) ≈ 2856 m,L_px ≈ 285.6 像素,远超 40×40 的合成影像尺寸,因此平移后阴影会完全移出边界、像元数为 0。这恰好说明:当云高与像元大小不匹配时,必须检查位移是否超出影像范围。实际工程中应把云高、像元大小与影像幅宽一起核对,或改用更小的云高/更大的像元做演示。

云高误差的量化评估

为评估云高误差影响,可对一组候选云高做敏感性扫描,统计阴影掩膜相对基准的位移偏差与重叠度(IoU):

def iou(a, b):
    inter = np.logical_and(a, b).sum()
    union = np.logical_or(a, b).sum()
    return inter / union if union else 1.0

base_H = 2000
base_shadow, _, _ = cloud_shadow_projection(
    cloud, 135, 35, base_H, 10, True)

for dH in [-500, -250, 0, 250, 500]:
    s, sh, _ = cloud_shadow_projection(
        cloud, 135, 35, base_H + dH, 10, True)
    print(f"ΔH={dH:+5d} m  偏移={sh}  IoU={iou(base_shadow, s):.3f}")

预期现象:ΔH 越大,行列偏移变化越明显,IoU 越低;在低太阳高度角(如 20°)下,同样的 ΔH 会造成更大的偏移差。判断依据是:若业务允许的阴影定位误差为若干像元,则可由 ΔL = ΔH / tan(El) / res 反推可接受的云高误差上限。

方位角与行列号的符号约定是本节最容易出错的地方。建议先用一个已知方位(如正南 Az=180°)的小云块做单点验证:正南方向太阳的阴影应落在云的北侧,即行号减小(若行号向南增长)。确认符号后再批量处理。
几何投影得到的是“阴影候选区”,它假设云为水平薄层、地面平坦。山区地形起伏、多层云叠加、云体倾斜都会使真实阴影偏离该候选区,因此后续仍需结合暗目标减法等光谱手段做精化,不能把候选区直接当作最终阴影掩膜。