diff --git a/src/gui/tabs/annotation_tab.py b/src/gui/tabs/annotation_tab.py index 62b3d22..d517f56 100644 --- a/src/gui/tabs/annotation_tab.py +++ b/src/gui/tabs/annotation_tab.py @@ -90,6 +90,10 @@ class AnnotationTab(QWidget): self.annotation_tools.polyline_pen_width_changed.connect( self.annotation_canvas.set_polyline_pen_width ) + # Show / hide bounding boxes + self.annotation_tools.show_bboxes_changed.connect( + self.annotation_canvas.set_show_bboxes + ) # RDP simplification controls self.annotation_tools.simplify_on_finish_changed.connect( self._on_simplify_on_finish_changed diff --git a/src/gui/widgets/annotation_canvas_widget.py b/src/gui/widgets/annotation_canvas_widget.py index 4a8be4e..acacc72 100644 --- a/src/gui/widgets/annotation_canvas_widget.py +++ b/src/gui/widgets/annotation_canvas_widget.py @@ -146,12 +146,17 @@ class AnnotationCanvasWidget(QWidget): self.polyline_enabled = False self.polyline_pen_color = QColor(255, 0, 0, 128) # Default red with 50% alpha self.polyline_pen_width = 3 + self.show_bboxes: bool = True # Control visibility of bounding boxes # 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) + # Stored bounding boxes in normalized coordinates (x_min, y_min, x_max, y_max) + self.bboxes: List[List[float]] = [] + self.bbox_meta: List[Dict[str, Any]] = [] # per-bbox style (color, width) + # Legacy collection of strokes in normalized coordinates (kept for API compatibility) self.all_strokes: List[dict] = [] @@ -219,6 +224,8 @@ class AnnotationCanvasWidget(QWidget): self.current_stroke = [] self.polylines = [] self.stroke_meta = [] + self.bboxes = [] + self.bbox_meta = [] self.is_drawing = False if self.annotation_pixmap: self.annotation_pixmap.fill(Qt.transparent) @@ -406,7 +413,7 @@ class AnnotationCanvasWidget(QWidget): self._redraw_annotations() def _redraw_annotations(self): - """Redraw all stored polylines onto the annotation pixmap.""" + """Redraw all stored polylines and (optionally) bounding boxes onto the annotation pixmap.""" if self.annotation_pixmap is None: return @@ -414,6 +421,8 @@ class AnnotationCanvasWidget(QWidget): self.annotation_pixmap.fill(Qt.transparent) painter = QPainter(self.annotation_pixmap) + + # Draw polylines for polyline, meta in zip(self.polylines, self.stroke_meta): pen_color: QColor = meta.get("color", self.polyline_pen_color) width: int = meta.get("width", self.polyline_pen_width) @@ -427,6 +436,37 @@ class AnnotationCanvasWidget(QWidget): painter.setPen(pen) for (x1, y1), (x2, y2) in zip(polyline[:-1], polyline[1:]): painter.drawLine(int(x1), int(y1), int(x2), int(y2)) + + # Draw bounding boxes (dashed) if enabled + if self.show_bboxes and self.original_pixmap is not None and self.bboxes: + img_width = float(self.original_pixmap.width()) + img_height = float(self.original_pixmap.height()) + + for bbox, meta in zip(self.bboxes, self.bbox_meta): + if len(bbox) != 4: + continue + + x_min_norm, y_min_norm, x_max_norm, y_max_norm = bbox + x_min = int(x_min_norm * img_width) + y_min = int(y_min_norm * img_height) + x_max = int(x_max_norm * img_width) + y_max = int(y_max_norm * img_height) + + rect_width = x_max - x_min + rect_height = y_max - y_min + + pen_color: QColor = meta.get("color", QColor(255, 0, 0, 128)) + width: int = meta.get("width", self.polyline_pen_width) + pen = QPen( + pen_color, + width, + Qt.DashLine, + Qt.SquareCap, + Qt.MiterJoin, + ) + painter.setPen(pen) + painter.drawRect(x_min, y_min, rect_width, rect_height) + painter.end() self._update_display() @@ -647,7 +687,7 @@ class AnnotationCanvasWidget(QWidget): Draw a bounding box from database coordinates onto the annotation canvas. Args: - bbox: Bounding box as [y_min_norm, x_min_norm, y_max_norm, x_max_norm] + bbox: Bounding box as [x_min_norm, y_min_norm, x_max_norm, y_max_norm] in normalized coordinates (0-1) color: Color hex string (e.g., '#FF0000') width: Line width in pixels @@ -662,8 +702,7 @@ class AnnotationCanvasWidget(QWidget): ) return - # Convert normalized coordinates to image coordinates - # bbox format: [y_min_norm, x_min_norm, y_max_norm, x_max_norm] + # Convert normalized coordinates to image coordinates (for logging/debug) img_width = self.original_pixmap.width() img_height = self.original_pixmap.height() @@ -677,29 +716,35 @@ class AnnotationCanvasWidget(QWidget): logger.debug(f" Image size: {img_width}x{img_height}") logger.debug(f" Pixel coords: ({x_min}, {y_min}) to ({x_max}, {y_max})") - # Draw bounding box on annotation pixmap - painter = QPainter(self.annotation_pixmap) + # Store bounding box (normalized) and its style; actual drawing happens + # in _redraw_annotations() together with all polylines. pen_color = QColor(color) pen_color.setAlpha(128) # Add semi-transparency - pen = QPen(pen_color, width, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) - painter.setPen(pen) - - # Draw rectangle - rect_width = x_max - x_min - rect_height = y_max - y_min - painter.drawRect(x_min, y_min, rect_width, rect_height) - - painter.end() + self.bboxes.append( + [float(x_min_norm), float(y_min_norm), float(x_max_norm), float(y_max_norm)] + ) + self.bbox_meta.append({"color": pen_color, "width": int(width)}) # Store in all_strokes for consistency self.all_strokes.append( {"bbox": bbox, "color": color, "alpha": 128, "width": width} ) - # Update display - self._update_display() + # Redraw overlay (polylines + all bounding boxes) + self._redraw_annotations() logger.debug(f"Drew saved bounding box in color {color}") + def set_show_bboxes(self, show: bool): + """ + Enable or disable drawing of bounding boxes. + + Args: + show: If True, draw bounding boxes; if False, hide them. + """ + self.show_bboxes = bool(show) + logger.debug(f"Set show_bboxes to {self.show_bboxes}") + self._redraw_annotations() + def keyPressEvent(self, event: QKeyEvent): """Handle keyboard events for zooming.""" if event.key() in (Qt.Key_Plus, Qt.Key_Equal): diff --git a/src/gui/widgets/annotation_tools_widget.py b/src/gui/widgets/annotation_tools_widget.py index ddbf690..70312bc 100644 --- a/src/gui/widgets/annotation_tools_widget.py +++ b/src/gui/widgets/annotation_tools_widget.py @@ -53,6 +53,8 @@ class AnnotationToolsWidget(QWidget): polyline_pen_width_changed = Signal(int) simplify_on_finish_changed = Signal(bool) simplify_epsilon_changed = Signal(float) + # Toggle visibility of bounding boxes on the canvas + show_bboxes_changed = Signal(bool) class_selected = Signal(dict) class_color_changed = Signal() clear_annotations_requested = Signal() @@ -170,6 +172,12 @@ class AnnotationToolsWidget(QWidget): actions_group = QGroupBox("Actions") actions_layout = QVBoxLayout() + # Show / hide bounding boxes + self.show_bboxes_checkbox = QCheckBox("Show bounding boxes") + self.show_bboxes_checkbox.setChecked(True) + self.show_bboxes_checkbox.stateChanged.connect(self._on_show_bboxes_toggle) + actions_layout.addWidget(self.show_bboxes_checkbox) + self.clear_btn = QPushButton("Clear All Annotations") self.clear_btn.clicked.connect(self._on_clear_annotations) actions_layout.addWidget(self.clear_btn) @@ -219,12 +227,12 @@ class AnnotationToolsWidget(QWidget): self.polyline_enabled = checked if checked: - self.polyline_toggle_btn.setText("Start Drawing Polyline") + self.polyline_toggle_btn.setText("Stop Drawing Polyline") self.polyline_toggle_btn.setStyleSheet( "QPushButton { background-color: #4CAF50; }" ) else: - self.polyline_toggle_btn.setText("Stop drawing Polyline") + self.polyline_toggle_btn.setText("Start Drawing Polyline") self.polyline_toggle_btn.setStyleSheet("") self.polyline_enabled_changed.emit(self.polyline_enabled) @@ -247,6 +255,12 @@ class AnnotationToolsWidget(QWidget): self.simplify_epsilon_changed.emit(epsilon) logger.debug(f"Simplification epsilon changed to {epsilon}") + def _on_show_bboxes_toggle(self, state: int): + """Handle 'Show bounding boxes' checkbox toggle.""" + show = bool(state) + self.show_bboxes_changed.emit(show) + logger.debug(f"Show bounding boxes set to {show}") + def _on_color_picker(self): """Open color picker dialog and update the selected object's class color.""" if not self.current_class: