Skip to content

Commit b02ec93

Browse files
authored
Merge pull request #2006 from roboflow/feature/xyxy_to_mask
Feature/xyxy to mask
2 parents 171687f + 89ee57f commit b02ec93

File tree

5 files changed

+245
-1
lines changed

5 files changed

+245
-1
lines changed

docs/detection/utils/converters.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ status: new
5858
</div>
5959

6060
:::supervision.detection.utils.converters.polygon_to_xyxy
61+
62+
<div class="md-typeset">
63+
<h2><a href="#supervision.detection.utils.converters.xyxy_to_mask">xyxy_to_mask</a></h2>
64+
</div>
65+
66+
:::supervision.detection.utils.converters.xyxy_to_mask

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "supervision"
33
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
44
license = { text = "MIT" }
5-
version = "0.27.0rc1"
5+
version = "0.27.0rc2"
66
readme = "README.md"
77
requires-python = ">=3.9"
88
authors = [

supervision/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
polygon_to_xyxy,
6666
xcycwh_to_xyxy,
6767
xywh_to_xyxy,
68+
xyxy_to_mask,
6869
xyxy_to_polygons,
6970
xyxy_to_xcycarh,
7071
xyxy_to_xywh,
@@ -249,6 +250,7 @@
249250
"tint_image",
250251
"xcycwh_to_xyxy",
251252
"xywh_to_xyxy",
253+
"xyxy_to_mask",
252254
"xyxy_to_polygons",
253255
"xyxy_to_xcycarh",
254256
"xyxy_to_xywh",

supervision/detection/utils/converters.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,70 @@ def mask_to_xyxy(masks: np.ndarray) -> np.ndarray:
229229
return xyxy
230230

231231

232+
def xyxy_to_mask(boxes: np.ndarray, resolution_wh: tuple[int, int]) -> np.ndarray:
233+
"""
234+
Converts a 2D `np.ndarray` of bounding boxes into a 3D `np.ndarray` of bool masks.
235+
236+
Parameters:
237+
boxes (np.ndarray): A 2D `np.ndarray` of shape `(N, 4)`
238+
containing bounding boxes `(x_min, y_min, x_max, y_max)`
239+
resolution_wh (Tuple[int, int]): A tuple `(width, height)` specifying
240+
the resolution of the output masks
241+
242+
Returns:
243+
np.ndarray: A 3D `np.ndarray` of shape `(N, height, width)`
244+
containing 2D bool masks for each bounding box
245+
246+
Examples:
247+
```python
248+
import numpy as np
249+
import supervision as sv
250+
251+
boxes = np.array([[0, 0, 2, 2]])
252+
253+
sv.xyxy_to_mask(boxes, (5, 5))
254+
# array([
255+
# [[ True, True, True, False, False],
256+
# [ True, True, True, False, False],
257+
# [ True, True, True, False, False],
258+
# [False, False, False, False, False],
259+
# [False, False, False, False, False]]
260+
# ])
261+
262+
boxes = np.array([[0, 0, 1, 1], [3, 3, 4, 4]])
263+
264+
sv.xyxy_to_mask(boxes, (5, 5))
265+
# array([
266+
# [[ True, True, False, False, False],
267+
# [ True, True, False, False, False],
268+
# [False, False, False, False, False],
269+
# [False, False, False, False, False],
270+
# [False, False, False, False, False]],
271+
#
272+
# [[False, False, False, False, False],
273+
# [False, False, False, False, False],
274+
# [False, False, False, False, False],
275+
# [False, False, False, True, True],
276+
# [False, False, False, True, True]]
277+
# ])
278+
```
279+
"""
280+
width, height = resolution_wh
281+
n = boxes.shape[0]
282+
masks = np.zeros((n, height, width), dtype=bool)
283+
284+
for i, (x_min, y_min, x_max, y_max) in enumerate(boxes):
285+
x_min = max(0, int(x_min))
286+
y_min = max(0, int(y_min))
287+
x_max = min(width - 1, int(x_max))
288+
y_max = min(height - 1, int(y_max))
289+
290+
if x_max >= x_min and y_max >= y_min:
291+
masks[i, y_min : y_max + 1, x_min : x_max + 1] = True
292+
293+
return masks
294+
295+
232296
def mask_to_polygons(mask: np.ndarray) -> list[np.ndarray]:
233297
"""
234298
Converts a binary mask to a list of polygons.

test/detection/utils/test_converters.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from supervision.detection.utils.converters import (
77
xcycwh_to_xyxy,
88
xywh_to_xyxy,
9+
xyxy_to_mask,
910
xyxy_to_xcycarh,
1011
xyxy_to_xywh,
1112
)
@@ -129,3 +130,174 @@ def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
129130
def test_xcycwh_to_xyxy(xcycwh: np.ndarray, expected_result: np.ndarray) -> None:
130131
result = xcycwh_to_xyxy(xcycwh)
131132
np.testing.assert_array_equal(result, expected_result)
133+
134+
135+
@pytest.mark.parametrize(
136+
"boxes,resolution_wh,expected",
137+
[
138+
# 0) Empty input
139+
(
140+
np.array([], dtype=float).reshape(0, 4),
141+
(5, 4),
142+
np.array([], dtype=bool).reshape(0, 4, 5),
143+
),
144+
# 1) Single pixel box
145+
(
146+
np.array([[2, 1, 2, 1]], dtype=float),
147+
(5, 4),
148+
np.array(
149+
[
150+
[
151+
[False, False, False, False, False],
152+
[False, False, True, False, False],
153+
[False, False, False, False, False],
154+
[False, False, False, False, False],
155+
]
156+
],
157+
dtype=bool,
158+
),
159+
),
160+
# 2) Horizontal line, inclusive bounds
161+
(
162+
np.array([[1, 2, 3, 2]], dtype=float),
163+
(5, 4),
164+
np.array(
165+
[
166+
[
167+
[False, False, False, False, False],
168+
[False, False, False, False, False],
169+
[False, True, True, True, False],
170+
[False, False, False, False, False],
171+
]
172+
],
173+
dtype=bool,
174+
),
175+
),
176+
# 3) Vertical line, inclusive bounds
177+
(
178+
np.array([[3, 0, 3, 2]], dtype=float),
179+
(5, 4),
180+
np.array(
181+
[
182+
[
183+
[False, False, False, True, False],
184+
[False, False, False, True, False],
185+
[False, False, False, True, False],
186+
[False, False, False, False, False],
187+
]
188+
],
189+
dtype=bool,
190+
),
191+
),
192+
# 4) Proper rectangle fill
193+
(
194+
np.array([[1, 1, 3, 2]], dtype=float),
195+
(5, 4),
196+
np.array(
197+
[
198+
[
199+
[False, False, False, False, False],
200+
[False, True, True, True, False],
201+
[False, True, True, True, False],
202+
[False, False, False, False, False],
203+
]
204+
],
205+
dtype=bool,
206+
),
207+
),
208+
# 5) Negative coordinates clipped to [0, 0]
209+
(
210+
np.array([[-2, -1, 1, 1]], dtype=float),
211+
(5, 4),
212+
np.array(
213+
[
214+
[
215+
[True, True, False, False, False],
216+
[True, True, False, False, False],
217+
[False, False, False, False, False],
218+
[False, False, False, False, False],
219+
]
220+
],
221+
dtype=bool,
222+
),
223+
),
224+
# 6) Overflow coordinates clipped to width-1 and height-1
225+
(
226+
np.array([[3, 2, 10, 10]], dtype=float),
227+
(5, 4),
228+
np.array(
229+
[
230+
[
231+
[False, False, False, False, False],
232+
[False, False, False, False, False],
233+
[False, False, False, True, True],
234+
[False, False, False, True, True],
235+
]
236+
],
237+
dtype=bool,
238+
),
239+
),
240+
# 7) Invalid box where max < min after ints, mask stays empty
241+
(
242+
np.array([[3, 2, 1, 4]], dtype=float),
243+
(5, 4),
244+
np.array(
245+
[
246+
[
247+
[False, False, False, False, False],
248+
[False, False, False, False, False],
249+
[False, False, False, False, False],
250+
[False, False, False, False, False],
251+
]
252+
],
253+
dtype=bool,
254+
),
255+
),
256+
# 8) Fractional coordinates are floored by int conversion
257+
# (0.2,0.2)-(2.8,1.9) -> (0,0)-(2,1)
258+
(
259+
np.array([[0.2, 0.2, 2.8, 1.9]], dtype=float),
260+
(5, 4),
261+
np.array(
262+
[
263+
[
264+
[True, True, True, False, False],
265+
[True, True, True, False, False],
266+
[False, False, False, False, False],
267+
[False, False, False, False, False],
268+
]
269+
],
270+
dtype=bool,
271+
),
272+
),
273+
# 9) Multiple boxes, separate masks
274+
(
275+
np.array([[0, 0, 1, 0], [2, 1, 4, 3]], dtype=float),
276+
(5, 4),
277+
np.array(
278+
[
279+
# Box 0: row 0, cols 0..1
280+
[
281+
[True, True, False, False, False],
282+
[False, False, False, False, False],
283+
[False, False, False, False, False],
284+
[False, False, False, False, False],
285+
],
286+
# Box 1: rows 1..3, cols 2..4
287+
[
288+
[False, False, False, False, False],
289+
[False, False, True, True, True],
290+
[False, False, True, True, True],
291+
[False, False, True, True, True],
292+
],
293+
],
294+
dtype=bool,
295+
),
296+
),
297+
],
298+
)
299+
def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) -> None:
300+
result = xyxy_to_mask(boxes, resolution_wh)
301+
assert result.dtype == np.bool_
302+
assert result.shape == expected.shape
303+
np.testing.assert_array_equal(result, expected)

0 commit comments

Comments
 (0)