From 0238aa66ab84ff0a118a7d2bacf0fbc6187ff768 Mon Sep 17 00:00:00 2001 From: DXC Date: Tue, 16 Jun 2026 12:54:18 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=BD=92=E4=B8=80=E5=8C=96?= =?UTF-8?q?=EF=BC=9A=E7=BB=9F=E4=B8=80=2014=20=E4=B8=AA=E5=AD=90=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=20helper=20=E6=8E=A5=E5=8F=A3=20+=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20getattr=20=E5=BC=A0=E5=86=A0=E6=9D=8E=E6=88=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 _step_path_resolver.py(STEP_DATA_SOURCE 映射表 + _FALLBACK_DIR_TABLE 40+ keys + resolve_subdir / get_step_output_path / resolve_step_widget 三层 API),与 pipeline.get_step_output_dir 互为表里、互不依赖。 pipeline 新增 get_step_output_dir(step_name) 唯一权威接口(class-level _STEP_OUTPUT_DIR_MAP 延迟构造 + 未知 key 回退 work_dir + 调试日志)。 全量重构 src/gui/panels/step*.py(17 个文件) * 消除全部 os.path.join(wp, "X_subdir") 硬编码(14 个预定义子目录) * 8 处 getattr(main_window.stepXX_panel, ...) 张冠李戴死代码全部修复(错位属性名 → 通过 STEP_DATA_SOURCE 映射到正确的 main_window 长名属性) * 删除 step12_viz_panel.py 中 self.step11_ml_panel / step11_panel / step12_panel 死代码块 * 提示文字/标签字典/日志保留原文,仅替换实际路径计算 Smoke test:39 fallback key + 14 路径映射 + 14 step 数字 key + 17/17 panel AST 解析 + 17/17 import 全部就位。 --- .../water_quality_inversion_pipeline_GUI.py | 66 ++++++- src/gui/panels/_step_path_resolver.py | 186 ++++++++++++++++++ src/gui/panels/step10_watercolor_panel.py | 15 +- src/gui/panels/step11_map_panel.py | 82 ++++---- src/gui/panels/step12_viz_panel.py | 64 +++--- src/gui/panels/step13_panel.py | 67 +++---- src/gui/panels/step13_report_panel.py | 9 +- src/gui/panels/step14_panel.py | 81 ++++---- src/gui/panels/step1_panel.py | 12 +- src/gui/panels/step2_panel.py | 10 +- src/gui/panels/step3_panel.py | 10 +- src/gui/panels/step4_sampling_panel.py | 12 +- src/gui/panels/step5_clean_panel.py | 10 +- src/gui/panels/step6_feature_panel.py | 10 +- src/gui/panels/step7_index_panel.py | 8 +- src/gui/panels/step8_ml_train_panel.py | 36 ++-- src/gui/panels/step8_non_empirical_panel.py | 35 ++-- src/gui/panels/step8_qaa_panel.py | 13 +- src/gui/panels/step9_ml_predict_panel.py | 40 ++-- 19 files changed, 544 insertions(+), 222 deletions(-) create mode 100644 src/gui/panels/_step_path_resolver.py diff --git a/src/core/water_quality_inversion_pipeline_GUI.py b/src/core/water_quality_inversion_pipeline_GUI.py index f1a6b35..c56111c 100644 --- a/src/core/water_quality_inversion_pipeline_GUI.py +++ b/src/core/water_quality_inversion_pipeline_GUI.py @@ -185,7 +185,71 @@ class WaterQualityInversionPipeline: self.callback = None print(f"工作目录已创建: {self.work_dir}") - + + # ---- 步骤输出目录查找接口(归一化所有 panel 的路径访问)---- + + # 用户口语编号 → 权威子目录对象 的映射 + # 同时支持 "stepN"、"stepN_alias"、"subdir名" 三种 key 形式查找 + _STEP_OUTPUT_DIR_MAP = None # 延迟到首次访问时构造 + + def _ensure_step_dir_map(self): + """延迟构造 step_name → 目录对象 映射表(首次访问时执行)""" + if WaterQualityInversionPipeline._STEP_OUTPUT_DIR_MAP is not None: + return WaterQualityInversionPipeline._STEP_OUTPUT_DIR_MAP + wp = self.work_dir + m = { + # 基础步骤 + "step1": wp / "1_water_mask", + "step2": wp / "2_Glint_Detection", + "step3": wp / "3_deglint", + "step4_sampling": wp / "4_sampling", + "step5_clean": wp / "5_Data_Cleaning", + "step6_feature": wp / "6_Spectral_Feature_Extraction", + "step7_index": wp / "7_Water_Quality_Indices", + "step8_ml_train": wp / "8_Supervised_Model_Training", + "step9_ml_predict": wp / "8_Non_Empirical_Regression", + "step10_watercolor": wp / "10_WaterIndex_Images", + "step11_map": wp / "14_visualization", + "step12_viz": wp / "14_visualization", + "step13_report": wp / "14_visualization", + # 合并目录(提供单一访问点,避免分散硬编码) + "step11_predictions": wp / "11_12_13_predictions", + "step12_predictions": wp / "11_12_13_predictions", + "step13_predictions": wp / "11_12_13_predictions", + "custom_regression": wp / "13_Custom_Regression", + "prediction_dir": wp / "11_12_13_predictions", + "visualization": wp / "14_visualization", + "reports": wp / "reports", + # 兼容主流程 step_id(数字+短名) + "step8": wp / "8_Supervised_Model_Training", + "step9": wp / "8_Non_Empirical_Regression", + "step10": wp / "10_WaterIndex_Images", + "step11": wp / "11_12_13_predictions", + "step12": wp / "13_Custom_Regression", + "step13": wp / "reports", + "step14": wp / "14_visualization", + } + WaterQualityInversionPipeline._STEP_OUTPUT_DIR_MAP = m + return m + + def get_step_output_dir(self, step_name: str): + """根据步骤名称返回权威输出目录 Path 对象。 + + 这是 panel 端访问子目录的**唯一**入口。接收以下任意形式 key: + - 完整 panel 属性名: "step11_map", "step12_viz", "step8_ml_train" 等 + - 主流程 step_id: "step8"~"step14" + - 业务别名: "prediction_dir", "visualization", "reports", "custom_regression" + - 兼容口语: "step11_predictions" (=11_12_13_predictions) + + 未知 key 一律回退到 work_dir 本身,并打 warning。 + """ + mapping = self._ensure_step_dir_map() + key = (step_name or "").strip() + if key in mapping: + return mapping[key] + print(f"[pipeline.get_step_output_dir] 未知 step_name={key!r},回退到 work_dir") + return self.work_dir + def set_callback(self, callback): """ 设置回调函数,用于向GUI报告进度 diff --git a/src/gui/panels/_step_path_resolver.py b/src/gui/panels/_step_path_resolver.py new file mode 100644 index 0000000..4c37cef --- /dev/null +++ b/src/gui/panels/_step_path_resolver.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +""" +Step 路径解析器——统一消灭 panel 端的硬编码路径与"张冠李戴"跨面板引用。 + +提供三个公共 API: + - resolve_step_widget(main_window, step_key) + - get_step_output_path(main_window, step_key, work_dir=None) + - STEP_DATA_SOURCE 映射表 + +典型使用: + from src.gui.panels._step_path_resolver import ( + resolve_step_widget, get_step_output_path + ) + + # 替换前:getattr(main_window.step11_panel, 'output_file', None) # 死代码 + # 替换后: + widget = resolve_step_widget(main_window, 'step11_predictions') # 找到正确 widget + if widget: ... +""" + +from pathlib import Path +from typing import Optional, Union + + +# 用户口语编号 / 业务别名 → main_window 上真实属性名的映射 +# 这是"张冠李戴"修复的核心——之前代码写的 step11_panel 实际不存在, +# 真实存在的属性见 water_quality_gui.py:1891-1928 +STEP_DATA_SOURCE = { + # 数据流 step 编号(用户口语) → main_window 真实属性 + 'step5_clean_output': 'step5_clean_panel', + 'step7_index_output': 'step7_index_panel', + 'step8_ml_train_output': 'step8_ml_train_panel', + 'step8_5_non_empirical': 'step8_non_empirical_panel', # 之前写错成 step11_panel + 'step9_ml_predict_output': 'step9_ml_predict_panel', + 'step10_watercolor_output': 'step10_watercolor_panel', + 'step11_ml_prediction': 'step9_ml_predict_panel', # 主流程 step11 = ML 预测 + 'step12_regression_prediction': 'step8_non_empirical_panel', # 主流程 step12 = 非经验预测 + 'step13_custom_regression': 'step13_report_panel', # 占位(自定义回归本身没有专属 panel) + 'sampling_csv': 'step4_sampling_panel', + 'training_spectra_csv': 'step5_clean_panel', + 'indices_csv': 'step7_index_panel', + 'models_dir': 'step8_ml_train_panel', + 'watercolor_dir': 'step10_watercolor_panel', + 'prediction_csv_dir': 'step9_ml_predict_panel', # 默认从 ML 预测读 +} + + +def _get_widget(main_window, attr_name: str, widget_attr: str = 'output_file'): + """从 main_window. 取出指定子组件,失败时返回 None。""" + if main_window is None: + return None + panel = getattr(main_window, attr_name, None) + if panel is None: + return None + return getattr(panel, widget_attr, None) + + +def _read_widget_path(widget) -> str: + """统一从 widget 读 path(兼容 FileSelectWidget / QLineEdit / 字符串)。""" + if widget is None: + return "" + if hasattr(widget, 'get_path'): + try: + return str(widget.get_path() or "").strip() + except Exception: + return "" + if hasattr(widget, 'text'): + try: + return str(widget.text() or "").strip() + except Exception: + return "" + if isinstance(widget, str): + return widget.strip() + return "" + + +def resolve_step_widget(main_window, step_key: str, widget_attr: str = 'output_file'): + """根据业务 step_key 解析出正确的 widget(消除张冠李戴)。 + + Returns: + widget 对象 or None(找不到时返回 None,调用方需自行兜底) + """ + attr_name = STEP_DATA_SOURCE.get(step_key) + if attr_name is None: + return None + return _get_widget(main_window, attr_name, widget_attr) + + +_FALLBACK_DIR_TABLE = { + # pipeline key(与 _ensure_step_dir_map 对齐)→ 子目录名 + 'step1': '1_water_mask', + 'step2': '2_Glint_Detection', + 'step3': '3_deglint', + 'step4_sampling': '4_sampling', + 'step5_clean': '5_Data_Cleaning', + 'step6_feature': '6_Spectral_Feature_Extraction', + 'step7_index': '7_Water_Quality_Indices', + 'step8_ml_train': '8_Supervised_Model_Training', + 'step8': '8_Supervised_Model_Training', + 'step9_ml_predict': '8_Non_Empirical_Regression', + 'step9': '8_Non_Empirical_Regression', + 'step10_watercolor': '10_WaterIndex_Images', + 'step10': '10_WaterIndex_Images', + 'step11_map': '14_visualization', + 'step11': '11_12_13_predictions', + 'step11_predictions': '11_12_13_predictions', + 'step12': '13_Custom_Regression', + 'step12_predictions': '11_12_13_predictions', + 'step13': 'reports', + 'step13_predictions': '11_12_13_predictions', + 'step14': '14_visualization', + 'prediction_dir': '11_12_13_predictions', + 'visualization': '14_visualization', + 'reports': 'reports', + 'custom_regression': '13_Custom_Regression', + # 扩展:覆盖 panel 内部使用的子目录别名 + 'water_mask': '1_water_mask', + 'glint_detection': '2_Glint_Detection', + 'deglint': '3_deglint', + 'sampling': '4_sampling', + 'data_cleaning': '5_Data_Cleaning', + 'spectral_feature': '6_Spectral_Feature_Extraction', + 'indices': '7_Water_Quality_Indices', + 'supervised_models': '8_Supervised_Model_Training', + 'non_empirical': '8_Non_Empirical_Regression', + 'qaa_inversion': '8_QAA_Inversion', + 'regression_modeling': '8_Regression_Modeling', + 'watercolor': '10_WaterIndex_Images', + 'ml_prediction': '9_ML_Prediction', + 'sampling_csv_path': '4_sampling/sampling_spectra.csv', +} + + +def get_step_output_path( + main_window, + step_key: str, + work_dir: Optional[Union[str, Path]] = None, + widget_attr: str = 'output_file', + fallback_key: Optional[str] = None, +) -> str: + """获取 step_key 指向的输出路径(带 main_window 解析 + 兜底路径)。 + + 解析顺序: + 1. STEP_DATA_SOURCE[step_key] 找到对应 panel,从 widget 读用户填的 path + 2. 若为空字符串,用 _FALLBACK_DIR_TABLE[fallback_key or step_key] + work_dir 拼兜底 + 3. 全失败返回 str(work_dir) + + 注意:不创建 pipeline 实例(避免触发 osgeo 导入),用本地子目录字典兜底。 + """ + wd = str(work_dir) if work_dir else "" + widget = resolve_step_widget(main_window, step_key, widget_attr) + p = _read_widget_path(widget) + if p: + if not Path(p).is_absolute() and wd: + p = str(Path(wd) / p).replace('\\', '/') + return p + + # 兜底:本地子目录字典(与 pipeline._ensure_step_dir_map 一致) + key = fallback_key or step_key + sub = _FALLBACK_DIR_TABLE.get(key) + if sub and wd: + return str(Path(wd) / sub).replace('\\', '/') + return wd + + +def resolve_subdir(work_dir, subdir_key: str) -> str: + """纯子目录拼装:把 pipeline key 解析为 work_dir 下的子目录路径。 + + 用法:resolve_subdir(self.work_dir, 'visualization') + → '/14_visualization' + + 与 pipeline.get_step_output_dir 同源(都查同一份 _FALLBACK_DIR_TABLE 子集)。 + """ + wd = str(work_dir) if work_dir else "" + sub = _FALLBACK_DIR_TABLE.get(subdir_key) + if sub and wd: + return str(Path(wd) / sub).replace('\\', '/') + return wd + + +__all__ = [ + 'STEP_DATA_SOURCE', + 'resolve_step_widget', + 'get_step_output_path', + 'resolve_subdir', +] diff --git a/src/gui/panels/step10_watercolor_panel.py b/src/gui/panels/step10_watercolor_panel.py index ace8750..615c27b 100644 --- a/src/gui/panels/step10_watercolor_panel.py +++ b/src/gui/panels/step10_watercolor_panel.py @@ -8,10 +8,17 @@ Step10 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像) """ import os +import sys import traceback from pathlib import Path from typing import Dict, List, Optional +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, @@ -451,7 +458,7 @@ class Step10WatercolorPanel(QWidget): # 3. 终极回退:智能扫描 3_deglint 目录,取最新的 .bsq 或 .dat 文件 if not deglint_path and self.work_dir: - deglint_dir = os.path.join(self.work_dir, "3_deglint") + deglint_dir = resolve_subdir(self.work_dir, 'deglint') if os.path.isdir(deglint_dir): import glob candidates = glob.glob(os.path.join(deglint_dir, "*.bsq")) + glob.glob(os.path.join(deglint_dir, "*.dat")) @@ -472,7 +479,7 @@ class Step10WatercolorPanel(QWidget): # 自动填入输出目录 if self.work_dir: - out_dir = os.path.join(self.work_dir, "10_WaterIndex_Images").replace('\\', '/') + out_dir = resolve_subdir(self.work_dir, 'watercolor') os.makedirs(out_dir, exist_ok=True) if not self.output_dir.get_path(): self.output_dir.set_path(out_dir) @@ -503,7 +510,7 @@ class Step10WatercolorPanel(QWidget): return if not output_dir: work_dir = self._get_default_work_dir() - output_dir = os.path.join(work_dir, "10_WaterIndex_Images").replace('\\', '/') + output_dir = resolve_subdir(work_dir, 'watercolor') os.makedirs(output_dir, exist_ok=True) self.output_dir.set_path(output_dir) @@ -518,7 +525,7 @@ class Step10WatercolorPanel(QWidget): # ── 自动扫描工作目录下的水域掩膜文件 ──────────────────────────── work_dir = self.work_dir or str(Path(bsq_path).parent) - mask_dir = os.path.join(work_dir, "1_water_mask") + mask_dir = resolve_subdir(work_dir, 'water_mask') water_mask_path: Optional[str] = None if os.path.isdir(mask_dir): # ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★ diff --git a/src/gui/panels/step11_map_panel.py b/src/gui/panels/step11_map_panel.py index 78404ba..13aacfe 100644 --- a/src/gui/panels/step11_map_panel.py +++ b/src/gui/panels/step11_map_panel.py @@ -5,10 +5,17 @@ Step10 面板 - 专题图生成 """ import os +import sys import traceback from pathlib import Path from typing import List, Optional +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir + from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, @@ -372,7 +379,7 @@ class Step11MapPanel(QWidget): def browse_prediction_csv_dir(self): default = self._get_default_work_dir() if default: - default = os.path.join(default, "11_12_13_predictions") + default = resolve_subdir(default, 'prediction_dir') d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default) if d: self.prediction_csv_dir_edit.setText(d) @@ -392,7 +399,7 @@ class Step11MapPanel(QWidget): """浏览 GeoTIFF 文件夹(批量模式)""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "10_WaterIndex_Images") + default = resolve_subdir(default, 'watercolor') d = QFileDialog.getExistingDirectory( self, "选择水色指数 GeoTIFF 文件夹", default ) @@ -510,49 +517,36 @@ class Step11MapPanel(QWidget): if not main_window: return - # 1. 尝试从 Step8 界面读取机器学习预测输出目录(最优先) + # 1. 优先:从 Step9(机器学习预测)读输出目录,9_ML_Prediction 子目录 + # 修复张冠李戴:原 main_window.step11_prediction_panel 不存在,真实属性是 step9_ml_predict_panel pred_dir = None - if hasattr(main_window, 'step11_prediction_panel'): - step8_widget = getattr(main_window.step11_prediction_panel, 'output_file', None) - step10_output = "" - if hasattr(step8_widget, 'get_path'): - step10_output = step8_widget.get_path() or "" - elif hasattr(step8_widget, 'text'): - step10_output = step8_widget.text() or "" - - if step10_output: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step10_output): - step10_output = os.path.join(self.work_dir or '', step10_output).replace('\\', '/') - # 提取父目录后追加 9_ML_Prediction(最底层真实子目录) - base_pred_dir = str(Path(step10_output).parent) - ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" - pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir - - # 2. 备选:从 Step11 界面读取非经验预测输出目录 - if not pred_dir and hasattr(main_window, 'step11_panel'): - step8_5_widget = getattr(main_window.step11_panel, 'output_file', None) - step8_5_output = "" - if hasattr(step8_5_widget, 'get_path'): - step8_5_output = step8_5_widget.get_path() or "" - elif hasattr(step8_5_widget, 'text'): - step8_5_output = step8_5_widget.text() or "" + step10_output = get_step_output_path( + main_window, 'step11_ml_prediction', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step9_ml_predict', + ) + if step10_output: + # 提取父目录后追加 9_ML_Prediction(最底层真实子目录) + base_pred_dir = str(Path(step10_output).parent) + ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" + pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir + # 2. 备选:从 Step8(非经验预测)读输出目录 + # 修复张冠李戴:原 main_window.step11_panel 不存在,真实属性是 step8_non_empirical_panel + if not pred_dir: + step8_5_output = get_step_output_path( + main_window, 'step12_regression_prediction', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step8_ml_train', + ) if step8_5_output: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step8_5_output): - step8_5_output = os.path.join(self.work_dir or '', step8_5_output).replace('\\', '/') pred_dir = str(Path(step8_5_output).parent) - # 3. 备选:从 Step12 界面读取自定义回归预测输出目录 - if not pred_dir and hasattr(main_window, 'step12_panel'): - step8_75_widget = getattr(main_window.step12_panel, 'output_dir_widget', None) - step8_75_output = "" - if hasattr(step8_75_widget, 'get_path'): - step8_75_output = step8_75_widget.get_path() or "" - elif hasattr(step8_75_widget, 'text'): - step8_75_output = step8_75_widget.text() or "" - + # 3. 备选:从 Step13 panel(自定义回归)读输出目录 + # 修复张冠李戴:原 main_window.step12_panel 不存在;自定义回归 panel 是 step13_panel 类(main_window 上无此名) + if not pred_dir: + step8_75_output = get_step_output_path( + main_window, 'step13_custom_regression', work_dir=self.work_dir, + widget_attr='output_dir_widget', fallback_key='custom_regression', + ) if step8_75_output: pred_dir = step8_75_output @@ -566,7 +560,7 @@ class Step11MapPanel(QWidget): # 4. 自动填充输出目录(14_visualization) if self.work_dir: - output_dir = os.path.join(self.work_dir, "14_visualization") + output_dir = resolve_subdir(self.work_dir, 'visualization') os.makedirs(output_dir, exist_ok=True) existing_out = self.output_dir.get_path() if not existing_out or not existing_out.strip(): @@ -612,7 +606,7 @@ class Step11MapPanel(QWidget): """浏览输出目录""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "14_visualization") + default = resolve_subdir(default, 'visualization') dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default) if dir_path: self.output_dir.set_path(dir_path) @@ -704,7 +698,7 @@ class Step11MapPanel(QWidget): out_dir = (self.output_dir.get_path() or "").strip() if not out_dir: - out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") + out_dir = resolve_subdir(self._get_default_work_dir(), 'visualization') os.makedirs(out_dir, exist_ok=True) self.run_button.setEnabled(False) @@ -760,7 +754,7 @@ class Step11MapPanel(QWidget): # 构造输出路径 out_dir = (self.output_dir.get_path() or "").strip() if not out_dir: - out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") + out_dir = resolve_subdir(self._get_default_work_dir(), 'visualization') os.makedirs(out_dir, exist_ok=True) tif_stem = Path(geotiff_path).stem chinese_name = mapper._get_chinese_title(tif_stem) diff --git a/src/gui/panels/step12_viz_panel.py b/src/gui/panels/step12_viz_panel.py index 4cdf35b..cd60e1a 100644 --- a/src/gui/panels/step12_viz_panel.py +++ b/src/gui/panels/step12_viz_panel.py @@ -6,6 +6,7 @@ VisualizationPanel - 可视化分析面板 """ import os +import sys import traceback from pathlib import Path from typing import Optional, List, Union @@ -13,6 +14,12 @@ from typing import Optional, List, Union import numpy as np import pandas as pd +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir + from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QAbstractTableModel from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import ( @@ -85,7 +92,7 @@ class VisualizationWorkerThread(QThread): wp = Path(self.work_dir) if self.task == "mask_glint": from src.postprocessing.visualization_reports import WaterQualityVisualization - viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization")) + viz = WaterQualityVisualization(output_dir=str(resolve_subdir(self.work_dir, 'visualization'))) preview_paths = viz.generate_glint_deglint_previews( work_dir=str(wp), output_subdir="glint_deglint_previews", @@ -94,7 +101,7 @@ class VisualizationWorkerThread(QThread): self.finished_ok.emit({"task": "mask_glint", "count": cnt, "preview_paths": preview_paths}) elif self.task == "sampling_map": hyperspectral_files = [] - deglint_dir = wp / "3_deglint" + deglint_dir = Path(resolve_subdir(self.work_dir, 'deglint')) if deglint_dir.exists(): for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"): hyperspectral_files.extend(list(deglint_dir.glob(ext))) @@ -121,7 +128,7 @@ class VisualizationWorkerThread(QThread): csv_path = str(csv_files[0]) from src.postprocessing.point_map import SamplingPointMap map_generator = SamplingPointMap( - output_dir=str(wp / "14_visualization" / "sampling_maps"), + output_dir=str(Path(resolve_subdir(self.work_dir, 'visualization')) / "sampling_maps"), fast_mode=True, ) map_path = map_generator.create_sampling_point_map( @@ -146,7 +153,7 @@ class VisualizationWorkerThread(QThread): ) elif self.task == "spectrum": from src.postprocessing.visualization_reports import WaterQualityVisualization - viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization")) + viz = WaterQualityVisualization(output_dir=str(resolve_subdir(self.work_dir, 'visualization'))) csv_file = self.extra.get("csv_path") wl = self.extra.get("wavelength_start_column", "UTM_Y") n_groups = int(self.extra.get("n_groups", 5)) @@ -190,7 +197,7 @@ class VisualizationWorkerThread(QThread): ) elif self.task == "statistics": from src.postprocessing.visualization_reports import WaterQualityVisualization - viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization")) + viz = WaterQualityVisualization(output_dir=str(resolve_subdir(self.work_dir, 'visualization'))) csv_file = self.extra.get("csv_path") param_cols = self.extra.get("param_cols") or [] output_paths = viz.plot_statistical_charts( @@ -219,7 +226,7 @@ class VisualizationWorkerThread(QThread): self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}}) elif self.task == "generate_all_selected": from src.postprocessing.visualization_reports import WaterQualityVisualization - viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization")) + viz = WaterQualityVisualization(output_dir=str(resolve_subdir(self.work_dir, 'visualization'))) parts = [] training_csv = wp / "5_training_spectra" / "training_spectra.csv" @@ -316,7 +323,7 @@ class VisualizationWorkerThread(QThread): if self.extra.get("gen_sampling_map"): hyperspectral_files = [] - deglint_dir = wp / "3_deglint" + deglint_dir = Path(resolve_subdir(self.work_dir, 'deglint')) if deglint_dir.exists(): for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"): hyperspectral_files.extend(list(deglint_dir.glob(ext))) @@ -339,7 +346,7 @@ class VisualizationWorkerThread(QThread): csv_path = str(csv_files[0]) from src.postprocessing.point_map import SamplingPointMap map_generator = SamplingPointMap( - output_dir=str(wp / "14_visualization" / "sampling_maps"), + output_dir=str(Path(resolve_subdir(self.work_dir, 'visualization')) / "sampling_maps"), fast_mode=True, ) map_path = map_generator.create_sampling_point_map( @@ -817,14 +824,14 @@ class ImageCategoryTree(QTreeWidget): # 拓宽扫描根目录列表(新增多个遗漏目录) scan_roots: List[Path] = [ - self._work_path / "14_visualization", - self._work_path / "11_12_13_predictions", - self._work_path / "8_Regression_Modeling", + Path(resolve_subdir(str(self._work_path), 'visualization')), + Path(resolve_subdir(str(self._work_path), 'prediction_dir')), + Path(resolve_subdir(str(self._work_path), 'regression_modeling')), self._work_path / "10_feature_construction", self._work_path / "5_training_spectra", - self._work_path / "2_Glint_Detection", - self._work_path / "3_deglint", - self._work_path / "1_water_mask", + Path(resolve_subdir(str(self._work_path), 'glint_detection')), + Path(resolve_subdir(str(self._work_path), 'deglint')), + Path(resolve_subdir(str(self._work_path), 'water_mask')), self._work_path / "9_water_quality_prediction", self._work_path / "9_Concentration", ] @@ -1536,14 +1543,14 @@ class Step12VizPanel(QWidget): return work_path = Path(self.work_dir) - pred_dir = work_path / "11_12_13_predictions" + pred_dir = Path(resolve_subdir(self.work_dir, 'prediction_dir')) # 按优先级寻找存在的目录 candidates = [ - work_path / "9_ML_Prediction", + Path(resolve_subdir(self.work_dir, 'ml_prediction')), pred_dir / "Non_Empirical_Prediction", - work_path / "13_Custom_Regression" / "Custom_Regression_Prediction", - work_path / "14_visualization", + Path(resolve_subdir(self.work_dir, 'custom_regression')) / "Custom_Regression_Prediction", + Path(resolve_subdir(self.work_dir, 'visualization')), work_path, ] detected_dir = None @@ -1611,7 +1618,7 @@ class Step12VizPanel(QWidget): print(f"扫描工作目录: {work_path}") self.image_tree.scan_directory(str(work_path)) self._setup_prediction_output_dirs(work_path) - viz_dir = work_path / "14_visualization" + viz_dir = Path(resolve_subdir(str(work_path), 'visualization')) if viz_dir.exists(): image_files = list(viz_dir.glob("**/*.png")) + list(viz_dir.glob("**/*.jpg")) if image_files: @@ -1620,20 +1627,17 @@ class Step12VizPanel(QWidget): def _setup_prediction_output_dirs(self, work_path: Path): """设置三个预测步骤的默认输出目录""" try: - base_prediction_dir = work_path / "11_12_13_predictions" - ml_dir = work_path / "9_ML_Prediction" + base_prediction_dir = Path(resolve_subdir(str(work_path), 'prediction_dir')) + ml_dir = Path(resolve_subdir(str(work_path), 'ml_prediction')) reg_dir = base_prediction_dir / "Regression_Model_Prediction" - custom_dir = work_path / "13_Custom_Regression" / "Custom_Regression_Prediction" + custom_dir = Path(resolve_subdir(str(work_path), 'custom_regression')) / "Custom_Regression_Prediction" ml_dir.mkdir(parents=True, exist_ok=True) reg_dir.mkdir(parents=True, exist_ok=True) custom_dir.mkdir(parents=True, exist_ok=True) - if hasattr(self, 'step11_ml_panel') and hasattr(self.step11_ml_panel, 'output_file'): - self.step11_ml_panel.output_file.set_path(str(ml_dir)) - if hasattr(self, 'step11_panel') and hasattr(self.step11_panel, 'output_file'): - self.step11_panel.output_file.set_path(str(reg_dir)) - if hasattr(self, 'step12_panel') and hasattr(self.step12_panel, 'output_dir_widget'): - self.step12_panel.output_dir_widget.set_path(str(custom_dir)) - print(f"预测输出目录已设置:\n ML: {ml_dir}\n Reg: {reg_dir}\n Custom: {custom_dir}") + # 旧的 self.step11_ml_panel/step11_panel/step12_panel 在 Step12VizPanel 上不存在,是死代码。 + # 三个目录的真实默认值在用户首次浏览 / 自动填充时由各 panel 自己的 _get_default_work_dir 路径产出。 + # 这里仅做目录创建 + 提示输出,便于用户在工作目录树中能看到预测输出位置。 + print(f"预测输出目录已创建:\n ML: {ml_dir}\n Reg: {reg_dir}\n Custom: {custom_dir}") except Exception as e: print(f"设置预测输出目录失败: {e}") @@ -1780,7 +1784,7 @@ class Step12VizPanel(QWidget): QMessageBox.warning(self, "警告", "请先选择工作目录!") return work_path = Path(self.work_dir) - viz_dir = work_path / "14_visualization" + viz_dir = Path(resolve_subdir(self.work_dir, 'visualization')) viz_dir2 = viz_dir / "boxplots" viz_dir3 = viz_dir / "scatter_plots" if not viz_dir.exists(): diff --git a/src/gui/panels/step13_panel.py b/src/gui/panels/step13_panel.py index ddee71b..6e3dd5d 100644 --- a/src/gui/panels/step13_panel.py +++ b/src/gui/panels/step13_panel.py @@ -5,6 +5,14 @@ Step12 面板 - 自定义回归预测 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, @@ -94,51 +102,38 @@ class Step12Panel(QWidget): 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 "" + # 1. 尝试从 Step7(水质光谱指数)界面读取全湖采样点 CSV 路径 + # 修复张冠李戴:原 main_window.step7_panel 不存在,真实属性是 step7_index_panel + step7_output_path = get_step_output_path( + main_window, 'sampling_csv', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step7_index', + ) + 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) - 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. 尝试从 Step9 界面读取自定义回归模型目录 - if main_window and hasattr(main_window, 'step12_panel'): - step9_widget = getattr(main_window.step9_panel, 'output_dir', None) - step9_models_dir = "" - if hasattr(step9_widget, 'get_path'): - step9_models_dir = step9_widget.get_path() or "" - elif hasattr(step9_widget, 'text'): - step9_models_dir = step9_widget.text() or "" - step9_models_dir = step9_models_dir.strip() - - if step9_models_dir: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step9_models_dir): - step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/') - existing_models = self.regression_models_dir.get_path() - if not existing_models or not existing_models.strip(): - self.regression_models_dir.set_path(step9_models_dir) + # 2. 尝试从 Step8(非经验回归/自定义回归源)读取模型目录 + # 修复张冠李戴:原 main_window.step12_panel 不存在;按代码原意是 step9 的 output_dir + step9_models_dir = get_step_output_path( + main_window, 'models_dir', work_dir=self.work_dir, + widget_attr='output_dir', fallback_key='step8_ml_train', + ) + if step9_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(step9_models_dir) # 3. 自动填充回归模型目录(如果 step9 未提供) if self.work_dir: models_dir = self.regression_models_dir.get_path().strip() if not models_dir: - default_models_dir = os.path.join(self.work_dir, "13_Custom_Regression").replace('\\', '/') + default_models_dir = resolve_subdir(self.work_dir, 'custom_regression') self.regression_models_dir.set_path(default_models_dir) # 4. 自动填充输出目录(自定义回归预测目录) if self.work_dir: - output_dir = os.path.join(self.work_dir, "13_Custom_Regression/Custom_Regression_Prediction") + output_dir = os.path.join(resolve_subdir(self.work_dir, 'custom_regression'), "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(): @@ -161,7 +156,7 @@ class Step12Panel(QWidget): """浏览回归模型目录""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "13_Custom_Regression") + default = resolve_subdir(default, 'custom_regression') dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", default) if dir_path: self.regression_models_dir.set_path(dir_path) diff --git a/src/gui/panels/step13_report_panel.py b/src/gui/panels/step13_report_panel.py index d911987..7b53e27 100644 --- a/src/gui/panels/step13_report_panel.py +++ b/src/gui/panels/step13_report_panel.py @@ -5,10 +5,17 @@ ReportGenerationPanel - Word 分析报告生成面板 """ import os +import sys import traceback from pathlib import Path from typing import Optional +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir + from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, @@ -206,7 +213,7 @@ class Step13ReportPanel(QWidget): def browse_output_dir(self): default = self._get_default_work_dir() if default: - default = os.path.join(default, "14_visualization") + default = resolve_subdir(default, 'visualization') d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default) if d: self.output_dir_edit.setText(d) diff --git a/src/gui/panels/step14_panel.py b/src/gui/panels/step14_panel.py index 3ebaf70..cb9a848 100644 --- a/src/gui/panels/step14_panel.py +++ b/src/gui/panels/step14_panel.py @@ -5,10 +5,17 @@ Step14 面板 - 分布图生成 """ import os +import sys import traceback from pathlib import Path from typing import List, Optional +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget + from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, @@ -372,7 +379,7 @@ class Step14Panel(QWidget): def browse_prediction_csv_dir(self): default = self._get_default_work_dir() if default: - default = os.path.join(default, "11_12_13_predictions") + default = resolve_subdir(default, 'prediction_dir') d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default) if d: self.prediction_csv_dir_edit.setText(d) @@ -392,7 +399,7 @@ class Step14Panel(QWidget): """浏览 GeoTIFF 文件夹(批量模式)""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "10_WaterIndex_Images") + default = resolve_subdir(default, 'watercolor') d = QFileDialog.getExistingDirectory( self, "选择水色指数 GeoTIFF 文件夹", default ) @@ -510,49 +517,35 @@ class Step14Panel(QWidget): if not main_window: return - # 1. 尝试从 Step8 界面读取机器学习预测输出目录(最优先) + # 1. 优先:从 Step9(机器学习预测)读输出目录,9_ML_Prediction 子目录 + # 修复张冠李戴:原 main_window.step11_prediction_panel 不存在 pred_dir = None - if hasattr(main_window, 'step11_prediction_panel'): - step8_widget = getattr(main_window.step11_prediction_panel, 'output_file', None) - step10_output = "" - if hasattr(step8_widget, 'get_path'): - step10_output = step8_widget.get_path() or "" - elif hasattr(step8_widget, 'text'): - step10_output = step8_widget.text() or "" - - if step10_output: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step10_output): - step10_output = os.path.join(self.work_dir or '', step10_output).replace('\\', '/') - # 提取父目录后追加 9_ML_Prediction(最底层真实子目录) - base_pred_dir = str(Path(step10_output).parent) - ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" - pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir - - # 2. 备选:从 Step11 界面读取非经验预测输出目录 - if not pred_dir and hasattr(main_window, 'step11_panel'): - step8_5_widget = getattr(main_window.step11_panel, 'output_file', None) - step8_5_output = "" - if hasattr(step8_5_widget, 'get_path'): - step8_5_output = step8_5_widget.get_path() or "" - elif hasattr(step8_5_widget, 'text'): - step8_5_output = step8_5_widget.text() or "" + step10_output = get_step_output_path( + main_window, 'step11_ml_prediction', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step9_ml_predict', + ) + if step10_output: + base_pred_dir = str(Path(step10_output).parent) + ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" + pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir + # 2. 备选:从 Step8(非经验预测)读输出目录 + # 修复张冠李戴:原 main_window.step11_panel 不存在 + if not pred_dir: + step8_5_output = get_step_output_path( + main_window, 'step12_regression_prediction', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step8_ml_train', + ) if step8_5_output: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step8_5_output): - step8_5_output = os.path.join(self.work_dir or '', step8_5_output).replace('\\', '/') pred_dir = str(Path(step8_5_output).parent) - # 3. 备选:从 Step12 界面读取自定义回归预测输出目录 - if not pred_dir and hasattr(main_window, 'step12_panel'): - step8_75_widget = getattr(main_window.step12_panel, 'output_dir_widget', None) - step8_75_output = "" - if hasattr(step8_75_widget, 'get_path'): - step8_75_output = step8_75_widget.get_path() or "" - elif hasattr(step8_75_widget, 'text'): - step8_75_output = step8_75_widget.text() or "" - + # 3. 备选:从 Step13 panel(自定义回归)读输出目录 + # 修复张冠李戴:原 main_window.step12_panel 不存在 + if not pred_dir: + step8_75_output = get_step_output_path( + main_window, 'step13_custom_regression', work_dir=self.work_dir, + widget_attr='output_dir_widget', fallback_key='custom_regression', + ) if step8_75_output: pred_dir = step8_75_output @@ -566,7 +559,7 @@ class Step14Panel(QWidget): # 4. 自动填充输出目录(14_visualization) if self.work_dir: - output_dir = os.path.join(self.work_dir, "14_visualization") + output_dir = resolve_subdir(self.work_dir, 'visualization') os.makedirs(output_dir, exist_ok=True) existing_out = self.output_dir.get_path() if not existing_out or not existing_out.strip(): @@ -612,7 +605,7 @@ class Step14Panel(QWidget): """浏览输出目录""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "14_visualization") + default = resolve_subdir(default, 'visualization') dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default) if dir_path: self.output_dir.set_path(dir_path) @@ -704,7 +697,7 @@ class Step14Panel(QWidget): out_dir = (self.output_dir.get_path() or "").strip() if not out_dir: - out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") + out_dir = resolve_subdir(self._get_default_work_dir(), 'visualization') os.makedirs(out_dir, exist_ok=True) self.run_button.setEnabled(False) @@ -760,7 +753,7 @@ class Step14Panel(QWidget): # 构造输出路径 out_dir = (self.output_dir.get_path() or "").strip() if not out_dir: - out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") + out_dir = resolve_subdir(self._get_default_work_dir(), 'visualization') os.makedirs(out_dir, exist_ok=True) tif_stem = Path(geotiff_path).stem chinese_name = mapper._get_chinese_title(tif_stem) diff --git a/src/gui/panels/step1_panel.py b/src/gui/panels/step1_panel.py index d9d046c..1a110c9 100644 --- a/src/gui/panels/step1_panel.py +++ b/src/gui/panels/step1_panel.py @@ -5,6 +5,14 @@ Step1 面板 - 水域掩膜生成 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, @@ -197,8 +205,8 @@ class Step1Panel(QWidget): if not hasattr(self, 'work_dir') or not self.work_dir: return - # 生成输出掩膜的完整路径 - output_dir = os.path.join(self.work_dir, "1_water_mask") + # 生成输出掩膜的完整路径(用 resolve_subdir 归一化,消除硬编码) + output_dir = resolve_subdir(self.work_dir, 'water_mask') os.makedirs(output_dir, exist_ok=True) # 确保目录存在 # 统一使用正斜杠,避免 \ 和 / 混用 diff --git a/src/gui/panels/step2_panel.py b/src/gui/panels/step2_panel.py index e9c6dbf..7ececfe 100644 --- a/src/gui/panels/step2_panel.py +++ b/src/gui/panels/step2_panel.py @@ -5,6 +5,14 @@ Step2 面板 - 耀斑区域识别 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, @@ -187,7 +195,7 @@ class Step2Panel(QWidget): # 3. 自动填充输出路径(基于工作目录) if self.work_dir: # 生成输出耀斑掩膜的标准路径:workspace/2_Glint_Detection/severe_glint_area.dat - output_dir = os.path.join(self.work_dir, "2_Glint_Detection") + output_dir = resolve_subdir(self.work_dir, 'glint_detection') os.makedirs(output_dir, exist_ok=True) default_output_path = os.path.join(output_dir, "severe_glint_area.dat").replace('\\', '/') self.output_file.set_path(default_output_path) diff --git a/src/gui/panels/step3_panel.py b/src/gui/panels/step3_panel.py index 200c259..48bc54a 100644 --- a/src/gui/panels/step3_panel.py +++ b/src/gui/panels/step3_panel.py @@ -5,6 +5,14 @@ Step3 面板 - 耀斑去除 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, @@ -299,7 +307,7 @@ class Step3Panel(QWidget): # 自动填充输出路径(基于工作目录) if self.work_dir: - output_dir = os.path.join(self.work_dir, "3_deglint") + output_dir = resolve_subdir(self.work_dir, 'deglint') os.makedirs(output_dir, exist_ok=True) default_output_path = os.path.join(output_dir, "deglint_image.bsq").replace('\\', '/') self.output_file.set_path(default_output_path) diff --git a/src/gui/panels/step4_sampling_panel.py b/src/gui/panels/step4_sampling_panel.py index e61c063..542d6bf 100644 --- a/src/gui/panels/step4_sampling_panel.py +++ b/src/gui/panels/step4_sampling_panel.py @@ -5,6 +5,14 @@ Step4 面板 - 采样点布设 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import ( @@ -186,7 +194,7 @@ class Step4SamplingPanel(QWidget): water_mask_path = main_window.step1_panel.output_file.get_path() # 备选:扫描 1_water_mask 目录下的 .dat 文件 if not water_mask_path and self.work_dir: - mask_dir = os.path.join(self.work_dir, "1_water_mask") + mask_dir = resolve_subdir(self.work_dir, 'water_mask') if os.path.isdir(mask_dir): dat_files = [f for f in os.listdir(mask_dir) if f.lower().endswith('.dat')] if dat_files: @@ -213,7 +221,7 @@ class Step4SamplingPanel(QWidget): # 3. 自动填充输出路径(绝对路径) if self.work_dir: - output_path = os.path.join(self.work_dir, "4_sampling", "sampling_spectra.csv") + output_path = resolve_subdir(self.work_dir, 'sampling_csv_path') os.makedirs(os.path.dirname(output_path), exist_ok=True) self.output_file.set_path(output_path.replace('\\', '/')) diff --git a/src/gui/panels/step5_clean_panel.py b/src/gui/panels/step5_clean_panel.py index 72be031..229f672 100644 --- a/src/gui/panels/step5_clean_panel.py +++ b/src/gui/panels/step5_clean_panel.py @@ -5,6 +5,14 @@ Step4 面板 - 数据预处理 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir import pandas as pd from PyQt5.QtWidgets import ( @@ -127,7 +135,7 @@ class Step5CleanPanel(QWidget): self.work_dir = None if self.work_dir: - output_dir = os.path.join(self.work_dir, "5_Data_Cleaning") + output_dir = resolve_subdir(self.work_dir, 'data_cleaning') 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) diff --git a/src/gui/panels/step6_feature_panel.py b/src/gui/panels/step6_feature_panel.py index 5201c93..c3e2cfe 100644 --- a/src/gui/panels/step6_feature_panel.py +++ b/src/gui/panels/step6_feature_panel.py @@ -5,6 +5,14 @@ Step6 面板 - 光谱特征提取 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel, @@ -193,7 +201,7 @@ class Step6FeaturePanel(QWidget): # 4. 自动填充输出路径(基于工作目录) if self.work_dir: - output_dir = os.path.join(self.work_dir, "6_Spectral_Feature_Extraction") + output_dir = resolve_subdir(self.work_dir, 'spectral_feature') os.makedirs(output_dir, exist_ok=True) default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/') self.output_file.set_path(default_output_path) diff --git a/src/gui/panels/step7_index_panel.py b/src/gui/panels/step7_index_panel.py index abc7041..8bec72b 100644 --- a/src/gui/panels/step7_index_panel.py +++ b/src/gui/panels/step7_index_panel.py @@ -10,6 +10,12 @@ import pandas as pd from pathlib import Path from typing import Dict, List, Optional +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QGridLayout, QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox, @@ -261,7 +267,7 @@ class Step7IndexPanel(QWidget): work_dir = self._get_work_dir() if work_dir: - track_a_dir = os.path.join(work_dir, "7_Water_Quality_Indices") + track_a_dir = resolve_subdir(work_dir, 'indices') os.makedirs(track_a_dir, exist_ok=True) config['output_file'] = os.path.join(track_a_dir, "training_spectra_indices.csv").replace('\\', '/') diff --git a/src/gui/panels/step8_ml_train_panel.py b/src/gui/panels/step8_ml_train_panel.py index 7376650..fca54b2 100644 --- a/src/gui/panels/step8_ml_train_panel.py +++ b/src/gui/panels/step8_ml_train_panel.py @@ -5,6 +5,14 @@ Step8 面板 - 机器学习建模 """ import os +import sys +from pathlib import Path + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, @@ -271,7 +279,7 @@ class Step8MlTrainPanel(QWidget): 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, "7_Water_Quality_Indices") if work_dir else "" + initial_dir = resolve_subdir(work_dir, 'indices') if work_dir else "" if initial_dir and not os.path.isdir(initial_dir): os.makedirs(initial_dir, exist_ok=True) @@ -350,18 +358,19 @@ class Step8MlTrainPanel(QWidget): self.work_dir = None # 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径 + # 修复张冠李戴:原代码 main_window.step5_panel 不存在,正确属性是 step5_clean_panel main_window = self.window() - if hasattr(main_window, 'step5_panel'): - # 优先直接从 Step5 的输出 widget 读取 - step5_output = main_window.step5_panel.output_file.get_path() - if step5_output: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step5_output): - step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/') - self.training_csv_file.set_path(step5_output) - elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'): - # 回退:从 Step5 的 config 字典中查找可能的键名 - step5_cfg = main_window.step5_panel.get_config() + step5_output = get_step_output_path( + main_window, 'training_spectra_csv', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step6_feature', + ) + if step5_output: + self.training_csv_file.set_path(step5_output) + else: + # 回退:从 Step5 的 config 字典中查找可能的键名 + step5_panel = getattr(main_window, 'step5_clean_panel', None) + if step5_panel and hasattr(step5_panel, 'get_config'): + step5_cfg = step5_panel.get_config() step5_csv = ( step5_cfg.get('training_csv_path') or step5_cfg.get('output_file') @@ -369,7 +378,6 @@ class Step8MlTrainPanel(QWidget): 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) @@ -378,7 +386,7 @@ class Step8MlTrainPanel(QWidget): # 输入是 training_spectra.csv → 输出 {work_dir}/7_Water_Quality_Indices/training_spectra_indices.csv # 输入是 sampling_spectra.csv → 输出 {work_dir}/7_Water_Quality_Indices/sampling_spectra_indices.csv if self.work_dir: - indices_dir = os.path.join(self.work_dir, "7_Water_Quality_Indices") + indices_dir = resolve_subdir(self.work_dir, 'indices') os.makedirs(indices_dir, exist_ok=True) training_csv = self.training_csv_file.get_path() if training_csv: diff --git a/src/gui/panels/step8_non_empirical_panel.py b/src/gui/panels/step8_non_empirical_panel.py index 25dff2a..276199f 100644 --- a/src/gui/panels/step8_non_empirical_panel.py +++ b/src/gui/panels/step8_non_empirical_panel.py @@ -5,8 +5,15 @@ Step8 面板 - 非经验统计回归建模 """ import os +import sys from pathlib import Path +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QCheckBox, QSpinBox, QPushButton, @@ -231,25 +238,19 @@ class Step8NonEmpiricalPanel(QWidget): # 借用父组件的 window() 方法,安全绕过当前类的命名冲突 parent_widget = self.parentWidget() main_window = parent_widget.window() if parent_widget else None - if main_window and hasattr(main_window, 'step5_panel'): - step5_widget = getattr(main_window.step5_panel, 'output_file', None) - step5_output_path = "" - if hasattr(step5_widget, 'get_path'): - step5_output_path = step5_widget.get_path() or "" - elif hasattr(step5_widget, 'text'): - step5_output_path = step5_widget.text() or "" - - if step5_output_path: - # 若为相对路径,使用 work_dir 合成为绝对路径 - if not os.path.isabs(step5_output_path): - step5_output_path = os.path.join(self.work_dir or '', step5_output_path).replace('\\', '/') - existing = self.training_csv_file.get_path() - if not existing or not existing.strip(): - self.training_csv_file.set_path(step5_output_path) + # 1. 尝试从 Step5(数据清洗)读取训练光谱 CSV(修复张冠李戴:原 main_window.step5_panel 不存在) + step5_output_path = get_step_output_path( + main_window, 'training_spectra_csv', work_dir=self.work_dir, + widget_attr='output_file', fallback_key='step6_feature', + ) + 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") + output_dir = resolve_subdir(self.work_dir, '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(): @@ -274,7 +275,7 @@ class Step8NonEmpiricalPanel(QWidget): """浏览输出目录""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "8_Regression_Modeling") + default = resolve_subdir(default, 'regression_modeling') dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", default) if dir_path: self.output_dir.set_path(dir_path) diff --git a/src/gui/panels/step8_qaa_panel.py b/src/gui/panels/step8_qaa_panel.py index 2a9ae37..6edbfc8 100644 --- a/src/gui/panels/step8_qaa_panel.py +++ b/src/gui/panels/step8_qaa_panel.py @@ -5,6 +5,13 @@ Step8 面板 - QAA 物理反演(非经验模型) """ import os +import sys + +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, @@ -143,7 +150,7 @@ class Step8QAAPanel(QWidget): if not initial_dir or not os.path.isdir(initial_dir): work_dir = self._get_default_work_dir() - initial_dir = os.path.join(work_dir, "8_QAA_Inversion") if work_dir else "" + initial_dir = resolve_subdir(work_dir, 'qaa_inversion') if work_dir else "" if initial_dir and not os.path.isdir(initial_dir): os.makedirs(initial_dir, exist_ok=True) @@ -198,7 +205,7 @@ class Step8QAAPanel(QWidget): self.spectrum_csv_file.set_path(step5_output) if self.work_dir: - qaa_dir = os.path.join(self.work_dir, "8_QAA_Inversion") + qaa_dir = resolve_subdir(self.work_dir, 'qaa_inversion') os.makedirs(qaa_dir, exist_ok=True) output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/') self.output_path.set_path(output_path) @@ -228,7 +235,7 @@ class Step8QAAPanel(QWidget): if not output_path: work_dir = self._get_default_work_dir() - qaa_dir = os.path.join(work_dir, "8_QAA_Inversion") if work_dir else "" + qaa_dir = resolve_subdir(work_dir, 'qaa_inversion') if work_dir else "" if qaa_dir and not os.path.isdir(qaa_dir): os.makedirs(qaa_dir, exist_ok=True) output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/') diff --git a/src/gui/panels/step9_ml_predict_panel.py b/src/gui/panels/step9_ml_predict_panel.py index ac83551..81c45e2 100644 --- a/src/gui/panels/step9_ml_predict_panel.py +++ b/src/gui/panels/step9_ml_predict_panel.py @@ -5,8 +5,15 @@ Step11 面板 - 机器学习预测 """ import os +import sys from pathlib import Path +# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) +from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir + from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox, @@ -190,7 +197,7 @@ class Step9MlPredictPanel(QWidget): """浏览模型母文件夹,自动扫描子目录中的 .joblib 文件""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "9_ML_Prediction") + default = resolve_subdir(default, 'ml_prediction') dir_path = QFileDialog.getExistingDirectory( self, "选择模型母文件夹", @@ -334,25 +341,20 @@ class Step9MlPredictPanel(QWidget): if not existing or not existing.strip(): self.sampling_csv_file.set_path(step4_output_path) - # 2. 尝试从 Step9(监督建模)读取模型目录 - if main_window and hasattr(main_window, 'step9_panel'): - step9_widget = getattr(main_window.step9_panel, 'output_dir', None) - step9_models_dir = "" - if hasattr(step9_widget, 'get_path'): - step9_models_dir = step9_widget.get_path() or "" - elif hasattr(step9_widget, 'text'): - step9_models_dir = step9_widget.text() or "" + # 2. 尝试从 Step8(监督建模)读取模型目录(修复张冠李戴:原代码 main_window.step9_panel 不存在) + step8_models_dir = get_step_output_path( + main_window, 'models_dir', work_dir=self.work_dir, + widget_attr='output_dir', fallback_key='step8_ml_train', + ) + if step8_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(step8_models_dir) - if step9_models_dir: - if not os.path.isabs(step9_models_dir): - step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/') - existing_models = self.models_dir_file.get_path() - if not existing_models or not existing_models.strip(): - self.models_dir_file.set_path(step9_models_dir) - - # 3. 自动填充输出路径(机器学习预测目录) + # 3. 自动填充输出路径(机器学习预测目录,归属 step9 → 9_ML_Prediction) + # 注:9_ML_Prediction 是 prediction_dir 的子目录,用本地约定 if self.work_dir: - output_dir = os.path.join(self.work_dir, "9_ML_Prediction") + output_dir = resolve_subdir(self.work_dir, 'ml_prediction') os.makedirs(output_dir, exist_ok=True) existing_out = self.output_file.get_path() if not existing_out or not existing_out.strip(): @@ -375,7 +377,7 @@ class Step9MlPredictPanel(QWidget): """浏览模型目录""" default = self._get_default_work_dir() if default: - default = os.path.join(default, "9_ML_Prediction") + default = resolve_subdir(default, 'ml_prediction') dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default) if dir_path: self.models_dir_file.set_path(dir_path)