feat: Step1~Step14 面板单步按钮 EventBus 解耦 + Handler 补全(Step8~Step14)+ 旧上帝类删除

- 9 个面板(step1~step6/step8_ml_train/step8_qaa/step9_ml_predict/step10)单步执行按钮从 parent 链上溯改为 global_event_bus.publish('RequestRunSingleStep')

- PipelineExecutor 新增 _on_request_run_single_step 订阅

- 新增 Handler: step8_ml_train / step9_ml_predict / step10_qaa_inversion / step11_concentration / step12_kriging / step13_visualization / step14_report

- 删除旧 water_quality_inversion_pipeline_GUI.py(上帝类已肢解完毕)
This commit is contained in:
DXC
2026-06-18 09:19:51 +08:00
parent 2d45610aa6
commit 2261b4b30e
28 changed files with 1446 additions and 2690 deletions

View File

@ -26,6 +26,7 @@ Pipeline 执行器
import os
import copy
import traceback
from pathlib import Path
from typing import Dict, List, Optional
@ -74,6 +75,9 @@ class PipelineExecutor(QObject):
self._workspace_initializer = workspace_initializer
self._worker: Optional[WorkerThread] = None
# 订阅面板发出的单步执行请求(解耦面板与执行器)
global_event_bus.subscribe('RequestRunSingleStep', self._on_request_run_single_step)
# ═══════════════════════════════════════════════════════════
# 公开 API
# ═══════════════════════════════════════════════════════════
@ -98,26 +102,60 @@ class PipelineExecutor(QObject):
6. 获取配置 + 模式裁剪
7. 一次性全预检 + 用户交互
8. 确认执行 → 创建 WorkerThread → 启动
关键防静默失败设计:
- 每一个 return 前必须通过 EventBus 发布 LogMessage
- 整个方法体包裹在 try/except 中,防止 PyQt5 槽函数静默吞异常
"""
print("==== [探针] run_full_pipeline 方法体已进入 ====", flush=True)
try:
self._run_full_pipeline_impl()
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] run_full_pipeline 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "运行失败",
f"启动流程时发生未预期的错误:\n\n{e}\n\n详细信息已输出到日志区。"
)
def _run_full_pipeline_impl(self):
"""run_full_pipeline 的实现体,由外层 try/except 保护。"""
# ★ 终端即时反馈:确保即使 EventBus/日志区未就绪也能看到
print("\n[PipelineExecutor] 收到「运行完整流程」指令,开始执行...")
if not PIPELINE_AVAILABLE:
global_event_bus.publish('LogMessage', {
'message': '无法导入 Pipeline 模块,请检查项目文件结构!',
'level': 'error',
})
# 阻断性错误仍需弹窗(用户必须知道)
QMessageBox.critical(
self.parent(), "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
# ── 1) 获取 work_dir ──
work_dir = self._workspace_initializer.work_dir
if not work_dir:
global_event_bus.publish('LogMessage', {
'message': '⚠ 未选择工作目录,流程中止。请先通过「工具 → 设置工作目录」选择工作目录。',
'level': 'warning',
})
QMessageBox.warning(self.parent(), "警告", "未选择工作目录,请先设置工作目录。")
return
work_path = Path(work_dir)
global_event_bus.publish('LogMessage', {
'message': f'[运行] 工作目录: {work_dir}',
'level': 'info',
})
# ── 2) 运行前扫描 + 自动回填 ──
global_event_bus.publish('LogMessage', {
@ -132,11 +170,19 @@ class PipelineExecutor(QObject):
# ── 3) step3 波段越界预检 ──
if not self._precheck_step3_bands():
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止step3 波段越界预检未通过(用户取消或波段配置无效)',
'level': 'warning',
})
return
# ── 4) 全流程模式选择弹窗 ──
mode_dlg = PipelineModeDialog(main_window=self.parent(), parent=self.parent())
if mode_dlg.exec() != QDialog.Accepted:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了模式选择对话框',
'level': 'warning',
})
return
selected_mode = mode_dlg.selected_mode
global_event_bus.publish('LogMessage', {
@ -147,8 +193,17 @@ class PipelineExecutor(QObject):
'level': 'info',
})
# ── 5) 获取配置 ──
# ── 5) 获取配置(★ 先预加载所有面板,确保配置完整) ──
global_event_bus.publish('LogMessage', {
'message': '[运行] 正在收集所有步骤面板的配置...',
'level': 'info',
})
self._panel_factory.preload_all()
config = self._get_current_config()
global_event_bus.publish('LogMessage', {
'message': f'[运行] 已收集 {len(config)} 个步骤的配置: {list(config.keys())}',
'level': 'info',
})
# ── 6) 模式裁剪 ──
if selected_mode == "prediction_only":
@ -164,9 +219,17 @@ class PipelineExecutor(QObject):
skip_list: List[str] = []
if missing_items:
global_event_bus.publish('LogMessage', {
'message': f'[预检] 发现 {len(missing_items)} 个缺失项,弹出预检对话框...',
'level': 'warning',
})
critical_items = [it for it in missing_items if it.is_critical]
if critical_items:
lines = "\n".join(f" - [{it.step_name}] {it.reason}" for it in critical_items)
global_event_bus.publish('LogMessage', {
'message': f'[预检] 阻断性错误 ({len(critical_items)} 项):\n{lines}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "预检失败(阻断性错误)",
f"以下为阻断性缺失,流程无法启动:\n\n{lines}\n\n请填写后重新运行。"
@ -175,21 +238,28 @@ class PipelineExecutor(QObject):
dialog = PreflightDialog(missing_items, parent=self.parent())
if dialog.exec() != QDialog.Accepted:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了预检对话框',
'level': 'warning',
})
return
result = dialog.get_result()
if result is None:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:预检对话框返回空结果',
'level': 'warning',
})
return
action, *payload = result
if action == "fill":
_, step_id, tab_index = result
# 发布事件:请求切换到指定 tab
global_event_bus.publish('NavigateToTab', {
'tab_index': tab_index,
'step_id': step_id,
})
global_event_bus.publish('LogMessage', {
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。',
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。流程暂停,填写完成后请重新运行。',
'level': 'info',
})
return
@ -197,8 +267,13 @@ class PipelineExecutor(QObject):
if skip_list:
global_event_bus.publish('LogMessage', {
'message': f'[预检] 用户强制跳过 {len(skip_list)} 个步骤: {skip_list}',
'level': 'info',
'level': 'warning',
})
else:
global_event_bus.publish('LogMessage', {
'message': '[预检] ✓ 所有必需项均已就绪,无需弹窗',
'level': 'info',
})
# ── 8) 确认执行 ──
reply = QMessageBox.question(
@ -207,6 +282,10 @@ class PipelineExecutor(QObject):
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了执行确认',
'level': 'warning',
})
return
# ── 9) 准备 worker_config ──
@ -222,6 +301,11 @@ class PipelineExecutor(QObject):
if not enabled:
worker_config.pop('step6_feature', None)
global_event_bus.publish('LogMessage', {
'message': f'[运行] 最终执行配置包含 {len(worker_config)} 个步骤: {list(worker_config.keys())}',
'level': 'info',
})
# ── 10) 创建 WorkerThread 并连线 ──
self._worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
self._worker.log_message.connect(self._on_log_message, Qt.QueuedConnection)
@ -245,17 +329,48 @@ class PipelineExecutor(QObject):
step_name: 步骤名称(如 'step1', 'step5_clean'
config: 步骤配置字典(可选,默认从面板获取)
"""
try:
self._run_single_step_impl(step_name, config)
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] run_single_step 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "运行失败",
f"启动单步执行时发生未预期的错误:\n\n{e}\n\n详细信息已输出到日志区。"
)
def _run_single_step_impl(self, step_name: str, config: dict = None):
if not PIPELINE_AVAILABLE:
global_event_bus.publish('LogMessage', {
'message': '无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
work_dir = self._workspace_initializer.work_dir or './work_dir'
if config is None:
global_event_bus.publish('LogMessage', {
'message': '[运行] 正在收集所有步骤面板的配置...',
'level': 'info',
})
self._panel_factory.preload_all()
config = self._get_current_config()
global_event_bus.publish('LogMessage', {
'message': f'[运行] 已收集 {len(config)} 个步骤的配置',
'level': 'info',
})
global_event_bus.publish('LogMessage', {
'message': f'初始化 Pipeline工作目录: {work_dir}',
@ -295,6 +410,47 @@ class PipelineExecutor(QObject):
})
global_event_bus.publish('PipelineStopped', {})
# ═══════════════════════════════════════════════════════════
# EventBus 订阅回调
# ═══════════════════════════════════════════════════════════
def _on_request_run_single_step(self, data: dict):
"""处理面板通过 EventBus 发出的单步执行请求。
data 格式: {'step_name': 'step1', 'config': {'step1': {...}}}
前置条件检查(预检/工作目录)由 run_single_step → _run_single_step_impl
内部统一处理,此处仅做解析 + 转发 + 异常兜底。
"""
try:
step_name = data.get('step_name')
config = data.get('config')
if not step_name:
global_event_bus.publish('LogMessage', {
'message': '[单步执行] 请求缺少 step_name忽略',
'level': 'warning',
})
return
global_event_bus.publish('LogMessage', {
'message': f'[单步执行] 收到 {step_name} 的执行请求',
'level': 'info',
})
self.run_single_step(step_name, config)
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] _on_request_run_single_step({step_name}) 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
# ═══════════════════════════════════════════════════════════
# WorkerThread 信号 → EventBus 事件(纯转发,零 UI 操作)
# ═══════════════════════════════════════════════════════════

View File

@ -178,7 +178,7 @@ class VisualizationWorkerThread(QThread):
{"task": "statistics", "output_paths": output_paths}
)
elif self.task == "scatter":
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
from src.core.visualization.scatter_plot import generate_model_scatter_plots
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
models_dir = (self.extra.get("models_dir") or "").strip()
@ -188,10 +188,9 @@ class VisualizationWorkerThread(QThread):
if not models_dir or not Path(models_dir).is_dir():
self.failed.emit("模型目录无效或不存在请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
return
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=training_csv_path,
scatter_paths = generate_model_scatter_plots(
models_dir=models_dir,
training_csv_path=training_csv_path,
)
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
elif self.task == "generate_all_selected":
@ -205,11 +204,10 @@ class VisualizationWorkerThread(QThread):
if training_csv.is_file():
models_dir = wp / "7_Supervised_Model_Training"
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=str(training_csv),
from src.core.visualization.scatter_plot import generate_model_scatter_plots
scatter_paths = generate_model_scatter_plots(
models_dir=str(models_dir),
training_csv_path=str(training_csv),
)
count = len(scatter_paths) if scatter_paths else 0
parts.append(f"散点图: {count}")

View File

@ -54,16 +54,16 @@ def diagnose_pipeline_import_error():
"[INFO] PyInstaller 环境Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
)
else:
pipeline_file = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", "core", "water_quality_inversion_pipeline_GUI.py")
handlers_dir = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", "core", "handlers")
)
if not os.path.exists(pipeline_file):
error_info.append(f"[ERROR] Pipeline文件不存在: {pipeline_file}")
if not os.path.isdir(handlers_dir):
error_info.append(f"[ERROR] Handlers 目录不存在: {handlers_dir}")
error_info.append(
" 解决方案: 请确保项目结构完整,检查 src/core/ 下是否有 water_quality_inversion_pipeline_GUI.py"
" 解决方案: 请确保项目结构完整,检查 src/core/handlers/ 目录是否存在"
)
else:
error_info.append(f"[OK] Pipeline文件存在: {pipeline_file}")
error_info.append(f"[OK] Handlers 目录存在: {handlers_dir}")
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
if current_dir not in sys.path:
@ -240,24 +240,34 @@ class WorkerThread(QThread):
self.log_message.emit(f" [WARNING] {message}", "warning")
def run(self):
"""运行 pipeline子线程内切换 Matplotlib 为 Agg避免 Qt5Agg 在后台线程绘图导致界面卡死。"""
"""运行 pipeline子线程内切换 Matplotlib 为 Agg避免 Qt5Agg 在后台线程绘图导致界面卡死。
终极防崩溃设计:
- 整个 run() 方法体包裹在单一 try/except 中
- 任何未预期的异常都会被捕获并通过 finished 信号回报主线程
- 确保前端永远不会面对"静默死亡"的后台线程
"""
import os
# GDAL 环境变量保护(放在最前面,防止路径/编码问题)
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
os.environ['SHAPE_ENCODING'] = 'UTF-8'
mpl_prev = None
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
try:
# ★ 终端即时反馈
print(f"\n[WorkerThread] 后台线程启动 (mode={self.mode}, work_dir={self.work_dir})")
# ── Matplotlib 后端切换Agg 线程安全) ──
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
# ── 新架构PipelineScheduler + Handler 注册表 ──
scheduler = PipelineScheduler(work_dir=self.work_dir)
scheduler.set_callback(self.pipeline_callback)
@ -267,14 +277,17 @@ class WorkerThread(QThread):
if self.mode == 'full':
self.log_message.emit("开始运行完整流程 (Handler 调度模式)...", "info")
# ── ★ 预检已由 GUI 层 perform_preflight() 完成,此处不再重复预检 ──
# 过滤 skip_list 中的步骤
active_config = {
k: v for k, v in self.config.items()
if k not in self.skip_list
}
self.log_message.emit(
f"[调度] 待执行步骤 ({len(active_config)} 个): {list(active_config.keys())}",
"info"
)
result = scheduler.run_full_pipeline(active_config)
errors = result.get('errors', {})
@ -295,16 +308,28 @@ class WorkerThread(QThread):
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
except PipelineHalt as exc:
# 预检失败 / 硬终止:透传清晰错误信息,不打印完整 traceback
error_msg = str(exc)
self.log_message.emit(f"[预检失败] {error_msg}", "error")
self.finished.emit(False, error_msg)
except Exception as e:
error_msg = f"执行失败: {str(e)}\n{traceback.format_exc()}"
self.log_message.emit(error_msg, "error")
self.finished.emit(False, error_msg)
# ★ 终极捕获:任何未预期的异常都会被完整回报
full_tb = traceback.format_exc()
self.log_message.emit(f"[致命错误] 后台线程崩溃: {e}", "error")
self.log_message.emit(f"详细追踪:\n{full_tb}", "error")
# 同时 print 到终端(确保即使信号失效也能看到)
print(f"\n{'='*60}")
print(f"[WorkerThread 崩溃] {e}")
print(f"{'='*60}")
print(full_tb)
print(f"{'='*60}\n")
self.finished.emit(False, f"后台线程崩溃: {e}\n\n{full_tb}")
finally:
# ── 恢复 Matplotlib 后端 ──
if mpl_prev:
try:
import matplotlib.pyplot as plt

View File

@ -243,7 +243,7 @@ class Step10WatercolorPanel(QWidget):
self.run_btn = QPushButton("▶ 执行水色指数反演")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -484,7 +484,54 @@ class Step10WatercolorPanel(QWidget):
if not self.output_dir.get_path():
self.output_dir.set_path(out_dir)
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
bsq_path = self.bsq_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip()
output_dir = self.output_dir.get_path().strip()
if not bsq_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑 BSQ 影像!")
return
if not Path(bsq_path).exists():
QMessageBox.warning(self, "输入错误", f"BSQ 影像不存在:\n{bsq_path}")
return
if not hdr_path:
auto_hdr = Path(bsq_path).with_suffix('.hdr')
if auto_hdr.exists():
hdr_path = str(auto_hdr)
self.hdr_file.set_path(hdr_path)
else:
QMessageBox.warning(self, "输入错误", "请选择 ENVI 头文件!")
return
if not Path(hdr_path).exists():
QMessageBox.warning(self, "输入错误", f"HDR 文件不存在:\n{hdr_path}")
return
if not output_dir:
work_dir = self._get_default_work_dir()
output_dir = resolve_subdir(work_dir, 'watercolor')
os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir)
selected = self._get_selected_formula_names()
if not selected:
QMessageBox.warning(self, "输入错误", "请至少选择一个公式!")
return
if self._waterindex_csv and not Path(self._waterindex_csv).exists():
QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}")
return
config = {'step10': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step10',
'config': config,
})
def run_step(self):
"""独立运行步骤10旧版 parent 链上溯方式,保留兼容)。"""
bsq_path = self.bsq_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip()
output_dir = self.output_dir.get_path().strip()

