#! /usr/bin/env python ################################################################# ### 从本地 FABDEM 瓦片库生成 GAMMA 格式 DEM ### ### 输入: 经纬度范围 + 本地 FABDEM ZIP 目录 ### ### 输出: .dem + .dem.par (与 makedem.py 输出完全一致) ### ### Author: Cascade AI + zouyuandong ### ### Date : 2026-03-15 ### ################################################################# import os import sys import re import math import glob import zipfile import tempfile import shutil import argparse import subprocess import numpy as np from pathlib import Path # ========================= GAMMA 参数文件写入 ========================= def write_dempar_file(filepath, corner_lon, corner_lat, post_lon, post_lat, width, nlines, data_format='REAL*4'): """写入 GAMMA DEM 参数文件 (.dem.par),与 makedem.py 格式完全一致""" with open(filepath, 'w') as f: f.write("Gamma DIFF&GEO DEM/MAP parameter file\n") f.write("title:\tIMPORTED DEM FROM FABDEM V1-2\n") f.write("DEM_projection: EQA\n") f.write("data_format: %s\n" % data_format) f.write("DEM_hgt_offset: 0.00000\n") f.write("DEM_scale: 1.00000\n") f.write("width: %s\n" % str(int(width))) f.write("nlines: %s\n" % str(int(nlines))) f.write("corner_lat: %s decimal degrees\n" % str(float(corner_lat))) f.write("corner_lon: %s decimal degrees\n" % str(float(corner_lon))) f.write("post_lat: %s decimal degrees\n" % str(float(post_lat))) f.write("post_lon: %s decimal degrees\n" % str(float(post_lon))) f.write("\n") f.write("ellipsoid_name: WGS 84\n") f.write("ellipsoid_ra: 6378137.000 m\n") f.write("ellipsoid_reciprocal_flattening: 298.2572236\n") f.write("\n") f.write("datum_name: WGS 1984\n") f.write("datum_shift_dx: 0.000 m\n") f.write("datum_shift_dy: 0.000 m\n") f.write("datum_shift_dz: 0.000 m\n") f.write("datum_scale_m: 0.00000e+00\n") f.write("datum_rotation_alpha: 0.00000e+00 arc-sec\n") f.write("datum_rotation_beta: 0.00000e+00 arc-sec\n") f.write("datum_rotation_gamma: 0.00000e+00 arc-sec\n") f.write("datum_country_list Global Definition, WGS84, World\n") f.write("\n") # ========================= FABDEM 瓦片索引逻辑 ========================= def parse_fabdem_zip_name(zip_name): """解析 FABDEM ZIP 文件名,提取覆盖的经纬度范围 示例: N30E120-N40E130_FABDEM_V1-2.zip → (lat_min=30, lon_min=120, lat_max=40, lon_max=130) S10W080-S00W070_FABDEM_V1-2.zip → (lat_min=-10, lon_min=-80, lat_max=0, lon_max=-70) """ basename = Path(zip_name).stem # N30E120-N40E130_FABDEM_V1-2 match = re.match( r'([NS])(\d+)([EW])(\d+)-([NS])(\d+)([EW])(\d+)_FABDEM', basename ) if not match: return None lat1 = int(match.group(2)) * (1 if match.group(1) == 'N' else -1) lon1 = int(match.group(4)) * (1 if match.group(3) == 'E' else -1) lat2 = int(match.group(6)) * (1 if match.group(5) == 'N' else -1) lon2 = int(match.group(8)) * (1 if match.group(7) == 'E' else -1) return { 'lat_min': min(lat1, lat2), 'lat_max': max(lat1, lat2), 'lon_min': min(lon1, lon2), 'lon_max': max(lon1, lon2), } def tile_name_for_coord(lat, lon): """根据经纬度生成 FABDEM 1°×1° 瓦片文件名 参数 lat, lon 为瓦片左下角整数坐标 示例: (31, 121) → 'N31E121_FABDEM_V1-2.tif' (-1, -70) → 'S01W070_FABDEM_V1-2.tif' """ lat_prefix = 'N' if lat >= 0 else 'S' lon_prefix = 'E' if lon >= 0 else 'W' return f"{lat_prefix}{abs(lat):02d}{lon_prefix}{abs(lon):03d}_FABDEM_V1-2.tif" def find_needed_tiles(west, south, east, north): """计算覆盖目标区域所需的全部 1°×1° 瓦片坐标列表 返回: [(lat, lon), ...] 瓦片左下角坐标 """ # 向下取整到最近的整数度 lat_start = math.floor(south) lat_end = math.ceil(north) # 不含 lon_start = math.floor(west) lon_end = math.ceil(east) tiles = [] for lat in range(lat_start, lat_end): for lon in range(lon_start, lon_end): tiles.append((lat, lon)) return tiles def find_zip_for_tile(lat, lon, fabdem_dir): """查找包含指定 1°×1° 瓦片的 ZIP 文件 FABDEM ZIP 按 10°×10° 分块,文件名编码了覆盖范围 """ fabdem_path = Path(fabdem_dir) zip_files = sorted(fabdem_path.glob('*_FABDEM_V1-2.zip')) for zf in zip_files: bounds = parse_fabdem_zip_name(zf.name) if bounds is None: continue if (bounds['lat_min'] <= lat < bounds['lat_max'] and bounds['lon_min'] <= lon < bounds['lon_max']): return zf return None def extract_tiles_from_zips(tile_coords, fabdem_dir, extract_dir): """从 FABDEM ZIP 文件中提取所需的 1°×1° GeoTIFF 瓦片 参数: tile_coords: [(lat, lon), ...] 需要提取的瓦片坐标 fabdem_dir: FABDEM ZIP 文件所在目录 extract_dir: 解压目标目录 返回: 提取成功的 GeoTIFF 文件路径列表 """ extract_path = Path(extract_dir) extract_path.mkdir(parents=True, exist_ok=True) # 按 ZIP 文件分组,避免重复打开同一个 ZIP zip_to_tiles = {} missing_tiles = [] for lat, lon in tile_coords: zip_file = find_zip_for_tile(lat, lon, fabdem_dir) if zip_file is None: tile_name = tile_name_for_coord(lat, lon) print(f" ⚠ 未找到包含 {tile_name} 的 ZIP 文件 (lat={lat}, lon={lon})") missing_tiles.append((lat, lon)) continue zip_key = str(zip_file) if zip_key not in zip_to_tiles: zip_to_tiles[zip_key] = [] zip_to_tiles[zip_key].append((lat, lon)) # 逐个 ZIP 文件提取 extracted_files = [] for zip_path, coords in zip_to_tiles.items(): zip_name = Path(zip_path).name print(f" 📦 从 {zip_name} 提取 {len(coords)} 个瓦片...") try: with zipfile.ZipFile(zip_path, 'r') as zf: zip_contents = zf.namelist() for lat, lon in coords: tile_name = tile_name_for_coord(lat, lon) if tile_name in zip_contents: zf.extract(tile_name, extract_dir) out_file = extract_path / tile_name if out_file.exists() and out_file.stat().st_size > 0: extracted_files.append(str(out_file)) print(f" ✓ {tile_name}") else: print(f" ✗ {tile_name} 提取后为空") else: # 海洋区域可能没有对应瓦片(正常) print(f" - {tile_name} 不在 ZIP 中(可能是海洋区域)") except Exception as e: print(f" ✗ 打开 {zip_name} 失败: {e}") if missing_tiles: print(f"\n ⚠ 共 {len(missing_tiles)} 个瓦片未找到对应的 ZIP 文件") return extracted_files # ========================= 瓦片拼接 + GAMMA 格式转换(一步完成) ========================= def _parse_gdalinfo(gdalinfo_text): """从 gdalinfo 输出解析元数据""" width = nlines = None corner_lon = corner_lat = post_lon = post_lat = None for line in gdalinfo_text.splitlines(): if 'Size is' in line: parts = line.split('Size is')[1].strip().split(',') width = int(parts[0].strip()) nlines = int(parts[1].strip()) elif 'Origin =' in line: parts = line.split('(')[1].split(')')[0].split(',') corner_lon = float(parts[0].strip()) corner_lat = float(parts[1].strip()) elif 'Pixel Size =' in line: parts = line.split('(')[1].split(')')[0].split(',') post_lon = float(parts[0].strip()) post_lat = float(parts[1].strip()) return width, nlines, corner_lon, corner_lat, post_lon, post_lat def tiles_to_gamma_dem(tif_files, output_name, west, south, east, north, byteorder='big'): """从多个 GeoTIFF 瓦片生成 GAMMA 格式 DEM (.dem + .dem.par) 流程: srtm2dem 逐个转换 → mosaic 合并(全部使用 GAMMA 原生工具) 参数: tif_files: GeoTIFF 瓦片文件路径列表 output_name: 输出文件名前缀(不含扩展名) west, south, east, north: 裁剪范围(度) byteorder: 字节序(未使用,srtm2dem 自动处理大端) 返回: (dem_file, dem_par_file) 或 (None, None) """ if not tif_files: print(" ✗ 没有可用的瓦片文件") return None, None Path(output_name).parent.mkdir(parents=True, exist_ok=True) dem_file = output_name + '.dem' dem_par_file = output_name + '.dem.par' # 临时目录放 /tmp/(本地盘),srtm2dem 单瓦片很快 import tempfile as _tf _tmp_dir = _tf.mkdtemp(prefix='gamma_dem_conv_') # ---- 步骤1: srtm2dem 逐个将 GeoTIFF 转为 GAMMA 格式 ---- n = len(tif_files) print(f" [1/2] srtm2dem 转换 {n} 个瓦片...") gamma_tiles = [] # [(dem_path, dem_par_path), ...] for i, tif in enumerate(tif_files): tile_name = Path(tif).stem tile_dem = os.path.join(_tmp_dir, f'{tile_name}.dem') tile_par = os.path.join(_tmp_dir, f'{tile_name}.dem.par') # gflg=3: 不做大地水准面校正, NODATA 替换为 0.0 cmd = f'srtm2dem {tif} {tile_dem} {tile_par} 3' ret = os.system(cmd + ' > /dev/null 2>&1') if ret == 0 and os.path.exists(tile_dem) and os.path.getsize(tile_dem) > 0: gamma_tiles.append((tile_dem, tile_par)) print(f" ✓ [{i+1}/{n}] {tile_name}") else: print(f" ✗ [{i+1}/{n}] {tile_name} srtm2dem 失败") if not gamma_tiles: print(" ✗ 所有瓦片转换失败") shutil.rmtree(_tmp_dir, ignore_errors=True) return None, None print(f" 成功转换 {len(gamma_tiles)}/{n} 个瓦片") # ---- 步骤2: mosaic 合并所有 GAMMA DEM 瓦片 ---- if len(gamma_tiles) == 1: # 只有一个瓦片,直接复制 shutil.copy2(gamma_tiles[0][0], dem_file) shutil.copy2(gamma_tiles[0][1], dem_par_file) print(f" [2/2] 单瓦片,直接输出") else: print(f" [2/2] mosaic 合并 {len(gamma_tiles)} 个 GAMMA DEM...") # 构建 mosaic 命令: mosaic nfiles dem1 par1 dem2 par2 ... dem_out par_out mode format cmd_parts = ['mosaic', str(len(gamma_tiles))] for tile_dem, tile_par in gamma_tiles: cmd_parts.append(tile_dem) cmd_parts.append(tile_par) cmd_parts.extend([dem_file, dem_par_file, '1', '0']) # mode=1: 重叠区取平均, format=0: FLOAT cmd_str = ' '.join(cmd_parts) ret = os.system(cmd_str) if ret != 0 or not os.path.exists(dem_file) or os.path.getsize(dem_file) == 0: print(f" ✗ mosaic 失败 (exit={ret})") shutil.rmtree(_tmp_dir, ignore_errors=True) return None, None # 清理临时目录 shutil.rmtree(_tmp_dir, ignore_errors=True) size_mb = os.path.getsize(dem_file) / (1024 * 1024) print(f" ✓ {dem_file} ({size_mb:.1f} MB)") print(f" ✓ {dem_par_file}") return dem_file, dem_par_file # ========================= 主流程 ========================= INTRODUCTION = ''' ================================================================================ make_local_dem.py — 从本地 FABDEM 瓦片库生成 GAMMA 格式 DEM 功能: 1. 根据经纬度范围自动查找所需的 FABDEM 1°×1° 瓦片 2. 从 10°×10° ZIP 包中提取瓦片 3. 使用 GDAL 拼接并裁剪到目标范围 4. 转换为 GAMMA 格式 (.dem + .dem.par) 输出与 makedem.py 完全一致,可直接用于 generate_rdc_dem.py ================================================================================ ''' EXAMPLE = """ 用法: # 方式1: 指定经纬度范围 make_local_dem.py -r 120/123/30/32 -f /mnt/ZYD/全球FABDEM -o output_dem # 方式2: 从 SLC 参数文件自动确定范围 make_local_dem.py -s master.slc.par -f /mnt/ZYD/全球FABDEM -o output_dem # 方式3: 从 PyINT 模板文件读取(集成到工作流) make_local_dem.py --template shanghaiT171F128S1A -f /mnt/ZYD/全球FABDEM """ def cmdLineParse(): parser = argparse.ArgumentParser( description='从本地 FABDEM 瓦片库生成 GAMMA 格式 DEM', formatter_class=argparse.RawTextHelpFormatter, epilog=INTRODUCTION + '\n' + EXAMPLE ) parser.add_argument('-r', dest='region', help='研究区域范围: west/east/south/north (如: 120/123/30/32)') parser.add_argument('-s', dest='slc_par', help='SLC 参数文件路径(自动从中提取研究区域范围)') parser.add_argument('-f', '--fabdem-dir', dest='fabdem_dir', required=True, help='本地 FABDEM ZIP 文件目录 (如: /mnt/ZYD/全球FABDEM)') parser.add_argument('-o', '--output', dest='output_name', default=None, help='输出文件名前缀(不含扩展名)[默认: 工作目录/out]') parser.add_argument('--dir', dest='work_dir', default=None, help='工作目录 [默认: 当前目录]') parser.add_argument('--template', dest='template_name', default=None, help='PyINT 项目名(从模板文件读取 DEM 路径和 SLC 位置)') parser.add_argument('--byteorder', dest='byteorder', choices=['big', 'little'], default='big', help='输出 DEM 字节序 [默认: big (GAMMA 标准)]') parser.add_argument('--margin', dest='margin', type=float, default=1.0, help='在 SLC 覆盖范围外扩展的余量(度)[默认: 1.0]') return parser.parse_args() def get_region_from_slc_par(slc_par_file): """从 GAMMA SLC 参数文件提取研究区域范围""" corners_txt = 'corners_tmp.txt' call_str = f"SLC_corners {slc_par_file} > {corners_txt}" os.system(call_str) if not os.path.isfile(corners_txt): print(f"✗ SLC_corners 执行失败") return None with open(corners_txt, 'r') as f: lines = f.readlines() os.remove(corners_txt) # 解析 SLC_corners 输出(第 9-10 行包含 lat/lon 范围) try: lat_line = lines[8] lon_line = lines[9] min_lat = float(lat_line.split(':')[1].split('max.')[0].strip()) max_lat = float(lat_line.split(':')[2].strip()) min_lon = float(lon_line.split(':')[1].split('max.')[0].strip()) max_lon = float(lon_line.split(':')[2].strip()) return min_lon, min_lat, max_lon, max_lat except (IndexError, ValueError) as e: print(f"✗ 解析 SLC_corners 输出失败: {e}") return None def get_region_from_template(project_name, margin=1.0): """从 PyINT 模板文件获取区域范围(通过 master SLC 参数文件)""" try: from pyint import _utils as ut except ImportError: print("✗ 无法导入 pyint._utils,请确保 PyINT 在 PYTHONPATH 中") return None, None scratchDir = os.getenv('SCRATCHDIR') templateDir = os.getenv('TEMPLATEDIR') if not scratchDir or not templateDir: print("✗ 环境变量 SCRATCHDIR 或 TEMPLATEDIR 未设置") return None, None template_file = Path(templateDir) / f"{project_name}.template" if not template_file.exists(): print(f"✗ 模板文件不存在: {template_file}") return None, None template_dict = ut.update_template(str(template_file)) master_date = template_dict['masterDate'] slc_dir = Path(scratchDir) / project_name / 'SLC' master_slc_par = slc_dir / master_date / f"{master_date}.slc.par" if not master_slc_par.exists(): print(f"✗ Master SLC 参数文件不存在: {master_slc_par}") return None, None region = get_region_from_slc_par(str(master_slc_par)) if region is None: return None, None west, south, east, north = region # 外扩 margin 度并取整 west = math.floor(west - margin) south = math.floor(south - margin) east = math.ceil(east + margin) north = math.ceil(north + margin) # 确定输出路径(使用模板中 DEM 路径或默认路径) dem_dir = os.getenv('DEMDIR', '') if 'DEM' in template_dict and template_dict['DEM'].strip(): output_name = template_dict['DEM'].replace('.dem', '') else: dem_out_dir = Path(dem_dir) / project_name dem_out_dir.mkdir(parents=True, exist_ok=True) output_name = str(dem_out_dir / project_name) return (west, south, east, north), output_name def main(): args = cmdLineParse() # 确定工作目录 if args.work_dir: work_dir = Path(args.work_dir) else: work_dir = Path.cwd() work_dir.mkdir(parents=True, exist_ok=True) # 确定 FABDEM 目录 fabdem_dir = Path(args.fabdem_dir) if not fabdem_dir.is_dir(): print(f"✗ FABDEM 目录不存在: {fabdem_dir}") sys.exit(1) zip_count = len(list(fabdem_dir.glob('*_FABDEM_V1-2.zip'))) print(f"✓ FABDEM 目录: {fabdem_dir} ({zip_count} 个 ZIP 文件)") # 确定研究区域 output_name = args.output_name region = None if args.template_name: # 从 PyINT 模板获取 result = get_region_from_template(args.template_name, args.margin) if result[0] is not None: region = result[0] if output_name is None: output_name = result[1] else: print("✗ 无法从模板文件获取区域范围") sys.exit(1) elif args.slc_par: # 从 SLC 参数文件获取 slc_region = get_region_from_slc_par(args.slc_par) if slc_region is None: sys.exit(1) west, south, east, north = slc_region west = math.floor(west - args.margin) south = math.floor(south - args.margin) east = math.ceil(east + args.margin) north = math.ceil(north + args.margin) region = (west, south, east, north) elif args.region: # 从命令行参数解析 parts = args.region.split('/') if len(parts) != 4: print("✗ 区域格式错误,应为: west/east/south/north") sys.exit(1) west, east, south, north = [float(x) for x in parts] region = (west, south, east, north) else: print("✗ 必须指定研究区域: -r, -s 或 --template") sys.exit(1) if output_name is None: output_name = str(work_dir / 'out') # 确保输出目录存在 Path(output_name).parent.mkdir(parents=True, exist_ok=True) west, south, east, north = region print(f"\n{'='*70}") print(f" 从本地 FABDEM 生成 GAMMA DEM") print(f"{'='*70}") print(f" 区域范围: {west}°E ~ {east}°E, {south}°N ~ {north}°N") print(f" FABDEM 目录: {fabdem_dir}") print(f" 输出文件: {output_name}.dem / {output_name}.dem.par") print(f" 字节序: {args.byteorder}") print(f"{'='*70}\n") # 1. 计算所需瓦片 tiles = find_needed_tiles(west, south, east, north) print(f"[1/4] 需要 {len(tiles)} 个 1°×1° 瓦片\n") # 2. 从 ZIP 中提取瓦片 print(f"[2/4] 从 FABDEM ZIP 文件中提取瓦片...") temp_dir = tempfile.mkdtemp(prefix='fabdem_tiles_') try: tif_files = extract_tiles_from_zips(tiles, str(fabdem_dir), temp_dir) if not tif_files: print("\n✗ 没有提取到任何瓦片文件,请检查 FABDEM 目录和区域范围") shutil.rmtree(temp_dir, ignore_errors=True) sys.exit(1) print(f"\n 共提取 {len(tif_files)}/{len(tiles)} 个瓦片\n") # 3-4. 一步完成: VRT → ENVI 二进制 → GAMMA .dem print(f"[3/4] 拼接裁剪 + 转换 GAMMA 格式...") dem_file, dem_par_file = tiles_to_gamma_dem( tif_files, output_name, west, south, east, north, args.byteorder ) if dem_file is None: print("\n✗ DEM 生成失败") shutil.rmtree(temp_dir, ignore_errors=True) sys.exit(1) finally: # 清理临时目录 shutil.rmtree(temp_dir, ignore_errors=True) # 完成 print(f"\n{'='*70}") print(f" ✓ FABDEM → GAMMA DEM 转换完成!") print(f"{'='*70}") print(f" DEM 文件: {dem_file}") print(f" 参数文件: {dem_par_file}") print(f" 字节序: {args.byteorder} endian") print(f" 可直接用于: generate_rdc_dem.py") print(f"{'='*70}\n") if __name__ == '__main__': main()