cv2.threshold
The one node that turns an image into a decision
- src
- float
- result
Everything in ComfyUI that consumes a mask needs the same thing first: a decision. Which pixels are subject, which are background. cv2.threshold is the bluntest possible way to make that decision - one number, applied to every pixel, plus a rule about what happens either side of it - and it's still the right tool more often than people expect, because it's instant, deterministic and gives you the same answer twice.
It's also the raw wrapper in this pack that replaces the curated threshold node the author removed. There's a decent threshold playground in the pack (04_threshold_playground.json) that runs this node beside OTSU, TRIANGLE, adaptive mean/Gaussian and ximgproc's Niblack/Sauvola on a photo with uneven lighting, and the canvas note is worth internalising: on that image the global methods all fail, and that isn't a knob problem. Single-threshold binarisation assumes bimodal brightness. When your image doesn't have it, no value of thresh saves you - go adaptive.
This node is one of ~470 auto-generated raw cv2.* wrappers in ComfyUI CV (bmad4ever/comfyui_cv), LLM-generated and uncurated by the author's own account.
Inputs
src takes NPARRAY, IMAGE or MASK. The important behaviour is unadvertised: an IMAGE is automatically converted to grayscale before it reaches cv2, because cv2.threshold only accepts single-channel input. An NPARRAY passes through untouched, which is your escape hatch if you genuinely want per-channel thresholding.
thresh is the cut, maxval the value assigned to pixels that pass, and type is a dropdown of OpenCV's threshold flags with one toggle per flag, pipe-joined - THRESH_BINARY (the default) plus any of THRESH_OTSU or THRESH_TRIANGLE. Piing in an automatic method means thresh is ignored and computed from the histogram instead.
Units trip up almost everyone here. An IMAGE arrives as 8-bit, so thresh and maxval live in 0..255. ComfyUI masks are 0..1 in memory, and it's very natural to type 0.5 - which thresholds at half the lowest intensity and returns an all-white image. For a mask out of a picture: thresh somewhere in the 100–200 range, maxval 255.
Outputs
Two, and both earn their place. result echoes the src's format - IMAGE in, IMAGE out, MASK in, MASK out, NPARRAY stays an ndarray. And float is the threshold value cv2 actually used, which is the whole point of the automatic methods: with THRESH_OTSU or THRESH_TRIANGLE selected, that output is the number OpenCV picked, and you can wire it somewhere useful or just look at it to see what the histogram thought.
The echo has a consequence worth planning for. Threshold an IMAGE and you get a 3-channel black-and-white IMAGE back, not a MASK - different socket type, and mask inputs won't accept it. Convert with core's Image → Mask, or with the pack's CV Array → Mask if you're already in ndarray land. Feed a MASK in and you get a MASK out with no conversion at all, which is the cleaner loop if you're binarising a soft mask.
Batch behaviour
Plain threshold is not one of the wrappers the pack loops per frame, so a batched IMAGE is read as frame 0. For a clip, split the batch with CV Unstack Batch and drive the threshold per frame, or use a batch-aware node. This is a real limitation and worth knowing before you build a 100-frame graph around it.
Installing the pack
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
cd comfyui_cv && pip install "opencv-contrib-python-headless~=5.0.0.93"
Manager → search ComfyUI CV → install → restart does the same. Requires Python ≥ 3.12 and a ComfyUI with the V3 node API; on older installs the nodes don't appear at all. The OpenCV wheel must be contrib - all four distributions share one site-packages/cv2, so installing plain opencv-python on top empties the contrib submodules and the contrib nodes vanish (tools/repair_opencv_contrib.py --check, then --apply). Behaviour is pinned to 5.0.0.93.
Where it bites
THRESH_BINARY keeps pixels above the threshold. If your mask comes out inverted - subject black, background white - that's the polarity, and THRESH_BINARY_INV is the fix, not a negative number in thresh. Typing 0..1 values. Expecting a MASK from an IMAGE input. And maxval left at its default of 0, which produces an all-black result that looks like a broken node rather than a zero-valued one.
One more honest note: this pack is new and thinly discussed - a corpus search finds essentially no community threads about it - so there's no crowd-sourced error list to lean on. OpenCV 5.0's own documentation for threshold is authoritative, and the author explicitly promises no support.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input array (multiple-channel, CV_8U, CV_16S, CV_16U, CV_32F or CV_64F). The image output(s) echo this input's format. 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. | |
| thresh | FLOAT | 0.0000-1e+38–1e+38 | threshold value. |
| maxval | FLOAT | 0.0000-1e+38–1e+38 | maximum value to use with the #THRESH_BINARY and #THRESH_BINARY_INV thresholding types. |
| type | STRING | THRESH_BINARY | thresholding type (see #ThresholdTypes). cv2.threshold flags: one of THRESH_BINARY, THRESH_BINARY_INV, THRESH_TRUNC, THRESH_TOZERO, THRESH_TOZERO_INV plus any of THRESH_OTSU, THRESH_TRIANGLE, pipe-joined (e.g. "THRESH_BINARY | THRESH_OTSU"). In the UI this renders as a dropdown with one toggle per flag. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| float | FLOAT | — |
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |