Color-based masking isolates pixels whose color falls inside a chosen range so later OpenCV steps can ignore the rest of the image. It is useful before contour detection, object tracking, measurement, or cutout creation when the target has a recognizable color.

OpenCV reads normal color images as BGR arrays, so the image is converted to HSV before thresholding. HSV keeps hue separate from saturation and brightness, which makes the range easier to tune than raw blue, green, and red channel limits.

A file-based Python run masks the blue rectangle in input/scene.png with cv.inRange(), applies that binary mask with cv.bitwise_and(), and writes both the mask and a visual preview. Adjust the lower and upper HSV values for another target color, then verify that the selected-pixel count and preview match the object before passing the mask into contour or tracking code.

Steps to mask an image by color with OpenCV:

  1. Create input and output directories.
    $ mkdir -p input output
  2. Copy the source image into the input directory.
    $ cp scene.png input/scene.png

    Replace scene.png with the image to mask. Keep the input/scene.png path or update the run command to point at the project image.

  3. Create the color-mask script.
    mask_color.py
    from argparse import ArgumentParser, ArgumentTypeError
    from pathlib import Path
     
    import cv2 as cv
    import numpy as np
     
     
    def parse_hsv(value):
        parts = value.split(",")
        if len(parts) != 3:
            raise ArgumentTypeError("HSV values must use H,S,V format")
     
        try:
            hue, saturation, brightness = [int(part) for part in parts]
        except ValueError as exc:
            raise ArgumentTypeError("HSV values must be integers") from exc
     
        if not 0 <= hue <= 179:
            raise ArgumentTypeError("Hue must be between 0 and 179 for 8-bit OpenCV HSV images")
        if not 0 <= saturation <= 255 or not 0 <= brightness <= 255:
            raise ArgumentTypeError("Saturation and value must be between 0 and 255")
     
        return np.array([hue, saturation, brightness], dtype=np.uint8)
     
     
    def write_image(path, image):
        if not cv.imwrite(str(path), image):
            raise SystemExit(f"could not write image: {path}")
     
     
    def labeled_tile(tile, label):
        tile = cv.resize(tile, (360, 240), interpolation=cv.INTER_AREA)
        cv.rectangle(tile, (0, 0), (360, 34), (0, 0, 0), thickness=-1)
        cv.putText(
            tile,
            label,
            (12, 24),
            cv.FONT_HERSHEY_SIMPLEX,
            0.7,
            (255, 255, 255),
            2,
            cv.LINE_AA,
        )
        return tile
     
     
    parser = ArgumentParser(description="Mask an image by HSV color range with OpenCV.")
    parser.add_argument("input_image", type=Path)
    parser.add_argument("--output-dir", type=Path, default=Path("output"))
    parser.add_argument("--lower-hsv", type=parse_hsv, default=parse_hsv("100,80,40"))
    parser.add_argument("--upper-hsv", type=parse_hsv, default=parse_hsv("130,255,255"))
    args = parser.parse_args()
     
    image = cv.imread(str(args.input_image), cv.IMREAD_COLOR)
    if image is None:
        raise SystemExit(f"could not read image: {args.input_image}")
     
    hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV)
    mask = cv.inRange(hsv, args.lower_hsv, args.upper_hsv)
    masked = cv.bitwise_and(image, image, mask=mask)
     
    args.output_dir.mkdir(parents=True, exist_ok=True)
    mask_path = args.output_dir / "blue-mask.png"
    masked_path = args.output_dir / "blue-cutout.png"
    preview_path = args.output_dir / "color-mask-preview.png"
     
    mask_preview = cv.cvtColor(mask, cv.COLOR_GRAY2BGR)
    preview = cv.vconcat(
        [
            cv.hconcat(
                [
                    labeled_tile(image.copy(), "BGR input"),
                    labeled_tile(mask_preview, "HSV mask"),
                ]
            ),
            cv.hconcat(
                [
                    labeled_tile(masked.copy(), "Masked result"),
                    labeled_tile(cv.cvtColor(hsv[:, :, 0], cv.COLOR_GRAY2BGR), "Hue channel"),
                ]
            ),
        ]
    )
     
    write_image(mask_path, mask)
    write_image(masked_path, masked)
    write_image(preview_path, preview)
     
    selected_pixels = int(cv.countNonZero(mask))
    total_pixels = int(mask.size)
    coverage = selected_pixels / total_pixels * 100
    unique_values = " ".join(str(int(value)) for value in np.unique(mask))
     
    print(f"input: {args.input_image}")
    print(f"lower HSV: {args.lower_hsv.tolist()}")
    print(f"upper HSV: {args.upper_hsv.tolist()}")
    print(f"selected pixels: {selected_pixels} / {total_pixels} ({coverage:.2f}%)")
    print(f"mask values: {unique_values}")
    print(f"wrote: {mask_path}")
    print(f"wrote: {masked_path}")
    print(f"wrote: {preview_path}")

    The default range selects a saturated blue target in an 8-bit OpenCV HSV image. For another object, set --lower-hsv and --upper-hsv with H,S,V values where hue uses 0-179 and saturation/value use 0-255.

  4. Run the script with the blue HSV range.
    $ python3 mask_color.py input/scene.png --lower-hsv 100,80,40 --upper-hsv 130,255,255
    input: input/scene.png
    lower HSV: [100, 80, 40]
    upper HSV: [130, 255, 255]
    selected pixels: 41800 / 345600 (12.09%)
    mask values: 0 255
    wrote: output/blue-mask.png
    wrote: output/blue-cutout.png
    wrote: output/color-mask-preview.png
  5. Review the preview image.

    The HSV mask tile should show the selected target in white and the rejected background in black. The Masked result tile should keep only the blue object from the original image.

  6. Verify the saved mask and cutout.
    $ python3 - <<'PY'
    import cv2 as cv
    import numpy as np
    
    mask = cv.imread("output/blue-mask.png", cv.IMREAD_GRAYSCALE)
    cutout = cv.imread("output/blue-cutout.png", cv.IMREAD_COLOR)
    
    if mask is None:
        raise SystemExit("could not read output/blue-mask.png")
    if cutout is None:
        raise SystemExit("could not read output/blue-cutout.png")
    
    print(f"mask shape: {mask.shape}")
    print(f"mask values: {np.unique(mask).tolist()}")
    print(f"selected pixels: {cv.countNonZero(mask)}")
    print(f"cutout shape: {cutout.shape}")
    PY
    mask shape: (480, 720)
    mask values: [0, 255]
    selected pixels: 41800
    cutout shape: (480, 720, 3)

    A two-value mask confirms cv.inRange() produced a binary selection. A selected-pixel count of 0 means the HSV range does not include the target color.

  7. Remove the sample script after adapting the masking code.
    $ rm mask_color.py