{"record":{"id":"94c61ea9edf8397e","repo":"roboflow/supervision","slug":"empty-group-detected-when-non-max-merging-detectio","errorCode":null,"errorMessage":"Empty group detected when non-max-merging detections: {merge_groups}","messagePattern":"Empty group detected when non-max-merging detections: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/detection/utils/iou_and_nms.py","lineNumber":1480,"sourceCode":"    When ``predictions`` has no class column, a single pass over all rows is\n    performed instead of per-category iteration.\n    \"\"\"\n    if predictions.shape[1] == 5:\n        global_indices = np.arange(len(predictions), dtype=int)\n        return [\n            global_indices[group].tolist() for group in group_within(global_indices)\n        ]\n\n    category_ids = predictions[:, 5]\n    merge_groups: list[list[int]] = []\n    for category_id in np.unique(category_ids):\n        curr_indices = np.where(category_ids == category_id)[0]\n        for local_group in group_within(curr_indices):\n            merge_groups.append(curr_indices[local_group].tolist())\n\n    for merge_group in merge_groups:\n        if len(merge_group) == 0:\n            raise ValueError(\n                f\"Empty group detected when non-max-merging detections: {merge_groups}\"\n            )\n    return merge_groups\n\n\ndef _group_overlapping_boxes(\n    predictions: npt.NDArray[np.floating],\n    iou_threshold: float = 0.5,\n    overlap_metric: OverlapMetric = OverlapMetric.IOU,\n) -> list[list[int]]:\n    \"\"\"\n    Apply greedy version of non-maximum merging to avoid detecting too many\n    overlapping bounding boxes for a given object.\n\n    Args:\n        predictions: An array of shape `(n, 5)` containing\n            the bounding boxes coordinates in format `[x1, y1, x2, y2]`\n            and the confidence scores.","sourceCodeStart":1462,"sourceCodeEnd":1498,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/detection/utils/iou_and_nms.py#L1462-L1498","documentation":"An internal invariant check inside non-max-merging group construction: after grouping prediction indices by class id and overlap, every group must contain at least one index. An empty group is impossible when inputs are well-formed (groups are built from `np.where(category_ids == id)` results, which are never empty), so hitting this error means the grouping logic received malformed state — effectively a defensive assert against corrupted predictions arrays or patched internals.","triggerScenarios":"Directly calling the private grouping helper `_group_overlapping_boxes` / non-max-merge internals with a hand-built `predictions` array whose shape or dtype breaks the class-id column (e.g. object dtype, NaN class ids making `np.unique`/`np.where` disagree); monkeypatched `group_within` returning empty lists.","commonSituations":"Almost never seen via the public API (`sv.Detections.with_nms` / `OverlapFilter.NON_MAX_MERGE`); appears when unit tests stub grouping functions or when a custom predictions array with wrong dtype/class column is passed into internal helpers.","solutions":["Use the public API instead: `detections.with_nms(threshold, overlap_filter=sv.OverlapFilter.NON_MAX_MERGE)`.","If you must call internals, build `predictions` as a float array of shape (N, 6): [x_min, y_min, x_max, y_max, confidence, class_id] with no NaNs.","Check your test doubles: a stubbed grouping callback that returns empty lists will trip this guard by design."],"exampleFix":"# before\ngroups = _group_overlapping_boxes(np.array([[0, 0, 1, 1, 0.9, np.nan]]))  # NaN class id\n\n# after\ndets = detections.with_nms(0.5, overlap_filter=sv.OverlapFilter.NON_MAX_MERGE)","handlingStrategy":"validation","validationCode":"assert predictions.ndim == 2 and predictions.shape[1] in (5, 6)\nassert not np.isnan(predictions.astype(float)).any()","typeGuard":"def is_valid_predictions(arr) -> bool:\n    arr = np.asarray(arr)\n    return arr.ndim == 2 and arr.shape[1] in (5, 6)","tryCatchPattern":"try:\n    dets = dets.with_nms(0.5, overlap_filter=sv.OverlapFilter.NON_MAX_MERGE)\nexcept ValueError as e:\n    if 'Empty group' in str(e):\n        raise RuntimeError('corrupted detections; rebuild from model output') from e\n    raise","preventionTips":["Prefer the public Detections.with_nms API over internal grouping helpers.","Keep predictions as float arrays with no NaNs in the class-id column."],"tags":["internal-invariant","non-max-merge","nms","detection"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}