Updating
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user