From bac2d2a9956f155047fe7445d1995db0f04b60c4 Mon Sep 17 00:00:00 2001 From: Kemal Eren Date: Thu, 19 Sep 2024 17:59:39 -0400 Subject: [PATCH] fix off-by-one errors parsing stuff and thing instances --- fiftyone/core/labels.py | 28 ++++++++++++++-------------- tests/unittests/label_tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/fiftyone/core/labels.py b/fiftyone/core/labels.py index 81a2994820..1b345253d3 100644 --- a/fiftyone/core/labels.py +++ b/fiftyone/core/labels.py @@ -1855,7 +1855,7 @@ def _parse_stuff_instance(mask, offset=None, frame_size=None): h = (ymax - ymin + 1) / height bbox = [x, y, w, h] - instance_mask = mask[ymin:ymax, xmin:xmax] + instance_mask = mask[ymin : (ymax + 1), xmin : (xmax + 1)] return bbox, instance_mask @@ -1873,23 +1873,23 @@ def _parse_thing_instances(mask, offset=None, frame_size=None): labeled = skm.label(mask) objects = _find_slices(labeled) - instances = [] - for idx, (yslice, xslice) in objects.items(): + for target, slc in objects.items(): + yslice, xslice = slc xmin = xslice.start - xmax = xslice.stop ymin = yslice.start - ymax = yslice.stop - - x = (xmin + x_offset) / width - y = (ymin + y_offset) / height - w = (xmax - xmin) / width - h = (ymax - ymin) / height - - bbox = [x, y, w, h] - instance_mask = mask[ymin:ymax, xmin:xmax] + instance_offset = ( + offset[0] + xmin, + offset[1] + ymin, + ) - instances.append((bbox, instance_mask)) + # use the labeled image mask so `_parse_stuff_instance()` + # can be re-used here + instance_mask = labeled[slc] == target + instance = _parse_stuff_instance( + instance_mask, instance_offset, frame_size + ) + instances.append(instance) return instances diff --git a/tests/unittests/label_tests.py b/tests/unittests/label_tests.py index 86c9e0131e..de7ab8ed9a 100644 --- a/tests/unittests/label_tests.py +++ b/tests/unittests/label_tests.py @@ -470,6 +470,38 @@ def poly_bounds(p): decimal=1, ) + @drop_datasets + def test_parse_stuff_instance(self): + mask = np.ones((3, 3), dtype=bool) + offset = (0, 0) + frame_size = (6, 6) + bbox, instance_mask = focl._parse_stuff_instance( + mask, offset, frame_size + ) + self.assertEqual(bbox, [0.0, 0.0, 0.5, 0.5]) + nptest.assert_array_equal(instance_mask, mask) + + @drop_datasets + def test_parse_thing_instances(self): + # test on multiple disconnected objects with overlapping + # bounding boxes + mask = np.eye(5, dtype=bool) + mask[0, -1] = True + offset = (0, 0) + frame_size = (10, 10) + results = focl._parse_thing_instances(mask, offset, frame_size) + self.assertEqual(len(results), 2) + + bbox, instance_mask = max(results, key=lambda x: x[1].size) + self.assertEqual(bbox, [0, 0, 0.5, 0.5]) + expected_mask = np.eye(5, dtype=bool) + nptest.assert_array_equal(instance_mask, expected_mask) + + bbox, instance_mask = min(results, key=lambda x: x[1].size) + self.assertEqual(bbox, [0.4, 0, 0.1, 0.1]) + expected_mask = np.eye(1, dtype=bool) + nptest.assert_array_equal(instance_mask, expected_mask) + @drop_datasets def test_transform_mask(self): # int to int