Files

494 lines
18 KiB
Python
Raw Permalink 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 python3
# -*- coding: utf-8 -*-
###########################################################################
# Header information
###########################################################################
"""API_download_S1_SLC.py: Script to download Sentinel-1 SLC images from the ASF mirror
Supports both bounding box and Shapefile for spatial filtering"""
__author__ = "Alexis Hrysiewicz"
__copyright__ = "Copyright 2022"
__credits__ = ["Alexis Hrysiewicz"]
__license__ = "GPL"
__version__ = "2.0.0"
__maintainer__ = "Alexis Hrysiewicz"
__status__ = "Production"
__date__ = "Jan. 2022"
###########################################################################
# Python packages
###########################################################################
import os
import sys
import pandas as pd
import datetime
import os.path
import optparse
# 尝试导入 geopandas 和 shapely 用于 Shapefile 支持
try:
import geopandas as gpd
from shapely.geometry import box, Polygon, MultiPolygon
HAS_SHAPEFILE_SUPPORT = True
except ImportError:
HAS_SHAPEFILE_SUPPORT = False
print("Warning: geopandas/shapely not installed. Shapefile support disabled.")
print("Install with: pip install geopandas shapely")
###########################################################################
###########################################################################
class OptionParser (optparse.OptionParser):
def check_required(self, opt):
option = self.get_option(opt)
if getattr(self.values, option.dest) is None:
self.error("%s option not supplied" % option)
def bbox_from_shapefile(shapefile_path):
"""
从 Shapefile 文件提取边界框
Args:
shapefile_path: Shapefile 文件路径(.shp
Returns:
边界框字符串格式: "min_lon,min_lat,max_lon,max_lat"
"""
try:
# 读取 Shapefile
gdf = gpd.read_file(shapefile_path)
# 计算总边界框
total_bounds = gdf.total_bounds
# total_bounds = (minx, miny, maxx, maxy)
# 转换为字符串格式
bbox_str = f"{total_bounds[0]},{total_bounds[1]},{total_bounds[2]},{total_bounds[3]}"
print(f"Shapefile bounding box: {bbox_str}")
return bbox_str
except Exception as e:
print(f"Error reading shapefile: {str(e)}")
return None
def check_slc_exists(slc_name, path_SLC, path_RSLC, acquisition_date):
"""
检查 SLC 文件是否已存在
Args:
slc_name: SLC 文件名
path_SLC: SLC 存储路径
path_RSLC: RSLC 存储路径
acquisition_date: 采集日期字符串
Returns:
bool: 文件是否已存在
"""
# 检查 .zip 文件
if os.path.exists(os.path.join(path_SLC, slc_name + '.zip')):
return True
# 检查 .rslc 文件
try:
datei = datetime.datetime.strptime(acquisition_date.split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime("%Y%m%d")
if os.path.exists(os.path.join(path_RSLC, datei + '.vv.rslc')):
return True
except:
pass
return False
def read_netrc(machine_name):
"""
从 ~/.netrc 文件读取登录凭证
Args:
machine_name: 机器名称 (例如: urs.earthdata.nasa.gov)
Returns:
tuple: (username, password) 或 (None, None) 如果未找到
"""
netrc_path = os.path.expanduser('~/.netrc')
if not os.path.exists(netrc_path):
return None, None
try:
with open(netrc_path, 'r') as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
# 查找 machine 行
if line.startswith('machine ' + machine_name):
username = None
password = None
# 查找接下来的几行中的 login 和 password
j = i + 1
while j < len(lines) and j < i + 5: # 最多查找5行
next_line = lines[j].strip()
tokens = next_line.split()
if len(tokens) >= 2:
if tokens[0] == 'login':
username = tokens[1]
elif tokens[0] == 'password':
password = tokens[1]
# 如果找到下一个 machine,停止查找
if next_line.startswith('machine '):
break
j += 1
if username and password:
return username, password
i += 1
except Exception as e:
print(f"Warning: Error reading .netrc file: {str(e)}")
return None, None
def download_slc(url, username, password, path_SLC):
"""
下载单个 SLC 文件
Args:
url: 下载 URL
username: ASF 用户名
password: ASF 密码
path_SLC: 存储路径
Returns:
bool: 下载是否成功
"""
# 如果提供了用户名和密码,使用它们
if username and password:
cmd = f'wget -c --http-user={username} --http-password={password} "{url}" -P {path_SLC}'
else:
# 否则使用 .netrc 文件
# --auth-no-challenge: 立即发送认证信息,不等待服务器挑战
# --netrc: 从 ~/.netrc 读取认证信息
cmd = f'wget -c --auth-no-challenge --netrc "{url}" -P {path_SLC}'
return os.system(cmd) == 0
def convert_yyyymmdd_to_iso(date_str):
"""
将 YYYYMMDD 格式转换为 ISO 格式 (YYYY-MM-DDTHH:MM:SS)
Args:
date_str: 日期字符串 (YYYYMMDD)
Returns:
str: ISO 格式日期字符串 (YYYY-MM-DDT00:00:00)
"""
if len(date_str) == 8:
year = date_str[0:4]
month = date_str[4:6]
day = date_str[6:8]
return f"{year}-{month}-{day}T00:00:00"
else:
# 如果已经是其他格式,直接返回
return date_str
###########################################################################
###########################################################################
if len(sys.argv) < 3:
prog = os.path.basename(sys.argv[0])
print("="*80)
print("Sentinel-1 SLC Downloader from ASF")
print("="*80)
print("\nUsage examples:")
print("\n1. Using bounding box:")
print(" python3 " + prog + " -u username -p password -s . -r . \\")
print(" -b -10.78,51.27,-5.03,55.70 \\")
print(" -i 20170101 -j 20231231 \\")
print(f" -o 1 -f a -q IW -m csv -w n")
print("\n2. Using Shapefile (new feature):")
print(" python3 " + prog + " -u username -p password -s . -r . \\")
print(" --shp study_area.shp \\")
print(" -i 20170101 -j 20231231 \\")
print(f" -o 1 -f a -q IW -m csv -w n")
print("\nOptions:")
print(" -u, --username ASF username (optional, can use .netrc)")
print(" -p, --password ASF password (optional, can use .netrc)")
print(" -n, --netrc Machine name in ~/.netrc (default: urs.earthdata.nasa.gov)")
print(" -s, --path_SLC Output directory for SLC files")
print(" -r, --path_RSLC Directory to check for processed RSLC files")
print(" -b, --bbox Bounding box (min_lon,min_lat,max_lon,max_lat)")
print(" --shp Shapefile path (.shp) for spatial filtering")
print(" -i, --date_start Start date (YYYYMMDD)")
print(" -j, --date_end End date (YYYYMMDD)")
print(" -o, --orbit_relative Relative orbit number (optional)")
print(" --frame Frame number (optional)")
print(" -f, --flight_direction Flight direction (a=ascending, d=descending, or all)")
print(" -q, --q_acqui_mode Acquisition mode (IW, EW, SM, or all)")
print(" -m, --mode Output format (csv or kml)")
print(" -w, --write Download files? (Y/N)")
print(" --parallel Number of parallel downloads (default: 1, serial)")
print("\nNote: Shapefile support requires geopandas and shapely")
print("="*80)
sys.exit(-1)
else:
usage = "usage: %prog [options] "
parser = OptionParser(usage=usage)
parser.add_option("-u", "--username", action="store", type="string", default=None,
help="ASF username (can be read from ~/.netrc)")
parser.add_option("-p", "--password", action="store", type="string", default=None,
help="ASF password (can be read from ~/.netrc)")
parser.add_option("-n", "--netrc", action="store", type="string", default='urs.earthdata.nasa.gov',
help="Machine name in ~/.netrc file (default: urs.earthdata.nasa.gov)")
parser.add_option("-s", "--path_SLC", action="store", type="string", default='.')
parser.add_option("-r", "--path_RSLC", action="store", type="string", default='.') # Only available for GAMMA stack
parser.add_option("-b", "--bbox", action="store", type="string", default='-10.78,51.27,-5.03,55.70')
parser.add_option("--shp", "--shapefile", action="store", type="string", default=None,
help="Shapefile path for spatial filtering")
parser.add_option("-i", "--date_start", action="store", type="string", default='20170101')
parser.add_option("-j", "--date_end", action="store", type="string", default='20240101')
parser.add_option("-o", "--orbit_relative", action="store", type="float", default=None,
help="Relative orbit number (optional, if not specified will search all orbits)")
parser.add_option("--frame", action="store", type="int", default=None,
help="Frame number (optional, if not specified will search all frames)")
parser.add_option("-f", "--flight_direction", action="store", type="string", default='a',
help="Flight direction (a=ascending, d=descending, or 'all' for both)")
parser.add_option("-q", "--q_acqui_mode", action="store", type="string", default='IW')
parser.add_option("-m", "--mode", action="store", type="string", default='csv')
parser.add_option("-w", "--write", action="store", type="string", default='n')
parser.add_option("--parallel", action="store", type="int", default=1,
help="Number of parallel downloads (default: 1, serial)")
(options, args) = parser.parse_args()
###########################################################################
# Main
###########################################################################
date_format = "%Y-%m-%d"
# 转换日期格式
date_start_iso = convert_yyyymmdd_to_iso(options.date_start)
date_end_iso = convert_yyyymmdd_to_iso(options.date_end)
# 处理用户名和密码:如果未提供,尝试从 .netrc 读取
if options.username is None or options.password is None:
netrc_username, netrc_password = read_netrc(options.netrc)
if netrc_username and netrc_password:
if options.username is None:
options.username = netrc_username
print(f"Username read from ~/.netrc ({options.netrc})")
if options.password is None:
options.password = netrc_password
print(f"Password read from ~/.netrc ({options.netrc})")
else:
if options.username is None:
print("Error: Username not provided and not found in ~/.netrc")
print("Please provide username with -u option or configure ~/.netrc")
sys.exit(1)
if options.password is None:
print("Error: Password not provided and not found in ~/.netrc")
print("Please provide password with -p option or configure ~/.netrc")
sys.exit(1)
# 处理空间范围:优先使用 Shapefile,否则使用 bounding box
if options.shp:
if not HAS_SHAPEFILE_SUPPORT:
print("Error: Shapefile support requires geopandas and shapely")
print("Install with: pip install geopandas shapely")
sys.exit(1)
print(f"Using Shapefile: {options.shp}")
bbox = bbox_from_shapefile(options.shp)
if bbox is None:
print("Error: Failed to extract bounding box from shapefile")
sys.exit(1)
else:
bbox = options.bbox
print(f"Using bounding box: {bbox}")
# 构建 API 查询命令
# 轨道号、frame 和飞行方向都是可选的
api_params = f"platform=s1&bbox={bbox}&start={date_start_iso}-UTC&end={date_end_iso}-UTC&processingLevel=SLC&maxResults=10000"
# 如果指定了轨道号,添加到查询参数
if options.orbit_relative is not None:
api_params += f"&relativeOrbit={int(options.orbit_relative)}"
# 如果指定了 frame 号,添加到查询参数
if hasattr(options, 'frame') and options.frame is not None:
api_params += f"&frame={options.frame}"
# 如果指定了飞行方向且不是 'all',添加到查询参数
if options.flight_direction and options.flight_direction.upper() != 'ALL':
api_params += f"&flightDirection={options.flight_direction.upper()}"
# 添加输出格式
api_params += f"&output={options.mode.upper()}"
cmd1 = f'curl "https://api.daac.asf.alaska.edu/services/search/param?{api_params}" > SLC_list.{options.mode.lower()}'
print(f"Querying ASF API for SLC data...")
print(f" Bounding Box: {bbox}")
print(f" Date Range: {options.date_start} to {options.date_end}")
if options.orbit_relative is not None:
print(f" Orbit: {options.orbit_relative}")
else:
print(f" Orbit: All")
if hasattr(options, 'frame') and options.frame is not None:
print(f" Frame: {options.frame}")
else:
print(f" Frame: All")
if options.flight_direction and options.flight_direction.upper() != 'ALL':
print(f" Flight Direction: {options.flight_direction.upper()}")
else:
print(f" Flight Direction: All")
print(f" Mode: {options.q_acqui_mode.upper()}")
print()
# 执行查询
if options.mode.upper() == 'KML':
os.system(cmd1)
elif options.mode.upper() == 'CSV':
os.system(cmd1)
else:
print('Error: Please select a correct mode (csv or kml)...')
sys.exit(1)
# 根据波束模式过滤列表
print(f"Filtering results by acquisition mode: {options.q_acqui_mode.upper()}")
if os.path.exists("SLC_list.csv"):
os.rename("SLC_list.csv","SLC_list_orig.csv")
h = 0
fout = open("SLC_list.csv",'w')
total_count = 0
matched_count = 0
with open("SLC_list_orig.csv") as fi:
for li in fi:
total_count += 1
if h > 0:
if options.q_acqui_mode.upper() == 'all':
fout.write(li)
matched_count += 1
elif options.q_acqui_mode.upper() in li:
fout.write(li)
matched_count += 1
else:
fout.write(li)
h = h + 1
fout.close()
os.remove("SLC_list_orig.csv")
print(f" Total results: {total_count - 1}")
print(f" Filtered results: {matched_count}")
else:
print("Warning: SLC_list.csv not found")
sys.exit(1)
# 下载文件
if options.mode.upper() == 'CSV' and options.write.upper() == 'Y':
try:
listSLC = pd.read_csv("SLC_list.csv")
except Exception as e:
print(f"Error reading SLC_list.csv: {str(e)}")
sys.exit(1)
print(f"\nFound {len(listSLC)} SLC files to process")
print("="*80)
# 创建输出目录
if not os.path.exists(options.path_SLC):
os.makedirs(options.path_SLC)
print(f"Created output directory: {options.path_SLC}")
# 检查是否有并行参数
parallel_numb = getattr(options, 'parallel', 1)
if parallel_numb is None:
parallel_numb = 1
# 准备下载数据列表
download_tasks = []
skipped_tasks = []
for h, slci in enumerate(listSLC['Granule Name']):
url = listSLC['URL'][h]
acquisition_date = listSLC['Acquisition Date'][h]
# 检查文件是否已存在
if check_slc_exists(slci, options.path_SLC, options.path_RSLC, acquisition_date):
skipped_tasks.append((h+1, slci, "Already exists"))
else:
download_tasks.append((h+1, slci, url, acquisition_date))
print(f"Files to download: {len(download_tasks)}")
print(f"Files skipped (already exists): {len(skipped_tasks)}")
# 并行下载函数
def download_worker(task):
idx, slci, url, acquisition_date = task
print(f"\n[{idx}/{len(listSLC)}] Downloading: {slci}")
if download_slc(url, options.username, options.password, options.path_SLC):
print(f" Status: Download SUCCESS")
return (idx, slci, "SUCCESS")
else:
print(f" Status: Download FAILED")
return (idx, slci, "FAILED")
# 执行下载
downloaded = 0
failed = 0
if parallel_numb > 1 and len(download_tasks) > 0:
# 并行下载
from multiprocessing.pool import ThreadPool
print(f"\nStarting parallel download with {parallel_numb} workers...")
pool = ThreadPool(parallel_numb)
results = pool.map(download_worker, download_tasks)
pool.close()
pool.join()
# 统计结果
for result in results:
if result[2] == "SUCCESS":
downloaded += 1
else:
failed += 1
elif len(download_tasks) > 0:
# 串行下载
print("\nStarting serial download...")
for task in download_tasks:
result = download_worker(task)
if result[2] == "SUCCESS":
downloaded += 1
else:
failed += 1
# 打印摘要
print("\n" + "="*80)
print("DOWNLOAD SUMMARY")
print("="*80)
print(f"Total SLC files: {len(listSLC)}")
print(f"Downloaded: {downloaded}")
print(f"Skipped (already exists): {len(skipped_tasks)}")
print(f"Failed: {failed}")
print("="*80)
if failed > 0:
print(f"\nWarning: {failed} downloads failed. Check the output above for details.")
sys.exit(1)
elif options.mode.upper() == 'KML' and options.write.upper() == 'Y':
print('Please, select CSV mode to download..')
elif options.write.upper() != 'Y':
print(f"\nSLC list saved to: SLC_list.csv")
print("To download files, re-run with -w Y option")