{"record":{"id":"b6094eeefcd15585","repo":"roboflow/supervision","slug":"mode-must-be-edge-or-centroid","errorCode":null,"errorMessage":"mode must be 'edge' or 'centroid'","messagePattern":"mode must be 'edge' or 'centroid'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/detection/utils/masks.py","lineNumber":512,"sourceCode":"        main_mask = labels == main_label\n        if np.isnan(threshold) or threshold < 0:\n            nearby_main = np.zeros_like(main_mask)\n        elif np.isposinf(threshold):\n            nearby_main = np.ones_like(main_mask)\n        else:\n            fixed_distances = _chamfer_distances(main_mask)\n            distances = fixed_distances.astype(np.float32) / 65536\n            nearby_main = distances <= threshold\n        for label in range(1, num_labels):\n            if label == main_label:\n                continue\n            component = labels == label\n            if not np.any(component):\n                continue\n            if np.any(nearby_main & component):\n                keep_labels[label] = True\n    else:\n        raise ValueError(\"mode must be 'edge' or 'centroid'\")\n\n    return keep_labels[labels]\n\n\ndef mask_to_roi(mask: npt.NDArray[np.bool_]) -> tuple[int, int, int, int] | None:\n    \"\"\"Return exclusive ``(x1, y1, x2, y2)`` bounds for true mask pixels.\n\n    Use this helper when you need NumPy slice semantics. Unlike\n    :func:`~supervision.detection.utils.converters.mask_to_xyxy`, this\n    function uses exclusive upper bounds (``+1``) and returns ``None`` for\n    empty masks instead of zeros. The inclusive ``mask_to_xyxy`` convention\n    stays in place for compatibility with CompactMask and box-based adapters.\n\n    Args:\n        mask: 2D boolean array of shape ``(H, W)``.\n\n    Returns:\n        Exclusive ``(x1, y1, x2, y2)`` bounds, or ``None`` when the mask","sourceCodeStart":494,"sourceCodeEnd":530,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/detection/utils/masks.py#L494-L530","documentation":"The mask cleanup helper supports exactly two distance modes: 'centroid' (distance between component centroids) and 'edge' (distance between component edges via Chamfer-style distances). Any other string — or a non-string value — reaches the final else branch and raises.","triggerScenarios":"Passing mode='center' (common typo for 'centroid'), mode='Edge' (case-sensitive), or a config value with whitespace like 'edge ' read from a file.","commonSituations":"Config/CLI values not normalized (case, whitespace); assuming sklearn-style naming ('distance'); typos between 'centroid' and 'center'.","solutions":["Use exactly 'centroid' or 'edge' (lowercase).","Normalize config values: mode = cfg['mode'].strip().lower().","Validate mode at the config boundary with an explicit error listing the two options."],"exampleFix":"# before\n filtered = filter_non_zero_mask_areas(mask=m, relative_distance=0.2, mode=\"center\")\n\n# after\n filtered = filter_non_zero_mask_areas(mask=m, relative_distance=0.2, mode=\"centroid\")","handlingStrategy":"validation","validationCode":"mode = mode.strip().lower()\nif mode not in (\"edge\", \"centroid\"):\n    raise ValueError(f\"mode must be 'edge' or 'centroid', got {mode!r}\")","typeGuard":"def is_valid_mode(mode: str) -> bool:\n    return mode.strip().lower() in (\"edge\", \"centroid\")","tryCatchPattern":null,"preventionTips":["Normalize mode strings from config with strip().lower().","Remember 'centroid' is the exact spelling, not 'center'.","Expose only the two valid values in your own CLI/config schema."],"tags":["masks","mode","validation","string-literal"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}