Skip to content

Commit 1726b1b

Browse files
ghanashyam03Borda
andcommitted
fix(detection): handle empty polygons in filter_polygons_by_area and polygon_to_xyxy (#2761)
* fix: handle empty polygons in filter_polygons_by_area and polygon_to_xyxy * fix: preserve array dtype for empty polygons in polygon_to_xyxy --------- Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
1 parent b3a1b96 commit 1726b1b

5 files changed

Lines changed: 52 additions & 1 deletion

File tree

‎docs/changelog.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ date_modified: 2026-10-16
77

88
### Unreleased <small>upcoming</small>
99

10+
- `sv.filter_polygons_by_area` and `sv.polygon_to_xyxy` now accept empty polygons instead of raising a `ValueError`. Empty polygons evaluate to zero area when filtering, and `polygon_to_xyxy` returns an all-zero bounding box while preserving the input NumPy array dtype. ([#2761](https://github.com/roboflow/supervision/pull/2761))
11+
1012
- Keypoint annotators use x/y when drawing accepted three-coordinate keypoints. Vertex and ellipse drawing no longer raises a tuple-unpacking error, and edge and label visibility checks ignore the extra coordinate. Rendering matches equivalent two-coordinate input, including missing-point and explicit-visibility handling. ([#2759](https://github.com/roboflow/supervision/pull/2759))
1113

1214
- `sv.metrics.MeanAverageRecall` now follows COCO's last-target tie rule when multiple eligible targets have the same IoU. This preserves confidence-ordered matching and the crowd/ignore handling added in #2750. ([#2758](https://github.com/roboflow/supervision/pull/2758))

‎src/supervision/detection/utils/converters.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,12 @@ def polygon_to_xyxy(polygon: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
865865
866866
```
867867
"""
868+
if len(polygon) == 0:
869+
output_dtype = polygon.dtype if isinstance(polygon, np.ndarray) else np.int_
870+
return np.zeros(4, dtype=output_dtype)
871+
872+
polygon = np.asarray(polygon)
873+
868874
x_min, y_min = np.min(polygon, axis=0)
869875
x_max, y_max = np.max(polygon, axis=0)
870876
return np.array([x_min, y_min, x_max, y_max])

‎src/supervision/detection/utils/polygons.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ def _to_local_cv2_polygon(
1313
before conversion. Translating first preserves representable local geometry when
1414
absolute coordinates exceed float32 precision.
1515
"""
16+
if len(polygon) == 0:
17+
return np.empty((0, 2), dtype=np.float32), np.zeros(2, dtype=polygon.dtype)
18+
1619
origin = polygon.min(axis=0)
1720
if np.issubdtype(polygon.dtype, np.integer):
1821
local_polygon = np.asarray(

‎tests/detection/utils/test_converters.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1223,11 +1223,27 @@ def test_mask_to_polygons_polygon_to_mask_round_trip() -> None:
12231223
np.array([4, 4, 4, 4]),
12241224
id="single-point",
12251225
),
1226+
pytest.param(
1227+
np.empty((0, 2), dtype=int),
1228+
np.array([0, 0, 0, 0]),
1229+
id="empty-polygon-int",
1230+
),
1231+
pytest.param(
1232+
np.empty((0, 2), dtype=np.float32),
1233+
np.zeros(4, dtype=np.float32),
1234+
id="empty-polygon-float32",
1235+
),
1236+
pytest.param(
1237+
[],
1238+
np.array([0, 0, 0, 0]),
1239+
id="empty-polygon-list",
1240+
),
12261241
],
12271242
)
12281243
def test_polygon_to_xyxy(
1229-
polygon: npt.NDArray[np.int_], expected: npt.NDArray[np.int_]
1244+
polygon: npt.NDArray[np.number], expected: npt.NDArray[np.number]
12301245
) -> None:
12311246
"""polygon_to_xyxy returns the tight axis-aligned bounding box."""
12321247
result = polygon_to_xyxy(polygon)
1248+
assert result.dtype == expected.dtype
12331249
np.testing.assert_array_equal(result, expected)

‎tests/detection/utils/test_polygons.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@
8585
DoesNotRaise(),
8686
), # two polygons with min_area and
8787
# max_area equal to the area of the second polygon
88+
(
89+
[np.empty((0, 2))],
90+
50,
91+
None,
92+
[],
93+
DoesNotRaise(),
94+
), # empty polygon with min_area constraint
95+
(
96+
[np.empty((0, 2))],
97+
None,
98+
50,
99+
[np.empty((0, 2))],
100+
DoesNotRaise(),
101+
), # empty polygon with max_area constraint
102+
(
103+
[
104+
np.empty((0, 2)),
105+
np.array([[0, 0], [0, 10], [10, 10], [10, 0]]),
106+
],
107+
50,
108+
None,
109+
[np.array([[0, 0], [0, 10], [10, 10], [10, 0]])],
110+
DoesNotRaise(),
111+
), # mixed empty and valid polygons with min_area
88112
],
89113
)
90114
def test_filter_polygons_by_area(

0 commit comments

Comments
 (0)