Compare commits

...

3 Commits

16 changed files with 567 additions and 174 deletions

View File

@ -2105,7 +2105,7 @@ class WaterQualityInversionPipeline:
training_spectra_path: Optional[str] = None, training_spectra_path: Optional[str] = None,
formula_csv_file: Optional[str] = None, formula_csv_file: Optional[str] = None,
formula_names: Optional[List[str]] = None, formula_names: Optional[List[str]] = None,
output_filename: str = "water_quality_indices.csv", output_file: Optional[str] = None,
enabled: bool = True, enabled: bool = True,
skip_dependency_check: bool = False) -> str: skip_dependency_check: bool = False) -> str:
""" """
@ -2117,7 +2117,7 @@ class WaterQualityInversionPipeline:
training_spectra_path: 训练光谱数据CSV路径如果为None使用步骤5的结果 training_spectra_path: 训练光谱数据CSV路径如果为None使用步骤5的结果
formula_csv_file: 公式CSV文件路径包含公式名称和具体公式 formula_csv_file: 公式CSV文件路径包含公式名称和具体公式
formula_names: 要计算的公式名称列表如果为None则计算所有公式 formula_names: 要计算的公式名称列表如果为None则计算所有公式
output_filename: 输出文件 output_file: 输出文件完整路径支持绝对路径如果为None则使用默认路径
Returns: Returns:
包含计算结果的新CSV文件路径 包含计算结果的新CSV文件路径
@ -2148,8 +2148,12 @@ class WaterQualityInversionPipeline:
if formula_csv_file is None: if formula_csv_file is None:
raise ValueError("必须提供formula_csv_file参数包含水质指数公式") raise ValueError("必须提供formula_csv_file参数包含水质指数公式")
output_path = str(self.indices_dir / output_filename) # 支持绝对路径output_file 完整路径;否则 fallback 到 indices_dir + 默认文件名
if output_file:
output_path = str(Path(output_file))
else:
output_path = str(self.indices_dir / "water_quality_indices.csv")
# 如果文件已存在且配置了跳过机制,则直接复用 # 如果文件已存在且配置了跳过机制,则直接复用
if Path(output_path).exists(): if Path(output_path).exists():

View File

