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.
Related: How to convert image color spaces with OpenCV
Related: How to threshold an image with OpenCV
Related: How to find contours with OpenCV
$ mkdir -p input output
$ 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.
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.
$ 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
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.
$ 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.
$ rm mask_color.py