1487 lines
56 KiB
Python
1487 lines
56 KiB
Python
#! /usr/bin/env python
|
||
import os
|
||
import sys
|
||
import numpy as np
|
||
import argparse
|
||
import subprocess
|
||
import glob
|
||
from skimage import io
|
||
|
||
# 尝试导入 NASADEM 库
|
||
try:
|
||
from NASADEM import NASADEM
|
||
from rasters import RasterGrid
|
||
HAS_NASADEM = True
|
||
except ImportError:
|
||
HAS_NASADEM = False
|
||
print("Warning: NASADEM library not installed. NASADEM download will not be available.")
|
||
print("Install with: pip install nasadem rasters")
|
||
|
||
# 尝试导入 srtm 库
|
||
try:
|
||
import srtm
|
||
HAS_SRTM = True
|
||
except ImportError:
|
||
HAS_SRTM = False
|
||
print("Warning: srtm library not installed. SRTM download will not be available.")
|
||
print("Install with: pip install srtm (requires Python >= 3.12)")
|
||
print(f"Current Python version: {sys.version_info.major}.{sys.version_info.minor}")
|
||
|
||
resolutions = 30 # 90
|
||
|
||
def write_demrsc_file(FILE, Corner_LON, Corner_LAT, X_STEP, Y_STEP, WIDTH, LENGTH):
|
||
"""Write ROI_PAC DEM resource file (.dem.rsc)
|
||
|
||
Args:
|
||
FILE (str): Output resource file path
|
||
Corner_LON (str): Corner longitude
|
||
Corner_LAT (str): Corner latitude
|
||
X_STEP (str): Longitude step size
|
||
Y_STEP (str): Latitude step size
|
||
WIDTH (str): Number of columns
|
||
LENGTH (str): Number of lines
|
||
"""
|
||
f = open(FILE, 'w')
|
||
f.write('DATE12 111111-222222\n')
|
||
f.write('FILE_LENGTH ' + str(int(LENGTH)) + '\n')
|
||
f.write('FILE_TYPE .dem\n')
|
||
f.write('PROCESSOR roipac\n')
|
||
f.write('PROJECTION LATLON\n')
|
||
f.write('RLOOKS 1\n')
|
||
f.write('WIDTH ' + str(int(WIDTH)) + '\n')
|
||
f.write('XMAX ' + str(int(int(WIDTH)-1)) + '\n')
|
||
f.write('XMIN 0\n')
|
||
f.write('X_FIRST ' + str(float(Corner_LON)) + '\n')
|
||
f.write('X_STEP ' + str(float(X_STEP)) + '\n')
|
||
f.write('X_UNIT degrees\n')
|
||
f.write('YMAX ' + str(int(int(LENGTH)-1)) + '\n')
|
||
f.write('YMIN 0\n')
|
||
f.write('Y_FIRST ' + str(float(Corner_LAT)) + '\n')
|
||
f.write('Y_STEP ' + str(float(Y_STEP)) + '\n')
|
||
f.write('Y_UNIT degrees\n')
|
||
f.write('Z_OFFSET 0\n')
|
||
f.write('Z_SCALE 1\n')
|
||
f.close()
|
||
|
||
def write_dempar_file(FILE, Corner_LON, Corner_LAT, X_STEP, Y_STEP, WIDTH, LENGTH, DATA_FORMAT):
|
||
"""Write Gamma DEM parameter file (.dem.par)
|
||
|
||
Args:
|
||
FILE (str): Output parameter file path
|
||
Corner_LON (str): Corner longitude
|
||
Corner_LAT (str): Corner latitude
|
||
X_STEP (str): Longitude step size
|
||
Y_STEP (str): Latitude step size
|
||
WIDTH (str): Number of columns
|
||
LENGTH (str): Number of lines
|
||
DATA_FORMAT (str): Data format (INTEGER*2 or REAL*4)
|
||
"""
|
||
DEM_TYPE = 'Copernicus30'
|
||
Proj = 'EQA'
|
||
f = open(FILE, 'w')
|
||
f.write("Gamma DIFF&GEO DEM/MAP parameter file\n")
|
||
f.write("title:\tIMPORTED DEM FROM %s\n" % DEM_TYPE)
|
||
f.write("DEM_projection: %s\n" % Proj)
|
||
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" % WIDTH)
|
||
f.write("nlines: %s\n" % LENGTH)
|
||
f.write("corner_lat: %s decimal degrees\n" % Corner_LAT)
|
||
f.write("corner_lon: %s decimal degrees\n" % Corner_LON)
|
||
f.write("post_lat: %s decimal degrees\n" % Y_STEP)
|
||
f.write("post_lon: %s decimal degrees\n" % X_STEP)
|
||
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")
|
||
f.close()
|
||
|
||
def convert_to_gamma(input_file, output_name, byteorder='big', processor='gamma'):
|
||
"""Convert TIF file to Gamma or ROI_PAC format (.dem and .dem.par/.dem.rsc)
|
||
|
||
Args:
|
||
input_file (str): Input TIF file path
|
||
output_name (str): Output name (without extension)
|
||
byteorder (str): Byte order ('big' or 'little')
|
||
processor (str): Processor type ('gamma' or 'roi_pac')
|
||
"""
|
||
processor_name = 'Gamma' if processor == 'gamma' else 'ROI_PAC'
|
||
print(f"\n开始转换到 {processor_name} 格式: {input_file}")
|
||
|
||
# 读取 DEM 数据 - 优先使用 GDAL(更稳定),然后是 rasterio
|
||
dem_data = None
|
||
|
||
# 方法1: 使用 GDAL(最稳定)
|
||
try:
|
||
from osgeo import gdal
|
||
print(" 尝试使用 GDAL 读取数据...")
|
||
ds = gdal.Open(input_file)
|
||
if ds is not None:
|
||
band = ds.GetRasterBand(1)
|
||
dem_data = band.ReadAsArray()
|
||
print(f" ✓ 使用 GDAL 读取成功")
|
||
ds = None # 关闭文件
|
||
except Exception as e:
|
||
print(f" ✗ GDAL 读取失败: {e}")
|
||
|
||
# 方法2: 如果GDAL失败,尝试rasterio
|
||
if dem_data is None:
|
||
try:
|
||
import rasterio
|
||
print(" 尝试使用 rasterio 读取数据...")
|
||
with rasterio.open(input_file) as src:
|
||
dem_data = src.read(1)
|
||
print(f" ✓ 使用 rasterio 读取成功")
|
||
except Exception as e:
|
||
print(f" ✗ rasterio 读取失败: {e}")
|
||
|
||
# 方法3: 最后尝试skimage
|
||
if dem_data is None:
|
||
try:
|
||
print(" 尝试使用 skimage 读取数据...")
|
||
dem_data = io.imread(input_file)
|
||
print(f" ✓ 使用 skimage 读取成功")
|
||
except Exception as e:
|
||
print(f" ✗ skimage 读取失败: {e}")
|
||
|
||
# 如果所有方法都失败
|
||
if dem_data is None:
|
||
raise ValueError(f"无法读取DEM文件: {input_file},所有读取方法都失败了")
|
||
|
||
# 确定数据格式
|
||
if dem_data.dtype == 'float32':
|
||
DATA_FORMAT = 'REAL*4'
|
||
else:
|
||
DATA_FORMAT = 'INTEGER*2'
|
||
|
||
# 字节序转换
|
||
if not sys.byteorder == byteorder:
|
||
dem_data.byteswap(True)
|
||
|
||
# 输出文件路径
|
||
dem_file = output_name + '.dem'
|
||
if processor == 'gamma':
|
||
dem_par_file = output_name + '.dem.par'
|
||
else:
|
||
dem_par_file = output_name + '.dem.rsc'
|
||
|
||
# 写入二进制 DEM 数据
|
||
dem_data.tofile(dem_file)
|
||
|
||
# 使用 gdalinfo 获取地理信息
|
||
info_file = 'temp_gdalinfo.txt'
|
||
cmd = f"gdalinfo {input_file} > {info_file}"
|
||
os.system(cmd)
|
||
|
||
# 解析地理信息
|
||
Corner_LON = None
|
||
Corner_LAT = None
|
||
Post_LON = None
|
||
Post_LAT = None
|
||
WIDTH = None
|
||
FILE_LENGTH = None
|
||
|
||
with open(info_file, 'r') as f:
|
||
for line in f:
|
||
if 'Origin =' in line:
|
||
parts = line.split('Origin =')[1].strip().split('(')[1].split(')')[0].split(',')
|
||
Corner_LON = parts[0]
|
||
Corner_LAT = parts[1]
|
||
elif 'Pixel Size ' in line:
|
||
parts = line.split('Pixel Size =')[1].strip().split('(')[1].split(')')[0].split(',')
|
||
Post_LON = parts[0]
|
||
Post_LAT = parts[1]
|
||
elif 'Size is' in line:
|
||
parts = line.split('Size is')[1].strip().split(',')
|
||
WIDTH = parts[0]
|
||
FILE_LENGTH = parts[1]
|
||
|
||
# 删除临时文件
|
||
if os.path.exists(info_file):
|
||
os.remove(info_file)
|
||
|
||
# 写入参数文件
|
||
if processor == 'gamma':
|
||
write_dempar_file(dem_par_file, Corner_LON, Corner_LAT, Post_LON, Post_LAT, WIDTH, FILE_LENGTH, DATA_FORMAT)
|
||
else:
|
||
write_demrsc_file(dem_par_file, Corner_LON, Corner_LAT, Post_LON, Post_LAT, WIDTH, FILE_LENGTH)
|
||
|
||
print(f"{byteorder} endian {dem_file} and {dem_par_file} are generated.")
|
||
print(f"{processor_name} 格式转换完成!")
|
||
|
||
def get_sufix(STR):
|
||
"""Get file extension"""
|
||
n = len(STR.split('.'))
|
||
SUFIX = STR.split('.')[n-1]
|
||
return SUFIX
|
||
|
||
def read_region(STR):
|
||
"""Parse region string 'west/east/south/north'"""
|
||
WEST = STR.split('/')[0]
|
||
EAST = STR.split('/')[1].split('/')[0]
|
||
SOUTH = STR.split(EAST+'/')[1].split('/')[0]
|
||
NORTH = STR.split(EAST+'/')[1].split('/')[1]
|
||
WEST = float(WEST)
|
||
SOUTH = float(SOUTH)
|
||
EAST = float(EAST)
|
||
NORTH = float(NORTH)
|
||
return WEST, SOUTH, EAST, NORTH
|
||
|
||
def cmd_init(lon, lat, save_path):
|
||
s_lon = str(abs(lon))
|
||
s_lat = str(abs(lat))
|
||
if abs(lon) < 10:
|
||
s_lon = "00" + str(abs(lon))
|
||
elif abs(lon) < 100:
|
||
s_lon = "0" + str(abs(lon))
|
||
if abs(lat) < 10:
|
||
s_lat = "0" + str(abs(lat))
|
||
if lon < 0:
|
||
c_lon = "W" + str(s_lon)
|
||
else:
|
||
c_lon = "E" + str(s_lon)
|
||
if lat < 0:
|
||
c_lat = "S" + str(s_lat)
|
||
else:
|
||
c_lat = "N" + str(s_lat)
|
||
cmd = "aws s3 cp --no-sign-request" + " s3://copernicus-dem-{0}m/Copernicus_DSM_COG_{1}_{2}_00_{3}_00_DEM/ {4} --recursive".format(
|
||
str(resolutions), str(int(resolutions / 3)), str(c_lat), str(c_lon), save_path)
|
||
return cmd
|
||
|
||
def get_remote_file(lon, lat, save_path, max_retries=3):
|
||
"""Get Copernicus Dem by lon and lat with retry mechanism
|
||
|
||
Args:
|
||
lon (number): lontitude
|
||
lat (number): latitude
|
||
save_path (str): directory to save data
|
||
max_retries (int): maximum retry attempts
|
||
"""
|
||
lon = int(lon)
|
||
lat = int(lat)
|
||
cmd = cmd_init(lon, lat, save_path)
|
||
|
||
for attempt in range(max_retries):
|
||
try:
|
||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
|
||
if result.returncode == 0:
|
||
print(f"✓ Successfully downloaded: lon={lon}, lat={lat}")
|
||
return True
|
||
else:
|
||
if attempt < max_retries - 1:
|
||
print(f"✗ Failed (attempt {attempt+1}/{max_retries}): lon={lon}, lat={lat}, retrying...")
|
||
import time
|
||
time.sleep(2)
|
||
else:
|
||
print(f"✗ Failed after {max_retries} attempts: lon={lon}, lat={lat}")
|
||
return False
|
||
except subprocess.TimeoutExpired:
|
||
if attempt < max_retries - 1:
|
||
print(f"⏱ Timeout (attempt {attempt+1}/{max_retries}): lon={lon}, lat={lat}, retrying...")
|
||
import time
|
||
time.sleep(2)
|
||
else:
|
||
print(f"✗ Timeout after {max_retries} attempts: lon={lon}, lat={lat}")
|
||
return False
|
||
except Exception as e:
|
||
print(f"✗ Exception occurred for lon={lon}, lat={lat}: {e}")
|
||
return False
|
||
|
||
return False
|
||
|
||
def get_remote_file_batch(west, east, south, north, save_path, num_workers=4):
|
||
"""Get Copernicus Dems by WESN extent with parallel download
|
||
|
||
Args:
|
||
west (number): western longitude boundary
|
||
east (number): eastern longitude boundary
|
||
south (number): southern latitude boundary
|
||
north (number): northern latitude boundary
|
||
save_path (str): directory to save data
|
||
num_workers (int): number of parallel download workers (default: 4)
|
||
"""
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from tqdm import tqdm
|
||
|
||
west = int(west)
|
||
east = int(east)
|
||
south = int(south)
|
||
north = int(north)
|
||
|
||
# 生成所有需要下载的坐标对
|
||
coords = [(lon, lat) for lon in range(west, east + 1) for lat in range(south, north + 1)]
|
||
total_tiles = len(coords)
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"并行下载 Copernicus DEM 数据")
|
||
print(f"{'='*80}")
|
||
print(f"区域范围: {west}°E - {east}°E, {south}°N - {north}°N")
|
||
print(f"总瓦片数: {total_tiles}")
|
||
print(f"并行线程: {num_workers}")
|
||
print(f"{'='*80}\n")
|
||
|
||
success_count = 0
|
||
failed_count = 0
|
||
|
||
# 使用线程池并行下载
|
||
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
||
# 提交所有下载任务
|
||
future_to_coord = {executor.submit(get_remote_file, lon, lat, save_path): (lon, lat)
|
||
for lon, lat in coords}
|
||
|
||
# 使用 tqdm 显示进度条
|
||
with tqdm(total=total_tiles, desc="下载进度", unit="瓦片") as pbar:
|
||
for future in as_completed(future_to_coord):
|
||
coord = future_to_coord[future]
|
||
try:
|
||
result = future.result()
|
||
if result:
|
||
success_count += 1
|
||
else:
|
||
failed_count += 1
|
||
except Exception as e:
|
||
print(f"\n✗ 下载失败 {coord}: {e}")
|
||
failed_count += 1
|
||
|
||
pbar.update(1)
|
||
|
||
# 打印统计信息
|
||
print(f"\n{'='*80}")
|
||
print(f"下载完成统计")
|
||
print(f"{'='*80}")
|
||
print(f"成功: {success_count}/{total_tiles} 瓦片")
|
||
print(f"失败: {failed_count}/{total_tiles} 瓦片")
|
||
if failed_count > 0:
|
||
print(f"\n⚠️ 有 {failed_count} 个瓦片下载失败,可能会影响 DEM 完整性")
|
||
print(f"{'='*80}\n")
|
||
|
||
def download_nasadem(west, south, east, north, save_path, cell_size_deg=0.000277778):
|
||
"""Download NASADEM data using NASADEM library
|
||
|
||
Args:
|
||
west (float): western longitude boundary
|
||
south (float): southern latitude boundary
|
||
east (float): eastern longitude boundary
|
||
north (float): northern latitude boundary
|
||
save_path (str): directory to save data
|
||
cell_size_deg (float): cell size in degrees (default: 0.000277778 ≈ 30m at equator)
|
||
|
||
Returns:
|
||
str: path to the downloaded DEM file, or None if failed
|
||
"""
|
||
if not HAS_NASADEM:
|
||
print("✗ NASADEM library not available. Cannot download NASADEM.")
|
||
print(" Install with: pip install nasadem rasters")
|
||
return None
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"下载 NASADEM 数据")
|
||
print(f"{'='*80}")
|
||
print(f"区域范围: {west}°E - {east}°E, {south}°N - {north}°N")
|
||
print(f"分辨率: {cell_size_deg}° (约 {cell_size_deg * 111000:.0f} 米)")
|
||
print(f"{'='*80}\n")
|
||
|
||
try:
|
||
from NASADEM.NASADEM import NASADEMConnection
|
||
from rasters import RasterGrid
|
||
import time
|
||
|
||
start_time = time.time()
|
||
|
||
# 定义目标区域
|
||
geometry = RasterGrid.from_bbox(
|
||
xmin=west, ymin=south, xmax=east, ymax=north,
|
||
cell_size=cell_size_deg,
|
||
crs="EPSG:4326"
|
||
)
|
||
|
||
print(f"RasterGrid 维度: {geometry.shape}")
|
||
print(f"正在从NASA服务器下载高程数据...")
|
||
print(f"提示: 首次下载需要从NASA服务器获取数据,可能需要几分钟时间...")
|
||
|
||
# 创建NASA DEM连接并获取高程数据
|
||
conn = NASADEMConnection()
|
||
elevation = conn.elevation_m(geometry)
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n✓ 下载完成!")
|
||
print(f" 高程范围: {float(elevation.min()):.1f} 到 {float(elevation.max()):.1f} 米")
|
||
print(f" 数据维度: {elevation.shape}")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
|
||
# 保存为 GeoTIFF
|
||
output_file = os.path.join(save_path, f"NASADEM_{west}_{east}_{south}_{north}.tif")
|
||
print(f"\n保存到: {output_file}")
|
||
|
||
# 检查elevation对象是否有save方法
|
||
if hasattr(elevation, 'save'):
|
||
elevation.save(output_file)
|
||
print(f"✓ 文件保存成功: {output_file}")
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024)
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
return output_file
|
||
|
||
# 否则使用 rasterio 保存
|
||
try:
|
||
import rasterio
|
||
from rasterio.crs import CRS
|
||
from rasterio.transform import from_bounds
|
||
|
||
# 获取数据数组
|
||
if hasattr(elevation, 'read'):
|
||
elev_array = elevation.read()
|
||
else:
|
||
elev_array = np.array(elevation)
|
||
|
||
# 计算变换矩阵
|
||
height, width = elev_array.shape
|
||
transform = from_bounds(west, south, east, north, width, height)
|
||
|
||
# 写入文件
|
||
with rasterio.open(
|
||
output_file,
|
||
'w',
|
||
driver='GTiff',
|
||
height=height,
|
||
width=width,
|
||
count=1,
|
||
dtype=elev_array.dtype,
|
||
crs=CRS.from_epsg(4326),
|
||
transform=transform,
|
||
nodata=-32767,
|
||
compress='LZW'
|
||
) as dst:
|
||
dst.write(elev_array, 1)
|
||
|
||
print(f"✓ 文件保存成功: {output_file}")
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024)
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
return output_file
|
||
|
||
except ImportError:
|
||
print("⚠️ rasterio 未安装,尝试使用 GDAL...")
|
||
|
||
# 使用 GDAL 创建 GeoTIFF
|
||
width, height = geometry.width, geometry.height
|
||
gdal_cmd = f"gdal_create -outsize {width} {height} -of GTiff -co COMPRESS=LZW -a_srs EPSG:4326 -a_ullr {west} {north} {east} {south} {output_file}"
|
||
result = os.system(gdal_cmd)
|
||
|
||
if result == 0 and os.path.exists(output_file):
|
||
print(f"✓ 文件创建成功: {output_file}")
|
||
return output_file
|
||
else:
|
||
print(f"✗ 文件创建失败")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 下载失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def download_srtm(west, south, east, north, save_path, srtm_data_dir=None, cell_size=0.000277778):
|
||
"""Download SRTM data using srtm library (requires pre-downloaded .hgt files)
|
||
|
||
Args:
|
||
west (float): western longitude boundary
|
||
south (float): southern latitude boundary
|
||
east (float): eastern longitude boundary
|
||
north (float): northern latitude boundary
|
||
save_path (str): directory to save output DEM
|
||
srtm_data_dir (str): directory containing SRTM .hgt files (required)
|
||
cell_size (float): cell size in degrees (default: 0.000277778 ≈ 30m)
|
||
|
||
Returns:
|
||
str: path to the generated DEM file, or None if failed
|
||
|
||
Note:
|
||
This function requires SRTM .hgt files to be pre-downloaded.
|
||
You can download SRTM data from:
|
||
- https://srtm.csi.cgiar.org/
|
||
- https://earthexplorer.usgs.gov/
|
||
|
||
Python version requirement:
|
||
- srtm library requires Python >= 3.12
|
||
- For Python < 3.12, use process_srtm_hgt_files() instead
|
||
"""
|
||
if not HAS_SRTM:
|
||
print("✗ srtm library not available. Cannot use SRTM.")
|
||
print(" Install with: pip install srtm (requires Python >= 3.12)")
|
||
print(f" Current Python version: {sys.version_info.major}.{sys.version_info.minor}")
|
||
print(" Alternative: Use process_srtm_hgt_files() to manually process .hgt files")
|
||
return None
|
||
|
||
if srtm_data_dir is None or not os.path.isdir(srtm_data_dir):
|
||
print("✗ SRTM data directory not provided or does not exist.")
|
||
print(" Please download SRTM .hgt files and specify the directory.")
|
||
print(" Download sources:")
|
||
print(" - https://srtm.csi.cgiar.org/")
|
||
print(" - https://earthexplorer.usgs.gov/")
|
||
return None
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"使用 SRTM 数据生成 DEM")
|
||
print(f"{'='*80}")
|
||
print(f"区域范围: {west}°E - {east}°E, {south}°N - {north}°N")
|
||
print(f"SRTM数据目录: {srtm_data_dir}")
|
||
print(f"分辨率: {cell_size}° (约 {cell_size * 111000:.0f} 米)")
|
||
print(f"{'='*80}\n")
|
||
|
||
try:
|
||
import time
|
||
from tqdm import tqdm
|
||
|
||
start_time = time.time()
|
||
|
||
# 创建 SrtmService
|
||
service = srtm.SrtmService(srtm_data_dir, cache_size=100)
|
||
print(f"✓ SrtmService 创建成功")
|
||
|
||
# 生成坐标网格
|
||
lats = np.arange(south, north + cell_size, cell_size)
|
||
lons = np.arange(west, east + cell_size, cell_size)
|
||
|
||
print(f"生成网格: {len(lats)} × {len(lons)} = {len(lats) * len(lons)} 个点")
|
||
|
||
# 批量查询高程
|
||
coords = [(lat, lon) for lat in lats for lon in lons]
|
||
print(f"正在查询高程数据...")
|
||
|
||
elevations = service.get_elevations_batch(coords, default=0)
|
||
|
||
# 重塑为数组
|
||
elevation_array = np.array(elevations, dtype=np.float32).reshape(len(lats), len(lons))
|
||
|
||
# 替换0值为NaN(可能是缺失数据)
|
||
elevation_array[elevation_array == 0] = np.nan
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n✓ 查询完成!")
|
||
print(f" 有效数据: {np.sum(~np.isnan(elevation_array))} / {elevation_array.size} 点")
|
||
if np.sum(~np.isnan(elevation_array)) > 0:
|
||
print(f" 高程范围: {np.nanmin(elevation_array):.1f} 到 {np.nanmax(elevation_array):.1f} 米")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
|
||
# 保存为 GeoTIFF
|
||
output_file = os.path.join(save_path, f"SRTM_{west}_{east}_{south}_{north}.tif")
|
||
print(f"\n保存到: {output_file}")
|
||
|
||
try:
|
||
import rasterio
|
||
from rasterio.crs import CRS
|
||
from rasterio.transform import from_bounds
|
||
|
||
height, width = elevation_array.shape
|
||
transform = from_bounds(west, south, east, north, width, height)
|
||
|
||
# 替换NaN为nodata值
|
||
elevation_array[np.isnan(elevation_array)] = -32767
|
||
|
||
with rasterio.open(
|
||
output_file,
|
||
'w',
|
||
driver='GTiff',
|
||
height=height,
|
||
width=width,
|
||
count=1,
|
||
dtype=np.float32,
|
||
crs=CRS.from_epsg(4326),
|
||
transform=transform,
|
||
nodata=-32767,
|
||
compress='LZW'
|
||
) as dst:
|
||
dst.write(elevation_array.astype(np.float32), 1)
|
||
|
||
print(f"✓ 文件保存成功: {output_file}")
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024)
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
return output_file
|
||
|
||
except ImportError:
|
||
print("⚠️ rasterio 未安装,无法保存文件")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 生成失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def split_large_region(west, south, east, north, tile_size=1.0):
|
||
"""将大区域分割成小块
|
||
|
||
Args:
|
||
west, south, east, north: 区域边界
|
||
tile_size: 分块大小(度),默认1°×1°
|
||
|
||
Returns:
|
||
list: 分块列表 [(west, south, east, north), ...]
|
||
"""
|
||
tiles = []
|
||
|
||
w = west
|
||
while w < east:
|
||
e = min(w + tile_size, east)
|
||
|
||
s = south
|
||
while s < north:
|
||
n = min(s + tile_size, north)
|
||
tiles.append((w, s, e, n))
|
||
s += tile_size
|
||
|
||
w += tile_size
|
||
|
||
return tiles
|
||
|
||
|
||
def download_srtm_cgiar(west, south, east, north, save_path, api_key=None, num_workers=4, dem_type='SRTMGL1', auto_tile=True):
|
||
"""Download DEM data from OpenTopography automatically
|
||
|
||
Args:
|
||
west (float): western longitude boundary
|
||
south (float): southern latitude boundary
|
||
east (float): eastern longitude boundary
|
||
north (float): northern latitude boundary
|
||
save_path (str): directory to save output DEM
|
||
api_key (str): OpenTopography API key (optional, will prompt if not provided)
|
||
num_workers (int): number of parallel downloads (not used for API, kept for compatibility)
|
||
dem_type (str): DEM type (default: 'SRTMGL1')
|
||
- SRTMGL1: SRTM GL1 30m (recommended for SRTM)
|
||
- SRTMGL3: SRTM GL3 90m
|
||
- NASADEM: NASADEM 30m
|
||
- COP30: Copernicus 30m
|
||
- COP90: Copernicus 90m
|
||
auto_tile (bool): 自动分块下载大区域
|
||
|
||
Returns:
|
||
str: path to the generated DEM file, or None if failed
|
||
|
||
Note:
|
||
OpenTopography API provides:
|
||
- SRTMGL1: SRTM GL1 30m (recommended)
|
||
- SRTMGL3: SRTM GL3 90m
|
||
- NASADEM: NASADEM Global DEM 30m
|
||
- COP30: Copernicus 30m
|
||
- COP90: Copernicus 90m
|
||
|
||
API Documentation: https://portal.opentopography.org/apidocs/
|
||
Get API key: https://opentopography.org/myOpenTopo
|
||
"""
|
||
# DEM类型信息
|
||
dem_info = {
|
||
'SRTMGL1': {'resolution': '30m', 'name': 'SRTM GL1'},
|
||
'SRTMGL3': {'resolution': '90m', 'name': 'SRTM GL3'},
|
||
'NASADEM': {'resolution': '30m', 'name': 'NASADEM'},
|
||
'COP30': {'resolution': '30m', 'name': 'Copernicus 30m'},
|
||
'COP90': {'resolution': '90m', 'name': 'Copernicus 90m'}
|
||
}
|
||
|
||
info = dem_info.get(dem_type, {'resolution': 'unknown', 'name': dem_type})
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"从 OpenTopography 自动下载 DEM 数据")
|
||
print(f"{'='*80}")
|
||
print(f"区域范围: {west}°E - {east}°E, {south}°N - {north}°N")
|
||
print(f"数据源: {info['name']} ({info['resolution']} resolution)")
|
||
print(f"{'='*80}\n")
|
||
|
||
# 计算区域面积
|
||
region_area = (east - west) * (north - south)
|
||
|
||
# 如果区域较大,自动分块
|
||
if auto_tile and region_area > 4:
|
||
print(f"⚠️ 区域较大 ({region_area:.1f} 平方度),将自动分块下载以提高稳定性")
|
||
|
||
# 计算分块
|
||
tiles = split_large_region(west, south, east, north, tile_size=1.0)
|
||
print(f" 将分成 {len(tiles)} 个 1°×1° 的区块下载\n")
|
||
|
||
# 创建临时目录存放分块文件
|
||
temp_dir = os.path.join(save_path, f"temp_tiles_{dem_type}_{west}_{east}_{south}_{north}")
|
||
if not os.path.exists(temp_dir):
|
||
os.makedirs(temp_dir)
|
||
|
||
# 下载每个分块
|
||
downloaded_files = []
|
||
failed_tiles = []
|
||
|
||
for i, (w, s, e, n) in enumerate(tiles, 1):
|
||
print(f"\n{'='*80}")
|
||
print(f"下载分块 {i}/{len(tiles)}: {w}°E-{e}°E, {s}°N-{n}°N")
|
||
print(f"{'='*80}")
|
||
|
||
tile_file = download_single_tile(w, s, e, n, temp_dir, api_key, dem_type, info)
|
||
|
||
if tile_file:
|
||
downloaded_files.append(tile_file)
|
||
print(f"✓ 分块 {i}/{len(tiles)} 下载成功")
|
||
else:
|
||
failed_tiles.append((w, s, e, n))
|
||
print(f"✗ 分块 {i}/{len(tiles)} 下载失败")
|
||
|
||
# 如果有失败的分块
|
||
if failed_tiles:
|
||
print(f"\n⚠️ 有 {len(failed_tiles)} 个分块下载失败")
|
||
print("失败的分块:")
|
||
for w, s, e, n in failed_tiles:
|
||
print(f" {w}°E-{e}°E, {s}°N-{n}°N")
|
||
|
||
# 合并所有分块
|
||
if downloaded_files:
|
||
print(f"\n{'='*80}")
|
||
print(f"合并 {len(downloaded_files)} 个分块...")
|
||
print(f"{'='*80}\n")
|
||
|
||
merged_file = merge_dem_tiles(downloaded_files, save_path, dem_type, west, east, south, north)
|
||
|
||
# 清理临时文件
|
||
import shutil
|
||
try:
|
||
shutil.rmtree(temp_dir)
|
||
print("✓ 临时文件已清理")
|
||
except:
|
||
pass
|
||
|
||
if merged_file:
|
||
print(f"\n✓ 最终输出文件: {merged_file}")
|
||
return merged_file
|
||
else:
|
||
print("✗ 合并失败")
|
||
return None
|
||
else:
|
||
print("✗ 所有分块下载失败")
|
||
return None
|
||
|
||
# 小区域直接下载
|
||
else:
|
||
return download_single_tile(west, south, east, north, save_path, api_key, dem_type, info)
|
||
|
||
|
||
def download_single_tile(west, south, east, north, save_path, api_key, dem_type, dem_info):
|
||
"""下载单个DEM块
|
||
|
||
Args:
|
||
west, south, east, north: 区域边界
|
||
save_path: 保存路径
|
||
api_key: OpenTopography API key
|
||
dem_type: DEM类型
|
||
dem_info: DEM信息字典
|
||
|
||
Returns:
|
||
str: 下载的文件路径,失败返回None
|
||
"""
|
||
try:
|
||
import time
|
||
import urllib.request
|
||
import urllib.parse
|
||
|
||
start_time = time.time()
|
||
|
||
# OpenTopography API endpoint
|
||
base_url = "https://portal.opentopography.org/API/globaldem"
|
||
|
||
# 如果没有提供API key,提示用户获取
|
||
if api_key is None:
|
||
print("⚠️ 需要OpenTopography API key才能下载SRTM数据")
|
||
print("\n获取免费API key的步骤:")
|
||
print(" 1. 访问: https://opentopography.org/myOpenTopo")
|
||
print(" 2. 注册免费账户")
|
||
print(" 3. 在 'My Account' 页面请求API key")
|
||
print(" 4. 使用 --opentopo-api-key 参数提供API key")
|
||
print("\n或者使用其他DEM数据源:")
|
||
print(" - Copernicus (推荐): --dem-source copernicus")
|
||
print(" - NASADEM: --dem-source nasadem")
|
||
return None
|
||
|
||
# 构建请求参数
|
||
params = {
|
||
'demtype': dem_type,
|
||
'south': south,
|
||
'north': north,
|
||
'west': west,
|
||
'east': east,
|
||
'outputFormat': 'GTiff',
|
||
'API_Key': api_key
|
||
}
|
||
|
||
# 构建完整URL
|
||
url = base_url + '?' + urllib.parse.urlencode(params)
|
||
|
||
# 输出文件名
|
||
output_file = os.path.join(save_path, f"{dem_type}_{west}_{east}_{south}_{north}.tif")
|
||
|
||
# 下载文件 - 使用curl以获得更好的稳定性
|
||
max_retries = 3
|
||
retry_count = 0
|
||
download_success = False
|
||
|
||
while retry_count < max_retries and not download_success:
|
||
try:
|
||
retry_count += 1
|
||
print(f"正在从OpenTopography下载数据... (尝试 {retry_count}/{max_retries})")
|
||
|
||
# 尝试使用 curl 下载(更稳定)
|
||
try:
|
||
import subprocess
|
||
curl_cmd = ['curl', '-L', '-o', output_file, '-s', '--show-error', url]
|
||
result = subprocess.run(curl_cmd, capture_output=True, text=True, timeout=1800) # 30分钟超时
|
||
|
||
if result.returncode != 0:
|
||
raise Exception(f"curl下载失败: {result.stderr}")
|
||
|
||
print(" ✓ curl下载完成")
|
||
download_success = True
|
||
|
||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||
# 如果curl不可用或超时,使用urllib
|
||
print(" 使用urllib下载...")
|
||
urllib.request.urlretrieve(url, output_file)
|
||
download_success = True
|
||
|
||
except urllib.error.HTTPError as e:
|
||
print(f"\n✗ HTTP错误: {e.code} {e.reason}")
|
||
if e.code == 401:
|
||
print(" API key无效或已过期")
|
||
print(" 请获取新的API key: https://opentopography.org/myOpenTopo")
|
||
elif e.code == 403:
|
||
print(" 访问被拒绝,可能是:")
|
||
print(" - 请求区域过大")
|
||
print(" - API key配额已用完")
|
||
print(" - 需要注册获取免费API key")
|
||
if retry_count < max_retries:
|
||
print(f" 将在3秒后重试...")
|
||
time.sleep(3)
|
||
continue
|
||
return None
|
||
except Exception as e:
|
||
print(f"\n✗ 下载失败: {e}")
|
||
if retry_count < max_retries:
|
||
print(f" 将在3秒后重试...")
|
||
time.sleep(3)
|
||
continue
|
||
return None
|
||
|
||
# 检查文件是否有效
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file)
|
||
|
||
# 如果文件很小,可能是错误消息
|
||
if file_size < 1000:
|
||
with open(output_file, 'r') as f:
|
||
error_msg = f.read()
|
||
print(f"\n✗ 下载失败: {error_msg}")
|
||
os.remove(output_file)
|
||
return None
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n✓ 下载完成!")
|
||
print(f" 输出文件: {output_file}")
|
||
print(f" 文件大小: {file_size / (1024 * 1024):.2f} MB")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
|
||
# 验证文件完整性
|
||
print("\n验证文件完整性...")
|
||
|
||
try:
|
||
import rasterio
|
||
with rasterio.open(output_file) as src:
|
||
# 尝试读取多个位置的数据验证
|
||
print(f" 文件大小: {src.width} x {src.height} 像素")
|
||
|
||
# 读取多个测试点
|
||
test_points = [
|
||
(0, 0, min(100, src.height), min(100, src.width)), # 左上角
|
||
(max(0, src.height-100), max(0, src.width-100), src.height, src.width), # 右下角
|
||
(src.height//2-50, src.width//2-50, src.height//2+50, src.width//2+50), # 中间
|
||
]
|
||
|
||
for i, (row_start, col_start, row_end, col_end) in enumerate(test_points):
|
||
try:
|
||
window = ((row_start, row_end), (col_start, col_end))
|
||
test_data = src.read(1, window=window)
|
||
print(f" ✓ 测试点 {i+1}/3 验证成功")
|
||
except Exception as e:
|
||
print(f" ✗ 测试点 {i+1}/3 验证失败: {e}")
|
||
raise
|
||
|
||
print(f" ✓ 所有验证点通过,文件完整")
|
||
return output_file
|
||
|
||
except Exception as e:
|
||
print(f" ✗ 文件验证失败: {e}")
|
||
print(f" 文件可能已损坏,正在删除...")
|
||
try:
|
||
os.remove(output_file)
|
||
except:
|
||
pass
|
||
return None
|
||
else:
|
||
print("✗ 文件下载失败")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 下载失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def merge_dem_tiles(tile_files, save_path, dem_type, west, east, south, north):
|
||
"""合并多个DEM分块文件
|
||
|
||
Args:
|
||
tile_files: 分块文件列表
|
||
save_path: 保存路径
|
||
dem_type: DEM类型
|
||
west, east, south, north: 最终区域边界
|
||
|
||
Returns:
|
||
str: 合并后的文件路径,失败返回None
|
||
"""
|
||
if not tile_files:
|
||
print("✗ 没有文件需要合并")
|
||
return None
|
||
|
||
try:
|
||
import rasterio
|
||
from rasterio.merge import merge
|
||
import time
|
||
|
||
print(f"开始合并 {len(tile_files)} 个分块文件...")
|
||
|
||
# 读取所有分块
|
||
src_files_to_mosaic = []
|
||
for tile_file in tile_files:
|
||
src = rasterio.open(tile_file)
|
||
src_files_to_mosaic.append(src)
|
||
|
||
# 合并
|
||
start_time = time.time()
|
||
mosaic, out_trans = merge(src_files_to_mosaic)
|
||
|
||
# 获取输出元数据
|
||
out_meta = src_files_to_mosaic[0].meta.copy()
|
||
out_meta.update({
|
||
"driver": "GTiff",
|
||
"height": mosaic.shape[1],
|
||
"width": mosaic.shape[2],
|
||
"transform": out_trans,
|
||
"compress": "lzw"
|
||
})
|
||
|
||
# 输出文件名
|
||
output_file = os.path.join(save_path, f"{dem_type}_{west}_{east}_{south}_{north}.tif")
|
||
|
||
# 写入合并后的文件
|
||
with rasterio.open(output_file, "w", **out_meta) as dest:
|
||
dest.write(mosaic)
|
||
|
||
# 关闭所有源文件
|
||
for src in src_files_to_mosaic:
|
||
src.close()
|
||
|
||
elapsed_time = time.time() - start_time
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024)
|
||
|
||
print(f"✓ 合并完成!")
|
||
print(f" 输出文件: {output_file}")
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
|
||
return output_file
|
||
|
||
except ImportError:
|
||
print("✗ rasterio 未安装,无法合并文件")
|
||
print(" 安装方法: pip install rasterio")
|
||
return None
|
||
except Exception as e:
|
||
print(f"✗ 合并失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
|
||
def download_srtm_cgiar_old(west, south, east, north, save_path, num_workers=4):
|
||
"""Download SRTM data from CSI-CGIAR (DEPRECATED - tiles not accessible)
|
||
|
||
This function is kept as backup but CSI-CGIAR tile downloads are not reliable.
|
||
Use download_srtm_cgiar() with OpenTopography API instead.
|
||
"""
|
||
print("警告: CSI-CGIAR瓦片下载不可靠,建议使用OpenTopography API")
|
||
return None
|
||
|
||
def process_srtm_hgt_files(west, south, east, north, srtm_data_dir, save_path, output_name=None):
|
||
"""Process SRTM .hgt files manually (alternative for Python < 3.12)
|
||
|
||
Args:
|
||
west (float): western longitude boundary
|
||
south (float): southern latitude boundary
|
||
east (float): eastern longitude boundary
|
||
north (float): northern latitude boundary
|
||
srtm_data_dir (str): directory containing SRTM .hgt files
|
||
save_path (str): directory to save output DEM
|
||
output_name (str): output filename (without extension)
|
||
|
||
Returns:
|
||
str: path to the generated DEM file, or None if failed
|
||
|
||
Note:
|
||
SRTM .hgt files are 1°×1° tiles with 1201×1201 pixels (3 arc-second) or
|
||
3601×3601 pixels (1 arc-second). This function reads and merges them.
|
||
"""
|
||
print(f"\n{'='*80}")
|
||
print(f"手动处理 SRTM .hgt 文件")
|
||
print(f"{'='*80}")
|
||
print(f"区域范围: {west}°E - {east}°E, {south}°N - {north}°N")
|
||
print(f"SRTM数据目录: {srtm_data_dir}")
|
||
print(f"{'='*80}\n")
|
||
|
||
try:
|
||
import rasterio
|
||
from rasterio.merge import merge
|
||
from rasterio.crs import CRS
|
||
import time
|
||
|
||
start_time = time.time()
|
||
|
||
# 查找需要的SRTM文件
|
||
hgt_files = []
|
||
for lat in range(int(south), int(north) + 1):
|
||
for lon in range(int(west), int(east) + 1):
|
||
# 构造文件名
|
||
lat_prefix = 'N' if lat >= 0 else 'S'
|
||
lon_prefix = 'E' if lon >= 0 else 'W'
|
||
|
||
# SRTM文件名格式: N39E116.hgt
|
||
filename = f"{lat_prefix}{abs(lat):02d}{lon_prefix}{abs(lon):03d}.hgt"
|
||
filepath = os.path.join(srtm_data_dir, filename)
|
||
|
||
if os.path.exists(filepath):
|
||
hgt_files.append(filepath)
|
||
print(f" ✓ 找到文件: {filename}")
|
||
else:
|
||
print(f" ⚠ 文件缺失: {filename}")
|
||
|
||
if not hgt_files:
|
||
print("\n✗ 没有找到任何SRTM文件!")
|
||
return None
|
||
|
||
print(f"\n找到 {len(hgt_files)} 个SRTM文件")
|
||
|
||
# 使用GDAL合并.hgt文件
|
||
merged_file = os.path.join(save_path, f"SRTM_merged_{west}_{east}_{south}_{north}.tif")
|
||
|
||
# 构建gdal_merge命令
|
||
gdal_merge_cmd = ['gdal_merge.py', '-o', merged_file, '-of', 'GTiff', '-co', 'COMPRESS=LZW']
|
||
gdal_merge_cmd.extend(hgt_files)
|
||
|
||
print(f"\n使用GDAL合并文件...")
|
||
result = subprocess.run(gdal_merge_cmd, capture_output=True, text=True)
|
||
|
||
if result.returncode != 0 or not os.path.exists(merged_file):
|
||
print(f"✗ GDAL合并失败: {result.stderr}")
|
||
return None
|
||
|
||
print(f"✓ 合并完成: {merged_file}")
|
||
|
||
# 裁剪到目标范围
|
||
if output_name is None:
|
||
output_name = os.path.join(save_path, f"SRTM_{west}_{east}_{south}_{north}")
|
||
|
||
output_file = output_name + '.tif'
|
||
|
||
gdal_warp_cmd = [
|
||
'gdalwarp',
|
||
'-te', str(west), str(south), str(east), str(north),
|
||
'-of', 'GTiff',
|
||
'-co', 'COMPRESS=LZW',
|
||
merged_file,
|
||
output_file
|
||
]
|
||
|
||
print(f"\n裁剪到目标范围...")
|
||
result = subprocess.run(gdal_warp_cmd, capture_output=True, text=True)
|
||
|
||
if result.returncode != 0 or not os.path.exists(output_file):
|
||
print(f"✗ GDAL裁剪失败: {result.stderr}")
|
||
return None
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n✓ SRTM DEM 生成完成!")
|
||
print(f" 输出文件: {output_file}")
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024)
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
|
||
# 清理临时文件
|
||
if os.path.exists(merged_file) and merged_file != output_file:
|
||
os.remove(merged_file)
|
||
print(f" 清理临时文件: {merged_file}")
|
||
|
||
return output_file
|
||
|
||
except ImportError:
|
||
print("✗ rasterio 未安装,无法处理文件")
|
||
return None
|
||
except Exception as e:
|
||
print(f"\n✗ 处理失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def merge_tif_files(input_pattern, output_file):
|
||
"""合并多个 TIF 文件为一个文件
|
||
|
||
Args:
|
||
input_pattern (str): 输入 TIF 文件的匹配模式
|
||
output_file (str): 输出合并后的 TIF 文件路径
|
||
"""
|
||
# 查找所有匹配的 TIF 文件
|
||
tif_files = sorted(glob.glob(input_pattern))
|
||
|
||
if not tif_files:
|
||
print(f"未找到匹配的 TIF 文件: {input_pattern}")
|
||
return False
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"合并 DEM 瓦片")
|
||
print(f"{'='*80}")
|
||
print(f"找到 {len(tif_files)} 个 TIF 文件待合并")
|
||
print(f"输出文件: {output_file}")
|
||
print(f"{'='*80}\n")
|
||
|
||
# 使用 gdal_merge.py 合并文件
|
||
try:
|
||
start_time = __import__('time').time()
|
||
cmd = ["gdal_merge.py", "-o", output_file, "-co", "COMPRESS=LZW", "-co", "BIGTIFF=YES"] + tif_files
|
||
print(f"执行合并命令...")
|
||
|
||
# 使用 tqdm 显示进度
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
elapsed_time = __import__('time').time() - start_time
|
||
|
||
if result.returncode == 0:
|
||
# 检查输出文件大小
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file) / (1024 * 1024) # MB
|
||
print(f"\n✓ 成功合并文件!")
|
||
print(f" 输出文件: {output_file}")
|
||
print(f" 文件大小: {file_size:.2f} MB")
|
||
print(f" 用时: {elapsed_time:.1f} 秒")
|
||
print(f"{'='*80}\n")
|
||
return True
|
||
else:
|
||
print(f"\n✗ 合并失败: {result.stderr}")
|
||
print(f"{'='*80}\n")
|
||
return False
|
||
except Exception as e:
|
||
print(f"\n✗ 合并过程中发生错误: {e}")
|
||
print(f"{'='*80}\n")
|
||
return False
|
||
|
||
def convert_format(input_file, output_file, format_type="GTiff", options=None):
|
||
"""转换 TIF 文件格式
|
||
|
||
Args:
|
||
input_file (str): 输入文件路径
|
||
output_file (str): 输出文件路径
|
||
format_type (str): 输出格式类型 (默认: GTiff)
|
||
options (list): gdal_translate 的额外选项
|
||
"""
|
||
if options is None:
|
||
options = []
|
||
|
||
try:
|
||
cmd = ["gdal_translate", "-of", format_type] + options + [input_file, output_file]
|
||
print(f"执行转换命令: {' '.join(cmd)}")
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
if result.returncode == 0:
|
||
print(f"成功转换文件到: {output_file}")
|
||
return True
|
||
else:
|
||
print(f"转换失败: {result.stderr}")
|
||
return False
|
||
except Exception as e:
|
||
print(f"转换过程中发生错误: {e}")
|
||
return False
|
||
|
||
def process_dem_files(save_path, west, east, south, north, merge=True, convert=True,
|
||
output_format="GTiff", convert_options=None, gamma=False, gamma_byteorder='big',
|
||
processor='gamma', output_name=None, num_workers=4,
|
||
srtm_data_dir=None, opentopo_api_key=None, opentopo_dem_type='SRTMGL1',
|
||
fabdem_dir=None):
|
||
"""处理下载的 DEM 文件:下载、合并和转换
|
||
|
||
Args:
|
||
save_path (str): 数据保存目录
|
||
west (float): 西经边界
|
||
east (float): 东经边界
|
||
south (float): 南纬边界
|
||
north (float): 北纬边界
|
||
merge (bool): 是否合并文件
|
||
convert (bool): 是否转换格式
|
||
output_format (str): 输出格式
|
||
convert_options (list): 转换选项
|
||
gamma (bool): 是否转换为 Gamma/ROI_PAC 格式
|
||
gamma_byteorder (str): 字节序 ('big' 或 'little')
|
||
processor (str): 处理器类型 ('gamma' 或 'roi_pac')
|
||
output_name (str): 输出文件名(不含扩展名)
|
||
num_workers (int): 并行下载线程数 (default: 4)
|
||
dem_source (str): DEM数据源 ('copernicus', 'nasadem' 或 'srtm', default: 'copernicus')
|
||
srtm_data_dir (str): SRTM数据目录 (仅srtm源需要)
|
||
"""
|
||
if convert_options is None:
|
||
convert_options = ["-co", "COMPRESS=LZW", "-co", "TILED=YES"]
|
||
|
||
# ============ 优先尝试本地 FABDEM 瓦片库 ============
|
||
if fabdem_dir and os.path.isdir(fabdem_dir):
|
||
print(f"\n{'='*80}")
|
||
print(f"优先使用本地 FABDEM 瓦片库: {fabdem_dir}")
|
||
print(f"{'='*80}")
|
||
try:
|
||
from make_local_dem import (find_needed_tiles, extract_tiles_from_zips,
|
||
tiles_to_gamma_dem)
|
||
import tempfile, shutil
|
||
|
||
tiles = find_needed_tiles(west, south, east, north)
|
||
print(f"需要 {len(tiles)} 个 1°×1° FABDEM 瓦片")
|
||
|
||
temp_dir = tempfile.mkdtemp(prefix='fabdem_tiles_')
|
||
try:
|
||
tif_files = extract_tiles_from_zips(tiles, fabdem_dir, temp_dir)
|
||
if tif_files:
|
||
# 一步完成: VRT → ENVI 二进制 → .dem(仅一次大文件写入)
|
||
out_name = output_name if output_name else os.path.join(save_path, 'out')
|
||
dem_file, dem_par_file = tiles_to_gamma_dem(
|
||
tif_files, out_name, west, south, east, north, gamma_byteorder)
|
||
if dem_file and dem_par_file:
|
||
print(f"\n✓ 本地 FABDEM 生成 GAMMA DEM 成功!")
|
||
print(f" DEM: {dem_file}")
|
||
print(f" PAR: {dem_par_file}")
|
||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||
return
|
||
else:
|
||
print("⚠ 本地 FABDEM 转换失败,回退到网络下载")
|
||
else:
|
||
print("⚠ 未从本地 FABDEM 提取到瓦片,回退到网络下载")
|
||
finally:
|
||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||
except ImportError as e:
|
||
print(f"⚠ 无法导入 make_local_dem 模块 ({e}),回退到网络下载")
|
||
except Exception as e:
|
||
print(f"⚠ 本地 FABDEM 处理异常 ({e}),回退到网络下载")
|
||
|
||
# ============ 回退: 网络下载 ============
|
||
# 检查是否使用预下载的SRTM数据
|
||
if srtm_data_dir and os.path.isdir(srtm_data_dir):
|
||
print(f"\n{'='*80}")
|
||
print(f"使用预下载的 SRTM 数据")
|
||
print(f"{'='*80}")
|
||
|
||
if HAS_SRTM:
|
||
# Python >= 3.12, 使用srtm库
|
||
dem_file = download_srtm(west, south, east, north, save_path, srtm_data_dir)
|
||
else:
|
||
# Python < 3.12, 使用手动处理方法
|
||
print("注意: srtm库需要Python >= 3.12,使用手动处理方法")
|
||
dem_file = process_srtm_hgt_files(west, south, east, north, srtm_data_dir,
|
||
save_path, output_name)
|
||
else:
|
||
# 自动从OpenTopography下载
|
||
print(f"\n{'='*80}")
|
||
print(f"开始下载 DEM 数据...")
|
||
print(f"{'='*80}")
|
||
print("自动从OpenTopography下载DEM数据")
|
||
dem_file = download_srtm_cgiar(west, south, east, north, save_path,
|
||
api_key=opentopo_api_key, num_workers=num_workers,
|
||
dem_type=opentopo_dem_type)
|
||
|
||
if dem_file is None:
|
||
print("✗ DEM 生成失败,退出")
|
||
return
|
||
|
||
current_file = dem_file
|
||
|
||
# 生成输出文件名
|
||
if output_name is None:
|
||
output_name = os.path.join(save_path, f"DEM_W{int(west)}_E{int(east)}_S{int(south)}_N{int(north)}")
|
||
|
||
# 转换到 Gamma/ROI_PAC 格式
|
||
if gamma:
|
||
print("\n开始转换到 {} 格式...".format(processor.upper()))
|
||
convert_to_gamma(current_file, output_name, gamma_byteorder, processor)
|
||
|
||
# 转换到其他格式(非 Gamma/ROI_PAC)
|
||
if not gamma and convert:
|
||
print("\n开始转换文件格式...")
|
||
converted_file = os.path.join(save_path, f"final_DEM_W{int(west)}_E{int(east)}_S{int(south)}_N{int(north)}.tif")
|
||
convert_format(current_file, converted_file, output_format, convert_options)
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(
|
||
description='Download DEM data from OpenTopography and convert to Gamma/ROI_PAC format.',
|
||
formatter_class=argparse.RawTextHelpFormatter
|
||
)
|
||
|
||
# 与 makedem.py 相同的参数
|
||
parser.add_argument('-r', dest='region', help='Research region, west/east/south/north (e.g., 106/110/36/40)')
|
||
parser.add_argument('-d', dest='dem', help='Raw dem file that used for further processing')
|
||
parser.add_argument('-s', dest='par', help='SLC parameter file of SAR image used for determining research region')
|
||
parser.add_argument('-p', dest='processor', help='Interferometry processor. [ gamma or roi_pac ] [default: gamma]')
|
||
parser.add_argument('-o', dest='out', help='Output name of the generated DEM')
|
||
parser.add_argument('--byteorder', dest='byteorder', choices=['big', 'little'],
|
||
help='Byteorder of the generated DEM: big or little. [default: big for gamma and little for roi_pac]')
|
||
parser.add_argument('--dir', dest='PATH', help='Processing directory for generating DEM. [default: Current directory]')
|
||
parser.add_argument('--num-workers', dest='num_workers', type=int, default=4,
|
||
help='Number of parallel download workers (used for batch processing). [default: 4]')
|
||
parser.add_argument('--srtm-data-dir', dest='srtm_data_dir', default=None,
|
||
help='Directory containing SRTM .hgt files (optional).\n'
|
||
'If not provided, DEM data will be automatically downloaded from OpenTopography.\n'
|
||
'Download SRTM data from: https://srtm.csi.cgiar.org/')
|
||
parser.add_argument('--opentopo-api-key', dest='opentopo_api_key',
|
||
default='09ad77d34545607fdf5cb182b64ac64e',
|
||
help='OpenTopography API key for downloading DEM data.\n'
|
||
'Get a free API key at: https://opentopography.org/myOpenTopo\n'
|
||
'Default key is provided, but you can use your own key.')
|
||
parser.add_argument('--opentopo-dem-type', dest='opentopo_dem_type',
|
||
choices=['SRTMGL1', 'SRTMGL3', 'NASADEM', 'COP30', 'COP90'],
|
||
default='SRTMGL1',
|
||
help='DEM type for OpenTopography download. [default: SRTMGL1]\n'
|
||
' - SRTMGL1: SRTM GL1 30m (recommended)\n'
|
||
' - SRTMGL3: SRTM GL3 90m\n'
|
||
' - NASADEM: NASADEM 30m\n'
|
||
' - COP30: Copernicus 30m\n'
|
||
' - COP90: Copernicus 90m')
|
||
parser.add_argument('--fabdem-dir', dest='fabdem_dir', default=None,
|
||
help='Directory containing local FABDEM ZIP tiles (e.g., /mnt/ZYD/全球FABDEM).\n'
|
||
'If provided, local FABDEM tiles will be used FIRST before network download.\n'
|
||
'Falls back to OpenTopography download if local tiles are unavailable.')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 确定工作目录
|
||
if args.PATH:
|
||
workdir = args.PATH
|
||
else:
|
||
workdir = os.getcwd()
|
||
|
||
os.chdir(workdir)
|
||
|
||
# 确定处理器类型
|
||
if args.processor:
|
||
processor = args.processor
|
||
else:
|
||
processor = 'gamma'
|
||
|
||
# 确定字节序
|
||
if args.byteorder:
|
||
Byteorder = args.byteorder
|
||
else:
|
||
if processor == 'gamma':
|
||
Byteorder = 'big'
|
||
else:
|
||
Byteorder = 'little'
|
||
|
||
# 确定输出名称
|
||
if args.out:
|
||
Name = args.out
|
||
else:
|
||
Name = "out"
|
||
|
||
# 处理已有 DEM 文件的情况
|
||
if args.dem:
|
||
dem = args.dem
|
||
print('Raw dem file is provided: %s.' % dem)
|
||
|
||
# 转换为 TIF 格式(如果不是 TIF)
|
||
SUFIX = get_sufix(dem)
|
||
if SUFIX != 'tif':
|
||
DTIF = dem.replace('.' + SUFIX, '.tif')
|
||
call_str = f'gdal_translate {dem} -of GTiff {DTIF}'
|
||
os.system(call_str)
|
||
DEM = DTIF
|
||
else:
|
||
DEM = dem
|
||
|
||
# 转换到 Gamma/ROI_PAC 格式
|
||
convert_to_gamma(DEM, Name, Byteorder, processor)
|
||
|
||
BB = Byteorder + ' endian'
|
||
print('')
|
||
print('%s %s and %s are generated.' % (BB, Name + '.dem', Name + ('.dem.par' if processor == 'gamma' else '.dem.rsc')))
|
||
print('Congratulations! Done!')
|
||
sys.exit(0)
|
||
|
||
# 处理从 SLC 参数文件确定区域的情况
|
||
if args.par:
|
||
Par = args.par
|
||
print("SLC_par file is provided: %s" % Par)
|
||
print("DEM over research region will be downloaded automatically based on %s" % Par)
|
||
print("DEM data will be downloaded from OpenTopography")
|
||
call_str = "SLC_corners " + Par + " > corners.txt"
|
||
os.system(call_str)
|
||
|
||
File = open("corners.txt", "r")
|
||
InfoLine = File.readlines()[8:10]
|
||
File.close()
|
||
|
||
MinLat = float(InfoLine[0].split(':')[1].split(' max. ')[0])
|
||
MaxLat = float(InfoLine[0].split(':')[2])
|
||
MinLon = float(InfoLine[1].split(':')[1].split(' max. ')[0])
|
||
MaxLon = float(InfoLine[1].split(':')[2])
|
||
|
||
north = int(MaxLat) + 2
|
||
south = int(MinLat)
|
||
east = int(MaxLon) + 2
|
||
west = int(MinLon)
|
||
elif args.region:
|
||
region = args.region
|
||
west, south, east, north = read_region(region)
|
||
else:
|
||
parser.print_usage()
|
||
sys.exit(os.path.basename(sys.argv[0]) + ': error: research region, raw_demfile and SLC parameter file, at least one is needed.')
|
||
|
||
print('Research region: %s(west) %s(south) %s(east) %s(north)' % (west, south, east, north))
|
||
print('>>> Ready to download DEM over research region.')
|
||
|
||
# 处理数据(下载、合并和转换到 Gamma/ROI_PAC 格式)
|
||
process_dem_files(
|
||
save_path=workdir,
|
||
west=west,
|
||
east=east,
|
||
south=south,
|
||
north=north,
|
||
merge=True,
|
||
convert=False,
|
||
gamma=True,
|
||
gamma_byteorder=Byteorder,
|
||
processor=processor,
|
||
output_name=os.path.join(workdir, Name),
|
||
num_workers=args.num_workers,
|
||
srtm_data_dir=args.srtm_data_dir,
|
||
opentopo_api_key=args.opentopo_api_key,
|
||
opentopo_dem_type=args.opentopo_dem_type,
|
||
fabdem_dir=args.fabdem_dir
|
||
)
|