Skillversitas

Color Spaces and Conversion

Color spaces are different coordinate systems for the same light: BGR packs the sensor's channels, grayscale folds them into one luminance value, and HSV separates what color (hue) from how much (saturation) and how bright (value). Converting between them is a single call - cv2.cvtColor - and the choice of space is a real algorithmic decision: thresholding on a raw channel is brittle, while thresholding on a hue range is stable across lighting. The HSV segmentation pattern is one of the most useful tricks in the whole library.

The Conversion API

import cv2
import numpy as np

img = cv2.imread("photo.jpg")        # BGR

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)      # (h, w)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)        # (h, w, 3)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)        # for matplotlib
bgra = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)      # adds an alpha channel

# the full code list is long: BGR <-> GRAY, HSV, HLS, LAB, YCrCb, XYZ...
print(gray.shape, hsv.shape, bgra.shape)

# round trip is (nearly) lossless for BGR <-> HSV
back = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
print(np.abs(back.astype(int) - img.astype(int)).max())   # small, 0-1 rounding

The conversion codes follow the pattern COLOR_SRC2DST - COLOR_BGR2GRAY, COLOR_HSV2BGR, etc. - and the direction matters. The conversions you will actually use: to GRAY (the default first step for any edge or feature work), to HSV (color segmentation), to RGB (display boundaries), and to LAB (perceptual color differences and whitening). The channel order of HSV is (H, S, V), and - the critical detail - OpenCV's H is 0-179, not 0-360, because it packs the hue into a uint8.

Segmenting a Color in HSV

import cv2
import numpy as np

img = cv2.imread("scene.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# green: hue roughly 40-80 (OpenCV scale), with some saturation and brightness
lower = np.array([40, 40, 40])
upper = np.array([80, 255, 255])
mask = cv2.inRange(hsv, lower, upper)      # 255 where inside the range

# clean the mask, then cut the object out
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN,
                        np.ones((5, 5), np.uint8))
result = cv2.bitwise_and(img, img, mask=mask)

cv2.imwrite("green_only.jpg", result)

inRange builds a binary mask from a channel-range box - the workhorse of color segmentation. The recipe: convert to HSV, pick the hue band for the target color (find the numbers empirically by printing the HSV values of a few sample pixels), require a minimum saturation and value so grayish or near-black pixels do not match, and clean the mask with a morphological open. The result is a mask you can use for anything - cutting out the object, counting it, measuring its area.

Why Not Threshold a BGR Channel?

ApproachBreaks when
Threshold the G channel: "G > 150"lighting changes - a shaded green falls below, a bright gray passes
Require G > R and G > Bwhite and pale colors (all channels high, differences small)
HSV hue band + S/V floorsonly when the hue itself is ambiguous (green vs yellow boundary)

Hue is approximately invariant to brightness - a green object in shadow and in sunlight has the same hue, only different V. That is the entire reason HSV segmentation survives real-world lighting where BGR thresholds die. The residual weakness is hue ambiguity at color boundaries, which is what pushes serious color work toward learned classifiers or a wider color model (LAB).

The Other Spaces

  • Grayscale - BGR2GRAY is a weighted luma (Y = 0.299R + 0.587G + 0.114B), not a channel drop. It is the default input for edges, features, and classical detectors.
  • HLS - like HSV but with Lightness centered at 0.5; occasionally used for perceptual blending.
  • LAB - perceptually uniform; L is lightness, a/b are opponent-color axes. Use it for color difference metrics and for whitening (making an image's color independent of illumination).
  • YCrCb - the JPEG compression space; useful when interoperating with video codecs.

Summary

Convert with cv2.cvtColor using the COLOR_SRC2DST codes: to GRAY for classical processing, to RGB at display boundaries, to HSV for color work. Remember OpenCV's hue is 0-179 (uint8-packed, half of the 360-degree circle). Segment a color by building an HSV mask with inRange - a hue band plus saturation and value floors - cleaning it with a morphological open, and applying it with bitwise_and. The principle: threshold on hue, not on raw channel values, because hue is what survives the lighting changes that kill naive BGR thresholds.