diff --git a/src/core/pipeline/context.py b/src/core/pipeline/context.py
index bebee3d..4fd77ce 100644
--- a/src/core/pipeline/context.py
+++ b/src/core/pipeline/context.py
@@ -20,13 +20,16 @@ from typing import Any, Dict, List, Optional, Set
# ============================================================
STEP_MAP_OLD_TO_NEW: Dict[str, str] = {
- "step5_5": "step8",
+ "step5_5": "step7",
"step6_5": "step8_non_empirical_modeling",
"step6_75": "step9",
"step8_5": "step11",
- "step8_75": "step12",
- "step7": "step10",
+ "step7": "step8",
+ "step8": "step7",
"step9": "step14",
+ "step10": "step4",
+ "step11_ml": "step10",
+ "step11": "step11",
}
STEP_MAP_NEW_TO_OLD: Dict[str, str] = {v: k for k, v in STEP_MAP_OLD_TO_NEW.items()}
diff --git a/src/core/pipeline/runner.py b/src/core/pipeline/runner.py
index 5cf54f5..288e434 100644
--- a/src/core/pipeline/runner.py
+++ b/src/core/pipeline/runner.py
@@ -115,14 +115,14 @@ PIPELINE_STEPS: List[StepSpec] = [
description="实测样本点光谱提取",
),
StepSpec(
- step_id="step8", method_name="step8_water_quality_indices",
+ step_id="step7", method_name="step7_water_quality_indices",
requires=["training_csv_path"], produces=["indices_path", "trad_indices_dir"],
required_input_files=["training_csv_path"],
output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv",
- description="水质光谱指数计算(双轨输出:A轨宽表 + B轨单文件)",
+ description="水质参数指数计算(双轨输出:A轨宽表 + B轨单文件)",
),
StepSpec(
- step_id="step7", method_name="step7_ml_modeling",
+ step_id="step8", method_name="step8_ml_modeling",
requires=["training_csv_path"], produces=["models_dir"],
required_input_files=["training_csv_path"],
output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl",
@@ -138,18 +138,17 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验统计回归",
),
StepSpec(
- step_id="step9", method_name="step9_custom_regression",
- requires=["indices_path"], produces=["models_dir"],
- parameter_map={"indices_path": "csv_path"},
- required_input_files=["indices_path"],
- output_file="{work_dir}/9_Custom_Regression_Modeling/custom_regression_models.pkl",
- description="自定义回归分析",
+ step_id="step9", method_name="step9_watercolor_inversion",
+ requires=["deglint_img_path", "water_mask_path"], produces=["watercolor_index_dir"],
+ required_input_files=["deglint_img_path"],
+ output_file="{work_dir}/9_WaterColor_Index_Images",
+ description="水色指数反演(BSQ 影像直接处理)",
),
StepSpec(
step_id="step10", method_name="step10_sampling",
requires=["deglint_img_path", "water_mask_path"], produces=["sampling_csv_path"],
required_input_files=["deglint_img_path", "water_mask_path"],
- output_file="{work_dir}/10_sampling/sampling_spectra.csv",
+ output_file="{work_dir}/4_sampling/sampling_spectra.csv",
description="整景密集采样点生成 + 光谱提取",
),
StepSpec(
@@ -167,15 +166,6 @@ PIPELINE_STEPS: List[StepSpec] = [
output_file="{work_dir}/11_12_13_predictions/non_empirical_predictions",
description="非经验模型预测",
),
- StepSpec(
- step_id="step12", method_name="step12_custom_regression_prediction",
- requires=["sampling_csv_path", "models_dir", "formula_csv_path"],
- produces=["prediction_dir"],
- parameter_map={"models_dir": "custom_regression_dir"},
- required_input_files=["sampling_csv_path", "models_dir", "formula_csv_path"],
- output_file="{work_dir}/11_12_13_predictions/custom_regression_predictions",
- description="自定义回归预测",
- ),
StepSpec(
step_id="step14", method_name="step14_distribution_map",
requires=["prediction_csv_path", "boundary_shp_path"],
diff --git a/src/gui/core/preflight_dialog.py b/src/gui/core/preflight_dialog.py
index c13830c..32cfc4e 100644
--- a/src/gui/core/preflight_dialog.py
+++ b/src/gui/core/preflight_dialog.py
@@ -59,14 +59,14 @@ class PreflightDialog(QDialog):
"step3": ("耀斑去除", 2),
"step4": ("数据清洗", 3),
"step5": ("特征构建", 4),
- "step8": ("水质指数", 5),
- "step7": ("监督建模", 6),
+ "step7": ("水质指数", 5),
+ "step8": ("监督建模", 6),
"step8_non_empirical_modeling": ("回归建模", 7),
- "step9": ("自定义回归建模", 8),
- "step10": ("采样点布设", 9),
- "step11_ml": ("监督预测", 10),
- "step11": ("回归预测", 11),
- "step12": ("自定义回归预测", 12),
+ "step9": ("水色指数反演", 8),
+ "step9_concentration": ("浓度反演", 9),
+ "step10": ("采样点布设", 10),
+ "step11_ml": ("监督预测", 11),
+ "step11": ("回归预测", 12),
"step14": ("专题图生成", 13),
}
diff --git a/src/gui/core/worker_thread.py b/src/gui/core/worker_thread.py
index caf4661..074530c 100644
--- a/src/gui/core/worker_thread.py
+++ b/src/gui/core/worker_thread.py
@@ -325,16 +325,15 @@ class WorkerThread(QThread):
'step3': 'step3_remove_glint',
'step4': 'step4_process_csv',
'step5': 'step5_extract_training_spectra',
- 'step6': 'step6_water_quality_indices',
- 'step7': 'step7_ml_modeling',
+ 'step7': 'step7_water_quality_indices',
+ 'step8': 'step8_ml_modeling',
'step8_non_empirical_modeling': 'step8_non_empirical_modeling',
'step8_qaa': 'step8_qaa_inversion',
+ 'step9': 'step9_watercolor_inversion',
'step9_concentration': 'step9_concentration_inversion',
- 'step9': 'step9_custom_regression',
'step10': 'step10_sampling',
'step11_ml': 'step11_ml_prediction',
'step11': 'step11_non_empirical_prediction',
- 'step12': 'step12_custom_regression_prediction',
'step14': 'step14_distribution_map'
}
diff --git a/src/gui/panels/step11_ml_panel.py b/src/gui/panels/step10_ml_panel.py
similarity index 87%
rename from src/gui/panels/step11_ml_panel.py
rename to src/gui/panels/step10_ml_panel.py
index 8881f1e..3fdbd0d 100644
--- a/src/gui/panels/step11_ml_panel.py
+++ b/src/gui/panels/step10_ml_panel.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step8 面板 - 机器学习预测
+Step11 面板 - 机器学习预测
"""
import os
@@ -19,7 +19,7 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-class Step11MlPanel(QWidget):
+class Step10MlPanel(QWidget):
"""步骤11:机器学习预测"""
def __init__(self, parent=None):
super().__init__(parent)
@@ -190,7 +190,7 @@ class Step11MlPanel(QWidget):
"""浏览模型母文件夹,自动扫描子目录中的 .joblib 文件"""
default = self._get_default_work_dir()
if default:
- default = os.path.join(default, "7_Supervised_Model_Training")
+ default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory(
self,
"选择模型母文件夹",
@@ -216,7 +216,6 @@ class Step11MlPanel(QWidget):
]
if not joblib_files:
continue
- # 每个子目录只取第一个 .joblib 文件(与 batch 逻辑一致)
joblib_path = joblib_files[0].path
try:
loaded = joblib.load(joblib_path)
@@ -319,43 +318,41 @@ class Step11MlPanel(QWidget):
main_window = self.window()
- # 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径
- if main_window and hasattr(main_window, 'step10_panel'):
- step7_widget = getattr(main_window.step10_panel, 'output_file', None)
- step7_output_path = ""
- if hasattr(step7_widget, 'get_path'):
- step7_output_path = step7_widget.get_path() or ""
- elif hasattr(step7_widget, 'text'):
- step7_output_path = step7_widget.text() or ""
+ # 1. 尝试从 Step4(采样点布设)读取全湖采样点 CSV 路径
+ if main_window and hasattr(main_window, 'step4_sampling_panel'):
+ step4_widget = getattr(main_window.step4_sampling_panel, 'output_file', None)
+ step4_output_path = ""
+ if hasattr(step4_widget, 'get_path'):
+ step4_output_path = step4_widget.get_path() or ""
+ elif hasattr(step4_widget, 'text'):
+ step4_output_path = step4_widget.text() or ""
- if step7_output_path:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(step7_output_path):
- step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
+ if step4_output_path:
+ if not os.path.isabs(step4_output_path):
+ step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
- self.sampling_csv_file.set_path(step7_output_path)
+ self.sampling_csv_file.set_path(step4_output_path)
- # 2. 尝试从 Step6 界面读取监督模型目录
- if main_window and hasattr(main_window, 'step7_panel'):
- step6_widget = getattr(main_window.step7_panel, 'output_dir', None)
- step6_models_dir = ""
- if hasattr(step6_widget, 'get_path'):
- step6_models_dir = step6_widget.get_path() or ""
- elif hasattr(step6_widget, 'text'):
- step6_models_dir = step6_widget.text() or ""
+ # 2. 尝试从 Step9(监督建模)读取模型目录
+ if main_window and hasattr(main_window, 'step9_panel'):
+ step9_widget = getattr(main_window.step9_panel, 'output_dir', None)
+ step9_models_dir = ""
+ if hasattr(step9_widget, 'get_path'):
+ step9_models_dir = step9_widget.get_path() or ""
+ elif hasattr(step9_widget, 'text'):
+ step9_models_dir = step9_widget.text() or ""
- if step6_models_dir:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(step6_models_dir):
- step6_models_dir = os.path.join(self.work_dir or '', step6_models_dir).replace('\\', '/')
+ if step9_models_dir:
+ if not os.path.isabs(step9_models_dir):
+ step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip():
- self.models_dir_file.set_path(step6_models_dir)
+ self.models_dir_file.set_path(step9_models_dir)
# 3. 自动填充输出路径(机器学习预测目录)
if self.work_dir:
- output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Machine_Learning_Prediction")
+ output_dir = os.path.join(self.work_dir, "11_ml_prediction")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip():
@@ -378,7 +375,7 @@ class Step11MlPanel(QWidget):
"""浏览模型目录"""
default = self._get_default_work_dir()
if default:
- default = os.path.join(default, "7_Supervised_Model_Training")
+ default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path:
self.models_dir_file.set_path(dir_path)
@@ -416,7 +413,7 @@ class Step11MlPanel(QWidget):
self.output_file.set_path(config['output_path'])
def run_step(self):
- """独立运行步骤8"""
+ """独立运行步骤11"""
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
@@ -431,7 +428,6 @@ class Step11MlPanel(QWidget):
"请先点击「浏览...」按钮选择模型母文件夹!",
)
return
- # 只传递用户勾选的模型
checked_dict = self._get_checked_models_dict()
if not checked_dict:
QMessageBox.warning(
@@ -459,4 +455,4 @@ class Step11MlPanel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step11_ml': self.get_config()}
- main_window.run_single_step('step11_ml', config)
+ main_window.run_single_step('step11_ml', config)
\ No newline at end of file
diff --git a/src/gui/panels/step11_panel.py b/src/gui/panels/step11_panel.py
index ce78fb9..7f7ac7c 100644
--- a/src/gui/panels/step11_panel.py
+++ b/src/gui/panels/step11_panel.py
@@ -17,7 +17,7 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-class Step11Panel(QWidget):
+class Step11NonEmpiricalPanel(QWidget):
"""步骤11:非经验模型预测"""
def __init__(self, parent=None):
super().__init__(parent)
diff --git a/src/gui/panels/step4_panel.py b/src/gui/panels/step4_panel.py
deleted file mode 100644
index cda8fe5..0000000
--- a/src/gui/panels/step4_panel.py
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Step4 面板 - 数据预处理
-"""
-
-import os
-
-import pandas as pd
-from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QLabel,
- QSpinBox, QPushButton, QCheckBox, QTableView,
- QAbstractItemView, QHeaderView, QMessageBox,
-)
-from PyQt5.QtCore import Qt
-
-from src.gui.components.custom_widgets import FileSelectWidget
-from src.gui.styles import ModernStylesheet
-
-
-class Step4Panel(QWidget):
- """步骤4:数据预处理"""
- def __init__(self, parent=None):
- super().__init__(parent)
- self.init_ui()
-
- def init_ui(self):
- layout = QVBoxLayout()
-
- # 标题
-
- # CSV文件
- self.csv_file = FileSelectWidget(
- "水质参数文件:",
- "CSV Files (*.csv);;All Files (*.*)"
- )
- layout.addWidget(self.csv_file)
-
- hint = QLabel("提示: 处理CSV文件,筛选剔除异常值")
- hint.setStyleSheet("color: #666; font-size: 10px;")
- layout.addWidget(hint)
-
- preview_group = QGroupBox("CSV数据预览")
- preview_layout = QVBoxLayout()
-
- controls_layout = QHBoxLayout()
- controls_layout.addWidget(QLabel("预览行数:"))
- self.preview_rows_spin = QSpinBox()
- self.preview_rows_spin.setRange(1, 200)
- self.preview_rows_spin.setValue(10)
- controls_layout.addWidget(self.preview_rows_spin)
- self.preview_btn = QPushButton("刷新预览")
- self.preview_btn.clicked.connect(self.load_csv_preview)
- controls_layout.addWidget(self.preview_btn)
- controls_layout.addStretch()
-
- self.preview_table = QTableView()
- self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
- self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
- self.preview_table.setSelectionMode(QAbstractItemView.SingleSelection)
- self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
- self.preview_table.verticalHeader().setVisible(False)
- self.preview_table.setMinimumHeight(200)
-
- self.preview_status_label = QLabel("请选择CSV文件并点击刷新预览")
- self.preview_status_label.setStyleSheet("color: #666; font-size: 11px;")
-
- preview_layout.addLayout(controls_layout)
- preview_layout.addWidget(self.preview_table)
- preview_layout.addWidget(self.preview_status_label)
- preview_group.setLayout(preview_layout)
- layout.addWidget(preview_group)
-
- # 输出文件路径
- self.output_file = FileSelectWidget(
- "输出处理后CSV:",
- "CSV Files (*.csv);;All Files (*.*)"
- )
- self.output_file.line_edit.setPlaceholderText("processed_data.csv")
- layout.addWidget(self.output_file)
-
- # 启用步骤
- self.enable_checkbox = QCheckBox("启用此步骤")
- self.enable_checkbox.setChecked(True)
- layout.addWidget(self.enable_checkbox)
-
- # 独立运行按钮
- self.run_btn = QPushButton("独立运行此步骤")
- self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
- self.run_btn.clicked.connect(self.run_step)
- layout.addWidget(self.run_btn)
-
- layout.addStretch()
- self.setLayout(layout)
- self.reset_preview()
-
- def get_config(self):
- """获取配置"""
- config = {
- 'csv_path': self.csv_file.get_path(),
- }
- output_path = self.output_file.get_path()
- if output_path:
- config['output_path'] = output_path
- return config
-
- def set_config(self, config):
- """设置配置"""
- if 'csv_path' in config:
- self.csv_file.set_path(config['csv_path'])
- self.load_csv_preview()
- if 'output_path' in config:
- self.output_file.set_path(config['output_path'])
-
- def update_from_config(self, work_dir=None, pipeline=None):
- """从全局配置自动填充输出路径
-
- Args:
- work_dir: 工作目录路径
- pipeline: Pipeline 实例(未使用,保留接口兼容性)
- """
- if work_dir:
- self.work_dir = work_dir
- elif hasattr(self, 'work_dir') and self.work_dir:
- pass
- else:
- self.work_dir = None
-
- if self.work_dir:
- output_dir = os.path.join(self.work_dir, "4_processed_data")
- os.makedirs(output_dir, exist_ok=True)
- default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/')
- self.output_file.set_path(default_output_path)
- else:
- self.output_file.set_path("")
-
- def run_step(self):
- """独立运行步骤4"""
- # 验证输入
- csv_path = self.csv_file.get_path()
- if not csv_path:
- QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
- return
-
- # 获取主窗口并运行步骤
- main_window = self.window()
- if hasattr(main_window, 'run_single_step'):
- config = {'step4': self.get_config()}
- main_window.run_single_step('step4', config)
-
- def reset_preview(self, message="请选择CSV文件并点击刷新预览"):
- """重置预览表格"""
- from src.gui.water_quality_gui import PandasTableModel
- empty_model = PandasTableModel(pd.DataFrame())
- self.preview_table.setModel(empty_model)
- self.preview_status_label.setText(message)
-
- def load_csv_preview(self):
- """加载CSV预览数据"""
- from src.gui.water_quality_gui import PandasTableModel
- csv_path = self.csv_file.get_path()
- if not csv_path:
- self.reset_preview("请先选择CSV文件")
- return
- if not os.path.exists(csv_path):
- self.reset_preview("文件不存在,请检查路径")
- return
-
- try:
- rows_to_preview = max(1, self.preview_rows_spin.value())
- # dtype=object 确保所有列以字符串读取,避免空值/混合类型导致 dtype 报错
- df = pd.read_csv(csv_path, nrows=rows_to_preview, dtype=object)
- # fillna 在 PandasTableModel.__init__ 中已执行,此处再次防御性处理
- df = df.fillna('')
- if df.empty:
- self.reset_preview("CSV文件为空")
- return
-
- model = PandasTableModel(df)
- self.preview_table.setModel(model)
- self.preview_status_label.setText(
- f"预览 {len(df)} 行,{len(df.columns)} 列(总行数可能更多)"
- )
- except Exception as exc:
- self.reset_preview(f"加载失败: {exc}")
diff --git a/src/gui/panels/step10_panel.py b/src/gui/panels/step4_sampling_panel.py
similarity index 96%
rename from src/gui/panels/step10_panel.py
rename to src/gui/panels/step4_sampling_panel.py
index 5e6150d..d62cb70 100644
--- a/src/gui/panels/step10_panel.py
+++ b/src/gui/panels/step4_sampling_panel.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step10 面板 - 采样点生成
+Step4 面板 - 采样点布设
"""
import os
@@ -16,8 +16,8 @@ from src.gui.dialogs import SamplingViewerDialog
from src.gui.styles import ModernStylesheet
-class Step10Panel(QWidget):
- """步骤10:采样点生成"""
+class Step4SamplingPanel(QWidget):
+ """步骤4:采样点布设"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@@ -71,7 +71,7 @@ class Step10Panel(QWidget):
"输出采样点:",
"CSV Files (*.csv);;All Files (*.*)"
)
- self.output_file.line_edit.setPlaceholderText("sampling_points.csv")
+ self.output_file.line_edit.setPlaceholderText("sampling_spectra.csv")
layout.addWidget(self.output_file)
# 启用步骤
@@ -207,7 +207,7 @@ class Step10Panel(QWidget):
# 3. 自动填充输出路径(绝对路径)
if self.work_dir:
- output_path = os.path.join(self.work_dir, "10_sampling", "sampling_spectra.csv")
+ output_path = os.path.join(self.work_dir, "4_sampling", "sampling_spectra.csv")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
self.output_file.set_path(output_path.replace('\\', '/'))
@@ -215,7 +215,7 @@ class Step10Panel(QWidget):
self._check_csv_exists()
def run_step(self):
- """独立运行步骤10"""
+ """独立运行步骤4"""
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
@@ -223,8 +223,8 @@ class Step10Panel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
- config = {'step10': self.get_config()}
- main_window.run_single_step('step10', config)
+ config = {'step4': self.get_config()}
+ main_window.run_single_step('step4', config)
def _check_csv_exists(self):
"""检查 output csv 是否存在,驱动预览按钮启停"""
@@ -243,10 +243,10 @@ class Step10Panel(QWidget):
if not csv_path or not os.path.exists(csv_path):
QMessageBox.warning(
self, "文件不存在",
- f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤10生成数据。"
+ f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤4生成数据。"
)
return
dialog = SamplingViewerDialog(csv_path, self)
dialog.exec_()
# 弹窗关闭后再次检查状态(可能文件被覆盖等)
- self._check_csv_exists()
+ self._check_csv_exists()
\ No newline at end of file
diff --git a/src/gui/panels/step5_panel.py b/src/gui/panels/step5_panel.py
index edc1e94..5c3fbf4 100644
--- a/src/gui/panels/step5_panel.py
+++ b/src/gui/panels/step5_panel.py
@@ -1,16 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step5 面板 - 光谱提取
+Step4 面板 - 数据预处理
"""
import os
+import pandas as pd
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel,
- QSpinBox, QPushButton, QCheckBox, QMessageBox,
+ QWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QLabel,
+ QSpinBox, QPushButton, QCheckBox, QTableView,
+ QAbstractItemView, QHeaderView, QMessageBox,
)
-from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget
@@ -18,7 +19,7 @@ from src.gui.styles import ModernStylesheet
class Step5Panel(QWidget):
- """步骤5:光谱提取"""
+ """步骤5:数据清洗"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@@ -27,67 +28,55 @@ class Step5Panel(QWidget):
layout = QVBoxLayout()
# 标题
- title = QLabel("步骤5:训练样本光谱提取")
- title.setFont(QFont("Arial", 12, QFont.Bold))
- layout.addWidget(title)
- # 去耀斑影像文件(用于独立运行)
- self.deglint_img_file = FileSelectWidget(
- "去耀斑影像:",
- "Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
- )
- layout.addWidget(self.deglint_img_file)
-
- # 处理后的CSV文件(用于独立运行)
+ # CSV文件
self.csv_file = FileSelectWidget(
- "处理后CSV:",
+ "水质参数文件:",
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.csv_file)
- # 水体掩膜文件(可选,用于独立运行)
- self.water_mask_file = FileSelectWidget(
- "水体掩膜:",
- "Mask Files (*.dat *.tif);;All Files (*.*)"
- )
- self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
- layout.addWidget(self.water_mask_file)
+ hint = QLabel("提示: 处理CSV文件,筛选剔除异常值")
+ hint.setStyleSheet("color: #666; font-size: 10px;")
+ layout.addWidget(hint)
- self.glint_mask_file = FileSelectWidget(
- "耀斑掩膜:",
- "Mask Files (*.dat *.tif);;All Files (*.*)"
- )
- layout.addWidget(self.glint_mask_file)
- step5_glint_hint = QLabel(
- "提示:独立运行本步骤时必须选择耀斑掩膜(通常为步骤2输出的 severe_glint_area.dat),用于在采样时避开耀斑像元。"
- )
- step5_glint_hint.setWordWrap(True)
- step5_glint_hint.setStyleSheet("color: #666; font-size: 10px;")
- layout.addWidget(step5_glint_hint)
+ preview_group = QGroupBox("CSV数据预览")
+ preview_layout = QVBoxLayout()
- # 参数设置
- params_group = QGroupBox("提取参数")
- params_layout = QFormLayout()
+ controls_layout = QHBoxLayout()
+ controls_layout.addWidget(QLabel("预览行数:"))
+ self.preview_rows_spin = QSpinBox()
+ self.preview_rows_spin.setRange(1, 200)
+ self.preview_rows_spin.setValue(10)
+ controls_layout.addWidget(self.preview_rows_spin)
+ self.preview_btn = QPushButton("刷新预览")
+ self.preview_btn.clicked.connect(self.load_csv_preview)
+ controls_layout.addWidget(self.preview_btn)
+ controls_layout.addStretch()
- self.radius = QSpinBox()
- self.radius.setRange(1, 50)
- self.radius.setValue(5)
- params_layout.addRow("采样半径(像素):", self.radius)
+ self.preview_table = QTableView()
+ self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.preview_table.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ self.preview_table.verticalHeader().setVisible(False)
+ self.preview_table.setMinimumHeight(200)
- self.source_epsg = QSpinBox()
- self.source_epsg.setRange(1000, 99999)
- self.source_epsg.setValue(4326)
- params_layout.addRow("源坐标系EPSG:", self.source_epsg)
+ self.preview_status_label = QLabel("请选择CSV文件并点击刷新预览")
+ self.preview_status_label.setStyleSheet("color: #666; font-size: 11px;")
- params_group.setLayout(params_layout)
- layout.addWidget(params_group)
+ preview_layout.addLayout(controls_layout)
+ preview_layout.addWidget(self.preview_table)
+ preview_layout.addWidget(self.preview_status_label)
+ preview_group.setLayout(preview_layout)
+ layout.addWidget(preview_group)
# 输出文件路径
self.output_file = FileSelectWidget(
- "输出训练数据:",
+ "输出处理后CSV:",
"CSV Files (*.csv);;All Files (*.*)"
)
- self.output_file.line_edit.setPlaceholderText("training_spectra.csv")
+ self.output_file.line_edit.setPlaceholderText("processed_data.csv")
layout.addWidget(self.output_file)
# 启用步骤
@@ -103,54 +92,33 @@ class Step5Panel(QWidget):
layout.addStretch()
self.setLayout(layout)
- # 信号连接:影像文件路径变化时动态更新波段范围
+ self.reset_preview()
def get_config(self):
"""获取配置"""
config = {
- 'radius': self.radius.value(),
- 'source_epsg': self.source_epsg.value(),
+ 'csv_path': self.csv_file.get_path(),
}
- # 添加独立运行所需的文件路径
- deglint_img_path = self.deglint_img_file.get_path()
- if deglint_img_path:
- config['deglint_img_path'] = deglint_img_path
- csv_path = self.csv_file.get_path()
- if csv_path:
- config['csv_path'] = csv_path
- water_mask_path = self.water_mask_file.get_path()
- if water_mask_path:
- config['boundary_path'] = water_mask_path
- glint_mask_path = self.glint_mask_file.get_path()
- if glint_mask_path:
- config['glint_mask_path'] = glint_mask_path
- # 注意:step5_extract_training_spectra 不接受 output_path / training_csv_path
- # 参数,输出路径由 pipeline 内部根据 training_spectra_dir 自动生成。
+ output_path = self.output_file.get_path()
+ if output_path:
+ config['output_path'] = output_path
return config
def set_config(self, config):
"""设置配置"""
- if 'radius' in config:
- self.radius.setValue(config['radius'])
- if 'source_epsg' in config:
- self.source_epsg.setValue(config['source_epsg'])
- if 'deglint_img_path' in config:
- self.deglint_img_file.set_path(config['deglint_img_path'])
if 'csv_path' in config:
self.csv_file.set_path(config['csv_path'])
- if 'boundary_path' in config:
- self.water_mask_file.set_path(config['boundary_path'])
- if 'glint_mask_path' in config:
- self.glint_mask_file.set_path(config['glint_mask_path'])
+ self.load_csv_preview()
+ if 'output_path' in config:
+ self.output_file.set_path(config['output_path'])
def update_from_config(self, work_dir=None, pipeline=None):
- """从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转
+ """从全局配置自动填充输出路径
Args:
work_dir: 工作目录路径
- pipeline: Pipeline 实例,用于获取步骤1生成的水域掩膜路径
+ pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
- # 保存工作目录引用
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
@@ -158,82 +126,60 @@ class Step5Panel(QWidget):
else:
self.work_dir = None
- # 1. 尝试从 Pipeline 获取水体掩膜路径
- mask_path = None
- if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path:
- mask_path = pipeline.water_mask_path
-
- # 2. 如果 Pipeline 中没有,则尝试直接从 Step1 界面读取
- main_window = self.window()
- if not mask_path and hasattr(main_window, 'step1_panel'):
- if main_window.step1_panel.use_ndwi_radio.isChecked():
- mask_path = main_window.step1_panel.output_file.get_path()
- else:
- mask_path = main_window.step1_panel.mask_file.get_path()
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if mask_path and not os.path.isabs(mask_path):
- mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
-
- # 填充水体掩膜路径
- if mask_path:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(mask_path):
- mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
- self.water_mask_file.set_path(mask_path)
-
- # 3. 尝试从 Step2 界面读取耀斑掩膜路径
- main_window = self.window()
- if hasattr(main_window, 'step2_panel'):
- glint_path = main_window.step2_panel.output_file.get_path()
- if glint_path:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(glint_path):
- glint_path = os.path.join(self.work_dir or '', glint_path).replace('\\', '/')
- self.glint_mask_file.set_path(glint_path)
-
- # 4. 自动填充输出路径(基于工作目录)
if self.work_dir:
- output_dir = os.path.join(self.work_dir, "5_training_spectra")
+ output_dir = os.path.join(self.work_dir, "4_processed_data")
os.makedirs(output_dir, exist_ok=True)
- default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
+ default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/')
self.output_file.set_path(default_output_path)
else:
self.output_file.set_path("")
- # 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板
- main_window = self.window()
- if main_window and hasattr(main_window, 'step4_panel'):
- step4_output_path = main_window.step4_panel.output_file.get_path()
- if step4_output_path:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(step4_output_path):
- step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
- existing_csv = self.csv_file.get_path()
- if not existing_csv or not existing_csv.strip():
- self.csv_file.set_path(step4_output_path)
-
def run_step(self):
- """独立运行步骤5"""
+ """独立运行步骤4"""
# 验证输入
- 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)。",
- )
+ QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return
# 获取主窗口并运行步骤
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
- config = {'step5': self.get_config()}
- main_window.run_single_step('step5', config)
+ config = {'step4': self.get_config()}
+ main_window.run_single_step('step4', config)
+
+ def reset_preview(self, message="请选择CSV文件并点击刷新预览"):
+ """重置预览表格"""
+ from src.gui.water_quality_gui import PandasTableModel
+ empty_model = PandasTableModel(pd.DataFrame())
+ self.preview_table.setModel(empty_model)
+ self.preview_status_label.setText(message)
+
+ def load_csv_preview(self):
+ """加载CSV预览数据"""
+ from src.gui.water_quality_gui import PandasTableModel
+ csv_path = self.csv_file.get_path()
+ if not csv_path:
+ self.reset_preview("请先选择CSV文件")
+ return
+ if not os.path.exists(csv_path):
+ self.reset_preview("文件不存在,请检查路径")
+ return
+
+ try:
+ rows_to_preview = max(1, self.preview_rows_spin.value())
+ # dtype=object 确保所有列以字符串读取,避免空值/混合类型导致 dtype 报错
+ df = pd.read_csv(csv_path, nrows=rows_to_preview, dtype=object)
+ # fillna 在 PandasTableModel.__init__ 中已执行,此处再次防御性处理
+ df = df.fillna('')
+ if df.empty:
+ self.reset_preview("CSV文件为空")
+ return
+
+ model = PandasTableModel(df)
+ self.preview_table.setModel(model)
+ self.preview_status_label.setText(
+ f"预览 {len(df)} 行,{len(df.columns)} 列(总行数可能更多)"
+ )
+ except Exception as exc:
+ self.reset_preview(f"加载失败: {exc}")
diff --git a/src/gui/panels/step6_panel.py b/src/gui/panels/step6_panel.py
index 6afde8d..2c910fe 100644
--- a/src/gui/panels/step6_panel.py
+++ b/src/gui/panels/step6_panel.py
@@ -1,423 +1,239 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Step5 面板 - 光谱提取
+"""
+
import os
-import sys
-import pandas as pd
-import numpy as np
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QGridLayout,
- QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
- QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
- QRadioButton, QButtonGroup
+ QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel,
+ QSpinBox, QPushButton, QCheckBox, QMessageBox,
)
+from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-def get_resource_path(relative_path: str) -> str:
- """适配开发与 PyInstaller 环境的路径获取逻辑。"""
- if hasattr(sys, '_MEIPASS'):
- internal = os.path.join(sys._MEIPASS, '_internal', relative_path)
- if os.path.exists(internal):
- return internal
- return os.path.join(sys._MEIPASS, relative_path)
-
- exe_dir = os.path.dirname(sys.executable)
- internal = os.path.join(exe_dir, '_internal', relative_path)
- if os.path.exists(internal):
- return internal
-
- base_dir = Path(__file__).resolve().parent.parent / "model"
- return str(base_dir / os.path.basename(relative_path))
-
-
class Step6Panel(QWidget):
- COLOR_RATIO = QColor(255, 255, 255)
- COLOR_CONCENTRATION = QColor(220, 240, 255)
- COLOR_HEADER = QColor(245, 245, 245)
-
+ """步骤6:光谱特征"""
def __init__(self, parent=None):
super().__init__(parent)
- self.index_checkboxes: Dict[str, QListWidgetItem] = {}
- self.work_dir: Optional[str] = None
- self.builtin_formula_path = get_resource_path("waterindex.csv")
- self._formula_type_map: Dict[str, str] = {}
- self._formula_color_map: Dict[str, QColor] = {}
- self._formula_coef_map: Dict[str, List[float]] = {}
-
self.init_ui()
- self._auto_load_formulas()
def init_ui(self):
- main_layout = QVBoxLayout()
- main_layout.setContentsMargins(20, 20, 20, 20)
- main_layout.setSpacing(10)
+ layout = QVBoxLayout()
- # 1. 公式配置源 (只读)
- path_group = QGroupBox("公式配置源 (内置)")
- path_layout = QVBoxLayout()
- self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
- self.formula_csv_widget.set_path(self.builtin_formula_path)
- self.formula_csv_widget.set_read_only(True)
- self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
- path_layout.addWidget(self.formula_csv_widget)
- path_group.setLayout(path_layout)
- main_layout.addWidget(path_group)
+ # 标题
+ title = QLabel("步骤5:训练样本光谱提取")
+ title.setFont(QFont("Arial", 12, QFont.Bold))
+ layout.addWidget(title)
- # 2. 训练数据输入
- input_group = QGroupBox("输入样本数据")
- input_layout = QVBoxLayout()
- self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
- input_layout.addWidget(self.training_data_widget)
- input_group.setLayout(input_layout)
- main_layout.addWidget(input_group)
+ # 去耀斑影像文件(用于独立运行)
+ self.deglint_img_file = FileSelectWidget(
+ "去耀斑影像:",
+ "Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
+ )
+ layout.addWidget(self.deglint_img_file)
- # 3. 公式选择区 (分组 ListWidget)
- self.formula_group = QGroupBox("待计算水质指数勾选")
- formula_outer_layout = QVBoxLayout()
+ # 处理后的CSV文件(用于独立运行)
+ self.csv_file = FileSelectWidget(
+ "处理后CSV:",
+ "CSV Files (*.csv);;All Files (*.*)"
+ )
+ layout.addWidget(self.csv_file)
- btn_layout = QHBoxLayout()
- self.select_all_btn = QPushButton("全选")
- self.deselect_all_btn = QPushButton("清空")
- self.select_ratio_btn = QPushButton("仅选比值型")
- self.select_conc_btn = QPushButton("仅选浓度型")
- self.select_all_btn.clicked.connect(self.select_all_formulas)
- self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
- self.select_ratio_btn.clicked.connect(self._select_ratio_only)
- self.select_conc_btn.clicked.connect(self._select_conc_only)
- btn_layout.addWidget(self.select_all_btn)
- btn_layout.addWidget(self.deselect_all_btn)
- btn_layout.addWidget(self.select_ratio_btn)
- btn_layout.addWidget(self.select_conc_btn)
- btn_layout.addStretch()
+ # 水体掩膜文件(可选,用于独立运行)
+ self.water_mask_file = FileSelectWidget(
+ "水体掩膜:",
+ "Mask Files (*.dat *.tif);;All Files (*.*)"
+ )
+ self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
+ layout.addWidget(self.water_mask_file)
- self.refresh_button = QPushButton("重新加载")
- self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
- btn_layout.addWidget(self.refresh_button)
+ self.glint_mask_file = FileSelectWidget(
+ "耀斑掩膜:",
+ "Mask Files (*.dat *.tif);;All Files (*.*)"
+ )
+ layout.addWidget(self.glint_mask_file)
+ step5_glint_hint = QLabel(
+ "提示:独立运行本步骤时必须选择耀斑掩膜(通常为步骤2输出的 severe_glint_area.dat),用于在采样时避开耀斑像元。"
+ )
+ step5_glint_hint.setWordWrap(True)
+ step5_glint_hint.setStyleSheet("color: #666; font-size: 10px;")
+ layout.addWidget(step5_glint_hint)
- formula_outer_layout.addLayout(btn_layout)
+ # 参数设置
+ params_group = QGroupBox("提取参数")
+ params_layout = QFormLayout()
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setMinimumHeight(280)
- self.scroll_content = QWidget()
- self.formula_layout = QVBoxLayout(self.scroll_content)
- self.formula_layout.setContentsMargins(4, 4, 4, 4)
- self.formula_layout.setSpacing(2)
- self.formula_layout.setAlignment(Qt.AlignTop)
+ self.radius = QSpinBox()
+ self.radius.setRange(1, 50)
+ self.radius.setValue(5)
+ params_layout.addRow("采样半径(像素):", self.radius)
- self.formula_list = QListWidget()
- self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
- self.formula_list.itemChanged.connect(self._on_item_changed)
- self.formula_layout.addWidget(self.formula_list)
+ self.source_epsg = QSpinBox()
+ self.source_epsg.setRange(1000, 99999)
+ self.source_epsg.setValue(4326)
+ params_layout.addRow("源坐标系EPSG:", self.source_epsg)
- scroll.setWidget(self.scroll_content)
- formula_outer_layout.addWidget(scroll)
+ params_group.setLayout(params_layout)
+ layout.addWidget(params_group)
- self.formula_group.setLayout(formula_outer_layout)
- main_layout.addWidget(self.formula_group)
+ # 输出文件路径
+ self.output_file = FileSelectWidget(
+ "输出训练数据:",
+ "CSV Files (*.csv);;All Files (*.*)"
+ )
+ self.output_file.line_edit.setPlaceholderText("training_spectra.csv")
+ layout.addWidget(self.output_file)
- # 4. 输出选项
- output_group = QGroupBox("输出模式")
- output_layout = QVBoxLayout()
-
- mode_layout = QHBoxLayout()
- self.mode_group = QButtonGroup()
- self.radio_both = QRadioButton("两者皆出")
- self.radio_wide = QRadioButton("仅宽表")
- self.radio_single = QRadioButton("仅单文件")
- self.mode_group.addButton(self.radio_both, 0)
- self.mode_group.addButton(self.radio_wide, 1)
- self.mode_group.addButton(self.radio_single, 2)
- self.radio_both.setChecked(True)
- mode_layout.addWidget(self.radio_both)
- mode_layout.addWidget(self.radio_wide)
- mode_layout.addWidget(self.radio_single)
- mode_layout.addStretch()
- output_layout.addLayout(mode_layout)
-
- self.enable_checkbox = QCheckBox("启用计算流程")
+ # 启用步骤
+ self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
- output_layout.addWidget(self.enable_checkbox)
+ layout.addWidget(self.enable_checkbox)
- output_group.setLayout(output_layout)
- main_layout.addWidget(output_group)
+ # 独立运行按钮
+ self.run_btn = QPushButton("独立运行此步骤")
+ self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
+ self.run_btn.clicked.connect(self.run_step)
+ layout.addWidget(self.run_btn)
- # 5. 运行按钮
- self.run_button = QPushButton("立即执行计算")
- self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
- self.run_button.setMinimumHeight(40)
- self.run_button.clicked.connect(self.run_step)
- main_layout.addWidget(self.run_button)
+ layout.addStretch()
+ self.setLayout(layout)
+ # 信号连接:影像文件路径变化时动态更新波段范围
- self.setLayout(main_layout)
-
- def _on_item_changed(self, item: QListWidgetItem):
- if item.checkState() == Qt.Checked:
- bg_color = self.COLOR_RATIO
- for name, ref_item in self.index_checkboxes.items():
- if ref_item is item:
- bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
- break
- item.setBackground(QBrush(bg_color))
- else:
- item.setBackground(QBrush(self.COLOR_RATIO))
-
- def _auto_load_formulas(self):
- if os.path.exists(self.builtin_formula_path):
- self.refresh_formulas(silent=True)
- else:
- print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
-
- def refresh_formulas(self, silent=False):
- path = self.builtin_formula_path
- if not os.path.exists(path):
- if not silent:
- QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
- return
-
- try:
- df = None
- for enc in ('utf-8', 'gbk', 'utf-8-sig'):
- try:
- df = pd.read_csv(path, encoding=enc)
- if 'Formula_Name' in df.columns:
- break
- except Exception:
- continue
-
- if df is None or 'Formula_Name' not in df.columns:
- if not silent:
- QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name' 列")
- return
-
- self._formula_type_map.clear()
- self._formula_coef_map.clear()
- for _, row in df.iterrows():
- name = str(row['Formula_Name']).strip()
- if not name:
- continue
- ftype = str(row.get('Formula_Type', 'ratio')).strip().lower()
- self._formula_type_map[name] = ftype
-
- # Parse Coefficient for concentration formulas
- coef_str = str(row.get('Coefficient', '')).strip()
- if coef_str:
- try:
- coeffs = [float(c.strip()) for c in coef_str.split(',') if c.strip()]
- self._formula_coef_map[name] = coeffs
- except Exception:
- self._formula_coef_map[name] = []
- else:
- self._formula_coef_map[name] = []
-
- self.formula_list.clear()
- self.index_checkboxes.clear()
-
- self._formula_color_map.clear()
- for name, ftype in self._formula_type_map.items():
- item = QListWidgetItem(name, self.formula_list)
- item.setCheckState(Qt.Checked)
- if ftype == 'concentration':
- bg_color = QColor(220, 240, 255)
- else:
- bg_color = self.COLOR_RATIO
- self._formula_color_map[name] = bg_color
- item.setBackground(QBrush(bg_color))
- self.index_checkboxes[name] = item
-
- self.formula_list.adjustSize()
- print(f"✅ 加载 {len(self.index_checkboxes)} 个公式")
-
- except Exception as e:
- if not silent:
- QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
-
- def _select_ratio_only(self):
- for name, item in self.index_checkboxes.items():
- ftype = self._formula_type_map.get(name, 'ratio')
- item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
-
- def _select_conc_only(self):
- for name, item in self.index_checkboxes.items():
- ftype = self._formula_type_map.get(name, 'ratio')
- item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
-
- def select_all_formulas(self):
- for item in self.index_checkboxes.values():
- item.setCheckState(Qt.Checked)
-
- def deselect_all_formulas(self):
- for item in self.index_checkboxes.values():
- item.setCheckState(Qt.Unchecked)
-
- def get_config(self) -> Dict:
- selected = [
- name for name, item in self.index_checkboxes.items()
- if item.checkState() == Qt.Checked
- ]
- # Build coefficient dict for selected formulas
- formula_coefficients = {
- name: self._formula_coef_map.get(name, [])
- for name in selected
- }
- return {
- 'training_csv_path': self.training_data_widget.get_path(),
- 'formula_csv_file': self.builtin_formula_path,
- 'formula_names': selected,
- 'formula_coefficients': formula_coefficients,
- 'enabled': self.enable_checkbox.isChecked(),
- 'output_mode': self.mode_group.checkedId(),
+ def get_config(self):
+ """获取配置"""
+ config = {
+ 'radius': self.radius.value(),
+ 'source_epsg': self.source_epsg.value(),
}
+ # 添加独立运行所需的文件路径
+ deglint_img_path = self.deglint_img_file.get_path()
+ if deglint_img_path:
+ config['deglint_img_path'] = deglint_img_path
+ csv_path = self.csv_file.get_path()
+ if csv_path:
+ config['csv_path'] = csv_path
+ water_mask_path = self.water_mask_file.get_path()
+ if water_mask_path:
+ config['boundary_path'] = water_mask_path
+ glint_mask_path = self.glint_mask_file.get_path()
+ if glint_mask_path:
+ config['glint_mask_path'] = glint_mask_path
+ # 注意:step5_extract_training_spectra 不接受 output_path / training_csv_path
+ # 参数,输出路径由 pipeline 内部根据 training_spectra_dir 自动生成。
+ return config
- def set_config(self, config: Dict):
- if 'training_csv_path' in config:
- self.training_data_widget.set_path(config['training_csv_path'])
- if 'formula_names' in config:
- sel = set(config['formula_names'])
- for name, item in self.index_checkboxes.items():
- item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
- self.enable_checkbox.setChecked(config.get('enabled', True))
- if 'output_mode' in config:
- btn = self.mode_group.button(config['output_mode'])
- if btn:
- btn.setChecked(True)
+ def set_config(self, config):
+ """设置配置"""
+ if 'radius' in config:
+ self.radius.setValue(config['radius'])
+ if 'source_epsg' in config:
+ self.source_epsg.setValue(config['source_epsg'])
+ if 'deglint_img_path' in config:
+ self.deglint_img_file.set_path(config['deglint_img_path'])
+ if 'csv_path' in config:
+ self.csv_file.set_path(config['csv_path'])
+ if 'boundary_path' in config:
+ self.water_mask_file.set_path(config['boundary_path'])
+ if 'glint_mask_path' in config:
+ self.glint_mask_file.set_path(config['glint_mask_path'])
def update_from_config(self, work_dir=None, pipeline=None):
+ """从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转
+
+ Args:
+ work_dir: 工作目录路径
+ pipeline: Pipeline 实例,用于获取步骤1生成的水域掩膜路径
+ """
+ # 保存工作目录引用
if work_dir:
self.work_dir = work_dir
- main = self.window()
- if hasattr(main, 'step5_panel'):
- p5 = main.step5_panel.output_file.get_path()
- if p5:
- if not os.path.isabs(p5):
- p5 = os.path.join(self.work_dir or '', p5)
- p5 = p5.replace('\\', '/')
- self.training_data_widget.set_path(p5)
+ elif hasattr(self, 'work_dir') and self.work_dir:
+ pass
+ else:
+ self.work_dir = None
- def _get_work_dir(self) -> Optional[str]:
+ # 1. 尝试从 Pipeline 获取水体掩膜路径
+ mask_path = None
+ if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path:
+ mask_path = pipeline.water_mask_path
+
+ # 2. 如果 Pipeline 中没有,则尝试直接从 Step1 界面读取
+ main_window = self.window()
+ if not mask_path and hasattr(main_window, 'step1_panel'):
+ if main_window.step1_panel.use_ndwi_radio.isChecked():
+ mask_path = main_window.step1_panel.output_file.get_path()
+ else:
+ mask_path = main_window.step1_panel.mask_file.get_path()
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if mask_path and not os.path.isabs(mask_path):
+ mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
+
+ # 填充水体掩膜路径
+ if mask_path:
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if not os.path.isabs(mask_path):
+ mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
+ self.water_mask_file.set_path(mask_path)
+
+ # 3. 尝试从 Step2 界面读取耀斑掩膜路径
+ main_window = self.window()
+ if hasattr(main_window, 'step2_panel'):
+ glint_path = main_window.step2_panel.output_file.get_path()
+ if glint_path:
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if not os.path.isabs(glint_path):
+ glint_path = os.path.join(self.work_dir or '', glint_path).replace('\\', '/')
+ self.glint_mask_file.set_path(glint_path)
+
+ # 4. 自动填充输出路径(基于工作目录)
if self.work_dir:
- return self.work_dir
- main = self.window()
- if hasattr(main, 'work_dir') and main.work_dir:
- return main.work_dir
- return None
+ output_dir = os.path.join(self.work_dir, "5_training_spectra")
+ os.makedirs(output_dir, exist_ok=True)
+ default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
+ self.output_file.set_path(default_output_path)
+ else:
+ self.output_file.set_path("")
- def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
- coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
- lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y']
-
- x_col, y_col = None, None
- for col in df.columns:
- cl = col.lower()
- if x_col is None and any(c in cl for c in coord_candidates):
- x_col = col
- if y_col is None and any(c in cl for c in lat_candidates):
- y_col = col
-
- if x_col is None and len(df.columns) >= 2:
- x_col = df.columns[0]
- if y_col is None and len(df.columns) >= 2:
- y_col = df.columns[1]
-
- return x_col or 'x_coord', y_col or 'y_coord'
+ # 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板
+ main_window = self.window()
+ if main_window and hasattr(main_window, 'step5_panel'):
+ step4_output_path = main_window.step5_panel.output_file.get_path()
+ if step4_output_path:
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if not os.path.isabs(step4_output_path):
+ step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
+ existing_csv = self.csv_file.get_path()
+ if not existing_csv or not existing_csv.strip():
+ self.csv_file.set_path(step4_output_path)
def run_step(self):
- config = self.get_config()
-
- if not config['enabled']:
- QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
+ """独立运行步骤5"""
+ # 验证输入
+ 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
-
- training_path = config['training_csv_path']
- if not training_path or not os.path.exists(training_path):
- QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
+ if not csv_path:
+ QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件!")
return
-
- formula_names = config['formula_names']
- if not formula_names:
- QMessageBox.warning(self, "提示", "请至少勾选一个公式")
- return
-
- output_mode = config['output_mode']
-
- try:
- from src.core.steps.data_preparation_step import DataPreparationStep
-
- spec_df = pd.read_csv(training_path)
- x_col, y_col = self._get_coord_cols(spec_df)
-
- # 构建 formula_csv_path(使用内置 waterindex.csv)
- formula_csv_path = self.builtin_formula_path
- if not formula_csv_path or not os.path.exists(formula_csv_path):
- # 尝试从 src/gui/model/ 目录找
- possible_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'gui', 'model', 'waterindex.csv')
- if os.path.exists(possible_path):
- formula_csv_path = possible_path
-
- work_dir = self._get_work_dir()
-
- # 调用 DataPreparationStep 的静态方法计算水质指数(宽表输出)
- indices_csv_path = DataPreparationStep.calculate_water_quality_indices(
- training_csv_path=training_path,
- formula_csv_file=formula_csv_path,
- formula_names=formula_names,
- output_file=None, # 不在此处指定输出,由下面的双轨输出逻辑接管
- enabled=True,
- output_dir=work_dir if work_dir else os.getcwd(),
+ if not self.glint_mask_file.get_path():
+ QMessageBox.warning(
+ self,
+ "输入错误",
+ "独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
+ "请提供与去耀斑影像对应的耀斑二值掩膜(一般为步骤2输出的 severe_glint_area.dat)。",
)
+ return
- # 读取计算结果(宽表)
- if indices_csv_path and os.path.exists(indices_csv_path):
- output_df = pd.read_csv(indices_csv_path)
- else:
- output_df = spec_df # fallback
-
- track_a_path = None
- track_b_dir = None
-
- if output_mode in (0, 1):
- track_a_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else "6_water_quality_indices"
- os.makedirs(track_a_dir, exist_ok=True)
- track_a_path = os.path.join(track_a_dir, "training_spectra_indices.csv")
-
- if output_mode in (0, 2):
- track_b_dir = os.path.join(work_dir, "11_12_13_predictions", "Traditional_Indices") if work_dir else "11_12_13_predictions/Traditional_Indices"
- os.makedirs(track_b_dir, exist_ok=True)
-
- saved = []
- if output_mode in (0, 1):
- output_df.to_csv(track_a_path, index=False, float_format='%.6f')
- saved.append(f"宽表: {track_a_path}")
-
- if output_mode in (0, 2):
- coord_x = spec_df[x_col].values if x_col in spec_df.columns else np.arange(len(spec_df))
- coord_y = spec_df[y_col].values if y_col in spec_df.columns else np.zeros(len(spec_df))
-
- for formula_name in formula_names:
- if formula_name not in output_df.columns:
- continue
- single_df = pd.DataFrame({
- 'x_coord': coord_x,
- 'y_coord': coord_y,
- 'value': output_df[formula_name].values,
- })
- safe_name = formula_name.replace('/', '_').replace(' ', '_')
- out_path = os.path.join(track_b_dir, f"{safe_name}_prediction.csv")
- single_df.to_csv(out_path, index=False, float_format='%.6f')
- saved.append(f"单文件目录: {track_b_dir}")
-
- QMessageBox.information(
- self, "计算完成",
- f"已保存 {len(saved)} 个输出目标:\n" + "\n".join(saved)
- )
-
- except ImportError as e:
- QMessageBox.critical(self, "依赖错误", f"无法导入模块:\n{e}")
- except Exception as e:
- import traceback
- QMessageBox.critical(self, "计算失败", f"原因: {str(e)}\n{traceback.format_exc()}")
\ No newline at end of file
+ # 获取主窗口并运行步骤
+ main_window = self.window()
+ if hasattr(main_window, 'run_single_step'):
+ config = {'step5': self.get_config()}
+ main_window.run_single_step('step5', config)
diff --git a/src/gui/panels/step7_panel.py b/src/gui/panels/step7_panel.py
index e049fce..30e3564 100644
--- a/src/gui/panels/step7_panel.py
+++ b/src/gui/panels/step7_panel.py
@@ -1,415 +1,423 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Step7 面板 - 机器学习建模
-"""
-
import os
+import sys
+import pandas as pd
+import numpy as np
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
- QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox,
- QPushButton, QFileDialog, QMessageBox,
+ QWidget, QVBoxLayout, QGroupBox, QGridLayout,
+ QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
+ QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
+ QRadioButton, QButtonGroup
)
from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-# ============================================================
-# 中文映射表(内部键名 -> 显示文本)
-# ============================================================
+def get_resource_path(relative_path: str) -> str:
+ """适配开发与 PyInstaller 环境的路径获取逻辑。"""
+ if hasattr(sys, '_MEIPASS'):
+ internal = os.path.join(sys._MEIPASS, '_internal', relative_path)
+ if os.path.exists(internal):
+ return internal
+ return os.path.join(sys._MEIPASS, relative_path)
-# 预处理方法:内部键 -> 显示文本
-PREPROC_CHINESE = {
- 'None': '无 (None)',
- 'MMS': '最小-最大归一化 (MMS)',
- 'SS': '标度化 (SS)',
- 'SNV': '标准正态变换 (SNV)',
- 'MA': '移动平均 (MA)',
- 'SG': 'Savitzky-Golay (SG)',
- 'MSC': '多元散射校正 (MSC)',
- 'D1': '一阶导数 (D1)',
- 'D2': '二阶导数 (D2)',
- 'DT': '去趋势 (DT)',
- 'CT': '中心化 (CT)',
-}
+ exe_dir = os.path.dirname(sys.executable)
+ internal = os.path.join(exe_dir, '_internal', relative_path)
+ if os.path.exists(internal):
+ return internal
-# 模型类型:内部键 -> 显示文本
-MODEL_CHINESE = {
- # 线性模型
- 'LinearRegression': '多元线性回归 (MLR)',
- 'Ridge': '岭回归 (Ridge)',
- 'Lasso': '套索回归 (Lasso)',
- 'ElasticNet': '弹性网络 (ElasticNet)',
- 'PLS': '偏最小二乘 (PLSR)',
- # 树模型
- 'DecisionTree': '决策树 (CART)',
- 'RF': '随机森林 (RF)',
- 'ExtraTrees': '极端随机树 (ET)',
- 'XGBoost': '极值梯度提升 (XGBoost)',
- 'LightGBM': '轻量梯度提升 (LightGBM)',
- 'CatBoost': '类别梯度提升 (CatBoost)',
- # 集成学习
- 'GradientBoosting': '梯度提升树 (GBDT)',
- 'AdaBoost': '自适应提升 (AdaBoost)',
- # 其他模型
- 'SVR': '支持向量回归 (SVR)',
- 'KNN': 'K近邻回归 (KNN)',
- 'MLP': '多层感知机 (BP神经网络)',
-}
-
-# 数据划分方法:内部键 -> 显示文本
-SPLIT_CHINESE = {
- 'spxy': 'SPXY 算法 (考量X-Y空间)',
- 'ks': 'KS 算法 (考量X空间)',
- 'random': '随机划分 (Random)',
-}
+ base_dir = Path(__file__).resolve().parent.parent / "model"
+ return str(base_dir / os.path.basename(relative_path))
class Step7Panel(QWidget):
- """步骤7:机器学习建模"""
+ COLOR_RATIO = QColor(255, 255, 255)
+ COLOR_CONCENTRATION = QColor(220, 240, 255)
+ COLOR_HEADER = QColor(245, 245, 245)
+
def __init__(self, parent=None):
super().__init__(parent)
+ self.index_checkboxes: Dict[str, QListWidgetItem] = {}
+ self.work_dir: Optional[str] = None
+ self.builtin_formula_path = get_resource_path("waterindex.csv")
+ self._formula_type_map: Dict[str, str] = {}
+ self._formula_color_map: Dict[str, QColor] = {}
+ self._formula_coef_map: Dict[str, List[float]] = {}
+
self.init_ui()
+ self._auto_load_formulas()
def init_ui(self):
- layout = QVBoxLayout()
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(10)
- # 标题
+ # 1. 公式配置源 (只读)
+ path_group = QGroupBox("公式配置源 (内置)")
+ path_layout = QVBoxLayout()
+ self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
+ self.formula_csv_widget.set_path(self.builtin_formula_path)
+ self.formula_csv_widget.set_read_only(True)
+ self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
+ path_layout.addWidget(self.formula_csv_widget)
+ path_group.setLayout(path_layout)
+ main_layout.addWidget(path_group)
+ # 2. 训练数据输入
+ input_group = QGroupBox("输入样本数据")
+ input_layout = QVBoxLayout()
+ self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
+ input_layout.addWidget(self.training_data_widget)
+ input_group.setLayout(input_layout)
+ main_layout.addWidget(input_group)
- # 训练数据文件(用于独立运行)
- self.training_csv_file = FileSelectWidget(
- "训练数据:",
- "CSV Files (*.csv);;All Files (*.*)"
- )
- layout.addWidget(self.training_csv_file)
+ # 3. 公式选择区 (分组 ListWidget)
+ self.formula_group = QGroupBox("待计算水质指数勾选")
+ formula_outer_layout = QVBoxLayout()
- # 机器学习模型页面
- self.ml_page = QWidget()
- self.create_ml_page()
- layout.addWidget(self.ml_page)
+ btn_layout = QHBoxLayout()
+ self.select_all_btn = QPushButton("全选")
+ self.deselect_all_btn = QPushButton("清空")
+ self.select_ratio_btn = QPushButton("仅选比值型")
+ self.select_conc_btn = QPushButton("仅选浓度型")
+ self.select_all_btn.clicked.connect(self.select_all_formulas)
+ self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
+ self.select_ratio_btn.clicked.connect(self._select_ratio_only)
+ self.select_conc_btn.clicked.connect(self._select_conc_only)
+ btn_layout.addWidget(self.select_all_btn)
+ btn_layout.addWidget(self.deselect_all_btn)
+ btn_layout.addWidget(self.select_ratio_btn)
+ btn_layout.addWidget(self.select_conc_btn)
+ btn_layout.addStretch()
- # 输出文件路径
- self.output_path = FileSelectWidget(
- "输出文件:",
- "CSV Files (*.csv);;All Files (*.*)",
- mode="save"
- )
- self.output_path.line_edit.setPlaceholderText("自动生成,或手动指定输出文件路径...")
- self.output_path.browse_btn.clicked.disconnect()
- self.output_path.browse_btn.clicked.connect(self.browse_output_path)
- layout.addWidget(self.output_path)
+ self.refresh_button = QPushButton("重新加载")
+ self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
+ btn_layout.addWidget(self.refresh_button)
- # 启用步骤
- self.enable_checkbox = QCheckBox("启用此步骤")
- self.enable_checkbox.setChecked(False)
- layout.addWidget(self.enable_checkbox)
+ formula_outer_layout.addLayout(btn_layout)
- # 独立运行按钮
- self.run_btn = QPushButton("独立运行此步骤")
- self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
- self.run_btn.clicked.connect(self.run_step)
- layout.addWidget(self.run_btn)
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setMinimumHeight(280)
+ self.scroll_content = QWidget()
+ self.formula_layout = QVBoxLayout(self.scroll_content)
+ self.formula_layout.setContentsMargins(4, 4, 4, 4)
+ self.formula_layout.setSpacing(2)
+ self.formula_layout.setAlignment(Qt.AlignTop)
- layout.addStretch()
- self.setLayout(layout)
+ self.formula_list = QListWidget()
+ self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
+ self.formula_list.itemChanged.connect(self._on_item_changed)
+ self.formula_layout.addWidget(self.formula_list)
- def create_ml_page(self):
- """创建机器学习模型页面"""
- layout = QVBoxLayout()
+ scroll.setWidget(self.scroll_content)
+ formula_outer_layout.addWidget(scroll)
- # 参数设置
- params_group = QGroupBox("训练参数")
- params_layout = QFormLayout()
+ self.formula_group.setLayout(formula_outer_layout)
+ main_layout.addWidget(self.formula_group)
- self.feature_start = QLineEdit()
- self.feature_start.setText("374.285004")
- params_layout.addRow("特征起始列:", self.feature_start)
+ # 4. 输出选项
+ output_group = QGroupBox("输出模式")
+ output_layout = QVBoxLayout()
- self.cv_folds = QSpinBox()
- self.cv_folds.setRange(2, 10)
- self.cv_folds.setValue(3)
- params_layout.addRow("交叉验证折数:", self.cv_folds)
+ mode_layout = QHBoxLayout()
+ self.mode_group = QButtonGroup()
+ self.radio_both = QRadioButton("两者皆出")
+ self.radio_wide = QRadioButton("仅宽表")
+ self.radio_single = QRadioButton("仅单文件")
+ self.mode_group.addButton(self.radio_both, 0)
+ self.mode_group.addButton(self.radio_wide, 1)
+ self.mode_group.addButton(self.radio_single, 2)
+ self.radio_both.setChecked(True)
+ mode_layout.addWidget(self.radio_both)
+ mode_layout.addWidget(self.radio_wide)
+ mode_layout.addWidget(self.radio_single)
+ mode_layout.addStretch()
+ output_layout.addLayout(mode_layout)
- params_group.setLayout(params_layout)
- layout.addWidget(params_group)
+ self.enable_checkbox = QCheckBox("启用计算流程")
+ self.enable_checkbox.setChecked(True)
+ output_layout.addWidget(self.enable_checkbox)
- # 预处理方法 - 多选
- preproc_group = QGroupBox("预处理方法 (可多选)")
- preproc_layout = QVBoxLayout()
+ output_group.setLayout(output_layout)
+ main_layout.addWidget(output_group)
- preproc_grid = QGridLayout()
- self.preproc_checkboxes = {}
- preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT']
+ # 5. 运行按钮
+ self.run_button = QPushButton("立即执行计算")
+ self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
+ self.run_button.setMinimumHeight(40)
+ self.run_button.clicked.connect(self.run_step)
+ main_layout.addWidget(self.run_button)
- for i, method in enumerate(preproc_methods):
- checkbox = QCheckBox(PREPROC_CHINESE.get(method, method))
- checkbox.setChecked(False)
- self.preproc_checkboxes[method] = checkbox
- preproc_grid.addWidget(checkbox, i // 4, i % 4)
+ self.setLayout(main_layout)
- button_layout = QHBoxLayout()
- select_all_btn = QPushButton("全选")
- deselect_all_btn = QPushButton("全不选")
- select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True))
- deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False))
- button_layout.addWidget(select_all_btn)
- button_layout.addWidget(deselect_all_btn)
- button_layout.addStretch()
-
- preproc_layout.addLayout(preproc_grid)
- preproc_layout.addLayout(button_layout)
- preproc_group.setLayout(preproc_layout)
- layout.addWidget(preproc_group)
-
- # 模型选择 - 多选
- model_group = QGroupBox("模型类型 (可多选)")
- model_layout = QVBoxLayout()
-
- model_grid = QGridLayout()
- self.model_checkboxes = {}
-
- model_groups = [
- ("【线性模型】", ['LinearRegression', 'Ridge', 'Lasso', 'ElasticNet', 'PLS']),
- ("【树模型】", ['DecisionTree', 'RF', 'ExtraTrees', 'XGBoost', 'LightGBM', 'CatBoost']),
- ("【集成学习】", ['GradientBoosting', 'AdaBoost']),
- ("【其他模型】", ['SVR', 'KNN', 'MLP'])
- ]
-
- row = 0
- for group_name, models in model_groups:
- group_label = QLabel(f"{group_name}")
- group_label.setStyleSheet(
- f"background-color: {ModernStylesheet.COLORS['hover']}; "
- f"padding: 5px; border: 1px solid {ModernStylesheet.COLORS['border_light']}; "
- f"border-radius: 3px;"
- )
- model_grid.addWidget(group_label, row, 0, 1, 4)
- row += 1
-
- for i, model in enumerate(models):
- checkbox = QCheckBox(MODEL_CHINESE.get(model, model))
- checkbox.setChecked(False)
- self.model_checkboxes[model] = checkbox
- model_grid.addWidget(checkbox, row, i % 4)
- if (i + 1) % 4 == 0:
- row += 1
-
- row += 1
-
- model_button_layout = QHBoxLayout()
- model_select_all = QPushButton("全选")
- model_deselect_all = QPushButton("全不选")
- model_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, True))
- model_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, False))
- model_button_layout.addWidget(model_select_all)
- model_button_layout.addWidget(model_deselect_all)
- model_button_layout.addStretch()
-
- model_layout.addLayout(model_grid)
- model_layout.addLayout(model_button_layout)
- model_group.setLayout(model_layout)
- layout.addWidget(model_group)
-
- # 数据划分方法 - 多选
- split_group = QGroupBox("数据划分方法 (可多选)")
- split_layout = QVBoxLayout()
-
- split_grid = QGridLayout()
- self.split_checkboxes = {}
- split_methods = ['spxy', 'ks', 'random']
-
- for i, method in enumerate(split_methods):
- checkbox = QCheckBox(SPLIT_CHINESE.get(method, method))
- checkbox.setChecked(False)
- self.split_checkboxes[method] = checkbox
- split_grid.addWidget(checkbox, 0, i)
-
- split_button_layout = QHBoxLayout()
- split_select_all = QPushButton("全选")
- split_deselect_all = QPushButton("全不选")
- split_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, True))
- split_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, False))
- split_button_layout.addWidget(split_select_all)
- split_button_layout.addWidget(split_deselect_all)
- split_button_layout.addStretch()
-
- split_layout.addLayout(split_grid)
- split_layout.addLayout(split_button_layout)
- split_group.setLayout(split_layout)
- layout.addWidget(split_group)
-
- self.ml_page.setLayout(layout)
-
- def _toggle_checkboxes(self, checkboxes_dict, checked):
- """统一设置checkbox状态"""
- for checkbox in checkboxes_dict.values():
- checkbox.setChecked(checked)
-
- def _get_default_work_dir(self):
- """获取 work_dir,优先用 panel 自身缓存的,否则尝试从主窗口取"""
- if hasattr(self, 'work_dir') and self.work_dir:
- return str(self.work_dir)
- mw = self.window()
- if mw and hasattr(mw, 'work_dir') and mw.work_dir:
- return str(mw.work_dir)
- return ""
-
- def browse_output_path(self):
- """浏览输出文件路径(保存对话框)"""
- current = self.output_path.get_path().strip()
- if current:
- initial_dir = os.path.dirname(current)
- initial_file = os.path.basename(current)
+ def _on_item_changed(self, item: QListWidgetItem):
+ if item.checkState() == Qt.Checked:
+ bg_color = self.COLOR_RATIO
+ for name, ref_item in self.index_checkboxes.items():
+ if ref_item is item:
+ bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
+ break
+ item.setBackground(QBrush(bg_color))
else:
- initial_dir = ""
- initial_file = ""
+ item.setBackground(QBrush(self.COLOR_RATIO))
- if not initial_dir or not os.path.isdir(initial_dir):
- # 默认定位到 indices 目录
- work_dir = self._get_default_work_dir()
- initial_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else ""
- if initial_dir and not os.path.isdir(initial_dir):
- os.makedirs(initial_dir, exist_ok=True)
-
- file_path, _ = QFileDialog.getSaveFileName(
- self, "保存输出文件", os.path.join(initial_dir, initial_file) if initial_file else initial_dir,
- "CSV Files (*.csv);;All Files (*.*)"
- )
- if file_path:
- self.output_path.set_path(file_path)
-
- def get_config(self):
- """获取配置"""
- preprocessing_methods = [
- method for method, checkbox in self.preproc_checkboxes.items()
- if checkbox.isChecked()
- ]
- model_names = [
- model for model, checkbox in self.model_checkboxes.items()
- if checkbox.isChecked()
- ]
- split_methods = [
- method for method, checkbox in self.split_checkboxes.items()
- if checkbox.isChecked()
- ]
-
- config = {
- 'feature_start_column': self.feature_start.text(),
- 'preprocessing_methods': preprocessing_methods if preprocessing_methods else ['None'],
- 'model_names': model_names if model_names else ['SVR'],
- 'split_methods': split_methods if split_methods else ['random'],
- 'cv_folds': self.cv_folds.value()
- }
- training_csv_path = self.training_csv_file.get_path()
- if training_csv_path:
- config['training_csv_path'] = training_csv_path
- output_path = self.output_path.get_path()
- if output_path:
- config['output_path'] = output_path
- return config
-
- def set_config(self, config):
- """设置配置"""
- if 'feature_start_column' in config:
- self.feature_start.setText(str(config['feature_start_column']))
- if 'cv_folds' in config:
- self.cv_folds.setValue(config['cv_folds'])
- if 'preprocessing_methods' in config:
- methods = config['preprocessing_methods']
- for method, checkbox in self.preproc_checkboxes.items():
- checkbox.setChecked(method in methods)
- if 'model_names' in config:
- models = config['model_names']
- for model, checkbox in self.model_checkboxes.items():
- checkbox.setChecked(model in models)
- if 'split_methods' in config:
- methods = config['split_methods']
- for method, checkbox in self.split_checkboxes.items():
- checkbox.setChecked(method in methods)
- if 'training_csv_path' in config:
- self.training_csv_file.set_path(config['training_csv_path'])
- if 'output_path' in config:
- self.output_path.set_path(config['output_path'])
-
- def update_from_config(self, work_dir=None, pipeline=None):
- """从全局配置自动填充训练数据和输出路径
-
- Args:
- work_dir: 工作目录路径
- pipeline: Pipeline 实例(未使用,保留接口兼容性)
- """
- if work_dir:
- self.work_dir = work_dir
- elif hasattr(self, 'work_dir') and self.work_dir:
- pass
+ def _auto_load_formulas(self):
+ if os.path.exists(self.builtin_formula_path):
+ self.refresh_formulas(silent=True)
else:
- self.work_dir = None
+ print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
- # 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径
- main_window = self.window()
- if hasattr(main_window, 'step5_panel'):
- # 优先直接从 Step5 的输出 widget 读取
- step5_output = main_window.step5_panel.output_file.get_path()
- if step5_output:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(step5_output):
- step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
- self.training_csv_file.set_path(step5_output)
- elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'):
- # 回退:从 Step5 的 config 字典中查找可能的键名
- step5_cfg = main_window.step5_panel.get_config()
- step5_csv = (
- step5_cfg.get('training_csv_path')
- or step5_cfg.get('output_file')
- or step5_cfg.get('csv_path')
- or step5_cfg.get('output_csv')
- )
- if step5_csv:
- # 若为相对路径,使用 work_dir 合成为绝对路径
- if not os.path.isabs(step5_csv):
- step5_csv = os.path.join(self.work_dir or '', step5_csv).replace('\\', '/')
- self.training_csv_file.set_path(step5_csv)
-
- # 2. 自动填充输出文件路径(基于工作目录和输入文件名)
- # 输入是 training_spectra.csv → 输出 {work_dir}/6_water_quality_indices/training_spectra_indices.csv
- # 输入是 sampling_spectra.csv → 输出 {work_dir}/6_water_quality_indices/sampling_spectra_indices.csv
- if self.work_dir:
- indices_dir = os.path.join(self.work_dir, "6_water_quality_indices")
- os.makedirs(indices_dir, exist_ok=True)
- training_csv = self.training_csv_file.get_path()
- if training_csv:
- basename = os.path.splitext(os.path.basename(training_csv))[0]
- output_file = f"{basename}_indices.csv"
- else:
- output_file = "water_quality_indices.csv"
- output_path = os.path.join(indices_dir, output_file).replace('\\', '/')
- self.output_path.set_path(output_path)
- else:
- self.output_path.set_path("")
-
- def run_step(self):
- """独立运行步骤7"""
- training_csv_path = self.training_csv_file.get_path()
- if not training_csv_path:
- QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
+ def refresh_formulas(self, silent=False):
+ path = self.builtin_formula_path
+ if not os.path.exists(path):
+ if not silent:
+ QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
return
- main_window = self.window()
- if hasattr(main_window, 'run_single_step'):
- config = {'step7': self.get_config()}
- main_window.run_single_step('step7', config)
+ try:
+ df = None
+ for enc in ('utf-8', 'gbk', 'utf-8-sig'):
+ try:
+ df = pd.read_csv(path, encoding=enc)
+ if 'Formula_Name' in df.columns:
+ break
+ except Exception:
+ continue
- def get_training_params(self):
- """获取模型训练参数"""
- return {
- 'pipeline_type': 'machine_learning',
- 'feature_start': float(self.feature_start.text()),
- 'cv_folds': self.cv_folds.value(),
- 'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()],
- 'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()],
- 'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()]
+ if df is None or 'Formula_Name' not in df.columns:
+ if not silent:
+ QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name' 列")
+ return
+
+ self._formula_type_map.clear()
+ self._formula_coef_map.clear()
+ for _, row in df.iterrows():
+ name = str(row['Formula_Name']).strip()
+ if not name:
+ continue
+ ftype = str(row.get('Formula_Type', 'ratio')).strip().lower()
+ self._formula_type_map[name] = ftype
+
+ # Parse Coefficient for concentration formulas
+ coef_str = str(row.get('Coefficient', '')).strip()
+ if coef_str:
+ try:
+ coeffs = [float(c.strip()) for c in coef_str.split(',') if c.strip()]
+ self._formula_coef_map[name] = coeffs
+ except Exception:
+ self._formula_coef_map[name] = []
+ else:
+ self._formula_coef_map[name] = []
+
+ self.formula_list.clear()
+ self.index_checkboxes.clear()
+
+ self._formula_color_map.clear()
+ for name, ftype in self._formula_type_map.items():
+ item = QListWidgetItem(name, self.formula_list)
+ item.setCheckState(Qt.Checked)
+ if ftype == 'concentration':
+ bg_color = QColor(220, 240, 255)
+ else:
+ bg_color = self.COLOR_RATIO
+ self._formula_color_map[name] = bg_color
+ item.setBackground(QBrush(bg_color))
+ self.index_checkboxes[name] = item
+
+ self.formula_list.adjustSize()
+ print(f"✅ 加载 {len(self.index_checkboxes)} 个公式")
+
+ except Exception as e:
+ if not silent:
+ QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
+
+ def _select_ratio_only(self):
+ for name, item in self.index_checkboxes.items():
+ ftype = self._formula_type_map.get(name, 'ratio')
+ item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
+
+ def _select_conc_only(self):
+ for name, item in self.index_checkboxes.items():
+ ftype = self._formula_type_map.get(name, 'ratio')
+ item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
+
+ def select_all_formulas(self):
+ for item in self.index_checkboxes.values():
+ item.setCheckState(Qt.Checked)
+
+ def deselect_all_formulas(self):
+ for item in self.index_checkboxes.values():
+ item.setCheckState(Qt.Unchecked)
+
+ def get_config(self) -> Dict:
+ selected = [
+ name for name, item in self.index_checkboxes.items()
+ if item.checkState() == Qt.Checked
+ ]
+ # Build coefficient dict for selected formulas
+ formula_coefficients = {
+ name: self._formula_coef_map.get(name, [])
+ for name in selected
}
+ return {
+ 'training_csv_path': self.training_data_widget.get_path(),
+ 'formula_csv_file': self.builtin_formula_path,
+ 'formula_names': selected,
+ 'formula_coefficients': formula_coefficients,
+ 'enabled': self.enable_checkbox.isChecked(),
+ 'output_mode': self.mode_group.checkedId(),
+ }
+
+ def set_config(self, config: Dict):
+ if 'training_csv_path' in config:
+ self.training_data_widget.set_path(config['training_csv_path'])
+ if 'formula_names' in config:
+ sel = set(config['formula_names'])
+ for name, item in self.index_checkboxes.items():
+ item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
+ self.enable_checkbox.setChecked(config.get('enabled', True))
+ if 'output_mode' in config:
+ btn = self.mode_group.button(config['output_mode'])
+ if btn:
+ btn.setChecked(True)
+
+ def update_from_config(self, work_dir=None, pipeline=None):
+ if work_dir:
+ self.work_dir = work_dir
+ main = self.window()
+ if hasattr(main, 'step6_panel'):
+ p5 = main.step6_panel.output_file.get_path()
+ if p5:
+ if not os.path.isabs(p5):
+ p5 = os.path.join(self.work_dir or '', p5)
+ p5 = p5.replace('\\', '/')
+ self.training_data_widget.set_path(p5)
+
+ def _get_work_dir(self) -> Optional[str]:
+ if self.work_dir:
+ return self.work_dir
+ main = self.window()
+ if hasattr(main, 'work_dir') and main.work_dir:
+ return main.work_dir
+ return None
+
+ def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
+ coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
+ lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y']
+
+ x_col, y_col = None, None
+ for col in df.columns:
+ cl = col.lower()
+ if x_col is None and any(c in cl for c in coord_candidates):
+ x_col = col
+ if y_col is None and any(c in cl for c in lat_candidates):
+ y_col = col
+
+ if x_col is None and len(df.columns) >= 2:
+ x_col = df.columns[0]
+ if y_col is None and len(df.columns) >= 2:
+ y_col = df.columns[1]
+
+ return x_col or 'x_coord', y_col or 'y_coord'
+
+ def run_step(self):
+ config = self.get_config()
+
+ if not config['enabled']:
+ QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
+ return
+
+ training_path = config['training_csv_path']
+ if not training_path or not os.path.exists(training_path):
+ QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
+ return
+
+ formula_names = config['formula_names']
+ if not formula_names:
+ QMessageBox.warning(self, "提示", "请至少勾选一个公式")
+ return
+
+ output_mode = config['output_mode']
+
+ try:
+ from src.core.steps.data_preparation_step import DataPreparationStep
+
+ spec_df = pd.read_csv(training_path)
+ x_col, y_col = self._get_coord_cols(spec_df)
+
+ # 构建 formula_csv_path(使用内置 waterindex.csv)
+ formula_csv_path = self.builtin_formula_path
+ if not formula_csv_path or not os.path.exists(formula_csv_path):
+ # 尝试从 src/gui/model/ 目录找
+ possible_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'gui', 'model', 'waterindex.csv')
+ if os.path.exists(possible_path):
+ formula_csv_path = possible_path
+
+ work_dir = self._get_work_dir()
+
+ # 调用 DataPreparationStep 的静态方法计算水质指数(宽表输出)
+ indices_csv_path = DataPreparationStep.calculate_water_quality_indices(
+ training_csv_path=training_path,
+ formula_csv_file=formula_csv_path,
+ formula_names=formula_names,
+ output_file=None, # 不在此处指定输出,由下面的双轨输出逻辑接管
+ enabled=True,
+ output_dir=work_dir if work_dir else os.getcwd(),
+ )
+
+ # 读取计算结果(宽表)
+ if indices_csv_path and os.path.exists(indices_csv_path):
+ output_df = pd.read_csv(indices_csv_path)
+ else:
+ output_df = spec_df # fallback
+
+ track_a_path = None
+ track_b_dir = None
+
+ if output_mode in (0, 1):
+ track_a_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else "6_water_quality_indices"
+ os.makedirs(track_a_dir, exist_ok=True)
+ track_a_path = os.path.join(track_a_dir, "training_spectra_indices.csv")
+
+ if output_mode in (0, 2):
+ track_b_dir = os.path.join(work_dir, "11_12_13_predictions", "Traditional_Indices") if work_dir else "11_12_13_predictions/Traditional_Indices"
+ os.makedirs(track_b_dir, exist_ok=True)
+
+ saved = []
+ if output_mode in (0, 1):
+ output_df.to_csv(track_a_path, index=False, float_format='%.6f')
+ saved.append(f"宽表: {track_a_path}")
+
+ if output_mode in (0, 2):
+ coord_x = spec_df[x_col].values if x_col in spec_df.columns else np.arange(len(spec_df))
+ coord_y = spec_df[y_col].values if y_col in spec_df.columns else np.zeros(len(spec_df))
+
+ for formula_name in formula_names:
+ if formula_name not in output_df.columns:
+ continue
+ single_df = pd.DataFrame({
+ 'x_coord': coord_x,
+ 'y_coord': coord_y,
+ 'value': output_df[formula_name].values,
+ })
+ safe_name = formula_name.replace('/', '_').replace(' ', '_')
+ out_path = os.path.join(track_b_dir, f"{safe_name}_prediction.csv")
+ single_df.to_csv(out_path, index=False, float_format='%.6f')
+ saved.append(f"单文件目录: {track_b_dir}")
+
+ QMessageBox.information(
+ self, "计算完成",
+ f"已保存 {len(saved)} 个输出目标:\n" + "\n".join(saved)
+ )
+
+ except ImportError as e:
+ QMessageBox.critical(self, "依赖错误", f"无法导入模块:\n{e}")
+ except Exception as e:
+ import traceback
+ QMessageBox.critical(self, "计算失败", f"原因: {str(e)}\n{traceback.format_exc()}")
\ No newline at end of file
diff --git a/src/gui/panels/step8_panel.py b/src/gui/panels/step8_panel.py
index 092f59b..e4932ba 100644
--- a/src/gui/panels/step8_panel.py
+++ b/src/gui/panels/step8_panel.py
@@ -1,424 +1,415 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Step7 面板 - 机器学习建模
+"""
+
import os
-import sys
-import pandas as pd
-import numpy as np
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QGridLayout,
- QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
- QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
- QRadioButton, QButtonGroup
+ QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
+ QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox,
+ QPushButton, QFileDialog, QMessageBox,
)
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-def get_resource_path(relative_path: str) -> str:
- """适配开发与 PyInstaller 环境的路径获取逻辑。"""
- if hasattr(sys, '_MEIPASS'):
- internal = os.path.join(sys._MEIPASS, '_internal', relative_path)
- if os.path.exists(internal):
- return internal
- return os.path.join(sys._MEIPASS, relative_path)
+# ============================================================
+# 中文映射表(内部键名 -> 显示文本)
+# ============================================================
- exe_dir = os.path.dirname(sys.executable)
- internal = os.path.join(exe_dir, '_internal', relative_path)
- if os.path.exists(internal):
- return internal
+# 预处理方法:内部键 -> 显示文本
+PREPROC_CHINESE = {
+ 'None': '无 (None)',
+ 'MMS': '最小-最大归一化 (MMS)',
+ 'SS': '标度化 (SS)',
+ 'SNV': '标准正态变换 (SNV)',
+ 'MA': '移动平均 (MA)',
+ 'SG': 'Savitzky-Golay (SG)',
+ 'MSC': '多元散射校正 (MSC)',
+ 'D1': '一阶导数 (D1)',
+ 'D2': '二阶导数 (D2)',
+ 'DT': '去趋势 (DT)',
+ 'CT': '中心化 (CT)',
+}
- base_dir = Path(__file__).resolve().parent.parent / "model"
- return str(base_dir / os.path.basename(relative_path))
+# 模型类型:内部键 -> 显示文本
+MODEL_CHINESE = {
+ # 线性模型
+ 'LinearRegression': '多元线性回归 (MLR)',
+ 'Ridge': '岭回归 (Ridge)',
+ 'Lasso': '套索回归 (Lasso)',
+ 'ElasticNet': '弹性网络 (ElasticNet)',
+ 'PLS': '偏最小二乘 (PLSR)',
+ # 树模型
+ 'DecisionTree': '决策树 (CART)',
+ 'RF': '随机森林 (RF)',
+ 'ExtraTrees': '极端随机树 (ET)',
+ 'XGBoost': '极值梯度提升 (XGBoost)',
+ 'LightGBM': '轻量梯度提升 (LightGBM)',
+ 'CatBoost': '类别梯度提升 (CatBoost)',
+ # 集成学习
+ 'GradientBoosting': '梯度提升树 (GBDT)',
+ 'AdaBoost': '自适应提升 (AdaBoost)',
+ # 其他模型
+ 'SVR': '支持向量回归 (SVR)',
+ 'KNN': 'K近邻回归 (KNN)',
+ 'MLP': '多层感知机 (BP神经网络)',
+}
+
+# 数据划分方法:内部键 -> 显示文本
+SPLIT_CHINESE = {
+ 'spxy': 'SPXY 算法 (考量X-Y空间)',
+ 'ks': 'KS 算法 (考量X空间)',
+ 'random': '随机划分 (Random)',
+}
class Step8Panel(QWidget):
- COLOR_RATIO = QColor(255, 255, 255)
- COLOR_CONCENTRATION = QColor(220, 240, 255)
- COLOR_HEADER = QColor(245, 245, 245)
-
+ """步骤8:水质参数指数计算"""
def __init__(self, parent=None):
super().__init__(parent)
- self.index_checkboxes: Dict[str, QListWidgetItem] = {}
- self.work_dir: Optional[str] = None
- self.builtin_formula_path = get_resource_path("waterindex.csv")
- self._formula_type_map: Dict[str, str] = {}
- self._formula_color_map: Dict[str, QColor] = {}
- self._formula_coef_map: Dict[str, List[float]] = {}
-
self.init_ui()
- self._auto_load_formulas()
def init_ui(self):
- main_layout = QVBoxLayout()
- main_layout.setContentsMargins(20, 20, 20, 20)
- main_layout.setSpacing(10)
+ layout = QVBoxLayout()
- # 1. 公式配置源 (只读)
- path_group = QGroupBox("公式配置源 (内置)")
- path_layout = QVBoxLayout()
- self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
- self.formula_csv_widget.set_path(self.builtin_formula_path)
- self.formula_csv_widget.set_read_only(True)
- self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
- path_layout.addWidget(self.formula_csv_widget)
- path_group.setLayout(path_layout)
- main_layout.addWidget(path_group)
+ # 标题
- # 2. 训练数据输入
- input_group = QGroupBox("输入样本数据")
- input_layout = QVBoxLayout()
- self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
- input_layout.addWidget(self.training_data_widget)
- input_group.setLayout(input_layout)
- main_layout.addWidget(input_group)
- # 3. 公式选择区 (分组 ListWidget)
- self.formula_group = QGroupBox("待计算水质指数勾选")
- formula_outer_layout = QVBoxLayout()
+ # 训练数据文件(用于独立运行)
+ self.training_csv_file = FileSelectWidget(
+ "训练数据:",
+ "CSV Files (*.csv);;All Files (*.*)"
+ )
+ layout.addWidget(self.training_csv_file)
- btn_layout = QHBoxLayout()
- self.select_all_btn = QPushButton("全选")
- self.deselect_all_btn = QPushButton("清空")
- self.select_ratio_btn = QPushButton("仅选比值型")
- self.select_conc_btn = QPushButton("仅选浓度型")
- self.select_all_btn.clicked.connect(self.select_all_formulas)
- self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
- self.select_ratio_btn.clicked.connect(self._select_ratio_only)
- self.select_conc_btn.clicked.connect(self._select_conc_only)
- btn_layout.addWidget(self.select_all_btn)
- btn_layout.addWidget(self.deselect_all_btn)
- btn_layout.addWidget(self.select_ratio_btn)
- btn_layout.addWidget(self.select_conc_btn)
- btn_layout.addStretch()
+ # 机器学习模型页面
+ self.ml_page = QWidget()
+ self.create_ml_page()
+ layout.addWidget(self.ml_page)
- self.refresh_button = QPushButton("重新加载")
- self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
- btn_layout.addWidget(self.refresh_button)
+ # 输出文件路径
+ self.output_path = FileSelectWidget(
+ "输出文件:",
+ "CSV Files (*.csv);;All Files (*.*)",
+ mode="save"
+ )
+ self.output_path.line_edit.setPlaceholderText("自动生成,或手动指定输出文件路径...")
+ self.output_path.browse_btn.clicked.disconnect()
+ self.output_path.browse_btn.clicked.connect(self.browse_output_path)
+ layout.addWidget(self.output_path)
- formula_outer_layout.addLayout(btn_layout)
+ # 启用步骤
+ self.enable_checkbox = QCheckBox("启用此步骤")
+ self.enable_checkbox.setChecked(False)
+ layout.addWidget(self.enable_checkbox)
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setMinimumHeight(280)
- self.scroll_content = QWidget()
- self.formula_layout = QVBoxLayout(self.scroll_content)
- self.formula_layout.setContentsMargins(4, 4, 4, 4)
- self.formula_layout.setSpacing(2)
- self.formula_layout.setAlignment(Qt.AlignTop)
+ # 独立运行按钮
+ self.run_btn = QPushButton("独立运行此步骤")
+ self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
+ self.run_btn.clicked.connect(self.run_step)
+ layout.addWidget(self.run_btn)
- self.formula_list = QListWidget()
- self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
- self.formula_list.itemChanged.connect(self._on_item_changed)
- self.formula_layout.addWidget(self.formula_list)
+ layout.addStretch()
+ self.setLayout(layout)
- scroll.setWidget(self.scroll_content)
- formula_outer_layout.addWidget(scroll)
+ def create_ml_page(self):
+ """创建机器学习模型页面"""
+ layout = QVBoxLayout()
- self.formula_group.setLayout(formula_outer_layout)
- main_layout.addWidget(self.formula_group)
+ # 参数设置
+ params_group = QGroupBox("训练参数")
+ params_layout = QFormLayout()
- # 4. 输出选项
- output_group = QGroupBox("输出模式")
- output_layout = QVBoxLayout()
+ self.feature_start = QLineEdit()
+ self.feature_start.setText("374.285004")
+ params_layout.addRow("特征起始列:", self.feature_start)
- mode_layout = QHBoxLayout()
- self.mode_group = QButtonGroup()
- self.radio_both = QRadioButton("两者皆出")
- self.radio_wide = QRadioButton("仅宽表")
- self.radio_single = QRadioButton("仅单文件")
- self.mode_group.addButton(self.radio_both, 0)
- self.mode_group.addButton(self.radio_wide, 1)
- self.mode_group.addButton(self.radio_single, 2)
- self.radio_both.setChecked(True)
- mode_layout.addWidget(self.radio_both)
- mode_layout.addWidget(self.radio_wide)
- mode_layout.addWidget(self.radio_single)
- mode_layout.addStretch()
- output_layout.addLayout(mode_layout)
+ self.cv_folds = QSpinBox()
+ self.cv_folds.setRange(2, 10)
+ self.cv_folds.setValue(3)
+ params_layout.addRow("交叉验证折数:", self.cv_folds)
- self.enable_checkbox = QCheckBox("启用计算流程")
- self.enable_checkbox.setChecked(True)
- output_layout.addWidget(self.enable_checkbox)
+ params_group.setLayout(params_layout)
+ layout.addWidget(params_group)
- output_group.setLayout(output_layout)
- main_layout.addWidget(output_group)
+ # 预处理方法 - 多选
+ preproc_group = QGroupBox("预处理方法 (可多选)")
+ preproc_layout = QVBoxLayout()
- # 5. 运行按钮
- self.run_button = QPushButton("立即执行计算")
- self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
- self.run_button.setMinimumHeight(40)
- self.run_button.clicked.connect(self.run_step)
- main_layout.addWidget(self.run_button)
+ preproc_grid = QGridLayout()
+ self.preproc_checkboxes = {}
+ preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT']
- self.setLayout(main_layout)
+ for i, method in enumerate(preproc_methods):
+ checkbox = QCheckBox(PREPROC_CHINESE.get(method, method))
+ checkbox.setChecked(False)
+ self.preproc_checkboxes[method] = checkbox
+ preproc_grid.addWidget(checkbox, i // 4, i % 4)
- def _on_item_changed(self, item: QListWidgetItem):
- if item.checkState() == Qt.Checked:
- bg_color = self.COLOR_RATIO
- for name, ref_item in self.index_checkboxes.items():
- if ref_item is item:
- bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
- break
- item.setBackground(QBrush(bg_color))
- else:
- item.setBackground(QBrush(self.COLOR_RATIO))
+ button_layout = QHBoxLayout()
+ select_all_btn = QPushButton("全选")
+ deselect_all_btn = QPushButton("全不选")
+ select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True))
+ deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False))
+ button_layout.addWidget(select_all_btn)
+ button_layout.addWidget(deselect_all_btn)
+ button_layout.addStretch()
- def _auto_load_formulas(self):
- if os.path.exists(self.builtin_formula_path):
- self.refresh_formulas(silent=True)
- else:
- print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
+ preproc_layout.addLayout(preproc_grid)
+ preproc_layout.addLayout(button_layout)
+ preproc_group.setLayout(preproc_layout)
+ layout.addWidget(preproc_group)
- def refresh_formulas(self, silent=False):
- path = self.builtin_formula_path
- if not os.path.exists(path):
- if not silent:
- QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
- return
+ # 模型选择 - 多选
+ model_group = QGroupBox("模型类型 (可多选)")
+ model_layout = QVBoxLayout()
- try:
- df = None
- for enc in ('utf-8', 'gbk', 'utf-8-sig'):
- try:
- df = pd.read_csv(path, encoding=enc)
- if 'Formula_Name' in df.columns:
- break
- except Exception:
- continue
+ model_grid = QGridLayout()
+ self.model_checkboxes = {}
- if df is None or 'Formula_Name' not in df.columns:
- if not silent:
- QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name' 列")
- return
-
- self._formula_type_map.clear()
- self._formula_coef_map.clear()
- for _, row in df.iterrows():
- name = str(row['Formula_Name']).strip()
- if not name:
- continue
- ftype = str(row.get('Formula_Type', 'ratio')).strip().lower()
- self._formula_type_map[name] = ftype
-
- # Parse Coefficient for concentration formulas
- coef_str = str(row.get('Coefficient', '')).strip()
- if coef_str:
- try:
- coeffs = [float(c.strip()) for c in coef_str.split(',') if c.strip()]
- self._formula_coef_map[name] = coeffs
- except Exception:
- self._formula_coef_map[name] = []
- else:
- self._formula_coef_map[name] = []
-
- self.formula_list.clear()
- self.index_checkboxes.clear()
-
- self._formula_color_map.clear()
- for name, ftype in self._formula_type_map.items():
- item = QListWidgetItem(name, self.formula_list)
- item.setCheckState(Qt.Checked)
- if ftype == 'concentration':
- bg_color = QColor(220, 240, 255)
- else:
- bg_color = self.COLOR_RATIO
- self._formula_color_map[name] = bg_color
- item.setBackground(QBrush(bg_color))
- self.index_checkboxes[name] = item
-
- self.formula_list.adjustSize()
- print(f"✅ 加载 {len(self.index_checkboxes)} 个公式")
-
- except Exception as e:
- if not silent:
- QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
-
- def _select_ratio_only(self):
- for name, item in self.index_checkboxes.items():
- ftype = self._formula_type_map.get(name, 'ratio')
- item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
-
- def _select_conc_only(self):
- for name, item in self.index_checkboxes.items():
- ftype = self._formula_type_map.get(name, 'ratio')
- item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
-
- def select_all_formulas(self):
- for item in self.index_checkboxes.values():
- item.setCheckState(Qt.Checked)
-
- def deselect_all_formulas(self):
- for item in self.index_checkboxes.values():
- item.setCheckState(Qt.Unchecked)
-
- def get_config(self) -> Dict:
- selected = [
- name for name, item in self.index_checkboxes.items()
- if item.checkState() == Qt.Checked
+ model_groups = [
+ ("【线性模型】", ['LinearRegression', 'Ridge', 'Lasso', 'ElasticNet', 'PLS']),
+ ("【树模型】", ['DecisionTree', 'RF', 'ExtraTrees', 'XGBoost', 'LightGBM', 'CatBoost']),
+ ("【集成学习】", ['GradientBoosting', 'AdaBoost']),
+ ("【其他模型】", ['SVR', 'KNN', 'MLP'])
]
- # Build coefficient dict for selected formulas
- formula_coefficients = {
- name: self._formula_coef_map.get(name, [])
- for name in selected
- }
- return {
- 'training_csv_path': self.training_data_widget.get_path(),
- 'formula_csv_file': self.builtin_formula_path,
- 'formula_names': selected,
- 'formula_coefficients': formula_coefficients,
- 'enabled': self.enable_checkbox.isChecked(),
- 'output_mode': self.mode_group.checkedId(),
- }
- def set_config(self, config: Dict):
+ row = 0
+ for group_name, models in model_groups:
+ group_label = QLabel(f"{group_name}")
+ group_label.setStyleSheet(
+ f"background-color: {ModernStylesheet.COLORS['hover']}; "
+ f"padding: 5px; border: 1px solid {ModernStylesheet.COLORS['border_light']}; "
+ f"border-radius: 3px;"
+ )
+ model_grid.addWidget(group_label, row, 0, 1, 4)
+ row += 1
+
+ for i, model in enumerate(models):
+ checkbox = QCheckBox(MODEL_CHINESE.get(model, model))
+ checkbox.setChecked(False)
+ self.model_checkboxes[model] = checkbox
+ model_grid.addWidget(checkbox, row, i % 4)
+ if (i + 1) % 4 == 0:
+ row += 1
+
+ row += 1
+
+ model_button_layout = QHBoxLayout()
+ model_select_all = QPushButton("全选")
+ model_deselect_all = QPushButton("全不选")
+ model_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, True))
+ model_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, False))
+ model_button_layout.addWidget(model_select_all)
+ model_button_layout.addWidget(model_deselect_all)
+ model_button_layout.addStretch()
+
+ model_layout.addLayout(model_grid)
+ model_layout.addLayout(model_button_layout)
+ model_group.setLayout(model_layout)
+ layout.addWidget(model_group)
+
+ # 数据划分方法 - 多选
+ split_group = QGroupBox("数据划分方法 (可多选)")
+ split_layout = QVBoxLayout()
+
+ split_grid = QGridLayout()
+ self.split_checkboxes = {}
+ split_methods = ['spxy', 'ks', 'random']
+
+ for i, method in enumerate(split_methods):
+ checkbox = QCheckBox(SPLIT_CHINESE.get(method, method))
+ checkbox.setChecked(False)
+ self.split_checkboxes[method] = checkbox
+ split_grid.addWidget(checkbox, 0, i)
+
+ split_button_layout = QHBoxLayout()
+ split_select_all = QPushButton("全选")
+ split_deselect_all = QPushButton("全不选")
+ split_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, True))
+ split_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, False))
+ split_button_layout.addWidget(split_select_all)
+ split_button_layout.addWidget(split_deselect_all)
+ split_button_layout.addStretch()
+
+ split_layout.addLayout(split_grid)
+ split_layout.addLayout(split_button_layout)
+ split_group.setLayout(split_layout)
+ layout.addWidget(split_group)
+
+ self.ml_page.setLayout(layout)
+
+ def _toggle_checkboxes(self, checkboxes_dict, checked):
+ """统一设置checkbox状态"""
+ for checkbox in checkboxes_dict.values():
+ checkbox.setChecked(checked)
+
+ def _get_default_work_dir(self):
+ """获取 work_dir,优先用 panel 自身缓存的,否则尝试从主窗口取"""
+ if hasattr(self, 'work_dir') and self.work_dir:
+ return str(self.work_dir)
+ mw = self.window()
+ if mw and hasattr(mw, 'work_dir') and mw.work_dir:
+ return str(mw.work_dir)
+ return ""
+
+ def browse_output_path(self):
+ """浏览输出文件路径(保存对话框)"""
+ current = self.output_path.get_path().strip()
+ if current:
+ initial_dir = os.path.dirname(current)
+ initial_file = os.path.basename(current)
+ else:
+ initial_dir = ""
+ initial_file = ""
+
+ if not initial_dir or not os.path.isdir(initial_dir):
+ # 默认定位到 indices 目录
+ work_dir = self._get_default_work_dir()
+ initial_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else ""
+ if initial_dir and not os.path.isdir(initial_dir):
+ os.makedirs(initial_dir, exist_ok=True)
+
+ file_path, _ = QFileDialog.getSaveFileName(
+ self, "保存输出文件", os.path.join(initial_dir, initial_file) if initial_file else initial_dir,
+ "CSV Files (*.csv);;All Files (*.*)"
+ )
+ if file_path:
+ self.output_path.set_path(file_path)
+
+ def get_config(self):
+ """获取配置"""
+ preprocessing_methods = [
+ method for method, checkbox in self.preproc_checkboxes.items()
+ if checkbox.isChecked()
+ ]
+ model_names = [
+ model for model, checkbox in self.model_checkboxes.items()
+ if checkbox.isChecked()
+ ]
+ split_methods = [
+ method for method, checkbox in self.split_checkboxes.items()
+ if checkbox.isChecked()
+ ]
+
+ config = {
+ 'feature_start_column': self.feature_start.text(),
+ 'preprocessing_methods': preprocessing_methods if preprocessing_methods else ['None'],
+ 'model_names': model_names if model_names else ['SVR'],
+ 'split_methods': split_methods if split_methods else ['random'],
+ 'cv_folds': self.cv_folds.value()
+ }
+ training_csv_path = self.training_csv_file.get_path()
+ if training_csv_path:
+ config['training_csv_path'] = training_csv_path
+ output_path = self.output_path.get_path()
+ if output_path:
+ config['output_path'] = output_path
+ return config
+
+ def set_config(self, config):
+ """设置配置"""
+ if 'feature_start_column' in config:
+ self.feature_start.setText(str(config['feature_start_column']))
+ if 'cv_folds' in config:
+ self.cv_folds.setValue(config['cv_folds'])
+ if 'preprocessing_methods' in config:
+ methods = config['preprocessing_methods']
+ for method, checkbox in self.preproc_checkboxes.items():
+ checkbox.setChecked(method in methods)
+ if 'model_names' in config:
+ models = config['model_names']
+ for model, checkbox in self.model_checkboxes.items():
+ checkbox.setChecked(model in models)
+ if 'split_methods' in config:
+ methods = config['split_methods']
+ for method, checkbox in self.split_checkboxes.items():
+ checkbox.setChecked(method in methods)
if 'training_csv_path' in config:
- self.training_data_widget.set_path(config['training_csv_path'])
- if 'formula_names' in config:
- sel = set(config['formula_names'])
- for name, item in self.index_checkboxes.items():
- item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
- self.enable_checkbox.setChecked(config.get('enabled', True))
- if 'output_mode' in config:
- btn = self.mode_group.button(config['output_mode'])
- if btn:
- btn.setChecked(True)
+ self.training_csv_file.set_path(config['training_csv_path'])
+ if 'output_path' in config:
+ self.output_path.set_path(config['output_path'])
def update_from_config(self, work_dir=None, pipeline=None):
+ """从全局配置自动填充训练数据和输出路径
+
+ Args:
+ work_dir: 工作目录路径
+ pipeline: Pipeline 实例(未使用,保留接口兼容性)
+ """
if work_dir:
self.work_dir = work_dir
- main = self.window()
- if hasattr(main, 'step5_panel'):
- p5 = main.step5_panel.output_file.get_path()
- if p5:
- if not os.path.isabs(p5):
- p5 = os.path.join(self.work_dir or '', p5)
- p5 = p5.replace('\\', '/')
- self.training_data_widget.set_path(p5)
+ elif hasattr(self, 'work_dir') and self.work_dir:
+ pass
+ else:
+ self.work_dir = None
- def _get_work_dir(self) -> Optional[str]:
+ # 1. 尝试从 Step6 界面读取训练数据路径,并确保为绝对路径
+ main_window = self.window()
+ if hasattr(main_window, 'step6_panel'):
+ # 优先直接从 Step6 的输出 widget 读取
+ step5_output = main_window.step6_panel.output_file.get_path()
+ if step5_output:
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if not os.path.isabs(step5_output):
+ step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
+ self.training_csv_file.set_path(step5_output)
+ elif hasattr(main_window, 'step6_panel') and hasattr(main_window.step6_panel, 'get_config'):
+ # 回退:从 Step6 的 config 字典中查找可能的键名
+ step6_cfg = main_window.step6_panel.get_config()
+ step6_csv = (
+ step6_cfg.get('training_csv_path')
+ or step6_cfg.get('output_file')
+ or step6_cfg.get('csv_path')
+ or step6_cfg.get('output_csv')
+ )
+ if step6_csv:
+ # 若为相对路径,使用 work_dir 合成为绝对路径
+ if not os.path.isabs(step6_csv):
+ step6_csv = os.path.join(self.work_dir or '', step6_csv).replace('\\', '/')
+ self.training_csv_file.set_path(step6_csv)
+
+ # 2. 自动填充输出文件路径(基于工作目录和输入文件名)
+ # 输入是 training_spectra.csv → 输出 {work_dir}/6_water_quality_indices/training_spectra_indices.csv
+ # 输入是 sampling_spectra.csv → 输出 {work_dir}/6_water_quality_indices/sampling_spectra_indices.csv
if self.work_dir:
- return self.work_dir
- main = self.window()
- if hasattr(main, 'work_dir') and main.work_dir:
- return main.work_dir
- return None
-
- def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
- coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
- lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y']
-
- x_col, y_col = None, None
- for col in df.columns:
- cl = col.lower()
- if x_col is None and any(c in cl for c in coord_candidates):
- x_col = col
- if y_col is None and any(c in cl for c in lat_candidates):
- y_col = col
-
- if x_col is None and len(df.columns) >= 2:
- x_col = df.columns[0]
- if y_col is None and len(df.columns) >= 2:
- y_col = df.columns[1]
-
- return x_col or 'x_coord', y_col or 'y_coord'
+ indices_dir = os.path.join(self.work_dir, "6_water_quality_indices")
+ os.makedirs(indices_dir, exist_ok=True)
+ training_csv = self.training_csv_file.get_path()
+ if training_csv:
+ basename = os.path.splitext(os.path.basename(training_csv))[0]
+ output_file = f"{basename}_indices.csv"
+ else:
+ output_file = "water_quality_indices.csv"
+ output_path = os.path.join(indices_dir, output_file).replace('\\', '/')
+ self.output_path.set_path(output_path)
+ else:
+ self.output_path.set_path("")
def run_step(self):
- config = self.get_config()
-
- if not config['enabled']:
- QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
+ """独立运行步骤7"""
+ training_csv_path = self.training_csv_file.get_path()
+ if not training_csv_path:
+ QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
return
- training_path = config['training_csv_path']
- if not training_path or not os.path.exists(training_path):
- QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
- return
+ main_window = self.window()
+ if hasattr(main_window, 'run_single_step'):
+ config = {'step7': self.get_config()}
+ main_window.run_single_step('step7', config)
- formula_names = config['formula_names']
- if not formula_names:
- QMessageBox.warning(self, "提示", "请至少勾选一个公式")
- return
-
- output_mode = config['output_mode']
-
- try:
- from src.core.steps.data_preparation_step import DataPreparationStep
-
- spec_df = pd.read_csv(training_path)
- x_col, y_col = self._get_coord_cols(spec_df)
-
- # 构建 formula_csv_path(使用内置 waterindex.csv)
- import os
- formula_csv_path = self.builtin_formula_path
- if not formula_csv_path or not os.path.exists(formula_csv_path):
- # 尝试从 src/gui/model/ 目录找
- possible_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'gui', 'model', 'waterindex.csv')
- if os.path.exists(possible_path):
- formula_csv_path = possible_path
-
- work_dir = self._get_work_dir()
-
- # 调用 DataPreparationStep 的静态方法计算水质指数(宽表输出)
- indices_csv_path = DataPreparationStep.calculate_water_quality_indices(
- training_csv_path=training_path,
- formula_csv_file=formula_csv_path,
- formula_names=formula_names,
- output_file=None, # 不在此处指定输出,由下面的双轨输出逻辑接管
- enabled=True,
- output_dir=work_dir if work_dir else os.getcwd(),
- )
-
- # 读取计算结果(宽表)
- if indices_csv_path and os.path.exists(indices_csv_path):
- output_df = pd.read_csv(indices_csv_path)
- else:
- output_df = spec_df # fallback
-
- track_a_path = None
- track_b_dir = None
-
- if output_mode in (0, 1):
- track_a_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else "6_water_quality_indices"
- os.makedirs(track_a_dir, exist_ok=True)
- track_a_path = os.path.join(track_a_dir, "training_spectra_indices.csv")
-
- if output_mode in (0, 2):
- track_b_dir = os.path.join(work_dir, "11_12_13_predictions", "Traditional_Indices") if work_dir else "11_12_13_predictions/Traditional_Indices"
- os.makedirs(track_b_dir, exist_ok=True)
-
- saved = []
- if output_mode in (0, 1):
- output_df.to_csv(track_a_path, index=False, float_format='%.6f')
- saved.append(f"宽表: {track_a_path}")
-
- if output_mode in (0, 2):
- coord_x = spec_df[x_col].values if x_col in spec_df.columns else np.arange(len(spec_df))
- coord_y = spec_df[y_col].values if y_col in spec_df.columns else np.zeros(len(spec_df))
-
- for formula_name in formula_names:
- if formula_name not in output_df.columns:
- continue
- single_df = pd.DataFrame({
- 'x_coord': coord_x,
- 'y_coord': coord_y,
- 'value': output_df[formula_name].values,
- })
- safe_name = formula_name.replace('/', '_').replace(' ', '_')
- out_path = os.path.join(track_b_dir, f"{safe_name}_prediction.csv")
- single_df.to_csv(out_path, index=False, float_format='%.6f')
- saved.append(f"单文件目录: {track_b_dir}")
-
- QMessageBox.information(
- self, "计算完成",
- f"已保存 {len(saved)} 个输出目标:\n" + "\n".join(saved)
- )
-
- except ImportError as e:
- QMessageBox.critical(self, "依赖错误", f"无法导入模块:\n{e}")
- except Exception as e:
- import traceback
- QMessageBox.critical(self, "计算失败", f"原因: {str(e)}\n{traceback.format_exc()}")
\ No newline at end of file
+ def get_training_params(self):
+ """获取模型训练参数"""
+ return {
+ 'pipeline_type': 'machine_learning',
+ 'feature_start': float(self.feature_start.text()),
+ 'cv_folds': self.cv_folds.value(),
+ 'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()],
+ 'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()],
+ 'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()]
+ }
diff --git a/src/gui/panels/step8_waterindex_panel.py b/src/gui/panels/step8_waterindex_panel.py
index 55dabdf..c3d8fc7 100644
--- a/src/gui/panels/step8_waterindex_panel.py
+++ b/src/gui/panels/step8_waterindex_panel.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step8 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像)
+Step9 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像)
将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像,
输出各水质参数指数的 GeoTIFF 栅格图像。
@@ -98,8 +98,8 @@ class WaterIndexWorker(QThread):
self.progress.emit(msg, pct)
-class Step8WaterIndexPanel(QWidget):
- """步骤8:水色指数反演(直接处理 BSQ 影像)"""
+class Step9WaterColorPanel(QWidget):
+ """步骤9:水色指数反演(直接处理 BSQ 影像)"""
def __init__(self, parent=None):
super().__init__(parent)
@@ -115,7 +115,7 @@ class Step8WaterIndexPanel(QWidget):
layout = QVBoxLayout()
# ---- 标题 ----
- title = QLabel("步骤8:水色指数反演(高光谱影像直接处理)")
+ title = QLabel("步骤9:水色指数反演(高光谱影像直接处理)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
diff --git a/src/gui/panels/step9_concentration_panel.py b/src/gui/panels/step9_concentration_panel.py
index 74f64b7..4d150b2 100644
--- a/src/gui/panels/step9_concentration_panel.py
+++ b/src/gui/panels/step9_concentration_panel.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step9 面板 - 浓度反演(基于 QAA 物理反演的二次反演)
+Step10 面板 - 浓度反演(基于 QAA 物理反演的二次反演)
"""
import os
@@ -18,8 +18,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
-class Step9ConcentrationPanel(QWidget):
- """步骤9:浓度反演(物理模型二次反演)"""
+class Step10ConcentrationPanel(QWidget):
+ """步骤10:浓度反演(物理模型二次反演)"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@@ -27,7 +27,7 @@ class Step9ConcentrationPanel(QWidget):
def init_ui(self):
layout = QVBoxLayout()
- title = QLabel("步骤9:浓度反演(物理模型二次反演)")
+ title = QLabel("步骤10:浓度反演(物理模型二次反演)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
diff --git a/src/gui/panels/step9_panel.py b/src/gui/panels/step9_panel.py
index c49f158..cc87fc8 100644
--- a/src/gui/panels/step9_panel.py
+++ b/src/gui/panels/step9_panel.py
@@ -1,400 +1,424 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
-Step9 面板 - 自定义回归分析
+Step9 面板 - 机器学习建模
"""
import os
-from typing import Dict
-
+import sys
import pandas as pd
+import numpy as np
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
- QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton,
- QScrollArea, QMessageBox,
+ QWidget, QVBoxLayout, QGroupBox, QGridLayout,
+ QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
+ QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
+ QRadioButton, QButtonGroup
)
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
+def get_resource_path(relative_path: str) -> str:
+ """适配开发与 PyInstaller 环境的路径获取逻辑。"""
+ if hasattr(sys, '_MEIPASS'):
+ internal = os.path.join(sys._MEIPASS, '_internal', relative_path)
+ if os.path.exists(internal):
+ return internal
+ return os.path.join(sys._MEIPASS, relative_path)
+
+ exe_dir = os.path.dirname(sys.executable)
+ internal = os.path.join(exe_dir, '_internal', relative_path)
+ if os.path.exists(internal):
+ return internal
+
+ base_dir = Path(__file__).resolve().parent.parent / "model"
+ return str(base_dir / os.path.basename(relative_path))
+
+
class Step9Panel(QWidget):
- """步骤9:自定义回归分析"""
+ """步骤9:机器学习建模"""
+ COLOR_RATIO = QColor(255, 255, 255)
+ COLOR_CONCENTRATION = QColor(220, 240, 255)
+ COLOR_HEADER = QColor(245, 245, 245)
+
def __init__(self, parent=None):
super().__init__(parent)
- self.x_column_checkboxes: Dict[str, QCheckBox] = {}
- self.y_column_checkboxes: Dict[str, QCheckBox] = {}
- self.method_checkboxes: Dict[str, QCheckBox] = {}
- self.csv_columns = []
+ self.index_checkboxes: Dict[str, QListWidgetItem] = {}
+ self.work_dir: Optional[str] = None
+ self.builtin_formula_path = get_resource_path("waterindex.csv")
+ self._formula_type_map: Dict[str, str] = {}
+ self._formula_color_map: Dict[str, QColor] = {}
+ self._formula_coef_map: Dict[str, List[float]] = {}
+
self.init_ui()
+ self._auto_load_formulas()
def init_ui(self):
- layout = QVBoxLayout()
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(10)
- hint = QLabel("指定自变量与因变量列,批量尝试不同回归方法")
- hint.setStyleSheet("color: #666; font-size: 11px;")
- layout.addWidget(hint)
+ # 1. 公式配置源 (只读)
+ path_group = QGroupBox("公式配置源 (内置)")
+ path_layout = QVBoxLayout()
+ self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
+ self.formula_csv_widget.set_path(self.builtin_formula_path)
+ self.formula_csv_widget.set_read_only(True)
+ self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
+ path_layout.addWidget(self.formula_csv_widget)
+ path_group.setLayout(path_layout)
+ main_layout.addWidget(path_group)
- # CSV文件选择
- csv_group = QGroupBox("数据文件")
- csv_layout = QVBoxLayout()
+ # 2. 训练数据输入
+ input_group = QGroupBox("输入样本数据")
+ input_layout = QVBoxLayout()
+ self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
+ input_layout.addWidget(self.training_data_widget)
+ input_group.setLayout(input_layout)
+ main_layout.addWidget(input_group)
- self.csv_file = FileSelectWidget(
- "输入CSV文件:",
- "CSV Files (*.csv);;All Files (*.*)"
- )
- self.csv_file.line_edit.textChanged.connect(self.on_csv_file_changed)
- csv_layout.addWidget(self.csv_file)
+ # 3. 公式选择区 (分组 ListWidget)
+ self.formula_group = QGroupBox("待计算水质指数勾选")
+ formula_outer_layout = QVBoxLayout()
- self.refresh_btn = QPushButton("刷新列信息")
- self.refresh_btn.clicked.connect(self.refresh_csv_columns)
- csv_layout.addWidget(self.refresh_btn)
+ btn_layout = QHBoxLayout()
+ self.select_all_btn = QPushButton("全选")
+ self.deselect_all_btn = QPushButton("清空")
+ self.select_ratio_btn = QPushButton("仅选比值型")
+ self.select_conc_btn = QPushButton("仅选浓度型")
+ self.select_all_btn.clicked.connect(self.select_all_formulas)
+ self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
+ self.select_ratio_btn.clicked.connect(self._select_ratio_only)
+ self.select_conc_btn.clicked.connect(self._select_conc_only)
+ btn_layout.addWidget(self.select_all_btn)
+ btn_layout.addWidget(self.deselect_all_btn)
+ btn_layout.addWidget(self.select_ratio_btn)
+ btn_layout.addWidget(self.select_conc_btn)
+ btn_layout.addStretch()
- csv_group.setLayout(csv_layout)
- layout.addWidget(csv_group)
+ self.refresh_button = QPushButton("重新加载")
+ self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
+ btn_layout.addWidget(self.refresh_button)
- # 自变量选择
- x_group = QGroupBox("自变量列选择 (可多选)")
- x_layout = QVBoxLayout()
+ formula_outer_layout.addLayout(btn_layout)
- x_scroll = QScrollArea()
- x_scroll.setWidgetResizable(True)
- x_scroll.setMinimumHeight(250)
- x_scroll.setMaximumHeight(350)
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setMinimumHeight(280)
+ self.scroll_content = QWidget()
+ self.formula_layout = QVBoxLayout(self.scroll_content)
+ self.formula_layout.setContentsMargins(4, 4, 4, 4)
+ self.formula_layout.setSpacing(2)
+ self.formula_layout.setAlignment(Qt.AlignTop)
- x_widget = QWidget()
- self.x_columns_layout = QGridLayout()
- x_widget.setLayout(self.x_columns_layout)
+ self.formula_list = QListWidget()
+ self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
+ self.formula_list.itemChanged.connect(self._on_item_changed)
+ self.formula_layout.addWidget(self.formula_list)
- x_scroll.setWidget(x_widget)
- x_layout.addWidget(x_scroll)
+ scroll.setWidget(self.scroll_content)
+ formula_outer_layout.addWidget(scroll)
- x_btn_layout = QHBoxLayout()
- self.x_select_all = QPushButton("全选")
- self.x_deselect_all = QPushButton("全不选")
- self.x_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, True))
- self.x_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, False))
- x_btn_layout.addWidget(self.x_select_all)
- x_btn_layout.addWidget(self.x_deselect_all)
- x_btn_layout.addStretch()
- x_layout.addLayout(x_btn_layout)
+ self.formula_group.setLayout(formula_outer_layout)
+ main_layout.addWidget(self.formula_group)
- x_group.setLayout(x_layout)
- layout.addWidget(x_group)
+ # 4. 输出选项
+ output_group = QGroupBox("输出模式")
+ output_layout = QVBoxLayout()
- # 因变量选择
- y_group = QGroupBox("因变量列选择 (可多选)")
- y_layout = QVBoxLayout()
+ mode_layout = QHBoxLayout()
+ self.mode_group = QButtonGroup()
+ self.radio_both = QRadioButton("两者皆出")
+ self.radio_wide = QRadioButton("仅宽表")
+ self.radio_single = QRadioButton("仅单文件")
+ self.mode_group.addButton(self.radio_both, 0)
+ self.mode_group.addButton(self.radio_wide, 1)
+ self.mode_group.addButton(self.radio_single, 2)
+ self.radio_both.setChecked(True)
+ mode_layout.addWidget(self.radio_both)
+ mode_layout.addWidget(self.radio_wide)
+ mode_layout.addWidget(self.radio_single)
+ mode_layout.addStretch()
+ output_layout.addLayout(mode_layout)
- y_scroll = QScrollArea()
- y_scroll.setWidgetResizable(True)
- y_scroll.setMinimumHeight(200)
- y_scroll.setMaximumHeight(300)
-
- y_widget = QWidget()
- self.y_columns_layout = QGridLayout()
- y_widget.setLayout(self.y_columns_layout)
-
- y_scroll.setWidget(y_widget)
- y_layout.addWidget(y_scroll)
-
- y_btn_layout = QHBoxLayout()
- self.y_select_all = QPushButton("全选")
- self.y_deselect_all = QPushButton("全不选")
- self.y_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, True))
- self.y_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, False))
- y_btn_layout.addWidget(self.y_select_all)
- y_btn_layout.addWidget(self.y_deselect_all)
- y_btn_layout.addStretch()
- y_layout.addLayout(y_btn_layout)
-
- y_group.setLayout(y_layout)
- layout.addWidget(y_group)
-
- # 回归方法选择
- method_group = QGroupBox("回归方法选择 (可多选)")
- method_layout = QVBoxLayout()
-
- method_grid = QGridLayout()
- regression_methods = [
- 'linear', 'exponential', 'power', 'logarithmic',
- 'polynomial', 'hyperbolic', 'sigmoidal'
- ]
-
- for i, method in enumerate(regression_methods):
- checkbox = QCheckBox(method)
- if method in ['linear', 'exponential', 'power', 'logarithmic']:
- checkbox.setChecked(True)
- self.method_checkboxes[method] = checkbox
- method_grid.addWidget(checkbox, i // 3, i % 3)
-
- method_layout.addLayout(method_grid)
-
- method_btn_layout = QHBoxLayout()
- self.method_select_all = QPushButton("全选")
- self.method_deselect_all = QPushButton("全不选")
- self.method_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, True))
- self.method_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, False))
- method_btn_layout.addWidget(self.method_select_all)
- method_btn_layout.addWidget(self.method_deselect_all)
- method_btn_layout.addStretch()
- method_layout.addLayout(method_btn_layout)
-
- method_group.setLayout(method_layout)
- layout.addWidget(method_group)
-
- # 输出目录
- output_group = QGroupBox("输出设置")
- output_layout = QFormLayout()
-
- self.output_dir = QLineEdit()
- self.output_dir.setText("") # 路径由 update_from_config 根据 work_dir 自动填充
- output_layout.addRow("输出目录名:", self.output_dir)
+ self.enable_checkbox = QCheckBox("启用计算流程")
+ self.enable_checkbox.setChecked(True)
+ output_layout.addWidget(self.enable_checkbox)
output_group.setLayout(output_layout)
- layout.addWidget(output_group)
+ main_layout.addWidget(output_group)
- # 启用步骤
- self.enable_checkbox = QCheckBox("启用此步骤")
- self.enable_checkbox.setChecked(True)
- layout.addWidget(self.enable_checkbox)
-
- # 独立运行按钮
- self.run_button = QPushButton("独立运行此步骤")
+ # 5. 运行按钮
+ self.run_button = QPushButton("立即执行计算")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
+ self.run_button.setMinimumHeight(40)
self.run_button.clicked.connect(self.run_step)
- layout.addWidget(self.run_button)
+ main_layout.addWidget(self.run_button)
- layout.addStretch()
- self.setLayout(layout)
+ self.setLayout(main_layout)
- def toggle_checkboxes(self, checkboxes_dict, checked):
- """统一设置checkbox状态"""
- for checkbox in checkboxes_dict.values():
- checkbox.setChecked(checked)
+ def _on_item_changed(self, item: QListWidgetItem):
+ if item.checkState() == Qt.Checked:
+ bg_color = self.COLOR_RATIO
+ for name, ref_item in self.index_checkboxes.items():
+ if ref_item is item:
+ bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
+ break
+ item.setBackground(QBrush(bg_color))
+ else:
+ item.setBackground(QBrush(self.COLOR_RATIO))
- def on_csv_file_changed(self):
- """CSV文件改变时自动刷新列信息"""
- self.refresh_csv_columns()
+ def _auto_load_formulas(self):
+ if os.path.exists(self.builtin_formula_path):
+ self.refresh_formulas(silent=True)
+ else:
+ print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
- def refresh_csv_columns(self):
- """刷新CSV文件的列信息"""
- csv_path = self.csv_file.get_path()
- if not csv_path or not os.path.exists(csv_path):
- self.csv_columns = []
- self.update_column_widgets()
+ def refresh_formulas(self, silent=False):
+ path = self.builtin_formula_path
+ if not os.path.exists(path):
+ if not silent:
+ QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
return
try:
- df = pd.read_csv(csv_path, nrows=0)
- self.csv_columns = list(df.columns)
- self.update_column_widgets()
+ df = None
+ for enc in ('utf-8', 'gbk', 'utf-8-sig'):
+ try:
+ df = pd.read_csv(path, encoding=enc)
+ if 'Formula_Name' in df.columns:
+ break
+ except Exception:
+ continue
+
+ if df is None or 'Formula_Name' not in df.columns:
+ if not silent:
+ QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name' 列")
+ return
+
+ self._formula_type_map.clear()
+ self._formula_coef_map.clear()
+ for _, row in df.iterrows():
+ name = str(row['Formula_Name']).strip()
+ if not name:
+ continue
+ ftype = str(row.get('Formula_Type', 'ratio')).strip().lower()
+ self._formula_type_map[name] = ftype
+
+ coef_str = str(row.get('Coefficient', '')).strip()
+ if coef_str:
+ try:
+ coeffs = [float(c.strip()) for c in coef_str.split(',') if c.strip()]
+ self._formula_coef_map[name] = coeffs
+ except Exception:
+ self._formula_coef_map[name] = []
+ else:
+ self._formula_coef_map[name] = []
+
+ self.formula_list.clear()
+ self.index_checkboxes.clear()
+
+ self._formula_color_map.clear()
+ for name, ftype in self._formula_type_map.items():
+ item = QListWidgetItem(name, self.formula_list)
+ item.setCheckState(Qt.Checked)
+ if ftype == 'concentration':
+ bg_color = QColor(220, 240, 255)
+ else:
+ bg_color = self.COLOR_RATIO
+ self._formula_color_map[name] = bg_color
+ item.setBackground(QBrush(bg_color))
+ self.index_checkboxes[name] = item
+
+ self.formula_list.adjustSize()
+ print(f"✅ 加载 {len(self.index_checkboxes)} 个公式")
+
except Exception as e:
- self.csv_columns = []
- self.update_column_widgets()
- print(f"读取CSV列信息失败: {e}")
+ if not silent:
+ QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
- def update_column_widgets(self):
- """更新列选择组件"""
- for checkbox in self.x_column_checkboxes.values():
- checkbox.setParent(None)
- self.x_column_checkboxes.clear()
+ def _select_ratio_only(self):
+ for name, item in self.index_checkboxes.items():
+ ftype = self._formula_type_map.get(name, 'ratio')
+ item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
- for checkbox in self.y_column_checkboxes.values():
- checkbox.setParent(None)
- self.y_column_checkboxes.clear()
+ def _select_conc_only(self):
+ for name, item in self.index_checkboxes.items():
+ ftype = self._formula_type_map.get(name, 'ratio')
+ item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
- if not self.csv_columns:
- return
+ def select_all_formulas(self):
+ for item in self.index_checkboxes.values():
+ item.setCheckState(Qt.Checked)
- for i, col in enumerate(self.csv_columns):
- checkbox = QCheckBox(col)
- if any(keyword in col.lower() for keyword in ['index', 'ratio', 'normalized', 'nd', 'b']):
- checkbox.setChecked(True)
- self.x_column_checkboxes[col] = checkbox
- self.x_columns_layout.addWidget(checkbox, i // 3, i % 3)
+ def deselect_all_formulas(self):
+ for item in self.index_checkboxes.values():
+ item.setCheckState(Qt.Unchecked)
- for i, col in enumerate(self.csv_columns):
- checkbox = QCheckBox(col)
- if any(keyword in col.lower() for keyword in ['chl', 'tn', 'tp', 'turbidity', 'do', 'ph', 'conductivity']):
- checkbox.setChecked(True)
- self.y_column_checkboxes[col] = checkbox
- self.y_columns_layout.addWidget(checkbox, i // 2, i % 2)
-
- self.x_columns_layout.update()
- self.y_columns_layout.update()
-
- def get_config(self):
- selected_x_columns = [
- col for col, checkbox in self.x_column_checkboxes.items()
- if checkbox.isChecked()
+ def get_config(self) -> Dict:
+ selected = [
+ name for name, item in self.index_checkboxes.items()
+ if item.checkState() == Qt.Checked
]
- selected_y_columns = [
- col for col, checkbox in self.y_column_checkboxes.items()
- if checkbox.isChecked()
- ]
- selected_methods = [
- method for method, checkbox in self.method_checkboxes.items()
- if checkbox.isChecked()
- ]
- if not selected_methods:
- selected_methods = 'all'
-
+ formula_coefficients = {
+ name: self._formula_coef_map.get(name, [])
+ for name in selected
+ }
return {
- 'csv_path': self.csv_file.get_path() or None,
- 'x_columns': selected_x_columns,
- 'y_columns': selected_y_columns,
- 'methods': selected_methods,
- 'output_dir': self.output_dir.text().strip() or None,
- 'enabled': self.enable_checkbox.isChecked()
+ 'training_csv_path': self.training_data_widget.get_path(),
+ 'formula_csv_file': self.builtin_formula_path,
+ 'formula_names': selected,
+ 'formula_coefficients': formula_coefficients,
+ 'enabled': self.enable_checkbox.isChecked(),
+ 'output_mode': self.mode_group.checkedId(),
}
- def set_config(self, config):
- if 'csv_path' in config:
- self.csv_file.set_path(config['csv_path'])
- self.refresh_csv_columns()
-
- if 'x_columns' in config:
- selected_x = set(config['x_columns']) if isinstance(config['x_columns'], list) else set()
- for col, checkbox in self.x_column_checkboxes.items():
- checkbox.setChecked(col in selected_x)
-
- if 'y_columns' in config:
- selected_y = set(config['y_columns']) if isinstance(config['y_columns'], list) else set()
- for col, checkbox in self.y_column_checkboxes.items():
- checkbox.setChecked(col in selected_y)
-
- if 'methods' in config:
- methods = config['methods']
- if isinstance(methods, list):
- selected_methods = set(methods)
- elif methods == 'all':
- selected_methods = set(self.method_checkboxes.keys())
- else:
- selected_methods = set()
- for method, checkbox in self.method_checkboxes.items():
- checkbox.setChecked(method in selected_methods)
-
- if 'output_dir' in config:
- self.output_dir.setText(config['output_dir'] or "9_Custom_Regression_Modeling")
- if 'enabled' in config:
- self.enable_checkbox.setChecked(config['enabled'])
+ def set_config(self, config: Dict):
+ if 'training_csv_path' in config:
+ self.training_data_widget.set_path(config['training_csv_path'])
+ if 'formula_names' in config:
+ sel = set(config['formula_names'])
+ for name, item in self.index_checkboxes.items():
+ item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
+ self.enable_checkbox.setChecked(config.get('enabled', True))
+ if 'output_mode' in config:
+ btn = self.mode_group.button(config['output_mode'])
+ if btn:
+ btn.setChecked(True)
def update_from_config(self, work_dir=None, pipeline=None):
- """从全局配置自动填充训练数据和输出路径
+ if work_dir:
+ self.work_dir = work_dir
+ main = self.window()
+ if hasattr(main, 'step5_panel'):
+ p5 = main.step5_panel.output_file.get_path()
+ if p5:
+ if not os.path.isabs(p5):
+ p5 = os.path.join(self.work_dir or '', p5)
+ p5 = p5.replace('\\', '/')
+ self.training_data_widget.set_path(p5)
- Args:
- work_dir: 工作目录路径
- pipeline: Pipeline 实例(未使用,保留接口兼容性)
- """
- try:
- import traceback
+ def _get_work_dir(self) -> Optional[str]:
+ if self.work_dir:
+ return self.work_dir
+ main = self.window()
+ if hasattr(main, 'work_dir') and main.work_dir:
+ return main.work_dir
+ return None
- if work_dir:
- self.work_dir = work_dir
- elif hasattr(self, 'work_dir') and self.work_dir:
- pass
- else:
- self.work_dir = None
+ def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
+ coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
+ lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utc', 'utm_y', 'pixel_y']
- # 1. 尝试从 Step8 界面读取训练光谱 CSV 路径
- main_window = self.window()
- if main_window and hasattr(main_window, 'step8_panel'):
- step8_widget = main_window.step8_panel.training_data_widget
- step8_output_path = ""
- if hasattr(step8_widget, 'get_path'):
- step8_output_path = step8_widget.get_path() or ""
+ x_col, y_col = None, None
+ for col in df.columns:
+ cl = col.lower()
+ if x_col is None and any(c in cl for c in coord_candidates):
+ x_col = col
+ if y_col is None and any(c in cl for c in lat_candidates):
+ y_col = col
- if step8_output_path:
- if not os.path.isabs(step8_output_path):
- step8_output_path = os.path.join(self.work_dir or '', step8_output_path).replace('\\', '/')
- existing = self.csv_file.get_path()
- if not existing or not existing.strip():
- self.csv_file.set_path(step8_output_path)
+ if x_col is None and len(df.columns) >= 2:
+ x_col = df.columns[0]
+ if y_col is None and len(df.columns) >= 2:
+ y_col = df.columns[1]
- # 1.2 尝试从 pipeline 读取 Step 8 宽表 indices_path(优先级最高)
- if pipeline and hasattr(pipeline, 'indices_path') and pipeline.indices_path:
- step8_indices_path = pipeline.indices_path
- if not os.path.isabs(step8_indices_path):
- step8_indices_path = os.path.join(self.work_dir or '', step8_indices_path).replace('\\', '/')
- current_path = self.csv_file.get_path()
- if not current_path or not current_path.strip():
- self.csv_file.set_path(step8_indices_path)
- print(f"✅ 从pipeline.indices_path回填Step8产出: {step8_indices_path}")
-
- # 1.5 自动探测并回填 Step 8 双轨输出的 Traditional_Indices 目录
- if self.work_dir:
- trad_indices_dir = os.path.join(
- self.work_dir, "11_12_13_predictions", "Traditional_Indices"
- )
- if os.path.isdir(trad_indices_dir):
- csv_files = [
- f for f in os.listdir(trad_indices_dir)
- if f.lower().endswith('.csv')
- ]
- if csv_files:
- csv_files.sort()
- first_csv = os.path.join(trad_indices_dir, csv_files[0])
- existing = self.csv_file.get_path()
- if not existing or not existing.strip():
- self.csv_file.set_path(first_csv)
- self.refresh_csv_columns()
- print(f"✅ 自动探测到 Traditional_Indices 目录,加载首个CSV: {csv_files[0]}")
-
- # 2. 自动填充输出目录(9_Custom_Regression_Modeling)
- if self.work_dir:
- output_dir = os.path.join(self.work_dir, "9_Custom_Regression_Modeling")
- os.makedirs(output_dir, exist_ok=True)
- existing_out = self.output_dir.text().strip()
- if not existing_out:
- self.output_dir.setText(output_dir)
- except Exception as e:
- import traceback
- print(f"【{self.__class__.__name__}】自动填充失败,跳过: {e}")
- traceback.print_exc()
+ return x_col or 'x_coord', y_col or 'y_coord'
def run_step(self):
- """独立运行步骤9"""
- csv_path = self.csv_file.get_path()
-
- if not csv_path:
- QMessageBox.warning(self, "输入验证失败", "请选择输入CSV文件")
- return
- if not os.path.exists(csv_path):
- QMessageBox.warning(self, "输入验证失败", "输入CSV文件不存在")
- return
-
- selected_x_columns = [
- col for col, checkbox in self.x_column_checkboxes.items()
- if checkbox.isChecked()
- ]
- if not selected_x_columns:
- QMessageBox.warning(self, "输入验证失败", "请至少选择一个自变量列")
- return
-
- selected_y_columns = [
- col for col, checkbox in self.y_column_checkboxes.items()
- if checkbox.isChecked()
- ]
- if not selected_y_columns:
- QMessageBox.warning(self, "输入验证失败", "请至少选择一个因变量列")
- return
-
- selected_methods = [
- method for method, checkbox in self.method_checkboxes.items()
- if checkbox.isChecked()
- ]
- if not selected_methods:
- QMessageBox.warning(self, "输入验证失败", "请至少选择一种回归方法")
- return
-
config = self.get_config()
- parent = self.parent()
- while parent and not hasattr(parent, 'run_single_step'):
- parent = parent.parent()
+ if not config['enabled']:
+ QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
+ return
- if parent and hasattr(parent, 'run_single_step'):
- parent.run_single_step('step9', {'step9': config})
- else:
- QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
+ training_path = config['training_csv_path']
+ if not training_path or not os.path.exists(training_path):
+ QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
+ return
+
+ formula_names = config['formula_names']
+ if not formula_names:
+ QMessageBox.warning(self, "提示", "请至少勾选一个公式")
+ return
+
+ output_mode = config['output_mode']
+
+ try:
+ from src.core.steps.data_preparation_step import DataPreparationStep
+
+ spec_df = pd.read_csv(training_path)
+ x_col, y_col = self._get_coord_cols(spec_df)
+
+ formula_csv_path = self.builtin_formula_path
+ if not formula_csv_path or not os.path.exists(formula_csv_path):
+ possible_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'gui', 'model', 'waterindex.csv')
+ if os.path.exists(possible_path):
+ formula_csv_path = possible_path
+
+ work_dir = self._get_work_dir()
+
+ indices_csv_path = DataPreparationStep.calculate_water_quality_indices(
+ training_csv_path=training_path,
+ formula_csv_file=formula_csv_path,
+ formula_names=formula_names,
+ output_file=None,
+ enabled=True,
+ output_dir=work_dir if work_dir else os.getcwd(),
+ )
+
+ if indices_csv_path and os.path.exists(indices_csv_path):
+ output_df = pd.read_csv(indices_csv_path)
+ else:
+ output_df = spec_df
+
+ track_a_path = None
+ track_b_dir = None
+
+ if output_mode in (0, 1):
+ track_a_dir = os.path.join(work_dir, "9_supervised_modeling") if work_dir else "9_supervised_modeling"
+ os.makedirs(track_a_dir, exist_ok=True)
+ track_a_path = os.path.join(track_a_dir, "training_spectra_indices.csv")
+
+ if output_mode in (0, 2):
+ track_b_dir = os.path.join(work_dir, "11_12_13_predictions", "Traditional_Indices") if work_dir else "11_12_13_predictions/Traditional_Indices"
+ os.makedirs(track_b_dir, exist_ok=True)
+
+ saved = []
+ if output_mode in (0, 1):
+ output_df.to_csv(track_a_path, index=False, float_format='%.6f')
+ saved.append(f"宽表: {track_a_path}")
+
+ if output_mode in (0, 2):
+ coord_x = spec_df[x_col].values if x_col in spec_df.columns else np.arange(len(spec_df))
+ coord_y = spec_df[y_col].values if y_col in spec_df.columns else np.zeros(len(spec_df))
+
+ for formula_name in formula_names:
+ if formula_name not in output_df.columns:
+ continue
+ single_df = pd.DataFrame({
+ 'x_coord': coord_x,
+ 'y_coord': coord_y,
+ 'value': output_df[formula_name].values,
+ })
+ safe_name = formula_name.replace('/', '_').replace(' ', '_')
+ out_path = os.path.join(track_b_dir, f"{safe_name}_prediction.csv")
+ single_df.to_csv(out_path, index=False, float_format='%.6f')
+ saved.append(f"单文件目录: {track_b_dir}")
+
+ QMessageBox.information(
+ self, "计算完成",
+ f"已保存 {len(saved)} 个输出目标:\n" + "\n".join(saved)
+ )
+
+ except ImportError as e:
+ QMessageBox.critical(self, "依赖错误", f"无法导入模块:\n{e}")
+ except Exception as e:
+ import traceback
+ QMessageBox.critical(self, "计算失败", f"原因: {str(e)}\n{traceback.format_exc()}")
\ No newline at end of file
diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py
index ea58e6a..22ab3dc 100644
--- a/src/gui/water_quality_gui.py
+++ b/src/gui/water_quality_gui.py
@@ -117,15 +117,17 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.panels.step1_panel import Step1Panel
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.step6_panel import Step6Panel # was step8_panel
-from src.gui.panels.step7_panel import Step7Panel # was step6_panel
-from src.gui.panels.step8_waterindex_panel import Step8WaterIndexPanel # 水色指数反演
-from src.gui.panels.step9_concentration_panel import Step9ConcentrationPanel # 浓度反演
-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.step14_panel import Step14Panel # was step9_panel
+from src.gui.panels.step4_sampling_panel import Step4SamplingPanel # 采样点布设(原step10→新step4)
+from src.gui.panels.step5_panel import Step5Panel # 数据清洗(原step4→新step5)
+from src.gui.panels.step6_panel import Step6Panel # 光谱特征(原step5→新step6)
+from src.gui.panels.step7_panel import Step7Panel # 水质光谱指数(原step6→新step7)
+from src.gui.panels.step8_panel import Step8Panel # 水质参数指数(原step7→新step8)
+from src.gui.panels.step8_waterindex_panel import Step9WaterColorPanel # 水色指数反演
+from src.gui.panels.step9_concentration_panel import Step10ConcentrationPanel # 浓度反演
+from src.gui.panels.step9_panel import Step9Panel # 机器学习建模(原step8→新step9)
+from src.gui.panels.step10_ml_panel import Step10MlPanel # 机器学习预测(原step11_ml→新step10)
+from src.gui.panels.step11_panel import Step11NonEmpiricalPanel # 非经验模型预测
+from src.gui.panels.step14_panel import Step14Panel
from src.gui.dialogs import BandConfirmDialog, AISettingsDialog
from src.gui.panels.visualization_panel import VisualizationPanel
from src.gui.panels.report_generation_panel import ReportGenerationPanel
@@ -1846,23 +1848,22 @@ class WaterQualityGUI(QMainWindow):
("step3", "3. 耀斑去除与修复"),
],
"阶段二:样本数据准备 ": [
- ("step4", "4. 数据标准化处理"),
- ("step5", "5. 光谱特征提取"),
- ("step6", "6. 水质参数指数计算"),
+ ("step4", "4. 采样点布设"),
+ ("step5", "5. 数据清洗"),
+ ("step6", "6. 光谱特征"),
+ ("step7", "7. 水质光谱指数计算"),
+ ("step8", "8. 水质参数指数计算"),
],
"阶段三:模型构建与训练": [
- ("step7", "7. 机器学习模型训练"),
- ("step8_non_empirical_modeling", "8. 回归模型训练"),
- ("step9", "9. 自定义回归模型训练"),
+ ("step9", "9. 机器学习建模"),
+ ("step8_non_empirical_modeling", "8b. 回归模型训练"),
],
"阶段四:预测与成果输出 ": [
- ("step10", "10. 采样点布设"),
- ("step11_ml", "11. 机器学习预测"),
- ("step11", "12. 回归预测"),
- ("step12", "13. 自定义回归预测"),
- ("step14", "14. 专题图生成"),
- ("step9_viz", "15. 可视化分析"),
- ("step_report", "16. 分析报告生成"),
+ ("step10", "10. 机器学习预测"),
+ ("step11", "11. 回归预测"),
+ ("step14", "12. 专题图生成"),
+ ("step9_viz", "13. 可视化分析"),
+ ("step_report", "14. 分析报告生成"),
]
}
@@ -1882,7 +1883,7 @@ class WaterQualityGUI(QMainWindow):
self.step_list.addItem(stage_item)
# 添加该阶段的所有步骤
- HIDDEN_STEP_IDS = {"step8_non_empirical_modeling", "step9", "step11", "step12"}
+ HIDDEN_STEP_IDS = {"step8_non_empirical_modeling"}
for step_id, step_display in steps:
if step_id in HIDDEN_STEP_IDS:
continue
@@ -1956,29 +1957,35 @@ class WaterQualityGUI(QMainWindow):
self.step3_panel = Step3Panel()
self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除")
- self.step4_panel = Step4Panel()
- self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "数据清洗")
+ self.step4_panel = Step4SamplingPanel()
+ self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "采样点布设")
self.step5_panel = Step5Panel()
- self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建")
+ self.step_stack.addTab(self.create_scroll_area(self.step5_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.step_stack.addTab(self.create_scroll_area(self.step6_panel), QIcon(self.get_icon_path("6.png")), "光谱特征")
self.step7_panel = Step7Panel()
- self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "监督建模")
+ self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "水质光谱指数计算")
- self.step8_waterindex_panel = Step8WaterIndexPanel()
- self.step_stack.addTab(self.create_scroll_area(self.step8_waterindex_panel), QIcon(self.get_icon_path("6.png")), "水色指数反演")
+ self.step8_panel = Step8Panel()
+ self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("7.png")), "水质参数指数计算")
- self.step9_concentration_panel = Step9ConcentrationPanel()
- self.step_stack.addTab(self.create_scroll_area(self.step9_concentration_panel), QIcon(self.get_icon_path("6.png")), "浓度反演")
+ self.step9_panel = Step9Panel()
+ self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("8.png")), "机器学习建模")
- self.step10_panel = Step10Panel()
- self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设")
+ self.step8_waterindex_panel = Step9WaterColorPanel()
+ self.step_stack.addTab(self.create_scroll_area(self.step8_waterindex_panel), QIcon(self.get_icon_path("8.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.step9_concentration_panel = Step10ConcentrationPanel()
+ self.step_stack.addTab(self.create_scroll_area(self.step9_concentration_panel), QIcon(self.get_icon_path("9.png")), "浓度反演")
+
+ self.step10_ml_panel = Step10MlPanel()
+ self.step_stack.addTab(self.create_scroll_area(self.step10_ml_panel), QIcon(self.get_icon_path("10.png")), "机器学习预测")
+
+ self.step11_non_empirical_panel = Step11NonEmpiricalPanel()
+ self.step_stack.addTab(self.create_scroll_area(self.step11_non_empirical_panel), QIcon(self.get_icon_path("11.png")), "回归预测")
self.step14_panel = Step14Panel()
self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
@@ -2133,12 +2140,12 @@ class WaterQualityGUI(QMainWindow):
'step5': 4,
'step6': 5,
'step7': 6,
- 'step8_non_empirical_modeling': 7,
+ 'step8': 7,
'step9': 8,
- 'step10': 9,
- 'step11_ml': 10,
- 'step11': 11,
- 'step12': 12,
+ 'step8_non_empirical_modeling': 9,
+ 'step9_concentration': 10,
+ 'step10': 11,
+ 'step11': 12,
'step14': 13,
'step9_viz': 14,
'step_report': 15,
@@ -2164,12 +2171,12 @@ class WaterQualityGUI(QMainWindow):
4: 'step5',
5: 'step6',
6: 'step7',
- 7: 'step8_non_empirical_modeling',
+ 7: 'step8',
8: 'step9',
- 9: 'step10',
- 10: 'step11_ml',
- 11: 'step11',
- 12: 'step12',
+ 9: 'step8_non_empirical_modeling',
+ 10: 'step9_concentration',
+ 11: 'step10',
+ 12: 'step11',
13: 'step14',
14: 'step9_viz',
15: 'step_report',
@@ -2199,44 +2206,48 @@ class WaterQualityGUI(QMainWindow):
elif index == 2:
self.step3_panel.update_from_config(work_dir=self.work_dir)
- # Step4 切换时自动填充输出路径
+ # Step4(采样点布设)切换时自动填充输出路径
elif index == 3:
self.step4_panel.update_from_config(work_dir=self.work_dir)
- # Step5 切换时自动填充数据流转路径
+ # Step5(数据清洗)切换时自动填充数据流转路径
elif index == 4:
self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
- # Step6(水质光谱指数)切换时自动填充输出路径
+ # Step6(光谱特征)切换时自动填充输出路径
elif index == 5:
self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
- # Step7(监督建模)切换时自动填充训练数据和输出路径
+ # Step7(水质光谱指数计算)切换时自动填充水质参数 CSV
elif index == 6:
self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
- # Step8 水色指数反演切换时自动填充光谱数据和输出路径
+ # Step8(水质参数指数计算)切换时自动填充水质参数 CSV
elif index == 7:
+ self.step8_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)
+
+ # Step8b(水色指数反演)切换时自动填充光谱数据和输出路径
+ elif index == 9:
self.step8_waterindex_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
- # Step9 浓度反演切换时自动填充 QAA 结果和输出路径
- elif index == 8:
+ # Step10(浓度反演)切换时自动填充 QAA 结果和输出路径
+ elif index == 10:
self.step9_concentration_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)
-
# Step11(机器学习预测)切换时自动填充采样光谱和模型目录
- elif index == 10:
- self.step11_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
+ elif index == 11:
+ self.step10_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step14(专题图生成)切换时自动填充预测结果目录
- elif index == 11:
+ elif index == 13:
self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 可视化分析面板切换时自动推断图像目录并加载目录树
- elif index == 12:
+ elif index == 14:
self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
def apply_stylesheet(self):
@@ -2285,9 +2296,9 @@ class WaterQualityGUI(QMainWindow):
if 'step7' in config:
self.step7_panel.set_config(config['step7'])
if 'step10' in config:
- self.step10_panel.set_config(config['step10'])
+ self.step4_panel.set_config(config['step10'])
if 'step11_ml' in config:
- self.step11_ml_panel.set_config(config['step11_ml'])
+ self.step10_ml_panel.set_config(config['step11_ml'])
if 'step14' in config:
self.step14_panel.set_config(config['step14'])
if 'visualization' in config:
@@ -2334,8 +2345,8 @@ class WaterQualityGUI(QMainWindow):
'step5': self.step5_panel.get_config(),
'step6': self.step6_panel.get_config(),
'step7': self.step7_panel.get_config(),
- 'step10': self.step10_panel.get_config(),
- 'step11_ml': self.step11_ml_panel.get_config(),
+ 'step10': self.step4_panel.get_config(),
+ 'step11_ml': self.step10_ml_panel.get_config(),
'step14': self.step14_panel.get_config(),
'visualization': self.viz_panel.get_config(),
'report_generation': self.report_panel.get_config(),
@@ -2389,8 +2400,8 @@ class WaterQualityGUI(QMainWindow):
'step5': self.step5_panel,
'step6': self.step6_panel,
'step7': self.step7_panel,
- 'step10': self.step10_panel,
- 'step11_ml': self.step11_ml_panel,
+ 'step10': self.step4_panel,
+ 'step11_ml': self.step10_ml_panel,
'step14': self.step14_panel,
}
return panel_map.get(step_id)
@@ -2591,8 +2602,8 @@ class WaterQualityGUI(QMainWindow):
('step5', self.step5_panel),
('step6', self.step6_panel),
('step7', self.step7_panel),
- ('step10', self.step10_panel),
- ('step11_ml', self.step11_ml_panel),
+ ('step10', self.step4_panel),
+ ('step11_ml', self.step10_ml_panel),
('step14', self.step14_panel)
]
@@ -3219,14 +3230,14 @@ class WaterQualityGUI(QMainWindow):
def update_ui_for_training_mode(self):
"""根据训练数据模式更新UI状态"""
# 需要禁用的步骤ID(对应无训练数据模式下需要禁用的步骤)
- disabled_step_ids = ['step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9']
+ disabled_step_ids = ['step4', 'step5', 'step6', 'step7', 'step8', 'step8_non_empirical_modeling', 'step9']
# 更新标签页的启用/禁用状态
step_id_to_tab = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3,
- 'step5': 4, 'step6': 5, 'step7': 6, 'step8_non_empirical_modeling': 7,
- 'step9': 8, 'step10': 9, 'step11_ml': 10, 'step11': 11,
- 'step12': 12, 'step14': 13, 'step9_viz': 14
+ 'step5': 4, 'step6': 5, 'step7': 6, 'step8': 7,
+ 'step9': 8, 'step8_non_empirical_modeling': 9, 'step9_concentration': 10,
+ 'step10': 11, 'step11': 12, 'step14': 13, 'step9_viz': 14
}
for step_id in disabled_step_ids: