Contours and object detection basics
Find and filter shapes with contours, then use a trained model when a threshold is not enough - and know where the line is.
Threshold, find, filter
Contours trace the boundary of connected bright regions after a binary threshold. They are ideal for counting, measuring and locating well-separated objects on a controlled background, and hopeless once objects overlap or lighting varies across the frame.
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
kept = [c for c in contours if cv2.contourArea(c) > 500] # drop noise specks
for c in kept:
x, y, w, h = cv2.boundingRect(c)
area = cv2.contourArea(c)
circularity = 4 * 3.14159 * area / (cv2.arcLength(c, True) ** 2)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
print(x, y, w, h, round(circularity, 2)) # near 1.0 means a circleRETR_EXTERNALreturns only outer boundaries;RETR_TREEgives the full hierarchy when shapes are nested.- Filter by area, aspect ratio and circularity before you use a contour - the count is otherwise dominated by noise.
- Compare areas with
contourArea, notlen(contour); the number of boundary points depends on the approximation flag. - Morphological open removes specks, close fills pinholes. Do this before finding contours, never after.
When a threshold is not enough
# A trained detector via cv2.dnn: same preprocessing contract as the training pipeline
net = cv2.dnn.readNetFromONNX("yolov8n.onnx")
blob = cv2.dnn.blobFromImage(frame, scalefactor=1 / 255.0, size=(640, 640),
mean=(0, 0, 0), swapRB=True, crop=False)
net.setInput(blob)
outputs = net.forward() # shape depends on the exported model - inspect it
for det in outputs[0].T: # [x, y, w, h, class scores...]
scores = det[4:]
class_id = int(scores.argmax())
if scores[class_id] > 0.4:
print(class_id, round(float(scores[class_id]), 3))| Approach | Depends on | Fails when |
|---|---|---|
| Threshold plus contours | Stable background and lighting | Objects touch, overlap or the scene changes |
| Haar cascade | Frontal, unoccluded faces | Pose, angle or lighting differs from the training set |
| HOG plus SVM | Rigid, silhouette-shaped objects | Shape varies a lot between instances |
| Deep detector (DNN, ONNX) | Training data coverage | Classes are rare, small, or absent from training |
⚠️
A detector reports boxes; it does not tell you whether they are right. Always measure precision and recall on your own images with your own definition of a correct match, and expect a model to miss small or overlapping objects and to hallucinate confident boxes on textures it has never seen.
FAQ
How do I choose between contours and a neural detector?
Use contours when the scene is controlled, the objects are separated and you can express what you want as brightness, size and shape rules. Move to a trained detector as soon as those rules need exceptions, because a rule list that grows with every new image is a detector you are writing by hand.
Why do my boxes shift after resizing?
Coordinates you computed at one resolution do not apply at another. Either do the detection at the original size, or scale the boxes by the same factor you resized the image by.
Related
Filters and edge detection NumPy arrays
Last refreshed 2026-09-18.