diff --git a/src/core/water_quality_inversion_pipeline_GUI.py b/src/core/water_quality_inversion_pipeline_GUI.py index defdcc2..d32fe92 100644 --- a/src/core/water_quality_inversion_pipeline_GUI.py +++ b/src/core/water_quality_inversion_pipeline_GUI.py @@ -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文件路径 @@ -2148,8 +2148,12 @@ 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(): diff --git a/src/gui/components/custom_widgets.py b/src/gui/components/custom_widgets.py index df4df43..0df2b42 100644 --- a/src/gui/components/custom_widgets.py +++ b/src/gui/components/custom_widgets.py @@ -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,14 +110,18 @@ class FileSelectWidget(QWidget): self.setLayout(layout) def browse_file(self): - """浏览文件""" + """浏览文件 - 智能记忆上次选择位置""" current_text = self.line_edit.text().strip() initial_dir = "" + # 最高优先级:输入框已有路径存在 if current_text: - dir_path = os.path.dirname(current_text) - if dir_path and os.path.exists(dir_path): - initial_dir = dir_path + 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 if self.mode == "save": file_path, _ = QFileDialog.getSaveFileName( diff --git a/src/gui/panels/step5_5_panel.py b/src/gui/panels/step5_5_panel.py index 21fd07e..7742cb5 100644 --- a/src/gui/panels/step5_5_panel.py +++ b/src/gui/panels/step5_5_panel.py @@ -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']) @@ -328,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() @@ -348,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 """ @@ -369,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() diff --git a/src/gui/panels/visualization_panel.py b/src/gui/panels/visualization_panel.py index 1709ebe..96c179b 100644 --- a/src/gui/panels/visualization_panel.py +++ b/src/gui/panels/visualization_panel.py @@ -503,6 +503,42 @@ class ImageCategoryTree(QTreeWidget): ("含量分布图", [], "📁"), ] + # 文件名中文翻译映射 + 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²得分图", + } + def __init__(self, parent=None): super().__init__(parent) self.setHeaderLabel("图像目录") @@ -545,16 +581,18 @@ class ImageCategoryTree(QTreeWidget): category_item.removeChild(category_item.child(0)) def add_image(self, file_path: Path, display_name: str = None): - """添加图像到对应的类别""" + """添加图像到对应的类别(带中文名称翻译)""" if display_name is None: - display_name = file_path.stem + # 使用翻译映射,查询不到则用原文件名 + file_base = file_path.stem + display_name = self.NAME_MAPPING.get(file_base, file_base) category = self._determine_category(file_path.name) category_item = self.category_items.get(category, self.category_items["含量分布图"]) image_item = QTreeWidgetItem(category_item) image_item.setText(0, f" └─ {display_name}") - image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path)}) + 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 @@ -570,7 +608,7 @@ class ImageCategoryTree(QTreeWidget): return "含量分布图" def scan_directory(self, work_dir: str): - """扫描目录中的所有图像文件""" + """扫描目录中的所有图像文件(深度递归扫描)""" self.clear_all_images() work_path = Path(work_dir) @@ -578,13 +616,17 @@ class ImageCategoryTree(QTreeWidget): return 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] = [ + work_path / "14_visualization", + work_path / "1_water_mask", + work_path / "9_water_quality_prediction", + work_path / "10_feature_construction", + ] + + # 只保留存在的目录,并补充工作根目录作为兜底 + scan_roots = [p for p in scan_roots if p.is_dir()] if not scan_roots: scan_roots.append(work_path) @@ -592,7 +634,8 @@ class ImageCategoryTree(QTreeWidget): image_files: List[Path] = [] for root in scan_roots: for ext in image_extensions: - for p in root.glob(f"**/{ext}"): + # 使用 rglob 进行深度递归扫描 + for p in root.rglob(ext): key = os.path.normcase(os.path.normpath(str(p.resolve()))) if key in seen_norm: continue