路径归一化:统一 14 个子目录 helper 接口 + 修复 getattr 张冠李戴

新增 _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 全部就位。
This commit is contained in:
DXC
2026-06-16 12:54:18 +08:00
parent 03c788a16c
commit 0238aa66ab
19 changed files with 544 additions and 222 deletions

View File

@ -186,6 +186,70 @@ class WaterQualityInversionPipeline:
print(f"工作目录已创建: {self.work_dir}") 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): def set_callback(self, callback):
""" """
设置回调函数用于向GUI报告进度 设置回调函数用于向GUI报告进度

View File

@ -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.<attr_name> 取出指定子组件,失败时返回 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')
'<work_dir>/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',
]

View File

@ -8,10 +8,17 @@ Step10 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像)
""" """
import os import os
import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout,
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton,
@ -451,7 +458,7 @@ class Step10WatercolorPanel(QWidget):
# 3. 终极回退:智能扫描 3_deglint 目录,取最新的 .bsq 或 .dat 文件 # 3. 终极回退:智能扫描 3_deglint 目录,取最新的 .bsq 或 .dat 文件
if not deglint_path and self.work_dir: 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): if os.path.isdir(deglint_dir):
import glob import glob
candidates = glob.glob(os.path.join(deglint_dir, "*.bsq")) + glob.glob(os.path.join(deglint_dir, "*.dat")) 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: 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) os.makedirs(out_dir, exist_ok=True)
if not self.output_dir.get_path(): if not self.output_dir.get_path():
self.output_dir.set_path(out_dir) self.output_dir.set_path(out_dir)
@ -503,7 +510,7 @@ class Step10WatercolorPanel(QWidget):
return return
if not output_dir: if not output_dir:
work_dir = self._get_default_work_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) os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir) 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) 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 water_mask_path: Optional[str] = None
if os.path.isdir(mask_dir): if os.path.isdir(mask_dir):
# ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★ # ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★

View File

@ -5,10 +5,17 @@ Step10 面板 - 专题图生成
""" """
import os import os
import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import List, Optional 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.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout,
@ -372,7 +379,7 @@ class Step11MapPanel(QWidget):
def browse_prediction_csv_dir(self): def browse_prediction_csv_dir(self):
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "11_12_13_predictions") default = resolve_subdir(default, 'prediction_dir')
d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default) d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default)
if d: if d:
self.prediction_csv_dir_edit.setText(d) self.prediction_csv_dir_edit.setText(d)
@ -392,7 +399,7 @@ class Step11MapPanel(QWidget):
"""浏览 GeoTIFF 文件夹(批量模式)""" """浏览 GeoTIFF 文件夹(批量模式)"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "10_WaterIndex_Images") default = resolve_subdir(default, 'watercolor')
d = QFileDialog.getExistingDirectory( d = QFileDialog.getExistingDirectory(
self, "选择水色指数 GeoTIFF 文件夹", default self, "选择水色指数 GeoTIFF 文件夹", default
) )
@ -510,49 +517,36 @@ class Step11MapPanel(QWidget):
if not main_window: if not main_window:
return return
# 1. 尝试从 Step8 界面读取机器学习预测输出目录(最优先) # 1. 优先:从 Step9机器学习预测)读输出目录9_ML_Prediction 子目录
# 修复张冠李戴:原 main_window.step11_prediction_panel 不存在,真实属性是 step9_ml_predict_panel
pred_dir = None pred_dir = None
if hasattr(main_window, 'step11_prediction_panel'): step10_output = get_step_output_path(
step8_widget = getattr(main_window.step11_prediction_panel, 'output_file', None) main_window, 'step11_ml_prediction', work_dir=self.work_dir,
step10_output = "" widget_attr='output_file', fallback_key='step9_ml_predict',
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: 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最底层真实子目录 # 提取父目录后追加 9_ML_Prediction最底层真实子目录
base_pred_dir = str(Path(step10_output).parent) base_pred_dir = str(Path(step10_output).parent)
ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" 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 pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir
# 2. 备选:从 Step11 界面读取非经验预测输出目录 # 2. 备选:从 Step8非经验预测)读输出目录
if not pred_dir and hasattr(main_window, 'step11_panel'): # 修复张冠李戴:原 main_window.step11_panel 不存在,真实属性是 step8_non_empirical_panel
step8_5_widget = getattr(main_window.step11_panel, 'output_file', None) if not pred_dir:
step8_5_output = "" step8_5_output = get_step_output_path(
if hasattr(step8_5_widget, 'get_path'): main_window, 'step12_regression_prediction', work_dir=self.work_dir,
step8_5_output = step8_5_widget.get_path() or "" widget_attr='output_file', fallback_key='step8_ml_train',
elif hasattr(step8_5_widget, 'text'): )
step8_5_output = step8_5_widget.text() or ""
if step8_5_output: 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) pred_dir = str(Path(step8_5_output).parent)
# 3. 备选:从 Step12 界面读取自定义回归预测输出目录 # 3. 备选:从 Step13 panel自定义回归)读输出目录
if not pred_dir and hasattr(main_window, 'step12_panel'): # 修复张冠李戴:原 main_window.step12_panel 不存在;自定义回归 panel 是 step13_panel 类main_window 上无此名)
step8_75_widget = getattr(main_window.step12_panel, 'output_dir_widget', None) if not pred_dir:
step8_75_output = "" step8_75_output = get_step_output_path(
if hasattr(step8_75_widget, 'get_path'): main_window, 'step13_custom_regression', work_dir=self.work_dir,
step8_75_output = step8_75_widget.get_path() or "" widget_attr='output_dir_widget', fallback_key='custom_regression',
elif hasattr(step8_75_widget, 'text'): )
step8_75_output = step8_75_widget.text() or ""
if step8_75_output: if step8_75_output:
pred_dir = step8_75_output pred_dir = step8_75_output
@ -566,7 +560,7 @@ class Step11MapPanel(QWidget):
# 4. 自动填充输出目录14_visualization # 4. 自动填充输出目录14_visualization
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.get_path() existing_out = self.output_dir.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -612,7 +606,7 @@ class Step11MapPanel(QWidget):
"""浏览输出目录""" """浏览输出目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "14_visualization") default = resolve_subdir(default, 'visualization')
dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default) dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default)
if dir_path: if dir_path:
self.output_dir.set_path(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() out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir: 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) os.makedirs(out_dir, exist_ok=True)
self.run_button.setEnabled(False) self.run_button.setEnabled(False)
@ -760,7 +754,7 @@ class Step11MapPanel(QWidget):
# 构造输出路径 # 构造输出路径
out_dir = (self.output_dir.get_path() or "").strip() out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir: 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) os.makedirs(out_dir, exist_ok=True)
tif_stem = Path(geotiff_path).stem tif_stem = Path(geotiff_path).stem
chinese_name = mapper._get_chinese_title(tif_stem) chinese_name = mapper._get_chinese_title(tif_stem)

