Unit 04.01: Contours, and what counts as one object
Contours turn a binary mask into regions. What counts as one region is not a question the algorithm can answer.
Touching objects are one contour
findContours traces the boundaries of connected components.
The code runs it on a mask containing one separate rectangle and two touching ones.
import cv2
import numpy as np
mask = np.zeros((64, 64), dtype="uint8")
mask[8:24, 8:24] = 255 # one square
mask[36:56, 36:44] = 255 # a second, touching a third
mask[36:56, 44:52] = 255
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"contours found: {len(contours)}")
for i, c in enumerate(contours):
x, y, w, h = cv2.boundingRect(c)
print(f" {i}: box {w}x{h} at ({x},{y}), area {cv2.contourArea(c):.0f}")
print("\nthe two touching rectangles were found as ONE contour")
# "One object" is a decision the algorithm cannot make. Two objects that touch
# are one connected region, and separating them needs something else --
# distance transforms, watershed, or a model that was taught what an object is.
The two touching rectangles are found as one contour, because they are one connected region. Nothing about the pixels says they are two objects.
Separating them needs additional information - a distance transform and watershed, a shape prior, or a model that was shown examples of what one object looks like. This is the specific point where classical methods stop and learned ones begin.
The mistake this prevents
The mistake is filtering contours by area to remove the merged ones. A merged pair has roughly twice the area of a single object, so the filter either keeps it or removes genuine large objects - the information needed to split it is simply not in the mask.
Takeaway
Contours find connected regions, not objects. Touching objects merge, and separating them requires information the binary mask does not contain.
