Computer Vision Chapter 7

Image thresholding

Thresholding turns a grayscale (or single channel) image into decisions: foreground vs background, or “keep vs zero.” Global methods use one cutoff; adaptive methods use local neighborhoods for uneven lighting. Below: fixed binary modes, Otsu and Triangle auto thresholds, adaptive blocks, and inRange for color—each with runnable snippets.

Global cv.threshold

The signature is retval, dst = cv2.threshold(src, thresh, maxval, type). For binary output, pixels above thresh become maxval (often 255), others become 0. THRESH_BINARY_INV flips the roles—useful when objects are darker than the background.

import cv2

gray = cv2.imread("scan.png", cv2.IMREAD_GRAYSCALE)
t, bin_img = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
_, bin_inv = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

Truncate and zero modes

# Values above 180 become 180; below unchanged (still grayscale)
_, trunc = cv2.threshold(gray, 180, 255, cv2.THRESH_TRUNC)

# Below threshold → 0; above → unchanged
_, tozero = cv2.threshold(gray, 100, 255, cv2.THRESH_TOZERO)
_, tozero_inv = cv2.threshold(gray, 100, 255, cv2.THRESH_TOZERO_INV)

Otsu and Triangle (automatic thresh)

Otsu picks a threshold by maximizing between-class variance of the histogram—works well for roughly bimodal histograms (clear foreground/background). Pass THRESH_OTSU as a flag combined with THRESH_BINARY; the returned t is the chosen value. Triangle fits a line from the histogram peak to the farthest point; good when one tail is long (e.g. bright objects on dark background).

import cv2

gray = cv2.imread("cells.png", cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(gray, (5, 5), 0)

t_otsu, bin_o = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print("Otsu threshold:", t_otsu)

t_tri, bin_t = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
print("Triangle threshold:", t_tri)

The first argument thresh is ignored when Otsu/Triangle is used; OpenCV still requires a placeholder (commonly 0).

Otsu on inverted image

# If objects are dark on light paper, invert first or use BINARY_INV
inv = cv2.bitwise_not(gray)
t2, bin2 = cv2.threshold(inv, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Adaptive thresholding

When illumination varies across the scene, a single global t fails. cv2.adaptiveThreshold computes a threshold from a blockSize × blockSize neighborhood around each pixel (odd size, e.g. 11, 21). ADAPTIVE_THRESH_MEAN_C uses the mean minus C; GAUSSIAN_C uses a weighted Gaussian window minus C.

import cv2

gray = cv2.imread("receipt.jpg", cv2.IMREAD_GRAYSCALE)
gray = cv2.GaussianBlur(gray, (3, 3), 0)

block, C = 15, 4
ad_mean = cv2.adaptiveThreshold(
    gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, block, C)
ad_gauss = cv2.adaptiveThreshold(
    gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 21, 5)

Increase blockSize for smoother, more global behavior; adjust C to bias lighter/darker as foreground.

Combined pipelines

Real workflows often chain blur → threshold → morphology (next chapter). Example: isolate dark text after evening out contrast.

import cv2

gray = cv2.imread("page.png", cv2.IMREAD_GRAYSCALE)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
_, bw = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# bw: likely text as white — ready for morphological cleanup

Color “thresholding” with inRange

For colored objects, threshold each channel in HSV (or LAB) space. cv2.inRange returns a binary mask where all channel constraints hold.

import cv2
import numpy as np

bgr = cv2.imread("fruit.jpg")
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
lower = np.array([35, 60, 60])
upper = np.array([85, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
fg = cv2.bitwise_and(bgr, bgr, mask=mask)

Tune lower/upper with sliders or by sampling pixels from the object; watch OpenCV’s H hue scale (0–179 for 8-bit).

Takeaways

  • Otsu for clean bimodal scenes; Triangle for skewed histograms.
  • Adaptive for shadows and uneven lighting on documents or outdoor text.
  • Use HSV + inRange when “brightness threshold” is not enough—separate hue from value.

Quick FAQ

Small Gaussian blur reduces salt-and-pepper noise so the histogram and local means are stabler—fewer speckles in the binary mask.

cv2.threshold expects single-channel 8-bit (or other supported types). For BGR, convert to gray or threshold each channel separately and combine masks with bitwise logic.