Files
WQ_GUI/src/new/services/step10_service.py
DXC 6a962f5e8f feat(new-arch):主窗口全功能增强(图标系统 + 全链路参数同步 + 服务输出统一解析 + Step12 分类浏览)
1. main_view.py:图标系统 + 全链路参数自动传导
   - 新增 _res() 解析项目根的相对路径,PyInstaller 打包后兼容 sys._MEIPASS。
   - 新增 QListWidgetItem / QMessageBox 导入,左侧导航列表支持右键菜单 + 错误弹窗。
   - ROUTES 12 条全部新增 icon 字段("1.png" 等),侧边栏显示业务图标。
   - 新增 step_outputs 缓存机制:每个 step 完成后把 output_path 写入 self.step_outputs。
   - 新增 _sync_dependencies() 同步函数 + _safe_set_config() 包装器,
     按依赖图把上游产物推给下游 view:
       step1 → step6 water_mask_path
       step3 → step4 / step6 / step10 deglint_img_path / bsq_path
       step4 → step9 sampling_csv_path
       step5 → step6 csv_path
       step6 → step7 / step8 training_csv_path
       step8 → step9 models_dir(父目录)
       step9 → step11 prediction_csv_dir / prediction_csv_path(双推)
       step10 → step11 geotiff_dir / geotiff_path(双推)

2. services/step1-13:统一输出解析器集成
   - 新增 src/new/services/_output_resolver.py,提供 resolve_output_dir /
     copy_to_user_path / get_user_output_path / is_user_specified 四个共享工具。
   - 每个 service 把原有的私有 _resolve_xxx_dir 改为调用 resolve_output_dir,
     强制执行"用户优先"规则(用户指定 output_path 时用其父目录,否则用 work_dir/<subdir>)。
   - 用户指定文件名 vs 底层硬编码文件名的"事后劫持"通过 copy_to_user_path 完成
     (覆盖 step2、step4、step7、step8 等底层 step 不接受 output_path 关键字的步骤)。

