Thresholding with Otsu's method
Pick a threshold automatically by maximizing the separation between dark and bright pixel groups.
def otsu(pixels):
histogram = [0] * 256
for pixel in pixels:
histogram[pixel] += 1
total = len(pixels)
total_sum = sum(level * count for level, count in enumerate(histogram))
best_variance, best_threshold = -1.0, 0
weight_dark = sum_dark = 0
for t in range(256):
weight_dark += histogram[t]
if weight_dark == 0:
continue
weight_bright = total - weight_dark
if weight_bright == 0:
break
sum_dark += t * histogram[t]
mean_dark = sum_dark / weight_dark
mean_bright = (total_sum - sum_dark) / weight_bright
between = weight_dark * weight_bright * (mean_dark - mean_bright) ** 2
if between > best_variance:
best_variance, best_threshold = between, t
return best_threshold
pixels = [28, 30, 35, 40, 33, 31, 38, 29, 205, 210, 215, 200, 220, 208, 212, 218]
threshold = otsu(pixels)
print(threshold)
print([1 if pixel > threshold else 0 for pixel in pixels])40 [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
Explanation
Thresholding turns a grayscale image into a binary one: pixels above a threshold become foreground (1) and the rest become background (0). It is a basic step in document scanning, microscopy, and many segmentation pipelines. The difficulty is choosing the threshold, since a fixed value fails when lighting changes.
Otsu's method (1979) chooses it automatically from the histogram. For each candidate threshold t it splits the pixels into a dark class and a bright class, and it computes the between-class variance w₀·w₁·(μ₀ − μ₁)², where w are the class sizes and μ the class means. The threshold with the largest value separates the two groups best. It works well when the histogram has two clear peaks.
The example has eight dark pixels (28 to 40) and eight bright ones (200 to 220). Every threshold from 40 to 199 separates them equally well, so the between-class variance is flat there, and the program keeps the first best value, 40. Pixels strictly above it become 1. Any value in the gap would do for this image. Otsu's method assumes a roughly bimodal histogram and uniform lighting. With uneven illumination, local (adaptive) thresholding, which picks a threshold for each neighbourhood, is usually better.
Explore the dry run
def otsu(pixels): histogram = [0] * 256 for pixel in pixels: histogram[pixel] += 1 total = len(pixels) total_sum = sum(level * count for level, count in enumerate(histogram)) best_variance, best_threshold = -1.0, 0 weight_dark = sum_dark = 0 for t in range(256): weight_dark += histogram[t] if weight_dark == 0: continue weight_bright = total - weight_dark if weight_bright == 0: break sum_dark += t * histogram[t] mean_dark = sum_dark / weight_dark mean_bright = (total_sum - sum_dark) / weight_bright between = weight_dark * weight_bright * (mean_dark - mean_bright) ** 2 if between > best_variance: best_variance, best_threshold = between, t return best_threshold pixels = [28, 30, 35, 40, 33, 31, 38, 29, 205, 210, 215, 200, 220, 208, 212, 218]threshold = otsu(pixels)print(threshold)print([1 if pixel > threshold else 0 for pixel in pixels])Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Build the histogram: how many pixels have each of the 256 gray levels. | — | — | |
| For each candidate threshold, measure how far apart the dark and bright class means are, weighted by class sizes. | — | — | |
| Keep the threshold with the largest separation. Ties keep the earliest, so the flat region gives 40. | — | — | |
| Every value between 40 and 199 separates the groups equally; the first maximum is 40. | — | 40 | |
| Pixels above 40 become foreground: the eight bright pixels are 1, the eight dark ones 0. | — | 40 [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] |
Cost and common mistakes
O(N + L) time for N pixels and L = 256 gray levels: one pass to build the histogram and one over the levels.
- Otsu assumes two classes. Histograms with more modes, or with a tiny foreground, can give a poor threshold.
- Uneven lighting spoils a single global threshold. Use adaptive thresholding or correct the illumination first.
- A tie in the maximum is broken by implementation choice. Libraries may return the first, the last, or the middle of the flat range.
Try it yourself
Would a threshold of 100 give the same binary image here?
Reveal answer and reasoning
Yes. Every dark pixel is at most 40 and every bright pixel is at least 200, so any threshold from 40 to 199 yields the same result.