一句话总结

时序 InSAR(TS-InSAR)通过堆叠多幅 SAR 干涉图、反演地表形变时间序列,精度可达毫米量级——但大气噪声、相位解缠错误和网络构建策略会让结果一塌糊涂。DefoEye 把 GMTSAR 的复杂流程包装成 Python 接口,加入干涉图网络剪枝和锚点校正,是当前开源 TS-InSAR 工具链中少见的端到端实现。


背景:为什么 TS-InSAR 这么难用?

InSAR(合成孔径雷达干涉测量)的基本思想很简单:两幅 SAR 图像的相位差包含了地表高程和形变信息。但实际使用中有三个令人头疼的问题:

问题 1:相位是缠绕的(wrapped) SAR 测量的是微波相位,范围是 $[0, 2\pi)$。真实形变可能跨越多个波长,需要”解缠”才能还原真实位移。解缠算法在低相干区域(植被、水体)极易出错,而且错误会蔓延。

问题 2:大气延迟淹没信号 水蒸气分布不均导致的大气相位延迟,可以轻松伪造数厘米的”形变”。Sentinel-1 的 C 波段(5.6 cm 波长)对大气尤其敏感。

问题 3:单幅干涉图太脆弱 任何一幅干涉图都可能因为时间去相干、大气异常或轨道误差而报废。时序方法(TS-InSAR)的本质是用统计冗余抗噪:构建干涉图网络,联合反演时间序列。

现有工具(StaMPS、MintPy、GMTSAR)各有局限:商业软件贵、GMTSAR 需要手动 C-shell 命令、MintPy 缺乏完整的网络剪枝。DefoEye 的定位是:GMTSAR 的 Python 封装 + 网络剪枝 + 解缠锚点校正,补上了关键缺口。


算法原理

直觉解释

把每次 Sentinel-1 过境想成一次”拍照”。两次拍照之间,地面如果下沉了,相位就会变化。我们有 $N$ 个时间点、$M$ 幅干涉图,形成一个网络:

t1 --- t2 --- t3 --- t4
  \   / \   / 
   t2   t3  

短基线干涉图相干性好,但只有短基线覆盖整个时序会漏掉长期趋势。网络剪枝的任务是:去掉低相干的”烂”干涉图,保留能约束时序的好图。

数学推导

第 $k$ 幅干涉图(连接时间点 $i$ 和 $j$)的观测相位:

\[\phi_k = \frac{4\pi}{\lambda}(d_j - d_i) + \phi_k^{atm} + \phi_k^{noise}\]

其中 $d_j, d_i$ 是视线方向(LOS)形变,$\lambda$ 是波长(Sentinel-1 约 5.6 cm)。

SBAS(Small Baseline Subset)方法把问题写成线性系统:

\[\mathbf{A} \mathbf{v} = \boldsymbol{\phi}\]
  • $\mathbf{A}$ 是 $M \times (N-1)$ 的设计矩阵,$A_{ki} = \Delta t_i$(时间间隔)
  • $\mathbf{v}$ 是各时间段的平均形变速率
  • $\boldsymbol{\phi}$ 是解缠后的干涉图相位观测

当网络连通时用最小二乘,网络有子集时用奇异值分解(SVD)或 L1 正则化。

与其他方法的关系

方法 核心思想 优点 局限
PS-InSAR 识别稳定散射体 城市精度高 农村/植被区稀疏
SBAS 小基线网络反演 时空覆盖均匀 解缠错误传播
DefoEye SBAS + 网络剪枝 + 锚点 端到端自动化 依赖 GMTSAR

实现

最小可运行版本:干涉图网络构建与剪枝

import numpy as np
import networkx as nx
from itertools import combinations
from dataclasses import dataclass

@dataclass
class Acquisition:
    date: str          # YYYYMMDD
    b_perp: float      # 垂直基线(相对参考景,米)

def build_interferogram_network(
    acquisitions: list[Acquisition],
    max_temporal_days: int = 180,
    max_spatial_baseline: float = 150.0,  # 米
    min_coherence: float = 0.3,
    coherence_map: dict = None,           # {(i,j): mean_coherence}
) -> nx.Graph:
    """构建小基线干涉图网络并剪枝"""
    G = nx.Graph()
    
    for i, acq in enumerate(acquisitions):
        G.add_node(i, date=acq.date, b_perp=acq.b_perp)
    
    for (i, a1), (j, a2) in combinations(enumerate(acquisitions), 2):
        dt = abs(int(a2.date) - int(a1.date))  # 简化的日期差计算
        db = abs(a2.b_perp - a1.b_perp)
        
        if dt > max_temporal_days * 10000 or db > max_spatial_baseline:
            continue  # 超出基线阈值,跳过
        
        coh = coherence_map.get((i, j), 0.5) if coherence_map else 0.5
        
        if coh >= min_coherence:
            G.add_edge(i, j, dt=dt, db=db, coherence=coh)
    
    # 确保网络连通:若有孤立节点,强制连接最近邻
    components = list(nx.connected_components(G))
    if len(components) > 1:
        for comp in components[1:]:
            node = min(comp)  # 取孤立子图的一个节点
            # 找主图中时间最近的节点
            main_nodes = list(components[0])
            nearest = min(main_nodes, key=lambda n: abs(n - node))
            G.add_edge(node, nearest, dt=999, db=0, coherence=0.3)
    
    return G

