Why it works: Unit 1.3: The image as data

Make a Color Mask with cv2.inRange in OpenCV

Turn an HSV range into a black-and-white mask with cv2.inRange, flip it with bitwise_not and apply it with bitwise_and. On the pool table one call marks 79.3% of the photo as cloth.

Luis Condados · · 3 min read Beginner
The cloth mask of the pool photo: white where the pixel's HSV falls inside the range.
The cloth mask of the pool photo: white where the pixel's HSV falls inside the range.

A mask is a black-and-white picture that marks which pixels pass a test, and cv2.inRange makes one from a colour range in a single call.

The pool photo beside its cloth mask: the cloth is white, the balls, cue, rails and room are black.
Left: the photo. Right: the mask for hue 75–100, saturation 80–255, value 40–255. White pixels passed. The balls and the cue are the black holes in it. Photo by MarkBuckawicki, CC0 [4].

Where you have met it: the chroma key filter in OBS and video editors, which is a mask on green; Select › By Color in GIMP and Select › Color Range in Photoshop.

TL;DR. One call turns the colour range from the last lesson into a map of where that colour is, and flipping the map gives everything else. On the pool photo 79.3% of the pixels fall in the cloth’s range; the rest, the “not cloth”, holds every ball but also the rails, the room and the cue.

The problem

Lesson 1 found the cloth’s range in HSV. In the project the balls are what is not cloth, so the next step is to mark every pixel as cloth or not, and then keep the others.

One range, one mask

Python · OpenCV
import cv2
import numpy as np

img = cv2.imread("pool.webp")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

lower = np.array([75, 80, 40])
upper = np.array([100, 255, 255])
cloth = cv2.inRange(hsv, lower, upper)

print(cloth.shape, cloth.dtype, np.unique(cloth))
print("cloth:", round(100 * np.mean(cloth > 0), 1), "% of pixels")
cv2.imshow("cloth", cloth)
Output
(720, 960) uint8 [  0 255]
cloth: 79.3 % of pixels
cloth
cloth
CallArgumentsWhat it does
cv2.inRange(hsv, lower, upper)an image, and the lowest and highest value allowed on each channel255 where every channel is inside its range, 0 elsewhere
np.unique(cloth)an arraythe distinct values in it: here only 0 and 255

The result has one channel, not three, and only two values. A pixel is white only if all three tests pass: hue between 75 and 100, and saturation between 80 and 255, and value between 40 and 255 [1].

Flip it, then apply it

The balls are the holes in the cloth. bitwise_not swaps black and white, and bitwise_and with mask= keeps the photo’s pixels only where a mask is white:

Python · OpenCV
not_cloth = cv2.bitwise_not(cloth)
# keep the photo's colours where the mask is white
cut = cv2.bitwise_and(img, img, mask=not_cloth)
cv2.imshow("not cloth", cut)
Output
not cloth
not cloth
CallWhat it does
cv2.bitwise_not(mask)255 becomes 0 and 0 becomes 255
cv2.bitwise_and(img, img, mask=m)img where m is white, black elsewhere

Every ball is in there. So is everything that is not the table: the rails, the pockets, the dark room and the cue. The mask answers “is this pixel cloth?”, and the room is not cloth. Lesson 4 uses the cloth mask a second time to find the table’s outline, so the room can be dropped. Before that, lesson 3 cleans up the specks.

Move the range yourself

The same six numbers, on sliders, applied to the same photo at a smaller size. Raise H min from 75 towards 85 and the mask starts to break up along the near edge, where the cloth’s hue is lowest; lower V min to 0 and patches of the dark room join the cloth, the failure lesson 5 measures.

image
mask
result

Break it

bitwise_and wants two images of the same shape. The photo has three channels and the mask one, so passing the mask as the second image, instead of as mask=, fails:

Break it
# the mask passed as the second image, not as mask=
cut = cv2.bitwise_and(img, not_cloth)
Output
Traceback (most recent call last):
  File "<exec>", line 2, in <module>
cv2.error: OpenCV(4.11.0) /home/runner/work/pyodide-recipes/pyodide-recipes/packages/opencv-python/build/opencv-python-4.11.0.86/opencv/modules/core/src/arithm.cpp:214: error: (-209:Sizes of input arguments do not match) The operation is neither 'array op array' (where arrays have the same size and type), nor 'array op scalar', nor 'scalar op array' in function 'binary_op'

OpenCV reports that the sizes of the inputs do not match. Masks go in the mask= argument.

Your turn

White has any hue, very little saturation and a high value. Make a mask called white that picks out the cue ball at x = 791, y = 311 and leaves the cloth black.

Your turn
white = cv2.inRange(hsv, (0, 0, 0), (179, 255, 255))  # narrow this
cv2.imshow("white", white)

Why this works

A mask is a binary image: every pixel is one of two values, and the two stand for “yes” and “no”. inRange asks the same yes-or-no question of every pixel, whether its three numbers fall inside a box in HSV space [1] [2]. Gonzalez and Woods treat segmentation by colour in HSI, a space close to HSV that also separates hue from brightness, in their chapter on colour [3]. The payoff section of Colour spaces: choosing axes you can threshold does the same with red, whose hue wraps around 0.

Common mistakes

  • Lower and upper swapped. inRange does not sort them. A lower bound above the upper bound matches nothing, with no error.
  • Red in one range. Red sits near both 0 and 179. Make two masks and join them with cv2.bitwise_or.
  • The mask as an image argument. Pass it as mask=, as the Break it cell shows. It must be a single-channel uint8 array the same height and width as the image.
  • Expecting “not cloth” to mean “balls”. Inverting a mask selects everything else, and in a photo that includes the background.

References

[1] OpenCV. Thresholding Operations using inRange (tutorial, doc/tutorials/imgproc/threshold_inRange/threshold_inRange.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[2] OpenCV. Changing Colorspaces (Python tutorial, doc/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[3] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 7.7 “Using Color in Image Segmentation”, Segmentation in HSI Color Space, p. 576. Pearson.

[4] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org