Morphology
Cleaning up shapes.
After thresholding, your shapes are messy. There are tiny white specks in the background, and small black holes inside your objects. Before counting anything, you need to tidy up. Two very simple operations do almost everything.

- White specks and black holes
- Tidy up before counting
- Two simple operations
Erode and dilate
Erosion shrinks white areas. It removes small specks, because a speck is too small to survive. Dilation grows white areas, and it fills small holes. Combine them. Opening is erode then dilate, which removes specks, and keeps the objects at their original size. Closing is dilate then erode, which fills holes, again keeping the size.
Opening and closing
We start with the thresholded coins. Opening removes the background specks. Closing fills the holes in the coins. The kernel sets how strongly each one acts. Run it. Three stages. The cleaned result has solid white shapes, ready for counting. These two lines are in nearly every classic vision system.
import cv2
import numpy as np
import matplotlib.pyplot as plt
from skimage import data
gray = data.coins()
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
kernel = np.ones((5, 5), np.uint8)
opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=3)
fig, ax = plt.subplots(1, 3, figsize=(10, 3))
for a, im, n in zip(ax, [binary, opened, closed], ["Threshold", "Opened", "Closed"]):
a.imshow(im, cmap="gray"); a.set_title(n); a.axis("off")
plt.tight_layout(); plt.savefig("morph.png", dpi=110)
print("White pixels:", (binary > 0).sum(), "->", (closed > 0).sum())White pixels: 45117 -> 37934
Finding and counting objects
Now we can count. Find contours traces the outline of each white shape. We drop anything small, using the area. Then we draw the outlines. Run it. Look at that. Twenty four coins. It's a complete pipeline. Blur, threshold, clean up, and count. And all of that with no machine learning, running in a few milliseconds. AppleEye's first version can count apples on a conveyor belt in exactly the same way.
import cv2
import numpy as np
import matplotlib.pyplot as plt
from skimage import data
gray = data.coins()
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 71, -8)
kernel = np.ones((5, 5), np.uint8)
clean = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=3)
contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
coins = [c for c in contours if cv2.contourArea(c) > 1000]
out = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
cv2.drawContours(out, coins, -1, (0, 255, 0), 2)
plt.figure(figsize=(8, 4)); plt.imshow(out); plt.axis("off")
plt.tight_layout(); plt.savefig("coins.png", dpi=110)
print("Objects counted:", len(coins))
print("Sizes:", sorted(int(cv2.contourArea(c)) for c in coins)[:5])Objects counted: 15 Sizes: [1070, 1269, 1281, 1487, 1532]
Summary
Blur, threshold, clean, and count. This classic pipeline solves many real problems, cheaply. But it only works when objects are easy to separate. When apples overlap, it fails. Next, we'll fix images before analysing them.
Key takeaways- Open removes specks
- Close fills holes
- Contours count objects