Skip to content

Commit 212287b

Browse files
Create binary segmentation mask from RLE (#54)
1 parent 7ef43ad commit 212287b

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

src/labelformat/model/binary_mask_segmentation.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ def from_binary_mask(
4242
bounding_box=bounding_box,
4343
)
4444

45+
@classmethod
46+
def from_rle(
47+
cls,
48+
rle_row_wise: list[int],
49+
width: int,
50+
height: int,
51+
bounding_box: BoundingBox | None = None,
52+
) -> "BinaryMaskSegmentation":
53+
"""
54+
Create a BinaryMaskSegmentation instance from row-wise RLE format.
55+
"""
56+
if bounding_box is None:
57+
bounding_box = _compute_bbox_from_rle(
58+
rle_row_wise=rle_row_wise, width=width, height=height
59+
)
60+
return cls(
61+
_rle_row_wise=rle_row_wise,
62+
width=width,
63+
height=height,
64+
bounding_box=bounding_box,
65+
)
66+
4567
def get_binary_mask(self) -> NDArray[np.int_]:
4668
"""
4769
Get the binary mask (2D numpy array) from the RLE format.
@@ -112,3 +134,49 @@ def decode_column_wise_rle(
112134
decoded.extend([run_val] * count)
113135
run_val = 1 - run_val
114136
return np.array(decoded, dtype=np.int_).reshape((height, width), order="F")
137+
138+
139+
def _compute_bbox_from_rle(
140+
rle_row_wise: list[int], width: int, height: int
141+
) -> BoundingBox:
142+
"""Compute bounding box from row-wise RLE.
143+
144+
Scans through the RLE and tracks the min/max x/y coordinates of the '1' pixels.
145+
The time complexity is O(len(rle_row_wise)).
146+
"""
147+
xmin = width
148+
ymin = height
149+
xmax = 0
150+
ymax = 0
151+
152+
x = 0
153+
y = 0
154+
next_symbol = 0
155+
for run_length in rle_row_wise:
156+
if next_symbol == 1:
157+
# Compute coordinates for the end of the run
158+
run_end_x = x + run_length - 1
159+
run_end_y = y
160+
if run_end_x >= width:
161+
run_end_y += run_end_x // width
162+
run_end_x = run_end_x % width
163+
164+
# Update bounding box
165+
ymin = min(ymin, y)
166+
ymax = max(ymax, run_end_y)
167+
if run_end_y > y:
168+
xmin = 0
169+
xmax = width - 1
170+
else:
171+
xmin = min(xmin, x)
172+
xmax = max(xmax, run_end_x)
173+
174+
# Compute coordinates for the start of the next run
175+
x += run_length
176+
if x >= width:
177+
y += x // width
178+
x = x % width
179+
180+
next_symbol = 1 - next_symbol
181+
182+
return BoundingBox(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)

tests/unit/model/test_binary_mask_segmentation.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
from numpy.typing import NDArray
33

4+
from labelformat.model import binary_mask_segmentation
45
from labelformat.model.binary_mask_segmentation import (
56
BinaryMaskSegmentation,
67
RLEDecoderEncoder,
@@ -22,6 +23,48 @@ def test_from_binary_mask(self) -> None:
2223
assert binary_mask_segmentation.bounding_box == bounding_box
2324
assert np.array_equal(binary_mask_segmentation.get_binary_mask(), binary_mask)
2425

26+
def test_from_rle(self) -> None:
27+
binary_mask_segmentation = BinaryMaskSegmentation.from_rle(
28+
rle_row_wise=[1, 1, 4, 2, 1, 3, 2, 1, 5],
29+
width=5,
30+
height=4,
31+
bounding_box=None,
32+
)
33+
assert binary_mask_segmentation.width == 5
34+
assert binary_mask_segmentation.height == 4
35+
assert binary_mask_segmentation.bounding_box == BoundingBox(0, 0, 4, 2)
36+
expected: NDArray[np.int_] = np.array(
37+
[
38+
[0, 1, 0, 0, 0],
39+
[0, 1, 1, 0, 1],
40+
[1, 1, 0, 0, 1],
41+
[0, 0, 0, 0, 0],
42+
],
43+
dtype=np.int_,
44+
)
45+
assert np.array_equal(binary_mask_segmentation.get_binary_mask(), expected)
46+
47+
# Test with provided bounding box
48+
# The box is larger than the actual mask, but should be preserved
49+
binary_mask_segmentation = BinaryMaskSegmentation.from_rle(
50+
rle_row_wise=[6, 3],
51+
width=3,
52+
height=3,
53+
bounding_box=BoundingBox(0, 0, 2, 2),
54+
)
55+
assert binary_mask_segmentation.width == 3
56+
assert binary_mask_segmentation.height == 3
57+
assert binary_mask_segmentation.bounding_box == BoundingBox(0, 0, 2, 2)
58+
expected = np.array(
59+
[
60+
[0, 0, 0],
61+
[0, 0, 0],
62+
[1, 1, 1],
63+
],
64+
dtype=np.int_,
65+
)
66+
assert np.array_equal(binary_mask_segmentation.get_binary_mask(), expected)
67+
2568

2669
class TestRLEDecoderEncoder:
2770
def test_encode_row_wise_rle(self) -> None:
@@ -79,3 +122,41 @@ def test_inverse__column_wise(self) -> None:
79122
rle, mask.shape[0], mask.shape[1]
80123
)
81124
assert np.array_equal(mask, mask_inverse_column_wise)
125+
126+
127+
def test_compute_bbox_from_rle() -> None:
128+
# 0011
129+
# 1111
130+
# 1100
131+
bbox = binary_mask_segmentation._compute_bbox_from_rle(
132+
rle_row_wise=[2, 8, 2],
133+
width=4,
134+
height=3,
135+
)
136+
assert bbox == BoundingBox(xmin=0, ymin=0, xmax=3, ymax=2)
137+
138+
# 0011
139+
# 0000
140+
bbox = binary_mask_segmentation._compute_bbox_from_rle(
141+
rle_row_wise=[2, 2, 4],
142+
width=4,
143+
height=2,
144+
)
145+
assert bbox == BoundingBox(xmin=2, ymin=0, xmax=3, ymax=0)
146+
147+
# 0011
148+
# 1000
149+
bbox = binary_mask_segmentation._compute_bbox_from_rle(
150+
rle_row_wise=[2, 3, 3],
151+
width=4,
152+
height=2,
153+
)
154+
assert bbox == BoundingBox(xmin=0, ymin=0, xmax=3, ymax=1)
155+
156+
# 1111
157+
bbox = binary_mask_segmentation._compute_bbox_from_rle(
158+
rle_row_wise=[0, 4],
159+
width=4,
160+
height=1,
161+
)
162+
assert bbox == BoundingBox(xmin=0, ymin=0, xmax=3, ymax=0)

0 commit comments

Comments
 (0)