Files
insar-management-system-v2/third_party/PyINT/pyint/gacos_gamma.py
T

1187 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#! /usr/bin/env python
#################################################################
### This program is part of PyINT v2.1 ###
### Copy Right (c): 2017-2019, Yunmeng Cao ###
### Author: Yunmeng Cao ###
### Contact : ymcmrs@gmail.com ###
#################################################################
"""
GACOS atmospheric correction for GAMMA interferograms.
This script applies GACOS-derived tropospheric corrections to unwrapped interferograms.
GACOS (Generic Atmospheric Correction Online Service) provides zenith total delay (ZTD)
maps that can be used to correct for atmospheric phase delays in InSAR data.
Usage:
gacos_gamma.py projectName Mdate-Sdate
gacos_gamma.py PacayaT163TsxHhA 20150102-20150601
"""
import numpy as np
import os
import sys
import argparse
import subprocess
import glob
import warnings
from pathlib import Path
from datetime import datetime
from scipy.interpolate import RegularGridInterpolator
from pyint import _utils as ut
warnings.filterwarnings('ignore', category=RuntimeWarning)
# Try to import AutoGACOS modules
try:
from gacos import Downloader, Submitter, SarDataset
AUTOGACOS_AVAILABLE = True
except Exception as _e:
AUTOGACOS_AVAILABLE = False
print(f"[DEBUG] AutoGACOS import failed: {type(_e).__name__}: {_e}")
INTRODUCTION = '''
-------------------------------------------------------------------
Apply GACOS atmospheric correction to interferograms.
This script:
1. Generates elevation angle file from GAMMA products
2. Downloads GACOS ZTD data for master and slave dates
3. Applies tropospheric correction to the interferogram
Prerequisites:
- GACOS data for the acquisition dates (or valid email for auto-download)
- Geocoded interferogram from geocode_gamma.py
-------------------------------------------------------------------
'''
EXAMPLE = '''
Usage:
gacos_gamma.py projectName Mdate-Sdate
gacos_gamma.py projectName Mdate-Sdate --ztd-dir /path/to/gacos/data
gacos_gamma.py PacayaT163TsxHhA 20150102-20150601
-------------------------------------------------------------------
'''
class HEADER:
"""Header class for GACOS file format"""
width = 0
length = 0
xfirst = 0.0
yfirst = 0.0
xstep = 0.0
ystep = 0.0
def read_header(filename):
"""Read header information from GACOS .rsc file"""
if not os.path.isfile(filename):
print(filename + " file not exist")
return None
header = HEADER()
with open(filename) as f:
for line in f:
data = line.split()
if len(data) >= 2:
if data[0] == "WIDTH":
header.width = int(data[1])
if data[0] == "FILE_LENGTH":
header.length = int(data[1])
if data[0] == "X_FIRST":
header.xfirst = float(data[1])
if data[0] == "Y_FIRST":
header.yfirst = float(data[1])
if data[0] == "X_STEP":
header.xstep = float(data[1])
if data[0] == "Y_STEP":
header.ystep = float(data[1])
return header
def cut_image2(filename, headername, yfirst_new, length_new, xfirst_new, width_new):
"""Cut GACOS ZTD image to match interferogram extent"""
header = read_header(headername)
if header is None:
return None
with open(filename, 'rb') as f:
data0 = np.fromfile(f, dtype=np.float32)
data = np.reshape(data0, (header.length, header.width))
out = np.zeros((length_new, width_new), dtype=np.float32)
for i in range(header.length):
lat = header.yfirst + header.ystep * i
row = int(round((lat - yfirst_new) / header.ystep))
if row < 0 or row >= length_new:
continue
for j in range(header.width):
lon = header.xfirst + header.xstep * j
col = int(round((lon - xfirst_new) / header.xstep))
if col < 0 or col >= width_new:
continue
out[row, col] = data[i, j]
out = np.where(out == 0, np.nan, out)
out.tofile(filename + ".cut")
f = open(filename + ".cut.rsc", 'w')
f.write("WIDTH " + str(width_new) + "\n")
f.write("FILE_LENGTH " + str(length_new) + "\n")
f.write("X_FIRST " + str(xfirst_new) + "\n")
f.write("Y_FIRST " + str(yfirst_new) + "\n")
f.write("X_STEP " + str(header.xstep) + "\n")
f.write("Y_STEP " + str(header.ystep) + "\n")
f.close()
return filename + ".cut"
def make_correction(phsfilename, ztd1filename, ztd2filename, elevfilename, wavelength=None):
"""
Apply GACOS tropospheric correction to interferogram.
Parameters:
-----------
phsfilename : str
Path to unwrapped phase file
ztd1filename : str
Path to GACOS ZTD file for master date
ztd2filename : str
Path to GACOS ZTD file for slave date
elevfilename : str
Path to elevation angle file
wavelength : float, optional
Radar wavelength in meters. If None, read from template.
"""
header = read_header(phsfilename + ".rsc")
if header is None:
print("Error reading phase header file")
return None
# Cut ZTD files to match interferogram extent
if not os.path.isfile(ztd1filename + ".cut"):
cut_image2(ztd1filename, ztd1filename + ".rsc", header.yfirst, header.length, header.xfirst, header.width)
if not os.path.isfile(ztd2filename + ".cut"):
cut_image2(ztd2filename, ztd2filename + ".rsc", header.yfirst, header.length, header.xfirst, header.width)
# Read phase data
with open(phsfilename, 'rb') as f:
data = np.fromfile(f, dtype=np.float32)
phase = np.reshape(data, [header.length, header.width])
# Read ZTD data
with open(ztd1filename + ".cut", 'rb') as f:
data = np.fromfile(f, dtype=np.float32)
ztd1 = np.reshape(data, [header.length, header.width])
with open(ztd2filename + ".cut", 'rb') as f:
data = np.fromfile(f, dtype=np.float32)
ztd2 = np.reshape(data, [header.length, header.width])
# Read elevation angle data
with open(elevfilename, 'rb') as f:
data = np.fromfile(f, dtype=np.float32)
elev = np.reshape(data, [header.length, header.width])
# Calculate tropospheric phase delay
# ZTD difference to phase: phase = 4*pi*dZTD / (wavelength * sin(elevation))
# Default wavelength factor for Sentinel-1: 0.0044138251819503 = 4*pi/lambda
if wavelength:
ztd_factor = 4 * np.pi / wavelength
else:
ztd_factor = 1 / 0.0044138251819503 # Default factor
dztd = ztd2 - ztd1
dztd = dztd / 0.0044138251819503 # Convert ZTD to phase
dztd = dztd / np.sin(elev) # Incidence angle correction
# Apply correction
index = np.where(phase == 0)
phase[index] = np.nan
phasemean = np.nanmean(phase)
print("Phase std before correction: " + str(np.nanstd(phase)))
phase = phase - phasemean
phase = phase - dztd # Apply correction
phase[index] = np.nan
phasemean = np.nanmean(phase)
print("Phase std after correction: " + str(np.nanstd(phase)))
phase = phase - phasemean
phase[index] = 0
phase.tofile(phsfilename + ".gacos")
return phsfilename + ".gacos"
def generate_elevation_angle(workDir, Mdate, Sdate, MampPar, offpar, dempar, dem, rlks):
"""
Generate elevation angle file from GAMMA products.
The elevation angle (90 - incidence angle) is needed for
projecting ZTD to line-of-sight phase delay.
"""
os.chdir(workDir)
# Generate look vector (incidence angle)
call_str = "look_vector " + MampPar + " " + offpar + " " + dempar + " " + dem + " lv_theta lv_phi"
os.system(call_str)
# Get DEM parameters
call_str = "grep 'corner_lat:' " + dempar + " | awk '{print $2}' "
North = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'corner_lon:' " + dempar + " | awk '{print $2}' "
West = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'post_lat:' " + dempar + " | awk '{print $2}' "
posty = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'post_lon:' " + dempar + " | awk '{print $2}' "
postx = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'width:' " + dempar + " | awk '{print $2}' "
width = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'nlines:' " + dempar + " | awk '{print $2}' "
length = subprocess.getstatusoutput(call_str)[1]
South = str(round(float(North) + (float(length) - 1) * float(posty), 7))
East = str(round(float(West) + (float(width) - 1) * float(postx), 7))
# Convert to elevation angle
call_str = "swap_bytes lv_theta lv_theta.phase_swap 4 > dinsar.log "
os.system(call_str)
call_str = "gmt xyz2grd lv_theta.phase_swap -Glv_theta.grd -Ddegree/degree/cm/1/0/=/= -R" + West + "/" + East + "/" + South + "/" + North + " -I" + postx + " -ZTLf -N0"
os.system(call_str)
# Elevation angle = 90 - incidence angle
call_str = "gmt grdmath 90 lv_theta.grd 3.1415926 DIV 180 MUL SUB = lv_theta_final.grd"
os.system(call_str)
call_str = "gmt grdmath 90 lv_theta_final.grd SUB = lv_elev.grd"
os.system(call_str)
# Resample to 3 arc-seconds
call_str = "gmt grdsample lv_elev.grd -Glv_elev_3c.grd -I3c"
os.system(call_str)
# Convert to binary
call_str = "gmt grd2xyz lv_elev_3c.grd -ZTLf -N0 > " + Mdate + '-' + Sdate + '.gacos.elev'
os.system(call_str)
return workDir + '/' + Mdate + '-' + Sdate + '.gacos.elev'
def prepare_unw_for_gacos(workDir, unw, Mdate, Sdate, dempar, rlks):
"""
Prepare unwrapped interferogram for GACOS correction.
Converts GAMMA format to GACOS-compatible format with .rsc file.
"""
os.chdir(workDir)
# Get DEM parameters
call_str = "grep 'corner_lat:' " + dempar + " | awk '{print $2}' "
North = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'corner_lon:' " + dempar + " | awk '{print $2}' "
West = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'post_lat:' " + dempar + " | awk '{print $2}' "
posty = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'post_lon:' " + dempar + " | awk '{print $2}' "
postx = subprocess.getstatusoutput(call_str)[1]
# Convert unw to grd
call_str = "swap_bytes " + unw + " unw.phase_swap 4 > dinsar.log "
os.system(call_str)
# Get dimensions
call_str = "gmt grdinfo unw_f.grd -C"
result = subprocess.getstatusoutput(call_str)[1].split()
if len(result) >= 12:
Width = result[9]
line = result[10]
West = result[1]
North = result[4]
else:
# Fallback to DEM parameters
call_str = "grep 'width:' " + dempar + " | awk '{print $2}' "
Width = subprocess.getstatusoutput(call_str)[1]
call_str = "grep 'nlines:' " + dempar + " | awk '{print $2}' "
line = subprocess.getstatusoutput(call_str)[1]
ymax = str(int(round(float(line), 7)))
xmax = str(int(round(float(Width), 7)))
# Create .rsc file
output = workDir + '/' + Mdate + '-' + Sdate + '.gacos.unw.rsc'
if os.path.exists(output):
os.remove(output)
with open(output, 'a+') as f:
f.write('WIDTH ' + str(Width) + '\n')
f.write('FILE_LENGTH ' + str(line) + '\n')
f.write('XMIN 1' + '\n')
f.write('XMAX ' + xmax + '\n')
f.write('YMIN 1' + '\n')
f.write('YMAX ' + ymax + '\n')
f.write('X_FIRST ' + str(West) + '\n')
f.write('Y_FIRST ' + str(North) + '\n')
f.write('X_STEP 8.33333333E-04' + '\n')
f.write('Y_STEP -8.33333333E-04' + '\n')
f.write('X_UNIT degrees' + '\n')
f.write('Y_UNIT degrees' + '\n')
f.write('Z_OFFSET 0' + '\n')
f.write('Z_SCALE 1' + '\n')
f.write('PROJECTION LATLON' + '\n')
f.write('DATUM WGS84' + '\n')
return workDir + '/' + Mdate + '-' + Sdate + '.gacos.unw'
def parse_dem_par(dempar):
"""
从 GAMMA DEM par 文件读取网格参数(纯 Python,无需 grep/awk
"""
info = {}
with open(dempar) as f:
for line in f:
parts = line.split(':')
if len(parts) >= 2:
key = parts[0].strip()
val = parts[1].strip().split()[0] if parts[1].strip() else ''
info[key] = val
return {
'width': int(info['width']),
'nlines': int(info['nlines']),
'corner_lat': float(info['corner_lat']),
'corner_lon': float(info['corner_lon']),
'post_lat': float(info['post_lat']),
'post_lon': float(info['post_lon']),
}
def resample_ztd_to_dem(ztd_file, rsc_file, dem_info):
"""
读取 ZTD 数据并双线性插值重采样到 DEM 网格
修复: RegularGridInterpolator 要求坐标严格递增,
ZTD 纬度为递减(北→南),需翻转
"""
# 读取 RSC 头文件
rsc = {}
with open(rsc_file) as f:
for line in f:
parts = line.split()
if len(parts) >= 2:
rsc[parts[0]] = parts[1]
ztd_w = int(rsc['WIDTH'])
ztd_h = int(rsc['FILE_LENGTH'])
ztd_x0 = float(rsc['X_FIRST'])
ztd_y0 = float(rsc['Y_FIRST'])
ztd_dx = float(rsc['X_STEP'])
ztd_dy = float(rsc['Y_STEP'])
raw = np.fromfile(ztd_file, dtype=np.float32).reshape(ztd_h, ztd_w)
# 构建 ZTD 网格坐标
ztd_lats = ztd_y0 + np.arange(ztd_h) * ztd_dy
ztd_lons = ztd_x0 + np.arange(ztd_w) * ztd_dx
# 翻转使坐标严格递增(RegularGridInterpolator 要求)
if ztd_lats[0] > ztd_lats[-1]:
ztd_lats = ztd_lats[::-1]
raw = raw[::-1, :]
if ztd_lons[0] > ztd_lons[-1]:
ztd_lons = ztd_lons[::-1]
raw = raw[:, ::-1]
interp = RegularGridInterpolator(
(ztd_lats, ztd_lons), raw,
method='linear', bounds_error=False, fill_value=np.nan
)
# 构建 DEM 网格查询点
dem_w = dem_info['width']
dem_h = dem_info['nlines']
dem_lats = dem_info['corner_lat'] + np.arange(dem_h) * dem_info['post_lat']
dem_lons = dem_info['corner_lon'] + np.arange(dem_w) * dem_info['post_lon']
# 逐行插值(节省内存)
result = np.empty((dem_h, dem_w), dtype=np.float32)
for i in range(dem_h):
pts = np.column_stack([np.full(dem_w, dem_lats[i]), dem_lons])
result[i] = interp(pts).astype(np.float32)
return result
def convert_ztd_tif_to_binary(tif_file):
"""
将 .ztd.tif (GeoTIFF) 转换为 .ztd (raw binary) + .ztd.rsc (header)
返回 .ztd 文件路径
"""
try:
import rasterio
except ImportError:
from osgeo import gdal
ds = gdal.Open(tif_file)
gt = ds.GetGeoTransform()
w, h = ds.RasterXSize, ds.RasterYSize
data = ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
x0, y0, dx, dy = gt[0], gt[3], gt[1], gt[5]
ds = None
else:
with rasterio.open(tif_file) as src:
data = src.read(1).astype(np.float32)
w, h = src.width, src.height
x0 = src.transform[2]
y0 = src.transform[5]
dx = src.transform[0]
dy = src.transform[4]
ztd_file = tif_file.replace('.ztd.tif', '.ztd')
rsc_file = tif_file.replace('.ztd.tif', '.ztd.rsc')
data.tofile(ztd_file)
with open(rsc_file, 'w') as f:
f.write(f"WIDTH {w}\n")
f.write(f"FILE_LENGTH {h}\n")
f.write(f"X_FIRST {x0}\n")
f.write(f"Y_FIRST {y0}\n")
f.write(f"X_STEP {dx}\n")
f.write(f"Y_STEP {dy}\n")
f.write("X_UNIT degrees\n")
f.write("Y_UNIT degrees\n")
f.write("PROJECTION LATLON\n")
f.write("DATUM WGS84\n")
return ztd_file
def apply_gacos_correction_python(geo_unw_file, ztd1_file, ztd1_rsc, ztd2_file, ztd2_rsc,
dempar, slcpar=None, wavelength=None, out_file=None,
figdir=None, pair_str=None):
"""
纯 Python GACOS 大气校正(不依赖 GMT
修复原 make_correction 的所有 Bug:
- 极端值掩码(GAMMA 无效标记)
- sin/cos 弧度转换
- cut_image2 慢速循环 → scipy 向量化重采样
- 增加校正前后对比图
参数:
-----
geo_unw_file : str 地理编码后的解缠相位文件
ztd1_file : str Master 日期 ZTD 二进制文件
ztd1_rsc : str Master 日期 ZTD RSC 头文件
ztd2_file : str Slave 日期 ZTD 二进制文件
ztd2_rsc : str Slave 日期 ZTD RSC 头文件
dempar : str DEM 参数文件
slcpar : str SLC 参数文件(读取入射角,可选)
wavelength : float 雷达波长(m),None则从slcpar读取
out_file : str 输出文件路径(默认 geo_unw_file + '.gacos'
figdir : str 对比图保存目录(None则不绘图)
pair_str : str 干涉对名称如 '20241105-20241117'
返回: 输出文件路径
"""
# 1. 读取 DEM 参数
dem = parse_dem_par(dempar)
dem_w, dem_h = dem['width'], dem['nlines']
# 2. 读取入射角和波长
inc_angle_deg = 39.0 # 默认值
if slcpar and os.path.isfile(slcpar):
with open(slcpar) as f:
for line in f:
if line.startswith('incidence_angle:'):
inc_angle_deg = float(line.split(':')[1].strip().split()[0])
if line.startswith('radar_frequency:') and wavelength is None:
freq = float(line.split(':')[1].strip().split()[0])
wavelength = 299792458.0 / freq
if wavelength is None:
wavelength = 0.0554657595 # Sentinel-1 C-band 默认值
cos_inc = np.cos(np.radians(inc_angle_deg))
ztd_to_phase = 4.0 * np.pi / wavelength
print(f" 入射角: {inc_angle_deg:.4f}°, cos={cos_inc:.6f}")
print(f" 波长: {wavelength:.10f} m")
# 3. 读取地理编码后的解缠相位
unw_data = np.fromfile(geo_unw_file, dtype=np.float32).reshape(dem_h, dem_w)
# 4. 重采样 ZTD 到 DEM 网格
print(" 重采样 ZTD Master...")
ztd_m = resample_ztd_to_dem(ztd1_file, ztd1_rsc, dem)
print(" 重采样 ZTD Slave...")
ztd_s = resample_ztd_to_dem(ztd2_file, ztd2_rsc, dem)
# 5. 计算 LOS 相位校正量
dztd = ztd_s - ztd_m
phase_correction = dztd * ztd_to_phase / cos_inc
# 6. 构建有效像素掩码
# 排除: 零值 | NaN | GAMMA 伪影/解缠错误(|x| >= 1000 rad ≈ 4.4m LOS
PHASE_THRESH = 1000.0
valid = ((unw_data != 0)
& np.isfinite(unw_data)
& (np.abs(unw_data) < PHASE_THRESH)
& np.isfinite(phase_correction))
n_valid = np.sum(valid)
n_total = dem_w * dem_h
print(f" 有效像素: {n_valid}/{n_total} ({n_valid/n_total*100:.1f}%)")
if n_valid == 0:
print(" 错误: 无有效像素,跳过校正")
return None
# 7. 统计校正前
std_before = float(np.std(unw_data[valid]))
# 8. 应用校正(保留原始数据结构,仅修改有效像素)
corrected = unw_data.copy()
corrected[valid] = unw_data[valid] - phase_correction[valid]
# 去均值(仅对有效像素)
mean_val = np.mean(corrected[valid])
corrected[valid] -= mean_val
std_after = float(np.std(corrected[valid]))
reduction = (1 - std_after / std_before) * 100 if std_before > 0 else 0
print(f" 校正前 std: {std_before:.3f} rad")
print(f" 校正后 std: {std_after:.3f} rad")
print(f" std 降低: {reduction:.1f}%")
# 9. 保存
if out_file is None:
out_file = geo_unw_file + '.gacos'
corrected.astype(np.float32).tofile(out_file)
print(f" 输出: {out_file}")
# 10. 绘制对比图
if figdir:
plot_gacos_comparison(unw_data, phase_correction, corrected, valid,
dem, std_before, std_after, pair_str, figdir)
return out_file
def plot_gacos_comparison(unw_before, correction, unw_after, valid_mask,
dem_info, std_before, std_after, pair_str, figdir):
"""
绘制 GACOS 校正前后相位对比图(三子图)
版本控制: 自动追加 _v1, _v2...
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
# 中文字体配置
plt.rcParams['font.sans-serif'] = ['Noto Serif CJK SC', 'Noto Sans CJK SC',
'AR PL UMing CN', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
figdir = Path(figdir)
figdir.mkdir(parents=True, exist_ok=True)
# 版本控制
base_name = f"GACOS_comparison_{pair_str}" if pair_str else "GACOS_comparison"
existing = sorted(figdir.glob(f"{base_name}_v*.png"))
ver = int(existing[-1].stem.split('_v')[-1]) + 1 if existing else 1
out_path = figdir / f"{base_name}_v{ver}.png"
# 准备显示数据(仅显示有效像素)
before_disp = np.full_like(unw_before, np.nan, dtype=np.float64)
before_disp[valid_mask] = unw_before[valid_mask]
corr_disp = np.full_like(correction, np.nan, dtype=np.float64)
corr_disp[valid_mask] = correction[valid_mask]
after_disp = np.full_like(unw_after, np.nan, dtype=np.float64)
after_disp[valid_mask] = unw_after[valid_mask]
# 色标范围:使用有效数据的 P2/P98
vals_b = unw_before[valid_mask]
vals_c = correction[valid_mask]
vals_a = unw_after[valid_mask]
vlim = max(abs(np.percentile(vals_b, 2)), abs(np.percentile(vals_b, 98)), 0.5)
vlim_c = max(abs(np.percentile(vals_c, 2)), abs(np.percentile(vals_c, 98)), 0.5)
vlim_a = max(abs(np.percentile(vals_a, 2)), abs(np.percentile(vals_a, 98)), 0.5)
# 地理坐标范围
w = dem_info['width']
h = dem_info['nlines']
extent = [dem_info['corner_lon'],
dem_info['corner_lon'] + w * dem_info['post_lon'],
dem_info['corner_lat'] + h * dem_info['post_lat'],
dem_info['corner_lat']]
# 降采样显示
step = max(1, h // 2000)
b_s = before_disp[::step, ::step]
c_s = corr_disp[::step, ::step]
a_s = after_disp[::step, ::step]
# 使用带 NaN 灰色背景的 colormap
cmap_phase = plt.cm.RdBu_r.copy()
cmap_phase.set_bad(color='#E0E0E0')
cmap_corr = plt.cm.coolwarm.copy()
cmap_corr.set_bad(color='#E0E0E0')
fig, axes = plt.subplots(1, 3, figsize=(18, 6), dpi=150)
for ax in axes:
ax.set_facecolor('#E0E0E0')
im1 = axes[0].imshow(b_s, cmap=cmap_phase, vmin=-vlim, vmax=vlim,
extent=extent, aspect='auto', interpolation='nearest')
axes[0].set_title(f'Unwrapped Phase (std={std_before:.2f} rad)', fontsize=11, fontweight='bold')
plt.colorbar(im1, ax=axes[0], label='rad', shrink=0.8)
im2 = axes[1].imshow(c_s, cmap=cmap_corr, vmin=-vlim_c, vmax=vlim_c,
extent=extent, aspect='auto', interpolation='nearest')
axes[1].set_title('GACOS Correction', fontsize=11, fontweight='bold')
plt.colorbar(im2, ax=axes[1], label='rad', shrink=0.8)
im3 = axes[2].imshow(a_s, cmap=cmap_phase, vmin=-vlim_a, vmax=vlim_a,
extent=extent, aspect='auto', interpolation='nearest')
axes[2].set_title(f'Corrected (std={std_after:.2f} rad)', fontsize=11, fontweight='bold')
plt.colorbar(im3, ax=axes[2], label='rad', shrink=0.8)
for ax in axes:
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.tick_params(labelsize=9)
reduction = (1 - std_after / std_before) * 100 if std_before > 0 else 0
n_valid = np.sum(valid_mask)
if pair_str:
m, s = pair_str.split('-')
fig.suptitle(f'GACOS: {m}-{s} | valid={n_valid:,} px | std {reduction:+.1f}%',
fontsize=13, fontweight='bold', y=1.02)
plt.tight_layout()
fig.savefig(str(out_path), dpi=150, bbox_inches='tight',
facecolor='white', edgecolor='none')
plt.close(fig)
print(f" 对比图: {out_path}")
def find_existing_ztd(date, gacos_dir):
"""
Find existing GACOS ZTD file for a specific date.
Returns:
--------
ztd_file : str or None
Path to ZTD file (without extension), or None if not found
"""
# Check for .ztd + .ztd.rsc format (preferred)
ztd_pattern = os.path.join(gacos_dir, "**", date + "*.ztd")
ztd_files = glob.glob(ztd_pattern, recursive=True)
if ztd_files:
for ztd_file in ztd_files:
if ztd_file.endswith('.ztd.tif'):
continue
rsc_file = ztd_file + '.rsc'
if os.path.exists(rsc_file):
return ztd_file
# Check for .ztd.tif format, auto-convert if needed
ztd_pattern = os.path.join(gacos_dir, "**", date + "*.ztd.tif")
tif_files = glob.glob(ztd_pattern, recursive=True)
if tif_files:
tif_file = tif_files[0]
ztd_bin = tif_file.replace('.ztd.tif', '.ztd')
rsc_file = tif_file.replace('.ztd.tif', '.ztd.rsc')
if not os.path.exists(ztd_bin) or not os.path.exists(rsc_file):
print(f" Auto-converting {os.path.basename(tif_file)} -> .ztd + .rsc")
convert_ztd_tif_to_binary(tif_file)
if os.path.exists(ztd_bin) and os.path.exists(rsc_file):
return ztd_bin
return None
def submit_gacos_request(dates, bounds, email, gacos_dir, acquisition_time=None):
"""
Submit GACOS data request for multiple dates.
Parameters:
-----------
dates : list
List of acquisition dates in YYYYMMDD format
bounds : tuple
Bounding box (West, South, East, North)
email : str
Email address for GACOS submission
gacos_dir : str
Directory to save GACOS data
acquisition_time : str, optional
Acquisition time in HH:MM format
Returns:
--------
success : bool
True if submission successful
"""
if not AUTOGACOS_AVAILABLE:
print("AutoGACOS module not available. Please download GACOS data manually.")
return False
try:
from gacos import Submitter, SarDataset
import pandas as pd
# Create datetime index
if acquisition_time:
hour, minute = map(int, acquisition_time.split(':'))
else:
hour, minute = 10, 0 # Sentinel-1 升轨默认采集时间 ~10:00 UTC
date_strings = [f"{d} {hour:02d}:{minute:02d}:00" for d in dates]
date_times = pd.to_datetime(date_strings)
# Create dataset
dataset = SarDataset(bounds, date_times, gacos_dir)
print(f"\nSubmitting GACOS request for {len(dates)} dates...")
print(f"Bounds: W={bounds[0]:.4f}, S={bounds[1]:.4f}, E={bounds[2]:.4f}, N={bounds[3]:.4f}")
print(f"Acquisition time: {hour:02d}:{minute:02d} UTC")
print(f"Email: {email}")
# Submit request
submitter = Submitter(dataset, email)
submitter.post_requests()
if submitter.succeed:
print(f"\nSuccessfully submitted {len(submitter.succeed)} requests.")
if submitter.failed:
print(f"\nFailed to submit {len(submitter.failed)} requests.")
return len(submitter.succeed) > 0
except Exception as e:
print(f"Error in GACOS submission: {e}")
import traceback
traceback.print_exc()
return False
def download_gacos_from_email(email_config, gacos_dir, bounds=None, times=None,
submit_time=None):
"""
从邮箱检索 gacos2017@foxmail.com 发来的 GACOS 下载链接并下载数据。
Parameters:
-----------
email_config : dict
邮箱配置: username, password, host, port, ssl
gacos_dir : str
GACOS 数据保存目录
bounds : tuple, optional
边界框过滤 (West, South, East, North)
times : list, optional
采集时间过滤
submit_time : str, optional
提交时间(YYYY-MM-DD HH:MM:SS),仅检索此时间之后的邮件
Returns:
--------
downloaded_files : list
已下载的 ZTD 文件路径列表
"""
if not AUTOGACOS_AVAILABLE:
print("AutoGACOS module not available.")
return []
try:
from gacos import Downloader, GACOSEmail
# Step 1: 从邮箱检索 GACOS 下载链接
print(" 检索 gacos2017@foxmail.com 邮件中的下载链接...")
email_retriever = GACOSEmail(
username=email_config.get('username'),
password=email_config.get('password'),
host=email_config.get('host', 'imap.163.com'),
port=email_config.get('port'),
email_protocol=email_config.get('protocol', 'imap'),
ssl=email_config.get('ssl', True),
gacos_email='gacos2017@foxmail.com',
start_date=submit_time
)
url_file = os.path.join(gacos_dir, 'gacos_urls.csv')
email_retriever.retrieve_gacos_urls(url_file)
if not os.path.exists(url_file):
print(" 未找到 GACOS 下载链接")
return []
# 检查 CSV 是否有内容
import pandas as pd
try:
df = pd.read_csv(url_file)
if len(df) == 0:
print(" 邮件中未发现新的 GACOS 数据链接")
return []
print(f" 找到 {len(df)} 个下载链接")
except Exception:
print(" URL 文件解析失败")
return []
# Step 2: 下载文件
print(" 下载 GACOS 数据...")
dl = Downloader(
url_file=url_file,
output_dir=gacos_dir,
bounds=bounds,
times=times,
keep_original=False
)
dl.download()
# Step 3: 将下载的 .ztd.tif 转换为 .ztd + .rsc
downloaded = []
for tif_file in glob.glob(os.path.join(gacos_dir, "*.ztd.tif")):
rsc_file = tif_file.replace('.ztd.tif', '.ztd.rsc')
if not os.path.exists(rsc_file):
ztd_file = convert_ztd_tif_to_binary(tif_file)
if ztd_file:
downloaded.append(ztd_file)
print(f" 转换: {os.path.basename(tif_file)} → .ztd + .rsc")
else:
downloaded.append(tif_file.replace('.ztd.tif', '.ztd'))
return downloaded
except Exception as e:
print(f" 邮箱检索/下载出错: {e}")
import traceback
traceback.print_exc()
return []
def auto_gacos_workflow(dates, bounds, gacos_dir, email, email_config=None,
acquisition_time=None, wait_for_email=False, max_wait_hours=24,
check_interval=60):
"""
Complete automatic GACOS workflow: submit request, wait, and download.
Parameters:
-----------
dates : list
List of acquisition dates in YYYYMMDD format
bounds : tuple
Bounding box (West, South, East, North)
gacos_dir : str
Directory to save GACOS data
email : str
Email address for GACOS submission
email_config : dict, optional
Email configuration for downloading (username, password, host, etc.)
acquisition_time : str, optional
Acquisition time in HH:MM format
wait_for_email : bool, optional
Whether to wait for email notification before downloading
max_wait_hours : int, optional
Maximum hours to wait for email
Returns:
--------
ztd_files : dict
Dictionary mapping dates to ZTD file paths
"""
import time as time_module
from datetime import datetime, timedelta
ztd_files = {}
missing_dates = []
# Step 1: Check existing files
print("\n" + "="*60)
print("Step 1: Checking existing GACOS data...")
print("="*60)
for date in dates:
ztd_file = find_existing_ztd(date, gacos_dir)
if ztd_file:
ztd_files[date] = ztd_file
else:
missing_dates.append(date)
if not missing_dates:
print("\nAll required GACOS data already exists!")
return ztd_files
print(f"\nMissing GACOS data for {len(missing_dates)} dates:")
for d in missing_dates:
print(f" - {d}")
# Step 2: Submit request for missing dates
print("\n" + "="*60)
print("Step 2: Submitting GACOS requests...")
print("="*60)
submit_success = submit_gacos_request(
dates=missing_dates,
bounds=bounds,
email=email,
gacos_dir=gacos_dir,
acquisition_time=acquisition_time
)
if not submit_success:
print("\n提交失败(可能是重复提交或服务器繁忙),尝试从邮箱获取已有数据...")
else:
print("\nGACOS request submitted successfully!")
print("You will receive an email with download links when data is ready.")
# Step 3: Wait and download from email (if configured)
if email_config and wait_for_email:
print("\n" + "="*60)
print("Step 3: 检查 gacos2017@foxmail.com 邮件并下载...")
print("="*60)
# 记录时间基准,仅检索此时间之后的 GACOS 邮件,过滤掉旧邮件
if submit_success:
submit_time_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
else:
# 提交失败(重复提交),向前回溯 1 小时查找已有邮件
submit_time_str = (datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')
print(f"邮件过滤起始时间: {submit_time_str}")
start_time = datetime.now()
# 提交成功则等 3 分钟再检查;提交失败则立即检查邮箱
first_wait = 180 if submit_success else 5
retry_wait = 120 # 之后每 2 分钟
attempt = 0
# 首次等待
print(f"\n提交完成,等待 {first_wait//60} 分钟后首次检查邮箱...")
time_module.sleep(first_wait)
while (datetime.now() - start_time) < timedelta(hours=max_wait_hours):
attempt += 1
elapsed = datetime.now() - start_time
elapsed_min = int(elapsed.total_seconds() // 60)
elapsed_sec = int(elapsed.total_seconds() % 60)
print(f"\n[第 {attempt} 次检查] 已等待 {elapsed_min}m{elapsed_sec}s")
downloaded = download_gacos_from_email(
email_config=email_config,
gacos_dir=gacos_dir,
bounds=bounds,
submit_time=submit_time_str
)
# 检查缺失日期是否已下载
for date in missing_dates[:]:
ztd_file = find_existing_ztd(date, gacos_dir)
if ztd_file:
ztd_files[date] = ztd_file
missing_dates.remove(date)
print(f" ✓ 已获取: {date}")
if not missing_dates:
print(f"\n所有 {len(dates)} 个日期的 GACOS 数据下载完成!")
break
print(f" 仍缺 {len(missing_dates)} 个日期,{retry_wait//60} 分钟后重试...")
time_module.sleep(retry_wait)
if missing_dates:
print(f"\n超时!仍缺 {len(missing_dates)} 个日期的 GACOS 数据。")
return ztd_files
def cmdLineParse():
parser = argparse.ArgumentParser(description='Apply GACOS atmospheric correction to GAMMA interferograms.',
formatter_class=argparse.RawTextHelpFormatter,
epilog=INTRODUCTION + '\n' + EXAMPLE)
parser.add_argument('projectName', help='projectName for processing.')
parser.add_argument('pair', help='Master-Slave, e.g., 20150101-20150106.')
parser.add_argument('--ztd-dir', dest='ztdDir', help='Directory containing GACOS ZTD files.')
parser.add_argument('--ztd1', dest='ztd1', help='GACOS ZTD file for master date.')
parser.add_argument('--ztd2', dest='ztd2', help='GACOS ZTD file for slave date.')
parser.add_argument('--email', dest='email', help='Email for GACOS auto-download.')
parser.add_argument('--wavelength', dest='wavelength', type=float, help='Radar wavelength in meters.')
# Email configuration for auto-download
parser.add_argument('--email-user', dest='emailUser', help='Email username for downloading.')
parser.add_argument('--email-pass', dest='emailPass', help='Email password for downloading.')
parser.add_argument('--email-host', dest='emailHost', default='imap.gmail.com', help='Email IMAP host.')
parser.add_argument('--email-port', dest='emailPort', type=int, default=993, help='Email IMAP port.')
parser.add_argument('--email-ssl', dest='emailSsl', action='store_true', default=True, help='Use SSL for email.')
# Auto-download options
parser.add_argument('--auto-download', dest='autoDownload', action='store_true',
help='Automatically submit request and download.')
parser.add_argument('--wait-hours', dest='waitHours', type=int, default=24,
help='Maximum hours to wait for email notification.')
inps = parser.parse_args()
return inps
def main(argv):
inps = cmdLineParse()
projectName = inps.projectName
Pair = inps.pair
# Parse dates
Mdate = ut.yyyymmdd(Pair.split('-')[0])
Sdate = ut.yyyymmdd(Pair.split('-')[1])
# Setup directories
scratchDir = os.getenv('SCRATCHDIR')
templateDir = os.getenv('TEMPLATEDIR')
templateFile = templateDir + "/" + projectName + ".template"
templateDict = ut.update_template(templateFile)
rlks = templateDict['range_looks']
azlks = templateDict['azimuth_looks']
masterDate = templateDict['masterDate']
ifgDir = scratchDir + '/' + projectName + "/ifgrams"
demDir = scratchDir + '/' + projectName + "/DEM"
slcDir = scratchDir + '/' + projectName + "/SLC"
workDir = ifgDir + '/' + Pair
# GACOS data directory
if inps.ztdDir:
gacosDir = inps.ztdDir
else:
gacosDir = scratchDir + '/' + projectName + "/GACOS"
if not os.path.exists(gacosDir):
os.makedirs(gacosDir)
# Define input files
dempar = demDir + '/' + masterDate + '_' + rlks + 'rlks.utm.dem.par'
slcpar = slcDir + '/' + masterDate + '/' + masterDate + '.slc.par'
geo_unw = workDir + '/geo_' + Pair + '_' + rlks + 'rlks.diff_filt.unw'
out_file = workDir + '/geo_' + Pair + '_' + rlks + 'rlks.diff_filt.unw.gacos'
figdir = scratchDir + '/' + projectName + "/figure"
# Check if geocoded interferogram exists
if not os.path.isfile(geo_unw):
print(f"Error: Geocoded interferogram not found: {geo_unw}")
print("Please run geocode_gamma.py first.")
sys.exit(1)
print("="*60)
print("GACOS Atmospheric Correction (Pure Python)")
print(f"Project: {projectName}")
print(f"Pair: {Pair}")
print("="*60)
# Step 1: Find GACOS ZTD files
print("\nStep 1: Checking GACOS ZTD files...")
ztd1 = None
ztd2 = None
if inps.ztd1 and inps.ztd2:
ztd1 = inps.ztd1
ztd2 = inps.ztd2
else:
ztd1 = find_existing_ztd(Mdate, gacosDir)
ztd2 = find_existing_ztd(Sdate, gacosDir)
if ztd1 and ztd2:
print(f" Master ZTD: {ztd1}")
print(f" Slave ZTD: {ztd2}")
elif inps.email:
# Auto-download workflow (existing logic preserved)
dem_info = parse_dem_par(dempar)
North = dem_info['corner_lat']
West = dem_info['corner_lon']
South = North + (dem_info['nlines'] - 1) * dem_info['post_lat']
East = West + (dem_info['width'] - 1) * dem_info['post_lon']
bounds = (West, South, East, North)
missing_dates = []
if not ztd1: missing_dates.append(Mdate)
if not ztd2: missing_dates.append(Sdate)
if inps.autoDownload:
email_config = None
if inps.emailUser and inps.emailPass:
email_config = {
'username': inps.emailUser, 'password': inps.emailPass,
'host': inps.emailHost, 'port': inps.emailPort, 'ssl': inps.emailSsl
}
ztd_files = auto_gacos_workflow(
dates=missing_dates, bounds=bounds, gacos_dir=gacosDir,
email=inps.email, email_config=email_config,
wait_for_email=(email_config is not None), max_wait_hours=inps.waitHours
)
ztd1 = ztd1 or ztd_files.get(Mdate)
ztd2 = ztd2 or ztd_files.get(Sdate)
else:
submit_gacos_request(dates=missing_dates, bounds=bounds,
email=inps.email, gacos_dir=gacosDir)
print(f"\nGACOS request submitted for: {missing_dates}")
sys.exit(0)
if ztd1 is None or ztd2 is None:
print("\nError: GACOS ZTD files not found.")
print(f" Missing: {Mdate if not ztd1 else ''} {Sdate if not ztd2 else ''}")
print(f" Search dir: {gacosDir}")
sys.exit(1)
# Step 2: Apply GACOS correction (Pure Python, no GMT dependency)
print("\nStep 2: Applying GACOS correction...")
ztd1_rsc = ztd1 + '.rsc'
ztd2_rsc = ztd2 + '.rsc'
corrected_file = apply_gacos_correction_python(
geo_unw_file=geo_unw,
ztd1_file=ztd1, ztd1_rsc=ztd1_rsc,
ztd2_file=ztd2, ztd2_rsc=ztd2_rsc,
dempar=dempar,
slcpar=slcpar,
wavelength=inps.wavelength,
out_file=out_file,
figdir=figdir,
pair_str=Pair
)
if corrected_file:
# Step 3: Generate BMP preview using rasdt_pwr
print("\nStep 3: Generating BMP preview...")
geo_amp = workDir + '/geo_' + masterDate + '_' + rlks + 'rlks.amp'
nWidthUTMDEM = str(parse_dem_par(dempar)['width'])
if os.path.isfile(geo_amp):
call_str = ('rasdt_pwr ' + corrected_file + ' ' + geo_amp + ' '
+ nWidthUTMDEM + ' - - - - -3.14 3.14 1 rmg.cm')
print(f" {call_str}")
os.system(call_str)
bmp_file = corrected_file + '.bmp'
if os.path.isfile(bmp_file):
print(f" BMP: {bmp_file}")
else:
print(" Warning: BMP file not generated")
else:
print(f" Warning: geo_amp not found: {geo_amp}")
print(f"\nGACOS atmospheric correction completed successfully!")
else:
print("\nGACOS correction failed!")
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main(sys.argv[:])