Counting Objects
Connected components.
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.

- 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.
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])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, spotsSummary
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