""" 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, QScrollArea, ) from PySide6.QtGui import QPixmap, QImage from PySide6.QtCore import Qt 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 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() # 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" "Planned Features:\n" "- Drawing tools for bounding boxes\n" "- Class label assignment\n" "- Export annotations to YOLO format\n" "- Annotation verification" ) info_layout.addWidget(info_label) info_group.setLayout(info_layout) layout.addWidget(info_group) self.setLayout(layout) 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)}" ) def refresh(self): """Refresh the tab.""" pass