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 circle
  • RETR_EXTERNAL returns only outer boundaries; RETR_TREE gives 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, not len(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))
ApproachDepends onFails when
Threshold plus contoursStable background and lightingObjects touch, overlap or the scene changes
Haar cascadeFrontal, unoccluded facesPose, angle or lighting differs from the training set
HOG plus SVMRigid, silhouette-shaped objectsShape varies a lot between instances
Deep detector (DNN, ONNX)Training data coverageClasses 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.

Filters and edge detection NumPy arrays

Last refreshed 2026-09-18.