From 1ad4c54b80805569575de9d4d34459b8dcaad5e7 Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 11 Jun 2026 15:14:26 +0800 Subject: [PATCH] Fix step4_panel variable name inconsistency causing AttributeError --- src/gui/core/preflight_dialog.py | 2 - src/gui/panels/step11_panel.py | 226 ------- src/gui/panels/step8_panel.py | 415 ------------- src/gui/water_quality_gui.py | 6 +- ...on_panel.py => tmp_concentration_rescue.py | 0 tmp_watercolor_rescue.py | 569 ++++++++++++++++++ 6 files changed, 572 insertions(+), 646 deletions(-) delete mode 100644 src/gui/panels/step11_panel.py delete mode 100644 src/gui/panels/step8_panel.py rename src/gui/panels/step9_concentration_panel.py => tmp_concentration_rescue.py (100%) create mode 100644 tmp_watercolor_rescue.py diff --git a/src/gui/core/preflight_dialog.py b/src/gui/core/preflight_dialog.py index 32cfc4e..54676d9 100644 --- a/src/gui/core/preflight_dialog.py +++ b/src/gui/core/preflight_dialog.py @@ -60,10 +60,8 @@ class PreflightDialog(QDialog): "step4": ("数据清洗", 3), "step5": ("特征构建", 4), "step7": ("水质指数", 5), - "step8": ("监督建模", 6), "step8_non_empirical_modeling": ("回归建模", 7), "step9": ("水色指数反演", 8), - "step9_concentration": ("浓度反演", 9), "step10": ("采样点布设", 10), "step11_ml": ("监督预测", 11), "step11": ("回归预测", 12), diff --git a/src/gui/panels/step11_panel.py b/src/gui/panels/step11_panel.py deleted file mode 100644 index 7f7ac7c..0000000 --- a/src/gui/panels/step11_panel.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Step11 面板 - 非经验模型预测 -""" - -import os -from pathlib import Path - -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QGroupBox, QFormLayout, - QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox, - QFileDialog, -) - -from src.gui.components.custom_widgets import FileSelectWidget -from src.gui.styles import ModernStylesheet - - -class Step11NonEmpiricalPanel(QWidget): - """步骤11:非经验模型预测""" - def __init__(self, parent=None): - super().__init__(parent) - self.init_ui() - - def init_ui(self): - layout = QVBoxLayout() - - # 采样光谱CSV文件选择 - self.sampling_csv_file = FileSelectWidget( - "采样光谱CSV:", - "CSV Files (*.csv);;All Files (*.*)" - ) - layout.addWidget(self.sampling_csv_file) - - # 模型目录选择 - self.models_dir_file = FileSelectWidget( - "模型目录:", - "Directories;;All Files (*.*)" - ) - self.models_dir_file.label.setText("模型目录:") - self.models_dir_file.browse_btn.clicked.disconnect() - self.models_dir_file.browse_btn.clicked.connect(self.browse_models_dir) - layout.addWidget(self.models_dir_file) - - # 参数设置 - params_group = QGroupBox("预测参数") - params_layout = QFormLayout() - - self.metric = QComboBox() - self.metric.addItems(['Average Accuracy(%)', 'Min Accuracy(%)', 'Max Accuracy(%)']) - params_layout.addRow("模型选择指标:", self.metric) - - self.prediction_column = QLineEdit() - self.prediction_column.setText("prediction") - params_layout.addRow("预测列名:", self.prediction_column) - - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - # 输出路径 - self.output_file = FileSelectWidget( - "输出文件夹:", - "Directories;;All Files (*.*)" - ) - self.output_file.label.setText("输出文件夹:") - self.output_file.browse_btn.clicked.disconnect() - self.output_file.browse_btn.clicked.connect(self.browse_output_dir) - layout.addWidget(self.output_file) - - # 启用步骤 - self.enable_checkbox = QCheckBox("启用此步骤") - 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.clicked.connect(self.run_step) - layout.addWidget(self.run_button) - - layout.addStretch() - self.setLayout(layout) - - def update_from_config(self, work_dir=None, pipeline=None): - """从全局配置自动填充采样光谱和回归模型目录 - - Args: - work_dir: 工作目录路径 - pipeline: Pipeline 实例(未使用,保留接口兼容性) - """ - try: - import traceback - - if work_dir: - self.work_dir = work_dir - elif hasattr(self, 'work_dir') and self.work_dir: - pass - else: - self.work_dir = None - - main_window = self.window() - - # 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 - if main_window and hasattr(main_window, 'step7_panel'): - step7_widget = getattr(main_window.step7_panel, 'output_file', None) - step7_output_path = "" - if hasattr(step7_widget, 'get_path'): - step7_output_path = step7_widget.get_path() or "" - elif hasattr(step7_widget, 'text'): - step7_output_path = step7_widget.text() or "" - - if step7_output_path: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step7_output_path): - step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/') - existing = self.sampling_csv_file.get_path() - if not existing or not existing.strip(): - self.sampling_csv_file.set_path(step7_output_path) - - # 2. 尝试从 Step8_Non_Empirical 界面读取回归模型目录 - if main_window and hasattr(main_window, 'step8_non_empirical_panel'): - step8_non_empirical_widget = getattr(main_window.step8_non_empirical_panel, 'output_dir', None) - step8_non_empirical_models_dir = "" - if hasattr(step8_non_empirical_widget, 'get_path'): - step8_non_empirical_models_dir = step8_non_empirical_widget.get_path() or "" - elif hasattr(step8_non_empirical_widget, 'text'): - step8_non_empirical_models_dir = step8_non_empirical_widget.text() or "" - - if step8_non_empirical_models_dir: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step8_non_empirical_models_dir): - step8_non_empirical_models_dir = os.path.join(self.work_dir or '', step8_non_empirical_models_dir).replace('\\', '/') - existing_models = self.models_dir_file.get_path() - if not existing_models or not existing_models.strip(): - self.models_dir_file.set_path(step8_non_empirical_models_dir) - - # 3. 自动填充输出路径(非经验模型预测目录) - if self.work_dir: - output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Non_Empirical_Prediction") - os.makedirs(output_dir, exist_ok=True) - existing_out = self.output_file.get_path() - if not existing_out or not existing_out.strip(): - self.output_file.set_path(output_dir) - except Exception as e: - import traceback - print(f"【{self.__class__.__name__}】自动填充失败,跳过: {e}") - traceback.print_exc() - - 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_models_dir(self): - """浏览模型目录""" - default = self._get_default_work_dir() - if default: - default = os.path.join(default, "8_Regression_Modeling") - dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default) - if dir_path: - self.models_dir_file.set_path(dir_path) - - def browse_output_dir(self): - """浏览输出目录""" - default = self._get_default_work_dir() - if default: - default = os.path.join(default, "11_12_13_predictions/Non_Empirical_Prediction") - dir_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹", default) - if dir_path: - self.output_file.set_path(dir_path) - - def get_config(self): - """获取配置""" - config = { - 'metric': self.metric.currentText(), - 'prediction_column': self.prediction_column.text(), - 'enabled': self.enable_checkbox.isChecked() - } - sampling_csv_path = self.sampling_csv_file.get_path() - if sampling_csv_path: - config['sampling_csv_path'] = sampling_csv_path - models_dir = self.models_dir_file.get_path() - if models_dir: - config['models_dir'] = models_dir - output_path = self.output_file.get_path() - if output_path: - config['output_path'] = output_path - return config - - def set_config(self, config): - """设置配置""" - if 'metric' in config: - idx = self.metric.findText(config['metric']) - if idx >= 0: - self.metric.setCurrentIndex(idx) - if 'prediction_column' in config: - self.prediction_column.setText(config['prediction_column']) - if 'sampling_csv_path' in config: - self.sampling_csv_file.set_path(config['sampling_csv_path']) - if 'models_dir' in config: - self.models_dir_file.set_path(config['models_dir']) - if 'enabled' in config: - self.enable_checkbox.setChecked(config['enabled']) - - def run_step(self): - """独立运行步骤11""" - sampling_csv_path = self.sampling_csv_file.get_path() - if not sampling_csv_path: - QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!") - return - - config = self.get_config() - - parent = self.parent() - while parent and not hasattr(parent, 'run_single_step'): - parent = parent.parent() - - if parent and hasattr(parent, 'run_single_step'): - parent.run_single_step('step11', {'step11': config}) - else: - QMessageBox.critical(self, "错误", "无法找到父级GUI对象") diff --git a/src/gui/panels/step8_panel.py b/src/gui/panels/step8_panel.py deleted file mode 100644 index e4932ba..0000000 --- a/src/gui/panels/step8_panel.py +++ /dev/null @@ -1,415 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Step7 面板 - 机器学习建模 -""" - -import os - -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, - QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox, - QPushButton, QFileDialog, QMessageBox, -) -from PyQt5.QtCore import Qt - -from src.gui.components.custom_widgets import FileSelectWidget -from src.gui.styles import ModernStylesheet - - -# ============================================================ -# 中文映射表(内部键名 -> 显示文本) -# ============================================================ - -# 预处理方法:内部键 -> 显示文本 -PREPROC_CHINESE = { - 'None': '无 (None)', - 'MMS': '最小-最大归一化 (MMS)', - 'SS': '标度化 (SS)', - 'SNV': '标准正态变换 (SNV)', - 'MA': '移动平均 (MA)', - 'SG': 'Savitzky-Golay (SG)', - 'MSC': '多元散射校正 (MSC)', - 'D1': '一阶导数 (D1)', - 'D2': '二阶导数 (D2)', - 'DT': '去趋势 (DT)', - 'CT': '中心化 (CT)', -} - -# 模型类型:内部键 -> 显示文本 -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): - """步骤8:水质参数指数计算""" - def __init__(self, parent=None): - super().__init__(parent) - self.init_ui() - - def init_ui(self): - layout = QVBoxLayout() - - # 标题 - - - # 训练数据文件(用于独立运行) - self.training_csv_file = FileSelectWidget( - "训练数据:", - "CSV Files (*.csv);;All Files (*.*)" - ) - layout.addWidget(self.training_csv_file) - - # 机器学习模型页面 - self.ml_page = QWidget() - self.create_ml_page() - layout.addWidget(self.ml_page) - - # 输出文件路径 - self.output_path = FileSelectWidget( - "输出文件:", - "CSV Files (*.csv);;All Files (*.*)", - mode="save" - ) - self.output_path.line_edit.setPlaceholderText("自动生成,或手动指定输出文件路径...") - self.output_path.browse_btn.clicked.disconnect() - self.output_path.browse_btn.clicked.connect(self.browse_output_path) - layout.addWidget(self.output_path) - - # 启用步骤 - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(False) - 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) - - def create_ml_page(self): - """创建机器学习模型页面""" - layout = QVBoxLayout() - - # 参数设置 - params_group = QGroupBox("训练参数") - params_layout = QFormLayout() - - self.feature_start = QLineEdit() - self.feature_start.setText("374.285004") - params_layout.addRow("特征起始列:", self.feature_start) - - self.cv_folds = QSpinBox() - self.cv_folds.setRange(2, 10) - self.cv_folds.setValue(3) - params_layout.addRow("交叉验证折数:", self.cv_folds) - - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - # 预处理方法 - 多选 - preproc_group = QGroupBox("预处理方法 (可多选)") - preproc_layout = QVBoxLayout() - - preproc_grid = QGridLayout() - self.preproc_checkboxes = {} - preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT'] - - 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) - - button_layout = QHBoxLayout() - select_all_btn = QPushButton("全选") - deselect_all_btn = QPushButton("全不选") - select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True)) - deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False)) - button_layout.addWidget(select_all_btn) - button_layout.addWidget(deselect_all_btn) - button_layout.addStretch() - - preproc_layout.addLayout(preproc_grid) - preproc_layout.addLayout(button_layout) - preproc_group.setLayout(preproc_layout) - layout.addWidget(preproc_group) - - # 模型选择 - 多选 - model_group = QGroupBox("模型类型 (可多选)") - model_layout = QVBoxLayout() - - model_grid = QGridLayout() - self.model_checkboxes = {} - - model_groups = [ - ("【线性模型】", ['LinearRegression', 'Ridge', 'Lasso', 'ElasticNet', 'PLS']), - ("【树模型】", ['DecisionTree', 'RF', 'ExtraTrees', 'XGBoost', 'LightGBM', 'CatBoost']), - ("【集成学习】", ['GradientBoosting', 'AdaBoost']), - ("【其他模型】", ['SVR', 'KNN', 'MLP']) - ] - - row = 0 - for group_name, models in model_groups: - group_label = QLabel(f"{group_name}") - group_label.setStyleSheet( - f"background-color: {ModernStylesheet.COLORS['hover']}; " - f"padding: 5px; border: 1px solid {ModernStylesheet.COLORS['border_light']}; " - f"border-radius: 3px;" - ) - model_grid.addWidget(group_label, row, 0, 1, 4) - row += 1 - - for i, model in enumerate(models): - checkbox = QCheckBox(MODEL_CHINESE.get(model, model)) - checkbox.setChecked(False) - self.model_checkboxes[model] = checkbox - model_grid.addWidget(checkbox, row, i % 4) - if (i + 1) % 4 == 0: - row += 1 - - row += 1 - - model_button_layout = QHBoxLayout() - model_select_all = QPushButton("全选") - model_deselect_all = QPushButton("全不选") - model_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, True)) - model_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, False)) - model_button_layout.addWidget(model_select_all) - model_button_layout.addWidget(model_deselect_all) - model_button_layout.addStretch() - - model_layout.addLayout(model_grid) - model_layout.addLayout(model_button_layout) - model_group.setLayout(model_layout) - layout.addWidget(model_group) - - # 数据划分方法 - 多选 - split_group = QGroupBox("数据划分方法 (可多选)") - split_layout = QVBoxLayout() - - split_grid = QGridLayout() - self.split_checkboxes = {} - split_methods = ['spxy', 'ks', 'random'] - - for i, method in enumerate(split_methods): - checkbox = QCheckBox(SPLIT_CHINESE.get(method, method)) - checkbox.setChecked(False) - self.split_checkboxes[method] = checkbox - split_grid.addWidget(checkbox, 0, i) - - split_button_layout = QHBoxLayout() - split_select_all = QPushButton("全选") - split_deselect_all = QPushButton("全不选") - split_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, True)) - split_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, False)) - split_button_layout.addWidget(split_select_all) - split_button_layout.addWidget(split_deselect_all) - split_button_layout.addStretch() - - split_layout.addLayout(split_grid) - split_layout.addLayout(split_button_layout) - split_group.setLayout(split_layout) - layout.addWidget(split_group) - - self.ml_page.setLayout(layout) - - def _toggle_checkboxes(self, checkboxes_dict, checked): - """统一设置checkbox状态""" - for checkbox in checkboxes_dict.values(): - checkbox.setChecked(checked) - - def _get_default_work_dir(self): - """获取 work_dir,优先用 panel 自身缓存的,否则尝试从主窗口取""" - if hasattr(self, 'work_dir') and self.work_dir: - return str(self.work_dir) - mw = self.window() - if mw and hasattr(mw, 'work_dir') and mw.work_dir: - return str(mw.work_dir) - return "" - - def browse_output_path(self): - """浏览输出文件路径(保存对话框)""" - current = self.output_path.get_path().strip() - if current: - initial_dir = os.path.dirname(current) - initial_file = os.path.basename(current) - else: - initial_dir = "" - initial_file = "" - - if not initial_dir or not os.path.isdir(initial_dir): - # 默认定位到 indices 目录 - work_dir = self._get_default_work_dir() - initial_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else "" - if initial_dir and not os.path.isdir(initial_dir): - os.makedirs(initial_dir, exist_ok=True) - - file_path, _ = QFileDialog.getSaveFileName( - self, "保存输出文件", os.path.join(initial_dir, initial_file) if initial_file else initial_dir, - "CSV Files (*.csv);;All Files (*.*)" - ) - if file_path: - self.output_path.set_path(file_path) - - def get_config(self): - """获取配置""" - preprocessing_methods = [ - method for method, checkbox in self.preproc_checkboxes.items() - if checkbox.isChecked() - ] - model_names = [ - model for model, checkbox in self.model_checkboxes.items() - if checkbox.isChecked() - ] - split_methods = [ - method for method, checkbox in self.split_checkboxes.items() - if checkbox.isChecked() - ] - - config = { - 'feature_start_column': self.feature_start.text(), - 'preprocessing_methods': preprocessing_methods if preprocessing_methods else ['None'], - 'model_names': model_names if model_names else ['SVR'], - 'split_methods': split_methods if split_methods else ['random'], - 'cv_folds': self.cv_folds.value() - } - training_csv_path = self.training_csv_file.get_path() - if training_csv_path: - config['training_csv_path'] = training_csv_path - output_path = self.output_path.get_path() - if output_path: - config['output_path'] = output_path - return config - - def set_config(self, config): - """设置配置""" - if 'feature_start_column' in config: - self.feature_start.setText(str(config['feature_start_column'])) - if 'cv_folds' in config: - self.cv_folds.setValue(config['cv_folds']) - if 'preprocessing_methods' in config: - methods = config['preprocessing_methods'] - for method, checkbox in self.preproc_checkboxes.items(): - checkbox.setChecked(method in methods) - if 'model_names' in config: - models = config['model_names'] - for model, checkbox in self.model_checkboxes.items(): - checkbox.setChecked(model in models) - if 'split_methods' in config: - methods = config['split_methods'] - for method, checkbox in self.split_checkboxes.items(): - checkbox.setChecked(method in methods) - if 'training_csv_path' in config: - self.training_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: - self.work_dir = None - - # 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: - 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 - - main_window = self.window() - if hasattr(main_window, 'run_single_step'): - config = {'step7': self.get_config()} - main_window.run_single_step('step7', config) - - def get_training_params(self): - """获取模型训练参数""" - return { - 'pipeline_type': 'machine_learning', - 'feature_start': float(self.feature_start.text()), - 'cv_folds': self.cv_folds.value(), - 'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()], - 'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()], - 'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()] - } diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py index 1faf593..35cdd85 100644 --- a/src/gui/water_quality_gui.py +++ b/src/gui/water_quality_gui.py @@ -1918,8 +1918,8 @@ class WaterQualityGUI(QMainWindow): self.step3_panel = Step3Panel() self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除") - self.step4_panel = Step4SamplingPanel() - self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "采样点布设") + self.step4_sampling_panel = Step4SamplingPanel() + self.step_stack.addTab(self.create_scroll_area(self.step4_sampling_panel), QIcon(self.get_icon_path("4.png")), "采样点布设") self.step5_clean_panel = Step5CleanPanel() self.step_stack.addTab(self.create_scroll_area(self.step5_clean_panel), QIcon(self.get_icon_path("5.png")), "数据清洗") @@ -2153,7 +2153,7 @@ class WaterQualityGUI(QMainWindow): # Step4(采样点布设)切换时自动填充输出路径 elif index == 3: - self.step4_panel.update_from_config(work_dir=self.work_dir) + self.step4_sampling_panel.update_from_config(work_dir=self.work_dir) # Step5(数据清洗)切换时自动填充数据流转路径 elif index == 4: diff --git a/src/gui/panels/step9_concentration_panel.py b/tmp_concentration_rescue.py similarity index 100% rename from src/gui/panels/step9_concentration_panel.py rename to tmp_concentration_rescue.py diff --git a/tmp_watercolor_rescue.py b/tmp_watercolor_rescue.py new file mode 100644 index 0000000..af382e6 --- /dev/null +++ b/tmp_watercolor_rescue.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Step9 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像) + +将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像, +输出各水质参数指数的 GeoTIFF 栅格图像。 +""" + +import os +import traceback +from pathlib import Path +from typing import Dict, List, Optional + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, + QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, + QFileDialog, QMessageBox, QListWidget, QListWidgetItem, + QAbstractItemView, QProgressBar, QTextEdit, QFrame, + QScrollArea, QSizePolicy, +) +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt, QThread, pyqtSignal + +from src.gui.components.custom_widgets import FileSelectWidget +from src.gui.styles import ModernStylesheet + + +class WaterIndexWorker(QThread): + """后台线程:执行水色指数反演""" + finished_ok = pyqtSignal(dict) + failed = pyqtSignal(str) + progress = pyqtSignal(str, float) # message, percent + log = pyqtSignal(str) + + def __init__( + self, + bsq_path: str, + hdr_path: str, + output_dir: str, + selected_formulas: List[str], + waterindex_csv: str, + water_mask_path: Optional[str] = None, + work_dir: Optional[str] = None, + ): + super().__init__() + self.bsq_path = bsq_path + self.hdr_path = hdr_path + self.output_dir = output_dir + self.selected_formulas = selected_formulas + self.waterindex_csv = waterindex_csv + self.water_mask_path = water_mask_path + self.work_dir = work_dir + + def run(self): + try: + from src.core.algorithms.waterindex_inversion import WaterIndexProcessor + + self.progress.emit("正在初始化水色指数处理器…", 2) + + processor = WaterIndexProcessor(self.waterindex_csv) + + self.progress.emit("正在读取影像元数据…", 5) + + # 获取影像元数据 + meta = processor.get_image_metadata(self.bsq_path, self.hdr_path) + if not meta: + self.failed.emit("无法读取影像元数据,请检查 BSQ 和 HDR 文件是否匹配") + return + + n_bands = meta.get('bands', 0) + wv_range = meta.get('wavelength_range', '未知') + self.log.emit( + f"影像信息: {meta['width']}×{meta['height']} 像素, " + f"{n_bands} 波段, {wv_range}" + ) + + if self.water_mask_path: + self.log.emit(f"使用水域掩膜: {self.water_mask_path}") + + # 使用 run_inversion 入口(含掩膜拦截链路) + results = processor.run_inversion( + deglint_img_path=self.bsq_path, + work_dir=self.work_dir or self.output_dir, + formula_csv_path=self.waterindex_csv, + selected_formulas=self.selected_formulas, + water_mask_path=self.water_mask_path, + callback=self._on_progress, + ) + + self.progress.emit(f"完成!共生成 {len(results)} 个指数图", 100) + self.finished_ok.emit(results) + + except Exception as e: + self.failed.emit(f"{e}\n{traceback.format_exc()}") + + def _on_progress(self, msg: str, pct: float): + self.progress.emit(msg, pct) + + +class Step11WaterColorPanel(QWidget): + """步骤11:水色指数反演(直接处理 BSQ 影像)""" + + def __init__(self, parent=None): + super().__init__(parent) + self._worker: Optional[WaterIndexWorker] = None + self._waterindex_csv = self._find_waterindex_csv() + self._categories: List[str] = [] + self._all_formulas: List[Dict] = [] + self._formula_list_widgets: Dict[str, QListWidgetItem] = {} + self.init_ui() + self._load_formulas() + + def init_ui(self): + layout = QVBoxLayout() + + # ---- 标题 ---- + title = QLabel("步骤11:水色指数反演(高光谱影像直接处理)") + title.setFont(QFont("Arial", 12, QFont.Bold)) + layout.addWidget(title) + + # ---- 说明 ---- + hint = QLabel( + "将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像(BSQ)," + "输出各水质参数指数的 GeoTIFF 栅格图像。" + "指数图可直接用于水质专题图生成。" + ) + hint.setWordWrap(True) + hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};") + layout.addWidget(hint) + + # ---- 输入影像选择 ---- + input_group = QGroupBox("输入影像") + input_layout = QFormLayout() + + self.bsq_file = FileSelectWidget( + "去耀斑 BSQ 影像:", + "BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" + ) + self.bsq_file.line_edit.setPlaceholderText("选择去耀斑处理后的 BSQ 影像") + self.bsq_file.browse_btn.clicked.disconnect() + self.bsq_file.browse_btn.clicked.connect(self._browse_bsq) + input_layout.addRow("BSQ 影像:", self.bsq_file) + + self.hdr_file = FileSelectWidget( + "ENVI 头文件:", + "HDR Files (*.hdr);;All Files (*.*)" + ) + self.hdr_file.line_edit.setPlaceholderText("自动关联同路径 .hdr 文件") + self.hdr_file.browse_btn.clicked.disconnect() + self.hdr_file.browse_btn.clicked.connect(self._browse_hdr) + input_layout.addRow("HDR 文件:", self.hdr_file) + + # 影像信息显示 + self.meta_label = QLabel("未加载影像") + self.meta_label.setStyleSheet( + "background: #f0f0f0; padding: 4px 8px; border-radius: 4px; " + "font-size: 12px; color: #333;" + ) + input_layout.addRow("影像信息:", self.meta_label) + + input_group.setLayout(input_layout) + layout.addWidget(input_group) + + # ---- 公式选择 ---- + formula_group = QGroupBox("公式选择") + formula_layout = QGridLayout() + + # 类别过滤 + formula_layout.addWidget(QLabel("按类别筛选:"), 0, 0) + self.category_combo = QComboBox() + self.category_combo.currentTextChanged.connect(self._on_category_changed) + formula_layout.addWidget(self.category_combo, 0, 1, 1, 2) + + # 全选/取消全选 + select_btn_layout = QHBoxLayout() + self.select_all_btn = QPushButton("全选") + self.select_all_btn.setMaximumWidth(80) + self.select_all_btn.clicked.connect(self._select_all) + select_btn_layout.addWidget(self.select_all_btn) + + self.deselect_all_btn = QPushButton("取消全选") + self.deselect_all_btn.setMaximumWidth(80) + self.deselect_all_btn.clicked.connect(self._deselect_all) + select_btn_layout.addWidget(self.deselect_all_btn) + select_btn_layout.addStretch() + formula_layout.addLayout(select_btn_layout, 0, 3) + + # 公式列表 + self.formula_list = QListWidget() + self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection) + self.formula_list.setMinimumHeight(200) + self.formula_list.itemChanged.connect(self._on_item_changed) + formula_layout.addWidget(self.formula_list, 1, 0, 1, 4) + + formula_group.setLayout(formula_layout) + layout.addWidget(formula_group) + + # ---- 输出设置 ---- + output_group = QGroupBox("输出设置") + output_layout = QFormLayout() + + self.output_dir = FileSelectWidget( + "输出目录:", + "Directories" + ) + self.output_dir.line_edit.setPlaceholderText("留空 → 工作目录/8_WaterIndex_Images") + self.output_dir.browse_btn.clicked.disconnect() + self.output_dir.browse_btn.clicked.connect(self._browse_output_dir) + output_layout.addRow("输出目录:", self.output_dir) + + self.format_combo = QComboBox() + self.format_combo.addItems(["GTiff (GeoTIFF)", "ENVI", "PCI"]) + self.format_combo.setCurrentIndex(0) + output_layout.addRow("输出格式:", self.format_combo) + + output_group.setLayout(output_layout) + layout.addWidget(output_group) + + # ---- 进度显示 ---- + self.progress_bar = QProgressBar() + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(100) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(True) + layout.addWidget(self.progress_bar) + + self.progress_label = QLabel("") + self.progress_label.setStyleSheet("font-size: 11px; color: #666;") + layout.addWidget(self.progress_label) + + # ---- 启用 & 运行 ---- + 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) + + def _find_waterindex_csv(self) -> str: + """查找 waterindex.csv 路径""" + candidates = [ + Path(__file__).parent.parent.parent / "model" / "waterindex.csv", + Path(__file__).parent.parent.parent.parent / "src" / "gui" / "model" / "waterindex.csv", + ] + for c in candidates: + if c.exists(): + return str(c) + return "" + + def _load_formulas(self): + """加载 waterindex.csv 中的公式""" + if not self._waterindex_csv or not Path(self._waterindex_csv).exists(): + self.meta_label.setText("⚠️ waterindex.csv 未找到") + return + + import csv + self._all_formulas = [] + try: + with open(self._waterindex_csv, 'r', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + self._all_formulas = list(reader) + except Exception as e: + self.meta_label.setText(f"⚠️ 加载公式失败: {e}") + return + + # 提取所有类别 + cats = set() + for f in self._all_formulas: + c = f.get('Category', '').strip() + if c: + cats.add(c) + + self._categories = sorted(cats) + self.category_combo.clear() + self.category_combo.addItem("全部") + self.category_combo.addItems(self._categories) + + self._populate_list("全部") + + def _populate_list(self, category: str): + """根据类别填充公式列表""" + self.formula_list.clear() + self._formula_list_widgets.clear() + + formulas_to_show = ( + [f for f in self._all_formulas if f.get('Category', '') == category] + if category != "全部" + else self._all_formulas + ) + + for f in formulas_to_show: + name = f.get('Formula_Name', '') + formula_str = f.get('Formula', '') + cat = f.get('Category', '') + ftype = f.get('Formula_Type', '') + + item = QListWidgetItem() + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked) + item.setData(Qt.UserRole, name) + item.setText( + f"☑ {name} [{cat}] ({ftype})\n {formula_str}" + ) + item.setToolTip(f"{name}\n{category}\n{formula_str}") + self.formula_list.addItem(item) + self._formula_list_widgets[name] = item + + def _on_category_changed(self, category: str): + self._populate_list(category) + + def _select_all(self): + for item in self.formula_list.selectedItems(): + item.setCheckState(Qt.Checked) + # 也全选当前显示的 + for i in range(self.formula_list.count()): + it = self.formula_list.item(i) + it.setCheckState(Qt.Checked) + + def _deselect_all(self): + for i in range(self.formula_list.count()): + it = self.formula_list.item(i) + it.setCheckState(Qt.Unchecked) + + def _on_item_changed(self, item: QListWidgetItem): + pass # 可扩展:实时统计选中数量 + + def _browse_bsq(self): + path, _ = QFileDialog.getOpenFileName( + self, "选择去耀斑 BSQ 影像", + "", + "BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" + ) + if path: + self.bsq_file.set_path(path) + # 自动关联同路径 hdr + hdr = Path(path).with_suffix('.hdr') + if hdr.exists(): + self.hdr_file.set_path(str(hdr)) + self._load_metadata(path, str(hdr) if hdr.exists() else "") + + def _browse_hdr(self): + path, _ = QFileDialog.getOpenFileName( + self, "选择 ENVI 头文件", + "", + "HDR Files (*.hdr);;All Files (*.*)" + ) + if path: + self.hdr_file.set_path(path) + bsq_path = self.bsq_file.get_path() + if bsq_path: + self._load_metadata(bsq_path, path) + + def _browse_output_dir(self): + d = QFileDialog.getExistingDirectory(self, "选择输出目录", "") + if d: + self.output_dir.set_path(d) + + def _load_metadata(self, bsq_path: str, hdr_path: str): + """加载并显示影像元数据""" + if not bsq_path or not Path(bsq_path).exists(): + self.meta_label.setText("⚠️ 影像文件不存在") + return + if not hdr_path or not Path(hdr_path).exists(): + self.meta_label.setText("⚠️ 头文件不存在") + return + + try: + from src.core.algorithms.waterindex_inversion import WaterIndexProcessor + processor = WaterIndexProcessor(self._waterindex_csv) + meta = processor.get_image_metadata(bsq_path, hdr_path) + if meta: + self.meta_label.setText( + f"✅ {meta['width']}×{meta['height']} | " + f"{meta['bands']} 波段 | {meta.get('wavelength_range', '未知')} | " + f"驱动: {meta['driver']}" + ) + else: + self.meta_label.setText("⚠️ 无法读取元数据") + except Exception as e: + self.meta_label.setText(f"⚠️ 元数据读取失败: {e}") + + def _get_selected_formula_names(self) -> List[str]: + names = [] + for i in range(self.formula_list.count()): + item = self.formula_list.item(i) + if item.checkState() == Qt.Checked: + name = item.data(Qt.UserRole) + if name: + names.append(name) + return names + + def _get_default_work_dir(self) -> str: + 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 get_config(self) -> dict: + bsq = self.bsq_file.get_path() + return { + 'bsq_path': bsq, + 'hdr_path': self.hdr_file.get_path(), + 'deglint_img_path': bsq, + 'output_dir': self.output_dir.get_path(), + 'output_format': self.format_combo.currentText().split()[0], + 'selected_formulas': self._get_selected_formula_names(), + } + + def set_config(self, config: dict): + if config.get('bsq_path'): + self.bsq_file.set_path(config['bsq_path']) + if config.get('hdr_path'): + self.hdr_file.set_path(config['hdr_path']) + if config.get('output_dir'): + self.output_dir.set_path(config['output_dir']) + if 'selected_formulas' in config: + names = set(config['selected_formulas']) + for i in range(self.formula_list.count()): + item = self.formula_list.item(i) + name = item.data(Qt.UserRole) + item.setCheckState(Qt.Checked if name in names else Qt.Unchecked) + + def update_from_config(self, work_dir=None, pipeline=None): + if work_dir: + self.work_dir = work_dir + elif hasattr(self, 'work_dir') and self.work_dir: + pass + else: + self.work_dir = None + + main_window = self.window() + + # 自动填入去耀斑影像 + if main_window and hasattr(main_window, 'step3_panel'): + deglint_path = main_window.step3_panel.output_file.get_path() + if deglint_path and not self.bsq_file.get_path(): + if not os.path.isabs(deglint_path): + deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/') + self.bsq_file.set_path(deglint_path) + hdr = Path(deglint_path).with_suffix('.hdr') + if hdr.exists(): + self.hdr_file.set_path(str(hdr)) + self._load_metadata(deglint_path, str(hdr)) + + # 自动填入输出目录 + if self.work_dir: + out_dir = os.path.join(self.work_dir, "8_WaterIndex_Images").replace('\\', '/') + os.makedirs(out_dir, exist_ok=True) + if not self.output_dir.get_path(): + self.output_dir.set_path(out_dir) + + def run_step(self): + bsq_path = self.bsq_file.get_path().strip() + hdr_path = self.hdr_file.get_path().strip() + output_dir = self.output_dir.get_path().strip() + + # 验证输入 + if not bsq_path: + QMessageBox.warning(self, "输入错误", "请选择去耀斑 BSQ 影像!") + return + if not Path(bsq_path).exists(): + QMessageBox.warning(self, "输入错误", f"BSQ 影像不存在:\n{bsq_path}") + return + if not hdr_path: + # 尝试自动查找 + auto_hdr = Path(bsq_path).with_suffix('.hdr') + if auto_hdr.exists(): + hdr_path = str(auto_hdr) + self.hdr_file.set_path(hdr_path) + else: + QMessageBox.warning(self, "输入错误", "请选择 ENVI 头文件!") + return + if not Path(hdr_path).exists(): + QMessageBox.warning(self, "输入错误", f"HDR 文件不存在:\n{hdr_path}") + return + if not output_dir: + work_dir = self._get_default_work_dir() + output_dir = os.path.join(work_dir, "8_WaterIndex_Images").replace('\\', '/') + os.makedirs(output_dir, exist_ok=True) + self.output_dir.set_path(output_dir) + + selected = self._get_selected_formula_names() + if not selected: + QMessageBox.warning(self, "输入错误", "请至少选择一个公式!") + return + + if self._waterindex_csv and not Path(self._waterindex_csv).exists(): + QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}") + return + + # ── 自动扫描工作目录下的水域掩膜文件 ──────────────────────────── + work_dir = self.work_dir or str(Path(bsq_path).parent) + mask_dir = os.path.join(work_dir, "1_water_mask") + water_mask_path: Optional[str] = None + if os.path.isdir(mask_dir): + # ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★ + for pattern in ("*.dat", "*.tif", "*.TIF", "*.DT"): + candidates = sorted(Path(mask_dir).glob(pattern)) + if candidates: + water_mask_path = str(candidates[0]) + break + + if water_mask_path: + print(f"[Step8] 自动找到水域掩膜: {water_mask_path}") + else: + print(f"[Step8] 未找到水域掩膜,跳过陆地剔除(陆地将保留在指数图中)") + + # 开始后台处理 + self.run_btn.setEnabled(False) + self.progress_bar.setValue(0) + self.progress_label.setText("") + + self._worker = WaterIndexWorker( + bsq_path=bsq_path, + hdr_path=hdr_path, + output_dir=output_dir, + selected_formulas=selected, + waterindex_csv=self._waterindex_csv, + water_mask_path=water_mask_path, + work_dir=work_dir, + ) + self._worker.progress.connect(self._on_progress) + self._worker.finished_ok.connect(self._on_finished) + self._worker.failed.connect(self._on_failed) + self._worker.log.connect(lambda m: self.progress_label.setText(m)) + self._worker.start() + + def _on_progress(self, msg: str, pct: float): + self.progress_bar.setValue(int(pct)) + self.progress_label.setText(msg) + + def _on_finished(self, results: Dict[str, str]): + self.run_btn.setEnabled(True) + n = len(results) + QMessageBox.information( + self, "执行成功", + f"水色指数反演完成!\n" + f"共生成 {n} 个指数图(GeoTIFF)。\n\n" + f"输出目录: {self.output_dir.get_path()}" + ) + main_window = self.window() + if main_window and hasattr(main_window, 'log_message'): + main_window.log_message(f"步骤8:水色指数反演完成,生成 {n} 个指数图", "info") + + def _on_failed(self, err: str): + self.run_btn.setEnabled(True) + self.progress_bar.setValue(0) + QMessageBox.critical(self, "执行错误", f"水色指数反演失败:\n\n{err[:500]}") + + def get_output_dir(self) -> str: + return self.output_dir.get_path().strip() or "" + + def get_output_tif_paths(self) -> List[str]: + """获取输出目录下的所有 GeoTIFF 文件路径""" + out_dir = self.get_output_dir() + if not out_dir or not os.path.isdir(out_dir): + return [] + return sorted( + str(p) for p in Path(out_dir).glob("*.tif") + if p.is_file() + ) \ No newline at end of file