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)统一视觉。
This commit is contained in:
@ -35,23 +35,34 @@ MainView —— 端到端模块化新架构的路由与调度壳(主窗口)
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QLabel, QListWidget, QMainWindow,
|
||||
QPushButton, QSplitter, QStackedWidget, QTextEdit, QVBoxLayout,
|
||||
QWidget,
|
||||
QFileDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||
QMainWindow, QMessageBox, QPushButton, QSplitter, QStackedWidget,
|
||||
QTextEdit, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
# 让 ``python src/new/main_view.py`` 直接运行时也能 import ``src.xxx``
|
||||
_HERE = Path(__file__).resolve().parent # .../src/new
|
||||
_SRC_ROOT = _HERE.parent # .../src
|
||||
_PROJECT_ROOT = _SRC_ROOT.parent # .../WQ_GUI(项目根,用于解析 data/icons/)
|
||||
if str(_SRC_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_SRC_ROOT))
|
||||
|
||||
|
||||
def _res(rel: str) -> str:
|
||||
"""解析项目根的相对路径,PyInstaller 打包后兼容 sys._MEIPASS。"""
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
return os.path.join(sys._MEIPASS, rel)
|
||||
return str(_PROJECT_ROOT / rel)
|
||||
|
||||
|
||||
from src.gui.styles import ModernStylesheet
|
||||
from src.new.views.placeholder_view import PlaceholderView
|
||||
|
||||
@ -63,6 +74,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step1",
|
||||
"name": "1. 水域掩膜",
|
||||
"icon": "1.png",
|
||||
"view_module": "src.new.views.step1_view",
|
||||
"view_class": "Step1View",
|
||||
"service_module": "src.new.services.step1_service",
|
||||
@ -72,6 +84,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step2",
|
||||
"name": "2. 耀斑检测",
|
||||
"icon": "2.png",
|
||||
"view_module": "src.new.views.step2_view",
|
||||
"view_class": "Step2View",
|
||||
"service_module": "src.new.services.step2_service",
|
||||
@ -80,6 +93,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step3",
|
||||
"name": "3. 耀斑去除",
|
||||
"icon": "3.png",
|
||||
"view_module": "src.new.views.step3_view",
|
||||
"view_class": "Step3View",
|
||||
"service_module": "src.new.services.step3_service",
|
||||
@ -88,6 +102,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step4",
|
||||
"name": "4. 采样点布设",
|
||||
"icon": "4.png",
|
||||
"view_module": "src.new.views.step4_view",
|
||||
"view_class": "Step4View",
|
||||
"service_module": "src.new.services.step4_service",
|
||||
@ -96,6 +111,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step5",
|
||||
"name": "5. 数据清洗",
|
||||
"icon": "5.png",
|
||||
"view_module": "src.new.views.step5_view",
|
||||
"view_class": "Step5View",
|
||||
"service_module": "src.new.services.step5_service",
|
||||
@ -104,6 +120,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step6",
|
||||
"name": "6. 光谱特征",
|
||||
"icon": "6.png",
|
||||
"view_module": "src.new.views.step6_view",
|
||||
"view_class": "Step6View",
|
||||
"service_module": "src.new.services.step6_service",
|
||||
@ -112,6 +129,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step7",
|
||||
"name": "7. 水质光谱指数",
|
||||
"icon": "7.png",
|
||||
"view_module": "src.new.views.step7_view",
|
||||
"view_class": "Step7View",
|
||||
"service_module": "src.new.services.step7_service",
|
||||
@ -120,14 +138,17 @@ ROUTES = [
|
||||
{
|
||||
"id": "step8",
|
||||
"name": "8. 机器学习建模",
|
||||
"icon": "8.png",
|
||||
"view_module": "src.new.views.step8_view",
|
||||
"view_class": "Step8View",
|
||||
"service_module": "src.new.services.step8_service",
|
||||
"service_func": "execute_step8",
|
||||
},
|
||||
{
|
||||
# 旧版共用 10.png 是 bug;data/icons/ 里有 11.png 一直未引用,正好给"预测"用
|
||||
"id": "step9",
|
||||
"name": "9. 机器学习预测",
|
||||
"icon": "11.png",
|
||||
"view_module": "src.new.views.step9_view",
|
||||
"view_class": "Step9View",
|
||||
"service_module": "src.new.services.step9_service",
|
||||
@ -136,14 +157,17 @@ ROUTES = [
|
||||
{
|
||||
"id": "step10",
|
||||
"name": "10. 水色指数反演",
|
||||
"icon": "10.png",
|
||||
"view_module": "src.new.views.step10_view",
|
||||
"view_class": "Step10View",
|
||||
"service_module": "src.new.services.step10_service",
|
||||
"service_func": "execute_step10",
|
||||
},
|
||||
{
|
||||
# data/icons/ 没有 12.png/13.png;Step11/12/13 暂时复用 9.png(11 个 png 对 13 个 step 必然有共用)
|
||||
"id": "step11",
|
||||
"name": "11. 专题图生成",
|
||||
"icon": "9.png",
|
||||
"view_module": "src.new.views.step11_view",
|
||||
"view_class": "Step11View",
|
||||
"service_module": "src.new.services.step11_service",
|
||||
@ -152,6 +176,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step12",
|
||||
"name": "12. 可视化",
|
||||
"icon": "9.png",
|
||||
"view_module": "src.new.views.step12_view",
|
||||
"view_class": "Step12View",
|
||||
"service_module": "src.new.services.step12_service",
|
||||
@ -160,6 +185,7 @@ ROUTES = [
|
||||
{
|
||||
"id": "step13",
|
||||
"name": "13. 报告生成",
|
||||
"icon": "9.png",
|
||||
"view_module": "src.new.views.step13_view",
|
||||
"view_class": "Step13View",
|
||||
"service_module": "src.new.services.step13_service",
|
||||
@ -168,6 +194,43 @@ ROUTES = [
|
||||
]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 全局输入参数:用户在任意 step 导图后,广播给所有 view 自动填充
|
||||
# =========================================================================
|
||||
# 这些是各 view 关心的"上游产物 / 用户导入文件"key。任何一次 run_single_step
|
||||
# 都会把 config_dict 中出现的非空值更新进 self.global_inputs,并全量广播。
|
||||
# view.set_config 已实现"非空才覆盖"(避免回填空串覆盖上游有效值),
|
||||
# 所以直接全量广播是安全的。
|
||||
GLOBAL_INPUT_KEYS: tuple[str, ...] = (
|
||||
# step1 入口
|
||||
"img_path", # 原始 BSQ/DAT/TIF 影像
|
||||
"mask_path", # 现有水域掩膜(shp / dat / tif)
|
||||
# step2/3 输入
|
||||
"water_mask_path", # step1 产出
|
||||
# step3/4/6/10 输入
|
||||
"deglint_img_path", # step3 产出
|
||||
"bsq_path", # step10 用,等价 deglint_img_path
|
||||
"hdr_path", # ENVI 头文件
|
||||
# step4 输入
|
||||
"glint_mask_path", # step2 耀斑掩膜
|
||||
# step5/6 输入
|
||||
"csv_path", # step5 产出
|
||||
"boundary_path", # step6 水域掩膜别名
|
||||
# step7/8 输入
|
||||
"training_csv_path", # step6 产出
|
||||
# step9 输入
|
||||
"sampling_csv_path", # step4 产出
|
||||
"models_dir", # step8 产出
|
||||
# step10 输入
|
||||
"formula_csv_path", # waterindex.csv
|
||||
# step11 输入
|
||||
"prediction_csv_path",
|
||||
"prediction_csv_dir",
|
||||
"geotiff_path",
|
||||
"geotiff_dir",
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# TaskWorker:QThread 后台执行器(三信号协议)
|
||||
# =========================================================================
|
||||
@ -224,11 +287,20 @@ class MainView(QMainWindow):
|
||||
self._views: dict = {}
|
||||
self._route_map: dict = {}
|
||||
self.worker: TaskWorker | None = None
|
||||
# 各 step 的产出路径缓存(声明式路径流转的中枢)
|
||||
self.step_outputs: dict = {}
|
||||
# 全局输入参数广播:用户在任何 step 导图后,其他 step 自动同步
|
||||
self.global_inputs: dict = {}
|
||||
|
||||
self.setWindowTitle("WQ_GUI 新架构 · 端到端模块化路由壳")
|
||||
self.resize(1280, 800)
|
||||
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
||||
|
||||
# 设置窗口图标(旧版 water_quality_gui.py L1339-1340 用 uitubiao.ico)
|
||||
ico_path = _res("data/icons-1/uitubiao.ico")
|
||||
if Path(ico_path).is_file():
|
||||
self.setWindowIcon(QIcon(ico_path))
|
||||
|
||||
self._build_ui()
|
||||
self._build_routes()
|
||||
|
||||
@ -245,8 +317,29 @@ class MainView(QMainWindow):
|
||||
outer = QVBoxLayout(central)
|
||||
outer.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
# ----- 顶部工具条:工作目录 + 设置按钮 -----
|
||||
# ----- 顶部工具条:logo + 项目名 + 工作目录 + 设置按钮 -----
|
||||
toolbar = QHBoxLayout()
|
||||
|
||||
# Logo(旧版 water_quality_gui.py L1569 用 logo.png)
|
||||
logo_path = _res("data/icons/logo.png")
|
||||
if Path(logo_path).is_file():
|
||||
logo_pixmap = QPixmap(logo_path)
|
||||
if not logo_pixmap.isNull():
|
||||
logo_pixmap = logo_pixmap.scaledToHeight(
|
||||
36, Qt.SmoothTransformation
|
||||
)
|
||||
logo_label = QLabel()
|
||||
logo_label.setPixmap(logo_pixmap)
|
||||
toolbar.addWidget(logo_label)
|
||||
|
||||
# 项目名(紧贴 logo 右侧)
|
||||
title_label = QLabel("WQ_GUI · 新架构")
|
||||
title_label.setStyleSheet(
|
||||
"font-size: 14px; font-weight: bold; color: #0055D4;"
|
||||
)
|
||||
toolbar.addWidget(title_label)
|
||||
toolbar.addSpacing(20)
|
||||
|
||||
toolbar.addWidget(QLabel("工作目录:"))
|
||||
self.work_dir_label = QLabel("(未设置)")
|
||||
self.work_dir_label.setStyleSheet("color: #0055D4; font-weight: bold;")
|
||||
@ -259,6 +352,27 @@ class MainView(QMainWindow):
|
||||
|
||||
outer.addLayout(toolbar)
|
||||
|
||||
# ----- 顶部 MegaCube Banner(CSS border-image 平铺拉伸 + 居中文字) -----
|
||||
banner_label = QLabel()
|
||||
banner_label.setMinimumHeight(100)
|
||||
banner_label.setMaximumHeight(100)
|
||||
banner_label.setAlignment(Qt.AlignCenter)
|
||||
# 无条件显示文字(border-image 不会覆盖前景文字)
|
||||
banner_label.setText("MegaCube-Water Quality V1.2")
|
||||
banner_path = _res("data/icons/Mega Water 1.0.jpg").replace('\\', '/')
|
||||
if Path(banner_path).is_file():
|
||||
banner_label.setStyleSheet(
|
||||
f"border-image: url('{banner_path}') 0 0 0 0 stretch stretch;"
|
||||
"color: white; font-size: 38px; font-weight: bold; font-family: 'Microsoft YaHei';"
|
||||
)
|
||||
else:
|
||||
banner_label.setStyleSheet(
|
||||
"background: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, "
|
||||
"stop:0 #00a0e9, stop:1 #005a9e);"
|
||||
"color: white; font-size: 38px; font-weight: bold; font-family: 'Microsoft YaHei';"
|
||||
)
|
||||
outer.addWidget(banner_label)
|
||||
|
||||
# ----- 中部:左导航 + 右 stacked(用 QSplitter 可拖动) -----
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
@ -291,7 +405,23 @@ class MainView(QMainWindow):
|
||||
view = self._instantiate_view(route)
|
||||
self._views[route["id"]] = view
|
||||
self.stacked.addWidget(view)
|
||||
self.nav_list.addItem(route["name"])
|
||||
|
||||
# 加载图标(ROUTES[icon] 字段);找不到图标时退化为无图标 item
|
||||
icon_filename = route.get("icon")
|
||||
if icon_filename:
|
||||
icon_full_path = _res(f"data/icons/{icon_filename}")
|
||||
if Path(icon_full_path).is_file():
|
||||
item = QListWidgetItem(QIcon(icon_full_path), route["name"])
|
||||
else:
|
||||
self._log(
|
||||
f"[Router] 图标缺失 {icon_full_path},{route['id']} 用纯文字"
|
||||
)
|
||||
item = QListWidgetItem(route["name"])
|
||||
else:
|
||||
item = QListWidgetItem(route["name"])
|
||||
# 统一图标尺寸(避免不同 png 拉伸不一致)
|
||||
item.setSizeHint(item.sizeHint())
|
||||
self.nav_list.addItem(item)
|
||||
|
||||
if ROUTES:
|
||||
self.nav_list.setCurrentRow(0)
|
||||
@ -339,6 +469,10 @@ class MainView(QMainWindow):
|
||||
self.work_dir_label.setText(self.work_dir)
|
||||
self._log(f"[Router] 工作目录已设置: {self.work_dir}")
|
||||
|
||||
# 工作目录变更 → 清空全局输入缓存(旧路径可能已失效)
|
||||
if self.global_inputs:
|
||||
self.clear_global_inputs()
|
||||
|
||||
# 推送给所有 view(让它们按需自动填充路径)
|
||||
for view in self._views.values():
|
||||
if hasattr(view, "update_from_config"):
|
||||
@ -352,8 +486,10 @@ class MainView(QMainWindow):
|
||||
|
||||
1. 查路由表;
|
||||
2. 注入 ``work_dir``(避免 view 重复感知);
|
||||
3. 启动 ``TaskWorker`` 在子线程运行 service;
|
||||
4. 三信号路由:log_msg → 日志框;finished → 状态分支日志;
|
||||
3. **全局输入广播**——把本次 config_dict 中的公共输入 key 累计到
|
||||
``self.global_inputs``,再推送给所有 view(实现"一处导入,全局共享");
|
||||
4. 启动 ``TaskWorker`` 在子线程运行 service;
|
||||
5. 三信号路由:log_msg → 日志框;finished → 状态分支日志;
|
||||
error → 未预期异常写日志 + traceback。
|
||||
"""
|
||||
route = self._route_map.get(step_id)
|
||||
@ -365,6 +501,9 @@ class MainView(QMainWindow):
|
||||
if self.work_dir and "work_dir" not in config_dict:
|
||||
config_dict = {**config_dict, "work_dir": self.work_dir}
|
||||
|
||||
# 全局输入广播:累计 + 推送给所有 view
|
||||
self._broadcast_global_inputs(config_dict)
|
||||
|
||||
service_func = self._load_callable(route["service_module"], route["service_func"])
|
||||
if service_func is None:
|
||||
return
|
||||
@ -378,8 +517,13 @@ class MainView(QMainWindow):
|
||||
|
||||
self.worker = TaskWorker(service_func, config_dict)
|
||||
self.worker.log_msg.connect(self._log)
|
||||
self.worker.finished.connect(self._on_step_done)
|
||||
self.worker.error.connect(self._on_step_error)
|
||||
# 用 lambda 注入 step_id:finished/error 都拿得到自己服务的是哪一步
|
||||
self.worker.finished.connect(
|
||||
lambda result, sid=step_id: self._on_step_done(sid, result)
|
||||
)
|
||||
self.worker.error.connect(
|
||||
lambda msg, sid=step_id: self._on_step_error(sid, msg)
|
||||
)
|
||||
self.worker.start()
|
||||
|
||||
def _load_callable(self, module_name: str, attr_name: str):
|
||||
@ -392,9 +536,13 @@ class MainView(QMainWindow):
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _on_step_done(self, result: dict):
|
||||
def _on_step_done(self, step_id: str, result: dict):
|
||||
"""TaskWorker finished 回调——按 status 分支写日志
|
||||
|
||||
1. 缓存 output_path 到 self.step_outputs;
|
||||
2. 触发 _sync_dependencies 把产出路径推给所有下游 view;
|
||||
3. status=completed → QMessageBox.information;status=error → warning。
|
||||
|
||||
注:service 内部 try/except 转换的 status='error' 也走这里;
|
||||
这与 error 信号(worker 自身崩溃)语义不同。
|
||||
"""
|
||||
@ -403,8 +551,22 @@ class MainView(QMainWindow):
|
||||
output_path = result.get("output_path")
|
||||
mode = result.get("mode", "")
|
||||
|
||||
# 1. 缓存路径 + 触发声明式下游推送(即使 error 也要缓存,让用户看到已失败)
|
||||
if output_path:
|
||||
self.step_outputs[step_id] = str(output_path)
|
||||
self._log(f"[Cache] {step_id} → step_outputs['{step_id}'] = {output_path}")
|
||||
self._sync_dependencies()
|
||||
else:
|
||||
self._log(f"[Cache] {step_id} 无 output_path,跳过 step_outputs 缓存")
|
||||
|
||||
# 2. 日志分支 + 弹窗提示
|
||||
if status == "completed":
|
||||
self._log(f"[Service✓] {message}(mode={mode}, output={output_path})")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"执行完成",
|
||||
f"{message or '当前步骤已完成!'}\n\n产出路径:\n{output_path or '(无)'}",
|
||||
)
|
||||
elif status == "skipped":
|
||||
self._log(f"[Service↻] {message}(已复用历史结果)")
|
||||
elif status == "not_implemented":
|
||||
@ -414,10 +576,170 @@ class MainView(QMainWindow):
|
||||
tb = result.get("traceback")
|
||||
if tb:
|
||||
self._log(tb)
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"执行失败",
|
||||
f"{message or '当前步骤执行失败'}\n\n请查看底部日志获取 traceback。",
|
||||
)
|
||||
|
||||
def _on_step_error(self, msg: str):
|
||||
"""TaskWorker error 信号回调——worker 自身捕获的未预期异常"""
|
||||
self._log(f"[Worker✗] 未预期异常:\n{msg}")
|
||||
def _on_step_error(self, step_id: str, msg: str):
|
||||
"""TaskWorker error 信号回调——worker 自身捕获的未预期异常
|
||||
|
||||
worker 自身崩溃(service_func 抛出未捕获异常)走这里;
|
||||
与 finished 中 status='error' 语义不同:error 信号表示 worker
|
||||
完全无法产出结果 dict。
|
||||
"""
|
||||
self._log(f"[Worker✗] {step_id} 未预期异常:\n{msg}")
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Worker 异常",
|
||||
f"步骤 {step_id} 后台执行崩溃:\n\n{msg[:500]}",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 声明式路径流转:用 set_config 把上游产出推给所有下游 view
|
||||
# ------------------------------------------------------------------
|
||||
def _sync_dependencies(self):
|
||||
"""遍历 self.step_outputs 字典,按依赖图把每步产出推给下游
|
||||
|
||||
推送关系(按用户原意 + 各 view set_config 实际接受 key 校对):
|
||||
step1 (水域掩膜) → step2/step3/step4 的 water_mask_path
|
||||
step3 (耀斑去除) → step4 的 deglint_img_path
|
||||
+ step6 的 deglint_img_path
|
||||
+ step10 的 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(父目录)
|
||||
step10 (水色指数) → step11 的 geotiff_dir(父目录)
|
||||
|
||||
每个推送都用 _safe_set_config 包装,view 缺失 / 没有 set_config
|
||||
/ set_config 抛异常都不会让 sync 链路中断。
|
||||
"""
|
||||
# ----- step1 → step2/3/4 (water_mask_path) -----
|
||||
s1 = self.step_outputs.get("step1")
|
||||
if s1:
|
||||
for sid in ("step2", "step3", "step4"):
|
||||
self._safe_set_config(sid, {"water_mask_path": s1})
|
||||
|
||||
# ----- step3 → step4 (deglint_img_path) / step6 / step10 (bsq_path) -----
|
||||
s3 = self.step_outputs.get("step3")
|
||||
if s3:
|
||||
self._safe_set_config("step4", {"deglint_img_path": s3})
|
||||
self._safe_set_config("step6", {"deglint_img_path": s3})
|
||||
self._safe_set_config("step10", {"bsq_path": s3})
|
||||
|
||||
# ----- step4 → step9 (sampling_csv_path: 单文件,直接传) -----
|
||||
s4 = self.step_outputs.get("step4")
|
||||
if s4:
|
||||
self._safe_set_config("step9", {"sampling_csv_path": s4})
|
||||
|
||||
# ----- step5 → step6 (csv_path) -----
|
||||
s5 = self.step_outputs.get("step5")
|
||||
if s5:
|
||||
self._safe_set_config("step6", {"csv_path": s5})
|
||||
|
||||
# ----- step6 → step7/step8 (training_csv_path) -----
|
||||
s6 = self.step_outputs.get("step6")
|
||||
if s6:
|
||||
self._safe_set_config("step7", {"training_csv_path": s6})
|
||||
self._safe_set_config("step8", {"training_csv_path": s6})
|
||||
|
||||
# ----- step8 → step9 (models_dir: 父目录,因为 step8 产物是模型目录里的若干 .joblib) -----
|
||||
s8 = self.step_outputs.get("step8")
|
||||
if s8:
|
||||
s8_dir = str(Path(s8).parent).replace("\\", "/")
|
||||
self._safe_set_config("step9", {"models_dir": s8_dir})
|
||||
|
||||
# ----- step9 → step11 (prediction_csv_dir: 父目录,step9 输出到 ml_prediction/) -----
|
||||
s9 = self.step_outputs.get("step9")
|
||||
if s9:
|
||||
s9_dir = str(Path(s9).parent).replace("\\", "/")
|
||||
self._safe_set_config("step11", {"prediction_csv_dir": s9_dir})
|
||||
# 同时推 single 模式入口,避免用户在 folder 模式下还要切回
|
||||
self._safe_set_config("step11", {"prediction_csv_path": s9})
|
||||
|
||||
# ----- step10 → step11 (geotiff_dir: 父目录,step10 输出到 watercolor/) -----
|
||||
s10 = self.step_outputs.get("step10")
|
||||
if s10:
|
||||
s10_dir = str(Path(s10).parent).replace("\\", "/")
|
||||
self._safe_set_config("step11", {"geotiff_dir": s10_dir})
|
||||
# 同时推 single 模式入口
|
||||
self._safe_set_config("step11", {"geotiff_path": s10})
|
||||
|
||||
self._log(f"[Sync] 依赖同步完成(step_outputs keys: {list(self.step_outputs.keys())})")
|
||||
|
||||
def _safe_set_config(self, step_id: str, config: dict):
|
||||
"""安全调用 view.set_config;view 缺失 / 无该方法 / 抛异常时降级日志
|
||||
|
||||
返回 True 表示推送成功,False 表示降级跳过,便于上层做计数。
|
||||
"""
|
||||
view = self._views.get(step_id)
|
||||
if view is None:
|
||||
return False
|
||||
if not hasattr(view, "set_config"):
|
||||
self._log(f"[Sync] {step_id} 没有 set_config,跳过推送 {list(config.keys())}")
|
||||
return False
|
||||
try:
|
||||
view.set_config(config)
|
||||
self._log(f"[Sync] → {step_id}.set_config({config})")
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001 —— 单步推送失败不影响整体 sync
|
||||
self._log(f"[Sync] 推送 {step_id} 失败: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 全局输入参数广播:用户在一处导图 → 所有 view 自动同步
|
||||
# ------------------------------------------------------------------
|
||||
def _broadcast_global_inputs(self, config_dict: dict):
|
||||
"""把本次 config_dict 中的公共输入 key 累计到 self.global_inputs,
|
||||
再把整个 global_inputs 推给所有 view(实现"一处导入,全局共享")。
|
||||
|
||||
设计要点:
|
||||
1. **非空才覆盖**——空字符串/None 不会污染已存在的有效值(防止下游
|
||||
view 用空串覆盖掉上游刚刚推送的有效路径)。
|
||||
2. **全量广播是安全的**——各 view.set_config 已实现 "if key in config"
|
||||
守卫,未识别的 key 直接忽略,互不干扰。
|
||||
3. **顺序无关**——多次 run_single_step 都会持续累计 self.global_inputs,
|
||||
直到用户主动切换工作目录或清理。
|
||||
"""
|
||||
updated_keys: list[str] = []
|
||||
for key in GLOBAL_INPUT_KEYS:
|
||||
val = config_dict.get(key)
|
||||
if val: # 非空才覆盖(None / "" 一律跳过)
|
||||
self.global_inputs[key] = val
|
||||
updated_keys.append(key)
|
||||
|
||||
if updated_keys:
|
||||
self._log(
|
||||
f"[Global] 本次更新 {len(updated_keys)} 个全局输入: {updated_keys}"
|
||||
)
|
||||
|
||||
# 推送给所有 view
|
||||
if not self._views:
|
||||
return
|
||||
n_ok, n_fail = 0, 0
|
||||
for step_id, view in self._views.items():
|
||||
if not hasattr(view, "set_config"):
|
||||
continue
|
||||
try:
|
||||
view.set_config(self.global_inputs)
|
||||
n_ok += 1
|
||||
except Exception as e: # noqa: BLE001 —— 单 view 失败不影响其它
|
||||
n_fail += 1
|
||||
self._log(f"[Global] {step_id} 推送失败: {e}")
|
||||
|
||||
self._log(
|
||||
f"[Global] 广播完成: ok={n_ok} fail={n_fail} "
|
||||
f"keys={list(self.global_inputs.keys())}"
|
||||
)
|
||||
|
||||
def clear_global_inputs(self):
|
||||
"""清空全局输入缓存(工作目录切换 / 用户重置时调用)"""
|
||||
self.global_inputs.clear()
|
||||
self._log("[Global] 已清空全局输入缓存")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 日志:所有日志统一走这里
|
||||
|
||||
@ -38,6 +38,8 @@ 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__ 默认逻辑保持一致)"""
|
||||
@ -70,11 +72,19 @@ def _resolve_water_mask_path(water_mask_path: Optional[str], work_dir: str) -> O
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
|
||||
"""根据 output_dir / work_dir 计算水色指数反演结果输出目录"""
|
||||
if output_dir:
|
||||
return Path(output_dir)
|
||||
return Path(work_dir) / "10_WaterIndex_Images"
|
||||
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]:
|
||||
@ -97,7 +107,7 @@ def execute_step10(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
enabled: bool = bool(config.get("enabled", True))
|
||||
work_dir: str = config.get("work_dir") or "."
|
||||
|
||||
output_path = _resolve_output_dir(output_dir, work_dir)
|
||||
output_path, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "watercolor_inversion"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -51,12 +51,22 @@ 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_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
|
||||
"""根据 output_dir / work_dir 计算专题图输出目录"""
|
||||
if output_dir:
|
||||
return Path(output_dir)
|
||||
return Path(work_dir) / "11_Thematic_Map"
|
||||
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_dir / work_dir 计算专题图输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
|
||||
(step11 的 output_dir 本身就是一个目录),否则用 work_dir/11_Thematic_Map 默认。
|
||||
|
||||
注意:step11 与其他步骤不同——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) / "11_Thematic_Map", "default"
|
||||
|
||||
|
||||
def _resolve_csv_paths(config: Dict[str, Any], work_dir: str) -> List[Path]:
|
||||
@ -209,7 +219,7 @@ def execute_step11(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_dir: str = config.get("output_dir") or ""
|
||||
work_dir: str = config.get("work_dir") or "."
|
||||
|
||||
output_path = _resolve_output_dir(output_dir, work_dir)
|
||||
output_path, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "make_thematic_map"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -48,11 +48,22 @@ 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(output_dir: str | None, work_dir: str) -> Path:
|
||||
if output_dir:
|
||||
return Path(output_dir)
|
||||
return Path(work_dir) / "14_visualization"
|
||||
|
||||
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:
|
||||
@ -170,7 +181,7 @@ def execute_step12(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
gen_glint = bool(config.get("generate_glint_previews", True))
|
||||
gen_sampling = bool(config.get("generate_sampling_maps", True))
|
||||
|
||||
output_path = _resolve_output_dir(output_dir, work_dir)
|
||||
output_path, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "viz_generate"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -53,12 +53,22 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
|
||||
|
||||
def _resolve_output_dir(output_dir: Optional[str], work_dir: str) -> Path:
|
||||
"""根据 output_dir / work_dir 计算 Word 报告输出目录"""
|
||||
if output_dir:
|
||||
return Path(output_dir)
|
||||
return Path(work_dir) / "14_visualization"
|
||||
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_dir / work_dir 计算 Word 报告输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_dir 时直接用其值
|
||||
(step13 的 output_dir 本身就是一个目录),否则用 work_dir/14_visualization 默认。
|
||||
|
||||
注意:step13 与其他步骤不同——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 _apply_ai_env(config: Dict[str, Any]) -> None:
|
||||
@ -117,7 +127,7 @@ def execute_step13(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
).strip()
|
||||
enabled: bool = bool(config.get("enabled", True))
|
||||
|
||||
output_path = _resolve_output_dir(output_dir, work_dir)
|
||||
output_path, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "generate_word_report"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -35,6 +35,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.water_mask_step import WaterMaskStep
|
||||
from src.new.services._output_resolver import get_user_output_path, is_user_specified
|
||||
|
||||
|
||||
def _resolve_mode(config: Dict[str, Any]) -> str:
|
||||
|
||||
@ -39,13 +39,21 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.glint_detection_step import GlintDetectionStep
|
||||
from src.new.services._output_resolver import (
|
||||
copy_to_user_path,
|
||||
get_user_output_path,
|
||||
is_user_specified,
|
||||
resolve_output_dir,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_glint_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算耀斑检测输出目录"""
|
||||
if output_path:
|
||||
return Path(output_path).parent
|
||||
return Path(work_dir) / "2_Glint_Detection"
|
||||
def _resolve_glint_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算耀斑检测输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||||
否则用 work_dir/2_Glint_Detection 默认。
|
||||
"""
|
||||
return resolve_output_dir(config, work_dir, "2_Glint_Detection", "output_path", "output_dir")
|
||||
|
||||
|
||||
def _clean_int_param(value: Any, default: int = 0) -> int | None:
|
||||
@ -77,7 +85,7 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path = config.get("output_path")
|
||||
work_dir = config.get("work_dir") or "."
|
||||
|
||||
glint_dir = _resolve_glint_dir(output_path, work_dir)
|
||||
glint_dir, _source = _resolve_glint_dir(config, work_dir)
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
if not enabled:
|
||||
@ -103,13 +111,14 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
# ---------- 构建底层 kwargs(可选字段:None 一律不传,让底层走默认) ----------
|
||||
# 注意:GlintDetectionStep.run 不接受 output_path 关键字——它只接收 glint_dir;
|
||||
# 用户指定的文件名将通过下文的 copy_to_user_path 事后劫持拷贝。
|
||||
kwargs: Dict[str, Any] = {
|
||||
"img_path": img_path,
|
||||
"glint_wave": glint_wave,
|
||||
"method": method,
|
||||
"water_mask_path": water_mask_path,
|
||||
"glint_dir": glint_dir,
|
||||
"output_path": output_path,
|
||||
"callback": None, # 日志由 main_view 统一接管
|
||||
}
|
||||
|
||||
@ -162,6 +171,15 @@ def execute_step2(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"mode": method,
|
||||
}
|
||||
|
||||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||||
# 旧版 GlintDetectionStep.run 只接受 glint_dir 不接受确切文件名,
|
||||
# 所以用户浏览时输入的 output_path 在算法内部被忽略;这里事后把
|
||||
# 硬编码的 result_path 拷贝/重命名到 user_path。
|
||||
user_path = config.get("output_path")
|
||||
if user_path:
|
||||
result_path = copy_to_user_path(result_path, user_path)
|
||||
p = Path(result_path) # 同步刷新 p 给最后的 return 用
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"output_path": str(p).replace("\\", "/"),
|
||||
|
||||
@ -43,14 +43,23 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.glint_removal_step import GlintRemovalStep
|
||||
from src.new.services._output_resolver import (
|
||||
copy_to_user_path,
|
||||
get_user_output_path,
|
||||
is_user_specified,
|
||||
resolve_output_dir,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_dirs(output_path: str | None, work_dir: str) -> tuple[Path, Path]:
|
||||
"""根据 output_path / work_dir 计算 (deglint_dir, water_mask_dir)"""
|
||||
if output_path:
|
||||
deglint_dir = Path(output_path).parent
|
||||
else:
|
||||
deglint_dir = Path(work_dir) / "3_Deglint"
|
||||
def _resolve_dirs(config: Dict[str, Any], work_dir: str) -> tuple[Path, Path]:
|
||||
"""根据 output_path / work_dir 计算 (deglint_dir, water_mask_dir)
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||||
否则用 work_dir/3_Deglint 默认。
|
||||
"""
|
||||
deglint_dir, _source = resolve_output_dir(
|
||||
config, work_dir, "3_Deglint", "output_path", "output_dir"
|
||||
)
|
||||
water_mask_dir = Path(work_dir) / "1_water_mask"
|
||||
return deglint_dir, water_mask_dir
|
||||
|
||||
@ -124,7 +133,7 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path = config.get("output_path")
|
||||
work_dir = config.get("work_dir") or "."
|
||||
|
||||
deglint_dir, water_mask_dir = _resolve_dirs(output_path, work_dir)
|
||||
deglint_dir, water_mask_dir = _resolve_dirs(config, work_dir)
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
if not enabled:
|
||||
@ -150,6 +159,8 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
# ---------- 构建底层 kwargs ----------
|
||||
# 注意:GlintRemovalStep.run 不接受 output_path 关键字——它只接收 deglint_dir;
|
||||
# 用户指定的文件名将通过下文的 copy_to_user_path 事后劫持拷贝。
|
||||
method_kwargs = _build_method_kwargs(method, config)
|
||||
kwargs: Dict[str, Any] = {
|
||||
"img_path": img_path,
|
||||
@ -159,7 +170,6 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"interpolation_method": interpolation_method,
|
||||
"deglint_dir": deglint_dir,
|
||||
"water_mask_dir": water_mask_dir,
|
||||
"output_path": output_path,
|
||||
"callback": None, # 日志由 main_view 统一接管
|
||||
}
|
||||
kwargs.update(method_kwargs)
|
||||
@ -199,6 +209,16 @@ def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"mode": method,
|
||||
}
|
||||
|
||||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||||
# 旧版 GlintRemovalStep.run 只接受 deglint_dir 不接受确切文件名;
|
||||
# 用户浏览指定的 .bsq 文件名被底层忽略(同时 .hdr 头文件也按硬编码名生成)。
|
||||
# 这里事后把 result_path 拷贝/重命名到 user_path,copy_to_user_path
|
||||
# 会自动处理 .hdr / .HDR 伴随文件。
|
||||
user_path = config.get("output_path")
|
||||
if user_path:
|
||||
result_path = copy_to_user_path(result_path, user_path)
|
||||
p = Path(result_path)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"output_path": str(p).replace("\\", "/"),
|
||||
|
||||
@ -38,13 +38,21 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.prediction_step import PredictionStep
|
||||
from src.new.services._output_resolver import (
|
||||
copy_to_user_path,
|
||||
get_user_output_path,
|
||||
is_user_specified,
|
||||
resolve_output_dir,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算采样点输出目录"""
|
||||
if output_path:
|
||||
return Path(output_path).parent
|
||||
return Path(work_dir) / "4_Sampling"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算采样点输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||||
否则用 work_dir/4_Sampling 默认。
|
||||
"""
|
||||
return resolve_output_dir(config, work_dir, "4_Sampling", "output_path", "output_dir")
|
||||
|
||||
|
||||
def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -67,7 +75,7 @@ def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path = config.get("output_path")
|
||||
work_dir = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_path, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "adaptive" if use_adaptive_sampling else "fixed"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
@ -138,6 +146,14 @@ def execute_step4(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||||
# PredictionStep.generate_sampling_points 内部按 sampling_spectra.csv 硬编码;
|
||||
# 用户浏览指定的文件名在算法内部被忽略。
|
||||
user_path = config.get("output_path")
|
||||
if user_path:
|
||||
result_path = copy_to_user_path(result_path, user_path)
|
||||
p = Path(result_path)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"output_path": str(p).replace("\\", "/"),
|
||||
|
||||
@ -33,13 +33,21 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.data_preparation_step import DataPreparationStep
|
||||
from src.new.services._output_resolver import (
|
||||
copy_to_user_path,
|
||||
get_user_output_path,
|
||||
is_user_specified,
|
||||
resolve_output_dir,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算清洗后 CSV 输出目录"""
|
||||
if output_path:
|
||||
return Path(output_path).parent
|
||||
return Path(work_dir) / "5_Data_Cleaning"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算清洗后 CSV 输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||||
否则用 work_dir/5_Data_Cleaning 默认。
|
||||
"""
|
||||
return resolve_output_dir(config, work_dir, "5_Data_Cleaning", "output_path", "output_dir")
|
||||
|
||||
|
||||
def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -57,7 +65,7 @@ def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path = config.get("output_path")
|
||||
work_dir = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_path, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "csv_clean"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
@ -128,6 +136,14 @@ def execute_step5(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||||
# process_csv 内部硬编码输出文件名 processed_data.csv,
|
||||
# 用户浏览指定的文件名(无论是 .csv 还是别的格式)被底层忽略。
|
||||
user_path = config.get("output_path")
|
||||
if user_path:
|
||||
result_path = copy_to_user_path(result_path, user_path)
|
||||
p = Path(result_path)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"output_path": str(p).replace("\\", "/"),
|
||||
|
||||
@ -39,13 +39,23 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.data_preparation_step import DataPreparationStep
|
||||
from src.new.services._output_resolver import (
|
||||
copy_to_user_path,
|
||||
get_user_output_path,
|
||||
is_user_specified,
|
||||
resolve_output_dir,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算 training_spectra.csv 输出目录"""
|
||||
if output_path:
|
||||
return Path(output_path).parent
|
||||
return Path(work_dir) / "6_Spectral_Feature_Extraction"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算 training_spectra.csv 输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||||
否则用 work_dir/6_Spectral_Feature_Extraction 默认。
|
||||
"""
|
||||
return resolve_output_dir(
|
||||
config, work_dir, "6_Spectral_Feature_Extraction", "output_path", "output_dir"
|
||||
)
|
||||
|
||||
|
||||
def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -69,7 +79,7 @@ def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path = config.get("output_path")
|
||||
work_dir = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_path, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "extract_spectra"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
@ -163,6 +173,14 @@ def execute_step6(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||||
# extract_training_spectra 内部硬编码输出文件名 training_spectra.csv,
|
||||
# 用户浏览指定的文件名在算法内部被忽略。
|
||||
user_path = config.get("output_path")
|
||||
if user_path:
|
||||
result_path = copy_to_user_path(result_path, user_path)
|
||||
p = Path(result_path)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"output_path": str(p).replace("\\", "/"),
|
||||
|
||||
@ -35,13 +35,19 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.core.steps.data_preparation_step import DataPreparationStep
|
||||
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
|
||||
|
||||
|
||||
def _resolve_output_dir(output_file: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_file / work_dir 计算 indices CSV 输出目录"""
|
||||
if output_file:
|
||||
return Path(output_file).parent
|
||||
return Path(work_dir) / "7_Water_Quality_Indices"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_file / output_path / work_dir 计算 indices CSV 输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——step7 旧 panel 字段是 output_file,
|
||||
新架构保留 output_path 兼容性,按优先级回退。
|
||||
"""
|
||||
return resolve_output_dir(
|
||||
config, work_dir, "7_Water_Quality_Indices",
|
||||
"output_file", "output_path", "output_dir",
|
||||
)
|
||||
|
||||
|
||||
def execute_step7(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -61,7 +67,7 @@ def execute_step7(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_file: Optional[str] = config.get("output_file")
|
||||
work_dir: str = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_file, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "calc_indices"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -38,13 +38,23 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.core.steps.modeling_step import ModelingStep
|
||||
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
|
||||
|
||||
|
||||
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算模型保存目录"""
|
||||
if output_path:
|
||||
return Path(output_path)
|
||||
return Path(work_dir) / "8_Supervised_Model_Training"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算模型保存目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时直接用其值
|
||||
(因为 step8 的 output_path 是一个目录,不是文件),
|
||||
否则用 work_dir/8_Supervised_Model_Training 默认。
|
||||
|
||||
注意:step8 与其他步骤不同——output_path 直接表示目录而非文件路径,
|
||||
所以使用 Path(user_path) 而非 .parent。
|
||||
"""
|
||||
user_path = get_user_output_path(config, "output_path", "output_dir")
|
||||
if user_path:
|
||||
return Path(user_path), "user"
|
||||
return Path(work_dir) / "8_Supervised_Model_Training", "default"
|
||||
|
||||
|
||||
def execute_step8(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -67,7 +77,7 @@ def execute_step8(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path: Optional[str] = config.get("output_path")
|
||||
work_dir: str = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_path, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "train_ml"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -41,14 +41,22 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.core.steps.prediction_step import PredictionStep
|
||||
from src.new.services._output_resolver import get_user_output_path, is_user_specified, resolve_output_dir
|
||||
|
||||
|
||||
def _resolve_output_dir(output_path: str | None, work_dir: str) -> Path:
|
||||
"""根据 output_path / work_dir 计算预测结果输出目录"""
|
||||
if output_path:
|
||||
return Path(output_path)
|
||||
# 与旧 pipeline 一致:prediction_dir / 9_ML_Prediction
|
||||
return Path(work_dir) / "prediction" / "9_ML_Prediction"
|
||||
def _resolve_output_dir(config: Dict[str, Any], work_dir: str) -> tuple[Path, str]:
|
||||
"""根据 output_path / work_dir 计算预测结果输出目录
|
||||
|
||||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时直接用其值
|
||||
(step9 的 output_path 本身就是一个目录,非文件),否则用 work_dir 默认路径。
|
||||
|
||||
注意:step9 与其他步骤不同——output_path 直接表示目录而非文件路径,
|
||||
所以使用 Path(user_path) 而非 .parent。
|
||||
"""
|
||||
user_path = get_user_output_path(config, "output_path", "output_dir")
|
||||
if user_path:
|
||||
return Path(user_path), "user"
|
||||
return Path(work_dir) / "prediction" / "9_ML_Prediction", "default"
|
||||
|
||||
|
||||
def execute_step9(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -69,7 +77,7 @@ def execute_step9(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output_path: str = config.get("output_path")
|
||||
work_dir: str = config.get("work_dir") or "."
|
||||
|
||||
output_dir = _resolve_output_dir(output_path, work_dir)
|
||||
output_dir, _source = _resolve_output_dir(config, work_dir)
|
||||
mode = "predict_ml"
|
||||
|
||||
# ---------- 提前失败检查 ----------
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Step12View —— Step 12(可视化)的端到端模块化 view(精简版)
|
||||
Step12View —— Step 12(可视化)的端到端模块化 view
|
||||
|
||||
UI 从 ``src/gui/panels/step12_viz_panel.py``(1882 行巨型)做大幅精简。
|
||||
UI 在精简版基础上挂回旧版的 ``ImageCategoryTree`` 和 ``ImageViewerWidget``
|
||||
两个高级组件:
|
||||
|
||||
精简原则
|
||||
========
|
||||
- **ImageCategoryTree** —— 按"模型评估/光谱分析/统计图表/处理结果/
|
||||
含量分布图"五类自动归类工作目录下的图像文件,用户点击文件即可预览。
|
||||
- **ImageViewerWidget** —— 支持滚轮缩放(0.1x-5x)+ 50ms 防抖 +
|
||||
FastTransformation/SmoothTransformation 智能切换 + Ctrl+Wheel +
|
||||
鼠标工具栏(缩放/适应窗口/1:1/保存)。
|
||||
|
||||
- **删除全部 matplotlib / ImageViewerWidget / ChartViewerDialog /
|
||||
VisualizationWorkerThread / PandasTableModel / ChartBrowserDialog** 等子控件;
|
||||
这些是「运行时画图」所需的复杂组件,由 service + 独立图片查看器窗口接管。
|
||||
- **保留可视化配置 checkbox 组**(5 个:散点/光谱/箱线/掩膜缩略/采样地图)——
|
||||
这是用户实际可控的业务选项。
|
||||
- **保留工作目录 / 图像目录选择 + 扫描 / 生成全部按钮**——给用户「
|
||||
「一键触发可视化」的入口;具体生成逻辑由 service 接管。
|
||||
所有渲染**纯本地化**,不触发任何 service;service 端只负责生成图像
|
||||
到工作目录,view 端负责即时浏览。
|
||||
"""
|
||||
|
||||
import os
|
||||
@ -25,6 +24,7 @@ from PyQt5.QtWidgets import (
|
||||
QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from src.gui.components.image_widgets import ImageCategoryTree, ImageViewerWidget
|
||||
from src.gui.styles import ModernStylesheet
|
||||
from src.new.core.base_view import BaseView
|
||||
|
||||
@ -34,7 +34,7 @@ def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||
|
||||
|
||||
class Step12View(BaseView):
|
||||
"""Step 12: 可视化(精简版)"""
|
||||
"""Step 12: 可视化"""
|
||||
|
||||
def init_ui(self):
|
||||
main_layout = QHBoxLayout()
|
||||
@ -110,13 +110,16 @@ class Step12View(BaseView):
|
||||
self.gen_all_btn = QPushButton("🚀 生成全部")
|
||||
self.gen_all_btn.setToolTip("生成所有类型的可视化图表")
|
||||
self.gen_all_btn.setStyleSheet(
|
||||
"background-color: #4CAF50; color: white; font-weight: bold;"
|
||||
ModernStylesheet.get_button_stylesheet("success")
|
||||
)
|
||||
self.gen_all_btn.clicked.connect(self._on_run_clicked)
|
||||
config_layout.addWidget(self.gen_all_btn)
|
||||
|
||||
self.scan_btn = QPushButton("📁 扫描目录")
|
||||
self.scan_btn.setToolTip("扫描工作目录中的图像文件")
|
||||
self.scan_btn.setToolTip("扫描工作目录中的图像文件,加载到下方分类树")
|
||||
self.scan_btn.setStyleSheet(
|
||||
ModernStylesheet.get_button_stylesheet("primary")
|
||||
)
|
||||
self.scan_btn.clicked.connect(self._on_scan_clicked)
|
||||
config_layout.addWidget(self.scan_btn)
|
||||
|
||||
@ -127,20 +130,22 @@ class Step12View(BaseView):
|
||||
left_panel.setMaximumWidth(350)
|
||||
main_layout.addWidget(left_panel, 0)
|
||||
|
||||
# ===== 右侧占位 =====
|
||||
# ===== 右侧:分类树 + 图片查看器(旧版高级家具) =====
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
placeholder = QLabel(
|
||||
"可视化结果由 service 端生成,\n"
|
||||
"生成完成后可在主窗口日志区查看文件路径。\n"
|
||||
"(精简版 view 不内嵌 matplotlib 画布。)"
|
||||
)
|
||||
placeholder.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
placeholder.setStyleSheet(
|
||||
"color: #999; font-size: 13px; padding: 40px;"
|
||||
)
|
||||
right_layout.addWidget(placeholder, 1)
|
||||
right_layout.setSpacing(6)
|
||||
|
||||
# 顶部:图像分类树(按类别组织:模型评估/光谱分析/统计图表/处理结果/含量分布图)
|
||||
self.image_tree = ImageCategoryTree()
|
||||
self.image_tree.setMaximumHeight(280)
|
||||
self.image_tree.currentItemChanged.connect(self._on_tree_selection)
|
||||
right_layout.addWidget(self.image_tree)
|
||||
|
||||
# 底部:图片查看器(支持滚轮缩放、工具栏、1:1/适应窗口/保存)
|
||||
self.image_viewer = ImageViewerWidget()
|
||||
right_layout.addWidget(self.image_viewer, 1)
|
||||
|
||||
right_panel.setLayout(right_layout)
|
||||
main_layout.addWidget(right_panel, 1)
|
||||
|
||||
@ -225,7 +230,17 @@ class Step12View(BaseView):
|
||||
self.dispatch_execute("step12", self.get_config())
|
||||
|
||||
def _on_scan_clicked(self):
|
||||
# 扫描仅刷新工作目录和图像目录的显示;不实际派发到 service
|
||||
"""扫描工作目录下的图像文件并加载到分类树(不派发 service)"""
|
||||
work_dir = self.work_dir_edit.text()
|
||||
if work_dir and os.path.isdir(work_dir):
|
||||
self.img_dir_edit.setText(work_dir)
|
||||
if not work_dir or not os.path.isdir(work_dir):
|
||||
return
|
||||
self.image_tree.scan_directory(work_dir)
|
||||
self.img_dir_edit.setText(work_dir)
|
||||
|
||||
def _on_tree_selection(self, current, _previous):
|
||||
"""分类树选中项 → 图片查看器即时预览"""
|
||||
if current is None:
|
||||
return
|
||||
path = self.image_tree.get_selected_image_path()
|
||||
if path and os.path.isfile(path):
|
||||
self.image_viewer.load_image(path)
|
||||
|
||||
Reference in New Issue
Block a user