cv2.convertScaleAbs
The node that makes float edge maps visible (and why it's wrong sometimes)
- src
- result
What it's for
You ran a Sobel. You subtracted one image from another. You have a float array with negative values in it, and it is unusable as an image - ComfyUI's IMAGE type is uint8, OpenCV's display paths assume 0–255, and negative numbers wrap or clip into nonsense. cv2.convertScaleAbs is the standard fix, and it's the one you'll reach for most days:
out = |src × alpha + beta|, cast to uint8
Three things in one line - a gain (alpha), a bias (beta), and the absolute value with saturating cast to 8-bit. That last part is why it's the canonical way to look at a signed gradient map: a Sobel response of -80 and +80 are both an edge, and the absolute value says so.
It's a raw wrapper from ComfyUI CV (bmad4ever/comfyui_cv), category image/CV/low-level/cv2 C.
But - the absolute value is a loaded gun
Here's the take, and it's the thing worth remembering about this node. Abs is right for magnitude data and wrong for anything else. If negative values mean "this pixel got darker", folding them back to positive destroys the information and produces an image that looks plausible and is a lie.
The pack's own docs put the case perfectly, and they're talking about neural style transfer output where a model runs well outside [0, 255] on both sides: model output "round[s] and clamp[s] to 0-255 without an absolute value - convertScaleAbs would reflect negative pixels back to bright and corrupt them." So the pack deliberately uses plain |-free clipping for DNN output and reserves convertScaleAbs for cases where magnitude genuinely is the signal.
Rule of thumb:
- Gradient / edge / difference magnitude →
convertScaleAbsis exactly right. - Anything where the sign is meaningful - an HDR-ish stack, an unbounded model output, a local-linear-fit slope from the pack's
CV Local Linear Fit→ use CV Cast Array with clipping instead, and pick your range deliberately.
Inputs and outputs that matter
- src - required. An IMAGE, MASK or NPARRAY; the output echoes the input's format, so an IMAGE link comes back as an IMAGE you can preview or save directly. The wrapper's batch machinery treats this as per-frame safe, so a batched IMAGE is processed frame by frame rather than only at frame 0.
- alpha - optional FLOAT, default
1.0. The gain. This is where you normalise a float map that lives in, say, 0–4.0 up to the 0–255 range. Preset to OpenCV's own default, so leaving it alone is safe. - beta - optional FLOAT, default
0.0. Additive offset, applied before the abs. Use it to centre a symmetric response before folding. - result - the 8-bit output, in the same format as
src.
Both optionals are advanced inputs, collapsed until you show them.
Where it fits
It's the invisible step in almost every classic CV visualisation, and in this pack it's the natural tail end of cv2.absdiff, cv2.subtract, cv2.Sobel, cv2.Laplacian, cv2.Canny's gradient inputs, and the pack's optical-flow and photometric-alignment nodes. It's also the standard "did anything change?" inspection: absdiff two frames, convertScaleAbs, look. For exposure/gain work specifically, post-processing.md argues the general case for the deterministic layer - a linear scale is a millisecond and a lookup table, and re-sampling a diffusion pass to do it is the anti-pattern.
Installing the pack
Manager → search ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart. Requirements: Python ≥ 3.12 and a V3 node API ComfyUI; dependency opencv-contrib-python-headless~=5.0.0.93. That OpenCV build is pinned on purpose - behaviour is curated against it.
Where people get burned
- The abs, as covered above. The most consequential thing about this node, and it's silent.
- An all-black or all-white result. Your data is out of the 0–255 window and
alphais still 1.0. Edge maps from a 0–1 float input, for instance, needalphaaround 255. Look at the actual min/max with CV Array Statistic instead of guessing. - Clamping, not normalising.
convertScaleAbsclamps. If your top value is 1200, everything above 255 is white mush. Either scale it down withalphafirst, or normalise upstream (cv2.normalize, or CV Cast Array with a scaling mode). - INT overflow avoided, not explained. The saturating cast is what stops
-80turning into176via wraparound. That's a feature; just don't expect the saturation to tell you it happened. - Pack caveats - the short version, once. Heavy LLM assistance is declared in the README, along with an acknowledged overfitting risk, a "not recommended in production" note, no planned updates, and essentially zero community footprint (a Reddit search for the pack returns nothing). Also relevant here: some OpenCV functions need 3-channel input, and the pack guards only a specific list of those - if a node in the pipeline chokes on a single-channel array, that's usually why.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input array. 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. | |
| alphaopt | FLOAT | 1.0000-1e+38–1e+38 | optional scale factor. Preset to the OpenCV default (1.0). |
| betaopt | FLOAT | 0.0000-1e+38–1e+38 | optional delta added to the scaled values. Preset to the OpenCV default (0.0). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |