351 lines
12 KiB
Python
351 lines
12 KiB
Python
#!/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}") |