Thresholding with Otsu's method

Pick a threshold automatically by maximizing the separation between dark and bright pixel groups.

Python
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])
Output
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
Guided walkthroughReady
1def otsu(pixels):
2 histogram = [0] * 256
3 for pixel in pixels:
4 histogram[pixel] += 1
5 total = len(pixels)
6 total_sum = sum(level * count for level, count in enumerate(histogram))
7 best_variance, best_threshold = -1.0, 0
8 weight_dark = sum_dark = 0
9 for t in range(256):
10 weight_dark += histogram[t]
11 if weight_dark == 0:
12 continue
13 weight_bright = total - weight_dark
14 if weight_bright == 0:
15 break
16 sum_dark += t * histogram[t]
17 mean_dark = sum_dark / weight_dark
18 mean_bright = (total_sum - sum_dark) / weight_bright
19 between = weight_dark * weight_bright * (mean_dark - mean_bright) ** 2
20 if between > best_variance:
21 best_variance, best_threshold = between, t
22 return best_threshold
23
24
25pixels = [28, 30, 35, 40, 33, 31, 38, 29, 205, 210, 215, 200, 220, 208, 212, 218]
26threshold = otsu(pixels)
27print(threshold)
28print([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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading