cv2.inRange
Turn 'keep pixels in this range' into a mask — and the hue-wrap trap that comes with it
- src
- lowerb
- upperb
- nparray
Selecting by colour is one of the oldest tricks in computer vision, and cv2.inRange is its one-liner: give it a lower bound and an upper bound, get back a binary mask of the pixels that fall inside. Green screens, skin tones, a specific shade of teal in a sky, the red pixels of a QR-ish marker - all of it is this function.
This is the raw wrapper from bmad4ever/comfyui_cv. It works, and for plenty of jobs it's exactly the right node. But the pack ships two curated alternatives that solve a problem this raw version can't, so read the trap section before you commit.
Mechanism
Per channel, inclusive bounds: a pixel survives if lowerb[c] <= src[c] <= upperb[c] on every channel. Output is a two-valued mask, 0 or 255 - OpenCV's binary image convention, not a 0..1 float mask.
src, lowerb and upperb all take an NPARRAY, IMAGE or MASK. That's a deliberate widening by the pack: instead of typing three literals, you can feed bounds from a CV Scalar node, or take them from CV Color Range From Sample, which measures the centre and spread of a scribbled region and hands you back the numbers. Bounds can also be a single value that broadcasts, which is what you want for a grayscale threshold band.
The output is nparray - NPARRAY, and it stays NPARRAY on purpose. A 3-channel image in gives a 1-channel binary out, which a socket type can't express; the pack's comment on this explains that handing a three-channel white picture to a mask consumer would be worse than making you convert deliberately. So you bridge it: CV Array → Mask turns it into a real ComfyUI MASK you can feed to anything mask-shaped, or keep it as an array and chain more cv2 ops on it (bitwise ops, morphology, cv2_findContours).
The trap: hue is a circle
Convert to HSV first - cv2_cvtColor with COLOR_BGR2HSV - because "the greens" as BGR triplets is nobody's idea of fun. But OpenCV's HSV hue runs 0..179, and it wraps. Red sits at both ends: 0-ish and 175-ish. A single inRange band centred on red either misses half of it or you build two bands and OR them with cv2_bitwise_or.
That's exactly what CV Color Range in this pack exists for. It's circular-aware: you tell it which channel is a hue and its period, and the band wraps, so a hue centred on 0 also keeps values near the top of the range in one pass. It also takes a centre plus a per-channel tolerance rather than you working out lower/upper by hand, and a single tolerance value broadcasts. There's a CV Color Range From Sample companion that learns the band from the image itself - sample the pixels (optionally inside a mask), and out come center and tolerance you can feed into CV Color Range to reuse on another frame.
Short version: use this raw node when your threshold is a simple non-circular band and you want to see the numbers. Use CV Color Range the moment hue is involved.
Installing the pack
Manager → search comfyui_cv, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Python ≥ 3.12, recent ComfyUI (V3 node API). inRange is core OpenCV so it doesn't need the contrib wheel - but the pack as a whole does, and installing a non-contrib opencv-python on top of a contrib one silently empties the contrib submodules, which makes contrib nodes vanish with no error at all. tools/repair_opencv_contrib.py --check diagnoses; --apply fixes.
Practical notes
Range, not value. inRange on an 8-bit image works in 0..255 per channel. If your NPARRAY is float 0..1, your bounds need to match it. Dtype mismatches are the classic silent failure here - a mask that comes back all black.
Bounds are inclusive on both ends. If two adjacent bands are supposed to tile a range without overlap, mind the shared endpoint.
Preview before you trust. Preview CV Array shows the mask as imagery; a quick look catches the inverted band (pixels you wanted, everything else white) far faster than debugging downstream.
The pack's own README is upfront that it's a personal, heavily LLM-assisted project, not production software. For a threshold-and-mask step in a personal workflow that's an acceptable trade; just verify the mask on your own footage, which you were going to do anyway.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| src | NPARRAY,IMAGE,MASK | first input array. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| lowerb | NPARRAY,IMAGE,MASK | inclusive lower boundary array or a scalar. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| upperb | NPARRAY,IMAGE,MASK | inclusive upper boundary array or a scalar. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| nparray | NPARRAY | — |