Files
WQ_GUI/src/new/services/step12_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

281 lines
11 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 -*-
"""
Step12 后端计算服务(数据可视化——散点/光谱/箱线/掩膜缩略/采样地图)
================================================================
纯计算函数——绝对不引用 PyQt、绝对不引用 main_view。它只:
1. 从 ``config`` 字典读取 ``generate_*`` 5 个开关;
2. 按开关依次调用 ``src/core/visualization`` 或
``src/postprocessing.visualization_reports`` 中的独立生成函数;
3. 每个子任务独立 try/except 隔离,单个失败不影响其它;
4. 返回结果字典 ``{status, output_path, message, mode}``。
调用入口(由 main_view 在后台 QThread 中调用):
execute_step12({
"work_dir": "D:/workspace", # 工作目录(必填)
"img_dir": "D:/workspace/9_ML_Prediction", # 图像目录(可省,自动推断)
"generate_scatter": True, # 模型评估散点图
"generate_spectrum": True, # 光谱曲线图
"generate_boxplots": True, # 箱线图
"generate_glint_previews": True, # 掩膜/耀斑缩略图
"generate_sampling_maps": True, # 采样点地图
"models_dir": "D:/8_Supervised_Model_Training", # 散点图依赖(可省,自动推断)
"training_csv_path": "D:/7_Water_Quality_Indices/training_with_indices.csv", # 光谱/箱线依赖(可省,自动推断)
"output_dir": "D:/14_visualization", # 输出目录(可省 → work_dir/14_visualization)
"enabled": True,
})
返回字典字段:
* ``status`` : "completed" | "skipped" | "error"
* ``output_path`` : 输出目录路径(失败时为 None)
* ``message`` : 人类可读说明
* ``mode`` : "viz_generate"(便于 UI 提示)
设计取舍
--------
- 旧 panel 把所有图嵌入 matplotlib 画布;service 端只生成 PNG 文件,
完全离线——view 层不再嵌入任何 chart widget。
- 单个可视化失败时 ``results[sub] = {"status": "error", ...}``,
但外层 status 仍然 = "completed"(局部失败不等于全部失败)。
- 完全没有可用数据(连 work_dir 都不存在)时才返回 status="error"。
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
"""根据 output_dir / work_dir 计算可视化输出目录
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
(step12 的 output_dir 本身就是一个目录),否则用 work_dir/14_visualization 默认。
注意:step12 与其他步骤不同——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) / "14_visualization", "default"
def _resolve_models_dir(work_dir: str, models_dir: str | None) -> str:
"""自动推断模型目录"""
if models_dir and Path(models_dir).is_dir():
return models_dir
cand = Path(work_dir) / "8_Supervised_Model_Training"
return str(cand) if cand.is_dir() else (models_dir or "")
def _resolve_training_csv(work_dir: str, training_csv_path: str | None) -> str:
"""自动推断训练 CSV"""
if training_csv_path and Path(training_csv_path).is_file():
return training_csv_path
for sub in (
"7_Water_Quality_Indices",
"6_Spectral_Feature_Extraction",
"4_processed_data",
"visualization",
):
d = Path(work_dir) / sub
if not d.is_dir():
continue
cands = sorted(d.glob("*.csv"))
if cands:
return str(cands[0])
return training_csv_path or ""
def _try_scatter(work_dir: str, output_dir: Path) -> Dict[str, Any]:
"""生成模型评估散点图(依赖 models_dir + training_csv_path)"""
from src.core.visualization.scatter_plot import generate_model_scatter_plots
models_dir = _resolve_models_dir(work_dir, None)
training_csv = _resolve_training_csv(work_dir, None)
if not models_dir or not Path(models_dir).is_dir():
raise FileNotFoundError(f"模型目录不存在: {models_dir or '(自动推断失败)'}")
if not training_csv or not Path(training_csv).is_file():
raise FileNotFoundError(f"训练 CSV 不存在: {training_csv or '(自动推断失败)'}")
out_sub = output_dir / "scatter_plots"
paths = generate_model_scatter_plots(
models_dir=models_dir,
training_csv_path=training_csv,
output_dir=str(out_sub),
)
return {"status": "completed", "count": len(paths), "output_dir": str(out_sub)}
def _try_spectrum(work_dir: str, output_dir: Path) -> Dict[str, Any]:
"""生成光谱曲线对比图(依赖 training CSV)"""
from src.core.visualization.spectrum_plot import generate_spectrum_comparison_plots
training_csv = _resolve_training_csv(work_dir, None)
if not training_csv or not Path(training_csv).is_file():
raise FileNotFoundError(f"训练 CSV 不存在: {training_csv or '(自动推断失败)'}")
out_sub = output_dir / "spectrum_plots"
paths = generate_spectrum_comparison_plots(
csv_path=training_csv,
output_dir=str(out_sub),
)
return {"status": "completed", "count": len(paths), "output_dir": str(out_sub)}
def _try_boxplots(work_dir: str, output_dir: Path) -> Dict[str, Any]:
"""生成水质参数箱型图(依赖 training CSV)"""
from src.core.visualization.boxplot import generate_boxplots
training_csv = _resolve_training_csv(work_dir, None)
if not training_csv or not Path(training_csv).is_file():
raise FileNotFoundError(f"训练 CSV 不存在: {training_csv or '(自动推断失败)'}")
out_sub = output_dir / "boxplots"
paths = generate_boxplots(
csv_path=training_csv,
output_dir=str(out_sub),
)
return {"status": "completed", "count": len(paths), "output_dir": str(out_sub)}
def _try_glint_previews(work_dir: str, output_dir: Path) -> Dict[str, Any]:
"""生成耀斑分析影像预览图"""
from src.postprocessing.visualization_reports import ReportGenerator
rg = ReportGenerator(output_dir=str(output_dir))
paths = rg.generate_glint_deglint_previews(
work_dir=work_dir,
output_subdir="glint_deglint_previews",
generate_glint=True,
generate_deglint=True,
)
return {"status": "completed", "count": len(paths), "output_dir": str(output_dir / "glint_deglint_previews")}
def _try_sampling_maps(work_dir: str, output_dir: Path) -> Dict[str, Any]:
"""生成采样点地图"""
from src.postprocessing.visualization_reports import ReportGenerator
rg = ReportGenerator(output_dir=str(output_dir))
p = rg.generate_sampling_point_map(output_subdir="sampling_maps")
return {"status": "completed" if p else "error", "path": p, "output_dir": str(output_dir / "sampling_maps")}
def execute_step12(config: Dict[str, Any]) -> Dict[str, Any]:
"""Step 12 后端计算入口——纯函数"""
work_dir: str = config.get("work_dir") or ""
img_dir: str = config.get("img_dir") or ""
enabled: bool = bool(config.get("enabled", True))
output_dir: str = config.get("output_dir") or ""
# 5 个开关:缺省默认 True(与旧 panel 行为一致)
gen_scatter = bool(config.get("generate_scatter", True))
gen_spectrum = bool(config.get("generate_spectrum", True))
gen_boxplots = bool(config.get("generate_boxplots", True))
gen_glint = bool(config.get("generate_glint_previews", True))
gen_sampling = bool(config.get("generate_sampling_maps", True))
output_path, _source = _resolve_output_dir(config, work_dir)
mode = "viz_generate"
# ---------- 提前失败检查 ----------
if not enabled:
return {
"status": "skipped",
"output_path": None,
"message": "用户禁用此步骤(enabled=False)",
"mode": mode,
}
if not work_dir:
return {
"status": "error",
"output_path": None,
"message": "未提供工作目录(work_dir)",
"mode": mode,
}
if not Path(work_dir).is_dir():
return {
"status": "error",
"output_path": None,
"message": f"工作目录不存在: {work_dir}",
"mode": mode,
}
output_path.mkdir(parents=True, exist_ok=True)
# img_dir 仅作提示用,不强制校验
_ = img_dir
# ---------- 逐项执行(独立 try/except 隔离) ----------
results: Dict[str, Dict[str, Any]] = {}
n_ok = 0
n_skip = 0
n_err = 0
tasks = []
if gen_scatter:
tasks.append(("scatter", _try_scatter))
if gen_spectrum:
tasks.append(("spectrum", _try_spectrum))
if gen_boxplots:
tasks.append(("boxplots", _try_boxplots))
if gen_glint:
tasks.append(("glint_previews", _try_glint_previews))
if gen_sampling:
tasks.append(("sampling_maps", _try_sampling_maps))
if not tasks:
return {
"status": "completed",
"output_path": str(output_path).replace("\\", "/"),
"message": "无可视化任务(5 个开关全部 False)",
"mode": mode,
}
print(f"[Step12 Service] 工作目录: {work_dir}")
print(f"[Step12 Service] 输出目录: {output_path}")
print(f"[Step12 Service] 子任务: {[n for n, _ in tasks]}")
for name, fn in tasks:
try:
r = fn(work_dir, output_path)
results[name] = r
if r.get("status") == "completed":
n_ok += 1
print(f" ✅ {name}: count={r.get('count', r.get('path', '?'))}")
else:
n_err += 1
print(f" ⚠ {name}: status={r.get('status')}")
except FileNotFoundError as e:
results[name] = {"status": "skipped", "reason": str(e)}
n_skip += 1
print(f" ↻ {name}: 跳过(缺数据): {e}")
except Exception as e: # noqa: BLE001
results[name] = {"status": "error", "message": f"{type(e).__name__}: {e}"}
n_err += 1
print(f" ❌ {name}: {type(e).__name__}: {e}")
# ---------- 汇总 ----------
# 局部失败不算全局失败——只有当所有任务都缺数据时才算 skipped
if n_ok == 0 and n_err == 0:
return {
"status": "skipped",
"output_path": None,
"message": f"全部 {n_skip} 个子任务缺数据被跳过(models_dir/training_csv 等前置产物不存在)",
"mode": mode,
}
return {
"status": "completed",
"output_path": str(output_path).replace("\\", "/"),
"message": (
f"可视化完成:成功 {n_ok} / 失败 {n_err} / 跳过 {n_skip};"
f"目录: {output_path.name or output_path}"
),
"mode": mode,
}