Skip to content

Thresholding

Thresholding is the art of making a decision: is this pixel “foreground” or “background”? It converts a grayscale image into a binary image — pure black and pure white, nothing in between. This is essential for tasks like contour detection, OCR, and object segmentation where you need to separate “stuff” from “not stuff.”

cv2.threshold() applies a single global threshold to every pixel in the image:

  • Pixel value above the threshold → set to maxval (usually 255, white)
  • Pixel value below the threshold → set to 0 (black)
Python
import cv2
img = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
# Returns: (threshold_value_used, thresholded_image)
ret, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

OpenCV provides five different threshold behaviors:

Input pixel value: V Threshold: T Max value: M
THRESH_BINARY: V > T → M, else → 0
THRESH_BINARY_INV: V > T → 0, else → M
THRESH_TRUNC: V > T → T, else → V (caps at threshold)
THRESH_TOZERO: V > T → V, else → 0 (keeps above, zeros below)
THRESH_TOZERO_INV: V > T → 0, else → V (keeps below, zeros above)

In practice, THRESH_BINARY handles 90% of use cases. The others are useful for specialized tasks like clamping dynamic range.

Python
# The most common variants
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
_, binary_inv = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

A single global threshold works great when lighting is uniform. But the real world is rarely that kind. Consider a photo of a document with a shadow across it — the left side is darker than the right:

Pixel intensities across the image:
Left (shadow): [40, 45, 50, 55, 60] ← Text here is ~50
Right (bright): [180, 190, 200, 210, 220] ← Text here is ~190
With threshold = 127:
Left side → ALL BLACK (text and background both below 127)
Right side → ALL WHITE (text and background both above 127)
Result: You've lost all the text on the shadowed side!

This is where adaptive thresholding comes in.

Comparison of simple, adaptive, and Otsu thresholding on an unevenly lit image

cv2.adaptiveThreshold() solves the uneven lighting problem by computing a different threshold for each small region of the image. Instead of one global number, it looks at the local neighborhood around each pixel.

Python
# adaptiveMethod: how to compute the local threshold
# blockSize: size of the neighborhood (must be odd)
# C: constant subtracted from the computed threshold
adaptive = cv2.adaptiveThreshold(
gray,
maxValue=255,
adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
thresholdType=cv2.THRESH_BINARY,
blockSize=11,
C=2
)
MethodHow It WorksWhen to Use
ADAPTIVE_THRESH_MEAN_CThreshold = mean of the neighborhood minus CSimple, fast
ADAPTIVE_THRESH_GAUSSIAN_CThreshold = Gaussian-weighted mean minus CBetter results, slightly slower
  • blockSize: Controls how “local” the threshold is. Larger values = smoother threshold, smaller values = more responsive to local changes. Must be odd (3, 5, 7, 11…).
  • C: A fine-tuning constant. Increasing C pushes the threshold lower (more white pixels). Decreasing makes it higher (more black pixels). Start with 2-5.

Otsu’s Method — Let the Computer Decide

Section titled “Otsu’s Method — Let the Computer Decide”

What if you don’t want to pick a threshold value at all? Otsu’s method analyzes the image histogram and automatically finds the optimal threshold that minimizes the combined spread (variance) of the foreground and background pixel groups.

The idea: if your histogram has two peaks (a “bimodal” distribution — one for the background, one for the foreground), Otsu finds the valley between them.

Python
# Pass 0 as the threshold value — Otsu will override it
# Add cv2.THRESH_OTSU as a flag
ret, binary = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 'ret' now contains the threshold value that Otsu calculated
print(f"Otsu selected threshold: {ret}")

Otsu works on the histogram, and noisy images have messy histograms. By blurring first, you smooth the histogram and help Otsu find cleaner separation:

Python
# 1. Blur to smooth the histogram
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 2. Otsu on the blurred image
ret, binary = cv2.threshold(blurred, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Practical Example: Threshold a Document for OCR

Section titled “Practical Example: Threshold a Document for OCR”
  1. Load and convert to grayscale:

    Python
    import cv2
    img = cv2.imread("document_photo.jpg")
    if img is None:
    raise FileNotFoundError("Could not load image!")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  2. Apply Gaussian blur to reduce noise:

    Python
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
  3. Try adaptive thresholding (best for documents with uneven lighting):

    Python
    binary = cv2.adaptiveThreshold(
    blurred, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    blockSize=11, C=2
    )
  4. Display the result:

    Python
    cv2.imshow("Original", gray)
    cv2.imshow("Binary (Adaptive)", binary)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
  • cv2.threshold(): Global threshold. Fast but fails on unevenly lit images. Input must be grayscale.
  • THRESH_BINARY: The most common type — above threshold = white, below = black.
  • cv2.adaptiveThreshold(): Computes local thresholds. Use GAUSSIAN_C for better results. Tune blockSize and C.
  • Otsu’s method: Automatically finds the optimal threshold. Add cv2.THRESH_OTSU flag and pass 0 as the threshold value.
  • Blur + Otsu: GaussianBlur before Otsu produces cleaner results — the go-to combo when in doubt.
  • Bimodal histograms: Otsu works best when the image has two clear brightness groups. Check with a histogram plot if unsure.