roboflow/supervision · error · ValueError

Contour input must be a two-dimensional image

Error message

Contour input must be a two-dimensional image

What it means

`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.

Source

Thrown at src/supervision/_cv2/_contours.py:150

            and np.any(following)
            and np.array_equal(np.sign(previous), np.sign(following))
        ):
            continue
        keep.append(point)
    return np.asarray(keep, dtype=np.int32)


def _find_contours(
    image: npt.NDArray[Any], mode: int, method: int
) -> tuple[list[npt.NDArray[np.int32]], npt.NDArray[np.int32] | None]:
    """Find contours for the supported tree and SIMPLE modes."""
    if mode != _RETR_TREE:
        raise ValueError("Only RETR_TREE is supported by the fallback")
    if method != _CHAIN_APPROX_SIMPLE:
        raise ValueError("Only CHAIN_APPROX_SIMPLE is supported by the fallback")
    values = np.asarray(image)
    if values.ndim != 2:
        raise ValueError("Contour input must be a two-dimensional image")
    traced = [_compress_contour(contour) for contour in _trace_borders(values != 0)]
    if not traced:
        return [], None
    return [contour.reshape(-1, 1, 2) for contour in traced], None

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Convert to a 2-D single-channel mask first: `cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)` then threshold
  2. Squeeze channel axes: `mask.squeeze()` for (H, W, 1) masks
  3. Index the batch: `images[i]` for (N, H, W) arrays

Example fix

// before
contours, _ = cv2.findContours(frame, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)  # frame is BGR

// after
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
Defensive patterns

Strategy: type-guard

Validate before calling

import numpy as np

def as_contour_mask(image) -> np.ndarray:
    """Coerce arbitrary image input to the 2-D mask findContours requires."""
    arr = np.asarray(image)
    if arr.ndim == 3 and arr.shape[-1] == 1:
        arr = arr[..., 0]
    elif arr.ndim == 3 and arr.shape[-1] == 3:
        arr = cv2.cvtColor(arr, cv2.COLOR_BGR2GRAY)
    if arr.ndim != 2:
        raise ValueError(f"cannot reduce ndim={arr.ndim} to a 2-D mask")
    return arr

Type guard

def is_contour_input(image) -> bool:
    """findContours accepts only 2-D single-channel arrays."""
    return np.asarray(image).ndim == 2

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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