roboflow/supervision · error · ValueError

Detections must include class_id for COCO export.

Error message

Detections must include class_id for COCO export.

What it means

Raised by detections_to_coco_annotations when iterating Detections and a detection has class_id=None. COCO annotations require a category_id for every object, so a Detections without class_id cannot be serialized to the COCO format. The check runs per-detection inside the export loop.

Source

Thrown at src/supervision/dataset/formats/coco.py:305

        ... )
        >>> detections = Detections(
        ...     xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
        ...     class_id=np.array([0], dtype=int),
        ... )
        >>> annotations, next_id = detections_to_coco_annotations(
        ...     detections=detections, image_id=1, annotation_id=1
        ... )
        >>> annotations[0]["category_id"]
        1
        >>> next_id
        2

        ```
    """
    coco_annotations: list[CocoDict] = []
    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: list[list[float]] | dict[str, list[int]] = []
        if mask is not None:
            mask_bool = mask
            if "iscrowd" in data:
                iscrowd = int(np.asarray(data["iscrowd"]).item())
            else:
                iscrowd = int(
                    contains_holes(mask=mask_bool)
                    or contains_multiple_segments(mask=mask_bool)
                )

            if iscrowd:
                segmentation = {
                    "counts": cast(
                        list[int], mask_to_rle(mask=mask_bool, compressed=False)
                    ),
                    "size": list(mask.shape[:2]),

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Provide class_id when constructing Detections: sv.Detections(xyxy=boxes, confidence=conf, class_id=class_ids).
  2. If all boxes belong to one known class, synthesize class_id=np.zeros(len(boxes), dtype=int).
  3. Check detections.class_id is not None before calling the COCO export and fail early with your own message.

Example fix

// before
detections = sv.Detections(xyxy=boxes, confidence=conf)
save_coco_annotations(dataset=ds, annotation_path="out.json")  # ds built from class_id-less detections

// after
detections = sv.Detections(xyxy=boxes, confidence=conf, class_id=np.zeros(len(boxes), dtype=int))
Defensive patterns

Strategy: type-guard

Validate before calling

if detections.class_id is None or len(detections) == 0 and detections.class_id is None:
    raise ValueError("Attach class_id before COCO export")
# single-class fallback:
# detections = sv.Detections(xyxy=detections.xyxy, confidence=detections.confidence,
#                            class_id=np.zeros(len(detections), dtype=int))

Type guard

def has_class_id(dets: sv.Detections) -> bool:
    """True when every detection carries a class id."""
    return dets.class_id is not None and len(dets.class_id) == len(dets)

Try / catch

try:
    save_coco_annotations(dataset=ds, annotation_path=p)
except ValueError as exc:
    if "class_id" in str(exc):
        raise RuntimeError("Model produced class-agnostic detections; map them to a class first") from exc
    raise

Prevention

When it happens

Trigger: Calling sv.Detections with xyxy/confidence but no class_id (e.g. raw model output from a detector run in NMS-only mode, or a manually built Detections), then passing it to save_coco_annotations / detections_to_coco_annotations / DetectionDataset save path that ultimately writes COCO.

Common situations: Using detections produced by a tracker or a model that drops class_id; constructing Detections from custom boxes for dataset conversion; filtering detections and losing the class_id array.

Related errors


AI-assisted analysis of roboflow/supervision@7f254d9784 (2026-08-15). Data as JSON: /api/errors/ccc65f4ecf635aa4. Report an issue: GitHub.