核心算法:SBAS 时间序列反演

def sbas_inversion(
    interferograms: list[tuple[int, int]],  # (master_idx, slave_idx)
    unwrapped_phases: np.ndarray,           # shape: (M, pixels)
    acquisition_dates: list[int],           # YYYYMMDD 整数列表
    reg_weight: float = 1e-2,              # 时间平滑正则化强度
) -> np.ndarray:
    """
    SBAS 反演:从 M 幅干涉图 → N-1 个时间段速率 → 累积形变时间序列
    返回 shape: (N, pixels),单位与输入相同(通常是毫米)
    """
    N = len(acquisition_dates)
    M = len(interferograms)
    P = unwrapped_phases.shape[1]  # 像素数
    
    # 计算时间间隔(年)
    def date_to_year(d):
        d = str(d)
        return int(d[:4]) + (int(d[4:6]) - 1) / 12 + int(d[6:]) / 365
    
    years = [date_to_year(d) for d in acquisition_dates]
    
    # 构建设计矩阵 A(M × N-1)
    A = np.zeros((M, N - 1))
    for k, (i, j) in enumerate(interferograms):
        # 干涉图 (i,j):phase = v[i]*dt[i] + ... + v[j-1]*dt[j-1]
        for t in range(i, j):
            A[k, t] = years[t + 1] - years[t]  # 该时段的时间长度
    
    # 添加时间平滑正则化(抑制速率突变)
    L = np.zeros((N - 2, N - 1))
    for i in range(N - 2):
        L[i, i] = 1; L[i, i + 1] = -1
    
    A_reg = np.vstack([A, reg_weight * L])
    b_reg = np.vstack([unwrapped_phases, np.zeros((N - 2, P))])
    
    # 最小二乘求解(SVD 处理网络不连通情况)
    v, _, _, _ = np.linalg.lstsq(A_reg, b_reg, rcond=None)  # shape: (N-1, P)
    
    # 积分得到累积形变
    dt = np.array([years[t + 1] - years[t] for t in range(N - 1)])
    displacement = np.cumsum(v * dt[:, None], axis=0)  # shape: (N-1, P)
    
    # 在第一个时间点之前插入零(参考时刻无形变)
    return np.vstack([np.zeros((1, P)), displacement])

解缠锚点校正

解缠后的相位可能含有整数 $2\pi$ 倍的偏差,锚点校正是关键 trick:

def anchor_unwrapped_phase(
    unwrapped: np.ndarray,   # shape: (M, rows, cols)
    anchor_mask: np.ndarray, # shape: (rows, cols),稳定参考区域为 True
) -> np.ndarray:
    """
    将每幅干涉图的解缠相位对齐到参考区域均值为 0
    anchor_mask 通常选取已知稳定的基岩区或 GNSS 站点周边
    """
    corrected = unwrapped.copy()
    for k in range(len(unwrapped)):
        anchor_values = unwrapped[k][anchor_mask]
        if anchor_values.size == 0:
            continue
        # 去除异常值后取均值
        q25, q75 = np.percentile(anchor_values, [25, 75])
        valid = anchor_values[(anchor_values >= q25) & (anchor_values <= q75)]
        corrected[k] -= valid.mean()
    return corrected

关键 Trick(没有就跑不起来)

1. 大气延迟估计与去除

def remove_atmospheric_ramp(phase_2d: np.ndarray) -> np.ndarray:
    """最简单的大气校正:拟合线性倾斜面并去除"""
    rows, cols = phase_2d.shape
    y, x = np.mgrid[0:rows, 0:cols]
    valid = ~np.isnan(phase_2d)
    
    # 线性最小二乘:phase = a*x + b*y + c
    A = np.column_stack([x[valid], y[valid], np.ones(valid.sum())])
    coeffs, _, _, _ = np.linalg.lstsq(A, phase_2d[valid], rcond=None)
    
    ramp = coeffs[0] * x + coeffs[1] * y + coeffs[2]
    return phase_2d - ramp

注意:线性倾斜面只是近似,真实大气是非线性的。ERA5 气象再分析数据校正效果更好,但需要额外下载。

2. 相干性加权

SBAS 反演时应该给高相干像素更高权重,否则噪声像素会污染结果:

# 在 sbas_inversion 中加入相干性加权
weights = coherence_stack.reshape(M, -1)  # (M, P)
A_weighted = A * weights.mean(axis=1, keepdims=True)  # 简化版

3. 参考点选择

  • 不要选在形变区域内
  • 避免植被覆盖区域(时间去相干)
  • 最好有附近的 GNSS 站可以验证
  • 多个参考点取中位数,比单点鲁棒

实验与验证

与 GNSS 对比

import matplotlib.pyplot as plt

