Put similar values into groups
K-means is a clustering algorithm. It finds groups in data without being told the name of each group.
The number of groups we want.
One group of similar values.
The average value of a cluster. It is also called a centroid.
For 2, 3, 4, 20, 22, 24 and K = 2, we expect one group near 3 and another near 22.
What does K mean?
Nearest center wins
Before using OpenCV, follow one round manually. This makes the library code much easier to understand.
Values: 2, 3, 4, 20, 22, 24
Starting centers: 3 and 20
For value 22:
Distance from 3 = |22 - 3| = 19
Distance from 20 = |22 - 20| = 2
22 joins the second cluster.Values: 2, 3, 4
New center: (2 + 3 + 4) / 3 = 3
Values: 20, 22, 24
New center: (20 + 22 + 24) / 3 = 22
Assign every value to the nearest center → calculate new average centers → repeat until the centers barely change.
How is a new center calculated?
Create a tiny Python project
We need one Python file, one image, OpenCV, and NumPy.
k-means-project/
├── main.py
└── image.jpgpip install opencv-python numpyimport cv2
import numpy as np| Line | Meaning |
|---|---|
import cv2 | Loads OpenCV so we can read, process, and display images. |
import numpy as np | Loads NumPy and gives it the shorter name np. NumPy works with the arrays inside images. |
An image is an array of numbers. A colour pixel has three values. OpenCV keeps them in BGR order: Blue, Green, Red.
Read the image safely
OpenCV must turn image.jpg into a NumPy array before K-means can use its pixels.
image = cv2.imread("image.jpg")cv2.imread() reads the file. The returned image array is stored in image.if image is None:
raise FileNotFoundError("Could not find image.jpg")if image is None:Checks whether OpenCV failed to read the image. The colon starts the indented block.
raise FileNotFoundError(...)Stops the program and shows a useful message. This is clearer than allowing a confusing error later.
A shape such as (400, 600, 3) means 400 rows, 600 columns, and 3 colour values per pixel.
What does image is None usually mean?
Give K-means one pixel per row
OpenCV K-means accepts a 2D float32 array. Our normal colour image is a 3D array, so we must prepare it.
pixels = image.reshape(-1, 3)| Part | Meaning |
|---|---|
image.reshape(...) | Changes the arrangement of the data. It does not delete pixels. |
-1 | “NumPy, calculate the number of rows for me.” |
3 | Keep three values in each row: B, G, and R. |
pixels = | Store the rearranged array in a new variable. |
(400, 600, 3)
-1, 3
(240000, 3)
pixels = np.float32(pixels)float32 data type. Without this line, you may get type == CV_32F.After reshaping, what does one row represent?
Create the termination criteria
K-means repeats its assignment and averaging steps. The criteria tell OpenCV when it has done enough work.
criteria = (
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100,
0.2
)| Code | Easy meaning |
|---|---|
criteria = ( | Create a tuple named criteria. |
TERM_CRITERIA_EPS | Allow stopping when the centers change by only a tiny amount. |
+ | Combine both stopping conditions. |
TERM_CRITERIA_MAX_ITER | Allow stopping at the maximum number of iterations. |
100 | Never repeat more than 100 times. |
0.2 | The epsilon, or tiny-change limit. |
) | Finish the tuple. |
Stop when the centers settle within 0.2, or after 100 repetitions—whichever happens first. It does not always run 100 times.
When can this K-means run stop?
Ask for three colour groups
Now pass the prepared pixels and settings to cv2.kmeans().
k = 3
compactness, labels, centers = cv2.kmeans(
pixels,
k,
None,
criteria,
10,
cv2.KMEANS_PP_CENTERS
)| Argument | What it tells OpenCV |
|---|---|
pixels | Group these BGR pixel values. |
k | Create 3 clusters. |
None | We are not supplying starting labels. |
criteria | Use our stopping rules. |
10 | Try the algorithm 10 times with different starting centers and keep the best result. |
KMEANS_PP_CENTERS | Choose smart, spread-out starting centers. |
The total squared distance between pixels and their centers. Smaller means tighter groups.
One cluster number—0, 1, or 2—for every pixel.
The average BGR colour of every cluster.
It does not know “sky,” “grass,” or “car.” It only sees numbers and groups similar colours.
Replace every pixel with its center
The cluster result is still a list. We will convert it into a displayable image.
centers = np.uint8(centers)uint8 converts them to whole image-colour values from 0 to 255.segmented_pixels = centers[labels.flatten()]| Part | Meaning |
|---|---|
labels.flatten() | Turns the label column into a simple one-dimensional list. |
centers[...] | Uses each label as an index to pick that cluster's average colour. |
segmented_pixels = | Stores all replacement pixel colours. |
segmented_image = segmented_pixels.reshape(image.shape)Use centers[0]
Cluster 0's average BGR
What decides a pixel's replacement colour?
Show both images
Display the original and segmented versions side by side so you can compare them.
cv2.imshow("Original Image", image)
cv2.imshow("K-Means Segmentation", segmented_image)
cv2.waitKey(0)
cv2.destroyAllWindows()| Line | Meaning |
|---|---|
cv2.imshow("Original Image", image) | Open a window titled “Original Image” and show the original array. |
cv2.imshow("K-Means Segmentation", segmented_image) | Open another window and show the clustered image. |
cv2.waitKey(0) | Wait until any key is pressed. Zero means wait without a time limit. |
cv2.destroyAllWindows() | Close all OpenCV windows cleanly. |
cv2.imwrite("segmented_image.jpg", segmented_image)imwrite() creates the output file in your project folder.
cv2.imshow() is meant for local desktop programs. In a notebook, display the image with Matplotlib or the notebook's image-display function.
Run the complete segmentation program
The comments summarize why each part exists. Compare this final program with every earlier step.
import cv2
import numpy as np
# Read the image from the project folder.
image = cv2.imread("image.jpg")
# Stop early if the image could not be read.
if image is None:
raise FileNotFoundError("Could not find image.jpg")
# Turn the image into rows of BGR pixels.
pixels = image.reshape(-1, 3)
# OpenCV K-means requires float32 input.
pixels = np.float32(pixels)
# Stop after convergence or at most 100 iterations.
criteria = (
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100,
0.2
)
# Create three colour clusters.
k = 3
# Group similar pixel colours.
compactness, labels, centers = cv2.kmeans(
pixels,
k,
None,
criteria,
10,
cv2.KMEANS_PP_CENTERS
)
# Convert center colours to whole numbers from 0 to 255.
centers = np.uint8(centers)
# Replace every pixel with its cluster's center colour.
segmented_pixels = centers[labels.flatten()]
# Restore the original image dimensions.
segmented_image = segmented_pixels.reshape(image.shape)
# Display the original and segmented images.
cv2.imshow("Original Image", image)
cv2.imshow("K-Means Segmentation", segmented_image)
# Wait for a key, then close the windows.
cv2.waitKey(0)
cv2.destroyAllWindows()Try it yourself
- Run the program with
k = 2. - Change it to
k = 5. - Change it to
k = 10. - Observe how a larger K keeps more colour detail.
- Save your preferred result using
cv2.imwrite().
Common errors
type == CV_32F
Your pixels are not float32. Add pixels = np.float32(pixels) before calling K-means.
data0.dims <= 2
You probably passed the original 3D image. Pass image.reshape(-1, 3).astype(np.float32).
The output groups unrelated objects
This is expected. Plain K-means groups colours, not objects, and it ignores pixel position.
Completion checklist
- I know that K is the number of clusters.
- I can explain assignment, averaging, and repetition.
- I know why the image becomes one pixel per row.
- I know why OpenCV requires
float32. - I understand the two stopping conditions.
- I can explain
labelsandcenters. - I can rebuild and display the segmented image.
Codelab complete! 🎉
You can now explain and implement colour-based K-means image segmentation one line at a time.