View File

@ -6,6 +6,7 @@ VisualizationPanel - 可视化分析面板
""" """
import os import os
import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union from typing import Optional, List, Union
@ -13,6 +14,12 @@ from typing import Optional, List, Union
import numpy as np import numpy as np
import pandas as pd 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.QtCore import Qt, QTimer, QThread, pyqtSignal, QAbstractTableModel
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -85,7 +92,7 @@ class VisualizationWorkerThread(QThread):
wp = Path(self.work_dir) wp = Path(self.work_dir)
if self.task == "mask_glint": if self.task == "mask_glint":
from src.postprocessing.visualization_reports import WaterQualityVisualization 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( preview_paths = viz.generate_glint_deglint_previews(
work_dir=str(wp), work_dir=str(wp),
output_subdir="glint_deglint_previews", 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}) self.finished_ok.emit({"task": "mask_glint", "count": cnt, "preview_paths": preview_paths})
elif self.task == "sampling_map": elif self.task == "sampling_map":
hyperspectral_files = [] hyperspectral_files = []
deglint_dir = wp / "3_deglint" deglint_dir = Path(resolve_subdir(self.work_dir, 'deglint'))
if deglint_dir.exists(): if deglint_dir.exists():
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"): for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
hyperspectral_files.extend(list(deglint_dir.glob(ext))) hyperspectral_files.extend(list(deglint_dir.glob(ext)))
@ -121,7 +128,7 @@ class VisualizationWorkerThread(QThread):
csv_path = str(csv_files[0]) csv_path = str(csv_files[0])
from src.postprocessing.point_map import SamplingPointMap from src.postprocessing.point_map import SamplingPointMap
map_generator = 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, fast_mode=True,
) )
map_path = map_generator.create_sampling_point_map( map_path = map_generator.create_sampling_point_map(
@ -146,7 +153,7 @@ class VisualizationWorkerThread(QThread):
) )
elif self.task == "spectrum": elif self.task == "spectrum":
from src.postprocessing.visualization_reports import WaterQualityVisualization 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") csv_file = self.extra.get("csv_path")
wl = self.extra.get("wavelength_start_column", "UTM_Y") wl = self.extra.get("wavelength_start_column", "UTM_Y")
n_groups = int(self.extra.get("n_groups", 5)) n_groups = int(self.extra.get("n_groups", 5))
@ -190,7 +197,7 @@ class VisualizationWorkerThread(QThread):
) )
elif self.task == "statistics": elif self.task == "statistics":
from src.postprocessing.visualization_reports import WaterQualityVisualization 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") csv_file = self.extra.get("csv_path")
param_cols = self.extra.get("param_cols") or [] param_cols = self.extra.get("param_cols") or []
output_paths = viz.plot_statistical_charts( output_paths = viz.plot_statistical_charts(
@ -219,7 +226,7 @@ class VisualizationWorkerThread(QThread):
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}}) self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
elif self.task == "generate_all_selected": elif self.task == "generate_all_selected":
from src.postprocessing.visualization_reports import WaterQualityVisualization 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 = [] parts = []
training_csv = wp / "5_training_spectra" / "training_spectra.csv" training_csv = wp / "5_training_spectra" / "training_spectra.csv"
@ -316,7 +323,7 @@ class VisualizationWorkerThread(QThread):
if self.extra.get("gen_sampling_map"): if self.extra.get("gen_sampling_map"):
hyperspectral_files = [] hyperspectral_files = []
deglint_dir = wp / "3_deglint" deglint_dir = Path(resolve_subdir(self.work_dir, 'deglint'))
if deglint_dir.exists(): if deglint_dir.exists():
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"): for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
hyperspectral_files.extend(list(deglint_dir.glob(ext))) hyperspectral_files.extend(list(deglint_dir.glob(ext)))
@ -339,7 +346,7 @@ class VisualizationWorkerThread(QThread):
csv_path = str(csv_files[0]) csv_path = str(csv_files[0])
from src.postprocessing.point_map import SamplingPointMap from src.postprocessing.point_map import SamplingPointMap
map_generator = 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, fast_mode=True,
) )
map_path = map_generator.create_sampling_point_map( map_path = map_generator.create_sampling_point_map(
@ -817,14 +824,14 @@ class ImageCategoryTree(QTreeWidget):
# 拓宽扫描根目录列表(新增多个遗漏目录) # 拓宽扫描根目录列表(新增多个遗漏目录)
scan_roots: List[Path] = [ scan_roots: List[Path] = [
self._work_path / "14_visualization", Path(resolve_subdir(str(self._work_path), 'visualization')),
self._work_path / "11_12_13_predictions", Path(resolve_subdir(str(self._work_path), 'prediction_dir')),
self._work_path / "8_Regression_Modeling", Path(resolve_subdir(str(self._work_path), 'regression_modeling')),
self._work_path / "10_feature_construction", self._work_path / "10_feature_construction",
self._work_path / "5_training_spectra", self._work_path / "5_training_spectra",
self._work_path / "2_Glint_Detection", Path(resolve_subdir(str(self._work_path), 'glint_detection')),
self._work_path / "3_deglint", Path(resolve_subdir(str(self._work_path), 'deglint')),
self._work_path / "1_water_mask", Path(resolve_subdir(str(self._work_path), 'water_mask')),
self._work_path / "9_water_quality_prediction", self._work_path / "9_water_quality_prediction",
self._work_path / "9_Concentration", self._work_path / "9_Concentration",
] ]
@ -1536,14 +1543,14 @@ class Step12VizPanel(QWidget):
return return
work_path = Path(self.work_dir) 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 = [ candidates = [
work_path / "9_ML_Prediction", Path(resolve_subdir(self.work_dir, 'ml_prediction')),
pred_dir / "Non_Empirical_Prediction", pred_dir / "Non_Empirical_Prediction",
work_path / "13_Custom_Regression" / "Custom_Regression_Prediction", Path(resolve_subdir(self.work_dir, 'custom_regression')) / "Custom_Regression_Prediction",
work_path / "14_visualization", Path(resolve_subdir(self.work_dir, 'visualization')),
work_path, work_path,
] ]
detected_dir = None detected_dir = None
@ -1611,7 +1618,7 @@ class Step12VizPanel(QWidget):
print(f"扫描工作目录: {work_path}") print(f"扫描工作目录: {work_path}")
self.image_tree.scan_directory(str(work_path)) self.image_tree.scan_directory(str(work_path))
self._setup_prediction_output_dirs(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(): if viz_dir.exists():
image_files = list(viz_dir.glob("**/*.png")) + list(viz_dir.glob("**/*.jpg")) image_files = list(viz_dir.glob("**/*.png")) + list(viz_dir.glob("**/*.jpg"))
if image_files: if image_files:
@ -1620,20 +1627,17 @@ class Step12VizPanel(QWidget):
def _setup_prediction_output_dirs(self, work_path: Path): def _setup_prediction_output_dirs(self, work_path: Path):
"""设置三个预测步骤的默认输出目录""" """设置三个预测步骤的默认输出目录"""
try: try:
base_prediction_dir = work_path / "11_12_13_predictions" base_prediction_dir = Path(resolve_subdir(str(work_path), 'prediction_dir'))
ml_dir = work_path / "9_ML_Prediction" ml_dir = Path(resolve_subdir(str(work_path), 'ml_prediction'))
reg_dir = base_prediction_dir / "Regression_Model_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) ml_dir.mkdir(parents=True, exist_ok=True)
reg_dir.mkdir(parents=True, exist_ok=True) reg_dir.mkdir(parents=True, exist_ok=True)
custom_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/step11_panel/step12_panel 在 Step12VizPanel 上不存在,是死代码。
self.step11_ml_panel.output_file.set_path(str(ml_dir)) # 三个目录的真实默认值在用户首次浏览 / 自动填充时由各 panel 自己的 _get_default_work_dir 路径产出。
if hasattr(self, 'step11_panel') and hasattr(self.step11_panel, 'output_file'): # 这里仅做目录创建 + 提示输出,便于用户在工作目录树中能看到预测输出位置。
self.step11_panel.output_file.set_path(str(reg_dir)) print(f"预测输出目录已创建:\n ML: {ml_dir}\n Reg: {reg_dir}\n Custom: {custom_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}")
except Exception as e: except Exception as e:
print(f"设置预测输出目录失败: {e}") print(f"设置预测输出目录失败: {e}")
@ -1780,7 +1784,7 @@ class Step12VizPanel(QWidget):
QMessageBox.warning(self, "警告", "请先选择工作目录!") QMessageBox.warning(self, "警告", "请先选择工作目录!")
return return
work_path = Path(self.work_dir) 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_dir2 = viz_dir / "boxplots"
viz_dir3 = viz_dir / "scatter_plots" viz_dir3 = viz_dir / "scatter_plots"
if not viz_dir.exists(): if not viz_dir.exists():

View File

@ -5,6 +5,14 @@ Step12 面板 - 自定义回归预测
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QWidget, QVBoxLayout, QGroupBox,
@ -94,37 +102,24 @@ class Step12Panel(QWidget):
main_window = self.window() main_window = self.window()
# 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 # 1. 尝试从 Step7(水质光谱指数)界面读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step7_panel'): # 修复张冠李戴:原 main_window.step7_panel 不存在,真实属性是 step7_index_panel
step7_widget = getattr(main_window.step7_panel, 'output_file', None) step7_output_path = get_step_output_path(
step7_output_path = "" main_window, 'sampling_csv', work_dir=self.work_dir,
if hasattr(step7_widget, 'get_path'): widget_attr='output_file', fallback_key='step7_index',
step7_output_path = step7_widget.get_path() or "" )
elif hasattr(step7_widget, 'text'):
step7_output_path = step7_widget.text() or ""
if 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() existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip(): if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path) self.sampling_csv_file.set_path(step7_output_path)
# 2. 尝试从 Step9 界面读取自定义回归模型目录 # 2. 尝试从 Step8非经验回归/自定义回归源)读取模型目录
if main_window and hasattr(main_window, 'step12_panel'): # 修复张冠李戴:原 main_window.step12_panel 不存在;按代码原意是 step9 的 output_dir
step9_widget = getattr(main_window.step9_panel, 'output_dir', None) step9_models_dir = get_step_output_path(
step9_models_dir = "" main_window, 'models_dir', work_dir=self.work_dir,
if hasattr(step9_widget, 'get_path'): widget_attr='output_dir', fallback_key='step8_ml_train',
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: 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() existing_models = self.regression_models_dir.get_path()
if not existing_models or not existing_models.strip(): if not existing_models or not existing_models.strip():
self.regression_models_dir.set_path(step9_models_dir) self.regression_models_dir.set_path(step9_models_dir)
@ -133,12 +128,12 @@ class Step12Panel(QWidget):
if self.work_dir: if self.work_dir:
models_dir = self.regression_models_dir.get_path().strip() models_dir = self.regression_models_dir.get_path().strip()
if not models_dir: 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) self.regression_models_dir.set_path(default_models_dir)
# 4. 自动填充输出目录(自定义回归预测目录) # 4. 自动填充输出目录(自定义回归预测目录)
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir_widget.get_path() existing_out = self.output_dir_widget.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -161,7 +156,7 @@ class Step12Panel(QWidget):
"""浏览回归模型目录""" """浏览回归模型目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "13_Custom_Regression") default = resolve_subdir(default, 'custom_regression')
dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", default) dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", default)
if dir_path: if dir_path:
self.regression_models_dir.set_path(dir_path) self.regression_models_dir.set_path(dir_path)

View File

@ -5,10 +5,17 @@ ReportGenerationPanel - Word 分析报告生成面板
""" """
import os import os
import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import Optional 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.QtCore import Qt, QThread, pyqtSignal, QSettings
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
@ -206,7 +213,7 @@ class Step13ReportPanel(QWidget):
def browse_output_dir(self): def browse_output_dir(self):
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "14_visualization") default = resolve_subdir(default, 'visualization')
d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default) d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default)
if d: if d:
self.output_dir_edit.setText(d) self.output_dir_edit.setText(d)

View File

@ -5,10 +5,17 @@ Step14 面板 - 分布图生成
""" """
import os import os
import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import List, Optional 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.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout,
@ -372,7 +379,7 @@ class Step14Panel(QWidget):
def browse_prediction_csv_dir(self): def browse_prediction_csv_dir(self):
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "11_12_13_predictions") default = resolve_subdir(default, 'prediction_dir')
d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default) d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default)
if d: if d:
self.prediction_csv_dir_edit.setText(d) self.prediction_csv_dir_edit.setText(d)
@ -392,7 +399,7 @@ class Step14Panel(QWidget):
"""浏览 GeoTIFF 文件夹(批量模式)""" """浏览 GeoTIFF 文件夹(批量模式)"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "10_WaterIndex_Images") default = resolve_subdir(default, 'watercolor')
d = QFileDialog.getExistingDirectory( d = QFileDialog.getExistingDirectory(
self, "选择水色指数 GeoTIFF 文件夹", default self, "选择水色指数 GeoTIFF 文件夹", default
) )
@ -510,49 +517,35 @@ class Step14Panel(QWidget):
if not main_window: if not main_window:
return return
# 1. 尝试从 Step8 界面读取机器学习预测输出目录(最优先) # 1. 优先:从 Step9机器学习预测)读输出目录9_ML_Prediction 子目录
# 修复张冠李戴:原 main_window.step11_prediction_panel 不存在
pred_dir = None pred_dir = None
if hasattr(main_window, 'step11_prediction_panel'): step10_output = get_step_output_path(
step8_widget = getattr(main_window.step11_prediction_panel, 'output_file', None) main_window, 'step11_ml_prediction', work_dir=self.work_dir,
step10_output = "" widget_attr='output_file', fallback_key='step9_ml_predict',
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: 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) base_pred_dir = str(Path(step10_output).parent)
ml_pred_dir = Path(base_pred_dir) / "9_ML_Prediction" 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 pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir
# 2. 备选:从 Step11 界面读取非经验预测输出目录 # 2. 备选:从 Step8非经验预测)读输出目录
if not pred_dir and hasattr(main_window, 'step11_panel'): # 修复张冠李戴:原 main_window.step11_panel 不存在
step8_5_widget = getattr(main_window.step11_panel, 'output_file', None) if not pred_dir:
step8_5_output = "" step8_5_output = get_step_output_path(
if hasattr(step8_5_widget, 'get_path'): main_window, 'step12_regression_prediction', work_dir=self.work_dir,
step8_5_output = step8_5_widget.get_path() or "" widget_attr='output_file', fallback_key='step8_ml_train',
elif hasattr(step8_5_widget, 'text'): )
step8_5_output = step8_5_widget.text() or ""
if step8_5_output: 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) pred_dir = str(Path(step8_5_output).parent)
# 3. 备选:从 Step12 界面读取自定义回归预测输出目录 # 3. 备选:从 Step13 panel自定义回归)读输出目录
if not pred_dir and hasattr(main_window, 'step12_panel'): # 修复张冠李戴:原 main_window.step12_panel 不存在
step8_75_widget = getattr(main_window.step12_panel, 'output_dir_widget', None) if not pred_dir:
step8_75_output = "" step8_75_output = get_step_output_path(
if hasattr(step8_75_widget, 'get_path'): main_window, 'step13_custom_regression', work_dir=self.work_dir,
step8_75_output = step8_75_widget.get_path() or "" widget_attr='output_dir_widget', fallback_key='custom_regression',
elif hasattr(step8_75_widget, 'text'): )
step8_75_output = step8_75_widget.text() or ""
if step8_75_output: if step8_75_output:
pred_dir = step8_75_output pred_dir = step8_75_output
@ -566,7 +559,7 @@ class Step14Panel(QWidget):
# 4. 自动填充输出目录14_visualization # 4. 自动填充输出目录14_visualization
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.get_path() existing_out = self.output_dir.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -612,7 +605,7 @@ class Step14Panel(QWidget):
"""浏览输出目录""" """浏览输出目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "14_visualization") default = resolve_subdir(default, 'visualization')
dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default) dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default)
if dir_path: if dir_path:
self.output_dir.set_path(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() out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir: 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) os.makedirs(out_dir, exist_ok=True)
self.run_button.setEnabled(False) self.run_button.setEnabled(False)
@ -760,7 +753,7 @@ class Step14Panel(QWidget):
# 构造输出路径 # 构造输出路径
out_dir = (self.output_dir.get_path() or "").strip() out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir: 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) os.makedirs(out_dir, exist_ok=True)
tif_stem = Path(geotiff_path).stem tif_stem = Path(geotiff_path).stem
chinese_name = mapper._get_chinese_title(tif_stem) chinese_name = mapper._get_chinese_title(tif_stem)

View File

@ -5,6 +5,14 @@ Step1 面板 - 水域掩膜生成
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel,
@ -197,8 +205,8 @@ class Step1Panel(QWidget):
if not hasattr(self, 'work_dir') or not self.work_dir: if not hasattr(self, 'work_dir') or not self.work_dir:
return return
# 生成输出掩膜的完整路径 # 生成输出掩膜的完整路径(用 resolve_subdir 归一化,消除硬编码)
output_dir = os.path.join(self.work_dir, "1_water_mask") output_dir = resolve_subdir(self.work_dir, 'water_mask')
os.makedirs(output_dir, exist_ok=True) # 确保目录存在 os.makedirs(output_dir, exist_ok=True) # 确保目录存在
# 统一使用正斜杠,避免 \ 和 / 混用 # 统一使用正斜杠,避免 \ 和 / 混用

View File

@ -5,6 +5,14 @@ Step2 面板 - 耀斑区域识别
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout,
@ -187,7 +195,7 @@ class Step2Panel(QWidget):
# 3. 自动填充输出路径(基于工作目录) # 3. 自动填充输出路径(基于工作目录)
if self.work_dir: if self.work_dir:
# 生成输出耀斑掩膜的标准路径workspace/2_Glint_Detection/severe_glint_area.dat # 生成输出耀斑掩膜的标准路径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) os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "severe_glint_area.dat").replace('\\', '/') default_output_path = os.path.join(output_dir, "severe_glint_area.dat").replace('\\', '/')
self.output_file.set_path(default_output_path) self.output_file.set_path(default_output_path)

View File

@ -5,6 +5,14 @@ Step3 面板 - 耀斑去除
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout,
@ -299,7 +307,7 @@ class Step3Panel(QWidget):
# 自动填充输出路径(基于工作目录) # 自动填充输出路径(基于工作目录)
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "deglint_image.bsq").replace('\\', '/') default_output_path = os.path.join(output_dir, "deglint_image.bsq").replace('\\', '/')
self.output_file.set_path(default_output_path) self.output_file.set_path(default_output_path)

View File

@ -5,6 +5,14 @@ Step4 面板 - 采样点布设
""" """
import os 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.QtCore import QTimer
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -186,7 +194,7 @@ class Step4SamplingPanel(QWidget):
water_mask_path = main_window.step1_panel.output_file.get_path() water_mask_path = main_window.step1_panel.output_file.get_path()
# 备选:扫描 1_water_mask 目录下的 .dat 文件 # 备选:扫描 1_water_mask 目录下的 .dat 文件
if not water_mask_path and self.work_dir: 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): if os.path.isdir(mask_dir):
dat_files = [f for f in os.listdir(mask_dir) if f.lower().endswith('.dat')] dat_files = [f for f in os.listdir(mask_dir) if f.lower().endswith('.dat')]
if dat_files: if dat_files:
@ -213,7 +221,7 @@ class Step4SamplingPanel(QWidget):
# 3. 自动填充输出路径(绝对路径) # 3. 自动填充输出路径(绝对路径)
if self.work_dir: 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) os.makedirs(os.path.dirname(output_path), exist_ok=True)
self.output_file.set_path(output_path.replace('\\', '/')) self.output_file.set_path(output_path.replace('\\', '/'))

View File

@ -5,6 +5,14 @@ Step4 面板 - 数据预处理
""" """
import os 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 import pandas as pd
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -127,7 +135,7 @@ class Step5CleanPanel(QWidget):
self.work_dir = None self.work_dir = None
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/') default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/')
self.output_file.set_path(default_output_path) self.output_file.set_path(default_output_path)

View File

@ -5,6 +5,14 @@ Step6 面板 - 光谱特征提取
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel,
@ -193,7 +201,7 @@ class Step6FeaturePanel(QWidget):
# 4. 自动填充输出路径(基于工作目录) # 4. 自动填充输出路径(基于工作目录)
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/') default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
self.output_file.set_path(default_output_path) self.output_file.set_path(default_output_path)

View File

@ -10,6 +10,12 @@ import pandas as pd
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout,
QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox, QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
@ -261,7 +267,7 @@ class Step7IndexPanel(QWidget):
work_dir = self._get_work_dir() work_dir = self._get_work_dir()
if 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) os.makedirs(track_a_dir, exist_ok=True)
config['output_file'] = os.path.join(track_a_dir, "training_spectra_indices.csv").replace('\\', '/') config['output_file'] = os.path.join(track_a_dir, "training_spectra_indices.csv").replace('\\', '/')

View File

@ -5,6 +5,14 @@ Step8 面板 - 机器学习建模
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
@ -271,7 +279,7 @@ class Step8MlTrainPanel(QWidget):
if not initial_dir or not os.path.isdir(initial_dir): if not initial_dir or not os.path.isdir(initial_dir):
# 默认定位到 indices 目录 # 默认定位到 indices 目录
work_dir = self._get_default_work_dir() 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): if initial_dir and not os.path.isdir(initial_dir):
os.makedirs(initial_dir, exist_ok=True) os.makedirs(initial_dir, exist_ok=True)
@ -350,18 +358,19 @@ class Step8MlTrainPanel(QWidget):
self.work_dir = None self.work_dir = None
# 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径 # 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径
# 修复张冠李戴:原代码 main_window.step5_panel 不存在,正确属性是 step5_clean_panel
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'step5_panel'): step5_output = get_step_output_path(
# 优先直接从 Step5 的输出 widget 读取 main_window, 'training_spectra_csv', work_dir=self.work_dir,
step5_output = main_window.step5_panel.output_file.get_path() widget_attr='output_file', fallback_key='step6_feature',
)
if step5_output: 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) self.training_csv_file.set_path(step5_output)
elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'): else:
# 回退:从 Step5 的 config 字典中查找可能的键名 # 回退:从 Step5 的 config 字典中查找可能的键名
step5_cfg = main_window.step5_panel.get_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_csv = (
step5_cfg.get('training_csv_path') step5_cfg.get('training_csv_path')
or step5_cfg.get('output_file') or step5_cfg.get('output_file')
@ -369,7 +378,6 @@ class Step8MlTrainPanel(QWidget):
or step5_cfg.get('output_csv') or step5_cfg.get('output_csv')
) )
if step5_csv: if step5_csv:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_csv): if not os.path.isabs(step5_csv):
step5_csv = os.path.join(self.work_dir or '', step5_csv).replace('\\', '/') step5_csv = os.path.join(self.work_dir or '', step5_csv).replace('\\', '/')
self.training_csv_file.set_path(step5_csv) 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 # 输入是 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 # 输入是 sampling_spectra.csv → 输出 {work_dir}/7_Water_Quality_Indices/sampling_spectra_indices.csv
if self.work_dir: 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) os.makedirs(indices_dir, exist_ok=True)
training_csv = self.training_csv_file.get_path() training_csv = self.training_csv_file.get_path()
if training_csv: if training_csv:

View File

@ -5,8 +5,15 @@ Step8 面板 - 非经验统计回归建模
""" """
import os import os
import sys
from pathlib import Path 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QCheckBox, QSpinBox, QPushButton, QHBoxLayout, QLabel, QCheckBox, QSpinBox, QPushButton,
@ -231,25 +238,19 @@ class Step8NonEmpiricalPanel(QWidget):
# 借用父组件的 window() 方法,安全绕过当前类的命名冲突 # 借用父组件的 window() 方法,安全绕过当前类的命名冲突
parent_widget = self.parentWidget() parent_widget = self.parentWidget()
main_window = parent_widget.window() if parent_widget else None main_window = parent_widget.window() if parent_widget else None
if main_window and hasattr(main_window, 'step5_panel'): # 1. 尝试从 Step5数据清洗读取训练光谱 CSV修复张冠李戴原 main_window.step5_panel 不存在)
step5_widget = getattr(main_window.step5_panel, 'output_file', None) step5_output_path = get_step_output_path(
step5_output_path = "" main_window, 'training_spectra_csv', work_dir=self.work_dir,
if hasattr(step5_widget, 'get_path'): widget_attr='output_file', fallback_key='step6_feature',
step5_output_path = step5_widget.get_path() or "" )
elif hasattr(step5_widget, 'text'):
step5_output_path = step5_widget.text() or ""
if step5_output_path: 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() existing = self.training_csv_file.get_path()
if not existing or not existing.strip(): if not existing or not existing.strip():
self.training_csv_file.set_path(step5_output_path) self.training_csv_file.set_path(step5_output_path)
# 2. 自动填充输出目录8_Regression_Modeling # 2. 自动填充输出目录8_Regression_Modeling
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.get_path() existing_out = self.output_dir.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -274,7 +275,7 @@ class Step8NonEmpiricalPanel(QWidget):
"""浏览输出目录""" """浏览输出目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "8_Regression_Modeling") default = resolve_subdir(default, 'regression_modeling')
dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", default) dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", default)
if dir_path: if dir_path:
self.output_dir.set_path(dir_path) self.output_dir.set_path(dir_path)

