refactor(gui): 重命名面板序号 step4-11,采样点布设移至 step4,ML 建模移至 step9

This commit is contained in:
DXC
2026-06-11 11:13:16 +08:00
parent 184f5fe9f4
commit 3c4d4081a4
16 changed files with 1538 additions and 1939 deletions

View File

@ -20,13 +20,16 @@ from typing import Any, Dict, List, Optional, Set
# ============================================================ # ============================================================
STEP_MAP_OLD_TO_NEW: Dict[str, str] = { STEP_MAP_OLD_TO_NEW: Dict[str, str] = {
"step5_5": "step8", "step5_5": "step7",
"step6_5": "step8_non_empirical_modeling", "step6_5": "step8_non_empirical_modeling",
"step6_75": "step9", "step6_75": "step9",
"step8_5": "step11", "step8_5": "step11",
"step8_75": "step12", "step7": "step8",
"step7": "step10", "step8": "step7",
"step9": "step14", "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()} STEP_MAP_NEW_TO_OLD: Dict[str, str] = {v: k for k, v in STEP_MAP_OLD_TO_NEW.items()}

View File

@ -115,14 +115,14 @@ PIPELINE_STEPS: List[StepSpec] = [
description="实测样本点光谱提取", description="实测样本点光谱提取",
), ),
StepSpec( 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"], requires=["training_csv_path"], produces=["indices_path", "trad_indices_dir"],
required_input_files=["training_csv_path"], required_input_files=["training_csv_path"],
output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv", output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv",
description="水质光谱指数计算双轨输出A轨宽表 + B轨单文件", description="水质参数指数计算双轨输出A轨宽表 + B轨单文件",
), ),
StepSpec( StepSpec(
step_id="step7", method_name="step7_ml_modeling", step_id="step8", method_name="step8_ml_modeling",
requires=["training_csv_path"], produces=["models_dir"], requires=["training_csv_path"], produces=["models_dir"],
required_input_files=["training_csv_path"], required_input_files=["training_csv_path"],
output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl", output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl",
@ -138,18 +138,17 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验统计回归", description="非经验统计回归",
), ),
StepSpec( StepSpec(
step_id="step9", method_name="step9_custom_regression", step_id="step9", method_name="step9_watercolor_inversion",
requires=["indices_path"], produces=["models_dir"], requires=["deglint_img_path", "water_mask_path"], produces=["watercolor_index_dir"],
parameter_map={"indices_path": "csv_path"}, required_input_files=["deglint_img_path"],
required_input_files=["indices_path"], output_file="{work_dir}/9_WaterColor_Index_Images",
output_file="{work_dir}/9_Custom_Regression_Modeling/custom_regression_models.pkl", description="水色指数反演BSQ 影像直接处理)",
description="自定义回归分析",
), ),
StepSpec( StepSpec(
step_id="step10", method_name="step10_sampling", step_id="step10", method_name="step10_sampling",
requires=["deglint_img_path", "water_mask_path"], produces=["sampling_csv_path"], requires=["deglint_img_path", "water_mask_path"], produces=["sampling_csv_path"],
required_input_files=["deglint_img_path", "water_mask_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="整景密集采样点生成 + 光谱提取", description="整景密集采样点生成 + 光谱提取",
), ),
StepSpec( StepSpec(
@ -167,15 +166,6 @@ PIPELINE_STEPS: List[StepSpec] = [
output_file="{work_dir}/11_12_13_predictions/non_empirical_predictions", output_file="{work_dir}/11_12_13_predictions/non_empirical_predictions",
description="非经验模型预测", 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( StepSpec(
step_id="step14", method_name="step14_distribution_map", step_id="step14", method_name="step14_distribution_map",
requires=["prediction_csv_path", "boundary_shp_path"], requires=["prediction_csv_path", "boundary_shp_path"],

View File

@ -59,14 +59,14 @@ class PreflightDialog(QDialog):
"step3": ("耀斑去除", 2), "step3": ("耀斑去除", 2),
"step4": ("数据清洗", 3), "step4": ("数据清洗", 3),
"step5": ("特征构建", 4), "step5": ("特征构建", 4),
"step8": ("水质指数", 5), "step7": ("水质指数", 5),
"step7": ("监督建模", 6), "step8": ("监督建模", 6),
"step8_non_empirical_modeling": ("回归建模", 7), "step8_non_empirical_modeling": ("回归建模", 7),
"step9": ("自定义回归建模", 8), "step9": ("水色指数反演", 8),
"step10": ("采样点布设", 9), "step9_concentration": ("浓度反演", 9),
"step11_ml": ("监督预测", 10), "step10": ("采样点布设", 10),
"step11": ("回归预测", 11), "step11_ml": ("监督预测", 11),
"step12": ("自定义回归预测", 12), "step11": ("回归预测", 12),
"step14": ("专题图生成", 13), "step14": ("专题图生成", 13),
} }

View File

@ -325,16 +325,15 @@ class WorkerThread(QThread):
'step3': 'step3_remove_glint', 'step3': 'step3_remove_glint',
'step4': 'step4_process_csv', 'step4': 'step4_process_csv',
'step5': 'step5_extract_training_spectra', 'step5': 'step5_extract_training_spectra',
'step6': 'step6_water_quality_indices', 'step7': 'step7_water_quality_indices',
'step7': 'step7_ml_modeling', 'step8': 'step8_ml_modeling',
'step8_non_empirical_modeling': 'step8_non_empirical_modeling', 'step8_non_empirical_modeling': 'step8_non_empirical_modeling',
'step8_qaa': 'step8_qaa_inversion', 'step8_qaa': 'step8_qaa_inversion',
'step9': 'step9_watercolor_inversion',
'step9_concentration': 'step9_concentration_inversion', 'step9_concentration': 'step9_concentration_inversion',
'step9': 'step9_custom_regression',
'step10': 'step10_sampling', 'step10': 'step10_sampling',
'step11_ml': 'step11_ml_prediction', 'step11_ml': 'step11_ml_prediction',
'step11': 'step11_non_empirical_prediction', 'step11': 'step11_non_empirical_prediction',
'step12': 'step12_custom_regression_prediction',
'step14': 'step14_distribution_map' 'step14': 'step14_distribution_map'
} }

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step8 面板 - 机器学习预测 Step11 面板 - 机器学习预测
""" """
import os import os
@ -19,7 +19,7 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step11MlPanel(QWidget): class Step10MlPanel(QWidget):
"""步骤11机器学习预测""" """步骤11机器学习预测"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -190,7 +190,7 @@ class Step11MlPanel(QWidget):
"""浏览模型母文件夹,自动扫描子目录中的 .joblib 文件""" """浏览模型母文件夹,自动扫描子目录中的 .joblib 文件"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "7_Supervised_Model_Training") default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory( dir_path = QFileDialog.getExistingDirectory(
self, self,
"选择模型母文件夹", "选择模型母文件夹",
@ -216,7 +216,6 @@ class Step11MlPanel(QWidget):
] ]
if not joblib_files: if not joblib_files:
continue continue
# 每个子目录只取第一个 .joblib 文件(与 batch 逻辑一致)
joblib_path = joblib_files[0].path joblib_path = joblib_files[0].path
try: try:
loaded = joblib.load(joblib_path) loaded = joblib.load(joblib_path)
@ -319,43 +318,41 @@ class Step11MlPanel(QWidget):
main_window = self.window() main_window = self.window()
# 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 # 1. 尝试从 Step4采样点布设读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step10_panel'): if main_window and hasattr(main_window, 'step4_sampling_panel'):
step7_widget = getattr(main_window.step10_panel, 'output_file', None) step4_widget = getattr(main_window.step4_sampling_panel, 'output_file', None)
step7_output_path = "" step4_output_path = ""
if hasattr(step7_widget, 'get_path'): if hasattr(step4_widget, 'get_path'):
step7_output_path = step7_widget.get_path() or "" step4_output_path = step4_widget.get_path() or ""
elif hasattr(step7_widget, 'text'): elif hasattr(step4_widget, 'text'):
step7_output_path = step7_widget.text() or "" step4_output_path = step4_widget.text() or ""
if step7_output_path: if step4_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(step4_output_path):
if not os.path.isabs(step7_output_path): step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path() existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip(): 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 界面读取监督模型目录 # 2. 尝试从 Step9监督建模读取模型目录
if main_window and hasattr(main_window, 'step7_panel'): if main_window and hasattr(main_window, 'step9_panel'):
step6_widget = getattr(main_window.step7_panel, 'output_dir', None) step9_widget = getattr(main_window.step9_panel, 'output_dir', None)
step6_models_dir = "" step9_models_dir = ""
if hasattr(step6_widget, 'get_path'): if hasattr(step9_widget, 'get_path'):
step6_models_dir = step6_widget.get_path() or "" step9_models_dir = step9_widget.get_path() or ""
elif hasattr(step6_widget, 'text'): elif hasattr(step9_widget, 'text'):
step6_models_dir = step6_widget.text() or "" step9_models_dir = step9_widget.text() or ""
if step6_models_dir: if step9_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(step9_models_dir):
if not os.path.isabs(step6_models_dir): step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/')
step6_models_dir = os.path.join(self.work_dir or '', step6_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path() existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip(): 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. 自动填充输出路径(机器学习预测目录) # 3. 自动填充输出路径(机器学习预测目录)
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path() existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -378,7 +375,7 @@ class Step11MlPanel(QWidget):
"""浏览模型目录""" """浏览模型目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: 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) dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path: if dir_path:
self.models_dir_file.set_path(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']) self.output_file.set_path(config['output_path'])
def run_step(self): def run_step(self):
"""独立运行步骤8""" """独立运行步骤11"""
sampling_csv_path = self.sampling_csv_file.get_path() sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path: if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件") QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
@ -431,7 +428,6 @@ class Step11MlPanel(QWidget):
"请先点击「浏览...」按钮选择模型母文件夹!", "请先点击「浏览...」按钮选择模型母文件夹!",
) )
return return
# 只传递用户勾选的模型
checked_dict = self._get_checked_models_dict() checked_dict = self._get_checked_models_dict()
if not checked_dict: if not checked_dict:
QMessageBox.warning( QMessageBox.warning(
@ -459,4 +455,4 @@ class Step11MlPanel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step11_ml': self.get_config()} config = {'step11_ml': self.get_config()}
main_window.run_single_step('step11_ml', config) main_window.run_single_step('step11_ml', config)

View File

@ -17,7 +17,7 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step11Panel(QWidget): class Step11NonEmpiricalPanel(QWidget):
"""步骤11非经验模型预测""" """步骤11非经验模型预测"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)

View File

@ -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}")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step10 面板 - 采样点生成 Step4 面板 - 采样点布设
""" """
import os import os
@ -16,8 +16,8 @@ from src.gui.dialogs import SamplingViewerDialog
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step10Panel(QWidget): class Step4SamplingPanel(QWidget):
"""步骤10:采样点生成""" """步骤4:采样点布设"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -71,7 +71,7 @@ class Step10Panel(QWidget):
"输出采样点:", "输出采样点:",
"CSV Files (*.csv);;All Files (*.*)" "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) layout.addWidget(self.output_file)
# 启用步骤 # 启用步骤
@ -207,7 +207,7 @@ class Step10Panel(QWidget):
# 3. 自动填充输出路径(绝对路径) # 3. 自动填充输出路径(绝对路径)
if self.work_dir: 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) os.makedirs(os.path.dirname(output_path), exist_ok=True)
self.output_file.set_path(output_path.replace('\\', '/')) self.output_file.set_path(output_path.replace('\\', '/'))
@ -215,7 +215,7 @@ class Step10Panel(QWidget):
self._check_csv_exists() self._check_csv_exists()
def run_step(self): def run_step(self):
"""独立运行步骤10""" """独立运行步骤4"""
deglint_img_path = self.deglint_img_file.get_path() deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path: if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!") QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
@ -223,8 +223,8 @@ class Step10Panel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step10': self.get_config()} config = {'step4': self.get_config()}
main_window.run_single_step('step10', config) main_window.run_single_step('step4', config)
def _check_csv_exists(self): def _check_csv_exists(self):
"""检查 output csv 是否存在,驱动预览按钮启停""" """检查 output csv 是否存在,驱动预览按钮启停"""
@ -243,10 +243,10 @@ class Step10Panel(QWidget):
if not csv_path or not os.path.exists(csv_path): if not csv_path or not os.path.exists(csv_path):
QMessageBox.warning( QMessageBox.warning(
self, "文件不存在", self, "文件不存在",
f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤10生成数据。" f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤4生成数据。"
) )
return return
dialog = SamplingViewerDialog(csv_path, self) dialog = SamplingViewerDialog(csv_path, self)
dialog.exec_() dialog.exec_()
# 弹窗关闭后再次检查状态(可能文件被覆盖等) # 弹窗关闭后再次检查状态(可能文件被覆盖等)
self._check_csv_exists() self._check_csv_exists()

View File

@ -1,16 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step5 面板 - 光谱提取 Step4 面板 - 数据预处理
""" """
import os import os
import pandas as pd
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel, QWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QLabel,
QSpinBox, QPushButton, QCheckBox, QMessageBox, QSpinBox, QPushButton, QCheckBox, QTableView,
QAbstractItemView, QHeaderView, QMessageBox,
) )
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
@ -18,7 +19,7 @@ from src.gui.styles import ModernStylesheet
class Step5Panel(QWidget): class Step5Panel(QWidget):
"""步骤5光谱提取""" """步骤5数据清洗"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -27,67 +28,55 @@ class Step5Panel(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
# 标题 # 标题
title = QLabel("步骤5训练样本光谱提取")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
# 去耀斑影像文件(用于独立运行) # CSV文件
self.deglint_img_file = FileSelectWidget(
"去耀斑影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.deglint_img_file)
# 处理后的CSV文件用于独立运行
self.csv_file = FileSelectWidget( self.csv_file = FileSelectWidget(
"处理后CSV:", "水质参数文件:",
"CSV Files (*.csv);;All Files (*.*)" "CSV Files (*.csv);;All Files (*.*)"
) )
layout.addWidget(self.csv_file) layout.addWidget(self.csv_file)
# 水体掩膜文件(可选,用于独立运行) hint = QLabel("提示: 处理CSV文件筛选剔除异常值")
self.water_mask_file = FileSelectWidget( hint.setStyleSheet("color: #666; font-size: 10px;")
"水体掩膜:", layout.addWidget(hint)
"Mask Files (*.dat *.tif);;All Files (*.*)"
)
self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
layout.addWidget(self.water_mask_file)
self.glint_mask_file = FileSelectWidget( preview_group = QGroupBox("CSV数据预览")
"耀斑掩膜:", preview_layout = QVBoxLayout()
"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)
# 参数设置 controls_layout = QHBoxLayout()
params_group = QGroupBox("提取参数") controls_layout.addWidget(QLabel("预览行数:"))
params_layout = QFormLayout() 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.preview_table = QTableView()
self.radius.setRange(1, 50) self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.radius.setValue(5) self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
params_layout.addRow("采样半径(像素):", self.radius) 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.preview_status_label = QLabel("请选择CSV文件并点击刷新预览")
self.source_epsg.setRange(1000, 99999) self.preview_status_label.setStyleSheet("color: #666; font-size: 11px;")
self.source_epsg.setValue(4326)
params_layout.addRow("源坐标系EPSG:", self.source_epsg)
params_group.setLayout(params_layout) preview_layout.addLayout(controls_layout)
layout.addWidget(params_group) 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( self.output_file = FileSelectWidget(
"输出训练数据:", "输出处理后CSV:",
"CSV Files (*.csv);;All Files (*.*)" "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) layout.addWidget(self.output_file)
# 启用步骤 # 启用步骤
@ -103,54 +92,33 @@ class Step5Panel(QWidget):
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
# 信号连接:影像文件路径变化时动态更新波段范围 self.reset_preview()
def get_config(self): def get_config(self):
"""获取配置""" """获取配置"""
config = { config = {
'radius': self.radius.value(), 'csv_path': self.csv_file.get_path(),
'source_epsg': self.source_epsg.value(),
} }
# 添加独立运行所需的文件路径 output_path = self.output_file.get_path()
deglint_img_path = self.deglint_img_file.get_path() if output_path:
if deglint_img_path: config['output_path'] = output_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 return config
def set_config(self, 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: if 'csv_path' in config:
self.csv_file.set_path(config['csv_path']) self.csv_file.set_path(config['csv_path'])
if 'boundary_path' in config: self.load_csv_preview()
self.water_mask_file.set_path(config['boundary_path']) if 'output_path' in config:
if 'glint_mask_path' in config: self.output_file.set_path(config['output_path'])
self.glint_mask_file.set_path(config['glint_mask_path'])
def update_from_config(self, work_dir=None, pipeline=None): def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转 """从全局配置自动填充输出路径
Args: Args:
work_dir: 工作目录路径 work_dir: 工作目录路径
pipeline: Pipeline 实例用于获取步骤1生成的水域掩膜路径 pipeline: Pipeline 实例(未使用,保留接口兼容性)
""" """
# 保存工作目录引用
if work_dir: if work_dir:
self.work_dir = work_dir self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir: elif hasattr(self, 'work_dir') and self.work_dir:
@ -158,82 +126,60 @@ class Step5Panel(QWidget):
else: else:
self.work_dir = None 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: 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) 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) self.output_file.set_path(default_output_path)
else: else:
self.output_file.set_path("") 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): def run_step(self):
"""独立运行步骤5""" """独立运行步骤4"""
# 验证输入 # 验证输入
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path() csv_path = self.csv_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
if not csv_path: if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件!") QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return
if not self.glint_mask_file.get_path():
QMessageBox.warning(
self,
"输入错误",
"独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
"请提供与去耀斑影像对应的耀斑二值掩膜一般为步骤2输出的 severe_glint_area.dat",
)
return return
# 获取主窗口并运行步骤 # 获取主窗口并运行步骤
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step5': self.get_config()} config = {'step4': self.get_config()}
main_window.run_single_step('step5', 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}")

View File

@ -1,423 +1,239 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step5 面板 - 光谱提取
"""
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel,
QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox, QSpinBox, QPushButton, QCheckBox, QMessageBox,
QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
QRadioButton, QButtonGroup
) )
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet 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): class Step6Panel(QWidget):
COLOR_RATIO = QColor(255, 255, 255) """步骤6光谱特征"""
COLOR_CONCENTRATION = QColor(220, 240, 255)
COLOR_HEADER = QColor(245, 245, 245)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) 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.init_ui()
self._auto_load_formulas()
def init_ui(self): def init_ui(self):
main_layout = QVBoxLayout() layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(10)
# 1. 公式配置源 (只读) # 标题
path_group = QGroupBox("公式配置源 (内置)") title = QLabel("步骤5训练样本光谱提取")
path_layout = QVBoxLayout() title.setFont(QFont("Arial", 12, QFont.Bold))
self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)") layout.addWidget(title)
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("输入样本数据") self.deglint_img_file = FileSelectWidget(
input_layout = QVBoxLayout() "去耀斑影像:",
self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)") "Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
input_layout.addWidget(self.training_data_widget) )
input_group.setLayout(input_layout) layout.addWidget(self.deglint_img_file)
main_layout.addWidget(input_group)
# 3. 公式选择区 (分组 ListWidget) # 处理后的CSV文件用于独立运行
self.formula_group = QGroupBox("待计算水质指数勾选") self.csv_file = FileSelectWidget(
formula_outer_layout = QVBoxLayout() "处理后CSV:",
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.csv_file)
btn_layout = QHBoxLayout() # 水体掩膜文件(可选,用于独立运行)
self.select_all_btn = QPushButton("全选") self.water_mask_file = FileSelectWidget(
self.deselect_all_btn = QPushButton("清空") "水体掩膜:",
self.select_ratio_btn = QPushButton("仅选比值型") "Mask Files (*.dat *.tif);;All Files (*.*)"
self.select_conc_btn = QPushButton("仅选浓度型") )
self.select_all_btn.clicked.connect(self.select_all_formulas) self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
self.deselect_all_btn.clicked.connect(self.deselect_all_formulas) layout.addWidget(self.water_mask_file)
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.refresh_button = QPushButton("重新加载") self.glint_mask_file = FileSelectWidget(
self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False)) "耀斑掩膜:",
btn_layout.addWidget(self.refresh_button) "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() self.radius = QSpinBox()
scroll.setWidgetResizable(True) self.radius.setRange(1, 50)
scroll.setMinimumHeight(280) self.radius.setValue(5)
self.scroll_content = QWidget() params_layout.addRow("采样半径(像素):", self.radius)
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.formula_list = QListWidget() self.source_epsg = QSpinBox()
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection) self.source_epsg.setRange(1000, 99999)
self.formula_list.itemChanged.connect(self._on_item_changed) self.source_epsg.setValue(4326)
self.formula_layout.addWidget(self.formula_list) params_layout.addRow("源坐标系EPSG:", self.source_epsg)
scroll.setWidget(self.scroll_content) params_group.setLayout(params_layout)
formula_outer_layout.addWidget(scroll) 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("输出模式") self.enable_checkbox = QCheckBox("启用此步骤")
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.setChecked(True) 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. 运行按钮 layout.addStretch()
self.run_button = QPushButton("立即执行计算") self.setLayout(layout)
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)
self.setLayout(main_layout) def get_config(self):
"""获取配置"""
def _on_item_changed(self, item: QListWidgetItem): config = {
if item.checkState() == Qt.Checked: 'radius': self.radius.value(),
bg_color = self.COLOR_RATIO 'source_epsg': self.source_epsg.value(),
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(),
} }
# 添加独立运行所需的文件路径
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): def set_config(self, config):
if 'training_csv_path' in config: """设置配置"""
self.training_data_widget.set_path(config['training_csv_path']) if 'radius' in config:
if 'formula_names' in config: self.radius.setValue(config['radius'])
sel = set(config['formula_names']) if 'source_epsg' in config:
for name, item in self.index_checkboxes.items(): self.source_epsg.setValue(config['source_epsg'])
item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked) if 'deglint_img_path' in config:
self.enable_checkbox.setChecked(config.get('enabled', True)) self.deglint_img_file.set_path(config['deglint_img_path'])
if 'output_mode' in config: if 'csv_path' in config:
btn = self.mode_group.button(config['output_mode']) self.csv_file.set_path(config['csv_path'])
if btn: if 'boundary_path' in config:
btn.setChecked(True) 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): def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例用于获取步骤1生成的水域掩膜路径
"""
# 保存工作目录引用
if work_dir: if work_dir:
self.work_dir = work_dir self.work_dir = work_dir
main = self.window() elif hasattr(self, 'work_dir') and self.work_dir:
if hasattr(main, 'step5_panel'): pass
p5 = main.step5_panel.output_file.get_path() else:
if p5: self.work_dir = None
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]: # 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: if self.work_dir:
return self.work_dir output_dir = os.path.join(self.work_dir, "5_training_spectra")
main = self.window() os.makedirs(output_dir, exist_ok=True)
if hasattr(main, 'work_dir') and main.work_dir: default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
return main.work_dir self.output_file.set_path(default_output_path)
return None else:
self.output_file.set_path("")
def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]: # 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板
coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x'] main_window = self.window()
lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y'] if main_window and hasattr(main_window, 'step5_panel'):
step4_output_path = main_window.step5_panel.output_file.get_path()
x_col, y_col = None, None if step4_output_path:
for col in df.columns: # 若为相对路径,使用 work_dir 合成为绝对路径
cl = col.lower() if not os.path.isabs(step4_output_path):
if x_col is None and any(c in cl for c in coord_candidates): step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
x_col = col existing_csv = self.csv_file.get_path()
if y_col is None and any(c in cl for c in lat_candidates): if not existing_csv or not existing_csv.strip():
y_col = col self.csv_file.set_path(step4_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]
return x_col or 'x_coord', y_col or 'y_coord'
def run_step(self): def run_step(self):
config = self.get_config() """独立运行步骤5"""
# 验证输入
if not config['enabled']: deglint_img_path = self.deglint_img_file.get_path()
QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)") csv_path = self.csv_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return return
if not csv_path:
training_path = config['training_csv_path'] QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件")
if not training_path or not os.path.exists(training_path):
QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
return return
if not self.glint_mask_file.get_path():
formula_names = config['formula_names'] QMessageBox.warning(
if not formula_names: self,
QMessageBox.warning(self, "提示", "请至少勾选一个公式") "输入错误",
return "独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
"请提供与去耀斑影像对应的耀斑二值掩膜一般为步骤2输出的 severe_glint_area.dat",
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(),
) )
return
# 读取计算结果(宽表) # 获取主窗口并运行步骤
if indices_csv_path and os.path.exists(indices_csv_path): main_window = self.window()
output_df = pd.read_csv(indices_csv_path) if hasattr(main_window, 'run_single_step'):
else: config = {'step5': self.get_config()}
output_df = spec_df # fallback main_window.run_single_step('step5', config)
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()}")

View File

@ -1,415 +1,423 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step7 面板 - 机器学习建模
"""
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox, QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
QPushButton, QFileDialog, QMessageBox, QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
QRadioButton, QButtonGroup
) )
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet 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)
PREPROC_CHINESE = { internal = os.path.join(exe_dir, '_internal', relative_path)
'None': '无 (None)', if os.path.exists(internal):
'MMS': '最小-最大归一化 (MMS)', return internal
'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"
MODEL_CHINESE = { return str(base_dir / os.path.basename(relative_path))
# 线性模型
'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 Step7Panel(QWidget): 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): def __init__(self, parent=None):
super().__init__(parent) 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.init_ui()
self._auto_load_formulas()
def init_ui(self): 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)
# 训练数据文件(用于独立运行) # 3. 公式选择区 (分组 ListWidget)
self.training_csv_file = FileSelectWidget( self.formula_group = QGroupBox("待计算水质指数勾选")
"训练数据:", formula_outer_layout = QVBoxLayout()
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.training_csv_file)
# 机器学习模型页面 btn_layout = QHBoxLayout()
self.ml_page = QWidget() self.select_all_btn = QPushButton("全选")
self.create_ml_page() self.deselect_all_btn = QPushButton("清空")
layout.addWidget(self.ml_page) 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.refresh_button = QPushButton("重新加载")
self.output_path = FileSelectWidget( self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
"输出文件:", btn_layout.addWidget(self.refresh_button)
"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()
self.run_btn = QPushButton("独立运行此步骤") scroll.setWidgetResizable(True)
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) scroll.setMinimumHeight(280)
self.run_btn.clicked.connect(self.run_step) self.scroll_content = QWidget()
layout.addWidget(self.run_btn) 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.formula_list = QListWidget()
self.setLayout(layout) 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): scroll.setWidget(self.scroll_content)
"""创建机器学习模型页面""" formula_outer_layout.addWidget(scroll)
layout = QVBoxLayout()
# 参数设置 self.formula_group.setLayout(formula_outer_layout)
params_group = QGroupBox("训练参数") main_layout.addWidget(self.formula_group)
params_layout = QFormLayout()
self.feature_start = QLineEdit() # 4. 输出选项
self.feature_start.setText("374.285004") output_group = QGroupBox("输出模式")
params_layout.addRow("特征起始列:", self.feature_start) output_layout = QVBoxLayout()
self.cv_folds = QSpinBox() mode_layout = QHBoxLayout()
self.cv_folds.setRange(2, 10) self.mode_group = QButtonGroup()
self.cv_folds.setValue(3) self.radio_both = QRadioButton("两者皆出")
params_layout.addRow("交叉验证折数:", self.cv_folds) 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) self.enable_checkbox = QCheckBox("启用计算流程")
layout.addWidget(params_group) self.enable_checkbox.setChecked(True)
output_layout.addWidget(self.enable_checkbox)
# 预处理方法 - 多选 output_group.setLayout(output_layout)
preproc_group = QGroupBox("预处理方法 (可多选)") main_layout.addWidget(output_group)
preproc_layout = QVBoxLayout()
preproc_grid = QGridLayout() # 5. 运行按钮
self.preproc_checkboxes = {} self.run_button = QPushButton("立即执行计算")
preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT'] 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): self.setLayout(main_layout)
checkbox = QCheckBox(PREPROC_CHINESE.get(method, method))
checkbox.setChecked(False)
self.preproc_checkboxes[method] = checkbox
preproc_grid.addWidget(checkbox, i // 4, i % 4)
button_layout = QHBoxLayout() def _on_item_changed(self, item: QListWidgetItem):
select_all_btn = QPushButton("全选") if item.checkState() == Qt.Checked:
deselect_all_btn = QPushButton("全不选") bg_color = self.COLOR_RATIO
select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True)) for name, ref_item in self.index_checkboxes.items():
deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False)) if ref_item is item:
button_layout.addWidget(select_all_btn) bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
button_layout.addWidget(deselect_all_btn) break
button_layout.addStretch() item.setBackground(QBrush(bg_color))
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"<b>{group_name}</b>")
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: else:
initial_dir = "" item.setBackground(QBrush(self.COLOR_RATIO))
initial_file = ""
if not initial_dir or not os.path.isdir(initial_dir): def _auto_load_formulas(self):
# 默认定位到 indices 目录 if os.path.exists(self.builtin_formula_path):
work_dir = self._get_default_work_dir() self.refresh_formulas(silent=True)
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
else: else:
self.work_dir = None print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
# 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径 def refresh_formulas(self, silent=False):
main_window = self.window() path = self.builtin_formula_path
if hasattr(main_window, 'step5_panel'): if not os.path.exists(path):
# 优先直接从 Step5 的输出 widget 读取 if not silent:
step5_output = main_window.step5_panel.output_file.get_path() QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{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文件")
return return
main_window = self.window() try:
if hasattr(main_window, 'run_single_step'): df = None
config = {'step7': self.get_config()} for enc in ('utf-8', 'gbk', 'utf-8-sig'):
main_window.run_single_step('step7', config) try:
df = pd.read_csv(path, encoding=enc)
if 'Formula_Name' in df.columns:
break
except Exception:
continue
def get_training_params(self): if df is None or 'Formula_Name' not in df.columns:
"""获取模型训练参数""" if not silent:
return { QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name'")
'pipeline_type': 'machine_learning', return
'feature_start': float(self.feature_start.text()),
'cv_folds': self.cv_folds.value(), self._formula_type_map.clear()
'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()], self._formula_coef_map.clear()
'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()], for _, row in df.iterrows():
'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()] 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()}")

View File

@ -1,424 +1,415 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step7 面板 - 机器学习建模
"""
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox,
QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView, QPushButton, QFileDialog, QMessageBox,
QRadioButton, QButtonGroup
) )
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet 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) PREPROC_CHINESE = {
if os.path.exists(internal): 'None': '无 (None)',
return internal '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): class Step8Panel(QWidget):
COLOR_RATIO = QColor(255, 255, 255) """步骤8水质参数指数计算"""
COLOR_CONCENTRATION = QColor(220, 240, 255)
COLOR_HEADER = QColor(245, 245, 245)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) 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.init_ui()
self._auto_load_formulas()
def init_ui(self): def init_ui(self):
main_layout = QVBoxLayout() 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)
# 3. 公式选择区 (分组 ListWidget) # 训练数据文件(用于独立运行)
self.formula_group = QGroupBox("待计算水质指数勾选") self.training_csv_file = FileSelectWidget(
formula_outer_layout = QVBoxLayout() "训练数据:",
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.training_csv_file)
btn_layout = QHBoxLayout() # 机器学习模型页面
self.select_all_btn = QPushButton("全选") self.ml_page = QWidget()
self.deselect_all_btn = QPushButton("清空") self.create_ml_page()
self.select_ratio_btn = QPushButton("仅选比值型") layout.addWidget(self.ml_page)
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.refresh_button = QPushButton("重新加载") # 输出文件路径
self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False)) self.output_path = FileSelectWidget(
btn_layout.addWidget(self.refresh_button) "输出文件:",
"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) self.run_btn = QPushButton("独立运行此步骤")
scroll.setMinimumHeight(280) self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.scroll_content = QWidget() self.run_btn.clicked.connect(self.run_step)
self.formula_layout = QVBoxLayout(self.scroll_content) layout.addWidget(self.run_btn)
self.formula_layout.setContentsMargins(4, 4, 4, 4)
self.formula_layout.setSpacing(2)
self.formula_layout.setAlignment(Qt.AlignTop)
self.formula_list = QListWidget() layout.addStretch()
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection) self.setLayout(layout)
self.formula_list.itemChanged.connect(self._on_item_changed)
self.formula_layout.addWidget(self.formula_list)
scroll.setWidget(self.scroll_content) def create_ml_page(self):
formula_outer_layout.addWidget(scroll) """创建机器学习模型页面"""
layout = QVBoxLayout()
self.formula_group.setLayout(formula_outer_layout) # 参数设置
main_layout.addWidget(self.formula_group) params_group = QGroupBox("训练参数")
params_layout = QFormLayout()
# 4. 输出选项 self.feature_start = QLineEdit()
output_group = QGroupBox("输出模式") self.feature_start.setText("374.285004")
output_layout = QVBoxLayout() params_layout.addRow("特征起始列:", self.feature_start)
mode_layout = QHBoxLayout() self.cv_folds = QSpinBox()
self.mode_group = QButtonGroup() self.cv_folds.setRange(2, 10)
self.radio_both = QRadioButton("两者皆出") self.cv_folds.setValue(3)
self.radio_wide = QRadioButton("仅宽表") params_layout.addRow("交叉验证折数:", self.cv_folds)
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("启用计算流程") params_group.setLayout(params_layout)
self.enable_checkbox.setChecked(True) layout.addWidget(params_group)
output_layout.addWidget(self.enable_checkbox)
output_group.setLayout(output_layout) # 预处理方法 - 多选
main_layout.addWidget(output_group) preproc_group = QGroupBox("预处理方法 (可多选)")
preproc_layout = QVBoxLayout()
# 5. 运行按钮 preproc_grid = QGridLayout()
self.run_button = QPushButton("立即执行计算") self.preproc_checkboxes = {}
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT']
self.run_button.setMinimumHeight(40)
self.run_button.clicked.connect(self.run_step)
main_layout.addWidget(self.run_button)
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): button_layout = QHBoxLayout()
if item.checkState() == Qt.Checked: select_all_btn = QPushButton("全选")
bg_color = self.COLOR_RATIO deselect_all_btn = QPushButton("全不选")
for name, ref_item in self.index_checkboxes.items(): select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True))
if ref_item is item: deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False))
bg_color = self._formula_color_map.get(name, self.COLOR_RATIO) button_layout.addWidget(select_all_btn)
break button_layout.addWidget(deselect_all_btn)
item.setBackground(QBrush(bg_color)) button_layout.addStretch()
else:
item.setBackground(QBrush(self.COLOR_RATIO))
def _auto_load_formulas(self): preproc_layout.addLayout(preproc_grid)
if os.path.exists(self.builtin_formula_path): preproc_layout.addLayout(button_layout)
self.refresh_formulas(silent=True) preproc_group.setLayout(preproc_layout)
else: layout.addWidget(preproc_group)
print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
def refresh_formulas(self, silent=False): # 模型选择 - 多选
path = self.builtin_formula_path model_group = QGroupBox("模型类型 (可多选)")
if not os.path.exists(path): model_layout = QVBoxLayout()
if not silent:
QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
return
try: model_grid = QGridLayout()
df = None self.model_checkboxes = {}
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: model_groups = [
if not silent: ("【线性模型】", ['LinearRegression', 'Ridge', 'Lasso', 'ElasticNet', 'PLS']),
QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name'") ("【树模型】", ['DecisionTree', 'RF', 'ExtraTrees', 'XGBoost', 'LightGBM', 'CatBoost']),
return ("【集成学习】", ['GradientBoosting', 'AdaBoost']),
("【其他模型】", ['SVR', 'KNN', 'MLP'])
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): row = 0
for group_name, models in model_groups:
group_label = QLabel(f"<b>{group_name}</b>")
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: if 'training_csv_path' in config:
self.training_data_widget.set_path(config['training_csv_path']) self.training_csv_file.set_path(config['training_csv_path'])
if 'formula_names' in config: if 'output_path' in config:
sel = set(config['formula_names']) self.output_path.set_path(config['output_path'])
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): def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充训练数据和输出路径
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
if work_dir: if work_dir:
self.work_dir = work_dir self.work_dir = work_dir
main = self.window() elif hasattr(self, 'work_dir') and self.work_dir:
if hasattr(main, 'step5_panel'): pass
p5 = main.step5_panel.output_file.get_path() else:
if p5: self.work_dir = None
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]: # 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: if self.work_dir:
return self.work_dir indices_dir = os.path.join(self.work_dir, "6_water_quality_indices")
main = self.window() os.makedirs(indices_dir, exist_ok=True)
if hasattr(main, 'work_dir') and main.work_dir: training_csv = self.training_csv_file.get_path()
return main.work_dir if training_csv:
return None basename = os.path.splitext(os.path.basename(training_csv))[0]
output_file = f"{basename}_indices.csv"
def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]: else:
coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x'] output_file = "water_quality_indices.csv"
lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y'] output_path = os.path.join(indices_dir, output_file).replace('\\', '/')
self.output_path.set_path(output_path)
x_col, y_col = None, None else:
for col in df.columns: self.output_path.set_path("")
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): def run_step(self):
config = self.get_config() """独立运行步骤7"""
training_csv_path = self.training_csv_file.get_path()
if not config['enabled']: if not training_csv_path:
QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)") QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")
return return
training_path = config['training_csv_path'] main_window = self.window()
if not training_path or not os.path.exists(training_path): if hasattr(main_window, 'run_single_step'):
QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件") config = {'step7': self.get_config()}
return main_window.run_single_step('step7', config)
formula_names = config['formula_names'] def get_training_params(self):
if not formula_names: """获取模型训练参数"""
QMessageBox.warning(self, "提示", "请至少勾选一个公式") return {
return 'pipeline_type': 'machine_learning',
'feature_start': float(self.feature_start.text()),
output_mode = config['output_mode'] 'cv_folds': self.cv_folds.value(),
'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()],
try: 'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()],
from src.core.steps.data_preparation_step import DataPreparationStep 'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()]
}
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()}")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step8 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像) Step9 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像)
将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像, 将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像,
输出各水质参数指数的 GeoTIFF 栅格图像。 输出各水质参数指数的 GeoTIFF 栅格图像。
@ -98,8 +98,8 @@ class WaterIndexWorker(QThread):
self.progress.emit(msg, pct) self.progress.emit(msg, pct)
class Step8WaterIndexPanel(QWidget): class Step9WaterColorPanel(QWidget):
"""步骤8:水色指数反演(直接处理 BSQ 影像)""" """步骤9:水色指数反演(直接处理 BSQ 影像)"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -115,7 +115,7 @@ class Step8WaterIndexPanel(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
# ---- 标题 ---- # ---- 标题 ----
title = QLabel("步骤8:水色指数反演(高光谱影像直接处理)") title = QLabel("步骤9:水色指数反演(高光谱影像直接处理)")
title.setFont(QFont("Arial", 12, QFont.Bold)) title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title) layout.addWidget(title)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step9 面板 - 浓度反演(基于 QAA 物理反演的二次反演) Step10 面板 - 浓度反演(基于 QAA 物理反演的二次反演)
""" """
import os import os
@ -18,8 +18,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step9ConcentrationPanel(QWidget): class Step10ConcentrationPanel(QWidget):
"""步骤9:浓度反演(物理模型二次反演)""" """步骤10:浓度反演(物理模型二次反演)"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -27,7 +27,7 @@ class Step9ConcentrationPanel(QWidget):
def init_ui(self): def init_ui(self):
layout = QVBoxLayout() layout = QVBoxLayout()
title = QLabel("步骤9:浓度反演(物理模型二次反演)") title = QLabel("步骤10:浓度反演(物理模型二次反演)")
title.setFont(QFont("Arial", 12, QFont.Bold)) title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title) layout.addWidget(title)

View File

@ -1,400 +1,424 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step9 面板 - 自定义回归分析 Step9 面板 - 机器学习建模
""" """
import os import os
from typing import Dict import sys
import pandas as pd import pandas as pd
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
QScrollArea, 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.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet 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): 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): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.x_column_checkboxes: Dict[str, QCheckBox] = {} self.index_checkboxes: Dict[str, QListWidgetItem] = {}
self.y_column_checkboxes: Dict[str, QCheckBox] = {} self.work_dir: Optional[str] = None
self.method_checkboxes: Dict[str, QCheckBox] = {} self.builtin_formula_path = get_resource_path("waterindex.csv")
self.csv_columns = [] 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.init_ui()
self._auto_load_formulas()
def init_ui(self): def init_ui(self):
layout = QVBoxLayout() main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(10)
hint = QLabel("指定自变量与因变量列,批量尝试不同回归方法") # 1. 公式配置源 (只读)
hint.setStyleSheet("color: #666; font-size: 11px;") path_group = QGroupBox("公式配置源 (内置)")
layout.addWidget(hint) 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文件选择 # 2. 训练数据输入
csv_group = QGroupBox("数据文件") input_group = QGroupBox("输入样本数据")
csv_layout = QVBoxLayout() 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( # 3. 公式选择区 (分组 ListWidget)
"输入CSV文件:", self.formula_group = QGroupBox("待计算水质指数勾选")
"CSV Files (*.csv);;All Files (*.*)" formula_outer_layout = QVBoxLayout()
)
self.csv_file.line_edit.textChanged.connect(self.on_csv_file_changed)
csv_layout.addWidget(self.csv_file)
self.refresh_btn = QPushButton("刷新列信息") btn_layout = QHBoxLayout()
self.refresh_btn.clicked.connect(self.refresh_csv_columns) self.select_all_btn = QPushButton("全选")
csv_layout.addWidget(self.refresh_btn) 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) self.refresh_button = QPushButton("重新加载")
layout.addWidget(csv_group) self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
btn_layout.addWidget(self.refresh_button)
# 自变量选择 formula_outer_layout.addLayout(btn_layout)
x_group = QGroupBox("自变量列选择 (可多选)")
x_layout = QVBoxLayout()
x_scroll = QScrollArea() scroll = QScrollArea()
x_scroll.setWidgetResizable(True) scroll.setWidgetResizable(True)
x_scroll.setMinimumHeight(250) scroll.setMinimumHeight(280)
x_scroll.setMaximumHeight(350) 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.formula_list = QListWidget()
self.x_columns_layout = QGridLayout() self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
x_widget.setLayout(self.x_columns_layout) self.formula_list.itemChanged.connect(self._on_item_changed)
self.formula_layout.addWidget(self.formula_list)
x_scroll.setWidget(x_widget) scroll.setWidget(self.scroll_content)
x_layout.addWidget(x_scroll) formula_outer_layout.addWidget(scroll)
x_btn_layout = QHBoxLayout() self.formula_group.setLayout(formula_outer_layout)
self.x_select_all = QPushButton("全选") main_layout.addWidget(self.formula_group)
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)
x_group.setLayout(x_layout) # 4. 输出选项
layout.addWidget(x_group) output_group = QGroupBox("输出模式")
output_layout = QVBoxLayout()
# 因变量选择 mode_layout = QHBoxLayout()
y_group = QGroupBox("因变量列选择 (可多选)") self.mode_group = QButtonGroup()
y_layout = QVBoxLayout() 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() self.enable_checkbox = QCheckBox("启用计算流程")
y_scroll.setWidgetResizable(True) self.enable_checkbox.setChecked(True)
y_scroll.setMinimumHeight(200) output_layout.addWidget(self.enable_checkbox)
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)
output_group.setLayout(output_layout) output_group.setLayout(output_layout)
layout.addWidget(output_group) main_layout.addWidget(output_group)
# 启用步骤 # 5. 运行按钮
self.enable_checkbox = QCheckBox("启用此步骤") self.run_button = QPushButton("立即执行计算")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_button = QPushButton("独立运行此步骤")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.setMinimumHeight(40)
self.run_button.clicked.connect(self.run_step) self.run_button.clicked.connect(self.run_step)
layout.addWidget(self.run_button) main_layout.addWidget(self.run_button)
layout.addStretch() self.setLayout(main_layout)
self.setLayout(layout)
def toggle_checkboxes(self, checkboxes_dict, checked): def _on_item_changed(self, item: QListWidgetItem):
"""统一设置checkbox状态""" if item.checkState() == Qt.Checked:
for checkbox in checkboxes_dict.values(): bg_color = self.COLOR_RATIO
checkbox.setChecked(checked) 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): def _auto_load_formulas(self):
"""CSV文件改变时自动刷新列信息""" if os.path.exists(self.builtin_formula_path):
self.refresh_csv_columns() self.refresh_formulas(silent=True)
else:
print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
def refresh_csv_columns(self): def refresh_formulas(self, silent=False):
"""刷新CSV文件的列信息""" path = self.builtin_formula_path
csv_path = self.csv_file.get_path() if not os.path.exists(path):
if not csv_path or not os.path.exists(csv_path): if not silent:
self.csv_columns = [] QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
self.update_column_widgets()
return return
try: try:
df = pd.read_csv(csv_path, nrows=0) df = None
self.csv_columns = list(df.columns) for enc in ('utf-8', 'gbk', 'utf-8-sig'):
self.update_column_widgets() 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: except Exception as e:
self.csv_columns = [] if not silent:
self.update_column_widgets() QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
print(f"读取CSV列信息失败: {e}")
def update_column_widgets(self): def _select_ratio_only(self):
"""更新列选择组件""" for name, item in self.index_checkboxes.items():
for checkbox in self.x_column_checkboxes.values(): ftype = self._formula_type_map.get(name, 'ratio')
checkbox.setParent(None) item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
self.x_column_checkboxes.clear()
for checkbox in self.y_column_checkboxes.values(): def _select_conc_only(self):
checkbox.setParent(None) for name, item in self.index_checkboxes.items():
self.y_column_checkboxes.clear() ftype = self._formula_type_map.get(name, 'ratio')
item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
if not self.csv_columns: def select_all_formulas(self):
return for item in self.index_checkboxes.values():
item.setCheckState(Qt.Checked)
for i, col in enumerate(self.csv_columns): def deselect_all_formulas(self):
checkbox = QCheckBox(col) for item in self.index_checkboxes.values():
if any(keyword in col.lower() for keyword in ['index', 'ratio', 'normalized', 'nd', 'b']): item.setCheckState(Qt.Unchecked)
checkbox.setChecked(True)
self.x_column_checkboxes[col] = checkbox
self.x_columns_layout.addWidget(checkbox, i // 3, i % 3)
for i, col in enumerate(self.csv_columns): def get_config(self) -> Dict:
checkbox = QCheckBox(col) selected = [
if any(keyword in col.lower() for keyword in ['chl', 'tn', 'tp', 'turbidity', 'do', 'ph', 'conductivity']): name for name, item in self.index_checkboxes.items()
checkbox.setChecked(True) if item.checkState() == Qt.Checked
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()
] ]
selected_y_columns = [ formula_coefficients = {
col for col, checkbox in self.y_column_checkboxes.items() name: self._formula_coef_map.get(name, [])
if checkbox.isChecked() for name in selected
] }
selected_methods = [
method for method, checkbox in self.method_checkboxes.items()
if checkbox.isChecked()
]
if not selected_methods:
selected_methods = 'all'
return { return {
'csv_path': self.csv_file.get_path() or None, 'training_csv_path': self.training_data_widget.get_path(),
'x_columns': selected_x_columns, 'formula_csv_file': self.builtin_formula_path,
'y_columns': selected_y_columns, 'formula_names': selected,
'methods': selected_methods, 'formula_coefficients': formula_coefficients,
'output_dir': self.output_dir.text().strip() or None, 'enabled': self.enable_checkbox.isChecked(),
'enabled': self.enable_checkbox.isChecked() 'output_mode': self.mode_group.checkedId(),
} }
def set_config(self, config): def set_config(self, config: Dict):
if 'csv_path' in config: if 'training_csv_path' in config:
self.csv_file.set_path(config['csv_path']) self.training_data_widget.set_path(config['training_csv_path'])
self.refresh_csv_columns() if 'formula_names' in config:
sel = set(config['formula_names'])
if 'x_columns' in config: for name, item in self.index_checkboxes.items():
selected_x = set(config['x_columns']) if isinstance(config['x_columns'], list) else set() item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
for col, checkbox in self.x_column_checkboxes.items(): self.enable_checkbox.setChecked(config.get('enabled', True))
checkbox.setChecked(col in selected_x) if 'output_mode' in config:
btn = self.mode_group.button(config['output_mode'])
if 'y_columns' in config: if btn:
selected_y = set(config['y_columns']) if isinstance(config['y_columns'], list) else set() btn.setChecked(True)
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 update_from_config(self, work_dir=None, pipeline=None): 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: def _get_work_dir(self) -> Optional[str]:
work_dir: 工作目录路径 if self.work_dir:
pipeline: Pipeline 实例(未使用,保留接口兼容性) return self.work_dir
""" main = self.window()
try: if hasattr(main, 'work_dir') and main.work_dir:
import traceback return main.work_dir
return None
if work_dir: def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
self.work_dir = work_dir coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
elif hasattr(self, 'work_dir') and self.work_dir: lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utc', 'utm_y', 'pixel_y']
pass
else:
self.work_dir = None
# 1. 尝试从 Step8 界面读取训练光谱 CSV 路径 x_col, y_col = None, None
main_window = self.window() for col in df.columns:
if main_window and hasattr(main_window, 'step8_panel'): cl = col.lower()
step8_widget = main_window.step8_panel.training_data_widget if x_col is None and any(c in cl for c in coord_candidates):
step8_output_path = "" x_col = col
if hasattr(step8_widget, 'get_path'): if y_col is None and any(c in cl for c in lat_candidates):
step8_output_path = step8_widget.get_path() or "" y_col = col
if step8_output_path: if x_col is None and len(df.columns) >= 2:
if not os.path.isabs(step8_output_path): x_col = df.columns[0]
step8_output_path = os.path.join(self.work_dir or '', step8_output_path).replace('\\', '/') if y_col is None and len(df.columns) >= 2:
existing = self.csv_file.get_path() y_col = df.columns[1]
if not existing or not existing.strip():
self.csv_file.set_path(step8_output_path)
# 1.2 尝试从 pipeline 读取 Step 8 宽表 indices_path优先级最高 return x_col or 'x_coord', y_col or 'y_coord'
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()
def run_step(self): 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() config = self.get_config()
parent = self.parent() if not config['enabled']:
while parent and not hasattr(parent, 'run_single_step'): QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
parent = parent.parent() return
if parent and hasattr(parent, 'run_single_step'): training_path = config['training_csv_path']
parent.run_single_step('step9', {'step9': config}) if not training_path or not os.path.exists(training_path):
else: QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
QMessageBox.critical(self, "错误", "无法找到父级GUI对象") 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()}")