View File

@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
class Step11MapBatchThread(QThread):
@ -63,19 +58,19 @@ class Step11MapBatchThread(QThread):
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
from src.core.steps.mapping_step import MappingStep
n = len(self.csv_paths)
for i, csv_p in enumerate(self.csv_paths):
self.progress.emit(i + 1, n)
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
kw = {**self.step10_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
kw = {**self.step10_kwargs, "prediction_csv_path": csv_p}
kw.pop("skip_dependency_check", None)
if self.output_dir_optional:
stem = Path(csv_p).stem
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step10_map(**kw)
MappingStep.generate_distribution_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")

View File

@ -32,12 +32,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
@ -208,7 +203,7 @@ class VisualizationWorkerThread(QThread):
{"task": "statistics", "output_paths": output_paths}
)
elif self.task == "scatter":
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
from src.core.visualization.scatter_plot import generate_model_scatter_plots
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
models_dir = (self.extra.get("models_dir") or "").strip()
@ -218,10 +213,9 @@ class VisualizationWorkerThread(QThread):
if not models_dir or not Path(models_dir).is_dir():
self.failed.emit("模型目录无效或不存在请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
return
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=training_csv_path,
scatter_paths = generate_model_scatter_plots(
models_dir=models_dir,
training_csv_path=training_csv_path,
)
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
elif self.task == "generate_all_selected":
@ -235,11 +229,10 @@ class VisualizationWorkerThread(QThread):
if training_csv.is_file():
models_dir = wp / "7_Supervised_Model_Training"
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=str(training_csv),
from src.core.visualization.scatter_plot import generate_model_scatter_plots
scatter_paths = generate_model_scatter_plots(
models_dir=str(models_dir),
training_csv_path=str(training_csv),
)
count = len(scatter_paths) if scatter_paths else 0
parts.append(f"散点图: {count}")

View File

@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
class Step14BatchThread(QThread):
@ -63,19 +58,19 @@ class Step14BatchThread(QThread):
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
from src.core.steps.mapping_step import MappingStep
n = len(self.csv_paths)
for i, csv_p in enumerate(self.csv_paths):
self.progress.emit(i + 1, n)
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
kw = {**self.step14_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
kw = {**self.step14_kwargs, "prediction_csv_path": csv_p}
kw.pop("skip_dependency_check", None)
if self.output_dir_optional:
stem = Path(csv_p).stem
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step10_map(**kw)
MappingStep.generate_distribution_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")

View File

@ -144,7 +144,7 @@ class Step1Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
# 连接信号
@ -257,8 +257,40 @@ class Step1Panel(QWidget):
self.update_ui_state()
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor
替代旧有的 parent 链上溯查找 run_single_step 的紧耦合方式。
PipelineExecutor 在 __init__ 中订阅 RequestRunSingleStep 事件,
收到后调用 run_single_step(step_name, config) 统一处理预检/工作目录/执行。
"""
from src.gui.core.event_bus import global_event_bus
# 验证输入(与旧 run_step 逻辑一致)
if self.use_ndwi_radio.isChecked():
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择参考影像文件!")
return
else:
mask_path = self.mask_file.get_path()
if not mask_path:
QMessageBox.warning(self, "输入错误", "请选择掩膜文件!")
return
if mask_path.lower().endswith('.shp'):
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "当使用shp文件时需要提供参考影像用于栅格化")
return
config = {'step1': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step1',
'config': config,
})
def run_step(self):
"""独立运行步骤1"""
"""独立运行步骤1(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
if self.use_ndwi_radio.isChecked():
# NDWI模式需要影像文件

View File

@ -108,7 +108,7 @@ class Step2Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -203,8 +203,23 @@ class Step2Panel(QWidget):
# 没有工作目录时,清空输出路径
self.output_file.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
config = {'step2': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step2',
'config': config,
})
def run_step(self):
"""独立运行步骤2"""
"""独立运行步骤2(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
img_path = self.img_file.get_path()
if not img_path:

View File

@ -228,7 +228,7 @@ class Step3Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -433,8 +433,34 @@ class Step3Panel(QWidget):
if 'sugar_bounds' in config:
self.sugar_bounds.setText(str(config['sugar_bounds']))
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
if self.enable_checkbox.isChecked():
water_mask_path = self.water_mask_file.get_path()
if not water_mask_path:
QMessageBox.warning(
self,
"输入错误",
"独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n"
"请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif或水域矢量边界.shp\n"
"若刚跑过完整流程可使用步骤1生成的水域掩膜文件。",
)
return
config = {'step3': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step3',
'config': config,
})
def run_step(self):
"""独立运行步骤3"""
"""独立运行步骤3(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
img_path = self.img_file.get_path()
if not img_path:

View File

@ -91,7 +91,7 @@ class Step4SamplingPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
# 交互式预览按钮
@ -228,8 +228,23 @@ class Step4SamplingPanel(QWidget):
# 4. 同步更新预览按钮状态(路径可能已自动填充)
self._check_csv_exists()
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
config = {'step4_sampling': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step4_sampling',
'config': config,
})
def run_step(self):
"""独立运行步骤4"""
"""独立运行步骤4(旧版 parent 链上溯方式,保留兼容)。"""
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")

View File

@ -95,7 +95,7 @@ class Step5CleanPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -142,8 +142,23 @@ class Step5CleanPanel(QWidget):
else:
self.output_file.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return
config = {'step5_clean': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step5_clean',
'config': config,
})
def run_step(self):
"""独立运行步骤5"""
"""独立运行步骤5(旧版 parent 链上溯方式,保留兼容)。"""
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")

View File

@ -106,7 +106,7 @@ class Step6FeaturePanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -258,8 +258,35 @@ class Step6FeaturePanel(QWidget):
if not existing_csv or not existing_csv.strip():
self.csv_file.set_path(step5_clean_output_path)
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件")
return
if not self.glint_mask_file.get_path():
QMessageBox.warning(
self,
"输入错误",
"独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
"请提供与去耀斑影像对应的耀斑二值掩膜一般为步骤2输出的 severe_glint_area.dat",
)
return
config = {'step6_feature': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step6_feature',
'config': config,
})
def run_step(self):
"""独立运行步骤6"""
"""独立运行步骤6(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path()

View File

@ -119,7 +119,7 @@ class Step8MlTrainPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -398,8 +398,23 @@ class Step8MlTrainPanel(QWidget):
else:
self.output_path.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
training_csv_path = self.training_csv_file.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")
return
config = {'step8_ml_train': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step8_ml_train',
'config': config,
})
def run_step(self):
"""独立运行步骤8"""
"""独立运行步骤8(旧版 parent 链上溯方式,保留兼容)。"""
training_csv_path = self.training_csv_file.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")

View File

@ -109,7 +109,7 @@ class Step8QAAPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("执行 QAA 反演")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -212,8 +212,23 @@ class Step8QAAPanel(QWidget):
else:
self.output_path.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
return
config = {'step8_qaa': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step8_qaa',
'config': config,
})
def run_step(self):
"""独立运行 QAA 反演"""
"""独立运行 QAA 反演(旧版 parent 链上溯方式,保留兼容)。"""
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")

View File

@ -175,7 +175,7 @@ class Step9MlPredictPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -414,8 +414,57 @@ class Step9MlPredictPanel(QWidget):
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
return
# 外部模型优先:用户选择了"导入本地预训练模型"
if self.use_external_model.isChecked():
if not self.external_models_dict:
QMessageBox.warning(
self,
"模型未加载",
"请先点击「浏览...」按钮选择模型母文件夹!",
)
return
checked_dict = self._get_checked_models_dict()
if not checked_dict:
QMessageBox.warning(
self,
"未选择模型",
"请至少勾选一个模型参与预测!",
)
return
config = {
'step9_ml_predict': self.get_config(),
'_external_models_dict': checked_dict,
'_external_model_dir': self.external_model_dir,
}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step9_ml_predict',
'config': config,
})
return
# 默认流程:使用模型目录
models_dir = self.models_dir_file.get_path()
if not models_dir:
QMessageBox.warning(self, "输入错误", "请选择模型目录!")
return
config = {'step9_ml_predict': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step9_ml_predict',
'config': config,
})
def run_step(self):
"""独立运行步骤11"""
"""独立运行步骤11(旧版 parent 链上溯方式,保留兼容)。"""
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")

View File

@ -1244,7 +1244,7 @@ class WaterQualityGUI(QMainWindow):
if not PIPELINE_AVAILABLE:
QMessageBox.critical(
self, "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
@ -1400,7 +1400,7 @@ class WaterQualityGUI(QMainWindow):
if not PIPELINE_AVAILABLE:
QMessageBox.critical(
self, "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return