Unit 03.03: Morphology for the gaps thresholding leaves
Thresholding leaves specks outside and holes inside. Morphology is how you clean both, and the order matters.
Opening then closing
Opening removes small bright regions; closing fills small dark ones.
The code applies both to a noisy mask and counts what changes.
import cv2
import numpy as np
mask = np.zeros((32, 32), dtype="uint8")
mask[8:24, 8:24] = 255
rng = np.random.default_rng(1)
holes = rng.random(mask.shape) < 0.12
mask[holes & (mask == 255)] = 0
specks = (rng.random(mask.shape) < 0.03) & (mask == 0)
mask[specks] = 255
kernel = np.ones((3, 3), dtype="uint8")
opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel)
for label, img in [("raw", mask), ("opened", opened), ("opened+closed", closed)]:
n, _ = cv2.connectedComponents(img)
inside_holes = int((img[10:22, 10:22] == 0).sum())
print(f"{label:14} components {n - 1:>3} holes inside the square {inside_holes:>3}")
# Opening removes specks outside; closing fills holes inside. In that order:
# closing first would grow the specks into blobs that opening can no longer
# remove.
Opening first, then closing. Reversed, closing would grow the specks into blobs large enough that opening can no longer remove them - so the order is not stylistic.
The component count is the useful measurement. Going from many components to one is the operation working; going to zero means the kernel was too large and removed the object as well.
The mistake this prevents
The mistake is increasing the kernel size until the mask looks clean. Past a point it is removing the object's own structure - thin parts disappear, and two nearby objects merge into one.
Takeaway
Open then close, and measure the component count. A kernel large enough to clean everything is usually large enough to merge or erase objects.
