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)
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)

View File

@ -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 arrayNaN 区域自动不显示
@ -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}")