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
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.