roboflow/supervision · error · ValueError

Contours must have shape (N, 2) or (N, 1, 2)

Error message

Contours must have shape (N, 2) or (N, 1, 2)

What it means

The fallback geometry helpers (contour area, approxPolyDP, etc.) normalize OpenCV contour inputs — either an (N, 2) point array or OpenCV's (N, 1, 2) contour layout — to (N, 2) float64. Any other rank or last dimension (e.g. (N, 3) keypoints, (N,) flat arrays) is rejected because shoelace/polygon math is undefined for it.

Source

Thrown at src/supervision/_cv2/_geometry.py:17

"""Private polygon geometry fallbacks."""

from __future__ import annotations

from typing import Any

import numpy as np
import numpy.typing as npt


def _as_points(contour: npt.NDArray[Any]) -> npt.NDArray[np.float64]:
    """Normalize an OpenCV contour to an ``(N, 2)`` float64 array."""
    points = np.asarray(contour)
    if points.size == 0:
        return np.empty((0, 2), dtype=np.float64)
    if points.ndim not in (2, 3) or points.shape[-1] != 2:
        raise ValueError("Contours must have shape (N, 2) or (N, 1, 2)")
    return points.reshape(-1, 2).astype(np.float64, copy=False)


def _contour_area(contour: npt.NDArray[Any], oriented: bool = False) -> float:
    """Compute a contour's signed or absolute shoelace area."""
    points = _as_points(contour)
    if len(points) < 3:
        return 0.0
    x = points[:, 0]
    y = points[:, 1]
    area = 0.5 * float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))
    return area if oriented else abs(area)


def _simplify_slices(
    points: npt.NDArray[np.float64], epsilon_squared: float, closed: bool
) -> npt.NDArray[np.float64]:
    """Run OpenCV's stack-based Douglas-Peucker slice traversal."""

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Reshape to (N, 2): points = np.asarray(contour).reshape(-1, 2) — verify N is even first for flat inputs.
  2. For OpenCV-style callers, pass contours as (N, 1, 2) arrays as returned by findContours.
  3. Drop extra columns before use: contour = xyz[:, :2].

Example fix

# before
area = cv2.contourArea(np.array(flat_xy_list))  # shape (2N,)

# after
points = np.asarray(flat_xy_list, dtype=np.float64).reshape(-1, 2)
area = cv2.contourArea(points)
Defensive patterns

Strategy: type-guard

Validate before calling

pts = np.asarray(contour)
if pts.ndim not in (2, 3) or pts.shape[-1] != 2:
    pts = pts.reshape(-1, 2)
area = cv2.contourArea(pts)

Type guard

def is_valid_contour(a: np.ndarray) -> bool:
    a = np.asarray(a)
    return a.ndim in (2, 3) and a.shape[-1] == 2 and a.size in (0,) or (a.ndim in (2, 3) and a.shape[-1] == 2)

Prevention

When it happens

Trigger: Passing a (N, 3) array (xyz points or 3-column detections), a flat (2N,) coordinate list, or an (N, 2, 1) wrongly-shaped tensor to cv2.contourArea / cv2.approxPolyDP via the fallback.

Common situations: Feeding keypoint or detection xy arrays directly as contours, forgetting reshape after flattening polygon coordinates from JSON, or transposed (2, N) point lists from math code.

Related errors


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