View File

@ -5,6 +5,13 @@ Step8 面板 - QAA 物理反演(非经验模型)
""" """
import os 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
@ -143,7 +150,7 @@ class Step8QAAPanel(QWidget):
if not initial_dir or not os.path.isdir(initial_dir): if not initial_dir or not os.path.isdir(initial_dir):
work_dir = self._get_default_work_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): if initial_dir and not os.path.isdir(initial_dir):
os.makedirs(initial_dir, exist_ok=True) os.makedirs(initial_dir, exist_ok=True)
@ -198,7 +205,7 @@ class Step8QAAPanel(QWidget):
self.spectrum_csv_file.set_path(step5_output) self.spectrum_csv_file.set_path(step5_output)
if self.work_dir: 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) os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/') output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')
self.output_path.set_path(output_path) self.output_path.set_path(output_path)
@ -228,7 +235,7 @@ class Step8QAAPanel(QWidget):
if not output_path: if not output_path:
work_dir = self._get_default_work_dir() 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): if qaa_dir and not os.path.isdir(qaa_dir):
os.makedirs(qaa_dir, exist_ok=True) os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/') output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')

View File

@ -5,8 +5,15 @@ Step11 面板 - 机器学习预测
""" """
import os import os
import sys
from pathlib import Path 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 ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QWidget, QVBoxLayout, QGroupBox, QFormLayout,
QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox, QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox,
@ -190,7 +197,7 @@ class Step9MlPredictPanel(QWidget):
"""浏览模型母文件夹,自动扫描子目录中的 .joblib 文件""" """浏览模型母文件夹,自动扫描子目录中的 .joblib 文件"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "9_ML_Prediction") default = resolve_subdir(default, 'ml_prediction')
dir_path = QFileDialog.getExistingDirectory( dir_path = QFileDialog.getExistingDirectory(
self, self,
"选择模型母文件夹", "选择模型母文件夹",
@ -334,25 +341,20 @@ class Step9MlPredictPanel(QWidget):
if not existing or not existing.strip(): if not existing or not existing.strip():
self.sampling_csv_file.set_path(step4_output_path) self.sampling_csv_file.set_path(step4_output_path)
# 2. 尝试从 Step9(监督建模)读取模型目录 # 2. 尝试从 Step8(监督建模)读取模型目录(修复张冠李戴:原代码 main_window.step9_panel 不存在)
if main_window and hasattr(main_window, 'step9_panel'): step8_models_dir = get_step_output_path(
step9_widget = getattr(main_window.step9_panel, 'output_dir', None) main_window, 'models_dir', work_dir=self.work_dir,
step9_models_dir = "" widget_attr='output_dir', fallback_key='step8_ml_train',
if hasattr(step9_widget, 'get_path'): )
step9_models_dir = step9_widget.get_path() or "" if step8_models_dir:
elif hasattr(step9_widget, 'text'):
step9_models_dir = step9_widget.text() or ""
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() existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip(): if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step9_models_dir) self.models_dir_file.set_path(step8_models_dir)
# 3. 自动填充输出路径(机器学习预测目录) # 3. 自动填充输出路径(机器学习预测目录,归属 step9 → 9_ML_Prediction
# 注9_ML_Prediction 是 prediction_dir 的子目录,用本地约定
if self.work_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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path() existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -375,7 +377,7 @@ class Step9MlPredictPanel(QWidget):
"""浏览模型目录""" """浏览模型目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "9_ML_Prediction") default = resolve_subdir(default, 'ml_prediction')
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default) dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path: if dir_path:
self.models_dir_file.set_path(dir_path) self.models_dir_file.set_path(dir_path)