def validate_against_gnss(
    insar_ts: np.ndarray,    # (N,) 某像素的时间序列,单位 mm
    dates: list,             # datetime 列表
    gnss_dates: list,
    gnss_los: np.ndarray,    # GNSS 投影到 LOS 方向的形变,mm
) -> dict:
    """计算 InSAR 与 GNSS 的 RMSE 和 Pearson 相关系数"""
    from scipy import interpolate, stats
    
    # 插值 InSAR 到 GNSS 时间点
    t_insar = np.array([(d - dates[0]).days for d in dates])
    t_gnss = np.array([(d - dates[0]).days for d in gnss_dates])
    
    f = interpolate.interp1d(t_insar, insar_ts, bounds_error=False, fill_value=np.nan)
    insar_at_gnss = f(t_gnss)
    
    valid = ~np.isnan(insar_at_gnss) & ~np.isnan(gnss_los)
    rmse = np.sqrt(np.mean((insar_at_gnss[valid] - gnss_los[valid]) ** 2))
    r, _ = stats.pearsonr(insar_at_gnss[valid], gnss_los[valid])
    
    return {"rmse_mm": rmse, "pearson_r": r}

# DefoEye 论文报告的验证结果(参考值)
results = {
    "Bologna, Italy": {"rmse": 4.3, "r": 0.95},
    "Gotland, Sweden": {"rmse": 8.7, "r": 0.78},
    "Houston, USA":   {"rmse": 11.9, "r": 0.63},
    "Karaj, Iran":    {"rmse": 4.8, "r": 0.98},  # 与其他工具对比
}

Houston 的 RMSE 偏高(11.9 mm)且相关系数偏低(0.63),值得注意——可能是石油开采导致的快速非线性沉降,或者大气噪声较强。论文没有详细分析这一点,是个遗憾。

与其他工具对比

工具 开源 端到端 网络剪枝 并行 易用性
GMTSAR 部分
MintPy 部分
StaMPS 部分
DefoEye

调试指南

常见问题

1. 时间序列完全不动(所有像素趋势为零)

先检查解缠是否成功:

  • 输出相位是否全是 0 或 NaN?
  • 相干性图是否合理(城区应该 > 0.7)?
  • 参考点选在了形变中心?(改到稳定区域)

2. 时间序列呈阶梯状跳变

典型的相位解缠错误。解决方案:

  • 提高相干性阈值,排除低质量干涉图
  • 检查跳变发生的日期对应哪幅干涉图
  • 对该干涉图单独可视化,定位解缠失败区域

3. 所有像素出现同步季节性振荡

几乎一定是大气延迟没去干净。检查:

  • 振荡幅度 > 10 mm 且周期约 1 年 → 季节性大气
  • 用 ERA5 或 GACOS 做大气校正
  • 检查参考点是否在高海拔区(大气效应更强)

4. 边缘区域突然形变异常

解缠的相位连续性在图像边缘常常断裂。用 anchor_mask 排除边缘像素。

超参数调优

参数 推荐值 敏感度 建议
最大时间基线 120-180 天 先用 180 天,若相干性差缩短到 60 天
最大空间基线 100-200 m Sentinel-1 临界基线约 5 km,200 m 很保守
最小相干阈值 0.25-0.4 植被区用 0.25,城区可用 0.4
时间平滑权重 1e-3 到 1e-1 从 1e-2 开始,太大会抹掉真实信号

什么时候用 / 不用?

适用场景 不适用场景
地面沉降监测(矿区、城市) 快速形变(同震位移 > 1 m,相位缠绕严重)
滑坡早期预警(缓慢蠕变) 热带雨林区(时间去相干严重)
地热/水文储层监测 需要米级精度的工程测量
火山形变研究 高海拔积雪区(季节性去相干)

我的观点

DefoEye 填补了一个真实的工程缺口:GMTSAR 的处理质量不错,但手动流程让大量用户望而却步。把它包装成 Python + 自动化流水线,是正确的工程决策。

但几个值得警惕的地方:

1. Houston 验证结果需要解释。RMSE 11.9 mm、相关系数 0.63 在 TS-InSAR 社区里属于偏弱的结果,论文没有深入分析原因。是算法局限还是 GNSS 本身有问题?

2. 大气校正是软肋。论文没有明确说使用了哪种大气校正方法。这是 TS-InSAR 结果质量最关键的因素之一,缺乏说明让结果可重复性存疑。

3. 工具链锁定。依赖 GMTSAR 意味着 DefoEye 继承了 GMTSAR 的所有限制。如果未来 SAR 社区迁移到其他处理引擎(如 ISCE3),DefoEye 的适用性会受限。

值不值得用?如果你的需求是 Sentinel-1 的标准沉降监测、GMTSAR 本地环境已搭好,DefoEye 能帮你省去大量手动步骤。如果你需要更精细的大气校正或者 PS-InSAR 分析,MintPy 的生态更成熟。

InSAR 这条路没有捷径:大气、解缠、参考点,每一关都要认真对待,工具再自动化也代替不了对数据的理解。