From dad5c2bf746f6d753be203a447cc696e36f74bcc Mon Sep 17 00:00:00 2001 From: Martin Laasmaa Date: Tue, 9 Dec 2025 22:44:23 +0200 Subject: [PATCH] Updating --- src/gui/tabs/annotation_tab.py | 267 ++++++++-------- src/gui/widgets/annotation_canvas_widget.py | 322 ++++++++++++++------ src/gui/widgets/annotation_tools_widget.py | 46 ++- 3 files changed, 411 insertions(+), 224 deletions(-) diff --git a/src/gui/tabs/annotation_tab.py b/src/gui/tabs/annotation_tab.py index ae16446..cec0d28 100644 --- a/src/gui/tabs/annotation_tab.py +++ b/src/gui/tabs/annotation_tab.py @@ -38,6 +38,7 @@ class AnnotationTab(QWidget): self.current_image = None self.current_image_path = None self.current_image_id = None + self.current_annotations = [] self._setup_ui() @@ -89,6 +90,13 @@ class AnnotationTab(QWidget): self.annotation_tools.pen_width_changed.connect( self.annotation_canvas.set_pen_width ) + # RDP simplification controls + self.annotation_tools.simplify_on_finish_changed.connect( + self._on_simplify_on_finish_changed + ) + self.annotation_tools.simplify_epsilon_changed.connect( + self._on_simplify_epsilon_changed + ) self.annotation_tools.class_selected.connect(self._on_class_selected) self.annotation_tools.clear_annotations_requested.connect( self._on_clear_annotations @@ -96,9 +104,6 @@ class AnnotationTab(QWidget): self.annotation_tools.process_annotations_requested.connect( self._on_process_annotations ) - self.annotation_tools.show_annotations_requested.connect( - self._on_show_annotations - ) self.right_splitter.addWidget(self.annotation_tools) # Image loading section @@ -180,6 +185,9 @@ class AnnotationTab(QWidget): # Display image using the AnnotationCanvasWidget self.annotation_canvas.load_image(self.current_image) + # Load and display any existing annotations for this image + self._load_annotations_for_current_image() + # Update info label self._update_image_info() @@ -217,7 +225,22 @@ class AnnotationTab(QWidget): self._update_image_info() def _on_annotation_drawn(self, points: list): - """Handle when an annotation stroke is drawn.""" + """ + Handle when an annotation stroke is drawn. + + Saves the new annotation directly to the database and refreshes the + on-canvas display of annotations for the current image. + """ + # Ensure we have an image loaded and in the DB + if not self.current_image or not self.current_image_id: + logger.warning("Annotation drawn but no image loaded") + QMessageBox.warning( + self, + "No Image", + "Please load an image before drawing annotations.", + ) + return + current_class = self.annotation_tools.get_current_class() if not current_class: @@ -229,14 +252,58 @@ class AnnotationTab(QWidget): ) return - logger.info( - f"Annotation drawn with {len(points)} points for class: {current_class['class_name']}" - ) - # Future: Save annotation to database or export + if not points: + logger.warning("Annotation drawn with no points, ignoring") + return + + # points are [(x_norm, y_norm), ...] + xs = [p[0] for p in points] + ys = [p[1] for p in points] + x_min, x_max = min(xs), max(xs) + y_min, y_max = min(ys), max(ys) + + # Store segmentation mask in [y_norm, x_norm] format to match DB + db_polyline = [[float(y), float(x)] for (x, y) in points] + + try: + annotation_id = self.db_manager.add_annotation( + image_id=self.current_image_id, + class_id=current_class["id"], + bbox=(x_min, y_min, x_max, y_max), + annotator="manual", + segmentation_mask=db_polyline, + verified=False, + ) + + logger.info( + f"Saved annotation (ID: {annotation_id}) for class " + f"'{current_class['class_name']}' " + f"Bounding box: ({x_min:.3f}, {y_min:.3f}) to ({x_max:.3f}, {y_max:.3f})\n" + f"with {len(points)} polyline points" + ) + + # Reload annotations from DB and redraw (respecting current class filter) + self._load_annotations_for_current_image() + + except Exception as e: + logger.error(f"Failed to save annotation: {e}") + QMessageBox.critical(self, "Error", f"Failed to save annotation:\n{str(e)}") + + def _on_simplify_on_finish_changed(self, enabled: bool): + """Update canvas simplify-on-finish flag from tools widget.""" + self.annotation_canvas.simplify_on_finish = enabled + logger.debug(f"Annotation simplification on finish set to {enabled}") + + def _on_simplify_epsilon_changed(self, epsilon: float): + """Update canvas RDP epsilon from tools widget.""" + 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 + self._redraw_annotations_for_current_filter() def _on_clear_annotations(self): """Handle clearing all annotations.""" @@ -244,138 +311,92 @@ class AnnotationTab(QWidget): logger.info("Cleared all annotations") def _on_process_annotations(self): - """Process annotations and save to database.""" - # Check if we have an image loaded + """ + 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 processing annotations." - ) - return - - # Get current class - current_class = self.annotation_tools.get_current_class() - if not current_class: QMessageBox.warning( self, - "No Class Selected", - "Please select an object class before processing annotations.", + "No Image", + "Please load an image before working with annotations.", ) return - # Compute annotation parameters asbounding boxes and polylines from annotations - parameters = self.annotation_canvas.get_annotation_parameters() - if not parameters: - QMessageBox.warning( - self, - "No Annotations", - "Please draw some annotations before processing.", - ) - return - - # polyline = self.annotation_canvas.get_annotation_polyline() - - for param in parameters: - bounds = param["bbox"] - polyline = param["polyline"] - - try: - # Save annotation to database - annotation_id = self.db_manager.add_annotation( - image_id=self.current_image_id, - class_id=current_class["id"], - bbox=bounds, - annotator="manual", - segmentation_mask=polyline, - verified=False, - ) - - logger.info( - f"Saved annotation (ID: {annotation_id}) for class '{current_class['class_name']}' " - f"Bounding box: ({bounds[0]:.3f}, {bounds[1]:.3f}) to ({bounds[2]:.3f}, {bounds[3]:.3f})\n" - f"with {len(polyline)} polyline points" - ) - - # QMessageBox.information( - # self, - # "Success", - # f"Annotation saved successfully!\n\n" - # f"Class: {current_class['class_name']}\n" - # f"Bounding box: ({bounds[0]:.3f}, {bounds[1]:.3f}) to ({bounds[2]:.3f}, {bounds[3]:.3f})\n" - # f"Polyline points: {len(polyline)}", - # ) - - except Exception as e: - logger.error(f"Failed to save annotation: {e}") - QMessageBox.critical( - self, "Error", f"Failed to save annotation:\n{str(e)}" - ) - - # Optionally clear annotations after saving - reply = QMessageBox.question( + QMessageBox.information( self, - "Clear Annotations", - "Do you want to clear the annotations to start a new one?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes, + "Annotations Already Saved", + "Annotations are saved automatically as you draw. " + "There is no separate processing step required.", ) - if reply == QMessageBox.Yes: + def _load_annotations_for_current_image(self): + """ + Load all annotations for the current image from the database and + redraw them on the canvas, honoring the currently selected class + filter (if any). + """ + if not self.current_image_id: + self.current_annotations = [] self.annotation_canvas.clear_annotations() - logger.info("Cleared annotations after saving") - - def _on_show_annotations(self): - """Load and display saved annotations from database.""" - # Check if we have an image loaded - if not self.current_image or not self.current_image_id: - QMessageBox.warning( - self, "No Image", "Please load an image to view its annotations." - ) return try: - # Clear current annotations - self.annotation_canvas.clear_annotations() - - # Retrieve annotations from database - annotations = self.db_manager.get_annotations_for_image( + self.current_annotations = self.db_manager.get_annotations_for_image( self.current_image_id ) - - if not annotations: - QMessageBox.information( - self, "No Annotations", "No saved annotations found for this image." - ) - return - - # Draw each annotation's polyline - drawn_count = 0 - for ann in annotations: - if ann.get("segmentation_mask"): - polyline = ann["segmentation_mask"] - color = ann.get("class_color", "#FF0000") - - # Draw the polyline - self.annotation_canvas.draw_saved_polyline(polyline, color, width=3) - self.annotation_canvas.draw_saved_bbox( - [ann["x_min"], ann["y_min"], ann["x_max"], ann["y_max"]], - color, - width=3, - ) - drawn_count += 1 - - logger.info(f"Displayed {drawn_count} saved annotations from database") - - QMessageBox.information( - self, - "Annotations Loaded", - f"Successfully loaded and displayed {drawn_count} annotation(s).", - ) - + self._redraw_annotations_for_current_filter() except Exception as e: - logger.error(f"Failed to load annotations: {e}") - QMessageBox.critical( - self, "Error", f"Failed to load annotations:\n{str(e)}" + logger.error( + f"Failed to load annotations for image {self.current_image_id}: {e}" ) + QMessageBox.critical( + self, + "Error", + f"Failed to load annotations for this image:\n{str(e)}", + ) + + def _redraw_annotations_for_current_filter(self): + """ + Redraw annotations for the current image, optionally filtered by the + currently selected object class. + """ + # Clear current on-canvas annotations but keep the image + self.annotation_canvas.clear_annotations() + + if not self.current_annotations: + return + + current_class = self.annotation_tools.get_current_class() + selected_class_id = current_class["id"] if current_class else None + + drawn_count = 0 + for ann in self.current_annotations: + # Filter by class if one is selected + if ( + selected_class_id is not None + and ann.get("class_id") != selected_class_id + ): + continue + + if ann.get("segmentation_mask"): + polyline = ann["segmentation_mask"] + color = ann.get("class_color", "#FF0000") + + self.annotation_canvas.draw_saved_polyline(polyline, color, width=3) + self.annotation_canvas.draw_saved_bbox( + [ann["x_min"], ann["y_min"], ann["x_max"], ann["y_max"]], + color, + width=3, + ) + drawn_count += 1 + + logger.info( + f"Displayed {drawn_count} annotation(s) for current image with " + f"{'no class filter' if selected_class_id is None else f'class_id={selected_class_id}'}" + ) def _restore_state(self): """Restore splitter positions from settings.""" diff --git a/src/gui/widgets/annotation_canvas_widget.py b/src/gui/widgets/annotation_canvas_widget.py index 3f57c2a..8ea5b56 100644 --- a/src/gui/widgets/annotation_canvas_widget.py +++ b/src/gui/widgets/annotation_canvas_widget.py @@ -4,6 +4,7 @@ Supports pen tool with color selection for manual annotation. """ import numpy as np +import math from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QScrollArea from PySide6.QtGui import ( @@ -19,18 +20,95 @@ from PySide6.QtGui import ( from PySide6.QtCore import Qt, QEvent, Signal, QPoint from typing import Any, Dict, List, Optional, Tuple -from scipy.ndimage import binary_dilation, label, binary_fill_holes, find_objects -from skimage.measure import find_contours - from src.utils.image import Image, ImageLoadError from src.utils.logger import get_logger -# For debugging visualization -import pylab as plt - logger = get_logger(__name__) +def perpendicular_distance( + point: Tuple[float, float], + start: Tuple[float, float], + end: Tuple[float, float], +) -> float: + """Perpendicular distance from `point` to the line defined by `start`->`end`.""" + (x, y), (x1, y1), (x2, y2) = point, start, end + dx = x2 - x1 + dy = y2 - y1 + if dx == 0.0 and dy == 0.0: + return math.hypot(x - x1, y - y1) + num = abs(dy * x - dx * y + x2 * y1 - y2 * x1) + den = math.hypot(dx, dy) + return num / den + + +def rdp(points: List[Tuple[float, float]], epsilon: float) -> List[Tuple[float, float]]: + """ + Recursive Ramer-Douglas-Peucker (RDP) polyline simplification. + + Args: + points: List of (x, y) points. + epsilon: Maximum allowed perpendicular distance in pixels. + + Returns: + Simplified list of (x, y) points including first and last. + """ + if len(points) <= 2: + return list(points) + + start = points[0] + end = points[-1] + max_dist = -1.0 + index = -1 + + for i in range(1, len(points) - 1): + d = perpendicular_distance(points[i], start, end) + if d > max_dist: + max_dist = d + index = i + + if max_dist > epsilon: + # Recursive split + left = rdp(points[: index + 1], epsilon) + right = rdp(points[index:], epsilon) + # Concatenate but avoid duplicate at split point + return left[:-1] + right + + # Keep only start and end + return [start, end] + + +def simplify_polyline( + points: List[Tuple[float, float]], epsilon: float +) -> List[Tuple[float, float]]: + """ + Simplify a polyline with RDP while preserving closure semantics. + + If the polyline is closed (first == last), the duplicate last point is removed + before simplification and then re-added after simplification. + """ + if not points: + return [] + + pts = [(float(x), float(y)) for x, y in points] + closed = False + + if len(pts) >= 2 and pts[0] == pts[-1]: + closed = True + pts = pts[:-1] # remove duplicate last for simplification + + if len(pts) <= 2: + simplified = list(pts) + else: + simplified = rdp(pts, epsilon) + + if closed and simplified: + if simplified[0] != simplified[-1]: + simplified.append(simplified[0]) + + return simplified + + class AnnotationCanvasWidget(QWidget): """ Widget for displaying images and drawing annotations with pen tool. @@ -68,8 +146,19 @@ class AnnotationCanvasWidget(QWidget): self.pen_enabled = False self.pen_color = QColor(255, 0, 0, 128) # Default red with 50% alpha self.pen_width = 3 - self.current_stroke = [] # Points in current stroke - self.all_strokes = [] # All completed strokes + + # Current stroke and stored polylines (in image coordinates, pixel units) + self.current_stroke: List[Tuple[float, float]] = [] + self.polylines: List[List[Tuple[float, float]]] = [] + self.stroke_meta: List[Dict[str, Any]] = [] # per-polyline style (color, width) + + # Legacy collection of strokes in normalized coordinates (kept for API compatibility) + self.all_strokes: List[dict] = [] + + # RDP simplification parameters (in pixels) + self.simplify_on_finish: bool = True + self.simplify_epsilon: float = 2.0 + self.sample_threshold: float = 2.0 # minimum movement to sample a new point self._setup_ui() @@ -128,6 +217,8 @@ class AnnotationCanvasWidget(QWidget): """Clear all drawn annotations.""" self.all_strokes = [] self.current_stroke = [] + self.polylines = [] + self.stroke_meta = [] self.is_drawing = False if self.annotation_pixmap: self.annotation_pixmap.fill(Qt.transparent) @@ -300,6 +391,46 @@ class AnnotationCanvasWidget(QWidget): norm_y = y / self.original_pixmap.height() return (norm_x, norm_y) + def _add_polyline( + self, img_points: List[Tuple[float, float]], color: QColor, width: int + ): + """Store a polyline in image coordinates and redraw annotations.""" + if not img_points or len(img_points) < 2: + return + + # Ensure all points are tuples of floats + normalized_points = [(float(x), float(y)) for x, y in img_points] + self.polylines.append(normalized_points) + self.stroke_meta.append({"color": QColor(color), "width": int(width)}) + + self._redraw_annotations() + + def _redraw_annotations(self): + """Redraw all stored polylines onto the annotation pixmap.""" + if self.annotation_pixmap is None: + return + + # Clear existing overlay + self.annotation_pixmap.fill(Qt.transparent) + + 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 = QPen( + pen_color, + width, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, + ) + painter.setPen(pen) + for (x1, y1), (x2, y2) in zip(polyline[:-1], polyline[1:]): + painter.drawLine(int(x1), int(y1), int(x2), int(y2)) + painter.end() + + self._update_display() + def mousePressEvent(self, event: QMouseEvent): """Handle mouse press events for drawing.""" if not self.pen_enabled or self.annotation_pixmap is None: @@ -313,7 +444,7 @@ class AnnotationCanvasWidget(QWidget): if img_coords: self.is_drawing = True - self.current_stroke = [img_coords] + self.current_stroke = [(float(img_coords[0]), float(img_coords[1]))] def mouseMoveEvent(self, event: QMouseEvent): """Handle mouse move events for drawing.""" @@ -330,18 +461,33 @@ class AnnotationCanvasWidget(QWidget): img_coords = self._canvas_to_image_coords(label_pos) if img_coords and len(self.current_stroke) > 0: - # Draw line from last point to current point + last_point = self.current_stroke[-1] + dx = img_coords[0] - last_point[0] + dy = img_coords[1] - last_point[1] + + # Only sample a new point if we moved enough pixels + if math.hypot(dx, dy) < self.sample_threshold: + return + + # Draw line from last point to current point for interactive feedback painter = QPainter(self.annotation_pixmap) pen = QPen( - self.pen_color, self.pen_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin + self.pen_color, + self.pen_width, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, ) painter.setPen(pen) - - last_point = self.current_stroke[-1] - painter.drawLine(last_point[0], last_point[1], img_coords[0], img_coords[1]) + painter.drawLine( + int(last_point[0]), + int(last_point[1]), + int(img_coords[0]), + int(img_coords[1]), + ) painter.end() - self.current_stroke.append(img_coords) + self.current_stroke.append((float(img_coords[0]), float(img_coords[1]))) self._update_display() def mouseReleaseEvent(self, event: QMouseEvent): @@ -352,23 +498,42 @@ class AnnotationCanvasWidget(QWidget): self.is_drawing = False - if len(self.current_stroke) > 1: - # Convert to normalized coordinates and save stroke - normalized_stroke = [ - self._image_to_normalized_coords(x, y) for x, y in self.current_stroke - ] - self.all_strokes.append( - { - "points": normalized_stroke, - "color": self.pen_color.name(), - "alpha": self.pen_color.alpha(), - "width": self.pen_width, - } - ) + if len(self.current_stroke) > 1 and self.original_pixmap is not None: + # Ensure the stroke is closed by connecting end -> start + raw_points = list(self.current_stroke) + if raw_points[0] != raw_points[-1]: + raw_points.append(raw_points[0]) - # Emit signal with normalized coordinates - self.annotation_drawn.emit(normalized_stroke) - logger.debug(f"Completed stroke with {len(normalized_stroke)} points") + # Optional RDP simplification (in image pixel space) + if self.simplify_on_finish: + simplified = simplify_polyline(raw_points, self.simplify_epsilon) + else: + simplified = raw_points + + if len(simplified) >= 2: + # Store polyline and redraw all annotations + self._add_polyline(simplified, self.pen_color, self.pen_width) + + # Convert to normalized coordinates for metadata + signal + normalized_stroke = [ + self._image_to_normalized_coords(int(x), int(y)) + for (x, y) in simplified + ] + self.all_strokes.append( + { + "points": normalized_stroke, + "color": self.pen_color.name(), + "alpha": self.pen_color.alpha(), + "width": self.pen_width, + } + ) + + # Emit signal with normalized coordinates + self.annotation_drawn.emit(normalized_stroke) + logger.debug( + f"Completed stroke with {len(simplified)} points " + f"(normalized len={len(normalized_stroke)})" + ) self.current_stroke = [] @@ -464,61 +629,54 @@ class AnnotationCanvasWidget(QWidget): # return polyline - def get_annotation_parameters(self) -> Dict[str, Any]: + def get_annotation_parameters(self) -> Optional[List[Dict[str, Any]]]: """ Get all annotation parameters including bounding box and polyline. Returns: - Dictionary containing: - - 'bbox': Bounding box coordinates (x_min, y_min, x_max, y_max) - - 'polyline': List of [x, y] coordinate pairs + List of dictionaries, each containing: + - 'bbox': [x_min, y_min, x_max, y_max] in normalized image coordinates + - 'polyline': List of [y_norm, x_norm] points describing the polygon """ - - # 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, - ) - arr = np.sum(arr, axis=2) - arr_bin = arr > 0 - arr_bin = binary_fill_holes(arr_bin) - - labels, _number_of_features = label( - arr_bin, - ) - if _number_of_features == 0: + if self.original_pixmap is None or not self.polylines: return None - objects = find_objects(labels) - w, h = arr.shape - bounding_boxes = [ - [obj[1].start / h, obj[0].start / w, obj[1].stop / h, obj[0].stop / w] - for obj in objects - ] + img_width = float(self.original_pixmap.width()) + img_height = float(self.original_pixmap.height()) - polylines = find_contours(arr_bin, 0.5) - params = [] - for i, pl in enumerate(polylines): - # pl is in [row, col] format from find_contours - # We need to normalize: row/height, col/width - # w = height (rows), h = width (cols) from line 510 - normalized_polyline = (pl[::-1] / np.array([w, h])).tolist() + params: List[Dict[str, Any]] = [] - logger.debug(f"Polyline {i}: {len(pl)} points") - logger.debug(f" w={w} (height), h={h} (width)") - logger.debug(f" First 3 normalized points: {normalized_polyline[:3]}") + for idx, polyline in enumerate(self.polylines): + if len(polyline) < 2: + continue + + xs = [p[0] for p in polyline] + ys = [p[1] for p in polyline] + + x_min_norm = min(xs) / img_width + x_max_norm = max(xs) / img_width + y_min_norm = min(ys) / img_height + y_max_norm = max(ys) / img_height + + # Store polyline as [y_norm, x_norm] to match DB convention and + # the expectations of draw_saved_polyline(). + normalized_polyline = [ + [y / img_height, x / img_width] for (x, y) in polyline + ] + + logger.debug( + f"Polyline {idx}: {len(polyline)} points, " + f"bbox=({x_min_norm:.3f}, {y_min_norm:.3f})-({x_max_norm:.3f}, {y_max_norm:.3f})" + ) params.append( { - "bbox": bounding_boxes[i], + "bbox": [x_min_norm, y_min_norm, x_max_norm, y_max_norm], "polyline": normalized_polyline, } ) - return params + return params or None def draw_saved_polyline( self, polyline: List[List[float]], color: str, width: int = 3 @@ -548,36 +706,24 @@ class AnnotationCanvasWidget(QWidget): logger.debug(f" Image size: {img_width}x{img_height}") logger.debug(f" First 3 normalized points from DB: {polyline[:3]}") - img_coords = [] + img_coords: List[Tuple[float, float]] = [] for y_norm, x_norm in polyline: - x = int(x_norm * img_width) - y = int(y_norm * img_height) + x = float(x_norm * img_width) + y = float(y_norm * img_height) img_coords.append((x, y)) logger.debug(f" First 3 pixel coords: {img_coords[:3]}") - # Draw polyline on annotation pixmap - painter = QPainter(self.annotation_pixmap) + # Store and redraw using common pipeline pen_color = QColor(color) pen_color.setAlpha(128) # Add semi-transparency - pen = QPen(pen_color, width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) - painter.setPen(pen) + self._add_polyline(img_coords, pen_color, width) - # Draw lines between consecutive points - for i in range(len(img_coords) - 1): - x1, y1 = img_coords[i] - x2, y2 = img_coords[i + 1] - painter.drawLine(x1, y1, x2, y2) - - painter.end() - - # Store in all_strokes for consistency + # Store in all_strokes for consistency (uses normalized coordinates) self.all_strokes.append( {"points": polyline, "color": color, "alpha": 128, "width": width} ) - # Update display - self._update_display() logger.debug( f"Drew saved polyline with {len(polyline)} points in color {color}" ) diff --git a/src/gui/widgets/annotation_tools_widget.py b/src/gui/widgets/annotation_tools_widget.py index 4153dc1..89e4340 100644 --- a/src/gui/widgets/annotation_tools_widget.py +++ b/src/gui/widgets/annotation_tools_widget.py @@ -12,6 +12,8 @@ from PySide6.QtWidgets import ( QPushButton, QComboBox, QSpinBox, + QDoubleSpinBox, + QCheckBox, QColorDialog, QInputDialog, QMessageBox, @@ -49,10 +51,11 @@ class AnnotationToolsWidget(QWidget): pen_enabled_changed = Signal(bool) pen_color_changed = Signal(QColor) pen_width_changed = Signal(int) + simplify_on_finish_changed = Signal(bool) + simplify_epsilon_changed = Signal(float) class_selected = Signal(dict) clear_annotations_requested = Signal() process_annotations_requested = Signal() - show_annotations_requested = Signal() def __init__(self, db_manager: DatabaseManager, parent=None): """ @@ -110,6 +113,23 @@ class AnnotationToolsWidget(QWidget): color_layout.addStretch() pen_layout.addLayout(color_layout) + # Simplification controls (RDP) + simplify_layout = QHBoxLayout() + self.simplify_checkbox = QCheckBox("Simplify on finish") + self.simplify_checkbox.setChecked(True) + self.simplify_checkbox.stateChanged.connect(self._on_simplify_toggle) + simplify_layout.addWidget(self.simplify_checkbox) + + simplify_layout.addWidget(QLabel("epsilon (px):")) + self.eps_spin = QDoubleSpinBox() + self.eps_spin.setRange(0.0, 1000.0) + self.eps_spin.setSingleStep(0.5) + self.eps_spin.setValue(2.0) + self.eps_spin.valueChanged.connect(self._on_eps_change) + simplify_layout.addWidget(self.eps_spin) + simplify_layout.addStretch() + pen_layout.addLayout(simplify_layout) + pen_group.setLayout(pen_layout) layout.addWidget(pen_group) @@ -155,13 +175,6 @@ class AnnotationToolsWidget(QWidget): ) actions_layout.addWidget(self.process_btn) - self.show_btn = QPushButton("Show Saved Annotations") - self.show_btn.clicked.connect(self._on_show_annotations) - self.show_btn.setStyleSheet( - "QPushButton { background-color: #4CAF50; color: white; }" - ) - actions_layout.addWidget(self.show_btn) - self.clear_btn = QPushButton("Clear All Annotations") self.clear_btn.clicked.connect(self._on_clear_annotations) actions_layout.addWidget(self.clear_btn) @@ -227,6 +240,18 @@ class AnnotationToolsWidget(QWidget): self.pen_width_changed.emit(width) logger.debug(f"Pen width changed to {width}") + def _on_simplify_toggle(self, state: int): + """Handle simplify-on-finish checkbox toggle.""" + enabled = bool(state) + self.simplify_on_finish_changed.emit(enabled) + logger.debug(f"Simplify on finish set to {enabled}") + + def _on_eps_change(self, val: float): + """Handle epsilon (RDP tolerance) value changes.""" + epsilon = float(val) + self.simplify_epsilon_changed.emit(epsilon) + logger.debug(f"Simplification epsilon changed to {epsilon}") + def _on_color_picker(self): """Open color picker dialog with alpha support.""" color = QColorDialog.getColor( @@ -364,11 +389,6 @@ class AnnotationToolsWidget(QWidget): self.process_annotations_requested.emit() logger.debug("Process annotations requested") - def _on_show_annotations(self): - """Handle show annotations button.""" - self.show_annotations_requested.emit() - logger.debug("Show annotations requested") - def get_current_class(self) -> Optional[Dict]: """Get currently selected object class.""" return self.current_class