feat(gui): 全流程面板合并 + 一键式运行 GUI 入口集成

This commit is contained in:
DXC
2026-06-09 11:30:42 +08:00
parent aefc9d5aac
commit 28394f2eda
20 changed files with 2843 additions and 2432 deletions

View File

@ -119,19 +119,22 @@ from src.gui.panels.step2_panel import Step2Panel
from src.gui.panels.step3_panel import Step3Panel
from src.gui.panels.step4_panel import Step4Panel
from src.gui.panels.step5_panel import Step5Panel
from src.gui.panels.step5_5_panel import Step5_5Panel
from src.gui.panels.step6_panel import Step6Panel
from src.gui.panels.step6_5_panel import Step6_5Panel
from src.gui.panels.step6_75_panel import Step6_75Panel
from src.gui.panels.step7_panel import Step7Panel
from src.gui.panels.step8_panel import Step8Panel
from src.gui.panels.step8_5_panel import Step8_5Panel
from src.gui.panels.step8_75_panel import Step8_75Panel
from src.gui.panels.step9_panel import Step9Panel
from src.gui.panels.step8_panel import Step8Panel # was step5_5_panel
from src.gui.panels.step7_panel import Step7Panel # was step6_panel
from src.gui.panels.step8_non_empirical_panel import Step8NonEmpiricalPanel # was step6_5_panel
from src.gui.panels.step9_panel import Step9Panel # was step6_75_panel
from src.gui.panels.step10_panel import Step10Panel # was step7_panel
from src.gui.panels.step11_ml_panel import Step11MlPanel # ML prediction (step11_ml)
from src.gui.panels.step11_panel import Step11Panel # was step8_5_panel
from src.gui.panels.step12_panel import Step12Panel # was step8_75_panel
from src.gui.panels.step14_panel import Step14Panel # was step9_panel
from src.gui.dialogs import BandConfirmDialog, AISettingsDialog
from src.gui.panels.visualization_panel import VisualizationPanel
from src.gui.panels.report_generation_panel import ReportGenerationPanel
# Pipeline 核心异常(用于预检弹窗)
from src.core.pipeline.runner import PipelineHalt
# Matplotlib相关导入 (推迟并加入底层防爆保护)
import matplotlib
try:
@ -152,6 +155,9 @@ from src.gui.core.worker_thread import (
check_pipeline_dependencies,
diagnose_pipeline_import_error,
)
# 预检交互对话框
from src.gui.core.preflight_dialog import PreflightDialog
from src.gui.core.pipeline_mode_dialog import PipelineModeDialog
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
@ -1384,31 +1390,31 @@ class WaterQualityGUI(QMainWindow):
'step5': {
'training_spectra': '5_training_spectra/training_spectra.csv'
},
'step5_5': {
'step8': {
'water_indices': '6_water_quality_indices/water_quality_indices.csv'
},
'step6': {
'step7': {
'models': '7_Supervised_Model_Training/' # 目录,包含各参数子目录
},
'step6_5': {
'step8_non_empirical_modeling': {
'regression_models': '8_Regression_Modeling/' # 目录,包含各参数子目录
},
'step6_75': {
'step9': {
'custom_regression_models': '9_Custom_Regression_Modeling/' # 目录
},
'step7': {
'step10': {
'sampling_points': '10_sampling/sampling_spectra.csv'
},
'step8': {
'step11_ml': {
'predictions': '11_12_13_predictions/Machine_Learning_Prediction/' # 目录,包含机器学习预测结果
},
'step8_5': {
'step11': {
'regression_predictions': '11_12_13_predictions/Non_Empirical_Prediction/' # 目录,包含非经验模型预测结果
},
'step8_75': {
'step12': {
'custom_predictions': '11_12_13_predictions/Custom_Regression_Prediction/' # 目录,包含自定义回归预测结果
},
'step9': {
'step14': {
'distribution_maps': '14_visualization/' # 目录,包含专题图
}
}
@ -1432,37 +1438,37 @@ class WaterQualityGUI(QMainWindow):
'boundary_mask_path': ('step1', 'water_mask', 'boundary_mask_file'), # 步骤5可选水体掩膜
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤5可选耀斑掩膜
},
'step5_5': {
'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤5.5需要步骤5输出的训练光谱
},
'step6': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤6需要训练光谱数据
},
'step6_5': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤6.5需要训练光谱数据
},
'step6_75': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤6.75需要训练光谱数据
'step8': {
'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤8需要步骤5输出的训练光谱
},
'step7': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'), # 步骤7需要去耀斑影像
'water_mask_path': ('step1', 'water_mask', 'water_mask_file'), # 步骤7需要水域掩膜
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤7可选耀斑掩膜
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤7需要训练光谱数据
},
'step8': {
'sampling_csv_path': ('step7', 'sampling_points', 'sampling_csv_file'), # 步骤8需要采样点
'models_dir': ('step6', 'models', 'models_dir_file') # 步骤8需要训练好的模型
},
'step8_5': {
'sampling_csv_path': ('step7', 'sampling_points', 'sampling_csv_file'), # 步骤8.5需要采样点
'models_dir': ('step6_5', 'regression_models', 'models_dir') # 步骤8.5需要回归模型
},
'step8_75': {
'sampling_csv_path': ('step7', 'sampling_points', 'sampling_csv_file'), # 步骤8.75需要采样点
'models_dir': ('step6_75', 'custom_regression_models', 'models_dir') # 步骤8.75需要自定义回归模型
'step8_non_empirical_modeling': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤8非经验建模需要训练光谱数据
},
'step9': {
'prediction_csv_path': ('step8', 'predictions', 'prediction_csv_file') # 步骤9需要预测结果CSV
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤9需要训练光谱数据
},
'step10': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'), # 步骤10需要去耀斑影像
'water_mask_path': ('step1', 'water_mask', 'water_mask_file'), # 步骤10需要水域掩膜
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤10可选耀斑掩膜
},
'step11_ml': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤11ML需要采样点
'models_dir': ('step7', 'models', 'models_dir_file') # 步骤11ML需要训练好的模型
},
'step11': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤11需要采样点
'models_dir': ('step8_non_empirical_modeling', 'regression_models', 'models_dir') # 步骤11需要回归模型
},
'step12': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤12需要采样点
'models_dir': ('step9', 'custom_regression_models', 'models_dir') # 步骤12需要自定义回归模型
},
'step14': {
'prediction_csv_path': ('step11_ml', 'predictions', 'prediction_csv_file') # 步骤14需要预测结果CSV
}
}
@ -1545,7 +1551,7 @@ class WaterQualityGUI(QMainWindow):
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("MegaCube-Water Quality V1.1")
self.setWindowTitle("MegaCube-Water Quality V1.2")
# 获取屏幕可用区域(排除任务栏)
screen_geometry = QApplication.primaryScreen().availableGeometry()
@ -1730,7 +1736,7 @@ class WaterQualityGUI(QMainWindow):
def create_banner_widget(self):
"""创建横幅区域 - 支持自适应等比缩放"""
# 横幅标题文字(方便后续直接修改版本号)
self._APP_TITLE = "MegaCube-Water Quality V1.1"
self._APP_TITLE = "MegaCube-Water Quality V1.2"
# 创建横幅容器
banner_widget = QWidget()
@ -1844,19 +1850,19 @@ class WaterQualityGUI(QMainWindow):
"阶段二:样本数据准备 ": [
("step4", "4. 数据标准化处理"),
("step5", "5. 光谱特征提取"),
("step5_5", "6. 水质参数指数计算"),
("step8", "6. 水质参数指数计算"),
],
"阶段三:模型构建与训练": [
("step6", "7. 机器学习模型训练"),
("step6_5", "8. 回归模型训练"),
("step6_75", "9. 自定义回归模型训练"),
("step7", "7. 机器学习模型训练"),
("step8_non_empirical_modeling", "8. 回归模型训练"),
("step9", "9. 自定义回归模型训练"),
],
"阶段四:预测与成果输出 ": [
("step7", "10. 采样点布设"),
("step8", "11. 机器学习预测"),
("step8_5", "12. 回归预测"),
("step8_75", "13. 自定义回归预测"),
("step9", "14. 专题图生成"),
("step10", "10. 采样点布设"),
("step11_ml", "11. 机器学习预测"),
("step11", "12. 回归预测"),
("step12", "13. 自定义回归预测"),
("step14", "14. 专题图生成"),
("step9_viz", "15. 可视化分析"),
("step_report", "16. 分析报告生成"),
]
@ -1878,7 +1884,7 @@ class WaterQualityGUI(QMainWindow):
self.step_list.addItem(stage_item)
# 添加该阶段的所有步骤
HIDDEN_STEP_IDS = {"step6_5", "step6_75", "step8_5", "step8_75"}
HIDDEN_STEP_IDS = {"step8_non_empirical_modeling", "step9", "step11", "step12"}
for step_id, step_display in steps:
if step_id in HIDDEN_STEP_IDS:
continue
@ -1958,36 +1964,36 @@ class WaterQualityGUI(QMainWindow):
self.step5_panel = Step5Panel()
self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建")
self.step5_5_panel = Step5_5Panel()
self.step_stack.addTab(self.create_scroll_area(self.step5_5_panel), QIcon(self.get_icon_path("5.png")), "水质指数")
self.step6_panel = Step6Panel()
self.step_stack.addTab(self.create_scroll_area(self.step6_panel), QIcon(self.get_icon_path("6.png")), "监督建模")
self.step6_5_panel = Step6_5Panel()
self.step_stack.addTab(self.create_scroll_area(self.step6_5_panel), QIcon(self.get_icon_path("6.png")), "回归建模")
self.step_stack.tabBar().setTabVisible(7, False) # 隐藏回归建模 Tab
self.step6_75_panel = Step6_75Panel()
self.step_stack.addTab(self.create_scroll_area(self.step6_75_panel), QIcon(self.get_icon_path("6.png")), "自定义回归建模")
self.step_stack.tabBar().setTabVisible(8, False) # 隐藏自定义回归建模 Tab
self.step8_panel = Step8Panel()
self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("5.png")), "水质指数")
self.step7_panel = Step7Panel()
self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "采样点布设")
self.step8_panel = Step8Panel()
self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("8.png")), "监督预测")
self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("6.png")), "监督建模")
self.step8_5_panel = Step8_5Panel()
self.step_stack.addTab(self.create_scroll_area(self.step8_5_panel), QIcon(self.get_icon_path("8.png")), "回归预测")
self.step_stack.tabBar().setTabVisible(11, False) # 隐藏回归预测 Tab
self.step8_75_panel = Step8_75Panel()
self.step_stack.addTab(self.create_scroll_area(self.step8_75_panel), QIcon(self.get_icon_path("8.png")), "自定义回归预测")
self.step_stack.tabBar().setTabVisible(12, False) # 隐藏自定义回归预测 Tab
self.step8_non_empirical_panel = Step8NonEmpiricalPanel()
self.step_stack.addTab(self.create_scroll_area(self.step8_non_empirical_panel), QIcon(self.get_icon_path("6.png")), "回归建模")
self.step_stack.tabBar().setTabVisible(7, False) # 隐藏回归建模 Tab
self.step9_panel = Step9Panel()
self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("6.png")), "自定义回归建模")
self.step_stack.tabBar().setTabVisible(8, False) # 隐藏自定义回归建模 Tab
self.step10_panel = Step10Panel()
self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设")
self.step11_ml_panel = Step11MlPanel() # ML prediction panel (step11_ml)
self.step_stack.addTab(self.create_scroll_area(self.step11_ml_panel), QIcon(self.get_icon_path("8.png")), "监督预测")
self.step11_panel = Step11Panel()
self.step_stack.addTab(self.create_scroll_area(self.step11_panel), QIcon(self.get_icon_path("8.png")), "回归预测")
self.step_stack.tabBar().setTabVisible(11, False) # 隐藏回归预测 Tab
self.step12_panel = Step12Panel()
self.step_stack.addTab(self.create_scroll_area(self.step12_panel), QIcon(self.get_icon_path("8.png")), "自定义回归预测")
self.step_stack.tabBar().setTabVisible(12, False) # 隐藏自定义回归预测 Tab
self.step14_panel = Step14Panel()
self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
self.viz_panel = VisualizationPanel()
self.step_stack.addTab(self.create_scroll_area(self.viz_panel), QIcon(self.get_icon_path("9.png")), "可视化")
@ -2137,15 +2143,15 @@ class WaterQualityGUI(QMainWindow):
'step3': 2,
'step4': 3,
'step5': 4,
'step5_5': 5,
'step6': 6,
'step6_5': 7,
'step6_75': 8,
'step7': 9,
'step8': 10,
'step8_5': 11,
'step8_75': 12,
'step9': 13,
'step8': 5,
'step7': 6,
'step8_non_empirical_modeling': 7,
'step9': 8,
'step10': 9,
'step11_ml': 10,
'step11': 11,
'step12': 12,
'step14': 13,
'step9_viz': 14,
'step_report': 15,
}
@ -2168,15 +2174,15 @@ class WaterQualityGUI(QMainWindow):
2: 'step3',
3: 'step4',
4: 'step5',
5: 'step5_5',
6: 'step6',
7: 'step6_5',
8: 'step6_75',
9: 'step7',
10: 'step8',
11: 'step8_5',
12: 'step8_75',
13: 'step9',
5: 'step8',
6: 'step7',
7: 'step8_non_empirical_modeling',
8: 'step9',
9: 'step10',
10: 'step11_ml',
11: 'step11',
12: 'step12',
13: 'step14',
14: 'step9_viz',
15: 'step_report',
}
@ -2213,41 +2219,41 @@ class WaterQualityGUI(QMainWindow):
elif index == 4:
self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step5_5 切换时自动填充输出路径
# Step8水质指数切换时自动填充输出路径
elif index == 5:
self.step5_5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
self.step8_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step6 切换时自动填充训练数据和输出路径
# Step7监督建模切换时自动填充训练数据和输出路径
elif index == 6:
self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step6.5(非经验回归建模)切换时自动填充训练数据和模型目录
elif index == 7:
self.step6_5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step6.75(自定义回归建模)切换时自动填充训练数据和模型目录
elif index == 8:
self.step6_75_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step7采样点布设切换时自动填充掩膜和输出路径
elif index == 9:
self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step8非经验建模切换时自动填充训练数据和模型目录
elif index == 7:
self.step8_non_empirical_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step9自定义回归建模切换时自动填充训练数据和模型目录
elif index == 8:
self.step9_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step10采样点布设切换时自动填充掩膜和输出路径
elif index == 9:
self.step10_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step8机器学习预测切换时自动填充采样光谱和模型目录
elif index == 10:
self.step8_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
self.step11_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step8.5(非经验模型预测)切换时自动填充采样光谱和回归模型目录
# Step11回归预测)切换时自动填充采样光谱和回归模型目录
elif index == 11:
self.step8_5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
self.step11_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step8.75(自定义回归预测)切换时自动填充采样光谱和自定义回归模型目录
# Step12(自定义回归预测)切换时自动填充采样光谱和自定义回归模型目录
elif index == 12:
self.step8_75_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
self.step12_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step9(专题图生成)切换时自动填充预测结果目录
# Step14(专题图生成)切换时自动填充预测结果目录
elif index == 13:
self.step9_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 可视化分析面板切换时自动推断图像目录并加载目录树
elif index == 14:
@ -2294,22 +2300,22 @@ class WaterQualityGUI(QMainWindow):
self.step4_panel.set_config(config['step4'])
if 'step5' in config:
self.step5_panel.set_config(config['step5'])
if 'step5_5' in config:
self.step5_5_panel.set_config(config['step5_5'])
if 'step6' in config:
self.step6_panel.set_config(config['step6'])
if 'step6_5' in config:
self.step6_5_panel.set_config(config['step6_5'])
if 'step6_75' in config:
self.step6_75_panel.set_config(config['step6_75'])
if 'step7' in config:
self.step7_panel.set_config(config['step7'])
if 'step8' in config:
self.step8_panel.set_config(config['step8'])
if 'step8_5' in config:
self.step8_5_panel.set_config(config['step8_5'])
if 'step7' in config:
self.step7_panel.set_config(config['step7'])
if 'step8_non_empirical_modeling' in config:
self.step8_non_empirical_panel.set_config(config['step8_non_empirical_modeling'])
if 'step9' in config:
self.step9_panel.set_config(config['step9'])
if 'step10' in config:
self.step10_panel.set_config(config['step10'])
if 'step11_ml' in config:
self.step11_ml_panel.set_config(config['step11_ml'])
if 'step11' in config:
self.step11_panel.set_config(config['step11'])
if 'step14' in config:
self.step14_panel.set_config(config['step14'])
if 'visualization' in config:
self.viz_panel.set_config(config['visualization'])
if 'report_generation' in config:
@ -2352,14 +2358,14 @@ class WaterQualityGUI(QMainWindow):
'step3': self.step3_panel.get_config(),
'step4': self.step4_panel.get_config(),
'step5': self.step5_panel.get_config(),
'step5_5': self.step5_5_panel.get_config(),
'step6': self.step6_panel.get_config(),
'step6_5': self.step6_5_panel.get_config(),
'step6_75': self.step6_75_panel.get_config(),
'step7': self.step7_panel.get_config(),
'step8': self.step8_panel.get_config(),
'step8_5': self.step8_5_panel.get_config(),
'step7': self.step7_panel.get_config(),
'step8_non_empirical_modeling': self.step8_non_empirical_panel.get_config(),
'step9': self.step9_panel.get_config(),
'step10': self.step10_panel.get_config(),
'step11_ml': self.step11_ml_panel.get_config(),
'step11': self.step11_panel.get_config(),
'step14': self.step14_panel.get_config(),
'visualization': self.viz_panel.get_config(),
'report_generation': self.report_panel.get_config(),
}
@ -2410,15 +2416,15 @@ class WaterQualityGUI(QMainWindow):
'step3': self.step3_panel,
'step4': self.step4_panel,
'step5': self.step5_panel,
'step5_5': self.step5_5_panel,
'step6': self.step6_panel,
'step6_5': self.step6_5_panel,
'step6_75': self.step6_75_panel,
'step7': self.step7_panel,
'step8': self.step8_panel,
'step8_5': self.step8_5_panel,
'step8_75': self.step8_75_panel,
'step7': self.step7_panel,
'step8_non_empirical_modeling': self.step8_non_empirical_panel,
'step9': self.step9_panel,
'step10': self.step10_panel,
'step11_ml': self.step11_ml_panel,
'step11': self.step11_panel,
'step12': self.step12_panel,
'step14': self.step14_panel,
}
return panel_map.get(step_id)
@ -2426,15 +2432,38 @@ class WaterQualityGUI(QMainWindow):
"""查找指定步骤的输出文件"""
if step_id not in self.step_default_outputs:
return None
step_outputs = self.step_default_outputs[step_id]
# ★ 掩膜类型列表:这些类型只接受科学数据格式
mask_types = {'water_mask', 'glint_mask', 'boundary_mask'}
# ★ 白名单机制:只允许 .dat .tif .tiff .shp拒绝其他一切格式
scientific_extensions = {'.dat', '.tif', '.tiff', '.shp'}
# ★ 临时文件关键词黑名单
tmp_keywords = ('__tmp', '_tmp')
def _is_scientific_mask(path_str):
"""白名单判断:只有 .dat .tif .tiff .shp 才算科学数据格式"""
p = Path(path_str)
name_lower = str(path_str).lower()
# 拒绝临时文件
if any(kw in name_lower for kw in tmp_keywords):
return False
# 白名单校验
return p.suffix.lower() in scientific_extensions
# 特殊处理从step_outputs记录中查找实际输出路径
if step_id in self.step_outputs:
actual_outputs = self.step_outputs[step_id]
if output_type in actual_outputs:
return actual_outputs[output_type]
candidate = actual_outputs[output_type]
# ★ 掩膜类型白名单二次校验:不在白名单内的一律拒绝
if output_type in mask_types and not _is_scientific_mask(candidate):
# 非科学格式被拒绝,不使用 step_outputs 中的值
pass
else:
return candidate
# 根据输出类型查找对应的文件
if output_type == 'water_mask':
# 水域掩膜优先查找NDWI生成的其次是shp生成的
@ -2485,19 +2514,19 @@ class WaterQualityGUI(QMainWindow):
# 扫描各个子目录
subdirs = {
'1_water_mask': 'step1',
'2_glint': 'step2',
'2_glint': 'step2',
'3_deglint': 'step3',
'4_processed_data': 'step4',
'5_training_spectra': 'step5',
'6_water_quality_indices': 'step5_5',
'7_Supervised_Model_Training': 'step6',
'8_Regression_Modeling': 'step6_5',
'9_Custom_Regression_Modeling': 'step6_75',
'10_sampling': 'step7',
'11_12_13_predictions/Machine_Learning_Prediction': 'step8',
'11_12_13_predictions/Non_Empirical_Prediction': 'step8_5',
'11_12_13_predictions/Custom_Regression_Prediction': 'step8_75',
'14_visualization': 'step9'
'6_water_quality_indices': 'step8',
'7_Supervised_Model_Training': 'step7',
'8_Regression_Modeling': 'step8_non_empirical_modeling',
'9_Custom_Regression_Modeling': 'step9',
'10_sampling': 'step10',
'11_12_13_predictions/Machine_Learning_Prediction': 'step11_ml',
'11_12_13_predictions/Non_Empirical_Prediction': 'step11',
'11_12_13_predictions/Custom_Regression_Prediction': 'step12',
'14_visualization': 'step14'
}
for subdir, step_ids in subdirs.items():
@ -2517,23 +2546,37 @@ class WaterQualityGUI(QMainWindow):
for step_id in step_ids:
if step_id not in discovered_outputs:
discovered_outputs[step_id] = {}
# ★ 掩膜文件白名单过滤:只有 .dat .tif .tiff .shp 才通过,拒绝 .hdr .xml .png 等
scientific_extensions = {'.dat', '.tif', '.tiff', '.shp'}
tmp_keywords = ('__tmp', '_tmp')
def _is_scientific_mask(path_str):
"""白名单判断:拒绝 .hdr .xml 临时文件等,只接受科学数据格式"""
p = Path(path_str)
name_lower = str(path_str).lower()
if any(kw in name_lower for kw in tmp_keywords):
return False
return p.suffix.lower() in scientific_extensions
# 匹配不同的文件类型
if 'water_mask' in file_name and step_id == 'step1':
discovered_outputs[step_id]['water_mask'] = str(file_path)
if _is_scientific_mask(file_path):
discovered_outputs[step_id]['water_mask'] = str(file_path)
elif 'glint' in file_name and 'mask' in file_name and step_id == 'step2':
discovered_outputs[step_id]['glint_mask'] = str(file_path)
if _is_scientific_mask(file_path):
discovered_outputs[step_id]['glint_mask'] = str(file_path)
elif 'deglint' in file_name and step_id == 'step3':
discovered_outputs[step_id]['deglint_image'] = str(file_path)
elif 'processed_data' in file_name and step_id == 'step4':
discovered_outputs[step_id]['processed_data'] = str(file_path)
elif 'training_spectra' in file_name and step_id == 'step5':
discovered_outputs[step_id]['training_spectra'] = str(file_path)
elif 'water_quality_indices' in file_name and step_id == 'step5_5':
elif 'water_quality_indices' in file_name and step_id == 'step8':
discovered_outputs[step_id]['water_indices'] = str(file_path)
elif 'sampling_spectra' in file_name and step_id == 'step7':
elif 'sampling_spectra' in file_name and step_id == 'step10':
discovered_outputs[step_id]['sampling_points'] = str(file_path)
elif file_name.endswith('.csv') and step_id in ['step8', 'step8_5', 'step8_75']:
elif file_name.endswith('.csv') and step_id in ['step11_ml', 'step11', 'step12']:
discovered_outputs[step_id]['predictions'] = str(file_path)
# 更新内部记录
@ -2556,8 +2599,8 @@ class WaterQualityGUI(QMainWindow):
# 首先扫描工作目录发现已有的输出文件
self.scan_work_directory_for_files(work_path)
step_order = ['step2', 'step3', 'step4', 'step5', 'step5_5', 'step6', 'step6_5', 'step6_75',
'step7', 'step8', 'step8_5', 'step8_75', 'step9']
step_order = ['step2', 'step3', 'step4', 'step5', 'step8', 'step7', 'step8_non_empirical_modeling', 'step9',
'step10', 'step11_ml', 'step11', 'step12', 'step14']
filled_count = 0
for step_id in step_order:
@ -2579,15 +2622,15 @@ class WaterQualityGUI(QMainWindow):
('step2', self.step2_panel),
('step3', self.step3_panel),
('step5', self.step5_panel),
('step5_5', self.step5_5_panel),
('step6', self.step6_panel),
('step6_5', self.step6_5_panel),
('step6_75', self.step6_75_panel),
('step7', self.step7_panel),
('step8', self.step8_panel),
('step8_5', self.step8_5_panel),
('step8_75', self.step8_75_panel),
('step9', self.step9_panel)
('step7', self.step7_panel),
('step8_non_empirical_modeling', self.step8_non_empirical_panel),
('step9', self.step9_panel),
('step10', self.step10_panel),
('step11_ml', self.step11_ml_panel),
('step11', self.step11_panel),
('step12', self.step12_panel),
('step14', self.step14_panel)
]
for step_id, panel in panels_with_dependencies:
@ -2735,7 +2778,7 @@ class WaterQualityGUI(QMainWindow):
"""显示关于对话框"""
QMessageBox.about(
self, "关于",
"MegaCube-Water Quality V1.1\n\n"
"MegaCube-Water Quality V1.2\n\n"
"一个完整的水质参数反演工作流程工具\n\n"
"功能包括:\n"
"- 水域掩膜生成\n"
@ -2858,6 +2901,41 @@ class WaterQualityGUI(QMainWindow):
return True
# ------------------------------------------------------------------
# ★ 全流程模式动态裁剪
# ------------------------------------------------------------------
def _prune_config_for_prediction_mode(self, config: dict) -> dict:
"""Prediction-only 模式:禁用训练相关步骤,保留预测和成图步骤。
被禁用的 step dict 中统一写入 'enabled': False
这些配置最终传给 PipelineRunnerRunner 会跳过它们。
同时,被跳过的步骤的 required_input_files 在 build_missing_items
中不会被检查,从而自然规避了"CSV 缺失"等训练模式下的误报。
Args:
config: 完整配置字典(来自 get_current_config
Returns:
裁剪后的 config深拷贝原 config 不被修改)
"""
cfg = copy.deepcopy(config)
# 在每个训练相关步骤的 dict 中写入 enabled=False
training_steps = [
"step4", # CSV 实测数据清洗
"step5", # 实测点光谱提取(→ training_csv_path
"step7", # ML 监督建模
"step8", # 水质指数计算(辅助训练)
"step8_non_empirical_modeling", # 非经验回归建模
"step9", # 自定义回归建模
]
for step_id in training_steps:
step_cfg = cfg.setdefault(step_id, {})
step_cfg["enabled"] = False
return cfg
def run_full_pipeline(self):
"""运行完整流程"""
if not PIPELINE_AVAILABLE:
@ -2867,8 +2945,14 @@ class WaterQualityGUI(QMainWindow):
)
return
# ── 0) 强制获取 work_dir禁止依赖外部或全局变量 ──
work_dir = getattr(self, 'work_dir', None)
if not work_dir:
QMessageBox.warning(self, "警告", "未选择工作目录,请先设置工作目录。")
return
# ── 1) 运行前智能预检与自动回填(硬盘已有产物自动跳过) ──
work_path = Path(getattr(self, 'work_dir', './work_dir'))
work_path = Path(work_dir)
self.log_message("正在进行运行前环境预检与自动扫描...", "info")
self.scan_work_directory_for_files(work_path)
self.auto_populate_all_steps()
@ -2878,31 +2962,52 @@ class WaterQualityGUI(QMainWindow):
if not self._precheck_step3_bands():
return # 用户点"取消运行"
# ── 1.6) ★ 全流程模式选择弹窗 ──
mode_dlg = PipelineModeDialog(main_window=self, parent=self)
if mode_dlg.exec() != QDialog.Accepted:
return # 用户点"取消"
selected_mode = mode_dlg.selected_mode
self.log_message(f"[模式选择] 选定模式: {'训练新模型' if selected_mode == 'training' else '使用已有模型直接预测'}", "info")
# ── 2) 刷新配置(拿到自动填充后的"满血版" config ──
config = self.get_current_config()
# ── 3) 根基数据校验step1.img_path参考影像 ──
if not config['step1'].get('img_path'):
QMessageBox.warning(self, "警告", "缺失核心数据:请先在步骤 1 中上传【参考影像】!")
for i in range(self.step_list.count()):
item = self.step_list.item(i)
if item.data(Qt.UserRole) == 'step1':
self.step_list.setCurrentRow(i)
break
return
# ── 2.1) ★ 根据模式动态裁剪配置 ──
if selected_mode == "prediction_only":
config = self._prune_config_for_prediction_mode(config)
self.log_message("[模式选择] 已裁剪训练相关步骤step4/5/7/8进入仅预测模式", "info")
# ── 4) 软提示csv_path 缺失 → 模型训练步骤会被静默跳过(不阻断) ──
csv_path = config.get('step4', {}).get('csv_path') or config.get('step5', {}).get('csv_path')
if not csv_path:
QMessageBox.information(
self,
"提示:模型训练将被跳过",
"未检测到实测水质数据 (CSV)。\n"
"流程将自动跳过模型训练(步骤 4-6仅执行预测与制图。\n"
"如果需要训练新模型,请先在步骤 4 中上传水质数据。",
)
# ── 3) ★ 一次性全预检 + 用户交互式决策 ──
missing_items = PreflightDialog.build_missing_items(config)
if missing_items:
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)
QMessageBox.critical(
self, "预检失败(阻断性错误)",
f"以下为阻断性缺失,流程无法启动:\n\n{lines}\n\n"
"请填写后重新运行。"
)
return
dialog = PreflightDialog(missing_items, parent=self)
if dialog.exec() != QDialog.Accepted:
return
result = dialog.get_result()
if result is None:
return
action, *payload = result
if action == "fill":
_, step_id, tab_index = result
self.step_stack.setCurrentIndex(tab_index)
self.log_message(f"[预检] 用户选择填写 {step_id},已切换到对应面板。", "info")
return
skip_list: List[str] = payload[0] if payload else []
if skip_list:
self.log_message(f"[预检] 用户强制跳过 {len(skip_list)} 个步骤: {skip_list}", "info")
else:
skip_list = []
# 确认执行
# ── 4) 确认执行
reply = QMessageBox.question(
self, "确认",
"是否开始执行完整流程?\n\n这可能需要较长时间,请确保配置正确。",
@ -2913,19 +3018,18 @@ class WaterQualityGUI(QMainWindow):
return
# 创建pipeline实例
work_dir = getattr(self, 'work_dir', './work_dir')
self.log_message(f"初始化pipeline工作目录: {work_dir}", "info")
# 准备实际运行配置(排除未启用的步骤)
worker_config = copy.deepcopy(config)
step5_5_cfg = worker_config.get('step5_5')
if step5_5_cfg:
enabled = step5_5_cfg.pop('enabled', True)
step8_cfg = worker_config.get('step8')
if step8_cfg:
enabled = step8_cfg.pop('enabled', True)
if not enabled:
worker_config.pop('step5_5', None)
worker_config.pop('step8', None)
# 工作线程内创建 Pipeline避免主线程阻塞及 Qt5Agg 子线程绘图卡死
self.worker = WorkerThread(work_dir, worker_config, mode='full')
self.worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
self.worker.log_message.connect(self.log_message, Qt.QueuedConnection)
self.worker.progress_update.connect(self.update_progress, Qt.QueuedConnection)
self.worker.step_completed.connect(self.on_step_completed, Qt.QueuedConnection)
@ -3152,14 +3256,14 @@ class WaterQualityGUI(QMainWindow):
def update_ui_for_training_mode(self):
"""根据训练数据模式更新UI状态"""
# 需要禁用的步骤ID对应无训练数据模式下需要禁用的步骤
disabled_step_ids = ['step4', 'step5', 'step5_5', 'step6', 'step6_5', 'step6_75']
disabled_step_ids = ['step4', 'step5', 'step8', 'step7', 'step8_non_empirical_modeling', 'step9']
# 更新标签页的启用/禁用状态
step_id_to_tab = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3,
'step5': 4, 'step5_5': 5, 'step6': 6, 'step6_5': 7,
'step6_75': 8, 'step7': 9, 'step8': 10, 'step8_5': 11,
'step8_75': 12, 'step9': 13, 'step9_viz': 14
'step5': 4, 'step8': 5, 'step7': 6, 'step8_non_empirical_modeling': 7,
'step9': 8, 'step10': 9, 'step11_ml': 10, 'step11': 11,
'step12': 12, 'step14': 13, 'step9_viz': 14
}
for step_id in disabled_step_ids: