Skip to content

Commit c2490e6

Browse files
abdogomaa201099claudeBordaclaude[bot]
authored
chore: preserve all polygons when exporting multi-part masks to COCO (roboflow#2322)
* test(coco): strengthen multi-polygon export test coverage * docs(coco): document segmentation shape and iscrowd routing in docstring * test(coco): parametrize multipart polygon tests --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: jirka <6035284+Borda@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent a914a00 commit c2490e6

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/supervision/dataset/formats/coco.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,18 @@ def detections_to_coco_annotations(
247247
Raises:
248248
ValueError: If any detection has ``class_id`` equal to ``None``.
249249
250+
Note:
251+
For ``iscrowd=0`` annotations, ``segmentation`` is a
252+
``list[list[float]]`` where each inner list encodes one polygon
253+
part as flat ``[x1, y1, x2, y2, ...]`` coordinates. A single
254+
object with *N* disjoint parts produces *N* inner lists.
255+
256+
When ``iscrowd`` is not in ``detections.data``, masks with holes
257+
or multiple disjoint segments are auto-encoded as RLE
258+
(``iscrowd=1``); simple single-region masks use polygon format
259+
(``iscrowd=0``). Supply ``data={"iscrowd": np.array([0])}`` to
260+
force polygon output regardless of mask topology.
261+
250262
Examples:
251263
```python
252264
import numpy as np

tests/dataset/formats/test_coco.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
build_coco_class_index_mapping,
1414
classes_to_coco_categories,
1515
coco_annotations_to_detections,
16+
coco_annotations_to_masks,
1617
coco_categories_to_classes,
1718
detections_to_coco_annotations,
1819
group_coco_annotations_by_image_id,
@@ -995,6 +996,80 @@ def test_detections_to_coco_annotations_handles_empty_approximated_polygons() ->
995996
assert annotations[0]["iscrowd"] == 0
996997

997998

999+
_DISJOINT_2X2_MASK = np.array(
1000+
[
1001+
[
1002+
[1, 1, 0, 0, 0],
1003+
[1, 1, 0, 0, 0],
1004+
[0, 0, 0, 0, 0],
1005+
[0, 0, 0, 1, 1],
1006+
[0, 0, 0, 1, 1],
1007+
]
1008+
],
1009+
dtype=bool,
1010+
)
1011+
1012+
_SINGLE_COMPONENT_MASK = np.array(
1013+
[
1014+
[
1015+
[1, 1, 1, 0, 0],
1016+
[1, 1, 1, 0, 0],
1017+
[1, 1, 1, 0, 0],
1018+
[0, 0, 0, 0, 0],
1019+
[0, 0, 0, 0, 0],
1020+
]
1021+
],
1022+
dtype=bool,
1023+
)
1024+
1025+
1026+
def _make_iscrowd0_detections(mask: np.ndarray) -> Detections:
1027+
"""Build a single-detection Detections with iscrowd=0 from a (1, H, W) mask."""
1028+
_, h, w = mask.shape
1029+
return Detections(
1030+
xyxy=np.array([[0, 0, w, h]], dtype=np.float32),
1031+
class_id=np.array([0], dtype=int),
1032+
mask=mask,
1033+
data={"iscrowd": np.array([0])},
1034+
)
1035+
1036+
1037+
@pytest.mark.parametrize(
1038+
("mask", "expected_segment_count"),
1039+
[
1040+
pytest.param(_DISJOINT_2X2_MASK, 2, id="disjoint-two-parts"),
1041+
pytest.param(_SINGLE_COMPONENT_MASK, 1, id="single-component"),
1042+
],
1043+
)
1044+
def test_detections_to_coco_annotations_segmentation_count(
1045+
mask: np.ndarray, expected_segment_count: int
1046+
) -> None:
1047+
"""Non-crowd mask export produces one polygon list entry per connected component."""
1048+
annotations, _ = detections_to_coco_annotations(
1049+
detections=_make_iscrowd0_detections(mask), image_id=0, annotation_id=0
1050+
)
1051+
1052+
segmentation = annotations[0]["segmentation"]
1053+
assert annotations[0]["iscrowd"] == 0
1054+
assert isinstance(segmentation, list)
1055+
assert len(segmentation) == expected_segment_count
1056+
assert all(len(part) >= 6 for part in segmentation)
1057+
assert all(np.isfinite(c) for part in segmentation for c in part)
1058+
1059+
1060+
def test_detections_to_coco_annotations_round_trip_disjoint_mask() -> None:
1061+
"""Two-part disjoint mask round-trips through COCO export and import unchanged."""
1062+
W, H = 5, 5
1063+
annotations, _ = detections_to_coco_annotations(
1064+
detections=_make_iscrowd0_detections(_DISJOINT_2X2_MASK),
1065+
image_id=0,
1066+
annotation_id=0,
1067+
)
1068+
reloaded = coco_annotations_to_masks([annotations[0]], resolution_wh=(W, H))
1069+
1070+
assert np.array_equal(reloaded[0], _DISJOINT_2X2_MASK[0])
1071+
1072+
9981073
def test_detections_to_coco_annotations_preserves_area_from_data() -> None:
9991074
"""area stored in detections.data should be used instead of bbox area."""
10001075
detections = Detections(

0 commit comments

Comments
 (0)