@ -12,6 +12,67 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
class DirSelectWidget(QWidget):
"""目录选择组件"""
def __init__(self, label_text, parent=None):
"""
初始化目录选择组件
Args:
label_text: 标签文本
parent: 父控件
"""
super().__init__(parent)
self.init_ui(label_text)
def init_ui(self, label_text):
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.label = QLabel(label_text)
self.label.setMinimumWidth(120)
self.line_edit = QLineEdit()
self.line_edit.setPlaceholderText("请选择目录...")
self.browse_btn = QPushButton("浏览...")
self.browse_btn.setMaximumWidth(80)
self.browse_btn.clicked.connect(self.browse_dir)
layout.addWidget(self.label)
layout.addWidget(self.line_edit, 1)
layout.addWidget(self.browse_btn)
self.setLayout(layout)
def browse_dir(self):
"""浏览目录 - 智能记忆上次选择位置"""
current_text = self.line_edit.text().strip()
initial_dir = ""
# 最高优先级:输入框已有路径存在
if current_text:
if os.path.isdir(current_text):
initial_dir = current_text
else:
dir_path = os.path.dirname(current_text)
if dir_path and os.path.exists(dir_path):
initial_dir = dir_path
# 调用目录选择对话框
dir_path = QFileDialog.getExistingDirectory(
self, "选择目录", initial_dir
)
if dir_path:
self.line_edit.setText(dir_path)
def get_path(self):
"""获取路径"""
return self.line_edit.text()
def set_path(self, path):
"""设置路径"""
self.line_edit.setText(str(path))
class FileSelectWidget(QWidget): class FileSelectWidget(QWidget):
"""文件选择组件""" """文件选择组件"""
def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None): def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None):
@ -49,14 +110,18 @@ class FileSelectWidget(QWidget):
self.setLayout(layout) self.setLayout(layout)
def browse_file(self): def browse_file(self):
"""浏览文件""" """浏览文件 - 智能记忆上次选择位置"""
current_text = self.line_edit.text().strip() current_text = self.line_edit.text().strip()
initial_dir = "" initial_dir = ""
# 最高优先级:输入框已有路径存在
if current_text: if current_text:
dir_path = os.path.dirname(current_text) if os.path.isdir(current_text):
if dir_path and os.path.exists(dir_path): initial_dir = current_text
initial_dir = dir_path else:
dir_path = os.path.dirname(current_text)
if dir_path and os.path.exists(dir_path):
initial_dir = dir_path
if self.mode == "save": if self.mode == "save":
file_path, _ = QFileDialog.getSaveFileName( file_path, _ = QFileDialog.getSaveFileName(

View File

@ -179,6 +179,9 @@ class Step2Panel(QWidget):
# 填充获取到的路径 # 填充获取到的路径
if mask_path: if mask_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(mask_path):
mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
self.water_mask_file.set_path(mask_path) self.water_mask_file.set_path(mask_path)
# 3. 自动填充输出路径(基于工作目录) # 3. 自动填充输出路径(基于工作目录)

View File

@ -294,6 +294,9 @@ class Step3Panel(QWidget):
mask_path = main_window.step1_panel.mask_file.get_path() mask_path = main_window.step1_panel.mask_file.get_path()
if mask_path: if mask_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(mask_path):
mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
self.water_mask_file.set_path(mask_path) self.water_mask_file.set_path(mask_path)
# 自动填充输出路径(基于工作目录) # 自动填充输出路径(基于工作目录)

View File

@ -85,11 +85,10 @@ class Step5_5Panel(QWidget):
output_group = QGroupBox("输出设置") output_group = QGroupBox("输出设置")
output_layout = QVBoxLayout() output_layout = QVBoxLayout()
output_hbox = QHBoxLayout() self.output_file_widget = FileSelectWidget(
output_hbox.addWidget(QLabel("输出文件名:")) "输出文件:", "CSV Files (*.csv)", mode="save"
self.output_filename = QLineEdit("water_quality_indices.csv") )
output_hbox.addWidget(self.output_filename) output_layout.addWidget(self.output_file_widget)
output_layout.addLayout(output_hbox)
output_group.setLayout(output_layout) output_group.setLayout(output_layout)
main_layout.addWidget(output_group) main_layout.addWidget(output_group)
@ -258,11 +257,12 @@ class Step5_5Panel(QWidget):
name for name, checkbox in self.index_checkboxes.items() name for name, checkbox in self.index_checkboxes.items()
if checkbox.isChecked() if checkbox.isChecked()
] ]
output_path = self.output_file_widget.get_path()
return { return {
'training_spectra_path': self.training_data_widget.get_path() or None, 'training_spectra_path': self.training_data_widget.get_path() or None,
'formula_csv_file': self.formula_csv_widget.get_path() or None, 'formula_csv_file': self.formula_csv_widget.get_path() or None,
'formula_names': selected, 'formula_names': selected,
'output_filename': self.output_filename.text().strip() or "water_quality_indices.csv", 'output_file': output_path or None,
'enabled': self.enable_checkbox.isChecked() 'enabled': self.enable_checkbox.isChecked()
} }
@ -273,7 +273,6 @@ class Step5_5Panel(QWidget):
if 'formula_csv_file' in config: if 'formula_csv_file' in config:
self.formula_csv_widget.set_path(config['formula_csv_file']) self.formula_csv_widget.set_path(config['formula_csv_file'])
# 设置CSV路径后自动刷新公式信息
self.refresh_formulas() self.refresh_formulas()
if 'formula_names' in config: if 'formula_names' in config:
@ -281,8 +280,10 @@ class Step5_5Panel(QWidget):
for name, checkbox in self.index_checkboxes.items(): for name, checkbox in self.index_checkboxes.items():
checkbox.setChecked(name in selected_formulas) checkbox.setChecked(name in selected_formulas)
if 'output_filename' in config: if 'output_file' in config and config['output_file']:
self.output_filename.setText(config['output_filename']) self.output_file_widget.set_path(config['output_file'])
elif 'output_filename' in config and config['output_filename']:
self.output_file_widget.set_path(config['output_filename'])
if 'enabled' in config: if 'enabled' in config:
self.enable_checkbox.setChecked(config['enabled']) self.enable_checkbox.setChecked(config['enabled'])
@ -308,11 +309,17 @@ class Step5_5Panel(QWidget):
# 优先直接从 Step5 的输出 widget 读取(已运行的最新输出) # 优先直接从 Step5 的输出 widget 读取(已运行的最新输出)
step5_output = main_window.step5_panel.output_file.get_path() step5_output = main_window.step5_panel.output_file.get_path()
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_data_widget.set_path(step5_output) self.training_data_widget.set_path(step5_output)
else: else:
# 退而求其次,使用 Step5 的输入 CSV # 退而求其次,使用 Step5 的输入 CSV
step5_csv = main_window.step5_panel.csv_file.get_path() step5_csv = main_window.step5_panel.csv_file.get_path()
if step5_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_data_widget.set_path(step5_csv) self.training_data_widget.set_path(step5_csv)
# 如果上述都没找到,尝试从 pipeline.step_outputs 回退 # 如果上述都没找到,尝试从 pipeline.step_outputs 回退
@ -322,9 +329,11 @@ class Step5_5Panel(QWidget):
if training_path: if training_path:
self.training_data_widget.set_path(training_path) self.training_data_widget.set_path(training_path)
# 2. 自动填输出文件名(通用逻辑,由 run_step 根据输入文件名动态覆盖) # 2. 自动填输出文件的绝对路径
# 核心方法只接受文件名,不接受完整路径。 if self.work_dir:
# 保持默认值run_step 会根据输入自动填入 _indices.csv 后缀 output_abs = os.path.join(self.work_dir, "6_water_quality_indices",
"training_spectra_indices.csv").replace('\\', '/')
self.output_file_widget.set_path(output_abs)
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
return self.enable_checkbox.isChecked() return self.enable_checkbox.isChecked()
@ -342,7 +351,7 @@ class Step5_5Panel(QWidget):
def run_step(self): def run_step(self):
"""独立运行步骤5.5:计算水质指数。 """独立运行步骤5.5:计算水质指数。
动态根据输入 CSV 文件名生成输出文件名,自动填入 output_filename 文本框 动态根据输入 CSV 文件名生成输出文件名,自动填入 output_file_widget
例如training_spectra.csv → training_spectra_indices.csv 例如training_spectra.csv → training_spectra_indices.csv
sampling_spectra.csv → sampling_spectra_indices.csv sampling_spectra.csv → sampling_spectra_indices.csv
""" """
@ -363,10 +372,18 @@ class Step5_5Panel(QWidget):
QMessageBox.warning(self, "输入验证失败", "公式CSV文件不存在") QMessageBox.warning(self, "输入验证失败", "公式CSV文件不存在")
return return
# 动态生成输出文件:自动拼接 _indices 后缀 # 动态生成输出文件:自动拼接 _indices 后缀
input_name = Path(training_csv_path).stem input_name = Path(training_csv_path).stem
dynamic_output = f"{input_name}_indices.csv" dynamic_output = f"{input_name}_indices.csv"
self.output_filename.setText(dynamic_output)
# 合成完整绝对路径(优先使用 work_dir其次从 training_csv_path 推导)
work_dir = getattr(self, 'work_dir', None)
if work_dir:
dynamic_output = os.path.join(
work_dir, "6_water_quality_indices", dynamic_output
).replace('\\', '/')
self.output_file_widget.set_path(dynamic_output)
# 获取配置 # 获取配置
config = self.get_config() config = self.get_config()

