Adding feature to remove annotations
This commit is contained in:
@@ -127,6 +127,9 @@ class AnnotationCanvasWidget(QWidget):
|
||||
|
||||
zoom_changed = Signal(float)
|
||||
annotation_drawn = Signal(list) # List of (x, y) points in normalized coordinates
|
||||
# Emitted when the user selects an existing polyline on the canvas.
|
||||
# Carries the associated annotation_id (int) or None if selection is cleared
|
||||
annotation_selected = Signal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the annotation canvas widget."""
|
||||
@@ -141,7 +144,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self.zoom_step = 0.1
|
||||
self.zoom_wheel_step = 0.15
|
||||
|
||||
# Drawing state
|
||||
# Drawing / interaction state
|
||||
self.is_drawing = False
|
||||
self.polyline_enabled = False
|
||||
self.polyline_pen_color = QColor(255, 0, 0, 128) # Default red with 50% alpha
|
||||
@@ -152,6 +155,10 @@ class AnnotationCanvasWidget(QWidget):
|
||||
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)
|
||||
# Optional DB annotation_id for each stored polyline (None for temporary / unsaved)
|
||||
self.polyline_annotation_ids: List[Optional[int]] = []
|
||||
# Indices in self.polylines of the currently selected polylines (multi-select)
|
||||
self.selected_polyline_indices: List[int] = []
|
||||
|
||||
# Stored bounding boxes in normalized coordinates (x_min, y_min, x_max, y_max)
|
||||
self.bboxes: List[List[float]] = []
|
||||
@@ -224,6 +231,8 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self.current_stroke = []
|
||||
self.polylines = []
|
||||
self.stroke_meta = []
|
||||
self.polyline_annotation_ids = []
|
||||
self.selected_polyline_indices = []
|
||||
self.bboxes = []
|
||||
self.bbox_meta = []
|
||||
self.is_drawing = False
|
||||
@@ -389,6 +398,41 @@ class AnnotationCanvasWidget(QWidget):
|
||||
return (int(x), int(y))
|
||||
return None
|
||||
|
||||
def _find_polyline_at(
|
||||
self, img_x: float, img_y: float, threshold_px: float = 5.0
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Find index of polyline whose geometry is within threshold_px of (img_x, img_y).
|
||||
Returns the index in self.polylines, or None if none is close enough.
|
||||
"""
|
||||
best_index: Optional[int] = None
|
||||
best_dist: float = float("inf")
|
||||
|
||||
for idx, polyline in enumerate(self.polylines):
|
||||
if len(polyline) < 2:
|
||||
continue
|
||||
|
||||
# Quick bounding-box check to skip obviously distant polylines
|
||||
xs = [p[0] for p in polyline]
|
||||
ys = [p[1] for p in polyline]
|
||||
if img_x < min(xs) - threshold_px or img_x > max(xs) + threshold_px:
|
||||
continue
|
||||
if img_y < min(ys) - threshold_px or img_y > max(ys) + threshold_px:
|
||||
continue
|
||||
|
||||
# Precise distance to all segments
|
||||
for (x1, y1), (x2, y2) in zip(polyline[:-1], polyline[1:]):
|
||||
d = perpendicular_distance(
|
||||
(img_x, img_y), (float(x1), float(y1)), (float(x2), float(y2))
|
||||
)
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
best_index = idx
|
||||
|
||||
if best_index is not None and best_dist <= threshold_px:
|
||||
return best_index
|
||||
return None
|
||||
|
||||
def _image_to_normalized_coords(self, x: int, y: int) -> Tuple[float, float]:
|
||||
"""Convert image coordinates to normalized coordinates (0-1)."""
|
||||
if self.original_pixmap is None:
|
||||
@@ -399,7 +443,11 @@ class AnnotationCanvasWidget(QWidget):
|
||||
return (norm_x, norm_y)
|
||||
|
||||
def _add_polyline(
|
||||
self, img_points: List[Tuple[float, float]], color: QColor, width: int
|
||||
self,
|
||||
img_points: List[Tuple[float, float]],
|
||||
color: QColor,
|
||||
width: int,
|
||||
annotation_id: Optional[int] = None,
|
||||
):
|
||||
"""Store a polyline in image coordinates and redraw annotations."""
|
||||
if not img_points or len(img_points) < 2:
|
||||
@@ -409,6 +457,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
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.polyline_annotation_ids.append(annotation_id)
|
||||
|
||||
self._redraw_annotations()
|
||||
|
||||
@@ -423,16 +472,29 @@ class AnnotationCanvasWidget(QWidget):
|
||||
painter = QPainter(self.annotation_pixmap)
|
||||
|
||||
# Draw polylines
|
||||
for polyline, meta in zip(self.polylines, self.stroke_meta):
|
||||
for idx, (polyline, meta) in enumerate(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)
|
||||
pen = QPen(
|
||||
pen_color,
|
||||
width,
|
||||
Qt.SolidLine,
|
||||
Qt.RoundCap,
|
||||
Qt.RoundJoin,
|
||||
)
|
||||
|
||||
if idx in self.selected_polyline_indices:
|
||||
# Highlight selected polylines in a distinct color / width
|
||||
highlight_color = QColor(255, 255, 0, 200) # yellow, semi-opaque
|
||||
pen = QPen(
|
||||
highlight_color,
|
||||
width + 1,
|
||||
Qt.SolidLine,
|
||||
Qt.RoundCap,
|
||||
Qt.RoundJoin,
|
||||
)
|
||||
else:
|
||||
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))
|
||||
@@ -472,19 +534,58 @@ class AnnotationCanvasWidget(QWidget):
|
||||
self._update_display()
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
"""Handle mouse press events for drawing."""
|
||||
if not self.polyline_enabled or self.annotation_pixmap is None:
|
||||
"""Handle mouse press events for drawing and selecting polylines."""
|
||||
if self.annotation_pixmap is None:
|
||||
super().mousePressEvent(event)
|
||||
return
|
||||
|
||||
if event.button() == Qt.LeftButton:
|
||||
# Get accurate position using global coordinates
|
||||
label_pos = self.canvas_label.mapFromGlobal(event.globalPos())
|
||||
img_coords = self._canvas_to_image_coords(label_pos)
|
||||
# Map click to image coordinates
|
||||
label_pos = self.canvas_label.mapFromGlobal(event.globalPos())
|
||||
img_coords = self._canvas_to_image_coords(label_pos)
|
||||
|
||||
# Left button + drawing tool enabled -> start a new stroke
|
||||
if event.button() == Qt.LeftButton and self.polyline_enabled:
|
||||
if img_coords:
|
||||
self.is_drawing = True
|
||||
self.current_stroke = [(float(img_coords[0]), float(img_coords[1]))]
|
||||
return
|
||||
|
||||
# Left button + drawing tool disabled -> attempt selection of existing polyline
|
||||
if event.button() == Qt.LeftButton and not self.polyline_enabled:
|
||||
if img_coords:
|
||||
idx = self._find_polyline_at(float(img_coords[0]), float(img_coords[1]))
|
||||
if idx is not None:
|
||||
if event.modifiers() & Qt.ShiftModifier:
|
||||
# Multi-select mode: add to current selection (if not already selected)
|
||||
if idx not in self.selected_polyline_indices:
|
||||
self.selected_polyline_indices.append(idx)
|
||||
else:
|
||||
# Single-select mode: replace current selection
|
||||
self.selected_polyline_indices = [idx]
|
||||
|
||||
# Build list of selected annotation IDs (ignore None entries)
|
||||
selected_ids: List[int] = []
|
||||
for sel_idx in self.selected_polyline_indices:
|
||||
if 0 <= sel_idx < len(self.polyline_annotation_ids):
|
||||
ann_id = self.polyline_annotation_ids[sel_idx]
|
||||
if isinstance(ann_id, int):
|
||||
selected_ids.append(ann_id)
|
||||
|
||||
if selected_ids:
|
||||
self.annotation_selected.emit(selected_ids)
|
||||
else:
|
||||
# No valid DB-backed annotations in selection
|
||||
self.annotation_selected.emit(None)
|
||||
else:
|
||||
# Clicked on empty space -> clear selection
|
||||
self.selected_polyline_indices = []
|
||||
self.annotation_selected.emit(None)
|
||||
|
||||
self._redraw_annotations()
|
||||
return
|
||||
|
||||
# Fallback for other buttons / cases
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event: QMouseEvent):
|
||||
"""Handle mouse move events for drawing."""
|
||||
@@ -633,7 +734,11 @@ class AnnotationCanvasWidget(QWidget):
|
||||
return params or None
|
||||
|
||||
def draw_saved_polyline(
|
||||
self, polyline: List[List[float]], color: str, width: int = 3
|
||||
self,
|
||||
polyline: List[List[float]],
|
||||
color: str,
|
||||
width: int = 3,
|
||||
annotation_id: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Draw a polyline from database coordinates onto the annotation canvas.
|
||||
@@ -671,7 +776,7 @@ class AnnotationCanvasWidget(QWidget):
|
||||
# Store and redraw using common pipeline
|
||||
pen_color = QColor(color)
|
||||
pen_color.setAlpha(128) # Add semi-transparency
|
||||
self._add_polyline(img_coords, pen_color, width)
|
||||
self._add_polyline(img_coords, pen_color, width, annotation_id=annotation_id)
|
||||
|
||||
# Store in all_strokes for consistency (uses normalized coordinates)
|
||||
self.all_strokes.append(
|
||||
|
||||
@@ -58,6 +58,8 @@ class AnnotationToolsWidget(QWidget):
|
||||
class_selected = Signal(dict)
|
||||
class_color_changed = Signal()
|
||||
clear_annotations_requested = Signal()
|
||||
# Request deletion of the currently selected annotation on the canvas
|
||||
delete_selected_annotation_requested = Signal()
|
||||
|
||||
def __init__(self, db_manager: DatabaseManager, parent=None):
|
||||
"""
|
||||
@@ -182,6 +184,12 @@ class AnnotationToolsWidget(QWidget):
|
||||
self.clear_btn.clicked.connect(self._on_clear_annotations)
|
||||
actions_layout.addWidget(self.clear_btn)
|
||||
|
||||
# Delete currently selected annotation (enabled when a selection exists)
|
||||
self.delete_selected_btn = QPushButton("Delete Selected Annotation")
|
||||
self.delete_selected_btn.clicked.connect(self._on_delete_selected_annotation)
|
||||
self.delete_selected_btn.setEnabled(False)
|
||||
actions_layout.addWidget(self.delete_selected_btn)
|
||||
|
||||
actions_group.setLayout(actions_layout)
|
||||
layout.addWidget(actions_group)
|
||||
|
||||
@@ -439,6 +447,20 @@ class AnnotationToolsWidget(QWidget):
|
||||
self.clear_annotations_requested.emit()
|
||||
logger.debug("Clear annotations requested")
|
||||
|
||||
def _on_delete_selected_annotation(self):
|
||||
"""Handle delete selected annotation button."""
|
||||
self.delete_selected_annotation_requested.emit()
|
||||
logger.debug("Delete selected annotation requested")
|
||||
|
||||
def set_has_selected_annotation(self, has_selection: bool):
|
||||
"""
|
||||
Enable/disable actions that require a selected annotation.
|
||||
|
||||
Args:
|
||||
has_selection: True if an annotation is currently selected on the canvas.
|
||||
"""
|
||||
self.delete_selected_btn.setEnabled(bool(has_selection))
|
||||
|
||||
def get_current_class(self) -> Optional[Dict]:
|
||||
"""Get currently selected object class."""
|
||||
return self.current_class
|
||||
|
||||
Reference in New Issue
Block a user