ai_project_v1/b3dm/slope_aspect_img.py

380 lines
14 KiB
Python
Raw Normal View History

# slope_aspect_img13
2026-01-14 11:37:35 +08:00
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import sobel
import matplotlib as mpl
import rasterio
from osgeo import gdal
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
import os
# 自定义3D箭头类
class Arrow3D(FancyArrowPatch):
def __init__(self, xs, ys, zs, *args, **kwargs):
super().__init__((0,0), (0,0), *args, **kwargs)
self._verts3d = xs, ys, zs
def do_3d_projection(self, renderer=None):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return min(zs)
def read_dem_rasterio(filepath):
"""使用rasterio读取DEM数据"""
try:
with rasterio.open(filepath) as src:
dem_data = src.read(1)
transform = src.transform
bounds = src.bounds
crs = src.crs
print(f"DEM信息:")
print(f" 尺寸: {dem_data.shape}")
print(f" 范围: {bounds}")
print(f" 坐标系: {crs}")
print(f" 高程范围: {np.nanmin(dem_data):.2f} - {np.nanmax(dem_data):.2f}")
if np.isnan(dem_data).any():
print(" 检测到NaN值")
dem_data = np.nan_to_num(dem_data, nan=np.nanmean(dem_data))
rows, cols = dem_data.shape
x = np.linspace(bounds.left, bounds.right, cols)
y = np.linspace(bounds.bottom, bounds.top, rows)
X, Y = np.meshgrid(x, y)
return X, Y, dem_data
except Exception as e:
print(f"使用rasterio读取失败: {e}")
return None
def read_slope_aspect_by_dem(dem_path, overall_3d_output_path=None) :
# ---------------------- 核心:解决中文乱码配置 ----------------------
setup_chinese_font()
# 尝试读取DEM数据
print("正在读取DEM文件...")
X, Y, Z = read_dem_rasterio(dem_path)
if X is None:
raise FileNotFoundError(f"无法读取DEM文件: {dem_path}")
# 检查数据有效性
if Z.size == 0 or np.all(Z == 0):
raise ValueError("DEM数据无效或全部为0")
# 重采样
if Z.shape[0] > 200 or Z.shape[1] > 200:
print(f"原始DEM尺寸较大 ({Z.shape}),进行重采样...")
from scipy.ndimage import zoom
scale_factor = min(200/Z.shape[0], 200/Z.shape[1])
Z = zoom(Z, scale_factor, order=1)
X = zoom(X, scale_factor, order=1)
Y = zoom(Y, scale_factor, order=1)
print(f"重采样后尺寸: {Z.shape}")
# ---------------------- 2. 计算坡度和坡向 ----------------------
print("计算坡度和坡向...")
dx_pixel = np.abs(X[0,1] - X[0,0]) * 111000
dy_pixel = np.abs(Y[1,0] - Y[0,0]) * 111000
dx = sobel(Z, axis=1) / (2 * dx_pixel)
dy = sobel(Z, axis=0) / (2 * dy_pixel)
slope_rad = np.arctan(np.sqrt(dx**2 + dy**2))
slope_deg = slope_rad * (180 / np.pi)
aspect_rad = np.arctan2(-dx, dy)
aspect_deg = aspect_rad * (180 / np.pi)
aspect_deg[aspect_deg < 0] += 360
print(f"坡度范围: {np.min(slope_deg):.2f}° - {np.max(slope_deg):.2f}°")
print(f"坡向范围: {np.min(aspect_deg):.2f}° - {np.max(aspect_deg):.2f}°")
# ---------------------- 3. 计算整体坡向 ----------------------
print("\n计算整体坡向...")
def calculate_overall_aspect(aspect_deg, slope_deg, method='weighted_mean'):
"""计算整体坡向"""
aspect_rad = np.deg2rad(aspect_deg)
if method == 'weighted_mean':
# 坡度加权平均法
u = np.sin(aspect_rad)
v = np.cos(aspect_rad)
weights = slope_deg.flatten()
weighted_u = np.nansum(u.flatten() * weights) / np.nansum(weights)
weighted_v = np.nansum(v.flatten() * weights) / np.nansum(weights)
weighted_aspect_rad = np.arctan2(weighted_u, weighted_v)
weighted_aspect_deg = np.rad2deg(weighted_aspect_rad)
if weighted_aspect_deg < 0:
weighted_aspect_deg += 360
weighted_strength = np.sqrt(weighted_u**2 + weighted_v**2)
return weighted_aspect_deg, weighted_strength, "坡度加权平均法"
elif method == 'vector_mean':
# 向量平均法
u = np.sin(aspect_rad)
v = np.cos(aspect_rad)
mean_u = np.nanmean(u)
mean_v = np.nanmean(v)
mean_aspect_rad = np.arctan2(mean_u, mean_v)
mean_aspect_deg = np.rad2deg(mean_aspect_rad)
if mean_aspect_deg < 0:
mean_aspect_deg += 360
vector_strength = np.sqrt(mean_u**2 + mean_v**2)
return mean_aspect_deg, vector_strength, "向量平均法"
# 使用坡度加权平均法计算整体坡向
overall_aspect, overall_strength, method_name = calculate_overall_aspect(aspect_deg, slope_deg, 'weighted_mean')
# 将整体坡向转换为方向描述
if overall_aspect < 22.5 or overall_aspect >= 337.5:
overall_direction = ""
elif 22.5 <= overall_aspect < 67.5:
overall_direction = "东北"
elif 67.5 <= overall_aspect < 112.5:
overall_direction = ""
elif 112.5 <= overall_aspect < 157.5:
overall_direction = "东南"
elif 157.5 <= overall_aspect < 202.5:
overall_direction = ""
elif 202.5 <= overall_aspect < 247.5:
overall_direction = "西南"
elif 247.5 <= overall_aspect < 292.5:
overall_direction = "西"
else:
overall_direction = "西北"
print(f"整体坡向 ({method_name}):")
print(f" 角度: {overall_aspect:.1f}°")
print(f" 方向: {overall_direction}")
# print(f" 一致性: {overall_strength:.3f}")
# ---------------------- 5. 3D可视化俯视图包含整体坡向和关键点坡向 ----------------------
print("\n生成3D俯视图可视化...")
fig3d, ax3d = plt.subplots(figsize=(16, 12), subplot_kw={"projection": "3d"})
# 绘制地形曲面 - 俯视图需要更清晰的地形表现
norm = mpl.colors.Normalize(vmin=np.percentile(slope_deg, 5),
vmax=np.percentile(slope_deg, 95))
plot_skip = max(2, Z.shape[0] // 60) # 增加采样密度,使俯视图更清晰
X_plot = X[::plot_skip, ::plot_skip]
Y_plot = Y[::plot_skip, ::plot_skip]
Z_plot = Z[::plot_skip, ::plot_skip]
slope_plot = slope_deg[::plot_skip, ::plot_skip]
surf = ax3d.plot_surface(
X_plot, Y_plot, Z_plot,
cmap="viridis_r",
alpha=0.85, # 增加透明度,使箭头更明显
linewidth=0.1, # 很细的网格线
facecolors=plt.cm.viridis_r(norm(slope_plot)),
zorder=1
)
# ---------------------- 绘制整体坡向箭头(中心位置,红色粗箭头) ----------------------
center_x = (X.min() + X.max()) / 2
center_y = (Y.min() + Y.max()) / 2
# 整体坡向箭头长度
arrow_length_overall = 0.15 * min(X.max()-X.min(), Y.max()-Y.min())
# 计算整体坡向箭头方向
scale_factor = 1.5
overall_aspect_rad = np.deg2rad(overall_aspect)
dx_overall = np.sin(overall_aspect_rad) * arrow_length_overall * scale_factor
dy_overall = np.cos(overall_aspect_rad) * arrow_length_overall * scale_factor
# 方案2如果希望箭头在地形上方一定高度
terrain_max_z = Z.max() # 地形最高点
float_height = 0.2 * terrain_max_z # 在地形最高点上方20%的高度
# 绘制整体坡向箭头(红色,粗)
arrow_overall = Arrow3D(
[center_x, center_x + dx_overall],
[center_y, center_y + dy_overall],
[terrain_max_z + float_height, terrain_max_z + float_height], # 在地形上方
mutation_scale=25, # 稍大一些的箭头
lw=5, # 更粗的线
arrowstyle='-|>',
color='red',
alpha=0.98, # 更高的透明度
zorder=20 # 更高的绘制顺序
)
ax3d.add_artist(arrow_overall)
# 在整体坡向箭头起点添加标记
ax3d.scatter(center_x, center_y, terrain_max_z + float_height,
color='red', s=120, edgecolor='white', linewidth=2, zorder=21)
# ---------------------- 整体坡向面板放置在左上角 ----------------------
# 计算左上角位置
panel_x = X.min() + 0.02 * (X.max() - X.min()) # 左边留2%的边距
panel_y = Y.max() - 0.02 * (Y.max() - Y.min()) # 上边留2%的边距
panel_z = Z.max() + 0.5 * (Z.max() - Z.min()) # 进一步提高Z坐标确保在视野内
# 整体坡向面板内容
overall_info = (
f"整体坡向分析\n"
f"角度: {overall_aspect:.1f}°\n"
f"方向: {overall_direction}"
# f"一致性: {overall_strength:.3f}"
)
# 绘制整体坡向面板(左上角)
ax3d.text(panel_x, panel_y, panel_z,
overall_info,
fontsize=12, fontweight='bold',
bbox=dict(facecolor='white', alpha=0.5, boxstyle="round,pad=0.5",
edgecolor='red', linewidth=2.5),
ha='left', va='top', zorder=30)
# ---------------------- 设置3D图参数 ----------------------
ax3d.set_xlabel("经度 (X)", fontsize=12)
ax3d.set_ylabel("纬度 (Y)", fontsize=12)
ax3d.set_zlabel("高程 (m)", fontsize=12)
# 获取文件名用于标题
filename = os.path.basename(dem_path)
ax3d.set_title(f"DEM三维俯视图 - 坡向分析\n文件: {filename}",
fontsize=14, fontweight='bold', pad=20)
# 添加颜色条
cbar = plt.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap='viridis_r'),
ax=ax3d, shrink=0.6, aspect=25, pad=0.15
)
cbar.set_label("坡度 (°)", fontsize=12)
# ---------------------- 设置为俯视图(高仰角) ----------------------
view_elev = 85 # 接近90度的仰角俯视效果
view_azim = overall_aspect + 180 # 从坡向的相反方向观看,可以看到坡面
# 确保方位角在0-360度范围内
view_azim = view_azim % 360
print(f"\n设置3D俯视图视角:")
print(f" 整体坡向: {overall_aspect:.1f}° ({overall_direction})")
print(f" 视角方位角: {view_azim:.1f}°")
print(f" 视角仰角: {view_elev:.1f}°")
# 应用俯视图视角设置
ax3d.view_init(elev=view_elev, azim=view_azim)
# 调整相机距离,使视角更广
ax3d.dist = 9.0 # 增加相机距离
# 设置坐标轴范围,确保所有元素都显示
z_min, z_max = Z.min(), Z.max()
z_padding = 0.6 * (z_max - z_min) # 适当增加Z轴范围
ax3d.set_zlim(z_min - 0.1*z_padding, z_max + z_padding)
# 设置XY轴范围
x_margin = 0.1 * (X.max() - X.min())
y_margin = 0.1 * (Y.max() - Y.min())
ax3d.set_xlim(X.min() - x_margin, X.max() + x_margin)
ax3d.set_ylim(Y.min() - y_margin, Y.max() + y_margin)
plt.tight_layout()
# 保存图片
if not overall_3d_output_path :
overall_3d_output_path = dem_path.replace('.tif', '_slopeAspect_3D_overlook.png')
plt.savefig(overall_3d_output_path, dpi=250, bbox_inches='tight', facecolor='white')
# plt.show()
os.remove(dem_path)
print(f"\n3D俯视图已保存: {overall_3d_output_path}")
print("分析完成!")
print("="*60)
return overall_3d_output_path
# ---------------------- 跨平台中文字体配置 ----------------------
def setup_chinese_font():
"""设置中文字体支持兼容Windows、Linux、macOS"""
import platform
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
system = platform.system()
# 尝试添加系统中文字体路径
font_paths = []
if system == 'Windows':
# Windows 字体路径
font_paths = [
'C:/Windows/Fonts/simhei.ttf', # 黑体
'C:/Windows/Fonts/simkai.ttf', # 楷体
'C:/Windows/Fonts/simsun.ttc', # 宋体
'C:/Windows/Fonts/microsoftyahei.ttf', # 微软雅黑
]
elif system == 'Darwin': # macOS
# macOS 字体路径
font_paths = [
'/System/Library/Fonts/PingFang.ttc', # 苹方
'/System/Library/Fonts/STHeiti Light.ttc', # 华文黑体
'/System/Library/Fonts/STHeiti Medium.ttc',
'/Library/Fonts/Arial Unicode.ttf', # Arial Unicode
]
elif system == 'Linux':
# Linux 字体路径
font_paths = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', # 文泉驿微米黑
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', # 文泉驿正黑
'/usr/share/fonts/truetype/arphic/uming.ttc', # AR PL UMing
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', # Noto
]
# 尝试找到并添加第一个可用的中文字体
font_added = False
for font_path in font_paths:
try:
if os.path.exists(font_path):
font_prop = mpl.font_manager.FontProperties(fname=font_path)
font_name = font_prop.get_name()
mpl.font_manager.fontManager.addfont(font_path)
plt.rcParams['font.sans-serif'] = [font_name] + plt.rcParams['font.sans-serif']
font_added = True
print(f"已添加中文字体: {font_name} ({font_path})")
break
except Exception as e:
print(f"添加字体 {font_path} 失败: {e}")
continue
# 如果以上方法都失败,使用通用备选方案
if not font_added:
if system == 'Windows':
fallback_fonts = ['SimHei', 'Microsoft YaHei', 'KaiTi', 'FangSong']
elif system == 'Darwin':
fallback_fonts = ['PingFang SC', 'Hiragino Sans GB', 'Apple LiGothic Medium']
else: # Linux and others
fallback_fonts = ['DejaVu Sans', 'WenQuanYi Micro Hei',
'Noto Sans CJK SC', 'Heiti TC', 'AR PL UMing CN']
current_fonts = plt.rcParams.get('font.sans-serif', [])
plt.rcParams['font.sans-serif'] = fallback_fonts + current_fonts
print(f"使用备选字体方案: {fallback_fonts[:2]}...")
# 设置字体族
plt.rcParams['font.family'] = 'sans-serif'
if __name__ == "__main__":
dem_path = r'D:\devForBdzlWork\ai_project_v1\b3dm\test\o_dem_df67f1cc.tif'
read_slope_aspect_by_dem(dem_path)