skip to content

In OpenCV, what input does cv2.findContours require and what does it return?

level: juniorimportance: must knowfreq 72%

answer

  1. binary in, not grayscale
  2. non-zero means foreground
  3. two values, not three, since v4
  4. each contour is (N, 1, 2) int32

basics

~20 s

cv2.findContours expects a single-channel 8-bit binary image with white objects on a black background - every non-zero pixel counts as foreground. In OpenCV 4 and 5 it returns two values: a list of contours and a hierarchy array.

solid answer

~40 s

`cv2.findContours(image, mode, method)` takes a **binary** single-channel 8-bit image. It does not threshold for you: it treats every non-zero pixel as foreground, so passing a raw grayscale photo produces garbage rather than an error. You also pass a retrieval mode (`cv2.RETR_EXTERNAL`, `RETR_LIST`, `RETR_CCOMP`, `RETR_TREE`) and an approximation method (`cv2.CHAIN_APPROX_NONE` keeps every boundary pixel, `CHAIN_APPROX_SIMPLE` keeps only segment endpoints, so a rectangle comes back as 4 points). In OpenCV 4 and 5 the call returns `(contours, hierarchy)`; OpenCV 3 returned a third value (the modified image) first, which is why so much old code unpacks three. Each contour is a NumPy array of shape `(N, 1, 2)` with dtype `int32`. Since 3.2 the source image is no longer modified in place.

code

python · 14 lines
python
import cv2
import numpy as np

binary = np.zeros((200, 200), np.uint8)
cv2.rectangle(binary, (20, 20), (80, 80), 255, -1)
cv2.circle(binary, (140, 140), 30, 255, -1)

