From a black-and-white mask to counted, measured and named objects.
A connected component is a group of connected pixels with the same value.
4-connectivity: up, down, left, right
8-connectivity: plus diagonals
Use cases: count objects, label regions, extract features.
import cv2
import numpy as np
# binary image
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# label connected components
num_labels, labels = cv2.connectedComponents(binary) # ans:
# ans: num_labels includes the background (label 0)
# ans: actual objects = num_labels - 1
# labels is the same size as binary
# ans: each pixel has a label (0=background, 1,2,3...=objects)
# visualize labels
labels_vis = np.uint8(255 * labels / labels.max()) # ans:
# get statistics for each component
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( # ans:
binary, connectivity=8) # ans:
# stats: [x, y, width, height, area] for each label
for i in range(1, num_labels): # ans:
x, y, w, h, area = stats[i] # ans:
cx, cy = centroids[i] # ans:
print(f"Object {i}: area={area}, center=({cx}, {cy})") # ans:
# filter by area
min_area = 100 # ans:
large_components = [i for i in range(1, num_labels) # ans:
if stats[i, cv2.CC_STAT_AREA] > min_area] # ans:
A contour is the boundary of a region. Click a shape to measure it.
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours: # ans:
# area
area = cv2.contourArea(cnt) # ans:
# perimeter
perimeter = cv2.arcLength(cnt, True) # ans:
# ans: True means a closed contour
# aspect ratio
x, y, w, h = cv2.boundingRect(cnt) # ans:
aspect_ratio = float(w) / h # ans:
contourArea measures the polygon through the boundary pixels, so it comes out a little smaller than the pixel count in stats.
# straight bounding rectangle
x, y, w, h = cv2.boundingRect(cnt) # ans:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) # ans:
# minimum area rectangle (can be rotated)
rect = cv2.minAreaRect(cnt) # ans:
box = cv2.boxPoints(rect) # ans:
box = np.intp(box) # ans:
cv2.drawContours(img, [box], 0, (0, 0, 255), 2) # ans:
minAreaRect returns ((cx, cy), (w, h), angle). Older code uses np.int0, which NumPy 2 removed; np.intp is the same type.
The centroid is the centre of mass of a region, computed from its moments.
# compute moments
M = cv2.moments(cnt) # ans:
# centroid coordinates
if M['m00'] != 0: # ans:
cx = int(M['m10'] / M['m00']) # ans:
cy = int(M['m01'] / M['m00']) # ans:
else: # ans:
cx, cy = 0, 0 # ans:
# draw centroid
cv2.circle(img, (cx, cy), 5, (255, 0, 0), -1) # ans:
The convex hull is the smallest convex polygon containing the contour: a rubber band around the shape.
Convexity defects are the gaps between the contour and its hull (pink).
# compute convex hull
hull = cv2.convexHull(cnt) # ans:
cv2.drawContours(img, [hull], 0, (0, 255, 0), 2) # ans:
# check if contour is convex
is_convex = cv2.isContourConvex(cnt) # ans:
# convexity defects
hull = cv2.convexHull(cnt, returnPoints=False) # ans:
defects = cv2.convexityDefects(cnt, hull) # ans:
cv2.matchShapes compares two contours using Hu moments, which do not change with rotation or scale. Lower distance = more similar; 0 means identical.
Click a shape to compare every other shape with it.
# compare two contours
cnt1 = contours[0]
cnt2 = contours[1]
distance = cv2.matchShapes(cnt1, cnt2, cv2.CONTOURS_MATCH_I1, 0) # ans:
# ans: lower distance = more similar shapes
# ans: 0 means identical, larger = more different
approxPolyDP simplifies a contour to a polygon with fewer points (the Douglas-Peucker algorithm). Count the vertices to name the shape.
# approximate contour
epsilon = 0.01 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True) # ans:
# ans: epsilon: approximation accuracy (smaller = more accurate)
# detect shapes by number of vertices
if len(approx) == 3: # ans:
shape = "Triangle" # ans:
elif len(approx) == 4: # ans:
shape = "Rectangle" # ans:
elif len(approx) > 4: # ans:
shape = "Circle" # ans:
A crude rule: on the previous slide the plus signs and digits have many vertices too, and get called circles.
Answer first, then press → to check.
What is a connected component?
A group of connected pixels with the same value.
4 vs 8 connectivity?
4 uses up/down/left/right; 8 adds the diagonals.
What is a contour?
The boundary of a shape or region.
What is a centroid?
The centre of mass of a region.
What is a convex hull?
The smallest convex polygon containing the contour.
What are moments?
Weighted sums over a shape's pixels that describe it: area, centre, spread, orientation.
How do you compute the centroid from moments?
cx = M10/M00, cy = M01/M00.
What is approxPolyDP?
It simplifies a contour to a polygon with fewer points.
What is shape matching?
Comparing the similarity of two contours.
# label connected components
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
num_labels, labels = cv2.connectedComponents(binary) # ans:
print(f"Found {num_labels - 1} objects") # ans:
# connected components with stats
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary) # ans:
for i in range(1, num_labels): # ans:
area = stats[i, cv2.CC_STAT_AREA] # ans:
cx, cy = centroids[i] # ans:
# find and draw contours
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours: # ans:
area = cv2.contourArea(cnt) # ans:
if area > 100: # ans:
x, y, w, h = cv2.boundingRect(cnt) # ans:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) # ans:
# compute centroid
M = cv2.moments(cnt)
cx = int(M['m10'] / M['m00']) # ans:
cy = int(M['m01'] / M['m00']) # ans:
cv2.circle(img, (cx, cy), 5, (255, 0, 0), -1) # ans:
# convex hull
hull = cv2.convexHull(cnt) # ans:
cv2.drawContours(img, [hull], 0, (0, 255, 0), 2) # ans:
# approximate contour
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True) # ans:
print(f"Vertices: {len(approx)}") # ans: