Skillversitas

Thresholding and Binarization

Thresholding turns a grayscale image into a binary image - every pixel becomes pure white (255) or pure black (0) based on a comparison. It is the bridge between "a picture" and "a set of shapes," because contour finding, connected-component analysis, and most classic object pipelines all want binary input. The global functions (threshold and its Otsu variant) assume even lighting; adaptive thresholding handles the uneven illumination that real documents and scenes have.

Global Thresholds

import cv2

gray = cv2.cvtColor(cv2.imread("photo.jpg"), cv2.COLOR_BGR2GRAY)

# threshold(src, maxval, thresh, type)
# pixels above thresh -> maxval (255), below -> the type's other value

_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
_, inverted = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

# Otsu: pick the threshold automatically from the histogram
otsu_val, binary_otsu = cv2.threshold(gray, 0, 255,
                                      cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print("otsu picked:", otsu_val)

# truncate: above -> thresh, below -> unchanged (used for contrast limiting)
_, truncated = cv2.threshold(gray, 127, 0, cv2.THRESH_TRUNC)

cv2.imwrite("binary.jpg", binary)
cv2.imwrite("binary_otsu.jpg", binary_otsu)

The type flags define the two outcomes: THRESH_BINARY (above -> 255, below -> 0), THRESH_BINARY_INV (the inverse - the one you want when objects are dark on a light background, since contours want white objects), and THRESH_TRUNC. Otsu mode (+ cv2.THRESH_OTSU) scans all 256 thresholds and picks the one that minimizes the within-class variance - the standard "no tuning" choice for bimodal images like text on paper. For inverted documents (dark ink), use THRESH_BINARY_INV + THRESH_OTSU so the ink becomes the white foreground.

Adaptive Thresholding: Uneven Lighting

import cv2

gray = cv2.cvtColor(cv2.imread("scan.jpg"), cv2.COLOR_BGR2GRAY)

# per-pixel threshold from a local neighborhood
adaptive = cv2.adaptiveThreshold(gray, 255,
                                 cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                 cv2.THRESH_BINARY_INV, 51, 15)

# mean variant: average of the neighborhood minus C
adaptive_mean = cv2.adaptiveThreshold(gray, 255,
                                      cv2.ADAPTIVE_THRESH_MEAN_C,
                                      cv2.THRESH_BINARY_INV, 51, 10)

cv2.imwrite("adaptive.jpg", adaptive)

Instead of one global value, adaptiveThreshold computes a threshold for every pixel from its neighborhood - a Gaussian-weighted average (or plain mean) minus the constant C. A pixel is bright if it is brighter than its surroundings by that margin, so a scan with a bright spot on the left and a shadow on the right still binarizes correctly. The neighborhood block size (51) must be odd and larger than any character, and C is the bias - larger means fewer false positives. Adaptive is the right tool for scanned documents, PCB photos, and anything where illumination is not uniform.

Gamma Correction: Before, Not Instead

A common confusion: gamma correction is not thresholding. convertScaleAbs or a gamma LUT stretches the gray levels so a dark image becomes visible - it preserves the gradient and is a preprocessing step. Thresholding discards all levels into two. The effective pipeline for a dark, uneven scan is: gamma to even out the dynamic range, Gaussian blur to kill grain, then adaptive threshold. Each step has a distinct job; skipping one makes the next one fight a problem it was not designed for.

Choosing the Mode

ScenarioUse
Even lighting, bimodal (text, documents)threshold + Otsu
Uneven lighting (scans, photos of paper)adaptiveThreshold
Dark source image firstgamma / convertScaleAbs, then threshold
Color source, one specific colorHSV inRange mask (see color spaces), then morphology

Summary

Binarize with cv2.threshold - the type flag picks the polarity, and THRESH_OTSU removes the need to tune the value on even-lighting images. When illumination varies across the frame, switch to adaptiveThreshold (Gaussian variant, odd block size, bias constant) so each pixel is compared against its own neighborhood. Remember the polarity convention: contours want white objects, so dark-on-light sources usually need BINARY_INV. And treat gamma correction as preprocessing that makes the threshold's job possible, not a replacement for it.