#!/usr/bin/env python # -*- coding: utf-8 -*- """ 图像浏览组件模块 包含 ImageCategoryTree 和 ImageViewerWidget 类。 """ import os from pathlib import Path from typing import List, Optional from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QScrollArea, QFrame, QGroupBox, QFileDialog, QMessageBox, ) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QPixmap 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"], "🖼️"), ("含量分布图", [], "📁"), ] def __init__(self, parent=None): super().__init__(parent) self.setHeaderLabel("图像目录") self.setMaximumWidth(300) self.setMinimumWidth(250) self.setup_categories() self.setStyleSheet(""" QTreeWidget { border: 1px solid #ddd; border-radius: 5px; background-color: #f8f9fa; } QTreeWidget::item { padding: 5px; border-radius: 3px; } QTreeWidget::item:selected { background-color: #0078D4; color: white; } QTreeWidget::item:hover { background-color: #e3f2fd; } """) 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)) def add_image(self, file_path: Path, display_name: str = None): """添加图像到对应的类别""" if display_name is None: display_name = file_path.stem 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.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(): 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) if not scan_roots: scan_roots.append(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}"): key = os.path.normcase(os.path.normpath(str(p.resolve()))) 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(img_file) for category_name, item in self.category_items.items(): 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 def get_selected_image_path(self) -> Optional[str]: """获取当前选中的图像路径""" selected_item = self.currentItem() if not selected_item: return None data = selected_item.data(0, Qt.UserRole) if data and data.get("type") == "image": return data.get("path") return None class ImageViewerWidget(QWidget): """图像查看器组件 - 支持缩放、平移""" def __init__(self, parent=None): super().__init__(parent) self.current_image_path = None self.scale_factor = 1.0 self._update_timer = QTimer() self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._do_update_display) self._pending_scale = None self.setup_ui() def setup_ui(self): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) toolbar = QHBoxLayout() self.refresh_btn = QPushButton("🔄 刷新目录") self.refresh_btn.setToolTip("重新扫描工作目录中的图像文件") toolbar.addWidget(self.refresh_btn) separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setFrameShadow(QFrame.Sunken) toolbar.addWidget(separator) self.zoom_in_btn = QPushButton("🔍+") self.zoom_in_btn.setToolTip("放大") self.zoom_in_btn.setMaximumWidth(50) toolbar.addWidget(self.zoom_in_btn) self.zoom_out_btn = QPushButton("🔍-") self.zoom_out_btn.setToolTip("缩小") self.zoom_out_btn.setMaximumWidth(50) toolbar.addWidget(self.zoom_out_btn) self.fit_btn = QPushButton("⬜ 适应窗口") self.fit_btn.setToolTip("适应窗口大小") toolbar.addWidget(self.fit_btn) self.original_btn = QPushButton("1:1 原始大小") self.original_btn.setToolTip("原始大小") toolbar.addWidget(self.original_btn) toolbar.addStretch() self.save_btn = QPushButton("💾 保存") self.save_btn.setToolTip("保存当前图像") toolbar.addWidget(self.save_btn) layout.addLayout(toolbar) self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area.setStyleSheet("background-color: white;") self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("background-color: white;") self.scroll_area.setWidget(self.image_label) layout.addWidget(self.scroll_area, 1) status_layout = QHBoxLayout() self.status_label = QLabel("就绪") self.status_label.setStyleSheet("color: #666; font-size: 11px;") status_layout.addWidget(self.status_label) status_layout.addStretch() layout.addLayout(status_layout) self.setLayout(layout) self.zoom_in_btn.clicked.connect(self.zoom_in) self.zoom_out_btn.clicked.connect(self.zoom_out) self.fit_btn.clicked.connect(self.fit_to_window) self.original_btn.clicked.connect(self.original_size) self.save_btn.clicked.connect(self.save_image) def load_image(self, image_path: str): """加载并显示图像""" if not image_path or not Path(image_path).exists(): self.image_label.setText("图像不存在") self.status_label.setText("图像加载失败") return self.current_image_path = image_path self.scale_factor = 1.0 pixmap = QPixmap(image_path) if pixmap.isNull(): self.image_label.setText("无法加载图像") self.status_label.setText("图像格式不支持") return self.original_pixmap = pixmap self.fit_to_window() file_info = Path(image_path).stat() size_mb = file_info.st_size / (1024 * 1024) self.status_label.setText(f"{pixmap.width()}x{pixmap.height()} | {size_mb:.2f} MB | {Path(image_path).name} | 适应窗口") def update_image_display(self): """更新图像显示 - 使用防抖避免频繁重绘卡顿""" self._update_timer.stop() self._pending_scale = self.scale_factor self._update_timer.start(50) def _do_update_display(self): """实际执行图像更新""" if not hasattr(self, 'original_pixmap') or self.original_pixmap.isNull(): return if self._pending_scale is None: return if self._pending_scale > 2.0 or self._pending_scale < 0.5: transform = Qt.FastTransformation else: transform = Qt.SmoothTransformation scaled_pixmap = self.original_pixmap.scaled( int(self.original_pixmap.width() * self._pending_scale), int(self.original_pixmap.height() * self._pending_scale), Qt.KeepAspectRatio, transform ) self.image_label.setPixmap(scaled_pixmap) self._pending_scale = None def wheelEvent(self, event): """鼠标滚轮缩放 - 实时响应""" delta = event.angleDelta().y() if delta > 0: if self.scale_factor < 5.0: self.scale_factor = min(self.scale_factor * 1.1, 5.0) self.update_image_display() else: if self.scale_factor > 0.1: self.scale_factor = max(self.scale_factor / 1.1, 0.1) self.update_image_display() event.accept() def zoom_in(self): """放大""" if self.scale_factor < 5.0: self.scale_factor = min(self.scale_factor * 1.25, 5.0) self.update_image_display() def zoom_out(self): """缩小""" if self.scale_factor > 0.1: self.scale_factor = max(self.scale_factor / 1.25, 0.1) self.update_image_display() def fit_to_window(self): """适应窗口""" if not hasattr(self, 'original_pixmap') or self.original_pixmap.isNull(): return view_size = self.scroll_area.viewport().size() img_size = self.original_pixmap.size() scale_w = view_size.width() / img_size.width() scale_h = view_size.height() / img_size.height() self._fit_scale = min(scale_w, scale_h) self.scale_factor = self._fit_scale self.update_image_display() self.status_label.setText(f"适应窗口 | 缩放: {self.scale_factor:.1%}") def original_size(self): """原始大小""" self.scale_factor = 1.0 self._fit_scale = None self.update_image_display() self.status_label.setText("原始大小 | 缩放: 100%") def save_image(self): """保存图像""" if not self.current_image_path: return file_path, _ = QFileDialog.getSaveFileName( self, "保存图像", Path(self.current_image_path).name, "PNG图片 (*.png);;JPG图片 (*.jpg);;所有文件 (*.*)" ) if file_path: try: import shutil shutil.copy(self.current_image_path, file_path) except Exception as e: QMessageBox.critical(self, "错误", f"保存失败: {e}")