从WM2到Terrain

西昌.何雨锋

步骤:

1、在WM2中建立一个陆地,导出为obj。

     WM2中建立一个mesh output节点。然后从Terrace中导出mesh和高度图,高度图负责在图形引擎中进行陆地的建立,而mesh则可以导入到3DSMAX中作为物体渲染。

当然有人会问,为什么不用高度图在物理中进行物理陆地的建立呢?当然也可以,但是精度很难控制,特别是在放大了多倍之后,物理陆地将变成卡斯特地形。

 

2、将导出的obj文件导入到max中。

需要注意以下几点:

a.没必要导入材质,因为物理引擎根本就不会在图形中渲染,所以没必要导这个。

b.作为单个网格导入且允许重新划分三角形

c.单位和比例导入比例应该自己输入而不要用系统自带的那些比例,比如要生成一个512x512的地形,则填写512倍.

d.必须选中翻转xz轴,否则导入max的地形将会是水平的,再导到物理中时就变垂直了,但没有要中心轴,怕到了物理里时以陆地的中间为00点.(结果物理陆地的00点位置在角上而没在中间,同TV同)

e.导入的地形是竖着的,没必要去将它旋转90度平放,否则转换出的hkt在运行时仍然会是竖着的,没必要到程序里又去给做旋转。相反,在max里将其左右旋转180度,则在物理中被读出时,才能使正面朝上,应在导入max时、导入hct时,导入物理时分别检查地形形状,应当完全相同才对。

f.无论在WM2中地图的宽和高设置得再大,产生的obj文件大小都是19M,面数都是52万个,导入MAX后的尺寸大小不会有任何变化。

 

3、将mesh导出到hct

将导入的地形mesh给加形状和刚体,形状类型为mesh,重力为0

然后导出到HCT

保存为hkt文件(注意hct的版本不能比sdk还高)

4、在程序中读出hct文件,并以刚体形式加入到场景中。

 

5、渲染引擎中的高度图

如图所示,在TV渲染中,00点是高度图的左上角,(0,x)在左下角.

实际上与WM2导出的地图上下、左右刚好颠倒,此时,生成的高度地形严格与物理地形匹配。

以生成物理与渲染均为512的地形为例,物理采用直接读取刚体地形,位置偏移:m_terrainBody->setPosition(hkVector4(512,0,0,0));

而TV渲染地形为:

land1->GenerateTerrain("map\\heightmap\\output2.bmp",cTV_PRECISION_LOW,8,8,512,512,true);
land1->SetScale(0.25,0.65,0.25);   //注意该放缩值
land1->SetPosition(0,2,0);

此时,渲染地形虽未必同物理地形方向相同,但形状已完全相同,且其中物体(物体坐标及旋转均无需做xz变换)严格遵守地形物理法则。

当然,地形的高度图方向变化之后,其贴图方向也应做相应方向变换,否则渲染出的地形贴图无法与地形匹配。

 6、4096地图的建立

一定要注意,前面WM2中的地形最好是512而不是513的,以免渲染地形时产生变形与物理地形不一致。

渲染地图的生成为:

land1->GenerateTerrain("map\\heightmap\\output.bmp",cTV_PRECISION_LOW,8,8,512,512,true);
 land1->SetScale(2,5.54,2);
 land1->SetPosition(0,-1074,0);

注意高度。

物理地形的位置为:

setPosition(hkVector4(0,-1024,4096,0));

其中物理地形在max中只有旋转,没有移动的。

 7、16384地图的建立

注意:setBroadPhaseWorldSize(50000.0f),否则地形过了界就会消失。

地形生成为

 land1->GenerateTerrain("map\\heightmap\\output.bmp",cTV_PRECISION_HIGH,8,8,512,512,true);  //这里地形变大了,所以要用精度要高
 land1->SetScale(8,21.09,8);      //这个Y值为物理最高最低点之差/渲染最高最低点之差*原放缩值
 land1->SetPosition(0,-4038,0);

物理地形的位置为:

hkVector4(0,-4048,16384,0));

 关于mesh物体物理与渲染之大小、零点位置对齐等问题,以《从WM2到mesh物体》中的做法为标准。

import os import sys import logging import traceback import numpy as np import matplotlib matplotlib.use('TkAgg') import matplotlib.pyplot as plt from datetime import datetime from matplotlib.colors import BoundaryNorm import cartopy.crs as ccrs import rioxarray as rxr import geopandas as gpd import tkinter as tk from tkinter import ttk, filedialog, messagebox, StringVar from PIL import Image, ImageTk import threading import requests import platform import psutil import gc import time class GeoElevationVisualizerStable: def __init__(self, root): self.root = root self.root.title("高程可视化工具 (稳定版)") self.root.geometry("1100x750") self.root.resizable(True, True) # 绑定关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # 配置日志 self.setup_logging() # 初始化变量 self.region_gdf = None self.running_thread = None self.stop_event = threading.Event() # 创建GUI框架 self.create_widgets() # 设置默认值 self.default_values() # 加载初始数据(如果存在) self.load_last_settings() # 启动内存监控 self.start_memory_monitor() self.logger.info("应用程序初始化完成") def on_closing(self): """窗口关闭时的处理""" self.logger.info("应用程序关闭") # 停止任何正在运行的线程 self.stop_event.set() if self.running_thread and self.running_thread.is_alive(): self.logger.info("等待运行线程结束...") self.running_thread.join(timeout=5.0) self.root.destroy() def setup_logging(self): """配置日志记录""" log_dir = os.path.join(os.path.expanduser("~"), "pyMet", "logs") os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, f"elevation_visualizer_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") self.logger = logging.getLogger('ElevationVisualizer') self.logger.setLevel(logging.DEBUG) # 文件处理器 fh = logging.FileHandler(log_file) fh.setLevel(logging.DEBUG) # 控制台处理器 ch = logging.StreamHandler() ch.setLevel(logging.INFO) # 格式器 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(formatter) ch.setFormatter(formatter) # 添加处理器 self.logger.addHandler(fh) self.logger.addHandler(ch) # 设置全局异常处理器 sys.excepthook = self.handle_uncaught_exception self.logger.info("应用程序启动") self.logger.info(f"Python版本: {sys.version}") self.logger.info(f"操作系统: {platform.system()} {platform.release()}") def handle_uncaught_exception(self, exc_type, exc_value, exc_traceback): """处理未捕获的异常""" error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) self.logger.critical(f"未捕获的异常:\n{error_msg}") # 在主线程中显示错误对话框 self.root.after(0, lambda: self.show_error_dialog( f"程序遇到严重错误:\n\n{exc_type.__name__}: {exc_value}\n\n详细信息已记录到日志。" )) def start_memory_monitor(self): """启动内存监控线程""" def monitor(): while not self.stop_event.is_set(): try: mem = psutil.virtual_memory() self.root.after(0, lambda: self.mem_var.set(f"内存: {mem.percent}% 已用")) if mem.percent > 85: self.logger.warning(f"内存使用率过高: {mem.percent}%") self.root.after(0, lambda: self.status_var.set( f"警告: 内存使用率过高 ({mem.percent}%)" )) # 每5秒检查一次 self.stop_event.wait(5) except Exception as e: self.logger.error(f"内存监控错误: {str(e)}") monitor_thread = threading.Thread(target=monitor, daemon=True) monitor_thread.start() def show_error_dialog(self, error_msg): """显示错误对话框""" error_dialog = tk.Toplevel(self.root) error_dialog.title("程序错误") error_dialog.geometry("600x400") error_dialog.resizable(True, True) # 错误信息标签 label = ttk.Label(error_dialog, text="程序遇到错误:", font=("Arial", 12, "bold")) label.pack(pady=(10, 5)) # 错误详情文本框 text_frame = ttk.Frame(error_dialog) text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) scrollbar = ttk.Scrollbar(text_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) error_text = tk.Text(text_frame, wrap=tk.WORD, yscrollcommand=scrollbar.set) error_text.pack(fill=tk.BOTH, expand=True) error_text.insert(tk.END, error_msg) error_text.config(state=tk.DISABLED) scrollbar.config(command=error_text.yview) # 按钮区域 btn_frame = ttk.Frame(error_dialog) btn_frame.pack(pady=10) # 关闭按钮 ttk.Button(btn_frame, text="关闭", command=error_dialog.destroy).pack(padx=5) def create_widgets(self): """创建GUI组件""" # 创建主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 左侧控制面板 control_frame = ttk.LabelFrame(main_frame, text="控制面板", padding="10") control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) # 右侧预览面板 preview_frame = ttk.LabelFrame(main_frame, text="预览", padding="10") preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) # 行政区划选择部分 region_frame = ttk.LabelFrame(control_frame, text="行政区划选择", padding="5") region_frame.pack(fill=tk.X, pady=5) # 行政区划级别选择 level_frame = ttk.Frame(region_frame) level_frame.pack(fill=tk.X, pady=5) ttk.Label(level_frame, text="行政区划级别:").pack(side=tk.LEFT, padx=5) self.region_level = StringVar(value="省") ttk.Combobox(level_frame, textvariable=self.region_level, values=["省", "市", "县"], state="readonly", width=8).pack(side=tk.LEFT, padx=5) # 行政区划名称 name_frame = ttk.Frame(region_frame) name_frame.pack(fill=tk.X, pady=5) ttk.Label(name_frame, text="行政区划名称:").pack(side=tk.LEFT, padx=5) self.region_name = StringVar(value="山西省") self.region_entry = ttk.Entry(name_frame, textvariable=self.region_name, width=25) self.region_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) # 行政区划边界获取按钮 ttk.Button(region_frame, text="获取行政区划边界", command=self.safe_fetch_region_boundary).pack(pady=5) # 行政区划信息显示 self.region_info = tk.Text(region_frame, height=4, width=40) self.region_info.pack(fill=tk.X, pady=5) self.region_info.insert(tk.END, "行政区划信息将在此显示") self.region_info.config(state=tk.DISABLED) # 输入文件部分 file_frame = ttk.LabelFrame(control_frame, text="高程数据", padding="5") file_frame.pack(fill=tk.X, pady=5) ttk.Label(file_frame, text="高程数据文件:").pack(anchor=tk.W) self.file_entry = ttk.Entry(file_frame) self.file_entry.pack(fill=tk.X, padx=5, pady=2) ttk.Button(file_frame, text="浏览...", command=self.safe_browse_file).pack(anchor=tk.E, pady=2) # 输出设置 output_frame = ttk.LabelFrame(control_frame, text="输出设置", padding="5") output_frame.pack(fill=tk.X, pady=5) ttk.Label(output_frame, text="输出目录:").pack(anchor=tk.W) self.output_dir_entry = ttk.Entry(output_frame) self.output_dir_entry.pack(fill=tk.X, padx=5, pady=2) ttk.Button(output_frame, text="浏览...", command=self.safe_browse_output_dir).pack(anchor=tk.E, pady=2) # 选项设置 options_frame = ttk.LabelFrame(control_frame, text="选项", padding="5") options_frame.pack(fill=tk.X, pady=5) self.grid_var = tk.BooleanVar(value=True) ttk.Checkbutton(options_frame, text="显示网格", variable=self.grid_var).pack(anchor=tk.W) self.dpi_var = tk.IntVar(value=300) ttk.Label(options_frame, text="图像DPI:").pack(anchor=tk.W) ttk.Scale(options_frame, from_=100, to=1200, variable=self.dpi_var, orient=tk.HORIZONTAL, length=180).pack(fill=tk.X) self.dpi_label = ttk.Label(options_frame, text=f"当前DPI: {self.dpi_var.get()}") self.dpi_label.pack(anchor=tk.E) self.dpi_var.trace_add("write", self.update_dpi_label) # 按钮区域 button_frame = ttk.Frame(control_frame) button_frame.pack(fill=tk.X, pady=10) ttk.Button(button_frame, text="预览", command=self.safe_preview).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="生成高程图", command=self.safe_generate_images).pack(side=tk.LEFT, padx=5) # 添加保存设置按钮 ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack(side=tk.RIGHT, padx=5) self.cancel_btn = ttk.Button(button_frame, text="取消操作", command=self.cancel_operation, state=tk.DISABLED) self.cancel_btn.pack(side=tk.RIGHT, padx=5) # 预览区域 self.preview_frame = ttk.Frame(preview_frame) self.preview_frame.pack(fill=tk.BOTH, expand=True) # 创建两个预览标签 self.preview_label1 = ttk.Label(self.preview_frame, text="基础高程图预览区域") self.preview_label1.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.preview_label2 = ttk.Label(self.preview_frame, text="详细高程图预览区域") self.preview_label2.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) # 状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪") status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 内存状态 self.mem_var = tk.StringVar() self.mem_var.set("内存: --") mem_bar = ttk.Label(self.root, textvariable=self.mem_var, relief=tk.SUNKEN, anchor=tk.W) mem_bar.pack(side=tk.BOTTOM, fill=tk.X) def update_dpi_label(self, *args): """更新DPI标签""" self.dpi_label.config(text=f"当前DPI: {self.dpi_var.get()}") def safe_browse_file(self): """安全浏览高程数据文件""" try: file_path = filedialog.askopenfilename( title="选择高程数据文件", filetypes=[("TIFF文件", "*.tif *.tiff"), ("所有文件", "*.*")] ) if file_path: self.file_entry.delete(0, tk.END) self.file_entry.insert(0, file_path) except Exception as e: self.logger.error(f"浏览文件时出错: {str(e)}") self.status_var.set(f"错误: {str(e)}") def safe_browse_output_dir(self): """安全浏览输出目录""" try: dir_path = filedialog.askdirectory(title="选择输出目录") if dir_path: self.output_dir_entry.delete(0, tk.END) self.output_dir_entry.insert(0, dir_path) except Exception as e: self.logger.error(f"浏览目录时出错: {str(e)}") self.status_var.set(f"错误: {str(e)}") def safe_fetch_region_boundary(self): """安全获取行政区划边界""" if not self.validate_region_input(): return # 检查是否已有线程在运行 if self.running_thread and self.running_thread.is_alive(): messagebox.showinfo("操作进行中", "请等待当前操作完成") return self.status_var.set(f"正在获取{self.region_name.get()}边界数据...") self.cancel_btn.config(state=tk.NORMAL) self.stop_event.clear() # 在新线程中获取边界数据 self.running_thread = threading.Thread( target=self._safe_fetch_region_boundary, daemon=True ) self.running_thread.start() def _safe_fetch_region_boundary(self): """线程安全的边界获取""" region_level = self.region_level.get() region_name = self.region_name.get().strip() try: self.logger.info(f"开始获取行政区划边界: {region_level} - {region_name}") # 使用阿里云行政区划API if region_level == "省": url = f"https://geo.datav.aliyun.com/areas_v3/bound/geojson?code=100000_full" elif region_level == "市": # 尝试获取省级编码 province_url = f"https://geo.datav.aliyun.com/areas_v3/bound/geojson?code=100000_full" province_resp = requests.get(province_url, timeout=15) province_resp.raise_for_status() province_data = province_resp.json() # 查找匹配的省份 found = False for feature in province_data['features']: if feature['properties']['name'] == region_name: adcode = feature['properties']['adcode'] url = f"https://geo.datav.aliyun.com/areas_v3/bound/geojson?code={adcode}_full" found = True break if not found: raise ValueError(f"未找到匹配的省份: {region_name}") else: # 县 # 需要先获取县级编码,这里简化处理 url = f"https://geo.datav.aliyun.com/areas_v3/bound/geojson?code={region_name}" self.logger.info(f"请求行政区划API: {url}") response = requests.get(url, timeout=15) response.raise_for_status() # 解析GeoJSON数据 geojson = response.json() self.region_gdf = gpd.GeoDataFrame.from_features(geojson['features']) # 计算边界范围 bounds = self.region_gdf.total_bounds xmin, ymin, xmax, ymax = bounds # 更新显示信息 self.root.after(0, lambda: self.update_region_info( region_name, region_level, xmin, xmax, ymin, ymax )) self.root.after(0, lambda: self.status_var.set(f"成功获取{region_name}边界数据")) self.logger.info(f"成功获取{region_name}边界数据") except requests.exceptions.Timeout: error_msg = f"获取{region_name}边界数据超时" self.root.after(0, lambda: self.status_var.set(error_msg)) self.root.after(0, lambda: messagebox.showerror("超时错误", error_msg)) self.logger.error(error_msg) except requests.exceptions.RequestException as e: error_msg = f"网络错误: {str(e)}" self.root.after(0, lambda: self.status_var.set(error_msg)) self.root.after(0, lambda: messagebox.showerror("网络错误", error_msg)) self.logger.error(error_msg) except ValueError as e: self.root.after(0, lambda: self.show_error_dialog(str(e))) self.logger.error(str(e)) except Exception as e: self.root.after(0, lambda: self.show_error_dialog(f"获取边界失败: {str(e)}")) self.logger.error(f"获取边界失败: {str(e)}", exc_info=True) finally: self.root.after(0, lambda: self.cancel_btn.config(state=tk.DISABLED)) self.running_thread = None def update_region_info(self, region_name, region_level, xmin, xmax, ymin, ymax): """更新行政区划信息显示""" self.region_info.config(state=tk.NORMAL) self.region_info.delete(1.0, tk.END) self.region_info.insert(tk.END, f"行政区划: {region_name} ({region_level})\n" f"边界范围: \n" f"经度: {xmin:.4f} - {xmax:.4f}\n" f"纬度: {ymin:.4f} - {ymax:.4f}\n" f"包含{len(self.region_gdf)}个多边形" ) self.region_info.config(state=tk.DISABLED) def validate_region_input(self): """验证行政区划输入""" region_name = self.region_name.get().strip() if not region_name: messagebox.showerror("输入错误", "请输入行政区划名称!") return False return True def validate_inputs(self): """验证所有输入参数""" # 验证行政区划输入 if not self.validate_region_input(): return False # 验证文件路径 file_path = self.file_entry.get().strip() if not file_path: messagebox.showerror("输入错误", "请选择高程数据文件!") return False if not os.path.isfile(file_path): messagebox.showerror("文件错误", f"高程数据文件不存在: {file_path}") return False # 检查是否已获取边界数据 if self.region_gdf is None: messagebox.showerror("数据错误", "请先获取行政区划边界数据!") return False return True def safe_preview(self): """安全预览地图""" if not self.validate_inputs(): return # 检查是否已有线程在运行 if self.running_thread and self.running_thread.is_alive(): messagebox.showinfo("操作进行中", "请等待当前操作完成") return self.status_var.set("正在生成预览...") self.cancel_btn.config(state=tk.NORMAL) self.stop_event.clear() # 在新线程中运行预览 self.running_thread = threading.Thread( target=self._safe_generate_preview, daemon=True ) self.running_thread.start() def _safe_generate_preview(self): """线程安全的预览生成""" try: region_name = self.region_name.get() region_level = self.region_level.get() elevation_file = self.file_entry.get() # 获取边界范围 bounds = self.region_gdf.total_bounds xmin, ymin, xmax, ymax = bounds # 扩展边界以确保完整覆盖 padding = 0.2 xmin -= padding xmax += padding ymin -= padding ymax += padding # 读取高程数据 self.logger.info("读取高程数据...") self.root.after(0, lambda: self.status_var.set("正在读取高程数据...")) # 使用分块读取避免大文件内存溢出 with rxr.open_rasterio(elevation_file, chunks=True) as src: src = src.isel(band=0) # 裁剪数据 self.logger.info("裁剪数据...") self.root.after(0, lambda: self.status_var.set("正在裁剪数据...")) lon = src.coords["x"] lat = src.coords["y"] da = src.loc[dict( x=lon[(lon >= xmin) & (lon <= xmax)], y=lat[(lat >= ymin) & (lat <= ymax)] )] # 重新提取经纬度 lon = da.coords["x"] lat = da.coords["y"] Lon, Lat = np.meshgrid(lon, lat) # 创建基础高程图 self.logger.info("创建基础高程图预览...") self.root.after(0, lambda: self.status_var.set("正在生成基础高程图...")) plt.rcParams['font.family'] = 'SimHei' plt.rcParams['axes.unicode_minus'] = False mapcrs = ccrs.PlateCarree() # 基础高程图 fig1 = plt.figure(figsize=(6, 6)) ax1 = plt.axes(projection=mapcrs) # 添加行政区划边界 ax1.add_geometries( self.region_gdf['geometry'], crs=mapcrs, facecolor='none', edgecolor='red', linewidth=1.5 ) # 绘制高程图 pm1 = ax1.pcolormesh( Lon, Lat, da, cmap='terrain', transform=mapcrs ) # 添加标题 ax1.set_title(f"{region_name}基础高程图", fontsize=12) # 保存预览图像 preview_dir = os.path.join(os.path.expanduser("~"), "pyMet", "previews") os.makedirs(preview_dir, exist_ok=True) preview_path1 = os.path.join(preview_dir, f"preview1_{datetime.now().strftime('%H%M%S')}.png") plt.savefig(preview_path1, dpi=150, bbox_inches='tight') plt.close(fig1) # 释放内存 del fig1, ax1, pm1 gc.collect() # 创建详细高程图 self.logger.info("创建详细高程图预览...") self.root.after(0, lambda: self.status_var.set("正在生成详细高程图...")) fig2 = plt.figure(figsize=(6, 6)) ax2 = plt.axes(projection=mapcrs) # 添加行政区划边界 ax2.add_geometries( self.region_gdf['geometry'], crs=mapcrs, facecolor='none', edgecolor='black', linewidth=1.2 ) # 设置高程色阶 levels = np.arange(50, 2501, 100) colorMap = plt.colormaps['terrain'] colorNorm = BoundaryNorm(levels, ncolors=colorMap.N, extend='both') # 绘制高程图 pm2 = ax2.pcolormesh( Lon, Lat, da, cmap=colorMap, norm=colorNorm, transform=mapcrs ) # 添加颜色条 cb2 = fig2.colorbar( pm2, ax=ax2, shrink=0.7, location="right", pad=0.05 ) cb2.set_label('高程 (米)', fontsize=9) # 添加标题 title = f"{region_name}{region_level}高程地形图" ax2.set_title(title, fontsize=12, pad=10) # 在图上标注行政区划名称 centroid = self.region_gdf.geometry.centroid.iloc[0] ax2.text( centroid.x, centroid.y, region_name, fontsize=14, fontweight='bold', color='darkred', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.7, boxstyle='round,pad=0.3'), transform=mapcrs ) # 保存预览图像 preview_path2 = os.path.join(preview_dir, f"preview2_{datetime.now().strftime('%H%M%S')}.png") plt.savefig(preview_path2, dpi=150, bbox_inches='tight') plt.close(fig2) # 释放内存 del fig2, ax2, pm2, cb2 gc.collect() # 在GUI中显示预览 self.root.after(0, lambda: self.update_preview( preview_path1, preview_path2, "预览生成完成" )) except Exception as e: self.root.after(0, lambda: self.show_error_dialog(f"生成预览失败: {str(e)}")) self.logger.error(f"生成预览失败: {str(e)}", exc_info=True) finally: self.root.after(0, lambda: self.cancel_btn.config(state=tk.DISABLED)) self.running_thread = None def update_preview(self, path1, path2, status): """更新预览图像""" try: # 加载第一张预览图 img1 = Image.open(path1) img1.thumbnail((450, 450), Image.LANCZOS) photo1 = ImageTk.PhotoImage(img1) # 加载第二张预览图 img2 = Image.open(path2) img2.thumbnail((450, 450), Image.LANCZOS) photo2 = ImageTk.PhotoImage(img2) # 更新标签 self.preview_label1.config(image=photo1) self.preview_label1.image = photo1 self.preview_label2.config(image=photo2) self.preview_label2.image = photo2 self.status_var.set(status) self.logger.info(status) except Exception as e: self.logger.error(f"更新预览时出错: {str(e)}") self.status_var.set(f"更新预览失败: {str(e)}") def safe_generate_images(self): """安全生成两张高质量高程图""" if not self.validate_inputs(): return # 检查是否已有线程在运行 if self.running_thread and self.running_thread.is_alive(): messagebox.showinfo("操作进行中", "请等待当前操作完成") return self.status_var.set("正在生成高质量高程图...") self.cancel_btn.config(state=tk.NORMAL) self.stop_event.clear() # 在新线程中运行图像生成 self.running_thread = threading.Thread( target=self._safe_generate_images, daemon=True ) self.running_thread.start() def _safe_generate_images(self): """线程安全的图像生成""" try: region_name = self.region_name.get() region_level = self.region_level.get() elevation_file = self.file_entry.get() output_dir = self.output_dir_entry.get() show_grid = self.grid_var.get() dpi = self.dpi_var.get() # 确保输出目录存在 os.makedirs(output_dir, exist_ok=True) # 生成文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_output_path = os.path.join(output_dir, f"{region_name}_{region_level}_基础高程图_{timestamp}.png") detail_output_path = os.path.join(output_dir, f"{region_name}_{region_level}_详细高程图_{timestamp}.png") # 获取边界范围 bounds = self.region_gdf.total_bounds xmin, ymin, xmax, ymax = bounds # 扩展边界以确保完整覆盖 padding = 0.2 xmin -= padding xmax += padding ymin -= padding ymax += padding # 读取高程数据 self.logger.info("读取高程数据...") self.root.after(0, lambda: self.status_var.set("正在读取高程数据...")) # 使用分块读取避免大文件内存溢出 with rxr.open_rasterio(elevation_file, chunks=True) as src: src = src.isel(band=0) # 裁剪数据 self.logger.info("裁剪数据...") self.root.after(0, lambda: self.status_var.set("正在裁剪数据...")) lon = src.coords["x"] lat = src.coords["y"] da = src.loc[dict( x=lon[(lon >= xmin) & (lon <= xmax)], y=lat[(lat >= ymin) & (lat <= ymax)] )] # 重新提取经纬度 lon = da.coords["x"] lat = da.coords["y"] Lon, Lat = np.meshgrid(lon, lat) # 创建基础高程图 self.logger.info("创建基础高程图...") self.root.after(0, lambda: self.status_var.set("正在生成基础高程图...")) plt.rcParams['font.family'] = 'SimHei' plt.rcParams['axes.unicode_minus'] = False mapcrs = ccrs.PlateCarree() fig1 = plt.figure(figsize=(10, 10)) ax1 = plt.axes(projection=mapcrs) # 添加行政区划边界 ax1.add_geometries( self.region_gdf['geometry'], crs=mapcrs, facecolor='none', edgecolor='red', linewidth=2.0 ) # 绘制高程图 pm1 = ax1.pcolormesh( Lon, Lat, da, cmap='terrain', transform=mapcrs ) # 添加标题 ax1.set_title(f"{region_name}{region_level}基础高程图", fontsize=16, pad=15) # 在图上标注行政区划名称 centroid = self.region_gdf.geometry.centroid.iloc[0] ax1.text( centroid.x, centroid.y, region_name, fontsize=18, fontweight='bold', color='darkred', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'), transform=mapcrs ) # 保存图像 plt.savefig(base_output_path, dpi=dpi, bbox_inches='tight') plt.close(fig1) # 释放内存 del fig1, ax1, pm1 gc.collect() # 创建详细高程图 self.logger.info("创建详细高程图...") self.root.after(0, lambda: self.status_var.set("正在生成详细高程图...")) fig2 = plt.figure(figsize=(10, 10)) ax2 = plt.axes(projection=mapcrs) # 添加行政区划边界 ax2.add_geometries( self.region_gdf['geometry'], crs=mapcrs, facecolor='none', edgecolor='black', linewidth=1.5 ) # 设置高程色阶 levels = np.arange(50, 2501, 100) colorMap = plt.colormaps['terrain'] colorNorm = BoundaryNorm(levels, ncolors=colorMap.N, extend='both') # 绘制高程图 pm2 = ax2.pcolormesh( Lon, Lat, da, cmap=colorMap, norm=colorNorm, transform=mapcrs ) # 添加颜色条 cb2 = fig2.colorbar( pm2, ax=ax2, shrink=0.8, location="right", pad=0.05 ) cb2.set_label('高程 (米)', fontsize=12) # 添加标题 title = f"{region_name}{region_level}高程地形图" ax2.set_title(title, fontsize=16, pad=15) # 在图上标注行政区划名称 ax2.text( centroid.x, centroid.y, region_name, fontsize=22, fontweight='bold', color='darkred', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.8'), transform=mapcrs ) # 添加网格线 if show_grid: gl = ax2.gridlines( crs=mapcrs, draw_labels=True, linewidth=0.5, linestyle='--', color='black', alpha=0.7 ) gl.top_labels = False gl.right_labels = False # 保存图像 plt.savefig(detail_output_path, dpi=dpi, bbox_inches='tight') plt.close(fig2) # 释放内存 del fig2, ax2, pm2, cb2 gc.collect() # 更新状态和预览 status = f"图像已保存到: {output_dir}" self.root.after(0, lambda: self.update_preview( base_output_path, detail_output_path, status )) self.root.after(0, lambda: messagebox.showinfo("生成成功", f"两张高程图已生成:\n" f"1. {base_output_path}\n" f"2. {detail_output_path}")) except Exception as e: self.root.after(0, lambda: self.show_error_dialog(f"生成图像失败: {str(e)}")) self.logger.error(f"生成图像失败: {str(e)}", exc_info=True) finally: self.root.after(0, lambda 还缺着点吧
最新发布
06-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值