View File

@ -170,9 +170,15 @@ class Step5Panel(QWidget):
mask_path = main_window.step1_panel.output_file.get_path() mask_path = main_window.step1_panel.output_file.get_path()
else: else:
mask_path = main_window.step1_panel.mask_file.get_path() mask_path = main_window.step1_panel.mask_file.get_path()
# 若为相对路径,使用 work_dir 合成为绝对路径
if mask_path and not os.path.isabs(mask_path):
mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
# 填充水体掩膜路径 # 填充水体掩膜路径
if mask_path: if mask_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(mask_path):
mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
self.water_mask_file.set_path(mask_path) self.water_mask_file.set_path(mask_path)
# 3. 尝试从 Step2 界面读取耀斑掩膜路径 # 3. 尝试从 Step2 界面读取耀斑掩膜路径
@ -180,6 +186,9 @@ class Step5Panel(QWidget):
if hasattr(main_window, 'step2_panel'): if hasattr(main_window, 'step2_panel'):
glint_path = main_window.step2_panel.output_file.get_path() glint_path = main_window.step2_panel.output_file.get_path()
if glint_path: if glint_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(glint_path):
glint_path = os.path.join(self.work_dir or '', glint_path).replace('\\', '/')
self.glint_mask_file.set_path(glint_path) self.glint_mask_file.set_path(glint_path)
# 4. 自动填充输出路径(基于工作目录) # 4. 自动填充输出路径(基于工作目录)
@ -196,6 +205,9 @@ class Step5Panel(QWidget):
if main_window and hasattr(main_window, 'step4_panel'): if main_window and hasattr(main_window, 'step4_panel'):
step4_output_path = main_window.step4_panel.output_file.get_path() step4_output_path = main_window.step4_panel.output_file.get_path()
if step4_output_path: if step4_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step4_output_path):
step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
existing_csv = self.csv_file.get_path() existing_csv = self.csv_file.get_path()
if not existing_csv or not existing_csv.strip(): if not existing_csv or not existing_csv.strip():
self.csv_file.set_path(step4_output_path) self.csv_file.set_path(step4_output_path)

View File

@ -102,11 +102,11 @@ class Step6_5Panel(QWidget):
self.spectral_start_col.setValue(1) self.spectral_start_col.setValue(1)
params_layout.addRow("光谱起始列索引:", self.spectral_start_col) params_layout.addRow("光谱起始列索引:", self.spectral_start_col)
# 窗口大小 # 窗口大小 (变量名已修正,避免覆盖 QWidget.window)
self.window = QSpinBox() self.window_size_spinbox = QSpinBox()
self.window.setRange(1, 20) self.window_size_spinbox.setRange(1, 20)
self.window.setValue(5) self.window_size_spinbox.setValue(5)
params_layout.addRow("窗口大小:", self.window) params_layout.addRow("窗口大小:", self.window_size_spinbox)
params_group.setLayout(params_layout) params_group.setLayout(params_layout)
layout.addWidget(params_group) layout.addWidget(params_group)
@ -160,7 +160,7 @@ class Step6_5Panel(QWidget):
'algorithms': selected_algorithms, 'algorithms': selected_algorithms,
'value_cols': value_cols, 'value_cols': value_cols,
'spectral_start_col': self.spectral_start_col.value(), 'spectral_start_col': self.spectral_start_col.value(),
'window': self.window.value(), 'window': self.window_size_spinbox.value(),
'enabled': self.enable_checkbox.isChecked() 'enabled': self.enable_checkbox.isChecked()
} }
@ -205,7 +205,7 @@ class Step6_5Panel(QWidget):
self.spectral_start_col.setValue(config['spectral_start_col']) self.spectral_start_col.setValue(config['spectral_start_col'])
if 'window' in config: if 'window' in config:
self.window.setValue(config['window']) self.window_size_spinbox.setValue(config['window'])
if 'output_dir' in config: if 'output_dir' in config:
self.output_dir.set_path(config['output_dir']) self.output_dir.set_path(config['output_dir'])
if 'csv_path' in config: if 'csv_path' in config:
@ -218,35 +218,54 @@ class Step6_5Panel(QWidget):
work_dir: 工作目录路径 work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性) pipeline: Pipeline 实例(未使用,保留接口兼容性)
""" """
if work_dir: try:
self.work_dir = work_dir import traceback
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
# 1. 尝试从 Step5 界面读取训练光谱 CSV 路径 if work_dir:
main_window = self.window() self.work_dir = work_dir
if main_window and hasattr(main_window, 'step5_panel'): elif hasattr(self, 'work_dir') and self.work_dir:
step5_output_path = main_window.step5_panel.output_file.get_path() pass
if step5_output_path: else:
existing = self.training_csv_file.get_path() self.work_dir = None
if not existing or not existing.strip():
self.training_csv_file.set_path(step5_output_path)
# 2. 自动填充输出目录8_Regression_Modeling # 借用父组件的 window() 方法,安全绕过当前类的命名冲突
if self.work_dir: parent_widget = self.parentWidget()
output_dir = os.path.join(self.work_dir, "8_Regression_Modeling") main_window = parent_widget.window() if parent_widget else None
os.makedirs(output_dir, exist_ok=True) if main_window and hasattr(main_window, 'step5_panel'):
existing_out = self.output_dir.get_path() step5_widget = getattr(main_window.step5_panel, 'output_file', None)
if not existing_out or not existing_out.strip(): step5_output_path = ""
self.output_dir.set_path(output_dir) 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)
# 2. 自动填充输出目录8_Regression_Modeling
if self.work_dir:
output_dir = os.path.join(self.work_dir, "8_Regression_Modeling")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.get_path()
if not existing_out or not existing_out.strip():
self.output_dir.set_path(output_dir)
except Exception as e:
import traceback
print(f"{self.__class__.__name__}】自动填充失败,跳过: {e}")
traceback.print_exc()
def _get_default_work_dir(self): def _get_default_work_dir(self):
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取""" """获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
if hasattr(self, 'work_dir') and self.work_dir: if hasattr(self, 'work_dir') and self.work_dir:
return str(self.work_dir) return str(self.work_dir)
mw = self.window() # 借用父组件的 window() 方法,安全绕过当前类的命名冲突
parent_widget = self.parentWidget()
mw = parent_widget.window() if parent_widget else None
if mw and hasattr(mw, 'work_dir') and mw.work_dir: if mw and hasattr(mw, 'work_dir') and mw.work_dir:
return str(mw.work_dir) return str(mw.work_dir)
return "" return ""

View File

@ -148,7 +148,7 @@ class Step6_75Panel(QWidget):
output_layout = QFormLayout() output_layout = QFormLayout()
self.output_dir = QLineEdit() self.output_dir = QLineEdit()
self.output_dir.setText("9_Custom_Regression_Modeling") self.output_dir.setText("") # 路径由 update_from_config 根据 work_dir 自动填充
output_layout.addRow("输出目录名:", self.output_dir) output_layout.addRow("输出目录名:", self.output_dir)
output_group.setLayout(output_layout) output_group.setLayout(output_layout)
@ -299,6 +299,9 @@ class Step6_75Panel(QWidget):
if main_window and hasattr(main_window, 'step5_panel'): if main_window and hasattr(main_window, 'step5_panel'):
step5_output_path = main_window.step5_panel.output_file.get_path() step5_output_path = main_window.step5_panel.output_file.get_path()
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.csv_file.get_path() existing = self.csv_file.get_path()
if not existing or not existing.strip(): if not existing or not existing.strip():
self.csv_file.set_path(step5_output_path) self.csv_file.set_path(step5_output_path)

View File

@ -298,12 +298,15 @@ class Step6Panel(QWidget):
else: else:
self.work_dir = None self.work_dir = None
# 1. 尝试从 Step5 界面读取训练数据路径 # 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'step5_panel'): if hasattr(main_window, 'step5_panel'):
# 优先直接从 Step5 的输出 widget 读取 # 优先直接从 Step5 的输出 widget 读取
step5_output = main_window.step5_panel.output_file.get_path() step5_output = main_window.step5_panel.output_file.get_path()
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'): elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'):
# 回退:从 Step5 的 config 字典中查找可能的键名 # 回退:从 Step5 的 config 字典中查找可能的键名
@ -315,6 +318,9 @@ class Step6Panel(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):
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)
# 2. 自动填充输出文件路径(基于工作目录和输入文件名) # 2. 自动填充输出文件路径(基于工作目录和输入文件名)

View File

@ -145,6 +145,9 @@ class Step7Panel(QWidget):
deglint_path = main_window.step3_panel.output_file.get_path() deglint_path = main_window.step3_panel.output_file.get_path()
if deglint_path: if deglint_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(deglint_path):
deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/')
self.deglint_img_file.set_path(deglint_path) self.deglint_img_file.set_path(deglint_path)
# 2. 填充水域掩膜路径(优先从 pipeline.step_outputs 获取绝对路径) # 2. 填充水域掩膜路径(优先从 pipeline.step_outputs 获取绝对路径)
@ -161,6 +164,9 @@ class Step7Panel(QWidget):
water_mask_path = main_window.step1_panel.output_file.get_path() water_mask_path = main_window.step1_panel.output_file.get_path()
if water_mask_path: if water_mask_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(water_mask_path):
water_mask_path = os.path.join(self.work_dir or '', water_mask_path).replace('\\', '/')
self.water_mask_file.set_path(water_mask_path) self.water_mask_file.set_path(water_mask_path)
# 3. 自动填充输出路径(绝对路径) # 3. 自动填充输出路径(绝对路径)

View File

@ -102,6 +102,9 @@ class Step8_5Panel(QWidget):
if main_window and hasattr(main_window, 'step7_panel'): if main_window and hasattr(main_window, 'step7_panel'):
step7_output_path = main_window.step7_panel.output_file.get_path() step7_output_path = main_window.step7_panel.output_file.get_path()
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)
@ -110,6 +113,9 @@ class Step8_5Panel(QWidget):
if main_window and hasattr(main_window, 'step6_5_panel'): if main_window and hasattr(main_window, 'step6_5_panel'):
step6_5_models_dir = main_window.step6_5_panel.output_dir.get_path() step6_5_models_dir = main_window.step6_5_panel.output_dir.get_path()
if step6_5_models_dir: if step6_5_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step6_5_models_dir):
step6_5_models_dir = os.path.join(self.work_dir or '', step6_5_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path() existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip(): if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step6_5_models_dir) self.models_dir_file.set_path(step6_5_models_dir)

View File

