Updating polyline saving and drawing
This commit is contained in:
@@ -262,9 +262,9 @@ class AnnotationTab(QWidget):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Compute bounding box and polyline from annotations
|
# Compute annotation parameters asbounding boxes and polylines from annotations
|
||||||
bounds = self.annotation_canvas.compute_annotation_bounds()
|
parameters = self.annotation_canvas.get_annotation_parameters()
|
||||||
if not bounds:
|
if not parameters:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"No Annotations",
|
"No Annotations",
|
||||||
@@ -272,48 +272,56 @@ class AnnotationTab(QWidget):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
polyline = self.annotation_canvas.get_annotation_polyline()
|
# polyline = self.annotation_canvas.get_annotation_polyline()
|
||||||
|
|
||||||
try:
|
for param in parameters:
|
||||||
# Save annotation to database
|
bounds = param["bbox"]
|
||||||
annotation_id = self.db_manager.add_annotation(
|
polyline = param["polyline"]
|
||||||
image_id=self.current_image_id,
|
|
||||||
class_id=current_class["id"],
|
|
||||||
bbox=bounds,
|
|
||||||
annotator="manual",
|
|
||||||
segmentation_mask=polyline,
|
|
||||||
verified=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
try:
|
||||||
f"Saved annotation (ID: {annotation_id}) for class '{current_class['class_name']}' "
|
# Save annotation to database
|
||||||
f"with {len(polyline)} polyline points"
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
QMessageBox.information(
|
logger.info(
|
||||||
self,
|
f"Saved annotation (ID: {annotation_id}) for class '{current_class['class_name']}' "
|
||||||
"Success",
|
f"Bounding box: ({bounds[0]:.3f}, {bounds[1]:.3f}) to ({bounds[2]:.3f}, {bounds[3]:.3f})\n"
|
||||||
f"Annotation saved successfully!\n\n"
|
f"with {len(polyline)} polyline points"
|
||||||
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)}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Optionally clear annotations after saving
|
# QMessageBox.information(
|
||||||
reply = QMessageBox.question(
|
# self,
|
||||||
self,
|
# "Success",
|
||||||
"Clear Annotations",
|
# f"Annotation saved successfully!\n\n"
|
||||||
"Do you want to clear the annotations to start a new one?",
|
# f"Class: {current_class['class_name']}\n"
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
# f"Bounding box: ({bounds[0]:.3f}, {bounds[1]:.3f}) to ({bounds[2]:.3f}, {bounds[3]:.3f})\n"
|
||||||
QMessageBox.Yes,
|
# f"Polyline points: {len(polyline)}",
|
||||||
)
|
# )
|
||||||
|
|
||||||
if reply == QMessageBox.Yes:
|
except Exception as e:
|
||||||
self.annotation_canvas.clear_annotations()
|
logger.error(f"Failed to save annotation: {e}")
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Error", f"Failed to save annotation:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
# Optionally clear annotations after saving
|
||||||
logger.error(f"Failed to save annotation: {e}")
|
reply = QMessageBox.question(
|
||||||
QMessageBox.critical(self, "Error", f"Failed to save annotation:\n{str(e)}")
|
self,
|
||||||
|
"Clear Annotations",
|
||||||
|
"Do you want to clear the annotations to start a new one?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.Yes,
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
self.annotation_canvas.clear_annotations()
|
||||||
|
logger.info("Cleared annotations after saving")
|
||||||
|
|
||||||
def _on_show_annotations(self):
|
def _on_show_annotations(self):
|
||||||
"""Load and display saved annotations from database."""
|
"""Load and display saved annotations from database."""
|
||||||
@@ -348,6 +356,11 @@ class AnnotationTab(QWidget):
|
|||||||
|
|
||||||
# Draw the polyline
|
# Draw the polyline
|
||||||
self.annotation_canvas.draw_saved_polyline(polyline, color, width=3)
|
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
|
drawn_count += 1
|
||||||
|
|
||||||
logger.info(f"Displayed {drawn_count} saved annotations from database")
|
logger.info(f"Displayed {drawn_count} saved annotations from database")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ Annotation canvas widget for drawing annotations on images.
|
|||||||
Supports pen tool with color selection for manual annotation.
|
Supports pen tool with color selection for manual annotation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QScrollArea
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QScrollArea
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QPixmap,
|
QPixmap,
|
||||||
@@ -15,12 +17,17 @@ from PySide6.QtGui import (
|
|||||||
QPaintEvent,
|
QPaintEvent,
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QEvent, Signal, QPoint
|
from PySide6.QtCore import Qt, QEvent, Signal, QPoint
|
||||||
from typing import List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
import numpy as np
|
|
||||||
|
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.image import Image, ImageLoadError
|
||||||
from src.utils.logger import get_logger
|
from src.utils.logger import get_logger
|
||||||
|
|
||||||
|
# For debugging visualization
|
||||||
|
import pylab as plt
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -369,49 +376,149 @@ class AnnotationCanvasWidget(QWidget):
|
|||||||
"""Get all drawn strokes with metadata."""
|
"""Get all drawn strokes with metadata."""
|
||||||
return self.all_strokes
|
return self.all_strokes
|
||||||
|
|
||||||
def compute_annotation_bounds(self) -> Optional[Tuple[float, float, float, float]]:
|
# 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) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Compute bounding box that encompasses all annotation strokes.
|
Get all annotation parameters including bounding box and polyline.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (x_min, y_min, x_max, y_max) in normalized coordinates (0-1),
|
Dictionary containing:
|
||||||
or None if no annotations exist.
|
- 'bbox': Bounding box coordinates (x_min, y_min, x_max, y_max)
|
||||||
|
- 'polyline': List of [x, y] coordinate pairs
|
||||||
"""
|
"""
|
||||||
if not self.all_strokes:
|
|
||||||
|
# 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:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Find min/max across all strokes
|
objects = find_objects(labels)
|
||||||
all_x = []
|
w, h = arr.shape
|
||||||
all_y = []
|
bounding_boxes = [
|
||||||
|
[obj[0].start / w, obj[1].start / h, obj[0].stop / w, obj[1].stop / h]
|
||||||
|
for obj in objects
|
||||||
|
]
|
||||||
|
|
||||||
for stroke in self.all_strokes:
|
polylines = find_contours(arr_bin, 0.5)
|
||||||
for x, y in stroke["points"]:
|
params = []
|
||||||
all_x.append(x)
|
for i, pl in enumerate(polylines):
|
||||||
all_y.append(y)
|
# 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()
|
||||||
|
|
||||||
if not all_x:
|
logger.debug(f"Polyline {i}: {len(pl)} points")
|
||||||
return None
|
logger.debug(f" w={w} (height), h={h} (width)")
|
||||||
|
logger.debug(f" First 3 normalized points: {normalized_polyline[:3]}")
|
||||||
|
|
||||||
x_min = min(all_x)
|
params.append(
|
||||||
y_min = min(all_y)
|
{
|
||||||
x_max = max(all_x)
|
"bbox": bounding_boxes[i],
|
||||||
y_max = max(all_y)
|
"polyline": normalized_polyline,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (x_min, y_min, x_max, y_max)
|
return params
|
||||||
|
|
||||||
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 = []
|
|
||||||
|
|
||||||
for stroke in self.all_strokes:
|
|
||||||
polyline.extend(stroke["points"])
|
|
||||||
|
|
||||||
return polyline
|
|
||||||
|
|
||||||
def draw_saved_polyline(
|
def draw_saved_polyline(
|
||||||
self, polyline: List[List[float]], color: str, width: int = 3
|
self, polyline: List[List[float]], color: str, width: int = 3
|
||||||
@@ -433,12 +540,22 @@ class AnnotationCanvasWidget(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Convert normalized coordinates to image coordinates
|
# Convert normalized coordinates to image coordinates
|
||||||
|
# Polyline is stored as [[y_norm, x_norm], ...] (row_norm, col_norm format)
|
||||||
|
img_width = self.original_pixmap.width()
|
||||||
|
img_height = self.original_pixmap.height()
|
||||||
|
|
||||||
|
logger.debug(f"Loading polyline with {len(polyline)} points")
|
||||||
|
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 = []
|
||||||
for x_norm, y_norm in polyline:
|
for y_norm, x_norm in polyline:
|
||||||
x = int(x_norm * self.original_pixmap.width())
|
x = int(x_norm * img_width)
|
||||||
y = int(y_norm * self.original_pixmap.height())
|
y = int(y_norm * img_height)
|
||||||
img_coords.append((x, y))
|
img_coords.append((x, y))
|
||||||
|
|
||||||
|
logger.debug(f" First 3 pixel coords: {img_coords[:3]}")
|
||||||
|
|
||||||
# Draw polyline on annotation pixmap
|
# Draw polyline on annotation pixmap
|
||||||
painter = QPainter(self.annotation_pixmap)
|
painter = QPainter(self.annotation_pixmap)
|
||||||
pen_color = QColor(color)
|
pen_color = QColor(color)
|
||||||
@@ -465,6 +582,64 @@ class AnnotationCanvasWidget(QWidget):
|
|||||||
f"Drew saved polyline with {len(polyline)} points in color {color}"
|
f"Drew saved polyline with {len(polyline)} points in color {color}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def draw_saved_bbox(self, bbox: List[float], color: str, width: int = 3):
|
||||||
|
"""
|
||||||
|
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]
|
||||||
|
in normalized coordinates (0-1)
|
||||||
|
color: Color hex string (e.g., '#FF0000')
|
||||||
|
width: Line width in pixels
|
||||||
|
"""
|
||||||
|
if not self.annotation_pixmap or not self.original_pixmap:
|
||||||
|
logger.warning("Cannot draw bounding box: no image loaded")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(bbox) != 4:
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid bounding box format: expected 4 values, got {len(bbox)}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert normalized coordinates to image coordinates
|
||||||
|
# bbox format: [y_min_norm, x_min_norm, y_max_norm, x_max_norm]
|
||||||
|
img_width = self.original_pixmap.width()
|
||||||
|
img_height = self.original_pixmap.height()
|
||||||
|
|
||||||
|
y_min_norm, x_min_norm, y_max_norm, x_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)
|
||||||
|
|
||||||
|
logger.debug(f"Drawing bounding box: {bbox}")
|
||||||
|
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)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Store in all_strokes for consistency
|
||||||
|
self.all_strokes.append(
|
||||||
|
{"bbox": bbox, "color": color, "alpha": 128, "width": width}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update display
|
||||||
|
self._update_display()
|
||||||
|
logger.debug(f"Drew saved bounding box in color {color}")
|
||||||
|
|
||||||
def keyPressEvent(self, event: QKeyEvent):
|
def keyPressEvent(self, event: QKeyEvent):
|
||||||
"""Handle keyboard events for zooming."""
|
"""Handle keyboard events for zooming."""
|
||||||
if event.key() in (Qt.Key_Plus, Qt.Key_Equal):
|
if event.key() in (Qt.Key_Plus, Qt.Key_Equal):
|
||||||
|
|||||||
Reference in New Issue
Block a user