Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/supervision/dataset/formats/coco.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,18 @@ def detections_to_coco_annotations(
approximation_percentage: float = 0.75,
) -> tuple[list[CocoDict], int]:
coco_annotations: list[CocoDict] = []
for xyxy, mask, _, class_id, _, _ in detections:
for xyxy, mask, _, class_id, _, data in detections:
if class_id is None:
raise ValueError("Detections must include class_id for COCO export.")
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
segmentation: Union[list[list[float]], dict[str, list[int]]] = []
iscrowd = 0
if mask is not None:
iscrowd = contains_holes(mask=mask) or contains_multiple_segments(mask=mask)
if "iscrowd" in data:
iscrowd = int(np.asarray(data["iscrowd"]).item())
else:
iscrowd = int(
contains_holes(mask=mask) or contains_multiple_segments(mask=mask)
)

Comment thread
Borda marked this conversation as resolved.
if iscrowd:
segmentation = {
Expand All @@ -196,12 +200,16 @@ def detections_to_coco_annotations(
"returned no polygons.",
stacklevel=2,
)
else:
iscrowd = int(np.asarray(data.get("iscrowd", 0)).item())

area: float = float(np.asarray(data.get("area", box_width * box_height)).item())
coco_annotation = {
"id": annotation_id,
"image_id": image_id,
"category_id": int(class_id),
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
"area": box_width * box_height,
"area": area,
"segmentation": segmentation,
"iscrowd": iscrowd,
}
Expand Down
104 changes: 104 additions & 0 deletions tests/dataset/formats/test_coco.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,110 @@ def test_detections_to_coco_annotations_handles_empty_approximated_polygons() ->
assert annotations[0]["iscrowd"] == 0


def test_detections_to_coco_annotations_preserves_area_from_data() -> None:
"""area stored in detections.data should be used instead of bbox area."""
detections = Detections(
xyxy=np.array([[10.0, 20.0, 110.0, 120.0]], dtype=np.float32),
class_id=np.array([0], dtype=int),
data={"iscrowd": np.array([0], dtype=int), "area": np.array([5000.0])},
)

annotations, _ = detections_to_coco_annotations(
detections=detections,
image_id=1,
annotation_id=1,
)

assert len(annotations) == 1
assert annotations[0]["area"] == 5000.0
assert annotations[0]["iscrowd"] == 0
assert type(annotations[0]["iscrowd"]) is int


def test_detections_to_coco_annotations_preserves_iscrowd_from_data_when_no_mask() -> (
None
):
"""iscrowd stored in detections.data should be used when no mask is present."""
detections = Detections(
xyxy=np.array([[0.0, 0.0, 100.0, 100.0]], dtype=np.float32),
class_id=np.array([0], dtype=int),
data={"iscrowd": np.array([1], dtype=int), "area": np.array([1234.5])},
)

annotations, _ = detections_to_coco_annotations(
detections=detections,
image_id=1,
annotation_id=1,
)

assert len(annotations) == 1
assert annotations[0]["iscrowd"] == 1
assert type(annotations[0]["iscrowd"]) is int
assert annotations[0]["area"] == 1234.5


def test_detections_to_coco_annotations_iscrowd_is_int_when_mask_provided() -> None:
"""iscrowd should be stored as int (0 or 1), not as Python bool."""
mask = np.zeros((1, 5, 5), dtype=bool)
mask[0, 0:3, 0:3] = True # simple single-component rectangle

detections = Detections(
xyxy=np.array([[0.0, 0.0, 3.0, 3.0]], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=mask,
)

annotations, _ = detections_to_coco_annotations(
detections=detections,
image_id=1,
annotation_id=1,
)

assert len(annotations) == 1
assert annotations[0]["iscrowd"] == 0
assert type(annotations[0]["iscrowd"]) is int


def test_detections_to_coco_annotations_data_area_overrides_bbox_with_mask() -> None:
"""data["area"] should override computed bbox area even when a mask is present."""
mask = np.zeros((1, 10, 10), dtype=bool)
mask[0, 0:4, 0:4] = True # 16-pixel polygon area

detections = Detections(
xyxy=np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
class_id=np.array([0], dtype=int),
mask=mask,
data={"area": np.array([999.0])},
)

annotations, _ = detections_to_coco_annotations(
detections=detections,
image_id=1,
annotation_id=1,
)

assert len(annotations) == 1
assert annotations[0]["area"] == 999.0


def test_detections_to_coco_annotations_fallback_area_when_no_data() -> None:
"""When detections have no area in data, area should fall back to bbox area."""
detections = Detections(
xyxy=np.array([[10.0, 20.0, 110.0, 120.0]], dtype=np.float32),
class_id=np.array([0], dtype=int),
)

annotations, _ = detections_to_coco_annotations(
detections=detections,
image_id=1,
annotation_id=1,
)

assert len(annotations) == 1
assert annotations[0]["area"] == 100.0 * 100.0
assert annotations[0]["iscrowd"] == 0


def test_load_coco_annotations_infers_masks_from_segmentation_field(
tmp_path, coco_data_with_and_without_segmentation: dict[str, object]
) -> None:
Expand Down
Loading