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.

Steps to threshold an image with OpenCV:

  1. Place the source image at input/scene.png.

    Use a grayscale or color image. The script reads it as grayscale with cv2.IMREAD_GRAYSCALE before thresholding.

  2. Create threshold_image.py.
    threshold_image.py
    #!/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}")
  3. Run a global inverse threshold.
    $ 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.

  4. Run adaptive thresholding when shadows or gradients change 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.

  5. Verify the saved mask before passing it to another OpenCV operation.
    $ 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.