Adding pen tool for annotation

This commit is contained in:
2025-12-08 23:15:54 +02:00
parent f84dea0bff
commit fc22479621
6 changed files with 1079 additions and 54 deletions

View File

@@ -1,6 +1,6 @@
"""
Annotation tab for the microscopy object detection application.
Future feature for manual annotation.
Manual annotation with pen tool and object class management.
"""
from PySide6.QtWidgets import (
@@ -21,13 +21,13 @@ 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
from src.gui.widgets import AnnotationCanvasWidget, AnnotationToolsWidget
logger = get_logger(__name__)
class AnnotationTab(QWidget):
"""Annotation tab placeholder (future feature)."""
"""Annotation tab for manual image annotation."""
def __init__(
self, db_manager: DatabaseManager, config_manager: ConfigManager, parent=None
@@ -37,6 +37,7 @@ class AnnotationTab(QWidget):
self.config_manager = config_manager
self.current_image = None
self.current_image_path = None
self.current_image_id = None
self._setup_ui()
@@ -52,49 +53,52 @@ class AnnotationTab(QWidget):
self.left_splitter = QSplitter(Qt.Vertical)
self.left_splitter.setHandleWidth(10)
# Image display section
display_group = QGroupBox("Image Display")
display_layout = QVBoxLayout()
# Annotation canvas section
canvas_group = QGroupBox("Annotation Canvas")
canvas_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)
# Use the AnnotationCanvasWidget
self.annotation_canvas = AnnotationCanvasWidget()
self.annotation_canvas.zoom_changed.connect(self._on_zoom_changed)
self.annotation_canvas.annotation_drawn.connect(self._on_annotation_drawn)
canvas_layout.addWidget(self.annotation_canvas)
display_group.setLayout(display_layout)
self.left_splitter.addWidget(display_group)
canvas_group.setLayout(canvas_layout)
self.left_splitter.addWidget(canvas_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)
# Controls info
controls_info = QLabel(
"Zoom: Mouse wheel or +/- keys | Drawing: Enable pen and drag mouse"
)
controls_info.setStyleSheet("QLabel { color: #888; font-style: italic; }")
self.left_splitter.addWidget(controls_info)
# }
# { Right splitter for annotation tools and controls
self.right_splitter = QSplitter(Qt.Vertical)
self.right_splitter.setHandleWidth(10)
# Annotation tools section
self.annotation_tools = AnnotationToolsWidget(self.db_manager)
self.annotation_tools.pen_enabled_changed.connect(
self.annotation_canvas.set_pen_enabled
)
self.annotation_tools.pen_color_changed.connect(
self.annotation_canvas.set_pen_color
)
self.annotation_tools.pen_width_changed.connect(
self.annotation_canvas.set_pen_width
)
self.annotation_tools.class_selected.connect(self._on_class_selected)
self.annotation_tools.clear_annotations_requested.connect(
self._on_clear_annotations
)
self.right_splitter.addWidget(self.annotation_tools)
# 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")
@@ -158,13 +162,22 @@ class AnnotationTab(QWidget):
"annotation_tab/last_directory", str(Path(file_path).parent)
)
# Display image using the ImageDisplayWidget
self.image_display_widget.load_image(self.current_image)
# Get or create image in database
relative_path = str(Path(file_path).name) # Simplified for now
self.current_image_id = self.db_manager.get_or_create_image(
relative_path,
Path(file_path).name,
self.current_image.width,
self.current_image.height,
)
# Display image using the AnnotationCanvasWidget
self.annotation_canvas.load_image(self.current_image)
# Update info label
self._update_image_info()
logger.info(f"Loaded image: {file_path}")
logger.info(f"Loaded image: {file_path} (DB ID: {self.current_image_id})")
except ImageLoadError as e:
logger.error(f"Failed to load image: {e}")
@@ -181,7 +194,7 @@ class AnnotationTab(QWidget):
self.image_info_label.setText("No image loaded")
return
zoom_percentage = self.image_display_widget.get_zoom_percentage()
zoom_percentage = self.annotation_canvas.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"
@@ -194,9 +207,36 @@ class AnnotationTab(QWidget):
self.image_info_label.setText(info_text)
def _on_zoom_changed(self, zoom_scale: float):
"""Handle zoom level changes from the image display widget."""
"""Handle zoom level changes from the annotation canvas."""
self._update_image_info()
def _on_annotation_drawn(self, points: list):
"""Handle when an annotation stroke is drawn."""
current_class = self.annotation_tools.get_current_class()
if not current_class:
logger.warning("Annotation drawn but no object class selected")
QMessageBox.warning(
self,
"No Class Selected",
"Please select an object class before drawing annotations.",
)
return
logger.info(
f"Annotation drawn with {len(points)} points for class: {current_class['class_name']}"
)
# Future: Save annotation to database or export
def _on_class_selected(self, class_data: dict):
"""Handle when an object class is selected."""
logger.debug(f"Object class selected: {class_data['class_name']}")
def _on_clear_annotations(self):
"""Handle clearing all annotations."""
self.annotation_canvas.clear_annotations()
logger.info("Cleared all annotations")
def _restore_state(self):
"""Restore splitter positions from settings."""
settings = QSettings("microscopy_app", "object_detection")