skip to content

Contours and Morphology

Threshold an image, clean it up with erode, dilate, open, and close, then pull out contours and measure them with bounding rects, moments, and connected components. It is the everyday recipe for shape analysis in industrial and document vision.

on this pageshow

questions

6

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

open as a page

In OpenCV, what is the difference between MORPH_OPEN and MORPH_CLOSE?

level: middleimportance: must knowfreq 66%

basics

~20 s

MORPH_OPEN erodes then dilates: it removes small white specks and thin bridges. MORPH_CLOSE dilates then erodes: it fills small holes and joins nearby blobs. Both roughly preserve blob size, unlike a bare erode or dilate.

open as a page

In OpenCV, what changes when you add cv2.THRESH_OTSU to cv2.threshold?

level: middleimportance: must knowfreq 62%

basics

~20 s

OpenCV then ignores the threshold value you passed and computes one from the image histogram by maximizing between-class variance. The chosen value comes back as the first return value of cv2.threshold, which is why the call is written with 0 as the threshold argument.

open as a page

In OpenCV, when does cv2.adaptiveThreshold beat a global threshold?

level: middleimportance: should knowfreq 52%

basics

~20 s

When illumination varies across the frame. cv2.adaptiveThreshold computes a separate threshold for every pixel from the mean of its blockSize x blockSize neighbourhood, minus a constant C, so a shadow or lighting gradient cancels out instead of blacking out half the image.

open as a page

In OpenCV, how do you get each blob's area and centroid from a binary mask?

level: middleimportance: should knowfreq 48%

basics

~20 s

Two routes. Per contour: cv2.moments gives m00 as the area and m10/m00, m01/m00 as the centroid. Per labelled region: cv2.connectedComponentsWithStats returns stats with CC_STAT_AREA in pixels plus a centroids array - remember label 0 is the background.

open as a page

What does the hierarchy array from cv2.findContours contain, and when do you need RETR_TREE?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Hierarchy has shape (1, N, 4); each row is [Next, Previous, First_Child, Parent] holding indices into the contours list, with -1 meaning none. RETR_TREE is needed when nesting matters - counting holes, or telling an outer boundary from the shapes inside it.

open as a page