Binary masks are the input to contours, morphology and measurement, and the threshold choice decides their quality.
Thresholding splits pixels into foreground and background. cv2.threshold(gray, thresh, maxval, type) returns a tuple (used_threshold, result):
cv2.THRESH_BINARY: pixels abovethreshbecomemaxval, others 0cv2.THRESH_BINARY_INV: the opposite, handy for dark objects on a light backgroundTHRESH_TRUNC,THRESH_TOZERO: clip or zero instead of binarising
Picking thresh by hand is fragile. Add cv2.THRESH_OTSU to the type (and pass 0 as thresh) and Otsu's method chooses the value that best separates a two-peaked histogram. The first return value tells you which threshold it chose.
A single global value fails when lighting varies across the image, as with a photographed document that is darker in one corner. cv2.adaptiveThreshold(gray, 255, method, type, blockSize, C) computes a separate threshold for each pixel from its neighbourhood (ADAPTIVE_THRESH_MEAN_C or ADAPTIVE_THRESH_GAUSSIAN_C), minus a constant C. blockSize must be odd.
Thresholds expect a single-channel image, so convert with cvtColor first. Blurring slightly before thresholding (cv2.GaussianBlur) removes speckle noise that would otherwise become tiny false blobs. For colour-based masks, use cv2.inRange on HSV instead.
Example
import cv2
import numpy as np
rng = np.random.default_rng(1)
gray = np.full((100, 100), 60, np.uint8) # dark background
gray[30:70, 30:70] = 190 # bright square
gray = cv2.add(gray, rng.integers(0, 30, gray.shape, dtype=np.uint8))
t, fixed = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
print(t, fixed[50, 50], fixed[5, 5]) # 127.0 255 0
t, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print("otsu chose", t) # somewhere between the two peaks
print((otsu == 255).sum()) # 1600 (the 40x40 square)
ramp = np.tile(np.linspace(0, 200, 100, dtype=np.uint8), (100, 1))
ramp[45:55, ::10] += 40 # faint marks on uneven light
ad = cv2.adaptiveThreshold(ramp, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 11, -10)
print(ad.dtype, np.unique(ad)) # uint8 [ 0 255]Remember
cv2.thresholdreturns (threshold, mask)- Add THRESH_OTSU to let OpenCV pick the threshold
- Use adaptiveThreshold when lighting varies across the image
- Threshold a single-channel image, ideally lightly blurred
Watch out for
- Forgetting that cv2.threshold returns a tuple and treating the whole tuple as the image.