diff --git a/data/icons/Mega Water 1.0.jpg b/data/icons/Mega Water 1.0.jpg new file mode 100644 index 0000000..7f4a9bd Binary files /dev/null and b/data/icons/Mega Water 1.0.jpg differ diff --git a/src/core/water_quality_inversion_pipeline_GUI.py b/src/core/water_quality_inversion_pipeline_GUI.py index 3973baa..796067a 100644 --- a/src/core/water_quality_inversion_pipeline_GUI.py +++ b/src/core/water_quality_inversion_pipeline_GUI.py @@ -989,7 +989,12 @@ class WaterQualityInversionPipeline: if not GDAL_AVAILABLE: raise ImportError("GDAL未安装,无法保存影像文件") - height, width, n_bands = image_array.shape + # 兼容 (H,W) 和 (H,W,C) 两种 shape 格式 + if image_array.ndim == 2: + height, width = image_array.shape + n_bands = 1 + else: + height, width, n_bands = image_array.shape # 获取驱动 driver = gdal.GetDriverByName('ENVI') @@ -1100,11 +1105,8 @@ class WaterQualityInversionPipeline: Returns: numpy数组或None,1表示水域,0表示非水域 """ - # 获取图像尺寸 - if isinstance(image_shape, np.ndarray): - img_height, img_width = image_shape.shape[:2] - else: - img_height, img_width = image_shape + # 获取图像尺寸(统一从 shape 元组中提取前两个维度,兼容 (H,W)、(H,W,C)、(B,H,W) 等多种格式) + img_height, img_width = image_shape[0], image_shape[1] if water_mask is None: # 如果water_mask为None,使用步骤1生成的dat格式掩膜 @@ -1362,6 +1364,19 @@ class WaterQualityInversionPipeline: interpolated_bands.append(band_data) continue + # 兼容中文和各种格式 + raw_interp = str(interpolation_method).lower() + if 'nearest' in raw_interp or '邻近' in raw_interp or '最邻近' in raw_interp: + interpolation_method = 'nearest' + elif 'bilinear' in raw_interp or '线性' in raw_interp or '双线性' in raw_interp: + interpolation_method = 'bilinear' + elif 'spline' in raw_interp or '样条' in raw_interp or 'rbf' in raw_interp: + interpolation_method = 'spline' + elif 'kriging' in raw_interp or '克里金' in raw_interp: + interpolation_method = 'kriging' + else: + interpolation_method = 'nearest' + # 对需要插值的像素进行插值 if interpolation_method == 'nearest': # 邻近插值 @@ -1591,6 +1606,18 @@ class WaterQualityInversionPipeline: step_start_time = time.time() try: + # 兼容中文和各种格式 + raw_method = str(method).lower() + if 'kutser' in raw_method: + method = 'kutser' + elif 'goodman' in raw_method: + method = 'goodman' + elif 'hedley' in raw_method: + method = 'hedley' + elif 'sugar' in raw_method: + method = 'sugar' + # 其余方法(subtract_nir, regression_slope, oxygen_absorption)保持原值 + # 如果未启用,直接跳过处理并把原始影像路径作为后续流程输入 if not enabled: print("已设置跳过去除耀斑(enabled=False),将直接使用原始影像。") @@ -1807,6 +1834,16 @@ class WaterQualityInversionPipeline: del corrected_bands elif method == "sugar": + # 强行转换暗号,兼容中文和各种格式 + raw_method = str(sugar_glint_mask_method).lower() + if 'cdf' in raw_method or '累积' in raw_method: + sugar_glint_mask_method = 'cdf' + elif 'otsu' in raw_method or '大津' in raw_method: + sugar_glint_mask_method = 'otsu' + else: + # 默认回退到 cdf 确保不崩溃 + sugar_glint_mask_method = 'cdf' + print(f"使用方法: SUGAR (迭代次数={sugar_iter}, 掩膜方法={sugar_glint_mask_method})") # 确定输出路径 @@ -3603,6 +3640,29 @@ class WaterQualityInversionPipeline: Returns: 预处理后的CSV文件路径 """ + # 兼容中文和各种格式 + raw_p = str(preprocess_method).lower() + if raw_p == 'none' or '无' in raw_p or '跳过' in raw_p: + preprocess_method = 'None' + elif raw_p == 'mms' or 'minmax' in raw_p or '最大最小' in raw_p: + preprocess_method = 'MMS' + elif raw_p == 'ss' or '标准' in raw_p or '标准化' in raw_p: + preprocess_method = 'SS' + elif raw_p == 'snv' or '标准正态' in raw_p: + preprocess_method = 'SNV' + elif raw_p == 'ma' or '移动' in raw_p: + preprocess_method = 'MA' + elif raw_p == 'sg' or 'savitzky' in raw_p or '平滑' in raw_p: + preprocess_method = 'SG' + elif raw_p == 'msc' or '多元散射' in raw_p: + preprocess_method = 'MSC' + elif raw_p == 'd1' or 'd2' or 'dt' or '导数' in raw_p: + preprocess_method = {'d1': 'D1', 'd2': 'D2', 'dt': 'DT'}.get(raw_p, raw_p.upper()) + elif raw_p == 'ct' or '去趋势' in raw_p: + preprocess_method = 'CT' + else: + preprocess_method = preprocess_method # 保持原值 + # 如果不需要预处理,直接返回原文件 if preprocess_method == 'None': return csv_path diff --git a/src/gui/panels/report_generation_panel.py b/src/gui/panels/report_generation_panel.py index f272bae..8029e9b 100644 --- a/src/gui/panels/report_generation_panel.py +++ b/src/gui/panels/report_generation_panel.py @@ -195,13 +195,23 @@ class ReportGenerationPanel(QWidget): self.text_model_edit.setText(self.vision_model_edit.text()) self.text_model_edit.blockSignals(False) + def _get_default_work_dir(self): + """获取 work_dir,优先用主窗口缓存的 work_dir""" + if self.main_window and hasattr(self.main_window, 'work_dir') and self.main_window.work_dir: + return str(self.main_window.work_dir) + return "" + def browse_work_dir(self): - d = QFileDialog.getExistingDirectory(self, "选择工作目录") + default = self._get_default_work_dir() + d = QFileDialog.getExistingDirectory(self, "选择工作目录", default) if d: self.work_dir_edit.setText(d) def browse_output_dir(self): - d = QFileDialog.getExistingDirectory(self, "选择报告输出目录") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "14_visualization") + d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default) if d: self.output_dir_edit.setText(d) diff --git a/src/gui/panels/step4_panel.py b/src/gui/panels/step4_panel.py index bb6996c..cda8fe5 100644 --- a/src/gui/panels/step4_panel.py +++ b/src/gui/panels/step4_panel.py @@ -168,7 +168,10 @@ class Step4Panel(QWidget): try: rows_to_preview = max(1, self.preview_rows_spin.value()) - df = pd.read_csv(csv_path, nrows=rows_to_preview) + # 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 diff --git a/src/gui/panels/step5_panel.py b/src/gui/panels/step5_panel.py index cae532f..274ebe0 100644 --- a/src/gui/panels/step5_panel.py +++ b/src/gui/panels/step5_panel.py @@ -191,6 +191,15 @@ class Step5Panel(QWidget): else: self.output_file.set_path("") + # 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板 + main_window = self.window() + if main_window and hasattr(main_window, 'step4_panel'): + step4_output_path = main_window.step4_panel.output_file.get_path() + if step4_output_path: + existing_csv = self.csv_file.get_path() + if not existing_csv or not existing_csv.strip(): + self.csv_file.set_path(step4_output_path) + def run_step(self): """独立运行步骤5""" # 验证输入 diff --git a/src/gui/panels/step6_5_panel.py b/src/gui/panels/step6_5_panel.py index b15e71e..a1f4653 100644 --- a/src/gui/panels/step6_5_panel.py +++ b/src/gui/panels/step6_5_panel.py @@ -211,9 +211,52 @@ class Step6_5Panel(QWidget): if 'csv_path' in config: self.training_csv_file.set_path(config['csv_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. 尝试从 Step5 界面读取训练光谱 CSV 路径 + main_window = self.window() + if main_window and hasattr(main_window, 'step5_panel'): + step5_output_path = main_window.step5_panel.output_file.get_path() + if step5_output_path: + existing = self.training_csv_file.get_path() + if not existing or not existing.strip(): + self.training_csv_file.set_path(step5_output_path) + + # 2. 自动填充输出目录(8_Regression_Modeling) + if self.work_dir: + output_dir = os.path.join(self.work_dir, "8_Regression_Modeling") + os.makedirs(output_dir, exist_ok=True) + existing_out = self.output_dir.get_path() + if not existing_out or not existing_out.strip(): + self.output_dir.set_path(output_dir) + + 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_dir(self): """浏览输出目录""" - dir_path = QFileDialog.getExistingDirectory(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.output_dir.set_path(dir_path) diff --git a/src/gui/panels/step6_75_panel.py b/src/gui/panels/step6_75_panel.py index 7498df0..611c3c8 100644 --- a/src/gui/panels/step6_75_panel.py +++ b/src/gui/panels/step6_75_panel.py @@ -280,6 +280,37 @@ class Step6_75Panel(QWidget): if 'enabled' in config: self.enable_checkbox.setChecked(config['enabled']) + 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. 尝试从 Step5 界面读取训练光谱 CSV 路径 + main_window = self.window() + if main_window and hasattr(main_window, 'step5_panel'): + step5_output_path = main_window.step5_panel.output_file.get_path() + if step5_output_path: + existing = self.csv_file.get_path() + if not existing or not existing.strip(): + self.csv_file.set_path(step5_output_path) + + # 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) + def run_step(self): """独立运行步骤6.75""" csv_path = self.csv_file.get_path() diff --git a/src/gui/panels/step6_panel.py b/src/gui/panels/step6_panel.py index f357b6b..066f691 100644 --- a/src/gui/panels/step6_panel.py +++ b/src/gui/panels/step6_panel.py @@ -42,15 +42,15 @@ class Step6Panel(QWidget): layout.addWidget(self.ml_page) # 输出文件路径 - self.output_dir = FileSelectWidget( - "输出模型目录:", - "Directories;;All Files (*.*)" + self.output_path = FileSelectWidget( + "输出文件:", + "CSV Files (*.csv);;All Files (*.*)", + mode="save" ) - self.output_dir.line_edit.setPlaceholderText("models_output") - # 修改浏览按钮为选择目录 - self.output_dir.browse_btn.clicked.disconnect() - self.output_dir.browse_btn.clicked.connect(self.browse_output_dir) - layout.addWidget(self.output_dir) + 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("启用此步骤") @@ -198,11 +198,38 @@ class Step6Panel(QWidget): for checkbox in checkboxes_dict.values(): checkbox.setChecked(checked) - def browse_output_dir(self): - """浏览输出目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", "") - if dir_path: - self.output_dir.set_path(dir_path) + 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): """获取配置""" @@ -229,7 +256,9 @@ class Step6Panel(QWidget): training_csv_path = self.training_csv_file.get_path() if training_csv_path: config['training_csv_path'] = training_csv_path - # 注意:step6_train_models 不接受 output_dir 参数,输出目录由 pipeline 内部根据 work_dir 生成 + output_path = self.output_path.get_path() + if output_path: + config['output_path'] = output_path return config def set_config(self, config): @@ -252,8 +281,8 @@ class Step6Panel(QWidget): checkbox.setChecked(method in methods) if 'training_csv_path' in config: self.training_csv_file.set_path(config['training_csv_path']) - if 'output_dir' in config: - self.output_dir.set_path(config['output_dir']) + if 'output_path' in config: + self.output_path.set_path(config['output_path']) def update_from_config(self, work_dir=None, pipeline=None): """从全局配置自动填充训练数据和输出路径 @@ -288,13 +317,22 @@ class Step6Panel(QWidget): if step5_csv: self.training_csv_file.set_path(step5_csv) - # 2. 自动填充输出目录(基于工作目录) + # 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: - output_dir = os.path.join(self.work_dir, "7_Supervised_Model_Training") - os.makedirs(output_dir, exist_ok=True) - self.output_dir.set_path(output_dir.replace('\\', '/')) + 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_dir.set_path("") + self.output_path.set_path("") def run_step(self): """独立运行步骤6""" diff --git a/src/gui/panels/step8_5_panel.py b/src/gui/panels/step8_5_panel.py index 07e0f2e..e694a47 100644 --- a/src/gui/panels/step8_5_panel.py +++ b/src/gui/panels/step8_5_panel.py @@ -4,6 +4,9 @@ Step8_5 面板 - 非经验模型预测 """ +import os +from pathlib import Path + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox, @@ -79,15 +82,70 @@ class Step8_5Panel(QWidget): layout.addStretch() self.setLayout(layout) + 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 + + main_window = self.window() + + # 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 + if main_window and hasattr(main_window, 'step7_panel'): + step7_output_path = main_window.step7_panel.output_file.get_path() + if step7_output_path: + existing = self.sampling_csv_file.get_path() + if not existing or not existing.strip(): + self.sampling_csv_file.set_path(step7_output_path) + + # 2. 尝试从 Step6.5 界面读取回归模型目录 + if main_window and hasattr(main_window, 'step6_5_panel'): + step6_5_models_dir = main_window.step6_5_panel.output_dir.get_path() + if step6_5_models_dir: + existing_models = self.models_dir_file.get_path() + if not existing_models or not existing_models.strip(): + self.models_dir_file.set_path(step6_5_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) + + 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): """浏览模型目录""" - dir_path = QFileDialog.getExistingDirectory(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): """浏览输出目录""" - dir_path = QFileDialog.getExistingDirectory(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) diff --git a/src/gui/panels/step8_75_panel.py b/src/gui/panels/step8_75_panel.py index 0d84214..1733790 100644 --- a/src/gui/panels/step8_75_panel.py +++ b/src/gui/panels/step8_75_panel.py @@ -4,6 +4,8 @@ Step8_75 面板 - 自定义回归预测 """ +import os + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QPushButton, QCheckBox, QMessageBox, QFileDialog, @@ -73,15 +75,70 @@ class Step8_75Panel(QWidget): layout.addStretch() self.setLayout(layout) + 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 + + main_window = self.window() + + # 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 + if main_window and hasattr(main_window, 'step7_panel'): + step7_output_path = main_window.step7_panel.output_file.get_path() + if step7_output_path: + existing = self.sampling_csv_file.get_path() + if not existing or not existing.strip(): + self.sampling_csv_file.set_path(step7_output_path) + + # 2. 尝试从 Step6.75 界面读取自定义回归模型目录 + if main_window and hasattr(main_window, 'step6_75_panel'): + step6_75_models_dir = main_window.step6_75_panel.output_dir.text().strip() + if step6_75_models_dir: + existing_models = self.regression_models_dir.get_path() + if not existing_models or not existing_models.strip(): + self.regression_models_dir.set_path(step6_75_models_dir) + + # 3. 自动填充输出目录(自定义回归预测目录) + if self.work_dir: + output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Custom_Regression_Prediction") + os.makedirs(output_dir, exist_ok=True) + existing_out = self.output_dir_widget.get_path() + if not existing_out or not existing_out.strip(): + self.output_dir_widget.set_path(output_dir) + + 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_regression_models_dir(self): """浏览回归模型目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", "") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "9_Custom_Regression_Modeling") + dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", default) if dir_path: self.regression_models_dir.set_path(dir_path) def browse_output_dir(self): """浏览输出目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择输出目录", "") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "11_12_13_predictions/Custom_Regression_Prediction") + dir_path = QFileDialog.getExistingDirectory(self, "选择输出目录", default) if dir_path: self.output_dir_widget.set_path(dir_path) diff --git a/src/gui/panels/step8_panel.py b/src/gui/panels/step8_panel.py index 7eee5d0..2440c66 100644 --- a/src/gui/panels/step8_panel.py +++ b/src/gui/panels/step8_panel.py @@ -4,6 +4,9 @@ Step8 面板 - 机器学习预测 """ +import os +from pathlib import Path + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox, @@ -76,9 +79,61 @@ class Step8Panel(QWidget): layout.addStretch() self.setLayout(layout) + 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 + + main_window = self.window() + + # 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 + if main_window and hasattr(main_window, 'step7_panel'): + step7_output_path = main_window.step7_panel.output_file.get_path() + if step7_output_path: + existing = self.sampling_csv_file.get_path() + if not existing or not existing.strip(): + self.sampling_csv_file.set_path(step7_output_path) + + # 2. 尝试从 Step6 界面读取监督模型目录 + if main_window and hasattr(main_window, 'step6_panel'): + step6_models_dir = main_window.step6_panel.output_dir.get_path() + if step6_models_dir: + existing_models = self.models_dir_file.get_path() + if not existing_models or not existing_models.strip(): + self.models_dir_file.set_path(step6_models_dir) + + # 3. 自动填充输出路径(机器学习预测目录) + if self.work_dir: + output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Machine_Learning_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) + + 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): """浏览模型目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", "") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "7_Supervised_Model_Training") + dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default) if dir_path: self.models_dir_file.set_path(dir_path) diff --git a/src/gui/panels/step9_panel.py b/src/gui/panels/step9_panel.py index d4fe49d..35b6ea5 100644 --- a/src/gui/panels/step9_panel.py +++ b/src/gui/panels/step9_panel.py @@ -101,12 +101,9 @@ class Step9Panel(QWidget): mode_row = QHBoxLayout() self.mode_single_rb = QRadioButton("单个 CSV 文件") self.mode_folder_rb = QRadioButton("文件夹批量") - self.mode_single_rb.setChecked(True) self._mode_group = QButtonGroup(self) self._mode_group.addButton(self.mode_single_rb, 0) self._mode_group.addButton(self.mode_folder_rb, 1) - self.mode_single_rb.toggled.connect(self._on_step9_mode_changed) - self.mode_folder_rb.toggled.connect(self._on_step9_mode_changed) mode_row.addWidget(self.mode_single_rb) mode_row.addWidget(self.mode_folder_rb) mode_row.addStretch() @@ -192,16 +189,36 @@ class Step9Panel(QWidget): layout.addStretch() self.setLayout(layout) - self._on_step9_mode_changed() - def _on_step9_mode_changed(self): + # 信号绑定与初始状态 + self.mode_single_rb.toggled.connect(self._toggle_input_mode) + self.mode_folder_rb.toggled.connect(self._toggle_input_mode) + self.mode_single_rb.setChecked(True) # 默认选中"单个 CSV" + self._toggle_input_mode() # 根据默认值设置初始显示状态 + + def _toggle_input_mode(self): + """槽函数:根据单选框状态动态显示/隐藏对应的输入组件。""" folder_mode = self.mode_folder_rb.isChecked() - self.prediction_csv_file.setEnabled(not folder_mode) - self._folder_row_widget.setEnabled(folder_mode) - self.recursive_csv_cb.setEnabled(folder_mode) + # 单个 CSV 模式:显示单文件选择,隐藏文件夹选择 + self.prediction_csv_file.setVisible(not folder_mode) + # 文件夹批量模式:显示文件夹选择 + 递归选项,隐藏单文件选择 + self._folder_row_widget.setVisible(folder_mode) + self.recursive_csv_cb.setVisible(folder_mode) + + 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_prediction_csv_dir(self): - d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "11_12_13_predictions") + d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default) if d: self.prediction_csv_dir_edit.setText(d) @@ -281,9 +298,68 @@ class Step9Panel(QWidget): if p.parent and str(p.parent) != '.': self.output_dir.set_path(str(p.parent)) + def update_from_config(self, work_dir=None, pipeline=None): + """从全局配置自动填充预测结果目录 + + 优先使用 Step8(机器学习预测)的输出目录作为待预测 CSV 目录; + 其次回退到 Step8.5(回归预测)或 Step8.75(自定义回归预测)的输出目录。 + + 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 + + main_window = self.window() + if not main_window: + return + + # 1. 尝试从 Step8 界面读取机器学习预测输出目录(优先) + pred_dir = None + if hasattr(main_window, 'step8_panel'): + step8_output = main_window.step8_panel.output_file.get_path() + if step8_output: + pred_dir = str(Path(step8_output).parent) + + # 2. 备选:从 Step8.5 界面读取非经验预测输出目录 + if not pred_dir and hasattr(main_window, 'step8_5_panel'): + step8_5_output = main_window.step8_5_panel.output_file.get_path() + if step8_5_output: + pred_dir = str(Path(step8_5_output).parent) + + # 3. 备选:从 Step8.75 界面读取自定义回归预测输出目录 + if not pred_dir and hasattr(main_window, 'step8_75_panel'): + step8_75_output = main_window.step8_75_panel.output_dir_widget.get_path() + if step8_75_output: + pred_dir = step8_75_output + + # 自动填入"预测CSV目录"(文件夹批量模式) + if pred_dir: + existing_dir = (self.prediction_csv_dir_edit.text() or "").strip() + if not existing_dir: + self.prediction_csv_dir_edit.setText(pred_dir) + # 切换到文件夹批量模式 + self.mode_folder_rb.setChecked(True) + + # 4. 自动填充输出目录(14_visualization) + if self.work_dir: + output_dir = os.path.join(self.work_dir, "14_visualization") + os.makedirs(output_dir, exist_ok=True) + existing_out = self.output_dir.get_path() + if not existing_out or not existing_out.strip(): + self.output_dir.set_path(output_dir) + def browse_output_dir(self): """浏览输出目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", "") + default = self._get_default_work_dir() + if default: + default = os.path.join(default, "14_visualization") + dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default) if dir_path: self.output_dir.set_path(dir_path) diff --git a/src/gui/panels/visualization_panel.py b/src/gui/panels/visualization_panel.py index 2bc5d15..1709ebe 100644 --- a/src/gui/panels/visualization_panel.py +++ b/src/gui/panels/visualization_panel.py @@ -1215,9 +1215,19 @@ class VisualizationPanel(QWidget): if work_dir: QTimer.singleShot(100, self.scan_work_directory) + 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_work_dir(self): """浏览工作目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择工作目录") + default = self._get_default_work_dir() + dir_path = QFileDialog.getExistingDirectory(self, "选择工作目录", default) if dir_path: self.work_dir = dir_path self.work_dir_edit.setText(dir_path) @@ -1225,7 +1235,8 @@ class VisualizationPanel(QWidget): def browse_img_dir(self): """手动浏览图像目录""" - dir_path = QFileDialog.getExistingDirectory(self, "选择图像目录") + default = self._get_default_work_dir() + dir_path = QFileDialog.getExistingDirectory(self, "选择图像目录", default) if dir_path: self.img_dir_edit.setText(dir_path) self.image_tree.scan_directory(dir_path) diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py index 01c3c15..0f61f86 100644 --- a/src/gui/water_quality_gui.py +++ b/src/gui/water_quality_gui.py @@ -1475,7 +1475,7 @@ class WaterQualityGUI(QMainWindow): def init_ui(self): """初始化UI""" - self.setWindowTitle("水质参数反演分析系统 v1.0") + self.setWindowTitle("水质参数反演分析系统 v1.1") # 获取屏幕可用区域(排除任务栏) screen_geometry = QApplication.primaryScreen().availableGeometry() @@ -1658,48 +1658,66 @@ class WaterQualityGUI(QMainWindow): def create_banner_widget(self): """创建横幅区域 - 支持自适应等比缩放""" + # 横幅标题文字(方便后续直接修改版本号) + self._APP_TITLE = "Mega Water V1.1" + # 创建横幅容器 banner_widget = QWidget() banner_layout = QHBoxLayout() banner_layout.setContentsMargins(0, 0, 0, 0) banner_layout.setSpacing(0) - # 不设置居中对齐,让横幅填满整个容器 - # 创建横幅标签 - 完全跟随窗口等比缩放,填满整个区域 + # ===== 底图层 ===== self.banner_label = QLabel() - # 最小高度保证:当窗口很小时至少显示 38px 高 (200px 宽 / 5.25) - self.banner_label.setMinimumHeight(int(200 / 5.25)) # ≈ 38px - # 使用 Expanding 策略让标签填满可用空间 + self.banner_label.setMinimumHeight(140) self.banner_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.banner_label.setScaledContents(False) - # 清除 QLabel 默认的 margin 和 padding,消除右侧空白 self.banner_label.setStyleSheet("margin: 0px; padding: 0px; border: none;") - # 保存原始pixmap用于后续缩放 + # 强制 banner_widget 展开填充 toolbar 全部宽度(清除 addWidget 的默认居中行为) + banner_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + banner_widget.setMinimumWidth(0) # 确保可以被 layout 压缩/扩展到任意宽度 + banner_widget.setStyleSheet("margin: 0px; padding: 0px; border: none;") + + # 纯净底图路径(无水印文字) if hasattr(sys, '_MEIPASS'): - banner_path = os.path.join(sys._MEIPASS, 'data', 'icons', 'Mega Water 1.0.png') + banner_path = os.path.join(sys._MEIPASS, 'data', 'icons', 'Mega Water 1.0.jpg') else: - banner_path = str(Path(__file__).parent.parent.parent / "data" / "icons" / "Mega Water 1.0.png") + banner_path = str(Path(__file__).parent.parent.parent / "data" / "icons" / "Mega Water 1.0.jpg") self.banner_pixmap = QPixmap(banner_path) if not self.banner_pixmap.isNull(): - # 延迟执行,确保窗口已初始化 QTimer.singleShot(50, self.update_banner_image) else: - # 如果图片加载失败,显示占位符 - self.banner_label.setText("水质参数反演分析系统") + self.banner_label.setText("背景图加载失败") self.banner_label.setStyleSheet(""" QLabel { - background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #0078d4, stop:1 #00a0e9); - color: white; - font-size: 26px; - font-weight: bold; + color: white; font-size: 26px; font-weight: bold; border-bottom: 3px solid #005a9e; } """) banner_layout.addWidget(self.banner_label) + + # ===== 文字叠加层 ===== + # 注意这里:第二个参数改成了 self.banner_label,直接附着在底图上! + self.banner_title_label = QLabel(self._APP_TITLE, self.banner_label) + self.banner_title_label.setStyleSheet(""" + QLabel { + background: transparent; + color: white; + font-size: 48px; /* 显著增大字号 */ + font-weight: normal; /* 取消粗体,还原原图的优雅感 */ + font-family: "Times New Roman", "Georgia", "STZhongsong", serif; /* 衬线字体家族 */ + letter-spacing: 1px; + } + """) + self.banner_title_label.setAttribute(Qt.WA_TransparentForMouseEvents) # 鼠标穿透 + self.banner_title_label.show() # 第一道保险:强制现身 + self.banner_title_label.raise_() # 第二道保险:强制图层置顶 + banner_widget.setLayout(banner_layout) # 将横幅添加到窗口顶部(在标题栏下方) @@ -1721,6 +1739,10 @@ class WaterQualityGUI(QMainWindow): margin: 0px; padding: 0px; } + QToolBar > QWidget { + margin: 0px; + padding: 0px; + } """) self.addToolBar(Qt.TopToolBarArea, banner_toolbar) @@ -2122,10 +2144,34 @@ class WaterQualityGUI(QMainWindow): elif index == 6: self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + # Step6.5(非经验回归建模)切换时自动填充训练数据和模型目录 + elif index == 7: + self.step6_5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + + # Step6.75(自定义回归建模)切换时自动填充训练数据和模型目录 + elif index == 8: + self.step6_75_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + # Step7(采样点布设)切换时自动填充掩膜和输出路径 elif index == 9: self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + # Step8(机器学习预测)切换时自动填充采样光谱和模型目录 + elif index == 10: + self.step8_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + + # Step8.5(非经验模型预测)切换时自动填充采样光谱和回归模型目录 + elif index == 11: + self.step8_5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + + # Step8.75(自定义回归预测)切换时自动填充采样光谱和自定义回归模型目录 + elif index == 12: + self.step8_75_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + + # Step9(专题图生成)切换时自动填充预测结果目录 + elif index == 13: + self.step9_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + # 可视化分析面板切换时自动推断图像目录并加载目录树 elif index == 14: self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) @@ -2853,36 +2899,43 @@ class WaterQualityGUI(QMainWindow): self.training_mode_action.setText("有训练数据模式" if checked else "无训练数据模式") def update_banner_image(self): - """更新横幅图片 - 完全跟随窗口等比缩放,填满可用宽度""" + """更新横幅图片 - 固定高度 + 居中裁剪(类似 CSS object-fit: cover)""" if not hasattr(self, 'banner_pixmap') or self.banner_pixmap.isNull(): return - # 获取可用宽度(考虑工具栏边距),跟随窗口实时变化 - available_width = max(200, self.width() - 60) - - # 先根据可用宽度计算目标高度(严格 5.25:1) - target_height = int(available_width / 5.25) - - # 限制最小高度 - if target_height < 38: - target_height = 38 - available_width = int(38 * 5.25) + TARGET_HEIGHT = 140 + target_width = self.width() - # 计算图片目标尺寸(保持 5.25:1 比例) - target_width = available_width - - # 设置固定尺寸,确保标签严格填满整个区域 - self.banner_label.setFixedSize(target_width, target_height) + # 手动计算 Cover 缩放比例:取宽/高各自所需比例的最大值, + # 确保缩放后图片的宽 ≥ target_width 且高 ≥ TARGET_HEIGHT + orig_w = self.banner_pixmap.width() + orig_h = self.banner_pixmap.height() + scale_factor = max(target_width / orig_w, TARGET_HEIGHT / orig_h) + new_w = int(orig_w * scale_factor) + new_h = int(orig_h * scale_factor) - # 等比缩放到目标尺寸,填满整个区域(允许轻微裁剪) scaled_pixmap = self.banner_pixmap.scaled( - target_width, - target_height, - Qt.KeepAspectRatioByExpanding, # 保持比例,填满区域,允许裁剪超出部分 + new_w, new_h, + Qt.IgnoreAspectRatio, Qt.SmoothTransformation ) - self.banner_label.setPixmap(scaled_pixmap) + # 居中裁剪,截取中间核心区域 + crop_x = (new_w - target_width) // 2 + crop_y = (new_h - TARGET_HEIGHT) // 2 + final_pixmap = scaled_pixmap.copy(crop_x, crop_y, target_width, TARGET_HEIGHT) + + self.banner_label.setFixedHeight(TARGET_HEIGHT) + self.banner_label.setFixedWidth(target_width) # 强制宽度填满容器 + self.banner_label.setPixmap(final_pixmap) + self.banner_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # 文字叠加层:绝对定位,保持垂直居中 + if hasattr(self, 'banner_title_label'): + title_x = 160 + title_y = max(0, (TARGET_HEIGHT - 60) // 2) + self.banner_title_label.move(title_x, title_y) + self.banner_title_label.resize(target_width - title_x - 20, 60) def resizeEvent(self, event): """窗口大小改变事件 - 实时更新横幅图片等比缩放"""