Dominant Colours
Grading fruit with clustering.
Fruit buyers talk about colour grades. How much of the fruit is deep red, and how much is pale? Rather than deciding on thresholds by hand, we can let the image tell us which colours it contains. That's a neat use of a tool that you've already met. Clustering.

- Buyers talk about colour grades
- Don't set thresholds by hand
- Let the image tell us
K-means on pixels
Each pixel is just a point, with three numbers. So we can run K means on the pixels, and find three groups of similar colours. We take every twentieth pixel, to keep it fast. Run it. Three colours come out, with their shares. Grey belt, at eighty nine percent. Deep apple red, at ten percent. And one percent pale highlight. In one line, we've got a summary of the image. Run this inside each apple's mask, and the share of deep red becomes a measure of ripeness, for each fruit. This is also how design tools pull a colour palette from a photo.
import cv2
import numpy as np
from sklearn.cluster import KMeans
from belt import belt
img, _, _ = belt(n=6)
pixels = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).reshape(-1, 3)[::20]
km = KMeans(3, n_init=5, random_state=0).fit(pixels)
share = np.bincount(km.labels_) / len(pixels)
for c, s in sorted(zip(km.cluster_centers_.astype(int).tolist(), share), key=lambda p: -p[1]):
print(f"RGB {c}: {s:.0%}")RGB [82, 77, 71]: 89% RGB [199, 40, 35]: 10% RGB [228, 88, 88]: 1%
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
Clustering pixels summarises colour. Next, finding damage in material that has a repeating pattern.
Key takeaways- Pixels are points in colour space
- K-means finds main colours
- Shares give a grade