contours, hierarchy = cv2.findContours(
    binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
print(len(contours), contours[0].shape, contours[0].dtype)

canvas = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
cv2.drawContours(canvas, contours, -1, (0, 255, 0), 2)

go deeper

for a junior

Be ready to say the input must be a binary single-channel image, objects white on black, and that the call returns contours plus a hierarchy. Knowing the threshold step comes first is most of the answer.

for a middle

Explain the mode and method flags concretely - RETR_EXTERNAL versus RETR_TREE, CHAIN_APPROX_SIMPLE versus NONE - and describe the (N, 1, 2) int32 contour array and the OpenCV 3 to 4 signature change.

for a senior

Show the production instinct: threshold, morphological clean-up, then contours filtered by area, and explain how an un-thresholded input fails silently rather than raising. Mention that the source is no longer modified in place.

for a principal

Own the pipeline design choice: when a classical contour pipeline is the right tool at all versus a learned detector, what it costs in tuning per lighting condition, and how you keep threshold and kernel parameters from becoming untraceable site-specific magic numbers.

## What a contour is here In OpenCV a contour is a closed curve traced along the boundary of a connected region of non-zero pixels. It is not an edge map and not a list of edge pixels: it is an **ordered** sequence of points that walks around one blob, which is what makes downstream measurement (area, perimeter, bounding box, shape matching) possible. ## The input contract `cv2.findContours` wants a single-channel, 8-bit image. Two rules follow from that: 1. **It must already be binary in spirit.** The function does not threshold. It partitions pixels into background (value 0) and foreground (anything non-zero). Hand it an un-thresholded grayscale frame and it will not raise - it will happily trace the boundary between the few true-zero pixels and everything else, returning a small number of huge, meaningless contours, or thousands of noisy ones. This is the classic silent-wrong-answer on this API. 2. **Objects must be white on black.** If your parts are dark on a light background, you get the *background's* boundary instead. Fix it at threshold time with `cv2.THRESH_BINARY_INV`, or invert with `cv2.bitwise_not`. Since OpenCV 3.2 the input array is no longer overwritten, so the defensive `binary.copy()` you see in older code is unnecessary in OpenCV 4 and 5. ## The two mandatory flags **Retrieval mode** decides *which* contours come back and whether nesting is recorded. `cv2.RETR_EXTERNAL` returns only the outermost contours - the usual choice for counting solid parts. `cv2.RETR_LIST` returns all of them, including the boundaries of holes, with no parent/child relationships. `cv2.RETR_CCOMP` gives a two-level result, and `cv2.RETR_TREE` the full nesting tree. **Approximation method** decides how many points each contour carries. `cv2.CHAIN_APPROX_NONE` stores every single boundary pixel; `cv2.CHAIN_APPROX_SIMPLE` collapses straight horizontal, vertical and diagonal runs to their endpoints, so an axis-aligned rectangle comes back as four points instead of hundreds. `SIMPLE` is the default choice: it is smaller and faster and loses nothing that area, perimeter or bounding-box code cares about. Use `NONE` only when you genuinely need every boundary pixel, for example to sample intensities along the outline. ## The return value In OpenCV 4 and OpenCV 5: ``` contours, hierarchy = cv2.findContours(binary, mode, method) ``` `contours` is a Python list (or tuple) of NumPy arrays. Each array has shape `(N, 1, 2)` and dtype `int32` - N points, each an `(x, y)` pair, with a redundant middle axis that is a legacy of the C++ `vector<Point>` binding. That middle axis is why `cnt[:, 0, :]` or `cnt.reshape(-1, 2)` shows up everywhere, and why `cnt.shape[0]` (not `len(cnt[0])`) is the point count. `hierarchy` is an array of shape `(1, N, 4)` describing nesting; when a mode records no relationships the unused slots are `-1`. If no contours are found, `contours` is empty and `hierarchy` is `None` - guard for that before indexing it. OpenCV 3 returned `(image, contours, hierarchy)`. Interviewers like this detail because it is the single most common breakage when old tutorials are pasted into a modern environment; the symptom is `ValueError: not enough values to unpack`. ## Coordinates and drawing back Contour points are `(x, y)` - column first - while NumPy indexing on the same image is `img[y, x]`. Mixing the two is a routine bug. `cv2.drawContours(image, contours, contourIdx, color, thickness)` takes the **list**, not one contour: to draw a single contour you pass `[cnt]`. `contourIdx=-1` draws all of them, and `thickness=cv2.FILLED` (-1) fills the interior, which is a handy way to rebuild a clean mask from selected contours. Drawing onto a single-channel image with a `(0, 255, 0)` colour silently uses only the first component, so convert to 3-channel first if you want colour. ## Practical sequence The everyday recipe is: grayscale, threshold (global, Otsu or adaptive), a morphological clean-up pass, then `findContours` with `RETR_EXTERNAL` and `CHAIN_APPROX_SIMPLE`, then filter the result by `cv2.contourArea` to drop specks before measuring what survives. Skipping the clean-up step is why naive pipelines report hundreds of one-pixel contours.

  • Your code does contours, hierarchy = cv2.findContours(...) and raises a ValueError about unpacking. What happened?
    You are running against OpenCV 3, which returned three values - the modified image, the contours, and the hierarchy. OpenCV 4 and 5 dropped the image and return two. Either pin the modern version and unpack two, or use the version-agnostic trick of taking the last two elements of the returned tuple.
  • When would you choose CHAIN_APPROX_NONE over CHAIN_APPROX_SIMPLE?
    When you need every boundary pixel rather than just the polygon corners: sampling image intensity along the outline, computing a per-pixel distance profile, or feeding a curvature or chain-code analysis. For area, perimeter, bounding boxes and shape matching, SIMPLE gives the same answers with far fewer points and less memory.
  • How do you turn a selected subset of contours back into a clean binary mask?
    Create a zeroed single-channel array the size of the image and call cv2.drawContours(mask, selected, -1, 255, cv2.FILLED). The FILLED thickness (-1) paints the interior, so you get a mask containing exactly the blobs that passed your filter, ready for bitwise_and against the original.

saying these in an interview costs you the question

  • Thinks findContours thresholds the image for you
  • Says it works on a color image directly
  • Unpacks three return values on OpenCV 4 or 5
  • Assumes dark objects on a light background are found
  • Reads contour points as (row, column) instead of (x, y)

context