Image thresholding turns grayscale intensity values into a mask that separates pixels kept for later processing from pixels treated as background. In OpenCV, it is a common preparation step before contour detection, OCR cleanup, measurement, or masking where downstream code needs a two-value image instead of a full grayscale range.
OpenCV cv2.threshold() applies one cutoff value across the whole image, while cv2.adaptiveThreshold() calculates local cutoffs from neighboring pixels. A fixed threshold is easier to tune on evenly lit images; adaptive thresholding is useful when shadows or gradients make one cutoff miss parts of the subject.
Darker subjects on a light background need an inverse binary mask so those subjects become white foreground pixels. The script prints both the foreground count and unique pixel values, which confirms the saved mask contains only black background and white foreground before another image-processing step uses it.
Related: How to blur an image with OpenCV
Related: How to apply morphology to an image with OpenCV
Related: How to find contours with OpenCV
Use a grayscale or color image. The script reads it as grayscale with cv2.IMREAD_GRAYSCALE before thresholding.
#!/usr/bin/env python3 import argparse from pathlib import Path import cv2 import numpy as np def odd_block_size(value: str) -> int: block_size = int(value) if block_size < 3 or block_size % 2 == 0: raise argparse.ArgumentTypeError("block size must be an odd integer greater than or equal to 3") return block_size parser = argparse.ArgumentParser(description="Create a binary mask from a grayscale OpenCV threshold.") parser.add_argument("input_image", type=Path) parser.add_argument("output_mask", type=Path) parser.add_argument("--method", choices=("global", "adaptive"), default="global") parser.add_argument("--threshold", type=int, default=127) parser.add_argument("--invert", action="store_true") parser.add_argument("--block-size", type=odd_block_size, default=31) parser.add_argument("--c", type=int, default=5) args = parser.parse_args() gray = cv2.imread(str(args.input_image), cv2.IMREAD_GRAYSCALE) if gray is None: raise SystemExit(f"could not read image: {args.input_image}") threshold_type = cv2.THRESH_BINARY_INV if args.invert else cv2.THRESH_BINARY if args.method == "global": used_threshold, mask = cv2.threshold(gray, args.threshold, 255, threshold_type) method_label = "global binary inverse" if args.invert else "global binary" parameter_line = f"threshold: {used_threshold:.1f}" else: mask = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, threshold_type, args.block_size, args.c, ) method_label = "adaptive gaussian inverse" if args.invert else "adaptive gaussian" parameter_line = f"block size: {args.block_size}, C: {args.c}" args.output_mask.parent.mkdir(parents=True, exist_ok=True) if not cv2.imwrite(str(args.output_mask), mask): raise SystemExit(f"could not write mask: {args.output_mask}") foreground_pixels = int(np.count_nonzero(mask == 255)) total_pixels = int(mask.size) foreground_percent = foreground_pixels / total_pixels * 100 unique_values = " ".join(str(int(value)) for value in np.unique(mask)) print(f"input: {args.input_image}") print(f"method: {method_label}") print(parameter_line) print(f"foreground pixels: {foreground_pixels} / {total_pixels} ({foreground_percent:.2f}%)") print(f"unique values: {unique_values}") print(f"output: {args.output_mask}")
$ python3 threshold_image.py input/scene.png output/scene-mask.png --threshold 180 --invert input: input/scene.png method: global binary inverse threshold: 180.0 foreground pixels: 78354 / 345600 (22.67%) unique values: 0 255 output: output/scene-mask.png
--invert makes darker input pixels white in the mask. Omit it when the subject is brighter than the background.
$ python3 threshold_image.py input/scene.png output/scene-adaptive-mask.png --method adaptive --block-size 31 --c 7 --invert input: input/scene.png method: adaptive gaussian inverse block size: 31, C: 7 foreground pixels: 25117 / 345600 (7.27%) unique values: 0 255 output: output/scene-adaptive-mask.png
--block-size must be an odd value greater than or equal to 3. Increase C when too much background becomes foreground, or lower it when parts of the subject disappear.
$ python3 -c 'import cv2, numpy as np; mask = cv2.imread("output/scene-mask.png", cv2.IMREAD_GRAYSCALE); print(mask.shape); print(np.unique(mask))'
(480, 720)
[ 0 255]
The unique values output should stay at 0 and 255 for a binary mask. A mask that is mostly white or mostly black usually needs a different threshold, inverse setting, or preprocessing step.