Colour masks and bitwise operations

OpenCV · Practical · 10 min

Colour segmentation and compositing (green screens, overlays, regions of interest) are built from masks.

cv2.inRange(img, lower, upper) returns a uint8 mask that is 255 where every channel lies within the inclusive bounds and 0 elsewhere. On an HSV image this is the classic colour detector:

mask = cv2.inRange(hsv, (35, 80, 60), (85, 255, 255))   # greens

For red, whose hue wraps around 0, combine two ranges with cv2.bitwise_or.

The bitwise functions work per pixel and per bit: cv2.bitwise_and, bitwise_or, bitwise_xor, bitwise_not. Their most useful form takes a mask= argument: cv2.bitwise_and(img, img, mask=mask) keeps the image where the mask is non-zero and blacks out the rest.

To composite one image onto another, cut a hole in the background with the inverted mask, keep the foreground with the mask, and add the two. cv2.addWeighted(a, alpha, b, beta, gamma) blends instead, for semi-transparent overlays such as heatmaps.

Masks must be single-channel uint8 of the same height and width as the image. A NumPy boolean mask works for indexing (img[mask > 0] = (0, 0, 255)), but cv2 functions want the uint8 form. Convert with mask.astype(np.uint8) * 255.

Example

import cv2
import numpy as np

img = np.zeros((50, 100, 3), np.uint8)
img[:, :50] = (0, 200, 0)          # green half
img[:, 50:] = (0, 0, 200)          # red half
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

green = cv2.inRange(hsv, (35, 80, 60), (85, 255, 255))
print(green.dtype, green[0, 10], green[0, 90])   # uint8 255 0

red = cv2.bitwise_or(cv2.inRange(hsv, (0, 80, 60), (10, 255, 255)),
                     cv2.inRange(hsv, (170, 80, 60), (179, 255, 255)))
print(red[0, 90])                                # 255

only_green = cv2.bitwise_and(img, img, mask=green)
print(only_green[0, 90])                         # [0 0 0]  blacked out

bg = np.full_like(img, 255)                      # white background
comp = cv2.add(cv2.bitwise_and(bg, bg, mask=cv2.bitwise_not(green)), only_green)
print(comp[0, 10], comp[0, 90])                  # [  0 200   0] [255 255 255]

overlay = cv2.addWeighted(img, 0.7, bg, 0.3, 0)
print(overlay[0, 10])                            # [ 76 216  76]

Remember

  • cv2.inRange makes a 0/255 mask from per-channel bounds
  • Combine masks with bitwise_or/and/not
  • bitwise_and(img, img, mask=m) keeps only the masked region
  • addWeighted blends two images for overlays

Watch out for

  • Passing a boolean or 3-channel mask to a cv2 function that expects single-channel uint8.

Check yourself

  1. What does `cv2.bitwise_and(img, img, mask=m)` return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.opencv.org/4.x/d0/d86/tutorial_py_image_arithmetics.html

Builds on

Unlocks

Nothing builds on this yet.