232 lines
8.7 KiB
Python
232 lines
8.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Step10 后端计算服务(水色指数反演 · 散点 CSV 模式)
|
||
====================================================
|
||
|
||
纯计算函数——绝对不引用 PyQt、绝对不引用 main_view、绝对不读写全局变量。它只:
|
||
|
||
1. 从 ``config`` 字典读取参数;
|
||
2. 调用 ``WaterIndexCsvProcessor.compute_indices_from_csv``
|
||
读取 Step 4 输出的 ``sampling_spectra.csv`` 散点,对每行采样点
|
||
套用 ``waterindex.csv`` 中勾选的公式,输出每公式一个 CSV;
|
||
3. 返回结果字典 ``{status, output_path, message, mode, ...}``。
|
||
|
||
调用入口(由 main_view 在后台 QThread 中调用):
|
||
|
||
execute_step10({
|
||
"sampling_csv_path": "D:/4_sampling/sampling_spectra.csv", # 必填
|
||
"selected_formulas": ["NDCI", "BGA_Am09KBBI"], # 勾选公式;空 → 全部
|
||
"formula_csv_path": "D:/waterindex.csv", # waterindex.csv 路径
|
||
"output_dir": "D:/10_WaterIndex_CSV", # 输出目录;可省
|
||
"enabled": True,
|
||
"work_dir": "D:/workspace", # 主窗口注入
|
||
})
|
||
|
||
返回字典字段:
|
||
|
||
* ``status`` : "completed" | "skipped" | "error"
|
||
* ``output_path`` : 输出目录路径(失败时为 None)
|
||
* ``output_files`` : {公式名: 公式 CSV 路径}(失败时为空 dict)
|
||
* ``message`` : 人类可读说明
|
||
* ``mode`` : "watercolor_inversion_csv"(便于 UI 提示)
|
||
|
||
设计要点
|
||
========
|
||
- 与 Step 9 (ML 预测) 完全对称的"散点处理模式":输入 CSV、输出 CSV,
|
||
坐标列重命名为 longitude/latitude,公式值以列形式追加。
|
||
- 旧"读 BSQ 全图 → 输出 GeoTIFF"模式已废弃(科学上误差大且与 GIS 栅格计算器重复)。
|
||
- 兼容调用方可能仍传旧键(bsq_path / hdr_path / deglint_img_path),检测到时
|
||
静默忽略并回退到 sampling_spectra.csv 路径解析(避免破坏已有 pipeline 配置)。
|
||
"""
|
||
|
||
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
|
||
|
||
|
||
def _resolve_sampling_csv_path(
|
||
sampling_csv_path: Optional[str],
|
||
work_dir: str,
|
||
) -> str:
|
||
"""解析采样点 CSV 路径
|
||
|
||
解析顺序:
|
||
1. 显式传入的 ``sampling_csv_path``
|
||
2. ``{work_dir}/4_sampling/sampling_spectra.csv``
|
||
3. ``{work_dir}/4_sampling/`` 下任意 ``.csv`` (取最新)
|
||
"""
|
||
if sampling_csv_path and Path(sampling_csv_path).is_file():
|
||
return sampling_csv_path
|
||
if not work_dir:
|
||
return sampling_csv_path or ""
|
||
|
||
primary = Path(work_dir) / "4_sampling" / "sampling_spectra.csv"
|
||
if primary.is_file():
|
||
return str(primary).replace("\\", "/")
|
||
|
||
sample_dir = Path(work_dir) / "4_sampling"
|
||
if sample_dir.is_dir():
|
||
cands = sorted(sample_dir.glob("*.csv"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||
if cands:
|
||
return str(cands[0]).replace("\\", "/")
|
||
|
||
return sampling_csv_path or ""
|
||
|
||
|
||
def _resolve_waterindex_csv(formula_csv_path: Optional[str], work_dir: str) -> str:
|
||
"""解析 waterindex.csv 路径(与 WaterIndexCsvProcessor.__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).replace("\\", "/")
|
||
return formula_csv_path or ""
|
||
|
||
|
||
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_CSV`` 默认。
|
||
"""
|
||
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_CSV", "default"
|
||
|
||
|
||
def execute_step10(config: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Step 10 后端计算入口——纯函数(散点 CSV 模式)
|
||
|
||
Args:
|
||
config: 由前端 view.get_config() 序列化、再经 main_view 注入 work_dir 的字典
|
||
|
||
Returns:
|
||
标准结果字典 ``{status, output_path, output_files, message, mode}``
|
||
"""
|
||
# ---------- 入参规整 ----------
|
||
sampling_csv_path: str = (
|
||
config.get("sampling_csv_path")
|
||
or config.get("spectrum_csv_path") # 兼容旧字段
|
||
or ""
|
||
)
|
||
selected_formulas: List[str] = config.get("selected_formulas") or []
|
||
formula_csv_path: str = config.get("formula_csv_path") or ""
|
||
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_csv"
|
||
|
||
# ---------- 提前失败检查 ----------
|
||
if not enabled:
|
||
return {
|
||
"status": "skipped",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": "用户禁用此步骤(enabled=False)",
|
||
"mode": mode,
|
||
}
|
||
|
||
# 解析采样点 CSV 路径
|
||
resolved_sampling_csv = _resolve_sampling_csv_path(sampling_csv_path, work_dir)
|
||
if not resolved_sampling_csv:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": "未提供 sampling_csv_path 且默认位置均找不到 sampling_spectra.csv",
|
||
"mode": mode,
|
||
}
|
||
if not Path(resolved_sampling_csv).is_file():
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": f"采样点 CSV 不存在: {resolved_sampling_csv}",
|
||
"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,
|
||
"output_files": {},
|
||
"message": "未提供 formula_csv_path 且默认位置均找不到 waterindex.csv",
|
||
"mode": mode,
|
||
}
|
||
if not Path(resolved_formula_csv).is_file():
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": f"waterindex.csv 不存在: {resolved_formula_csv}",
|
||
"mode": mode,
|
||
}
|
||
|
||
# ---------- 执行(包一层 try/except 把异常转 dict,避免炸线程) ----------
|
||
try:
|
||
from src.core.algorithms.waterindex_inversion import (
|
||
WaterIndexCsvProcessor,
|
||
)
|
||
|
||
print(f"[Step10 Service] 水色指数反演(散点模式): sampling_csv={resolved_sampling_csv}")
|
||
print(f"[Step10 Service] formula_csv={resolved_formula_csv}")
|
||
print(f"[Step10 Service] selected_formulas={selected_formulas or '全部'}")
|
||
print(f"[Step10 Service] output_dir={output_path}")
|
||
|
||
processor = WaterIndexCsvProcessor(resolved_formula_csv)
|
||
out_files = processor.compute_indices_from_csv(
|
||
sampling_csv_path=resolved_sampling_csv,
|
||
output_dir=str(output_path).replace("\\", "/"),
|
||
selected_formulas=selected_formulas or None,
|
||
progress_callback=None, # 日志由 main_view 统一接管
|
||
)
|
||
except FileNotFoundError as e:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": f"文件不存在: {e}",
|
||
"mode": mode,
|
||
}
|
||
except ValueError as e:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": f"参数错误: {e}",
|
||
"mode": mode,
|
||
}
|
||
except Exception as e: # noqa: BLE001 —— service 层兜底捕获所有
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"output_files": {},
|
||
"message": f"{type(e).__name__}: {e}",
|
||
"mode": mode,
|
||
}
|
||
|
||
# ---------- 成功路径 ----------
|
||
p = Path(output_path)
|
||
n_results = len(out_files) if isinstance(out_files, dict) else 0
|
||
return {
|
||
"status": "completed",
|
||
"output_path": str(p).replace("\\", "/"),
|
||
"output_files": out_files,
|
||
"message": f"水色指数反演完成,共生成 {n_results} 个指数 CSV",
|
||
"mode": mode,
|
||
}
|