""" Annotation tab for the microscopy object detection application. Future feature for manual annotation. """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QPushButton, QFileDialog, QMessageBox, QSplitter, ) from PySide6.QtCore import Qt, QSettings from pathlib import Path from src.database.db_manager import DatabaseManager from src.utils.config_manager import ConfigManager from src.utils.image import Image, ImageLoadError from src.utils.logger import get_logger from src.gui.widgets import ImageDisplayWidget logger = get_logger(__name__) 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 self.current_image = None self.current_image_path = None self._setup_ui() def _setup_ui(self): """Setup user interface.""" layout = QVBoxLayout() # Main horizontal splitter to divide left (image) and right (controls) self.main_splitter = QSplitter(Qt.Horizontal) self.main_splitter.setHandleWidth(10) # { Left splitter for image display and zoom info self.left_splitter = QSplitter(Qt.Vertical) self.left_splitter.setHandleWidth(10) # Image display section display_group = QGroupBox("Image Display") display_layout = QVBoxLayout() # 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) display_group.setLayout(display_layout) 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() # 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" "Planned Features:\n" "- Drawing tools for bounding boxes\n" "- Class label assignment\n" "- Export annotations to YOLO format\n" "- Annotation verification" ) info_label.setWordWrap(True) info_layout.addWidget(info_label) info_group.setLayout(info_layout) self.right_splitter.addWidget(info_group) # 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) # } # 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) self.setLayout(layout) # Restore splitter positions from settings self._restore_state() def _load_image(self): """Load and display an image file.""" # 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()) # 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 # Store the directory for next time settings.setValue( "annotation_tab/last_directory", str(Path(file_path).parent) ) # Display image using the ImageDisplayWidget self.image_display_widget.load_image(self.current_image) # Update info label self._update_image_info() 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 _update_image_info(self): """Update the image info label with current image details.""" if self.current_image is None: self.image_info_label.setText("No image loaded") return 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) def _on_zoom_changed(self, zoom_scale: float): """Handle zoom level changes from the image display widget.""" self._update_image_info() 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") def refresh(self): """Refresh the tab.""" pass