Contours and shape measurement

OpenCV · Practical · 15 min

Contours turn a mask into countable objects with positions and sizes, the core of classic detection and inspection.

cv2.findContours(mask, mode, method) traces the boundaries of white regions in a binary image and returns (contours, hierarchy) in OpenCV 4 and later. Each contour is an int32 array of points with shape (n, 1, 2).

  • mode: cv2.RETR_EXTERNAL returns only outer boundaries (usually what you want); RETR_TREE also returns holes, with parent/child links in hierarchy.
  • method: cv2.CHAIN_APPROX_SIMPLE compresses straight runs to their end points, saving memory; CHAIN_APPROX_NONE keeps every pixel.

With contours you can measure:

  • cv2.contourArea(c) and cv2.arcLength(c, closed=True)
  • cv2.boundingRect(c) gives an upright (x, y, w, h); cv2.minAreaRect(c) a rotated box
  • cv2.moments(c): centroid at (m10/m00, m01/m00)
  • cv2.approxPolyDP(c, eps, True) simplifies to a polygon; 4 vertices suggests a rectangle
  • cv2.convexHull, and circularity 4*pi*area/perimeter**2 (1 for a circle)

Filter by area to drop noise, sort by area to find the largest object. cv2.drawContours(img, contours, -1, color, thickness) draws them; index -1 means all.

Feed it a clean binary mask (threshold, Canny, morphology first). Old OpenCV 3 returned three values, which is why older tutorials unpack _, contours, _.

Example

import cv2
import numpy as np

mask = np.zeros((100, 160), np.uint8)
cv2.rectangle(mask, (10, 10), (60, 40), 255, -1)
cv2.circle(mask, (110, 60), 25, 255, -1)
mask[90, 5] = 255                               # one-pixel noise

contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL,
                                       cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))                            # 3
objs = [c for c in contours if cv2.contourArea(c) > 20]
for c in sorted(objs, key=cv2.contourArea, reverse=True):
    area = cv2.contourArea(c)
    x, y, w, h = cv2.boundingRect(c)
    m = cv2.moments(c)
    cx, cy = m["m10"] / m["m00"], m["m01"] / m["m00"]
    peri = cv2.arcLength(c, True)
    corners = len(cv2.approxPolyDP(c, 0.02 * peri, True))
    circ = 4 * np.pi * area / peri ** 2
    print(int(area), (x, y, w, h), (round(cx), round(cy)), corners, round(circ, 2))
# 1890 (85, 35, 51, 51) (110, 60) 8 0.87   circle: many corners, round
# 1500 (10, 10, 51, 31) (35, 25) 4 0.74    rectangle: 4 corners

Remember

  • findContours needs a binary mask and returns (contours, hierarchy)
  • RETR_EXTERNAL for outer outlines, RETR_TREE for nested holes
  • Measure with contourArea, boundingRect, moments, approxPolyDP
  • Filter tiny contours by area to ignore noise

Watch out for

  • Running findContours on a noisy, un-thresholded grayscale image and getting thousands of tiny contours.

Check yourself

  1. You only want the outlines of separate objects, ignoring holes inside them. Which mode?
  2. How do you compute a contour's centroid x from its moments `m`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.opencv.org/4.x/d4/d73/tutorial_py_contours_begin.html