{"record":{"id":"ef66130acb9f7136","repo":"roboflow/supervision","slug":"contours-must-have-shape-n-2-or-n-1-2","errorCode":null,"errorMessage":"Contours must have shape (N, 2) or (N, 1, 2)","messagePattern":"Contours must have shape \\(N, 2\\) or \\(N, 1, 2\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/_cv2/_geometry.py","lineNumber":17,"sourceCode":"\"\"\"Private polygon geometry fallbacks.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport numpy as np\nimport numpy.typing as npt\n\n\ndef _as_points(contour: npt.NDArray[Any]) -> npt.NDArray[np.float64]:\n    \"\"\"Normalize an OpenCV contour to an ``(N, 2)`` float64 array.\"\"\"\n    points = np.asarray(contour)\n    if points.size == 0:\n        return np.empty((0, 2), dtype=np.float64)\n    if points.ndim not in (2, 3) or points.shape[-1] != 2:\n        raise ValueError(\"Contours must have shape (N, 2) or (N, 1, 2)\")\n    return points.reshape(-1, 2).astype(np.float64, copy=False)\n\n\ndef _contour_area(contour: npt.NDArray[Any], oriented: bool = False) -> float:\n    \"\"\"Compute a contour's signed or absolute shoelace area.\"\"\"\n    points = _as_points(contour)\n    if len(points) < 3:\n        return 0.0\n    x = points[:, 0]\n    y = points[:, 1]\n    area = 0.5 * float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))\n    return area if oriented else abs(area)\n\n\ndef _simplify_slices(\n    points: npt.NDArray[np.float64], epsilon_squared: float, closed: bool\n) -> npt.NDArray[np.float64]:\n    \"\"\"Run OpenCV's stack-based Douglas-Peucker slice traversal.\"\"\"","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/_cv2/_geometry.py#L1-L35","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Reshape to (N, 2): points = np.asarray(contour).reshape(-1, 2) — verify N is even first for flat inputs.","For OpenCV-style callers, pass contours as (N, 1, 2) arrays as returned by findContours.","Drop extra columns before use: contour = xyz[:, :2]."],"exampleFix":"# before\narea = cv2.contourArea(np.array(flat_xy_list))  # shape (2N,)\n\n# after\npoints = np.asarray(flat_xy_list, dtype=np.float64).reshape(-1, 2)\narea = cv2.contourArea(points)","handlingStrategy":"type-guard","validationCode":"pts = np.asarray(contour)\nif pts.ndim not in (2, 3) or pts.shape[-1] != 2:\n    pts = pts.reshape(-1, 2)\narea = cv2.contourArea(pts)","typeGuard":"def is_valid_contour(a: np.ndarray) -> bool:\n    a = np.asarray(a)\n    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)","tryCatchPattern":null,"preventionTips":["Reshape point data to (N, 2) at ingestion","Drop keypoint/xyz extra columns before contour ops","Keep findContours output shape (N, 1, 2) untouched"],"tags":["opencv-fallback","contours","shape-mismatch","numpy"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}