fix(step14): 批量渲染文件名唯一性 + Colorbar 样式 + 2σ拉伸
This commit is contained in:
@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user