What range does the hue channel use after cv2.cvtColor(img, cv2.COLOR_BGR2HSV)?
answer
- hue is an angle, but it must fit in a byte
- not the 0-360 your colour picker shows
- halved for 8-bit images
- S and V still use the full 0-255
- red straddles the seam, so two masks
basics
~20 sFor an 8-bit image, OpenCV stores hue as 0-179, halving the usual 0-360 degrees so it fits in a byte; saturation and value use 0-255. Copying hue values from a colour picker that reports 0-360 selects the wrong colour band.
solid answer
~50 sOpenCV has to fit hue into a `uint8`, so for 8-bit input `cv2.COLOR_BGR2HSV` divides degrees by two: H lands in **0-179**, while S and V occupy the full 0-255. That single fact breaks most first attempts at colour thresholding, because every colour picker and design tool reports hue in 0-360 — a value of 240 for blue means 120 in OpenCV. If you convert the image to `float32` in the 0-1 range first, hue comes back as real degrees, 0-360, with S and V in 0-1. `cv2.COLOR_BGR2HSV_FULL` is the third option: it maps hue onto the full 0-255 byte range instead of halving it. The second trap is that hue is circular and red sits at the seam, so a red mask needs two `cv2.inRange` calls — roughly 0-10 and 170-179 — combined with `cv2.bitwise_or`.
code
python · 12 linesimport cv2
bgr = cv2.imread("scene.jpg")
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) # H: 0-179, S/V: 0-255
# green band: one range is enough
green = cv2.inRange(hsv, (35, 80, 60), (85, 255, 255))
# red straddles the 0/179 seam, so it needs two
low = cv2.inRange(hsv, (0, 100, 60), (10, 255, 255))
high = cv2.inRange(hsv, (170, 100, 60), (179, 255, 255))
red = cv2.bitwise_or(low, high)go deeper
Remember the number: for 8-bit images OpenCV's hue runs 0-179, not 0-360, while saturation and value run 0-255. Halving a hue you read from a colour picker is the fix for a mask that selects nothing.
Explain why hue is halved (it must fit in a uint8), how the float32 path gives 0-360 instead, and why red needs two inRange masks combined with bitwise_or. Know that saturation and value floors are what keep gray pixels out.
Demonstrate deriving thresholds from sampled pixels rather than guessing, and diagnosing a mask that fails under new lighting by looking at where V moved. Be ready to say when fixed HSV thresholds have stopped being the right tool.
Own the tradeoff between a cheap, explainable, hand-tuned colour rule and a learned segmentation model: HSV thresholds are debuggable and cost microseconds but are brittle to illumination, so the question is what lighting control you can guarantee and what the failure costs.
## Why HSV at all In BGR, the identity of a colour and its brightness are entangled across all three channels: a red shirt in shadow and the same shirt in sunlight have completely different BGR triples. HSV separates them. **Hue** is the position on the colour wheel, **saturation** is how vivid the colour is, and **value** is how bright. Shading mostly moves V, leaving H roughly stable, which is why colour-based selection is done in HSV rather than by writing per-channel BGR ranges. ## The ranges, precisely For an **8-bit** (`uint8`) input image, `cv2.cvtColor(img, cv2.COLOR_BGR2HSV)` produces: - H in **0-179** — degrees divided by two, so the whole 360-degree wheel fits in a byte - S in **0-255** - V in **0-255** For a **32-bit float** input scaled to 0-1, the same conversion produces: - H in **0-360** — actual degrees - S in **0-1** - V in **0-1** And `cv2.COLOR_BGR2HSV_FULL` on 8-bit input spreads hue across **0-255** instead of halving it, which buys back a little resolution at the cost of yet another convention to remember. The practical consequence: a hue value you read anywhere else in the world is in degrees, and OpenCV's 8-bit hue is half of it. Blue at 240 degrees is 120 in OpenCV. Green at 120 degrees is 60. If a threshold silently selects nothing, this is the first thing to check. ## Building a colour mask ``` hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (35, 80, 60), (85, 255, 255)) # greens ``` `cv2.inRange` returns a `uint8` single-channel mask with 255 inside the bounds and 0 outside, testing all three channels simultaneously. Two details matter as much as the hue numbers themselves: - **Set a saturation floor.** Near-gray pixels have almost no chroma, so their hue is numerically meaningless and jitters wildly with sensor noise. A lower bound of roughly 60-100 on S removes most false positives that a hue-only threshold would collect from walls, shadows and highlights. - **Set a value floor.** Very dark pixels have unstable hue for the same reason, and near-white pixels are unsaturated rather than coloured. ## The red wrap-around Hue is an angle, so 179 is adjacent to 0 — and red sits exactly on that seam. A single range like `(0, ...)` to `(10, ...)` catches only half of red, and the intuitive fix of writing a low bound above the high bound produces an empty mask, because `inRange` compares component-wise and never wraps. The correct approach is two masks combined: ``` low = cv2.inRange(hsv, (0, 100, 60), (10, 255, 255)) high = cv2.inRange(hsv, (170, 100, 60), (179, 255, 255)) mask = cv2.bitwise_or(low, high) ``` This is the single most-asked HSV follow-up, because it is the only colour on the wheel that requires it and it is the colour people most often want to detect (stop signs, brake lights, safety vests). ## Picking the numbers honestly Do not guess bounds from a colour name. Sample real pixels: crop a patch of the target colour from a representative frame, convert it, and look at the actual H/S/V distribution — the percentiles tell you the bounds. Bounds derived from one image under one lighting condition are the classic reason a demo works in the office and fails on site. ## When HSV is the wrong tool HSV is convenient, not perceptual: equal numeric distances in hue do not correspond to equal perceived colour differences, and V is a crude brightness proxy. If the task is measuring how *different* two colours look, or manipulating lightness while leaving colour identity alone, a perceptually-oriented space is the better choice. HSV's strength is exactly one thing — carving out a band of the colour wheel with cheap integer thresholds — and it does that better than anything else at this price. ## Converting back `cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)` returns to BGR. Round-tripping 8-bit data is lossy because of the hue halving and the integer quantisation, so do not treat HSV as a lossless intermediate for pixel-exact work. And remember the input has to be BGR, not RGB — feeding an RGB array to `COLOR_BGR2HSV` produces a plausible-looking HSV image with hues rotated to nonsense.
- Why does a single cv2.inRange call fail to select red?Because hue is circular and red sits at the wrap point: part of it lies just above 0 and part just below 179. inRange compares component-wise and never wraps, so a range with a low bound above the high bound returns an empty mask. Build two masks — roughly 0-10 and 170-179 — and combine them with cv2.bitwise_or.
- What changes if you convert the image to float32 before calling cvtColor?With float32 input scaled to 0-1, hue comes back in actual degrees, 0-360, and saturation and value in 0-1. That removes the halving surprise and the quantisation, at the cost of four times the memory and slower integer-free processing. cv2.COLOR_BGR2HSV_FULL is the middle option: 8-bit data with hue spread over 0-255.
- Why put a floor on saturation in a hue-based mask?Because hue is undefined for gray pixels and numerically unstable for nearly-gray ones — a white wall or a deep shadow can report any hue at all, and sensor noise makes it flicker frame to frame. A saturation floor around 60-100 discards those pixels before their meaningless hue can pollute the mask.
- Your HSV thresholds work indoors and fail outdoors. What do you do?Re-derive the bounds from real pixels under the new lighting rather than nudging numbers by hand: sample patches of the target colour and use the observed H/S/V percentiles. Widen the value range far more than the hue range, since illumination mostly moves V. If the lighting varies continuously, fixed thresholds are the wrong tool and a learned or adaptive segmentation is warranted.
saying these in an interview costs you the question
- Using 0-360 hue values straight from a colour picker
- Writing one inRange range for red and wondering why it misses
- Thresholding hue with no saturation or value floor
- Assuming H, S and V all share the same 0-255 range
- Feeding an RGB array to cv2.COLOR_BGR2HSV