3. views/step12_view.py:恢复 ImageCategoryTree + ImageViewerWidget 高级组件
   - 删掉精简版占位 Label,挂回旧版的 ImageCategoryTree(按"模型评估/光谱分析/
     统计图表/处理结果/含量分布图"五类自动归类工作目录下的图像文件)。
   - 挂回 ImageViewerWidget(滚轮缩放 0.1x-5x + 50ms 防抖 + FastTransformation/
     SmoothTransformation 智能切换 + Ctrl+Wheel + 工具栏)。
   - 扫描按钮接通 image_tree.scan_directory(),选中节点即时加载到 image_viewer。
   - 按钮样式切换为 ModernStylesheet(success/primary)统一视觉。
2026-06-17 13:28:58 +08:00

217 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Step10 后端计算服务(水色指数反演)
====================================
纯计算函数——绝对不引用 PyQt、绝对不引用 main_view、绝对不读写全局变量。它只
1. 从 ``config`` 字典读取参数;
2. 调用 ``WaterIndexProcessor.run_inversion`` 用 ``waterindex.csv`` 中的
公式直接处理去耀斑 BSQ 影像,输出各水质参数指数的 GeoTIFF
3. 返回结果字典 ``{status, output_path, message, mode}``。
调用入口(由 main_view 在后台 QThread 中调用):
execute_step10({
"bsq_path": "D:/deglint_output.bsq", # 去耀斑 BSQ 影像(必填)
"deglint_img_path": "D:/deglint_output.bsq", # 同上(兼容旧 panel 字段)
"hdr_path": "D:/deglint_output.hdr", # ENVI 头文件(可省,自动 .bsq→.hdr 推断)
"selected_formulas": ["NDCI", "BGA_Am09KBBI"], # 要处理的公式名列表(空 → 全部)
"formula_csv_path": "D:/waterindex.csv", # waterindex.csv 路径(可省,自动探测)
"water_mask_path": "D:/water_mask.dat", # 水域掩膜路径(可省)
"nodata_value": -9999.0, # NoData 标记值
"output_dir": "D:/10_WaterIndex_Images", # 输出目录(可省 → work_dir/10_WaterIndex_Images
"enabled": True,
"work_dir": "D:/workspace", # 工作目录main_view 注入)
})
返回字典字段:
* ``status`` : "completed" | "skipped" | "error"
* ``output_path`` : 输出目录路径(失败时为 None
* ``message`` : 人类可读说明
* ``mode`` : "watercolor_inversion"(便于 UI 提示)
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_waterindex_csv(formula_csv_path: Optional[str], work_dir: str) -> str:
"""解析 waterindex.csv 路径(与 WaterIndexProcessor.__init__ 默认逻辑保持一致)"""
if formula_csv_path and Path(formula_csv_path).is_file():
return formula_csv_path
candidates = [
Path(work_dir) / "waterindex.csv",
Path(work_dir) / "model" / "waterindex.csv",
Path("src/gui/model/waterindex.csv"),
]
for c in candidates:
if c.is_file():
return str(c)
return formula_csv_path or ""
def _resolve_water_mask_path(water_mask_path: Optional[str], work_dir: str) -> Optional[str]:
"""解析水域掩膜路径(缺省时尝试从 work_dir/1_water_mask 自动扫盘)"""
if water_mask_path and Path(water_mask_path).is_file():
return water_mask_path
if not work_dir:
return None
mask_dir = Path(work_dir) / "1_water_mask"
if not mask_dir.is_dir():
return None
for pat in ("*.tif", "*.TIF", "*.dat", "*.DT"):
cands = sorted(mask_dir.glob(pat))
if cands:
return str(cands[0])
return None
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算水色指数反演结果输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
step10 的 output_dir 本身就是一个目录),否则用 work_dir/10_WaterIndex_Images 默认。
注意step10 与其他步骤不同——output_dir 直接表示目录而非文件路径,
所以使用 Path(user_path) 而非 .parent。
"""
user_path = get_user_output_path(config, "output_dir", "output_path")
if user_path:
return Path(user_path), "user"
return Path(work_dir) / "10_WaterIndex_Images", "default"
def execute_step10(config: Dict[str, Any]) -> Dict[str, Any]:
"""Step 10 后端计算入口——纯函数
Args:
config: 由前端 view.get_config() 序列化、再经 main_view 注入 work_dir 的字典
Returns:
标准结果字典 ``{status, output_path, message, mode}``
"""
# ---------- 入参规整 ----------
bsq_path: str = config.get("bsq_path") or config.get("deglint_img_path") or ""
hdr_path: str = config.get("hdr_path") or ""
selected_formulas: List[str] = config.get("selected_formulas") or []
formula_csv_path: str = config.get("formula_csv_path") or ""
water_mask_path: Optional[str] = config.get("water_mask_path")
nodata_value: float = float(config.get("nodata_value", -9999.0))
output_dir: str = config.get("output_dir") or ""
enabled: bool = bool(config.get("enabled", True))
work_dir: str = config.get("work_dir") or "."
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "watercolor_inversion"
# ---------- 提前失败检查 ----------
if not enabled:
return {
"status": "skipped",
"output_path": None,
"message": "用户禁用此步骤enabled=False",
"mode": mode,
}
if not bsq_path:
return {
"status": "error",
"output_path": None,
"message": "未提供 BSQ 影像路径bsq_path / deglint_img_path",
"mode": mode,
}
if not Path(bsq_path).is_file():
return {
"status": "error",
"output_path": None,
"message": f"BSQ 影像不存在: {bsq_path}",
"mode": mode,
}
if not hdr_path:
# 自动探测 .hdr
hdr_path = str(Path(bsq_path).with_suffix(".hdr"))
if not Path(hdr_path).is_file():
hdr_alt = str(Path(bsq_path).with_suffix(".HDR"))
if Path(hdr_alt).is_file():
hdr_path = hdr_alt
else:
hdr_path = ""
if not hdr_path or not Path(hdr_path).is_file():
return {
"status": "error",
"output_path": None,
"message": f"未找到 ENVI 头文件(与 BSQ 同名 .hdr: {bsq_path}",
"mode": mode,
}
# ---------- 解析 waterindex.csv ----------
resolved_formula_csv = _resolve_waterindex_csv(formula_csv_path, work_dir)
if not resolved_formula_csv:
return {
"status": "error",
"output_path": None,
"message": "未提供 formula_csv_path 且默认位置均找不到 waterindex.csv",
"mode": mode,
}
# ---------- 解析水域掩膜(可选) ----------
resolved_water_mask = _resolve_water_mask_path(water_mask_path, work_dir)
# ---------- 执行(包一层 try/except 把异常转 dict避免炸线程 ----------
try:
from src.core.algorithms.waterindex_inversion import WaterIndexProcessor
print(f"[Step10 Service] 水色指数反演: bsq={bsq_path}")
print(f"[Step10 Service] hdr={hdr_path}")
print(f"[Step10 Service] formula_csv={resolved_formula_csv}")
print(f"[Step10 Service] selected_formulas={selected_formulas or '全部'}")
if resolved_water_mask:
print(f"[Step10 Service] water_mask={resolved_water_mask}")
processor = WaterIndexProcessor(resolved_formula_csv)
results = processor.run_inversion(
deglint_img_path=bsq_path,
work_dir=work_dir,
formula_csv_path=resolved_formula_csv,
selected_formulas=selected_formulas or None,
water_mask_path=resolved_water_mask,
nodata_value=nodata_value,
callback=None, # 日志由 main_view 统一接管
)
except FileNotFoundError as e:
return {
"status": "error",
"output_path": None,
"message": f"文件不存在: {e}",
"mode": mode,
}
except ValueError as e:
return {
"status": "error",
"output_path": None,
"message": f"参数错误: {e}",
"mode": mode,
}
except Exception as e: # noqa: BLE001 —— service 层兜底捕获所有
return {
"status": "error",
"output_path": None,
"message": f"{type(e).__name__}: {e}",
"mode": mode,
}
# ---------- 成功路径 ----------
p = Path(output_path)
n_results = len(results) if isinstance(results, dict) else 0
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),
"message": f"水色指数反演完成,共生成 {n_results} 个指数 GeoTIFF",
"mode": mode,
}