@ -39,7 +39,7 @@ class Step8_75Panel(QWidget):
self.regression_models_dir.label.setText("回归模型目录:") self.regression_models_dir.label.setText("回归模型目录:")
self.regression_models_dir.browse_btn.clicked.disconnect() self.regression_models_dir.browse_btn.clicked.disconnect()
self.regression_models_dir.browse_btn.clicked.connect(self.browse_regression_models_dir) self.regression_models_dir.browse_btn.clicked.connect(self.browse_regression_models_dir)
self.regression_models_dir.set_path("9_Custom_Regression_Modeling") self.regression_models_dir.set_path("") # 路径由 update_from_config 根据 work_dir 自动填充
layout.addWidget(self.regression_models_dir) layout.addWidget(self.regression_models_dir)
# 公式CSV文件选择用于查找index_formula # 公式CSV文件选择用于查找index_formula
@ -95,6 +95,9 @@ class Step8_75Panel(QWidget):
if main_window and hasattr(main_window, 'step7_panel'): if main_window and hasattr(main_window, 'step7_panel'):
step7_output_path = main_window.step7_panel.output_file.get_path() step7_output_path = main_window.step7_panel.output_file.get_path()
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)
@ -103,11 +106,21 @@ class Step8_75Panel(QWidget):
if main_window and hasattr(main_window, 'step6_75_panel'): if main_window and hasattr(main_window, 'step6_75_panel'):
step6_75_models_dir = main_window.step6_75_panel.output_dir.text().strip() step6_75_models_dir = main_window.step6_75_panel.output_dir.text().strip()
if step6_75_models_dir: if step6_75_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step6_75_models_dir):
step6_75_models_dir = os.path.join(self.work_dir or '', step6_75_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(step6_75_models_dir) self.regression_models_dir.set_path(step6_75_models_dir)
# 3. 自动填充输出目录(自定义回归预测目录 # 3. 自动填充回归模型目录(如果 step6_75 未提供
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, "9_Custom_Regression_Modeling").replace('\\', '/')
self.regression_models_dir.set_path(default_models_dir)
# 4. 自动填充输出目录(自定义回归预测目录)
if self.work_dir: if self.work_dir:
output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Custom_Regression_Prediction") output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Custom_Regression_Prediction")
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)

View File

@ -99,6 +99,9 @@ class Step8Panel(QWidget):
if main_window and hasattr(main_window, 'step7_panel'): if main_window and hasattr(main_window, 'step7_panel'):
step7_output_path = main_window.step7_panel.output_file.get_path() step7_output_path = main_window.step7_panel.output_file.get_path()
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)
@ -107,6 +110,9 @@ class Step8Panel(QWidget):
if main_window and hasattr(main_window, 'step6_panel'): if main_window and hasattr(main_window, 'step6_panel'):
step6_models_dir = main_window.step6_panel.output_dir.get_path() step6_models_dir = main_window.step6_panel.output_dir.get_path()
if step6_models_dir: if step6_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step6_models_dir):
step6_models_dir = os.path.join(self.work_dir or '', step6_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path() existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip(): if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step6_models_dir) self.models_dir_file.set_path(step6_models_dir)

View File

@ -324,12 +324,18 @@ class Step9Panel(QWidget):
if hasattr(main_window, 'step8_panel'): if hasattr(main_window, 'step8_panel'):
step8_output = main_window.step8_panel.output_file.get_path() step8_output = main_window.step8_panel.output_file.get_path()
if step8_output: if step8_output:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step8_output):
step8_output = os.path.join(self.work_dir or '', step8_output).replace('\\', '/')
pred_dir = str(Path(step8_output).parent) pred_dir = str(Path(step8_output).parent)
# 2. 备选:从 Step8.5 界面读取非经验预测输出目录 # 2. 备选:从 Step8.5 界面读取非经验预测输出目录
if not pred_dir and hasattr(main_window, 'step8_5_panel'): if not pred_dir and hasattr(main_window, 'step8_5_panel'):
step8_5_output = main_window.step8_5_panel.output_file.get_path() step8_5_output = main_window.step8_5_panel.output_file.get_path()
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. 备选:从 Step8.75 界面读取自定义回归预测输出目录 # 3. 备选:从 Step8.75 界面读取自定义回归预测输出目录

View File

@ -493,22 +493,88 @@ class ChartViewerDialog(QDialog):
class ImageCategoryTree(QTreeWidget): class ImageCategoryTree(QTreeWidget):
"""图像分类目录树 - 按类别组织图像文件""" """图像分类目录树 - 按真实物理文件夹结构组织图像文件"""
CATEGORIES = [ # 文件名中文翻译映射key: 文件名前缀 → 中文显示名)
("模型评估", ["scatter", "regression", "validation", "r2", "rmse"], "📊"), NAME_MAPPING = {
("光谱分析", ["spectrum", "spectral", "band", "wavelength"], "📈"), "hsi_preview": "高光谱影像预览",
("统计图表", ["boxplot", "histogram", "heatmap", "statistics", "stats"], "📉"), "hsi_original": "原始高光谱影像",
("处理结果", ["mask", "glint", "deglint", "preview", "overlay", "water_mask"], "🖼️"), "hsi_deglint": "去耀斑高光谱影像",
("含量分布图", [], "📁"), "water_mask_overlay": "水域掩膜叠加图",
] "water_mask": "水域掩膜图",
"glint_mask": "耀斑掩膜预览",
"glint_overlay": "耀斑叠加对比图",
"deglint_comparison": "去耀斑前后对比",
"training_spectra": "训练光谱特征",
"spectrum_by_param": "参数光谱图",
"model_evaluation": "模型评估散点图",
"model_scatter": "模型散点图",
"regression": "回归分析图",
"validation": "验证结果图",
"spatial_distribution": "参数空间分布图",
"distribution_map": "分布图",
"thematic_map": "水质专题图",
"water_quality_map": "水质分布图",
"prediction_map": "预测结果图",
"inversion_map": "反演结果图",
"correlation_matrix": "特征相关性矩阵",
"feature_correlation": "特征相关性",
"sampling_point_map": "采样点分布图",
"sampling_points": "采样点图",
"point_locations": "采样位置图",
"boxplot": "箱线图",
"histogram": "直方图",
"statistics": "统计图表",
"statistical_chart": "统计图",
"error_analysis": "误差分析图",
"rmse": "RMSE评估图",
"r2_score": "R²得分图",
"flight": "飞行轨迹图",
"path": "轨迹图",
"trajectory": "轨迹图",
"glint_deglint": "耀斑去耀斑影像",
"enhanced": "增强分布图",
"content": "含量分布图",
"distribution": "分布图",
"prediction": "预测图",
"inversion": "反演图",
"scatter_true_vs_pred": "真值-预测散点图",
"true_vs_pred": "真值-预测散点图",
"correlation_heatmap": "相关性热力图",
"parameter_boxplot": "水质参数箱线图",
"spectrum_comparison": "光谱曲线对比图",
"scatter": "散点图",
}
# 目录层级中文翻译
DIR_MAPPING = {
"14_visualization": "可视化产物",
"glint_deglint_previews": "耀斑与去耀斑预览",
"sampling_maps": "采样点地图",
"scatter_plots": "模型评估散点图",
"flight_maps": "飞行轨迹图",
"11_12_13_predictions": "预测结果",
"Machine_Learning_Prediction": "机器学习预测",
"Non_Empirical_Prediction": "非经验模型预测",
"Custom_Regression_Prediction": "自定义回归预测",
"8_Regression_Modeling": "回归建模",
"10_feature_construction": "特征构建",
"5_training_spectra": "训练光谱",
"2_glint": "耀斑分析",
"3_deglint": "去耀斑处理",
"1_water_mask": "水掩膜",
"9_water_quality_prediction": "水质预测",
"8_spatial_inversion": "空间反演",
"4_processed_data": "处理数据",
}
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._dir_node_map: dict = {} # 目录路径字符串 → QTreeWidgetItem
self._work_path: Optional[Path] = None
self.setHeaderLabel("图像目录") self.setHeaderLabel("图像目录")
self.setMaximumWidth(300) self.setMaximumWidth(300)
self.setMinimumWidth(250) self.setMinimumWidth(250)
self.setup_categories()
self.setStyleSheet(""" self.setStyleSheet("""
QTreeWidget { QTreeWidget {
border: 1px solid #ddd; border: 1px solid #ddd;
@ -528,89 +594,211 @@ class ImageCategoryTree(QTreeWidget):
} }
""") """)
def setup_categories(self):
"""初始化类别节点"""
self.category_items = {}
for category_name, keywords, icon in self.CATEGORIES:
item = QTreeWidgetItem(self)
item.setText(0, f"{icon} {category_name}")
item.setData(0, Qt.UserRole, {"type": "category", "keywords": keywords, "name": category_name})
item.setExpanded(True)
self.category_items[category_name] = item
def clear_all_images(self): def clear_all_images(self):
"""清除所有图像项""" """清除所有图像项"""
for category_item in self.category_items.values(): try:
while category_item.childCount() > 0: self.invisibleRootItem().takeChildren()
category_item.removeChild(category_item.child(0)) if hasattr(self, '_dir_node_map'):
self._dir_node_map.clear()
except Exception as e:
print(f"清空树状图出错: {e}")
import traceback
traceback.print_exc()
def add_image(self, file_path: Path, display_name: str = None): def _translate_dir_name(self, dir_name: str) -> str:
"""添加图像到对应的类别""" """翻译目录名为中文"""
if display_name is None: return self.DIR_MAPPING.get(dir_name, dir_name)
display_name = file_path.stem
category = self._determine_category(file_path.name) def _translate_filename(self, filename: str) -> str:
category_item = self.category_items.get(category, self.category_items["含量分布图"]) """翻译文件名为中文(动态替换后缀片段)"""
# 依次替换常见后缀模式
replacements = [
("_spectrum_comparison", " 光谱曲线对比图"),
("_scatter_true_vs_pred", " 真值-预测散点图"),
("_true_vs_pred", " 真值-预测散点图"),
("_histogram", " 分布直方图"),
("_boxplot", " 箱线图"),
("_distribution_map", " 分布图"),
("_distribution_enhanced", " 增强分布图"),
("_thematic_map", " 专题图"),
("_water_quality_map", " 水质分布图"),
("_prediction_map", " 预测结果图"),
("_inversion_map", " 反演结果图"),
("_glint_deglint", " 耀斑去耀斑对比"),
("_glint_mask", " 耀斑掩膜"),
("_deglint", " 去耀斑"),
("_mask_overlay", " 掩膜叠加"),
("_content", " 含量"),
("_distribution", " 分布"),
("_prediction", " 预测"),
("_inversion", " 反演"),
("_enhanced", " 增强"),
("_scatter", " 散点图"),
("_boxplot", " 箱线图"),
("_correlation_heatmap", " 相关性热力图"),
("_parameter_boxplot", " 箱线图"),
("_sampling_point", " 采样点"),
("_sampling_points", " 采样点"),
("_flight_path", " 飞行轨迹"),
("_trajectory", " 轨迹"),
]
result = filename
for pattern, replacement in replacements:
result = result.replace(pattern, replacement)
image_item = QTreeWidgetItem(category_item) # 处理参数名(常见水质参数翻译)
image_item.setText(0, f" └─ {display_name}") param_map = {
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path)}) "Chla": "叶绿素", "COD": "化学需氧量", "TN": "总氮", "TP": "总磷",
"Turbidity": "浊度", "DO": "溶解氧", "pH": "pH值",
"Conductivity": "电导率", "BOD": "生化需氧量", "NH3_N": "氨氮",
}
for eng, chn in param_map.items():
# 在文件名中找到参数名并翻译
if eng.lower() in result.lower():
result = result.replace(eng, chn)
if eng.lower() in result.lower():
result = result.replace(eng.lower(), chn)
# 如果没有任何替换,返回原文件名(去掉扩展名)
if result == filename:
return filename.rsplit(".", 1)[0] if "." in filename else filename
return result
def add_image_by_dir(self, file_path: Path, work_path: Path):
"""按真实物理目录层级挂载图片节点
Args:
file_path: 图片文件的完整路径
work_path: 工作目录根路径
"""
# 计算相对路径
try:
rel_path = file_path.relative_to(work_path)
except ValueError:
rel_path = Path(file_path.name)
# 分离父目录链和文件名
parts = rel_path.parts
if len(parts) <= 1:
parent_key = "__root__"
parent_display = "根目录"
else:
# 父目录路径相对于work_path
parent_key = str(Path(*parts[:-1]))
# 取最后一层目录名作为显示名
parent_display = self._translate_dir_name(parts[-2])
# 根目录节点特殊处理
root_display = self._translate_dir_name(parts[0]) if parts else "根目录"
# 获取或创建根目录节点
if root_display not in self._dir_node_map:
root_item = QTreeWidgetItem(self)
root_item.setText(0, f"📁 {root_display}")
root_item.setData(0, Qt.UserRole, {"type": "root_dir", "path": str(work_path / parts[0])})
root_item.setExpanded(True)
self._dir_node_map[root_display] = root_item
self._dir_node_map[f"__root__{root_display}"] = root_item
root_item = self._dir_node_map.get(f"__root__{root_display}")
if len(parts) > 1:
# 获取或创建子目录节点
if parent_key not in self._dir_node_map:
dir_item = QTreeWidgetItem(root_item)
dir_item.setText(0, f" 📂 {parent_display}")
dir_item.setData(0, Qt.UserRole, {"type": "sub_dir", "path": str(work_path / parent_key)})
dir_item.setExpanded(True)
self._dir_node_map[parent_key] = dir_item
parent_item = self._dir_node_map[parent_key]
else:
parent_item = root_item
# 创建图片节点
display_name = self._translate_filename(file_path.stem) + file_path.suffix
image_item = QTreeWidgetItem(parent_item)
image_item.setText(0, f" 🖼 {display_name}")
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path), "display_name": display_name})
image_item.setToolTip(0, str(file_path)) image_item.setToolTip(0, str(file_path))
return image_item return image_item
def _determine_category(self, filename: str) -> str:
"""根据文件名确定类别"""
filename_lower = filename.lower()
for category_name, keywords, _ in self.CATEGORIES:
if any(keyword in filename_lower for keyword in keywords):
return category_name
return "含量分布图"
def scan_directory(self, work_dir: str): def scan_directory(self, work_dir: str):
"""扫描目录中的所有图像文件""" """扫描目录中的所有图像文件(深度递归扫描)—— 按真实物理目录结构挂载"""
self.clear_all_images() try:
if not work_dir:
print("可视化面板:工作目录为空,跳过扫描")
return
work_path = Path(work_dir) self._work_path = Path(work_dir)
if not work_path.exists():
# 阻塞信号,防止在清空树状图时触发 selected 槽函数导致崩溃
# 因为当前类继承自 QTreeWidget所以 self 本身就是树
self.blockSignals(True)
self.clear_all_images()
self.blockSignals(False)
if not self._work_path.exists():
return
except Exception as e:
import traceback
print(f"可视化面板初始化扫描出错: {e}")
traceback.print_exc()
# 确保信号锁被解开
self.blockSignals(False)
return return
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp'] try:
scan_roots: List[Path] = [] image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp']
_viz = work_path / "14_visualization"
if _viz.is_dir():
scan_roots.append(_viz)
_wm = work_path / "1_water_mask"
if _wm.is_dir():
scan_roots.append(_wm)
if not scan_roots:
scan_roots.append(work_path)
seen_norm: set = set() # 拓宽扫描根目录列表(新增多个遗漏目录)
image_files: List[Path] = [] scan_roots: List[Path] = [
for root in scan_roots: self._work_path / "14_visualization",
for ext in image_extensions: self._work_path / "11_12_13_predictions",
for p in root.glob(f"**/{ext}"): self._work_path / "8_Regression_Modeling",
key = os.path.normcase(os.path.normpath(str(p.resolve()))) self._work_path / "10_feature_construction",
if key in seen_norm: self._work_path / "5_training_spectra",
continue self._work_path / "2_glint",
seen_norm.add(key) self._work_path / "3_deglint",
image_files.append(p) self._work_path / "1_water_mask",
self._work_path / "9_water_quality_prediction",
]
for img_file in sorted(image_files): # 只保留存在的目录,并补充工作根目录作为兜底
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower(): scan_roots = [p for p in scan_roots if p.is_dir()]
continue if not scan_roots:
self.add_image(img_file) scan_roots.append(self._work_path)
for category_name, item in self.category_items.items(): seen_norm: set = set()
count = item.childCount() image_files: List[Path] = []
if count > 0: for root in scan_roots:
for cat_name, _, icon in self.CATEGORIES: for ext in image_extensions:
if cat_name == category_name: for p in root.rglob(ext):
item.setText(0, f"{icon} {category_name} ({count})") key = os.path.normcase(os.path.normpath(str(p.resolve())))
break if key in seen_norm:
continue
seen_norm.add(key)
image_files.append(p)
for img_file in sorted(image_files):
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
continue
self.add_image_by_dir(img_file, self._work_path)
# 更新目录节点计数
for key, item in self._dir_node_map.items():
if key.startswith("__root__"):
continue
if item.data(0, Qt.UserRole).get("type") == "sub_dir":
count = item.childCount()
name = item.text(0)
if count > 0 and f"({count})" not in name:
# 从目录名中提取显示名并附加计数
display = name.strip()
item.setText(0, f" 📂 {display} ({count})")
except Exception as e:
import traceback
print(f"可视化面板图片挂载出错: {e}")
traceback.print_exc()
def get_selected_image_path(self) -> Optional[str]: def get_selected_image_path(self) -> Optional[str]:
"""获取当前选中的图像路径""" """获取当前选中的图像路径"""
@ -1252,54 +1440,78 @@ class VisualizationPanel(QWidget):
4. {work_dir}/14_visualization可视化目录 4. {work_dir}/14_visualization可视化目录
5. {work_dir}(工作目录根) 5. {work_dir}(工作目录根)
""" """
if work_dir: try:
self.work_dir = work_dir if work_dir:
self.work_dir_edit.setText(str(work_dir)) self.work_dir = work_dir
elif not self.work_dir: self.work_dir_edit.setText(str(work_dir))
return elif not self.work_dir:
return
work_path = Path(self.work_dir) work_path = Path(self.work_dir)
pred_dir = work_path / "11_12_13_predictions" pred_dir = work_path / "11_12_13_predictions"
# 按优先级寻找存在的目录 # 按优先级寻找存在的目录
candidates = [ candidates = [
pred_dir / "Machine_Learning_Prediction", pred_dir / "Machine_Learning_Prediction",
pred_dir / "Non_Empirical_Prediction", pred_dir / "Non_Empirical_Prediction",
pred_dir / "Custom_Regression_Prediction", pred_dir / "Custom_Regression_Prediction",
work_path / "14_visualization", work_path / "14_visualization",
work_path, work_path,
] ]
detected_dir = None detected_dir = None
for candidate in candidates: for candidate in candidates:
if candidate.exists() and candidate.is_dir(): if candidate.exists() and candidate.is_dir():
detected_dir = candidate detected_dir = candidate
break break
if detected_dir: if detected_dir:
detected_str = str(detected_dir) detected_str = str(detected_dir)
self.img_dir_edit.setText(detected_str) self.img_dir_edit.setText(detected_str)
self.image_tree.scan_directory(detected_str) self.image_tree.scan_directory(detected_str)
else: else:
# 无预测目录时扫描整个工作目录 # 无预测目录时扫描整个工作目录
self.image_tree.scan_directory(self.work_dir) self.image_tree.scan_directory(self.work_dir)
# 自动触发加载第一张图像 # 自动触发加载第一张图像
self._load_first_image_from_tree() self._load_first_image_from_tree()
except Exception as e:
import traceback
print(f"可视化面板 update_from_config 出错: {e}")
traceback.print_exc()
def _load_first_image_from_tree(self): def _load_first_image_from_tree(self):
"""从目录树中加载第一张图像到右侧查看器""" """自动加载树状图中的第一张有效图片(兼容物理目录层级结构)"""
tree = self.image_tree try:
if tree is None: tree = getattr(self, 'image_tree', None)
return if not tree:
for category_item in tree.category_items.values(): return
for i in range(category_item.childCount()):
child = category_item.child(i) from PyQt5.QtCore import Qt
data = child.data(0, Qt.UserRole)
if data and data.get("type") == "image": def find_first_image(item):
img_path = data.get("path") # 检查当前节点是否是图片节点
if img_path and Path(img_path).exists(): data = item.data(0, Qt.UserRole)
self.image_viewer.load_image(img_path) if isinstance(data, dict) and data.get("type") == "image":
return return item
# 如果不是,递归检查所有子节点
for i in range(item.childCount()):
found = find_first_image(item.child(i))
if found:
return found
return None
# 遍历所有顶层节点
for i in range(tree.topLevelItemCount()):
first_img_item = find_first_image(tree.topLevelItem(i))
if first_img_item:
tree.setCurrentItem(first_img_item)
# 主动触发一次点击槽函数,以在右侧渲染图片
self.on_tree_item_clicked(first_img_item, 0)
return
except Exception as e:
import traceback
print(f"自动加载首张图片失败: {e}")
traceback.print_exc()
def scan_work_directory(self): def scan_work_directory(self):
"""扫描工作目录中的图像文件""" """扫描工作目录中的图像文件"""

View File

@ -29,6 +29,18 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
import sys
import traceback
def global_exception_handler(exc_type, exc_value, exc_traceback):
print("\n" + "="*50)
print("【严重错误拦截 - PyQt 崩溃死因】")
traceback.print_exception(exc_type, exc_value, exc_traceback)
print("="*50 + "\n")
# 挂载全局异常钩子,阻止 PyQt 静默闪退
sys.excepthook = global_exception_handler
# 导入样式模块 - 兼容开发环境和 PyInstaller 打包 # 导入样式模块 - 兼容开发环境和 PyInstaller 打包
try: try:
from styles import ModernStylesheet from styles import ModernStylesheet