fix(step14): 批量渲染文件名唯一性 + Colorbar 样式 + 2σ拉伸

This commit is contained in:
DXC
2026-06-11 10:29:32 +08:00
parent aa539db9bd
commit 184f5fe9f4
2 changed files with 156 additions and 30 deletions

View File

@ -122,9 +122,10 @@ class Step14GeoTIFFBatchThread(QThread):
n = len(self.tif_paths) n = len(self.tif_paths)
for i, tif_path in enumerate(self.tif_paths): for i, tif_path in enumerate(self.tif_paths):
self.progress.emit(i + 1, n) self.progress.emit(i + 1, n)
tif_name = Path(tif_path).stem tif_stem = Path(tif_path).stem
output_png = str(Path(self.output_dir) / f"{tif_name}_map.png") chinese_name = mapper._get_chinese_title(tif_stem)
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_name}", "info") output_png = str(Path(self.output_dir) / f"{chinese_name}_专题图.png")
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_stem}", "info")
try: try:
mapper.visualize_raster( mapper.visualize_raster(
raster_tif_path=tif_path, raster_tif_path=tif_path,
@ -132,7 +133,6 @@ class Step14GeoTIFFBatchThread(QThread):
boundary_shp_path=self.boundary_shp_path, boundary_shp_path=self.boundary_shp_path,
nodata_value=-9999.0, nodata_value=-9999.0,
figsize=(14, 10), figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9, alpha=0.9,
) )
except Exception as vis_err: except Exception as vis_err:
@ -762,8 +762,9 @@ class Step14Panel(QWidget):
if not out_dir: if not out_dir:
out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") out_dir = os.path.join(self._get_default_work_dir(), "14_visualization")
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
tif_name = Path(geotiff_path).stem tif_stem = Path(geotiff_path).stem
output_png = os.path.join(out_dir, f"{tif_name}_rendered.png") chinese_name = mapper._get_chinese_title(tif_stem)
output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png")
self.run_button.setEnabled(False) self.run_button.setEnabled(False)
try: try:
@ -775,7 +776,6 @@ class Step14Panel(QWidget):
boundary_shp_path=boundary_shp_path if boundary_shp_path else None, boundary_shp_path=boundary_shp_path if boundary_shp_path else None,
nodata_value=-9999.0, nodata_value=-9999.0,
figsize=(14, 10), figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9, alpha=0.9,
) )
self.run_button.setEnabled(True) self.run_button.setEnabled(True)

View File

