Skip to main content...
Python + Classical CV — the Catalog Tool
25 min

Day 8: OpenCV I: images as arrays; color spaces (RGB vs HSV)

HSV separates a pixel's color from its brightness, so 'find everything blue' survives shadows and highlights in a way RGB thresholding can't.

Where Day 3's array model meets a real photo

OpenCV (cv2) is a NumPy user, not a replacement for it — every image you load is an ndarray, and everything from Days 3-4 (shape, dtype, vectorization, broadcasting) applies directly. Today starts the four-day OpenCV run that ends in the Catalog Tool.

The gotcha that will bite you at least once

OpenCV loads images with channels in BGR order (Blue-Green-Red), not the RGB order every other library (Matplotlib, PyTorch, PIL) expects. Forget to convert and every color-based operation is subtly wrong — most commonly, a photo displays or processes with red and blue swapped.

Loading and the BGR/RGB conversion
import cv2

img_bgr = cv2.imread("garment.jpg")      # shape (H, W, 3), BGR order
img_bgr.shape, img_bgr.dtype              # e.g. (1024, 768, 3), uint8

img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)   # for display / other libraries

RGB vs HSV

RGB encodes a pixel as how much red, green, and blue light it has — intuitive to build, but color and brightness are tangled together across all three channels. Dim the lights on a red shirt and *all three* RGB values drop, so 'is this pixel red?' becomes a moving target. HSV (Hue, Saturation, Value) separates them explicitly: Hue is the actual color (as an angle on a color wheel, independent of lighting), Saturation is how vivid vs washed-out it is, and Value is brightness. Filtering 'is this pixel red' becomes a check on Hue alone — robust to shadows, highlights, and lighting changes across a whole catalog shoot.

Converting to HSV and thresholding on hue alone
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

# "red" hues wrap around 0/180 in OpenCV's 0-180 hue range — two ranges needed
lower_red1, upper_red1 = (0, 70, 50), (10, 255, 255)
lower_red2, upper_red2 = (170, 70, 50), (180, 255, 255)
mask1 = cv2.inRange(img_hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(img_hsv, lower_red2, upper_red2)
red_mask = cv2.bitwise_or(mask1, mask2)   # a boolean-style mask — Day 4's idea again

Key terms

BGR
OpenCV's default channel order for loaded images — Blue, Green, Red — the reverse of the RGB every other library expects.
HSV
Hue-Saturation-Value color space that separates color identity (Hue) from lighting/brightness (Value).
inRange
OpenCV's function for producing a binary mask of pixels falling within given per-channel bounds.

Stage 0 exit criterion: you should now be able to

You load an image with cv2.imread and pass it straight to plt.imshow without converting it first. What happens?

We use cookies

We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies. Learn more

    Day 8: OpenCV I: images as arrays; color spaces (RGB vs HSV) | RBTechIconX