diff --git a/src/gui/tabs/annotation_tab.py b/src/gui/tabs/annotation_tab.py index cec0d28..62b3d22 100644 --- a/src/gui/tabs/annotation_tab.py +++ b/src/gui/tabs/annotation_tab.py @@ -81,14 +81,14 @@ class AnnotationTab(QWidget): # 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.polyline_enabled_changed.connect( + self.annotation_canvas.set_polyline_enabled ) - self.annotation_tools.pen_color_changed.connect( - self.annotation_canvas.set_pen_color + self.annotation_tools.polyline_pen_color_changed.connect( + self.annotation_canvas.set_polyline_pen_color ) - self.annotation_tools.pen_width_changed.connect( - self.annotation_canvas.set_pen_width + self.annotation_tools.polyline_pen_width_changed.connect( + self.annotation_canvas.set_polyline_pen_width ) # RDP simplification controls self.annotation_tools.simplify_on_finish_changed.connect( @@ -97,13 +97,12 @@ class AnnotationTab(QWidget): self.annotation_tools.simplify_epsilon_changed.connect( self._on_simplify_epsilon_changed ) + # Class selection and class-color changes self.annotation_tools.class_selected.connect(self._on_class_selected) + self.annotation_tools.class_color_changed.connect(self._on_class_color_changed) self.annotation_tools.clear_annotations_requested.connect( self._on_clear_annotations ) - self.annotation_tools.process_annotations_requested.connect( - self._on_process_annotations - ) self.right_splitter.addWidget(self.annotation_tools) # Image loading section @@ -299,10 +298,37 @@ class AnnotationTab(QWidget): self.annotation_canvas.simplify_epsilon = float(epsilon) logger.debug(f"Annotation simplification epsilon set to {epsilon}") - 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']}") - # When a class is selected, update which annotations are visible + def _on_class_color_changed(self): + """ + Handle changes to the selected object's class color. + + When the user updates a class color in the tools widget, reload the + annotations for the current image so that all polylines are redrawn + using the updated per-class colors. + """ + if not self.current_image_id: + return + + logger.debug( + f"Class color changed; reloading annotations for image ID {self.current_image_id}" + ) + self._load_annotations_for_current_image() + + def _on_class_selected(self, class_data): + """ + Handle when an object class is selected or cleared. + + When a specific class is selected, only annotations of that class are drawn. + When the selection is cleared (\"-- Select Class --\"), all annotations are shown. + """ + if class_data: + logger.debug(f"Object class selected: {class_data['class_name']}") + else: + logger.debug( + 'No class selected ("-- Select Class --"), showing all annotations' + ) + + # Whenever the selection changes, update which annotations are visible self._redraw_annotations_for_current_filter() def _on_clear_annotations(self): @@ -310,28 +336,6 @@ class AnnotationTab(QWidget): self.annotation_canvas.clear_annotations() logger.info("Cleared all annotations") - def _on_process_annotations(self): - """ - Legacy hook kept for UI compatibility. - - Annotations are now saved automatically when a stroke is completed, - so this handler does not perform any additional database writes. - """ - if not self.current_image or not self.current_image_id: - QMessageBox.warning( - self, - "No Image", - "Please load an image before working with annotations.", - ) - return - - QMessageBox.information( - self, - "Annotations Already Saved", - "Annotations are saved automatically as you draw. " - "There is no separate processing step required.", - ) - def _load_annotations_for_current_image(self): """ Load all annotations for the current image from the database and diff --git a/src/gui/widgets/annotation_canvas_widget.py b/src/gui/widgets/annotation_canvas_widget.py index 8ea5b56..4a8be4e 100644 --- a/src/gui/widgets/annotation_canvas_widget.py +++ b/src/gui/widgets/annotation_canvas_widget.py @@ -1,6 +1,6 @@ """ Annotation canvas widget for drawing annotations on images. -Supports pen tool with color selection for manual annotation. +Currently supports polyline drawing tool with color selection for manual annotation. """ import numpy as np @@ -111,11 +111,11 @@ def simplify_polyline( class AnnotationCanvasWidget(QWidget): """ - Widget for displaying images and drawing annotations with pen tool. + Widget for displaying images and drawing annotations with zoom and drawing tools. Features: - Display images with zoom functionality - - Pen tool for drawing annotations + - Polyline tool for drawing annotations - Configurable pen color and width - Mouse-based drawing interface - Zoom in/out with mouse wheel and keyboard @@ -143,9 +143,9 @@ class AnnotationCanvasWidget(QWidget): # Drawing state self.is_drawing = False - self.pen_enabled = False - self.pen_color = QColor(255, 0, 0, 128) # Default red with 50% alpha - self.pen_width = 3 + self.polyline_enabled = False + self.polyline_pen_color = QColor(255, 0, 0, 128) # Default red with 50% alpha + self.polyline_pen_width = 3 # Current stroke and stored polylines (in image coordinates, pixel units) self.current_stroke: List[Tuple[float, float]] = [] @@ -309,21 +309,21 @@ class AnnotationCanvasWidget(QWidget): """Update display after drawing.""" self._apply_zoom() - def set_pen_enabled(self, enabled: bool): - """Enable or disable pen tool.""" - self.pen_enabled = enabled + def set_polyline_enabled(self, enabled: bool): + """Enable or disable polyline tool.""" + self.polyline_enabled = enabled if enabled: self.canvas_label.setCursor(Qt.CrossCursor) else: self.canvas_label.setCursor(Qt.ArrowCursor) - def set_pen_color(self, color: QColor): - """Set pen color.""" - self.pen_color = color + def set_polyline_pen_color(self, color: QColor): + """Set polyline pen color.""" + self.polyline_pen_color = color - def set_pen_width(self, width: int): - """Set pen width.""" - self.pen_width = max(1, width) + def set_polyline_pen_width(self, width: int): + """Set polyline pen width.""" + self.polyline_pen_width = max(1, width) def get_zoom_percentage(self) -> int: """Get current zoom level as percentage.""" @@ -415,8 +415,8 @@ class AnnotationCanvasWidget(QWidget): painter = QPainter(self.annotation_pixmap) for polyline, meta in zip(self.polylines, self.stroke_meta): - pen_color: QColor = meta.get("color", self.pen_color) - width: int = meta.get("width", self.pen_width) + pen_color: QColor = meta.get("color", self.polyline_pen_color) + width: int = meta.get("width", self.polyline_pen_width) pen = QPen( pen_color, width, @@ -433,7 +433,7 @@ class AnnotationCanvasWidget(QWidget): def mousePressEvent(self, event: QMouseEvent): """Handle mouse press events for drawing.""" - if not self.pen_enabled or self.annotation_pixmap is None: + if not self.polyline_enabled or self.annotation_pixmap is None: super().mousePressEvent(event) return @@ -450,7 +450,7 @@ class AnnotationCanvasWidget(QWidget): """Handle mouse move events for drawing.""" if ( not self.is_drawing - or not self.pen_enabled + or not self.polyline_enabled or self.annotation_pixmap is None ): super().mouseMoveEvent(event) @@ -472,8 +472,8 @@ class AnnotationCanvasWidget(QWidget): # Draw line from last point to current point for interactive feedback painter = QPainter(self.annotation_pixmap) pen = QPen( - self.pen_color, - self.pen_width, + self.polyline_pen_color, + self.polyline_pen_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin, @@ -512,7 +512,9 @@ class AnnotationCanvasWidget(QWidget): if len(simplified) >= 2: # Store polyline and redraw all annotations - self._add_polyline(simplified, self.pen_color, self.pen_width) + self._add_polyline( + simplified, self.polyline_pen_color, self.polyline_pen_width + ) # Convert to normalized coordinates for metadata + signal normalized_stroke = [ @@ -522,9 +524,9 @@ class AnnotationCanvasWidget(QWidget): self.all_strokes.append( { "points": normalized_stroke, - "color": self.pen_color.name(), - "alpha": self.pen_color.alpha(), - "width": self.pen_width, + "color": self.polyline_pen_color.name(), + "alpha": self.polyline_pen_color.alpha(), + "width": self.polyline_pen_width, } ) @@ -541,94 +543,6 @@ class AnnotationCanvasWidget(QWidget): """Get all drawn strokes with metadata.""" return self.all_strokes - # def get_annotation_bounds(self) -> Optional[Tuple[float, float, float, float]]: - # """ - # Compute bounding box that encompasses all annotation strokes. - - # Returns: - # Tuple of (x_min, y_min, x_max, y_max) in normalized coordinates (0-1), - # or None if no annotations exist. - # """ - # if not self.all_strokes: - # return None - - # # Find min/max across all strokes - # all_x = [] - # all_y = [] - - # for stroke in self.all_strokes: - # for x, y in stroke["points"]: - # all_x.append(x) - # all_y.append(y) - - # if not all_x: - # return None - - # x_min = min(all_x) - # y_min = min(all_y) - # x_max = max(all_x) - # y_max = max(all_y) - - # return (x_min, y_min, x_max, y_max) - - # def get_annotation_polyline(self) -> List[List[float]]: - # """ - # Get polyline coordinates representing all annotation strokes. - - # Returns: - # List of [x, y] coordinate pairs in normalized coordinates (0-1). - # """ - # polyline = [] - - # fig = plt.figure() - # ax1 = fig.add_subplot(411) - # ax2 = fig.add_subplot(412) - # ax3 = fig.add_subplot(413) - # ax4 = fig.add_subplot(414) - - # # Get np.arrays from annotation_pixmap accoriding to the color of the stroke - # qimage = self.annotation_pixmap.toImage() - # arr = np.ndarray( - # (qimage.height(), qimage.width(), 4), - # buffer=qimage.constBits(), - # strides=[qimage.bytesPerLine(), 4, 1], - # dtype=np.uint8, - # ) - # print(arr.shape, arr.dtype, arr.min(), arr.max()) - # arr = np.sum(arr, axis=2) - # ax1.imshow(arr) - - # arr_bin = arr > 0 - # ax2.imshow(arr_bin) - - # arr_bin = binary_fill_holes(arr_bin) - # ax3.imshow(arr_bin) - - # labels, _number_of_features = label( - # arr_bin, - # ) - - # ax4.imshow(labels) - - # objects = find_objects(labels) - # bounding_boxes = np.array( - # [[obj[0].start, obj[0].stop, obj[1].start, obj[1].stop] for obj in objects] - # ) / np.array([arr.shape[0], arr.shape[1]]) - - # print(objects) - # print(bounding_boxes) - # print(np.array([arr.shape[0], arr.shape[1]])) - - # polylines = find_contours(arr_bin, 0.5) - # for pl in polylines: - # ax1.plot(pl[:, 1], pl[:, 0], "k") - - # print(arr.shape, arr.dtype, arr.min(), arr.max()) - - # plt.show() - - # return polyline - def get_annotation_parameters(self) -> Optional[List[Dict[str, Any]]]: """ Get all annotation parameters including bounding box and polyline. diff --git a/src/gui/widgets/annotation_tools_widget.py b/src/gui/widgets/annotation_tools_widget.py index 89e4340..ddbf690 100644 --- a/src/gui/widgets/annotation_tools_widget.py +++ b/src/gui/widgets/annotation_tools_widget.py @@ -1,6 +1,6 @@ """ Annotation tools widget for controlling annotation parameters. -Includes pen tool, color picker, class selection, and annotation management. +Includes polyline tool, color picker, class selection, and annotation management. """ from PySide6.QtWidgets import ( @@ -33,29 +33,29 @@ class AnnotationToolsWidget(QWidget): Widget for annotation tool controls. Features: - - Enable/disable pen tool - - Color selection for pen + - Enable/disable polyline tool + - Color selection for polyline pen - Object class selection - Add new object classes - Pen width control - Clear annotations Signals: - pen_enabled_changed: Emitted when pen tool is enabled/disabled (bool) - pen_color_changed: Emitted when pen color changes (QColor) - pen_width_changed: Emitted when pen width changes (int) + polyline_enabled_changed: Emitted when polyline tool is enabled/disabled (bool) + polyline_pen_color_changed: Emitted when polyline pen color changes (QColor) + polyline_pen_width_changed: Emitted when polyline pen width changes (int) class_selected: Emitted when object class is selected (dict) clear_annotations_requested: Emitted when clear button is pressed """ - pen_enabled_changed = Signal(bool) - pen_color_changed = Signal(QColor) - pen_width_changed = Signal(int) + polyline_enabled_changed = Signal(bool) + polyline_pen_color_changed = Signal(QColor) + polyline_pen_width_changed = Signal(int) simplify_on_finish_changed = Signal(bool) simplify_epsilon_changed = Signal(float) class_selected = Signal(dict) + class_color_changed = Signal() clear_annotations_requested = Signal() - process_annotations_requested = Signal() def __init__(self, db_manager: DatabaseManager, parent=None): """ @@ -67,7 +67,7 @@ class AnnotationToolsWidget(QWidget): """ super().__init__(parent) self.db_manager = db_manager - self.pen_enabled = False + self.polyline_enabled = False self.current_color = QColor(255, 0, 0, 128) # Red with 50% alpha self.current_class = None @@ -78,40 +78,31 @@ class AnnotationToolsWidget(QWidget): """Setup user interface.""" layout = QVBoxLayout() - # Pen Tool Group - pen_group = QGroupBox("Pen Tool") - pen_layout = QVBoxLayout() + # Polyline Tool Group + polyline_group = QGroupBox("Polyline Tool") + polyline_layout = QVBoxLayout() - # Enable/Disable pen + # Enable/Disable polyline tool button_layout = QHBoxLayout() - self.pen_toggle_btn = QPushButton("Enable Pen") - self.pen_toggle_btn.setCheckable(True) - self.pen_toggle_btn.clicked.connect(self._on_pen_toggle) - button_layout.addWidget(self.pen_toggle_btn) - pen_layout.addLayout(button_layout) + self.polyline_toggle_btn = QPushButton("Start Drawing Polyline") + self.polyline_toggle_btn.setCheckable(True) + self.polyline_toggle_btn.clicked.connect(self._on_polyline_toggle) + button_layout.addWidget(self.polyline_toggle_btn) + polyline_layout.addLayout(button_layout) - # Pen width control + # Polyline pen width control width_layout = QHBoxLayout() width_layout.addWidget(QLabel("Pen Width:")) - self.pen_width_spin = QSpinBox() - self.pen_width_spin.setMinimum(1) - self.pen_width_spin.setMaximum(20) - self.pen_width_spin.setValue(3) - self.pen_width_spin.valueChanged.connect(self._on_pen_width_changed) - width_layout.addWidget(self.pen_width_spin) + self.polyline_pen_width_spin = QSpinBox() + self.polyline_pen_width_spin.setMinimum(1) + self.polyline_pen_width_spin.setMaximum(20) + self.polyline_pen_width_spin.setValue(3) + self.polyline_pen_width_spin.valueChanged.connect( + self._on_polyline_pen_width_changed + ) + width_layout.addWidget(self.polyline_pen_width_spin) width_layout.addStretch() - pen_layout.addLayout(width_layout) - - # Color selection - color_layout = QHBoxLayout() - color_layout.addWidget(QLabel("Color:")) - self.color_btn = QPushButton() - self.color_btn.setFixedSize(40, 30) - self.color_btn.clicked.connect(self._on_color_picker) - self._update_color_button() - color_layout.addWidget(self.color_btn) - color_layout.addStretch() - pen_layout.addLayout(color_layout) + polyline_layout.addLayout(width_layout) # Simplification controls (RDP) simplify_layout = QHBoxLayout() @@ -128,10 +119,10 @@ class AnnotationToolsWidget(QWidget): self.eps_spin.valueChanged.connect(self._on_eps_change) simplify_layout.addWidget(self.eps_spin) simplify_layout.addStretch() - pen_layout.addLayout(simplify_layout) + polyline_layout.addLayout(simplify_layout) - pen_group.setLayout(pen_layout) - layout.addWidget(pen_group) + polyline_group.setLayout(polyline_layout) + layout.addWidget(polyline_group) # Object Class Group class_group = QGroupBox("Object Class") @@ -142,7 +133,7 @@ class AnnotationToolsWidget(QWidget): self.class_combo.currentIndexChanged.connect(self._on_class_selected) class_layout.addWidget(self.class_combo) - # Add class button + # Add / manage classes class_button_layout = QHBoxLayout() self.add_class_btn = QPushButton("Add New Class") self.add_class_btn.clicked.connect(self._on_add_class) @@ -153,6 +144,17 @@ class AnnotationToolsWidget(QWidget): class_button_layout.addWidget(self.refresh_classes_btn) class_layout.addLayout(class_button_layout) + # Class color (associated with selected object class) + color_layout = QHBoxLayout() + color_layout.addWidget(QLabel("Class Color:")) + self.color_btn = QPushButton() + self.color_btn.setFixedSize(40, 30) + self.color_btn.clicked.connect(self._on_color_picker) + self._update_color_button() + color_layout.addWidget(self.color_btn) + color_layout.addStretch() + class_layout.addLayout(color_layout) + # Selected class info self.class_info_label = QLabel("No class selected") self.class_info_label.setWordWrap(True) @@ -168,13 +170,6 @@ class AnnotationToolsWidget(QWidget): actions_group = QGroupBox("Actions") actions_layout = QVBoxLayout() - self.process_btn = QPushButton("Process Annotations") - self.process_btn.clicked.connect(self._on_process_annotations) - self.process_btn.setStyleSheet( - "QPushButton { background-color: #2196F3; color: white; font-weight: bold; }" - ) - actions_layout.addWidget(self.process_btn) - self.clear_btn = QPushButton("Clear All Annotations") self.clear_btn.clicked.connect(self._on_clear_annotations) actions_layout.addWidget(self.clear_btn) @@ -206,7 +201,7 @@ class AnnotationToolsWidget(QWidget): # Clear and repopulate combo box self.class_combo.clear() - self.class_combo.addItem("-- Select Class --", None) + self.class_combo.addItem("-- Select Class / Show All --", None) for cls in classes: self.class_combo.addItem(cls["class_name"], cls) @@ -219,26 +214,26 @@ class AnnotationToolsWidget(QWidget): self, "Error", f"Failed to load object classes:\n{str(e)}" ) - def _on_pen_toggle(self, checked: bool): - """Handle pen tool enable/disable.""" - self.pen_enabled = checked + def _on_polyline_toggle(self, checked: bool): + """Handle polyline tool enable/disable.""" + self.polyline_enabled = checked if checked: - self.pen_toggle_btn.setText("Disable Pen") - self.pen_toggle_btn.setStyleSheet( + self.polyline_toggle_btn.setText("Start Drawing Polyline") + self.polyline_toggle_btn.setStyleSheet( "QPushButton { background-color: #4CAF50; }" ) else: - self.pen_toggle_btn.setText("Enable Pen") - self.pen_toggle_btn.setStyleSheet("") + self.polyline_toggle_btn.setText("Stop drawing Polyline") + self.polyline_toggle_btn.setStyleSheet("") - self.pen_enabled_changed.emit(self.pen_enabled) - logger.debug(f"Pen tool {'enabled' if checked else 'disabled'}") + self.polyline_enabled_changed.emit(self.polyline_enabled) + logger.debug(f"Polyline tool {'enabled' if checked else 'disabled'}") - def _on_pen_width_changed(self, width: int): - """Handle pen width changes.""" - self.pen_width_changed.emit(width) - logger.debug(f"Pen width changed to {width}") + def _on_polyline_pen_width_changed(self, width: int): + """Handle polyline pen width changes.""" + self.polyline_pen_width_changed.emit(width) + logger.debug(f"Polyline pen width changed to {width}") def _on_simplify_toggle(self, state: int): """Handle simplify-on-finish checkbox toggle.""" @@ -253,24 +248,75 @@ class AnnotationToolsWidget(QWidget): logger.debug(f"Simplification epsilon changed to {epsilon}") def _on_color_picker(self): - """Open color picker dialog with alpha support.""" + """Open color picker dialog and update the selected object's class color.""" + if not self.current_class: + QMessageBox.warning( + self, + "No Class Selected", + "Please select an object class before changing its color.", + ) + return + + # Use current class color (without alpha) as the base + base_color = QColor(self.current_class.get("color", self.current_color.name())) color = QColorDialog.getColor( - self.current_color, + base_color, self, - "Select Pen Color", - QColorDialog.ShowAlphaChannel, # Enable alpha channel selection + "Select Class Color", + QColorDialog.ShowAlphaChannel, # Allow alpha in UI, but store RGB in DB ) - if color.isValid(): - self.current_color = color - self._update_color_button() - self.pen_color_changed.emit(color) - logger.debug( - f"Pen color changed to {color.name()} with alpha {color.alpha()}" + if not color.isValid(): + return + + # Normalize to opaque RGB for storage + new_color = QColor(color) + new_color.setAlpha(255) + hex_color = new_color.name() + + try: + # Update in database + self.db_manager.update_object_class( + class_id=self.current_class["id"], color=hex_color ) + except Exception as e: + logger.error(f"Failed to update class color in database: {e}") + QMessageBox.critical( + self, + "Error", + f"Failed to update class color in database:\n{str(e)}", + ) + return + + # Update local class data and combo box item data + self.current_class["color"] = hex_color + current_index = self.class_combo.currentIndex() + if current_index >= 0: + self.class_combo.setItemData(current_index, dict(self.current_class)) + + # Update info label text + info_text = f"Class: {self.current_class['class_name']}\nColor: {hex_color}" + if self.current_class.get("description"): + info_text += f"\nDescription: {self.current_class['description']}" + self.class_info_label.setText(info_text) + + # Use semi-transparent version for polyline pen / button preview + class_color = QColor(hex_color) + class_color.setAlpha(128) + self.current_color = class_color + self._update_color_button() + self.polyline_pen_color_changed.emit(class_color) + + logger.debug( + f"Updated class '{self.current_class['class_name']}' color to " + f"{hex_color} (polyline pen alpha={class_color.alpha()})" + ) + + # Notify listeners (e.g., AnnotationTab) so they can reload/redraw + self.class_color_changed.emit() def _on_class_selected(self, index: int): - """Handle object class selection.""" + """Handle object class selection (including '-- Select Class --').""" class_data = self.class_combo.currentData() if class_data: @@ -285,20 +331,23 @@ class AnnotationToolsWidget(QWidget): self.class_info_label.setText(info_text) - # Update pen color to match class color with semi-transparency + # Update polyline pen color to match class color with semi-transparency class_color = QColor(class_data["color"]) if class_color.isValid(): # Add 50% alpha for semi-transparency class_color.setAlpha(128) self.current_color = class_color self._update_color_button() - self.pen_color_changed.emit(class_color) + self.polyline_pen_color_changed.emit(class_color) self.class_selected.emit(class_data) logger.debug(f"Selected class: {class_data['class_name']}") else: + # "-- Select Class --" chosen: clear current class and show all annotations self.current_class = None self.class_info_label.setText("No class selected") + self.class_selected.emit(None) + logger.debug("Class selection cleared: showing annotations for all classes") def _on_add_class(self): """Handle adding a new object class.""" @@ -376,31 +425,18 @@ class AnnotationToolsWidget(QWidget): self.clear_annotations_requested.emit() logger.debug("Clear annotations requested") - def _on_process_annotations(self): - """Handle process annotations button.""" - if not self.current_class: - QMessageBox.warning( - self, - "No Class Selected", - "Please select an object class before processing annotations.", - ) - return - - self.process_annotations_requested.emit() - logger.debug("Process annotations requested") - def get_current_class(self) -> Optional[Dict]: """Get currently selected object class.""" return self.current_class - def get_pen_color(self) -> QColor: - """Get current pen color.""" + def get_polyline_pen_color(self) -> QColor: + """Get current polyline pen color.""" return self.current_color - def get_pen_width(self) -> int: - """Get current pen width.""" - return self.pen_width_spin.value() + def get_polyline_pen_width(self) -> int: + """Get current polyline pen width.""" + return self.polyline_pen_width_spin.value() - def is_pen_enabled(self) -> bool: - """Check if pen tool is enabled.""" - return self.pen_enabled + def is_polyline_enabled(self) -> bool: + """Check if polyline tool is enabled.""" + return self.polyline_enabled