Files
BRDF/slope/slope_aspectV2.py
2026-04-22 09:27:59 +08:00

381 lines
12 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 python3
# -*- coding: utf-8 -*-
"""
DEM地形处理工具 - 计算坡度、坡向和太阳入射角余弦
该工具从DEM文件中提取坐标信息根据给定时间计算太阳位置
并生成坡度、坡向和cosine_i的多波段ENVI文件。
作者: BRDF_GUI Team
版本: 2.0.0
"""
import xdem
import rasterio
import numpy as np
import pandas as pd
import argparse
import sys
from datetime import datetime
from pathlib import Path
import pvlib.solarposition as solarposition
from rasterio.crs import CRS
import pyproj
def calc_cosine_i(solar_zn, solar_az, aspect, slope):
"""
计算入射角余弦cosine i
入射角余弦定义为地表法线与太阳天顶方向之间夹角的余弦值。
所有角度输入必须为弧度。
Args:
solar_az (numpy.ndarray): 太阳方位角(弧度)
solar_zn (numpy.ndarray): 太阳天顶角(弧度)
aspect (numpy.ndarray): 地面坡向(弧度)
slope (numpy.ndarray): 地面坡度(弧度)
Returns:
numpy.ndarray: 入射角余弦图像
"""
relative_az = aspect - solar_az
cosine_i = (np.cos(solar_zn) * np.cos(slope) +
np.sin(solar_zn) * np.sin(slope) * np.cos(relative_az))
return cosine_i
def get_dem_center_coords(dem):
"""
获取DEM图像中心点的经纬度坐标。
Args:
dem: xdem.DEM对象
Returns:
tuple: (纬度, 经度) - 单位:度
"""
# 获取DEM的范围边界
bounds = dem.bounds # (left, bottom, right, top)
center_x = (bounds.left + bounds.right) / 2
center_y = (bounds.bottom + bounds.top) / 2
# 如果DEM是投影坐标系需要转换为WGS84经纬度
if dem.crs is not None and not dem.crs.is_geographic:
# 创建从投影坐标系到WGS84的转换
transformer = pyproj.Transformer.from_crs(dem.crs, CRS.from_epsg(4326), always_xy=True)
lon, lat = transformer.transform(center_x, center_y)
else:
# 已经是地理坐标系(经纬度)
lon, lat = center_x, center_y
return lat, lon
def parse_datetime(datetime_str):
"""
解析日期时间字符串。
支持的格式:
- YYYY-MM-DD HH:MM:SS
- YYYY-MM-DDTHH:MM:SS
- YYYYMMDD_HHMMSS
Args:
datetime_str (str): 日期时间字符串
Returns:
datetime: datetime对象UTC时间
"""
formats = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S",
"%Y%m%d_%H%M%S",
"%Y-%m-%d",
]
for fmt in formats:
try:
dt = datetime.strptime(datetime_str, fmt)
# 如果只输入了日期默认时间为正午12:00
if fmt == "%Y-%m-%d":
dt = dt.replace(hour=12, minute=0, second=0)
return dt
except ValueError:
continue
raise ValueError(f"无法解析日期时间: {datetime_str}. 支持的格式: YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SS, YYYYMMDD_HHMMSS")
def get_user_input_datetime():
"""
交互式获取用户输入的日期和时间。
Returns:
datetime: datetime对象UTC时间
"""
print("请输入观测日期和时间UTC时间")
try:
year = int(input(" 年份 (YYYY): "))
month = int(input(" 月份 (MM): "))
day = int(input(" 日期 (DD): "))
hour = int(input(" 小时 (HH, 24小时制): "))
minute = int(input(" 分钟 (MM): "))
second = int(input(" 秒 (SS): "))
dt = datetime(year, month, day, hour, minute, second)
print(f"\n使用的时间: {dt.strftime('%Y-%m-%d %H:%M:%S')} UTC")
return dt
except ValueError as e:
print(f"输入错误: {e}")
print("将使用默认时间 2024-06-15 10:00:00 UTC")
return datetime(2024, 6, 15, 10, 0, 0)
def calc_solar_angles(lat, lon, dt):
"""
使用pvlib计算太阳天顶角和方位角。
Args:
lat (float): 纬度(度)
lon (float): 经度(度)
dt (datetime): datetime对象UTC时间
Returns:
tuple: (太阳天顶角, 太阳方位角) - 单位:度
天顶角: 0=天顶, 90=地平线
方位角: 从北顺时针0=北90=东180=南270=西
"""
# 创建时间索引
times = pd.DatetimeIndex([dt])
# 使用pvlib计算太阳位置
solpos = solarposition.get_solarposition(times, lat, lon)
solar_zn_deg = solpos['zenith'].values[0] # 天顶角
solar_az_deg = solpos['azimuth'].values[0] # 方位角
return solar_zn_deg, solar_az_deg
def write_envi_output(output_path, data, transform, crs, metadata=None):
"""
将数据写入ENVI格式文件。
Args:
output_path (str): 输出文件路径
data (numpy.ndarray): 数据数组 (bands, rows, cols)
transform: 地理变换参数
crs: 坐标参考系
metadata (dict, optional): 额外的元数据
"""
with rasterio.open(
output_path,
'w',
driver='ENVI',
height=data.shape[1],
width=data.shape[2],
count=data.shape[0],
dtype=data.dtype,
crs=crs,
transform=transform,
nodata=0.0,
) as dst:
dst.write(data)
# 写入ENVI头文件元数据
if metadata:
dst.update_tags(**metadata)
def process_dem(dem_path, output_path, datetime_input=None, interactive=False):
"""
主处理函数读取DEM计算坡度坡向计算太阳角度输出结果。
Args:
dem_path (str): DEM输入文件路径
output_path (str): 输出文件路径
datetime_input (str, optional): 日期时间字符串如果为None则使用交互模式
interactive (bool): 是否使用交互式时间输入
Returns:
dict: 处理结果信息
"""
# ========== 1. 读取 DEM ==========
print(f"读取DEM文件: {dem_path}")
dem = xdem.DEM(dem_path)
# ========== 2. 计算坡度、坡向 ==========
print("计算坡度和坡向...")
slope = xdem.terrain.slope(dem, method='ZevenbergThorne')
aspect = xdem.terrain.aspect(dem)
# ========== 3. 获取空间参考信息 ==========
transform = dem.transform
crs = dem.crs
# ========== 4. 转换为 numpy 数组 ==========
slope_data = slope.data
aspect_data = aspect.data
# ========== 5. 获取DEM中心点坐标并计算太阳角度 ==========
print("\n获取DEM坐标信息...")
latitude, longitude = get_dem_center_coords(dem)
print(f" 中心点纬度: {latitude:.6f}°")
print(f" 中心点经度: {longitude:.6f}°")
print(f" 坐标系: WGS84")
# 获取观测时间
if datetime_input:
observation_time = parse_datetime(datetime_input)
print(f"\n使用命令行提供的时间: {observation_time.strftime('%Y-%m-%d %H:%M:%S')} UTC")
elif interactive or not sys.stdin.isatty():
observation_time = get_user_input_datetime()
else:
# 默认时间(如果非交互式且未提供时间)
observation_time = datetime(2024, 6, 15, 10, 0, 0)
print(f"\n使用默认时间: {observation_time.strftime('%Y-%m-%d %H:%M:%S')} UTC")
# 计算太阳角度
print("\n计算太阳位置...")
solar_zn_deg, solar_az_deg = calc_solar_angles(latitude, longitude, observation_time)
print(f" 太阳天顶角: {solar_zn_deg:.2f}°")
print(f" 太阳高度角: {90 - solar_zn_deg:.2f}°")
print(f" 太阳方位角: {solar_az_deg:.2f}°")
# 转换为弧度
solar_zn_rad = np.deg2rad(solar_zn_deg)
solar_az_rad = np.deg2rad(solar_az_deg)
# 坡度和坡向也转换为弧度
slope_rad = np.deg2rad(slope_data)
aspect_rad = np.deg2rad(aspect_data)
# ========== 6. 计算 cosine_i ==========
print("\n计算入射角余弦 (cosine_i)...")
cosine_i = calc_cosine_i(solar_zn_rad, solar_az_rad, aspect_rad, slope_rad)
# ========== 7. 处理无效值NaN ==========
slope_data = np.nan_to_num(slope_data, nan=0.0)
aspect_data = np.nan_to_num(aspect_data, nan=0.0)
cosine_i = np.nan_to_num(cosine_i, nan=0.0)
# ========== 8. 堆叠为多波段数组 ==========
stacked = np.stack([slope_data, aspect_data, cosine_i], axis=0)
# ========== 9. 准备元数据 ==========
metadata = {
'description': 'Slope, Aspect, and Cosine_i derived from DEM',
'band_names': 'slope(deg),aspect(deg),cosine_i',
'solar_zenith_angle': str(solar_zn_deg),
'solar_azimuth_angle': str(solar_az_deg),
'solar_elevation_angle': str(90 - solar_zn_deg),
'observation_time_utc': observation_time.strftime('%Y-%m-%d %H:%M:%S'),
'dem_center_lat': str(latitude),
'dem_center_lon': str(longitude),
}
# ========== 10. 输出 ENVI 格式文件 ==========
print(f"\n保存结果到: {output_path}")
write_envi_output(output_path, stacked, transform, crs, metadata)
print("\n处理完成!")
print(f" 输出文件: {output_path}")
print(f" 波段顺序: 1-坡度(°), 2-坡向(°), 3-cosine_i")
return {
'output_path': output_path,
'solar_zenith': solar_zn_deg,
'solar_azimuth': solar_az_deg,
'center_lat': latitude,
'center_lon': longitude,
'observation_time': observation_time,
}
def main():
"""主函数 - 命令行入口"""
parser = argparse.ArgumentParser(
description='DEM地形处理工具 - 计算坡度、坡向和太阳入射角余弦',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
使用示例:
# 基本用法(交互式输入时间)
python slope_aspectV2.py -i dem.tif -o output.dat
# 通过命令行指定时间
python slope_aspectV2.py -i dem.tif -o output.dat -t "2024-06-15 10:30:00"
# 仅指定日期默认使用正午12:00
python slope_aspectV2.py -i dem.tif -o output.dat -t "2024-06-15"
# 显示详细处理信息
python slope_aspectV2.py -i dem.tif -o output.dat -t "2024-06-15 10:30:00" -v
支持的日期时间格式:
- "YYYY-MM-DD HH:MM:SS" (推荐)
- "YYYY-MM-DDTHH:MM:SS" (ISO格式)
- "YYYYMMDD_HHMMSS"
- "YYYY-MM-DD" (默认使用12:00:00)
'''
)
parser.add_argument('-i', '--input', required=True,
help='输入DEM文件路径 (支持GeoTIFF等格式)')
parser.add_argument('-o', '--output', required=True,
help='输出ENVI文件路径 (.dat)')
parser.add_argument('-t', '--time',
help='观测日期时间 (UTC),格式: "YYYY-MM-DD HH:MM:SS"'
'如果不提供,将进入交互式输入模式')
parser.add_argument('--interactive', action='store_true',
help='强制使用交互式时间输入模式')
parser.add_argument('-v', '--verbose', action='store_true',
help='显示详细处理信息')
parser.add_argument('--version', action='version', version='%(prog)s 2.0.0')
args = parser.parse_args()
# 验证输入文件存在
if not Path(args.input).exists():
print(f"错误: 输入文件不存在: {args.input}", file=sys.stderr)
sys.exit(1)
# 创建输出目录(如果不存在)
output_dir = Path(args.output).parent
if not output_dir.exists():
output_dir.mkdir(parents=True, exist_ok=True)
print(f"创建输出目录: {output_dir}")
try:
# 执行处理
result = process_dem(
dem_path=args.input,
output_path=args.output,
datetime_input=args.time,
interactive=args.interactive
)
if args.verbose:
print("\n详细结果:")
for key, value in result.items():
print(f" {key}: {value}")
sys.exit(0)
except Exception as e:
print(f"\n处理失败: {e}", file=sys.stderr)
if args.verbose:
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()