AI Hints
Home›Computer Vision Tutorial›Measuring the World
Computer Vision · Lesson 51 of 111

Counting Objects

Connected components.

⏱ 2 min read▶ Video at 1:22:09🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

The owner has a simple question. How many apples went past today? Counting sounds easy, and for a computer, it's a lovely little problem. Find the apple pixels, then find groups of pixels which touch each other.

Counting Objects
From the video: Counting Objects
  • How many apples today?
  • Sounds easy
  • Find groups of touching pixels

Blobs of colour

We convert to HSV, and keep red pixels. Red wraps around the colour wheel, so we need two ranges. Closing fills up small holes. Then connected components labels each separate blob, and gives us its area. We ignore tiny ones. Run it. We found eight, and the truth is nine. Why the miss? Look at the biggest area. Thirteen thousand pixels, which is roughly twice the size of the others. Two apples were touching, so they became one blob. A simple check like this one reveals the error, and the fix is the watershed method that you learned earlier.

count.pypython
import cv2
from belt import belt

img, boxes, _ = belt(n=9)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
low = cv2.inRange(hsv, (0, 90, 60), (12, 255, 255))
high = cv2.inRange(hsv, (170, 90, 60), (180, 255, 255))
mask = cv2.morphologyEx(low | high, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)))

count, labels, stats, centres = cv2.connectedComponentsWithStats(mask)
apples = [s for s in stats[1:] if s[cv2.CC_STAT_AREA] > 800]
print(f"Found {len(apples)} apples, truth {len(boxes)}")
for s in sorted(apples, key=lambda s: -s[cv2.CC_STAT_AREA])[:3]:
    print("Area in pixels:", s[cv2.CC_STAT_AREA])
Output
Found 8 apples, truth 9
Area in pixels: 13252
Area in pixels: 7043
Area in pixels: 6633
Also uses belt.py
import numpy as np
import cv2

def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
    """Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
    rng = np.random.default_rng(seed)
    h, w = size
    img = np.full((h, w, 3), (70, 75, 80), np.uint8)
    img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
    boxes, spots, taken = [], [], []
    while len(boxes) < n:
        r = int(rng.integers(32, 48))
        x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
        if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
            continue
        taken.append((x, y, r))
        red = int(rng.integers(150, 225))
        cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
        cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
        boxes.append((x - r, y - r, x + r, y + r))
    for x, y, r in taken[:bruises]:
        cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
        spots.append((x, y, r))
    img = cv2.GaussianBlur(img, (3, 3), 0)
    return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spots

Summary

Connected components count blobs, and strange areas warn you about mistakes. Next, how big is each apple?

Key takeaways
  • Two ranges for red
  • Components label blobs
  • Unusual areas reveal merges
Next up: Measuring Size and Shape — from pixels to millimetres