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,
formula_csv_file: Optional[str] = None,
formula_names: Optional[List[str]] = None,
output_filename: str = "water_quality_indices.csv",
output_file: Optional[str] = None,
enabled: bool = True,
skip_dependency_check: bool = False) -> str:
"""
@ -2117,7 +2117,7 @@ class WaterQualityInversionPipeline:
training_spectra_path: 训练光谱数据CSV路径如果为None使用步骤5的结果
formula_csv_file: 公式CSV文件路径包含公式名称和具体公式
formula_names: 要计算的公式名称列表如果为None则计算所有公式
output_filename: 输出文件
output_file: 输出文件完整路径支持绝对路径如果为None则使用默认路径
Returns:
包含计算结果的新CSV文件路径
@ -2149,7 +2149,11 @@ class WaterQualityInversionPipeline:
if formula_csv_file is None:
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():

View File

@ -12,6 +12,67 @@ from PyQt5.QtWidgets import (
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):
"""文件选择组件"""
def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None):
@ -49,11 +110,15 @@ class FileSelectWidget(QWidget):
self.setLayout(layout)
def browse_file(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

View File

@ -179,6 +179,9 @@ class Step2Panel(QWidget):
# 填充获取到的路径
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)
# 3. 自动填充输出路径(基于工作目录)

View File

@ -294,6 +294,9 @@ class Step3Panel(QWidget):
mask_path = main_window.step1_panel.mask_file.get_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)
# 自动填充输出路径(基于工作目录)

View File

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

View File

@ -170,9 +170,15 @@ class Step5Panel(QWidget):
mask_path = main_window.step1_panel.output_file.get_path()
else:
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:
# 若为相对路径,使用 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)
# 3. 尝试从 Step2 界面读取耀斑掩膜路径
@ -180,6 +186,9 @@ class Step5Panel(QWidget):
if hasattr(main_window, 'step2_panel'):
glint_path = main_window.step2_panel.output_file.get_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)
# 4. 自动填充输出路径(基于工作目录)
@ -196,6 +205,9 @@ class Step5Panel(QWidget):
if main_window and hasattr(main_window, 'step4_panel'):
step4_output_path = main_window.step4_panel.output_file.get_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()
if not existing_csv or not existing_csv.strip():
self.csv_file.set_path(step4_output_path)

View File

@ -102,11 +102,11 @@ class Step6_5Panel(QWidget):
self.spectral_start_col.setValue(1)
params_layout.addRow("光谱起始列索引:", self.spectral_start_col)
# 窗口大小
self.window = QSpinBox()
self.window.setRange(1, 20)
self.window.setValue(5)
params_layout.addRow("窗口大小:", self.window)
# 窗口大小 (变量名已修正,避免覆盖 QWidget.window)
self.window_size_spinbox = QSpinBox()
self.window_size_spinbox.setRange(1, 20)
self.window_size_spinbox.setValue(5)
params_layout.addRow("窗口大小:", self.window_size_spinbox)
params_group.setLayout(params_layout)
layout.addWidget(params_group)
@ -160,7 +160,7 @@ class Step6_5Panel(QWidget):
'algorithms': selected_algorithms,
'value_cols': value_cols,
'spectral_start_col': self.spectral_start_col.value(),
'window': self.window.value(),
'window': self.window_size_spinbox.value(),
'enabled': self.enable_checkbox.isChecked()
}
@ -205,7 +205,7 @@ class Step6_5Panel(QWidget):
self.spectral_start_col.setValue(config['spectral_start_col'])
if 'window' in config:
self.window.setValue(config['window'])
self.window_size_spinbox.setValue(config['window'])
if 'output_dir' in config:
self.output_dir.set_path(config['output_dir'])
if 'csv_path' in config:
@ -218,6 +218,9 @@ class Step6_5Panel(QWidget):
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
try:
import traceback
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
@ -225,11 +228,21 @@ class Step6_5Panel(QWidget):
else:
self.work_dir = None
# 1. 尝试从 Step5 界面读取训练光谱 CSV 路径
main_window = self.window()
# 借用父组件的 window() 方法,安全绕过当前类的命名冲突
parent_widget = self.parentWidget()
main_window = parent_widget.window() if parent_widget else None
if main_window and hasattr(main_window, 'step5_panel'):
step5_output_path = main_window.step5_panel.output_file.get_path()
step5_widget = getattr(main_window.step5_panel, 'output_file', None)
step5_output_path = ""
if hasattr(step5_widget, 'get_path'):
step5_output_path = step5_widget.get_path() or ""
elif hasattr(step5_widget, 'text'):
step5_output_path = step5_widget.text() or ""
if step5_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_output_path):
step5_output_path = os.path.join(self.work_dir or '', step5_output_path).replace('\\', '/')
existing = self.training_csv_file.get_path()
if not existing or not existing.strip():
self.training_csv_file.set_path(step5_output_path)
@ -241,12 +254,18 @@ class Step6_5Panel(QWidget):
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):
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
if hasattr(self, 'work_dir') and 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:
return str(mw.work_dir)
return ""

View File

@ -148,7 +148,7 @@ class Step6_75Panel(QWidget):
output_layout = QFormLayout()
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_group.setLayout(output_layout)
@ -299,6 +299,9 @@ class Step6_75Panel(QWidget):
if main_window and hasattr(main_window, 'step5_panel'):
step5_output_path = main_window.step5_panel.output_file.get_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()
if not existing or not existing.strip():
self.csv_file.set_path(step5_output_path)

View File

@ -298,12 +298,15 @@ class Step6Panel(QWidget):
else:
self.work_dir = None
# 1. 尝试从 Step5 界面读取训练数据路径
# 1. 尝试从 Step5 界面读取训练数据路径,并确保为绝对路径
main_window = self.window()
if hasattr(main_window, 'step5_panel'):
# 优先直接从 Step5 的输出 widget 读取
step5_output = main_window.step5_panel.output_file.get_path()
if step5_output:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_output):
step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
self.training_csv_file.set_path(step5_output)
elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'):
# 回退:从 Step5 的 config 字典中查找可能的键名
@ -315,6 +318,9 @@ class Step6Panel(QWidget):
or step5_cfg.get('output_csv')
)
if step5_csv:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_csv):
step5_csv = os.path.join(self.work_dir or '', step5_csv).replace('\\', '/')
self.training_csv_file.set_path(step5_csv)
# 2. 自动填充输出文件路径(基于工作目录和输入文件名)

View File

@ -145,6 +145,9 @@ class Step7Panel(QWidget):
deglint_path = main_window.step3_panel.output_file.get_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)
# 2. 填充水域掩膜路径(优先从 pipeline.step_outputs 获取绝对路径)
@ -161,6 +164,9 @@ class Step7Panel(QWidget):
water_mask_path = main_window.step1_panel.output_file.get_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)
# 3. 自动填充输出路径(绝对路径)

View File

@ -102,6 +102,9 @@ class Step8_5Panel(QWidget):
if main_window and hasattr(main_window, 'step7_panel'):
step7_output_path = main_window.step7_panel.output_file.get_path()
if step7_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step7_output_path):
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path)
@ -110,6 +113,9 @@ class Step8_5Panel(QWidget):
if main_window and hasattr(main_window, 'step6_5_panel'):
step6_5_models_dir = main_window.step6_5_panel.output_dir.get_path()
if step6_5_models_dir:
# 若为相对路径,使用 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()
if not existing_models or not existing_models.strip():
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.browse_btn.clicked.disconnect()
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)
# 公式CSV文件选择用于查找index_formula
@ -95,6 +95,9 @@ class Step8_75Panel(QWidget):
if main_window and hasattr(main_window, 'step7_panel'):
step7_output_path = main_window.step7_panel.output_file.get_path()
if step7_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step7_output_path):
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path)
@ -103,11 +106,21 @@ class Step8_75Panel(QWidget):
if main_window and hasattr(main_window, 'step6_75_panel'):
step6_75_models_dir = main_window.step6_75_panel.output_dir.text().strip()
if step6_75_models_dir:
# 若为相对路径,使用 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()
if not existing_models or not existing_models.strip():
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:
output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Custom_Regression_Prediction")
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'):
step7_output_path = main_window.step7_panel.output_file.get_path()
if step7_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step7_output_path):
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path)
@ -107,6 +110,9 @@ class Step8Panel(QWidget):
if main_window and hasattr(main_window, 'step6_panel'):
step6_models_dir = main_window.step6_panel.output_dir.get_path()
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()
if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step6_models_dir)

View File

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

View File

@ -493,22 +493,88 @@ class ChartViewerDialog(QDialog):
class ImageCategoryTree(QTreeWidget):
"""图像分类目录树 - 按类别组织图像文件"""
"""图像分类目录树 - 按真实物理文件夹结构组织图像文件"""
CATEGORIES = [
("模型评估", ["scatter", "regression", "validation", "r2", "rmse"], "📊"),
("光谱分析", ["spectrum", "spectral", "band", "wavelength"], "📈"),
("统计图表", ["boxplot", "histogram", "heatmap", "statistics", "stats"], "📉"),
("处理结果", ["mask", "glint", "deglint", "preview", "overlay", "water_mask"], "🖼️"),
("含量分布图", [], "📁"),
]
# 文件名中文翻译映射key: 文件名前缀 → 中文显示名)
NAME_MAPPING = {
"hsi_preview": "高光谱影像预览",
"hsi_original": "原始高光谱影像",
"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):
super().__init__(parent)
self._dir_node_map: dict = {} # 目录路径字符串 → QTreeWidgetItem
self._work_path: Optional[Path] = None
self.setHeaderLabel("图像目录")
self.setMaximumWidth(300)
self.setMinimumWidth(250)
self.setup_categories()
self.setStyleSheet("""
QTreeWidget {
border: 1px solid #ddd;
@ -528,71 +594,185 @@ 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):
"""清除所有图像项"""
for category_item in self.category_items.values():
while category_item.childCount() > 0:
category_item.removeChild(category_item.child(0))
try:
self.invisibleRootItem().takeChildren()
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):
"""添加图像到对应的类别"""
if display_name is None:
display_name = file_path.stem
def _translate_dir_name(self, dir_name: str) -> str:
"""翻译目录名为中文"""
return self.DIR_MAPPING.get(dir_name, dir_name)
category = self._determine_category(file_path.name)
category_item = self.category_items.get(category, self.category_items["含量分布图"])
def _translate_filename(self, filename: str) -> str:
"""翻译文件名为中文(动态替换后缀片段)"""
# 依次替换常见后缀模式
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}")
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path)})
# 处理参数名(常见水质参数翻译)
param_map = {
"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))
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):
"""扫描目录中的所有图像文件"""
self.clear_all_images()
work_path = Path(work_dir)
if not work_path.exists():
"""扫描目录中的所有图像文件(深度递归扫描)—— 按真实物理目录结构挂载"""
try:
if not work_dir:
print("可视化面板:工作目录为空,跳过扫描")
return
self._work_path = Path(work_dir)
# 阻塞信号,防止在清空树状图时触发 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
try:
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp']
scan_roots: List[Path] = []
_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)
# 拓宽扫描根目录列表(新增多个遗漏目录)
scan_roots: List[Path] = [
self._work_path / "14_visualization",
self._work_path / "11_12_13_predictions",
self._work_path / "8_Regression_Modeling",
self._work_path / "10_feature_construction",
self._work_path / "5_training_spectra",
self._work_path / "2_glint",
self._work_path / "3_deglint",
self._work_path / "1_water_mask",
self._work_path / "9_water_quality_prediction",
]
# 只保留存在的目录,并补充工作根目录作为兜底
scan_roots = [p for p in scan_roots if p.is_dir()]
if not scan_roots:
scan_roots.append(work_path)
scan_roots.append(self._work_path)
seen_norm: set = set()
image_files: List[Path] = []
for root in scan_roots:
for ext in image_extensions:
for p in root.glob(f"**/{ext}"):
for p in root.rglob(ext):
key = os.path.normcase(os.path.normpath(str(p.resolve())))
if key in seen_norm:
continue
@ -602,15 +782,23 @@ class ImageCategoryTree(QTreeWidget):
for img_file in sorted(image_files):
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
continue
self.add_image(img_file)
self.add_image_by_dir(img_file, self._work_path)
for category_name, item in self.category_items.items():
# 更新目录节点计数
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()
if count > 0:
for cat_name, _, icon in self.CATEGORIES:
if cat_name == category_name:
item.setText(0, f"{icon} {category_name} ({count})")
break
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]:
"""获取当前选中的图像路径"""
@ -1252,6 +1440,7 @@ class VisualizationPanel(QWidget):
4. {work_dir}/14_visualization可视化目录
5. {work_dir}(工作目录根)
"""
try:
if work_dir:
self.work_dir = work_dir
self.work_dir_edit.setText(str(work_dir))
@ -1285,21 +1474,44 @@ class VisualizationPanel(QWidget):
# 自动触发加载第一张图像
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):
"""从目录树中加载第一张图像到右侧查看器"""
tree = self.image_tree
if tree is None:
"""自动加载树状图中的第一张有效图片(兼容物理目录层级结构)"""
try:
tree = getattr(self, 'image_tree', None)
if not tree:
return
for category_item in tree.category_items.values():
for i in range(category_item.childCount()):
child = category_item.child(i)
data = child.data(0, Qt.UserRole)
if data and data.get("type") == "image":
img_path = data.get("path")
if img_path and Path(img_path).exists():
self.image_viewer.load_image(img_path)
from PyQt5.QtCore import Qt
def find_first_image(item):
# 检查当前节点是否是图片节点
data = item.data(0, Qt.UserRole)
if isinstance(data, dict) and data.get("type") == "image":
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):
"""扫描工作目录中的图像文件"""

View File

@ -29,6 +29,18 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize
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 打包
try:
from styles import ModernStylesheet