Updating annotations
This commit is contained in:
@@ -93,6 +93,12 @@ class AnnotationTab(QWidget):
|
|||||||
self.annotation_tools.clear_annotations_requested.connect(
|
self.annotation_tools.clear_annotations_requested.connect(
|
||||||
self._on_clear_annotations
|
self._on_clear_annotations
|
||||||
)
|
)
|
||||||
|
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)
|
self.right_splitter.addWidget(self.annotation_tools)
|
||||||
|
|
||||||
# Image loading section
|
# Image loading section
|
||||||
@@ -237,6 +243,127 @@ class AnnotationTab(QWidget):
|
|||||||
self.annotation_canvas.clear_annotations()
|
self.annotation_canvas.clear_annotations()
|
||||||
logger.info("Cleared all annotations")
|
logger.info("Cleared all annotations")
|
||||||
|
|
||||||
|
def _on_process_annotations(self):
|
||||||
|
"""Process annotations and save to 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 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.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute bounding box and polyline from annotations
|
||||||
|
bounds = self.annotation_canvas.compute_annotation_bounds()
|
||||||
|
if not bounds:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"No Annotations",
|
||||||
|
"Please draw some annotations before processing.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
polyline = self.annotation_canvas.get_annotation_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"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)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optionally clear annotations after saving
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
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()
|
||||||
|
|
||||||
|
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_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_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)
|
||||||
|
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).",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load annotations: {e}")
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Error", f"Failed to load annotations:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
def _restore_state(self):
|
def _restore_state(self):
|
||||||
"""Restore splitter positions from settings."""
|
"""Restore splitter positions from settings."""
|
||||||
settings = QSettings("microscopy_app", "object_detection")
|
settings = QSettings("microscopy_app", "object_detection")
|
||||||
|
|||||||
@@ -369,6 +369,102 @@ 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]]:
|
||||||
|
"""
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
for stroke in self.all_strokes:
|
||||||
|
polyline.extend(stroke["points"])
|
||||||
|
|
||||||
|
return polyline
|
||||||
|
|
||||||
|
def draw_saved_polyline(
|
||||||
|
self, polyline: List[List[float]], color: str, width: int = 3
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Draw a polyline from database coordinates onto the annotation canvas.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
polyline: List of [x, y] coordinate pairs 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 polyline: no image loaded")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(polyline) < 2:
|
||||||
|
logger.warning("Polyline has less than 2 points, cannot draw")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert normalized coordinates to image coordinates
|
||||||
|
img_coords = []
|
||||||
|
for x_norm, y_norm in polyline:
|
||||||
|
x = int(x_norm * self.original_pixmap.width())
|
||||||
|
y = int(y_norm * self.original_pixmap.height())
|
||||||
|
img_coords.append((x, y))
|
||||||
|
|
||||||
|
# Draw polyline 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.RoundCap, Qt.RoundJoin)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
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):
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class AnnotationToolsWidget(QWidget):
|
|||||||
pen_width_changed = Signal(int)
|
pen_width_changed = Signal(int)
|
||||||
class_selected = Signal(dict)
|
class_selected = Signal(dict)
|
||||||
clear_annotations_requested = Signal()
|
clear_annotations_requested = Signal()
|
||||||
|
process_annotations_requested = Signal()
|
||||||
|
show_annotations_requested = Signal()
|
||||||
|
|
||||||
def __init__(self, db_manager: DatabaseManager, parent=None):
|
def __init__(self, db_manager: DatabaseManager, parent=None):
|
||||||
"""
|
"""
|
||||||
@@ -146,6 +148,20 @@ class AnnotationToolsWidget(QWidget):
|
|||||||
actions_group = QGroupBox("Actions")
|
actions_group = QGroupBox("Actions")
|
||||||
actions_layout = QVBoxLayout()
|
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.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 = QPushButton("Clear All Annotations")
|
||||||
self.clear_btn.clicked.connect(self._on_clear_annotations)
|
self.clear_btn.clicked.connect(self._on_clear_annotations)
|
||||||
actions_layout.addWidget(self.clear_btn)
|
actions_layout.addWidget(self.clear_btn)
|
||||||
@@ -335,6 +351,24 @@ class AnnotationToolsWidget(QWidget):
|
|||||||
self.clear_annotations_requested.emit()
|
self.clear_annotations_requested.emit()
|
||||||
logger.debug("Clear annotations requested")
|
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 _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]:
|
def get_current_class(self) -> Optional[Dict]:
|
||||||
"""Get currently selected object class."""
|
"""Get currently selected object class."""
|
||||||
return self.current_class
|
return self.current_class
|
||||||
|
|||||||
Reference in New Issue
Block a user