Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions anylabeling/views/labeling/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,8 @@ def group_selected_shapes(self):
if len(self.selected_shapes) == 0:
return

previous_group_ids = [shape.group_id for shape in self.shapes]

# List all group ids for selected shapes
group_ids = set()
has_non_group_shape = False
Expand All @@ -1269,13 +1271,18 @@ def group_selected_shapes(self):
if shape.group_id is None:
shape.group_id = new_group_id

if previous_group_ids != [shape.group_id for shape in self.shapes]:
self.store_shapes()
self.shape_moved.emit()
self.update()

def ungroup_selected_shapes(self):
"""Ungroup selected shapes"""
if len(self.selected_shapes) == 0:
return

previous_group_ids = [shape.group_id for shape in self.shapes]

# List all group ids for selected shapes
group_ids = set()
for shape in self.selected_shapes:
Expand All @@ -1287,4 +1294,7 @@ def ungroup_selected_shapes(self):
if shape.group_id == group_id:
shape.group_id = None

if previous_group_ids != [shape.group_id for shape in self.shapes]:
self.store_shapes()
self.shape_moved.emit()
self.update()
99 changes: 99 additions & 0 deletions tests/test_canvas_grouping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Regression tests for Canvas grouping and persisted group IDs."""

import io
import os
import tempfile
import unittest

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

from PIL import Image
from PyQt6 import QtCore, QtWidgets

from anylabeling.views.labeling.label_file import LabelFile
from anylabeling.views.labeling.shape import Shape
from anylabeling.views.labeling.widgets.canvas import Canvas

_APP = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])


def make_shape(offset, group_id=None):
shape = Shape(label="part", shape_type="rectangle", group_id=group_id)
shape.add_point(QtCore.QPointF(offset, offset))
shape.add_point(QtCore.QPointF(offset + 5, offset + 5))
shape.close()
return shape


class TestShapeGrouping(unittest.TestCase):
def test_grouping_marks_canvas_changed_and_is_undoable(self):
canvas = Canvas(parent=None)
shapes = [make_shape(1), make_shape(10)]
canvas.load_shapes(shapes)
canvas.selected_shapes = shapes
changes = []
canvas.shape_moved.connect(lambda: changes.append(True))

canvas.group_selected_shapes()

self.assertEqual([shape.group_id for shape in canvas.shapes], [1, 1])
self.assertEqual(len(changes), 1)
self.assertTrue(canvas.is_shape_restorable)

canvas.restore_shape()
self.assertEqual([shape.group_id for shape in canvas.shapes], [None, None])

def test_ungrouping_updates_every_shape_in_the_selected_group(self):
canvas = Canvas(parent=None)
shapes = [make_shape(1, group_id=7), make_shape(10, group_id=7)]
canvas.load_shapes(shapes)
canvas.selected_shapes = [shapes[0]]
changes = []
canvas.shape_moved.connect(lambda: changes.append(True))

canvas.ungroup_selected_shapes()

self.assertEqual([shape.group_id for shape in canvas.shapes], [None, None])
self.assertEqual(len(changes), 1)
self.assertTrue(canvas.is_shape_restorable)

def test_group_id_survives_label_file_round_trip(self):
shapes = [
{
"label": "part",
"text": "",
"points": [[1, 1], [6, 6]],
"group_id": 3,
"shape_type": "rectangle",
"flags": {},
},
{
"label": "part",
"text": "",
"points": [[10, 10], [15, 15]],
"group_id": 3,
"shape_type": "rectangle",
"flags": {},
},
]
image_buffer = io.BytesIO()
Image.new("RGB", (20, 20)).save(image_buffer, format="PNG")

with tempfile.TemporaryDirectory() as directory:
filename = os.path.join(directory, "labels.json")
LabelFile().save(
filename=filename,
shapes=shapes,
image_path="image.png",
image_height=20,
image_width=20,
image_data=image_buffer.getvalue(),
)

loaded = LabelFile(filename)

self.assertEqual([shape["group_id"] for shape in loaded.shapes], [3, 3])


if __name__ == "__main__":
unittest.main()
Loading