fix(step14): 批量渲染文件名唯一性 + Colorbar 样式 + 2σ拉伸
This commit is contained in:
@ -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)
|
||||||
|
|||||||
@ -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 array:NaN 区域自动不显示
|
# 使用 masked array:NaN 区域自动不显示
|
||||||
@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user