Separating Touching Objects
The watershed trick.
When apples touch on a conveyor, a threshold sees one big blob. Count it, and you get one apple instead of five. There's a clever classic fix, inspired by water filling up a landscape.

- Touching apples form one blob
- One instead of five
- Think of water filling a valley
Watershed
First, the distance transform. For each white pixel, how far is it from the background? Each object's centre becomes a peak. We threshold those peaks, which gives separate seeds, one per object. Then watershed floods out from the seeds, until the floods meet. Those meeting lines separate the objects. Run it. Touching coins become separate objects. No training, no GPU, and it runs in milliseconds.
import cv2
import numpy as np
import matplotlib.pyplot as plt
from skimage import data
rgb = cv2.cvtColor(data.coins(), cv2.COLOR_GRAY2BGR)
gray = data.coins()
_, b = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
b = cv2.morphologyEx(b, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8), iterations=2)
dist = cv2.distanceTransform(b, cv2.DIST_L2, 5)
_, sure = cv2.threshold(dist, 0.4 * dist.max(), 255, 0)
n, markers = cv2.connectedComponents(sure.astype(np.uint8))
markers = cv2.watershed(rgb, markers + 1)
print("Separate objects:", n - 1)
fig, ax = plt.subplots(1, 2, figsize=(9, 3.2))
ax[0].imshow(dist, cmap="magma"); ax[0].set_title("Distance from background")
ax[1].imshow(markers, cmap="tab20"); ax[1].set_title("Split objects")
for a in ax: a.axis("off")
plt.tight_layout(); plt.savefig("watershed.png", dpi=110)Separate objects: 11
Grading a bruise
Now let's put this to work for AppleEye. We draw a simulated apple with a dark bruise. Using HSV, we find fruit pixels, then the dark ones inside the fruit. The ratio gives the bruise area. Run it. It reports the share, and it applies a grading rule. That's a real business decision, made from pixels. On real apples, you'd replace the colour rule with a trained segmentation model, but the final logic is the same.
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = np.full((240, 240, 3), 30, np.uint8)
cv2.circle(img, (120, 120), 90, (200, 40, 40), -1)
cv2.ellipse(img, (150, 90), (26, 18), 20, 0, 360, (95, 50, 40), -1)
hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
fruit = hsv[..., 1] > 80
dark = fruit & (hsv[..., 2] < 140)
share = dark.sum() / fruit.sum()
print(f"Bruise covers {share:.1%} of the apple")
print("Grade:", "reject" if share > 0.05 else "accept")
fig, ax = plt.subplots(1, 2, figsize=(7, 3.4))
ax[0].imshow(img); ax[1].imshow(dark, cmap="gray")
for a in ax: a.axis("off")
plt.tight_layout(); plt.savefig("bruise.png", dpi=110)Bruise covers 6.0% of the apple Grade: reject
Summary
Distance peaks separate touching objects, and a mask turns into a grade. So far, we've looked at single images. But AppleEye watches a moving belt. Next, video.
Key takeaways- Distance transform finds centres
- Watershed splits blobs
- Masks become decisions