{"record":{"id":"d34cca6d5c7ae479","repo":"roboflow/supervision","slug":"contour-input-must-be-a-two-dimensional-image","errorCode":null,"errorMessage":"Contour input must be a two-dimensional image","messagePattern":"Contour input must be a two-dimensional image","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/_cv2/_contours.py","lineNumber":150,"sourceCode":"            and np.any(following)\n            and np.array_equal(np.sign(previous), np.sign(following))\n        ):\n            continue\n        keep.append(point)\n    return np.asarray(keep, dtype=np.int32)\n\n\ndef _find_contours(\n    image: npt.NDArray[Any], mode: int, method: int\n) -> tuple[list[npt.NDArray[np.int32]], npt.NDArray[np.int32] | None]:\n    \"\"\"Find contours for the supported tree and SIMPLE modes.\"\"\"\n    if mode != _RETR_TREE:\n        raise ValueError(\"Only RETR_TREE is supported by the fallback\")\n    if method != _CHAIN_APPROX_SIMPLE:\n        raise ValueError(\"Only CHAIN_APPROX_SIMPLE is supported by the fallback\")\n    values = np.asarray(image)\n    if values.ndim != 2:\n        raise ValueError(\"Contour input must be a two-dimensional image\")\n    traced = [_compress_contour(contour) for contour in _trace_borders(values != 0)]\n    if not traced:\n        return [], None\n    return [contour.reshape(-1, 1, 2) for contour in traced], None\n","sourceCodeStart":132,"sourceCodeEnd":155,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/_cv2/_contours.py#L132-L155","documentation":"`cv2.findContours` in OpenCV treats a 2-D single-channel image as input. The fallback at src/supervision/_cv2/_contours.py:150 enforces this explicitly: after `np.asarray(image)`, `values.ndim != 2` raises. A 3-D BGR frame or a 1-D signal array cannot be traced for borders.","triggerScenarios":"Passing a color BGR frame (`shape (H, W, 3)`), an RGBA image, a batched array `(N, H, W)`, or a 1-D array to `cv2.findContours` on the fallback backend (real OpenCV errors differently, often with a cryptic assertion).","commonSituations":"Forgetting to grayscale/threshold a camera frame before contour extraction; masks stored as (H, W, 1); operating on an already-batched tensor converted to ndarray.","solutions":["Convert to a 2-D single-channel mask first: `cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)` then threshold","Squeeze channel axes: `mask.squeeze()` for (H, W, 1) masks","Index the batch: `images[i]` for (N, H, W) arrays"],"exampleFix":"// before\ncontours, _ = cv2.findContours(frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  # frame is BGR\n\n// after\ngray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n_, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)\ncontours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)","handlingStrategy":"type-guard","validationCode":"import numpy as np\n\ndef as_contour_mask(image) -> np.ndarray:\n    \"\"\"Coerce arbitrary image input to the 2-D mask findContours requires.\"\"\"\n    arr = np.asarray(image)\n    if arr.ndim == 3 and arr.shape[-1] == 1:\n        arr = arr[..., 0]\n    elif arr.ndim == 3 and arr.shape[-1] == 3:\n        arr = cv2.cvtColor(arr, cv2.COLOR_BGR2GRAY)\n    if arr.ndim != 2:\n        raise ValueError(f\"cannot reduce ndim={arr.ndim} to a 2-D mask\")\n    return arr","typeGuard":"def is_contour_input(image) -> bool:\n    \"\"\"findContours accepts only 2-D single-channel arrays.\"\"\"\n    return np.asarray(image).ndim == 2","tryCatchPattern":null,"preventionTips":["Always grayscale+threshold frames before findContours","Squeeze (H, W, 1) masks and index (N, H, W) batches per image"],"tags":["cv2-fallback","contours","shape-validation","image-processing"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}