View File

@ -117,15 +117,17 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.panels.step1_panel import Step1Panel from src.gui.panels.step1_panel import Step1Panel
from src.gui.panels.step2_panel import Step2Panel from src.gui.panels.step2_panel import Step2Panel
from src.gui.panels.step3_panel import Step3Panel from src.gui.panels.step3_panel import Step3Panel
from src.gui.panels.step4_panel import Step4Panel from src.gui.panels.step4_sampling_panel import Step4SamplingPanel # 采样点布设原step10→新step4
from src.gui.panels.step5_panel import Step5Panel from src.gui.panels.step5_panel import Step5Panel # 数据清洗原step4→新step5
from src.gui.panels.step6_panel import Step6Panel # was step8_panel from src.gui.panels.step6_panel import Step6Panel # 光谱特征原step5→新step6
from src.gui.panels.step7_panel import Step7Panel # was step6_panel from src.gui.panels.step7_panel import Step7Panel # 水质光谱指数原step6→新step7
from src.gui.panels.step8_waterindex_panel import Step8WaterIndexPanel # 水色指数反演 from src.gui.panels.step8_panel import Step8Panel # 水质参数指数原step7→新step8
from src.gui.panels.step9_concentration_panel import Step9ConcentrationPanel # 浓度反演 from src.gui.panels.step8_waterindex_panel import Step9WaterColorPanel # 水色指数反演
from src.gui.panels.step10_panel import Step10Panel # was step7_panel from src.gui.panels.step9_concentration_panel import Step10ConcentrationPanel # 浓度反演
from src.gui.panels.step11_ml_panel import Step11MlPanel # ML prediction (step11_ml) from src.gui.panels.step9_panel import Step9Panel # 机器学习建模原step8→新step9
from src.gui.panels.step14_panel import Step14Panel # was step9_panel 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.dialogs import BandConfirmDialog, AISettingsDialog
from src.gui.panels.visualization_panel import VisualizationPanel from src.gui.panels.visualization_panel import VisualizationPanel
from src.gui.panels.report_generation_panel import ReportGenerationPanel from src.gui.panels.report_generation_panel import ReportGenerationPanel
@ -1846,23 +1848,22 @@ class WaterQualityGUI(QMainWindow):
("step3", "3. 耀斑去除与修复"), ("step3", "3. 耀斑去除与修复"),
], ],
"阶段二:样本数据准备 ": [ "阶段二:样本数据准备 ": [
("step4", "4. 数据标准化处理"), ("step4", "4. 采样点布设"),
("step5", "5. 光谱特征提取"), ("step5", "5. 数据清洗"),
("step6", "6. 水质参数指数计算"), ("step6", "6. 光谱特征"),
("step7", "7. 水质光谱指数计算"),
("step8", "8. 水质参数指数计算"),
], ],
"阶段三:模型构建与训练": [ "阶段三:模型构建与训练": [
("step7", "7. 机器学习模型训练"), ("step9", "9. 机器学习"),
("step8_non_empirical_modeling", "8. 回归模型训练"), ("step8_non_empirical_modeling", "8b. 回归模型训练"),
("step9", "9. 自定义回归模型训练"),
], ],
"阶段四:预测与成果输出 ": [ "阶段四:预测与成果输出 ": [
("step10", "10. 采样点布设"), ("step10", "10. 机器学习预测"),
("step11_ml", "11. 机器学习预测"), ("step11", "11. 回归预测"),
("step11", "12. 回归预测"), ("step14", "12. 专题图生成"),
("step12", "13. 自定义回归预测"), ("step9_viz", "13. 可视化分析"),
("step14", "14. 专题图生成"), ("step_report", "14. 分析报告生成"),
("step9_viz", "15. 可视化分析"),
("step_report", "16. 分析报告生成"),
] ]
} }
@ -1882,7 +1883,7 @@ class WaterQualityGUI(QMainWindow):
self.step_list.addItem(stage_item) 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: for step_id, step_display in steps:
if step_id in HIDDEN_STEP_IDS: if step_id in HIDDEN_STEP_IDS:
continue continue
@ -1956,29 +1957,35 @@ class WaterQualityGUI(QMainWindow):
self.step3_panel = Step3Panel() self.step3_panel = Step3Panel()
self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除") self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除")
self.step4_panel = Step4Panel() self.step4_panel = Step4SamplingPanel()
self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "数据清洗") self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "采样点布设")
self.step5_panel = Step5Panel() 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.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.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.step8_panel = Step8Panel()
self.step_stack.addTab(self.create_scroll_area(self.step8_waterindex_panel), QIcon(self.get_icon_path("6.png")), "色指数反演") self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("7.png")), "质参数指数计算")
self.step9_concentration_panel = Step9ConcentrationPanel() self.step9_panel = Step9Panel()
self.step_stack.addTab(self.create_scroll_area(self.step9_concentration_panel), QIcon(self.get_icon_path("6.png")), "浓度反演") self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("8.png")), "机器学习建模")
self.step10_panel = Step10Panel() self.step8_waterindex_panel = Step9WaterColorPanel()
self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设") 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.step9_concentration_panel = Step10ConcentrationPanel()
self.step_stack.addTab(self.create_scroll_area(self.step11_ml_panel), QIcon(self.get_icon_path("8.png")), "监督预测") 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.step14_panel = Step14Panel()
self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成") 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, 'step5': 4,
'step6': 5, 'step6': 5,
'step7': 6, 'step7': 6,
'step8_non_empirical_modeling': 7, 'step8': 7,
'step9': 8, 'step9': 8,
'step10': 9, 'step8_non_empirical_modeling': 9,
'step11_ml': 10, 'step9_concentration': 10,
'step11': 11, 'step10': 11,
'step12': 12, 'step11': 12,
'step14': 13, 'step14': 13,
'step9_viz': 14, 'step9_viz': 14,
'step_report': 15, 'step_report': 15,
@ -2164,12 +2171,12 @@ class WaterQualityGUI(QMainWindow):
4: 'step5', 4: 'step5',
5: 'step6', 5: 'step6',
6: 'step7', 6: 'step7',
7: 'step8_non_empirical_modeling', 7: 'step8',
8: 'step9', 8: 'step9',
9: 'step10', 9: 'step8_non_empirical_modeling',
10: 'step11_ml', 10: 'step9_concentration',
11: 'step11', 11: 'step10',
12: 'step12', 12: 'step11',
13: 'step14', 13: 'step14',
14: 'step9_viz', 14: 'step9_viz',
15: 'step_report', 15: 'step_report',
@ -2199,44 +2206,48 @@ class WaterQualityGUI(QMainWindow):
elif index == 2: elif index == 2:
self.step3_panel.update_from_config(work_dir=self.work_dir) self.step3_panel.update_from_config(work_dir=self.work_dir)
# Step4 切换时自动填充输出路径 # Step4(采样点布设)切换时自动填充输出路径
elif index == 3: elif index == 3:
self.step4_panel.update_from_config(work_dir=self.work_dir) self.step4_panel.update_from_config(work_dir=self.work_dir)
# Step5 切换时自动填充数据流转路径 # Step5(数据清洗)切换时自动填充数据流转路径
elif index == 4: elif index == 4:
self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step6水质光谱指数)切换时自动填充输出路径 # Step6光谱特征)切换时自动填充输出路径
elif index == 5: elif index == 5:
self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step7监督建模)切换时自动填充训练数据和输出路径 # Step7水质光谱指数计算)切换时自动填充水质参数 CSV
elif index == 6: elif index == 6:
self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step8 水色指数反演切换时自动填充光谱数据和输出路径 # Step8(水质参数指数计算)切换时自动填充水质参数 CSV
elif index == 7: 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) self.step8_waterindex_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step9 浓度反演切换时自动填充 QAA 结果和输出路径 # Step10浓度反演切换时自动填充 QAA 结果和输出路径
elif index == 8: elif index == 10:
self.step9_concentration_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) 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机器学习预测切换时自动填充采样光谱和模型目录 # Step11机器学习预测切换时自动填充采样光谱和模型目录
elif index == 10: elif index == 11:
self.step11_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.step10_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step14专题图生成切换时自动填充预测结果目录 # Step14专题图生成切换时自动填充预测结果目录
elif index == 11: elif index == 13:
self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 可视化分析面板切换时自动推断图像目录并加载目录树 # 可视化分析面板切换时自动推断图像目录并加载目录树
elif index == 12: elif index == 14:
self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
def apply_stylesheet(self): def apply_stylesheet(self):
@ -2285,9 +2296,9 @@ class WaterQualityGUI(QMainWindow):
if 'step7' in config: if 'step7' in config:
self.step7_panel.set_config(config['step7']) self.step7_panel.set_config(config['step7'])
if 'step10' in config: if 'step10' in config:
self.step10_panel.set_config(config['step10']) self.step4_panel.set_config(config['step10'])
if 'step11_ml' in config: 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: if 'step14' in config:
self.step14_panel.set_config(config['step14']) self.step14_panel.set_config(config['step14'])
if 'visualization' in config: if 'visualization' in config:
@ -2334,8 +2345,8 @@ class WaterQualityGUI(QMainWindow):
'step5': self.step5_panel.get_config(), 'step5': self.step5_panel.get_config(),
'step6': self.step6_panel.get_config(), 'step6': self.step6_panel.get_config(),
'step7': self.step7_panel.get_config(), 'step7': self.step7_panel.get_config(),
'step10': self.step10_panel.get_config(), 'step10': self.step4_panel.get_config(),
'step11_ml': self.step11_ml_panel.get_config(), 'step11_ml': self.step10_ml_panel.get_config(),
'step14': self.step14_panel.get_config(), 'step14': self.step14_panel.get_config(),
'visualization': self.viz_panel.get_config(), 'visualization': self.viz_panel.get_config(),
'report_generation': self.report_panel.get_config(), 'report_generation': self.report_panel.get_config(),
@ -2389,8 +2400,8 @@ class WaterQualityGUI(QMainWindow):
'step5': self.step5_panel, 'step5': self.step5_panel,
'step6': self.step6_panel, 'step6': self.step6_panel,
'step7': self.step7_panel, 'step7': self.step7_panel,
'step10': self.step10_panel, 'step10': self.step4_panel,
'step11_ml': self.step11_ml_panel, 'step11_ml': self.step10_ml_panel,
'step14': self.step14_panel, 'step14': self.step14_panel,
} }
return panel_map.get(step_id) return panel_map.get(step_id)
@ -2591,8 +2602,8 @@ class WaterQualityGUI(QMainWindow):
('step5', self.step5_panel), ('step5', self.step5_panel),
('step6', self.step6_panel), ('step6', self.step6_panel),
('step7', self.step7_panel), ('step7', self.step7_panel),
('step10', self.step10_panel), ('step10', self.step4_panel),
('step11_ml', self.step11_ml_panel), ('step11_ml', self.step10_ml_panel),
('step14', self.step14_panel) ('step14', self.step14_panel)
] ]
@ -3219,14 +3230,14 @@ class WaterQualityGUI(QMainWindow):
def update_ui_for_training_mode(self): def update_ui_for_training_mode(self):
"""根据训练数据模式更新UI状态""" """根据训练数据模式更新UI状态"""
# 需要禁用的步骤ID对应无训练数据模式下需要禁用的步骤 # 需要禁用的步骤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 = { step_id_to_tab = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3, 'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3,
'step5': 4, 'step6': 5, 'step7': 6, 'step8_non_empirical_modeling': 7, 'step5': 4, 'step6': 5, 'step7': 6, 'step8': 7,
'step9': 8, 'step10': 9, 'step11_ml': 10, 'step11': 11, 'step9': 8, 'step8_non_empirical_modeling': 9, 'step9_concentration': 10,
'step12': 12, 'step14': 13, 'step9_viz': 14 'step10': 11, 'step11': 12, 'step14': 13, 'step9_viz': 14
} }
for step_id in disabled_step_ids: for step_id in disabled_step_ids: