From 184f5fe9f4bc1bb4f254862350667e7fc7f44c1c Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 11 Jun 2026 10:29:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(step14):=20=E6=89=B9=E9=87=8F=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E6=96=87=E4=BB=B6=E5=90=8D=E5=94=AF=E4=B8=80=E6=80=A7?= =?UTF-8?q?=20+=20Colorbar=20=E6=A0=B7=E5=BC=8F=20+=202=CF=83=E6=8B=89?= =?UTF-8?q?=E4=BC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/panels/step14_panel.py | 14 +-- src/postprocessing/map.py | 172 ++++++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 30 deletions(-) diff --git a/src/gui/panels/step14_panel.py b/src/gui/panels/step14_panel.py index 025d0a0..346b654 100644 --- a/src/gui/panels/step14_panel.py +++ b/src/gui/panels/step14_panel.py @@ -122,9 +122,10 @@ class Step14GeoTIFFBatchThread(QThread): n = len(self.tif_paths) for i, tif_path in enumerate(self.tif_paths): self.progress.emit(i + 1, n) - tif_name = Path(tif_path).stem - output_png = str(Path(self.output_dir) / f"{tif_name}_map.png") - self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_name}", "info") + tif_stem = Path(tif_path).stem + chinese_name = mapper._get_chinese_title(tif_stem) + output_png = str(Path(self.output_dir) / f"{chinese_name}_专题图.png") + self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_stem}", "info") try: mapper.visualize_raster( raster_tif_path=tif_path, @@ -132,7 +133,6 @@ class Step14GeoTIFFBatchThread(QThread): boundary_shp_path=self.boundary_shp_path, nodata_value=-9999.0, figsize=(14, 10), - title=f"水色指数专题图 - {tif_name}", alpha=0.9, ) except Exception as vis_err: @@ -762,8 +762,9 @@ class Step14Panel(QWidget): if not out_dir: out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") os.makedirs(out_dir, exist_ok=True) - tif_name = Path(geotiff_path).stem - output_png = os.path.join(out_dir, f"{tif_name}_rendered.png") + tif_stem = Path(geotiff_path).stem + chinese_name = mapper._get_chinese_title(tif_stem) + output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png") self.run_button.setEnabled(False) try: @@ -775,7 +776,6 @@ class Step14Panel(QWidget): boundary_shp_path=boundary_shp_path if boundary_shp_path else None, nodata_value=-9999.0, figsize=(14, 10), - title=f"水色指数专题图 - {tif_name}", alpha=0.9, ) self.run_button.setEnabled(True) diff --git a/src/postprocessing/map.py b/src/postprocessing/map.py index 422f688..b542c8c 100644 --- a/src/postprocessing/map.py +++ b/src/postprocessing/map.py @@ -7,7 +7,7 @@ from typing import Optional, Tuple from pyproj import CRS, Transformer import matplotlib.pyplot as plt import matplotlib.patches as patches -from matplotlib.ticker import FuncFormatter +from matplotlib.ticker import FuncFormatter, MaxNLocator from matplotlib_scalebar.scalebar import ScaleBar from scipy.interpolate import griddata from scipy import ndimage @@ -63,6 +63,72 @@ PARAMS_CMAP = { } +# ── 水色指数英文名 → 中文标题映射(精确整词匹配)──────────────────── +# 优先于动态拼接;当文件名恰好命中完整 key 时使用 +INDEX_TITLE_MAP = { + # Chl_a 叶绿素a + "Chl_Conc_NDCI": "叶绿素a浓度估算_NDCI模型", + "Chl_MM12NDCI": "叶绿素a相对指数_Matthews12模型", + "Chl_Conc_Gao": "叶绿素a浓度估算_Gao模型", + "Chl_Conc_QAA": "叶绿素a浓度估算_QAA模型", + # BGA 蓝藻/藻蓝蛋白 + "BGA_Go04MCI": "蓝藻相对指数_Gower04模型", + "BGA_PC_Conc_Mishra": "藻蓝蛋白浓度估算_Mishra模型", + "BGA_Conc": "蓝藻浓度估算", + "BGA_Am09KBBI": "蓝藻相对指数_Am09模型", + # Turb 浊度 + "Turb_Dox02NIRoverRed": "水体浊度指数_Doxaran02模型", + "Turbidity": "水体浊度", + "Turb_Conc": "浊度估算", + # TSM 悬浮物 + "TSM_Conc_Bowling": "总悬浮物浓度估算_Bowling模型", + "TSM_Conc": "总悬浮物浓度估算", + # CDOM 有色溶解有机物 + "CDOM_Conc": "有色溶解有机物浓度估算", + # WI 水色指数综合 + "WaterIndex": "水色指数", + "NDCI": "归一化叶绿素差值指数_NDCI", + "MCI": "最大Chlorophyll指数_MCI", +} + +# ── 关键词 → 中文词根映射(用于动态拼接)──────────────────────────── +# 顺序即优先级:长词优先,短词兜底 +PART_NAME_MAP = [ + # 1. 指数/浓度类(最高优先级,描述输出类型) + ("Conc", "浓度估算"), + ("_Conc", "浓度估算"), + ("Concentration", "浓度"), + ("Index", "指数"), + ("_Index", "指数"), + # 2. 模型/方法标识(次高优先级) + ("NDCI", "NDCI模型"), + ("MCI", "MCI模型"), + ("FLH", "FLH荧光基线"), + ("QAA", "QAA模型"), + ("Go04", "Gower04"), + ("MM12", "Matthews12"), + ("Gao", "Gao模型"), + ("Dox02", "Doxaran02"), + ("Bowling", "Bowling模型"), + ("Mishra", "Mishra模型"), + ("Am09", "Am09"), + ("KBBI", "KBBI"), + ("PC", "藻蓝蛋白"), + # 3. 参数大类(核心水质指标) + ("BGA", "蓝藻相对指数"), + ("Chl", "叶绿素a"), + ("Chl_a", "叶绿素a"), + ("Turb", "浊度"), + ("TSM", "总悬浮物"), + ("CDOM", "有色溶解有机物"), + ("DO", "溶解氧"), + ("pH", "pH值"), + ("NH3", "氨氮"), + ("NO3", "硝态氮"), + ("TDS", "溶解性总固体"), +] + + class ContentMapper: def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'): """ @@ -97,6 +163,63 @@ class ContentMapper: print(f"坐标转换设置: {input_crs} -> {output_crs}") + # ── 内部工具 ───────────────────────────────────────────────────── + @staticmethod + def _get_chinese_title(stem: str) -> str: + """ + 根据 GeoTIFF 文件名 stem 返回中文图表标题(绝对唯一)。 + + 匹配策略: + 1. 精确整词命中 INDEX_TITLE_MAP + 2. 中文分类前缀 + 原始模型后缀(绝对唯一保证) + 3. 未匹配任何关键词 → 返回原英文 stem + + Parameters + ---------- + stem : str + GeoTIFF 文件名(不含路径和扩展名) + + Returns + ------- + str + 中文标题;若未匹配则返回英文 stem + """ + # 策略1:精确整词匹配(优先级最高) + if stem in INDEX_TITLE_MAP: + return INDEX_TITLE_MAP[stem] + + # 策略2:中文分类前缀 + 原始模型后缀(确保绝对唯一) + category = "" + suffix = "" + + if stem.startswith("BGA_PC_Conc"): + category = "藻蓝蛋白浓度估算" + suffix = stem.replace("BGA_PC_Conc_", "") + elif stem.startswith("BGA"): + category = "蓝藻相对指数" + suffix = stem.replace("BGA_", "") + elif stem.startswith("Chl_Conc"): + category = "叶绿素a浓度估算" + suffix = stem.replace("Chl_Conc_", "") + elif stem.startswith("Chl"): + category = "叶绿素a相对指数" + suffix = stem.replace("Chl_", "") + elif stem.startswith("Turb_Conc"): + category = "浊度浓度估算" + suffix = stem.replace("Turb_Conc_", "") + elif stem.startswith("Turb"): + category = "浊度相对指数" + suffix = stem.replace("Turb_", "") + elif stem.startswith("TSM_Conc"): + category = "总悬浮物浓度估算" + suffix = stem.replace("TSM_Conc_", "") + else: + # 兜底机制:未知分类直接使用原名 + category = "水质参数" + suffix = stem + + return f"{category}_{suffix}" + def _extract_param_name(self, csv_file): """ 从CSV文件名或内容中提取参数名称 @@ -2080,12 +2203,15 @@ class ContentMapper: str 输出图片路径 """ - # ── 输出路径自动派生 ────────────────────────────────────────── + # ── 始终从路径提取 stem(供后续中文标题和文件派生使用)────────── + stem = Path(raster_tif_path).stem + + # ── 输出路径自动派生(中文文件名)────────────────────────────── if output_file is None: - stem = Path(raster_tif_path).stem + chinese_title = self._get_chinese_title(stem) out_dir = Path(raster_tif_path).parent / 'visualization' out_dir.mkdir(parents=True, exist_ok=True) - output_file = str(out_dir / f"{stem}_map.png") + output_file = str(out_dir / f"{chinese_title}_专题图.png") # ── 读取 GeoTIFF(优先 rasterio,备选 GDAL)────────────────── tif_path = Path(raster_tif_path) @@ -2214,6 +2340,10 @@ class ContentMapper: param_name = self._extract_param_name(str(tif_path)) cmap = self._get_colormap(param_name) + # ── 中文标题(文件名汉化 + 绘图标题)────────────────────────── + # 用户显式传入 title 时直接使用;否则用中文映射 + chinese_title = self._get_chinese_title(stem) if not title else title + # ── 计算空间范围(extent)────────────────────────────────────── # 优先使用 rasterio 原生 bounds,保证坐标轴为真实 UTM 米 # GDAL 回退使用 GeoTransform 计算 @@ -2251,23 +2381,24 @@ class ContentMapper: safe_figsize = (safe_w, safe_h) fig, ax = plt.subplots(figsize=safe_figsize) - # 计算有效值统计(使用 nanpercentile 精准锁定水体内部,排除陆地 NoData 干扰) + # 计算有效值统计(2σ 标准差拉伸,排除长尾异常值干扰) valid = array[~np.isnan(array)] if valid.size == 0: raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData)") - vmin = float(np.nanpercentile(array, 2)) - vmax = float(np.nanpercentile(array, 98)) - data_range = vmax - vmin + mean_val = float(np.nanmean(array)) + std_val = float(np.nanstd(array)) + vmin = max(float(np.nanmin(array)), mean_val - 2 * std_val) + vmax = min(float(np.nanmax(array)), mean_val + 2 * std_val) - if data_range < 1e-9: - center = float(np.nanmean(array)) + if (vmax - vmin) < 1e-9: + center = mean_val exp = max(abs(center) * 0.01, 1e-9) vmin = center - exp vmax = center + exp - print(f"[visualize_raster] 分位数拉伸: P2={vmin:.4f}, P98={vmax:.4f}," - f"有效像元: {valid.size}/{array.size}") + print(f"[visualize_raster] 2σ 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f}," + f"mean={mean_val:.4f}, std={std_val:.4f},有效像元: {valid.size}/{array.size}") # ── 栅格绘图 ───────────────────────────────────────────────── # 使用 masked array:NaN 区域自动不显示 @@ -2321,21 +2452,16 @@ class ContentMapper: ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray') ax.set_axisbelow(True) - # ── 标题 ───────────────────────────────────────────────────── - if title: - ax.set_title(title, fontsize=13, fontweight='bold', pad=10) - elif param_name: - ax.set_title(param_name, fontsize=13, fontweight='bold', pad=10) + # ── 标题(中文)────────────────────────────────────────────── + ax.set_title(chinese_title, fontsize=13, fontweight='bold', pad=10) - # ── 颜色条 ─────────────────────────────────────────────────── + # ── 颜色条(工业级样式:extend 三角 + MaxNLocator 刻度防重叠)───────── if show_colorbar and im is not None: try: - cbar = plt.colorbar(im, ax=ax, shrink=0.55, aspect=35, pad=0.02) + cbar = fig.colorbar(im, ax=ax, shrink=0.55, aspect=35, pad=0.02, extend='both') cbar.set_label('Index Value', fontsize=10) - if data_range > 1e-9: - ticks = np.linspace(vmin, vmax, 6) - cbar.set_ticks(ticks) - cbar.set_ticklabels([f'{t:.3f}' for t in ticks]) + cbar.locator = MaxNLocator(nbins=6) + cbar.update_ticks() print("[visualize_raster] 颜色条添加成功") except Exception as e: print(f"[visualize_raster] 颜色条添加失败: {e}")