Skip to content

Commit dc66ee5

Browse files
authored
Improve default mask crop behavior (#32)
* Fix segment crops for objects fully inside the background ones * Improve default behavior for CropCoveredSegments transform * Improve type naming for polygon segments
1 parent 26bd789 commit dc66ee5

File tree

3 files changed

+46
-9
lines changed

3 files changed

+46
-9
lines changed

datumaro/plugins/transforms.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ def crop_segments(
110110
rle = mask_tools.mask_to_rle(s.image)
111111
segments.append(rle)
112112

113-
segments = mask_tools.crop_covered_segments(segments, img_width, img_height)
113+
segments = mask_tools.crop_covered_segments(
114+
segments, img_width, img_height, ratio_tolerance=0
115+
)
114116

115117
new_anns = []
116118
for ann, new_segment in zip(segment_anns, segments):

datumaro/util/mask_tools.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ class CompressedRle(TypedDict):
2222

2323

2424
Rle = Union[CompressedRle, UncompressedRle]
25-
Polygon = List[List[int]]
25+
Polygon = List[int]
26+
PolygonGroup = List[Polygon]
2627
BboxCoords = NamedTuple("BboxCoords", [("x", int), ("y", int), ("w", int), ("h", int)])
27-
Segment = Union[Polygon, Rle]
28+
Segment = Union[PolygonGroup, Rle]
2829

2930
BinaryMask = NewType("BinaryMask", np.ndarray)
3031
IndexMask = NewType("IndexMask", np.ndarray)
@@ -234,7 +235,7 @@ def is_uncompressed_rle(obj: Segment) -> bool:
234235
return isinstance(obj, dict) and isinstance(obj.get("counts"), bytes)
235236

236237

237-
def is_polygon(obj: Segment) -> bool:
238+
def is_polygon_group(obj: Segment) -> bool:
238239
return (
239240
isinstance(obj, list)
240241
and isinstance(obj[0], list)
@@ -259,7 +260,7 @@ def crop_covered_segments(
259260
ratio_tolerance: float = 0.001,
260261
area_threshold: int = 1,
261262
return_masks: bool = False,
262-
) -> List[Union[Optional[BinaryMask], Polygon]]:
263+
) -> List[Union[Optional[BinaryMask], List[Polygon]]]:
263264
"""
264265
Find all segments occluded by others and crop them to the visible part only.
265266
Input segments are expected to be sorted from background to foreground.
@@ -314,13 +315,14 @@ def crop_covered_segments(
314315
area_top = sum(mask_utils.area(rle_top))
315316
area_ratio = area_top / area_bottom
316317

317-
# If a segment is already fully inside the top ones, stop accumulating the top
318+
# If the top segment is (almost) fully inside the background one,
319+
# we may need to skip it to avoid making a hole in the background object
318320
if abs(area_ratio - iou) < ratio_tolerance:
319-
break
321+
continue
320322

321323
rles_top += rle_top
322324

323-
if not rles_top and is_polygon(wrapped_segments[i]) and not return_masks:
325+
if not rles_top and is_polygon_group(wrapped_segments[i]) and not return_masks:
324326
output_segments.append(wrapped_segments[i])
325327
continue
326328

@@ -334,7 +336,7 @@ def crop_covered_segments(
334336
bottom_mask -= top_mask
335337
bottom_mask[bottom_mask != 1] = 0
336338

337-
if not return_masks and is_polygon(wrapped_segments[i]):
339+
if not return_masks and is_polygon_group(wrapped_segments[i]):
338340
output_segments.append(mask_to_polygons(bottom_mask, area_threshold=area_threshold))
339341
else:
340342
if np.sum(bottom_mask) < area_threshold:

tests/test_masks.py

+33
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,39 @@ def test_can_crop_covered_segments(self):
115115
for i, (e_mask, c_mask) in enumerate(zip(expected, computed)):
116116
self.assertTrue(np.array_equal(e_mask, c_mask), "#%s: %s\n%s\n" % (i, e_mask, c_mask))
117117

118+
@mark_requirement(Requirements.DATUM_GENERAL_REQ)
119+
def test_can_crop_covered_segments_and_avoid_holes_from_objects_inside_background_object(self):
120+
image_size = [7, 7]
121+
initial = [
122+
[1, 1, 6, 1, 6, 6, 1, 6],
123+
mask_tools.mask_to_rle(
124+
np.array(
125+
[
126+
[0, 0, 0, 0, 0, 0, 0],
127+
[0, 0, 0, 0, 0, 0, 0],
128+
[0, 0, 1, 1, 1, 0, 0],
129+
[0, 0, 0, 0, 0, 0, 0],
130+
[0, 0, 0, 0, 0, 0, 0],
131+
[0, 0, 0, 0, 0, 0, 0],
132+
[0, 0, 0, 0, 0, 0, 0],
133+
]
134+
)
135+
),
136+
]
137+
expected = [
138+
# no changes expected
139+
mask_tools.rles_to_mask([initial[0]], *image_size),
140+
mask_tools.rles_to_mask([initial[1]], *image_size),
141+
]
142+
143+
computed = mask_tools.crop_covered_segments(
144+
initial, *image_size, ratio_tolerance=0.1, return_masks=True
145+
)
146+
147+
self.assertEqual(len(initial), len(computed))
148+
for i, (e_mask, c_mask) in enumerate(zip(expected, computed)):
149+
self.assertTrue(np.array_equal(e_mask, c_mask), "#%s: %s\n%s\n" % (i, e_mask, c_mask))
150+
118151
def _test_mask_to_rle(self, source_mask):
119152
rle_uncompressed = mask_tools.mask_to_rle(source_mask)
120153

0 commit comments

Comments
 (0)