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,
|
2025-12-08 22:40:07 +02:00
|
|
|
QSplitter,
|
2025-12-08 16:28:58 +02:00
|
|
|
)
|
2025-12-08 17:33:32 +02:00
|
|
|
from PySide6.QtCore import Qt, QSettings
|
2025-12-08 16:28:58 +02:00
|
|
|
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
|
2025-12-08 17:33:32 +02:00
|
|
|
from src.gui.widgets import ImageDisplayWidget
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
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 22:40:07 +02:00
|
|
|
# Main horizontal splitter to divide left (image) and right (controls)
|
|
|
|
|
self.main_splitter = QSplitter(Qt.Horizontal)
|
|
|
|
|
self.main_splitter.setHandleWidth(10)
|
2025-12-08 16:28:58 +02:00
|
|
|
|
2025-12-08 22:40:07 +02:00
|
|
|
# { Left splitter for image display and zoom info
|
|
|
|
|
self.left_splitter = QSplitter(Qt.Vertical)
|
|
|
|
|
self.left_splitter.setHandleWidth(10)
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
# Image display section
|
|
|
|
|
display_group = QGroupBox("Image Display")
|
|
|
|
|
display_layout = QVBoxLayout()
|
|
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
# Use the reusable ImageDisplayWidget
|
|
|
|
|
self.image_display_widget = ImageDisplayWidget()
|
|
|
|
|
self.image_display_widget.zoom_changed.connect(self._on_zoom_changed)
|
|
|
|
|
display_layout.addWidget(self.image_display_widget)
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
display_group.setLayout(display_layout)
|
2025-12-08 22:40:07 +02:00
|
|
|
self.left_splitter.addWidget(display_group)
|
|
|
|
|
|
|
|
|
|
# Zoom controls info
|
|
|
|
|
zoom_info = QLabel("Zoom: Mouse wheel or +/- keys to zoom in/out")
|
|
|
|
|
zoom_info.setStyleSheet("QLabel { color: #888; font-style: italic; }")
|
|
|
|
|
self.left_splitter.addWidget(zoom_info)
|
|
|
|
|
# }
|
|
|
|
|
|
|
|
|
|
# { Right splitter for annotation tools and controls
|
|
|
|
|
self.right_splitter = QSplitter(Qt.Vertical)
|
|
|
|
|
self.right_splitter.setHandleWidth(10)
|
|
|
|
|
|
|
|
|
|
# Image loading section
|
|
|
|
|
load_group = QGroupBox("Image Loading")
|
|
|
|
|
load_layout = QVBoxLayout()
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
# 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 22:40:07 +02:00
|
|
|
info_label.setWordWrap(True)
|
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 22:40:07 +02:00
|
|
|
self.right_splitter.addWidget(info_group)
|
2025-12-08 17:33:32 +02:00
|
|
|
|
2025-12-08 22:40:07 +02:00
|
|
|
# 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)
|
|
|
|
|
self.right_splitter.addWidget(load_group)
|
|
|
|
|
# }
|
2025-12-08 17:33:32 +02:00
|
|
|
|
2025-12-08 22:40:07 +02:00
|
|
|
# Add both splitters to the main horizontal splitter
|
|
|
|
|
self.main_splitter.addWidget(self.left_splitter)
|
|
|
|
|
self.main_splitter.addWidget(self.right_splitter)
|
|
|
|
|
|
|
|
|
|
# Set initial sizes: 75% for left (image), 25% for right (controls)
|
|
|
|
|
self.main_splitter.setSizes([750, 250])
|
|
|
|
|
|
|
|
|
|
layout.addWidget(self.main_splitter)
|
2025-12-05 09:50:50 +02:00
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
2025-12-08 22:40:07 +02:00
|
|
|
# Restore splitter positions from settings
|
|
|
|
|
self._restore_state()
|
|
|
|
|
|
2025-12-08 16:28:58 +02:00
|
|
|
def _load_image(self):
|
|
|
|
|
"""Load and display an image file."""
|
2025-12-08 17:33:32 +02:00
|
|
|
# Get last opened directory from QSettings
|
|
|
|
|
settings = QSettings("microscopy_app", "object_detection")
|
|
|
|
|
last_dir = settings.value("annotation_tab/last_directory", None)
|
|
|
|
|
|
|
|
|
|
# Fallback to image repository path or home directory
|
|
|
|
|
if last_dir and Path(last_dir).exists():
|
|
|
|
|
start_dir = last_dir
|
|
|
|
|
else:
|
|
|
|
|
repo_path = self.config_manager.get_image_repository_path()
|
|
|
|
|
start_dir = repo_path if repo_path else str(Path.home())
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
# Store the directory for next time
|
|
|
|
|
settings.setValue(
|
|
|
|
|
"annotation_tab/last_directory", str(Path(file_path).parent)
|
2025-12-08 16:28:58 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
# Display image using the ImageDisplayWidget
|
|
|
|
|
self.image_display_widget.load_image(self.current_image)
|
|
|
|
|
|
|
|
|
|
# Update info label
|
|
|
|
|
self._update_image_info()
|
2025-12-08 16:28:58 +02:00
|
|
|
|
|
|
|
|
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)}")
|
|
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
def _update_image_info(self):
|
|
|
|
|
"""Update the image info label with current image details."""
|
2025-12-08 16:28:58 +02:00
|
|
|
if self.current_image is None:
|
2025-12-08 17:33:32 +02:00
|
|
|
self.image_info_label.setText("No image loaded")
|
2025-12-08 16:28:58 +02:00
|
|
|
return
|
|
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
zoom_percentage = self.image_display_widget.get_zoom_percentage()
|
|
|
|
|
info_text = (
|
|
|
|
|
f"File: {Path(self.current_image_path).name}\n"
|
|
|
|
|
f"Size: {self.current_image.width}x{self.current_image.height} pixels\n"
|
|
|
|
|
f"Channels: {self.current_image.channels}\n"
|
|
|
|
|
f"Data type: {self.current_image.dtype}\n"
|
|
|
|
|
f"Format: {self.current_image.format.upper()}\n"
|
|
|
|
|
f"File size: {self.current_image.size_mb:.2f} MB\n"
|
|
|
|
|
f"Zoom: {zoom_percentage}%"
|
|
|
|
|
)
|
|
|
|
|
self.image_info_label.setText(info_text)
|
2025-12-08 16:28:58 +02:00
|
|
|
|
2025-12-08 17:33:32 +02:00
|
|
|
def _on_zoom_changed(self, zoom_scale: float):
|
|
|
|
|
"""Handle zoom level changes from the image display widget."""
|
|
|
|
|
self._update_image_info()
|
2025-12-08 16:28:58 +02:00
|
|
|
|
2025-12-08 22:40:07 +02:00
|
|
|
def _restore_state(self):
|
|
|
|
|
"""Restore splitter positions from settings."""
|
|
|
|
|
settings = QSettings("microscopy_app", "object_detection")
|
|
|
|
|
|
|
|
|
|
# Restore main splitter state
|
|
|
|
|
main_state = settings.value("annotation_tab/main_splitter_state")
|
|
|
|
|
if main_state:
|
|
|
|
|
self.main_splitter.restoreState(main_state)
|
|
|
|
|
logger.debug("Restored main splitter state")
|
|
|
|
|
|
|
|
|
|
# Restore left splitter state
|
|
|
|
|
left_state = settings.value("annotation_tab/left_splitter_state")
|
|
|
|
|
if left_state:
|
|
|
|
|
self.left_splitter.restoreState(left_state)
|
|
|
|
|
logger.debug("Restored left splitter state")
|
|
|
|
|
|
|
|
|
|
# Restore right splitter state
|
|
|
|
|
right_state = settings.value("annotation_tab/right_splitter_state")
|
|
|
|
|
if right_state:
|
|
|
|
|
self.right_splitter.restoreState(right_state)
|
|
|
|
|
logger.debug("Restored right splitter state")
|
|
|
|
|
|
|
|
|
|
def save_state(self):
|
|
|
|
|
"""Save splitter positions to settings."""
|
|
|
|
|
settings = QSettings("microscopy_app", "object_detection")
|
|
|
|
|
|
|
|
|
|
# Save main splitter state
|
|
|
|
|
settings.setValue(
|
|
|
|
|
"annotation_tab/main_splitter_state", self.main_splitter.saveState()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Save left splitter state
|
|
|
|
|
settings.setValue(
|
|
|
|
|
"annotation_tab/left_splitter_state", self.left_splitter.saveState()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Save right splitter state
|
|
|
|
|
settings.setValue(
|
|
|
|
|
"annotation_tab/right_splitter_state", self.right_splitter.saveState()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logger.debug("Saved annotation tab splitter states")
|
|
|
|
|
|
2025-12-05 09:50:50 +02:00
|
|
|
def refresh(self):
|
|
|
|
|
"""Refresh the tab."""
|
|
|
|
|
pass
|