@ -7,7 +7,7 @@ from typing import Optional, Tuple
from pyproj import CRS, Transformer from pyproj import CRS, Transformer
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.patches as patches import matplotlib.patches as patches
from matplotlib.ticker import FuncFormatter from matplotlib.ticker import FuncFormatter, MaxNLocator
from matplotlib_scalebar.scalebar import ScaleBar from matplotlib_scalebar.scalebar import ScaleBar
from scipy.interpolate import griddata from scipy.interpolate import griddata
from scipy import ndimage 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: class ContentMapper:
def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'): def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'):
""" """
@ -97,6 +163,63 @@ class ContentMapper:
print(f"坐标转换设置: {input_crs} -> {output_crs}") 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): def _extract_param_name(self, csv_file):
""" """
从CSV文件名或内容中提取参数名称 从CSV文件名或内容中提取参数名称
@ -2080,12 +2203,15 @@ class ContentMapper:
str str
输出图片路径 输出图片路径
""" """
# ── 输出路径自动派生 ────────────────────────────────────────── # ── 始终从路径提取 stem供后续中文标题和文件派生使用──────────
if output_file is None:
stem = Path(raster_tif_path).stem stem = Path(raster_tif_path).stem
# ── 输出路径自动派生(中文文件名)──────────────────────────────
if output_file is None:
chinese_title = self._get_chinese_title(stem)
out_dir = Path(raster_tif_path).parent / 'visualization' out_dir = Path(raster_tif_path).parent / 'visualization'
out_dir.mkdir(parents=True, exist_ok=True) 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────────────────── # ── 读取 GeoTIFF优先 rasterio备选 GDAL──────────────────
tif_path = Path(raster_tif_path) tif_path = Path(raster_tif_path)
@ -2214,6 +2340,10 @@ class ContentMapper:
param_name = self._extract_param_name(str(tif_path)) param_name = self._extract_param_name(str(tif_path))
cmap = self._get_colormap(param_name) cmap = self._get_colormap(param_name)
# ── 中文标题(文件名汉化 + 绘图标题)──────────────────────────
# 用户显式传入 title 时直接使用;否则用中文映射
chinese_title = self._get_chinese_title(stem) if not title else title
# ── 计算空间范围extent────────────────────────────────────── # ── 计算空间范围extent──────────────────────────────────────
# 优先使用 rasterio 原生 bounds保证坐标轴为真实 UTM 米 # 优先使用 rasterio 原生 bounds保证坐标轴为真实 UTM 米
# GDAL 回退使用 GeoTransform 计算 # GDAL 回退使用 GeoTransform 计算
@ -2251,23 +2381,24 @@ class ContentMapper:
safe_figsize = (safe_w, safe_h) safe_figsize = (safe_w, safe_h)
fig, ax = plt.subplots(figsize=safe_figsize) fig, ax = plt.subplots(figsize=safe_figsize)
# 计算有效值统计(使用 nanpercentile 精准锁定水体内部,排除陆地 NoData 干扰) # 计算有效值统计(2σ 标准差拉伸,排除长尾异常值干扰)
valid = array[~np.isnan(array)] valid = array[~np.isnan(array)]
if valid.size == 0: if valid.size == 0:
raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData") raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData")
vmin = float(np.nanpercentile(array, 2)) mean_val = float(np.nanmean(array))
vmax = float(np.nanpercentile(array, 98)) std_val = float(np.nanstd(array))
data_range = vmax - vmin 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: if (vmax - vmin) < 1e-9:
center = float(np.nanmean(array)) center = mean_val
exp = max(abs(center) * 0.01, 1e-9) exp = max(abs(center) * 0.01, 1e-9)
vmin = center - exp vmin = center - exp
vmax = center + exp vmax = center + exp
print(f"[visualize_raster] 分位数拉伸: P2={vmin:.4f}, P98={vmax:.4f}" print(f"[visualize_raster] 2σ 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f}"
f"有效像元: {valid.size}/{array.size}") f"mean={mean_val:.4f}, std={std_val:.4f}有效像元: {valid.size}/{array.size}")
# ── 栅格绘图 ───────────────────────────────────────────────── # ── 栅格绘图 ─────────────────────────────────────────────────
# 使用 masked arrayNaN 区域自动不显示 # 使用 masked arrayNaN 区域自动不显示
@ -2321,21 +2452,16 @@ class ContentMapper:
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray') ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray')
ax.set_axisbelow(True) ax.set_axisbelow(True)
# ── 标题 ───────────────────────────────────────────────────── # ── 标题(中文)──────────────────────────────────────────────
if title: ax.set_title(chinese_title, fontsize=13, fontweight='bold', pad=10)
ax.set_title(title, fontsize=13, fontweight='bold', pad=10)
elif param_name:
ax.set_title(param_name, fontsize=13, fontweight='bold', pad=10)
# ── 颜色条 ─────────────────────────────────────────────────── # ── 颜色条工业级样式extend 三角 + MaxNLocator 刻度防重叠)─────────
if show_colorbar and im is not None: if show_colorbar and im is not None:
try: 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) cbar.set_label('Index Value', fontsize=10)
if data_range > 1e-9: cbar.locator = MaxNLocator(nbins=6)
ticks = np.linspace(vmin, vmax, 6) cbar.update_ticks()
cbar.set_ticks(ticks)
cbar.set_ticklabels([f'{t:.3f}' for t in ticks])
print("[visualize_raster] 颜色条添加成功") print("[visualize_raster] 颜色条添加成功")
except Exception as e: except Exception as e:
print(f"[visualize_raster] 颜色条添加失败: {e}") print(f"[visualize_raster] 颜色条添加失败: {e}")