2025-12-05 09:50:50 +02:00
|
|
|
"""
|
|
|
|
|
Annotation tab for the microscopy object detection application.
|
|
|
|
|
Future feature for manual annotation.
|
|
|
|
|
"""
|
|
|
|
|
|
2025-12-08 16:28:58 +02:00
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
QWidget,
|
|
|
|
|
QVBoxLayout,
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
QLabel,
|
|
|
|
|
QGroupBox,
|
|
|
|
|
QPushButton,
|
|
|
|
|
QFileDialog,
|
|
|
|
|
QMessageBox,
|
|
|
|
|
QScrollArea,
|
|
|
|
|
)
|
|
|
|
|
from PySide6.QtGui import QPixmap, QImage
|
|
|
|
|
from PySide6.QtCore import Qt
|
|
|
|
|
from pathlib import Path
|
2025-12-05 09:50:50 +02:00
|
|
|
|
|
|
|
|
from src.database.db_manager import DatabaseManager
|
|
|
|
|
from src.utils.config_manager import ConfigManager
|
2025-12-08 16:28:58 +02:00
|
|
|
from src.utils.image import Image, ImageLoadError
|
|
|
|
|
from src.utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
2025-12-05 09:50:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnnotationTab(QWidget):
|
|
|
|
|
"""Annotation tab placeholder (future feature)."""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self, db_manager: DatabaseManager, config_manager: ConfigManager, parent=None
|
|
|
|
|
):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.db_manager = db_manager
|
|
|
|
|
self.config_manager = config_manager
|
2025-12-08 16:28:58 +02:00
|
|
|
self.current_image = None
|
|
|
|
|
self.current_image_path = None
|
2025-12-05 09:50:50 +02:00
|
|
|
|
|
|
|
|
self._setup_ui()
|
|
|
|
|
|
|
|
|
|
def _setup_ui(self):
|
|
|
|
|
"""Setup user interface."""
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
|
|
|
2025-12-08 16:28:58 +02:00
|
|
|
# Image loading section
|
|
|
|
|
load_group = QGroupBox("Image Loading")
|
|
|
|
|
load_layout = QVBoxLayout()
|
|
|
|
|
|
|
|
|
|
# Load image button
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
|
self.load_image_btn = QPushButton("Load Image")
|
|
|
|
|
self.load_image_btn.clicked.connect(self._load_image)
|
|
|
|
|
button_layout.addWidget(self.load_image_btn)
|
|
|
|
|
button_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
load_layout.addLayout(button_layout)
|
|
|
|
|
|
|
|
|
|
# Image info label
|
|
|
|
|
self.image_info_label = QLabel("No image loaded")
|
|
|
|
|
load_layout.addWidget(self.image_info_label)
|
|
|
|
|
|
|
|
|
|
load_group.setLayout(load_layout)
|
|
|
|
|
layout.addWidget(load_group)
|
|
|
|
|
|
|
|
|
|
# Image display section
|
|
|
|
|
display_group = QGroupBox("Image Display")
|
|
|
|
|
display_layout = QVBoxLayout()
|
|
|
|
|
|
|
|
|
|
# Scroll area for image
|
|
|
|
|
scroll_area = QScrollArea()
|
|
|
|
|
scroll_area.setWidgetResizable(True)
|
|
|
|
|
scroll_area.setMinimumHeight(400)
|
|
|
|
|
|
|
|
|
|
self.image_label = QLabel("No image loaded")
|
|
|
|
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
self.image_label.setStyleSheet(
|
|
|
|
|
"QLabel { background-color: #2b2b2b; color: #888; }"
|
|
|
|
|
)
|
|
|
|
|
self.image_label.setScaledContents(False)
|
|
|
|
|
|
|
|
|
|
scroll_area.setWidget(self.image_label)
|
|
|
|
|
display_layout.addWidget(scroll_area)
|
|
|
|
|
|
|
|
|
|
display_group.setLayout(display_layout)
|
|
|
|
|
layout.addWidget(display_group)
|
|
|
|
|
|
|
|
|
|
# Future features info
|
|
|
|
|
info_group = QGroupBox("Annotation Tool (Future Feature)")
|
|
|
|
|
info_layout = QVBoxLayout()
|
|
|
|
|
info_label = QLabel(
|
|
|
|
|
"Full annotation functionality will be implemented in future version.\n\n"
|
2025-12-05 09:50:50 +02:00
|
|
|
"Planned Features:\n"
|
|
|
|
|
"- Drawing tools for bounding boxes\n"
|
|
|
|
|
"- Class label assignment\n"
|
|
|
|
|
"- Export annotations to YOLO format\n"
|
|
|
|
|
"- Annotation verification"
|
|
|
|
|
)
|
2025-12-08 16:28:58 +02:00
|
|
|
info_layout.addWidget(info_label)
|
|
|
|
|
info_group.setLayout(info_layout)
|
2025-12-05 09:50:50 +02:00
|
|
|
|
2025-12-08 16:28:58 +02:00
|
|
|
layout.addWidget(info_group)
|
2025-12-05 09:50:50 +02:00
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
2025-12-08 16:28:58 +02:00
|
|
|
def _load_image(self):
|
|
|
|
|
"""Load and display an image file."""
|
|
|
|
|
# Get image repository path or use home directory
|
|
|
|
|
repo_path = self.config_manager.get_image_repository_path()
|
|
|
|
|
start_dir = repo_path if repo_path else str(Path.home())
|
|
|
|
|
|
|
|
|
|
# Open file dialog
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
self,
|
|
|
|
|
"Select Image",
|
|
|
|
|
start_dir,
|
|
|
|
|
"Images (*.jpg *.jpeg *.png *.tif *.tiff *.bmp)",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not file_path:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Load image using Image class
|
|
|
|
|
self.current_image = Image(file_path)
|
|
|
|
|
self.current_image_path = file_path
|
|
|
|
|
|
|
|
|
|
# Update info label
|
|
|
|
|
info_text = (
|
|
|
|
|
f"File: {Path(file_path).name}\n"
|
|
|
|
|
f"Size: {self.current_image.width}x{self.current_image.height} pixels\n"
|
|
|
|
|
f"Channels: {self.current_image.channels}\n"
|
|
|
|
|
f"Format: {self.current_image.format.upper()}\n"
|
|
|
|
|
f"File size: {self.current_image.size_mb:.2f} MB"
|
|
|
|
|
)
|
|
|
|
|
self.image_info_label.setText(info_text)
|
|
|
|
|
|
|
|
|
|
# Convert to QPixmap and display
|
|
|
|
|
self._display_image()
|
|
|
|
|
|
|
|
|
|
logger.info(f"Loaded image: {file_path}")
|
|
|
|
|
|
|
|
|
|
except ImageLoadError as e:
|
|
|
|
|
logger.error(f"Failed to load image: {e}")
|
|
|
|
|
QMessageBox.critical(
|
|
|
|
|
self, "Error Loading Image", f"Failed to load image:\n{str(e)}"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Unexpected error loading image: {e}")
|
|
|
|
|
QMessageBox.critical(self, "Error", f"Unexpected error:\n{str(e)}")
|
|
|
|
|
|
|
|
|
|
def _display_image(self):
|
|
|
|
|
"""Display the current image in the image label."""
|
|
|
|
|
if self.current_image is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Get RGB image data
|
|
|
|
|
rgb_data = self.current_image.get_rgb()
|
|
|
|
|
|
|
|
|
|
# Convert numpy array to QImage
|
|
|
|
|
height, width, channels = rgb_data.shape
|
|
|
|
|
bytes_per_line = channels * width
|
|
|
|
|
|
|
|
|
|
if channels == 3:
|
|
|
|
|
qimage = QImage(
|
|
|
|
|
rgb_data.data,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
bytes_per_line,
|
|
|
|
|
QImage.Format_RGB888,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# Grayscale
|
|
|
|
|
qimage = QImage(
|
|
|
|
|
rgb_data.data,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
bytes_per_line,
|
|
|
|
|
QImage.Format_Grayscale8,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Convert to pixmap
|
|
|
|
|
pixmap = QPixmap.fromImage(qimage)
|
|
|
|
|
|
|
|
|
|
# Scale to fit display (max 800px width or height)
|
|
|
|
|
max_size = 800
|
|
|
|
|
if pixmap.width() > max_size or pixmap.height() > max_size:
|
|
|
|
|
pixmap = pixmap.scaled(
|
|
|
|
|
max_size,
|
|
|
|
|
max_size,
|
|
|
|
|
Qt.KeepAspectRatio,
|
|
|
|
|
Qt.SmoothTransformation,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Display in label
|
|
|
|
|
self.image_label.setPixmap(pixmap)
|
|
|
|
|
self.image_label.setScaledContents(False)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error displaying image: {e}")
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
self, "Display Error", f"Failed to display image:\n{str(e)}"
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-05 09:50:50 +02:00
|
|
|
def refresh(self):
|
|
|
|
|
"""Refresh the tab."""
|
|
|
|
|
pass
|