Nodes/ComfyUI CV/cv2.intensity_transform.autoscaling
ComfyUI Node

cv2.intensity_transform.autoscaling

Stop guessing exposure numbers, let the frame set its own range

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
cv2.intensity_transform.autoscaling
  • input
  • result

A flat, hazy frame where nothing reaches black or white looks like a bad photo, but it's really a range problem: the darkest pixel in your shot is maybe 40 and the brightest is 190, so the whole histogram sits crushed in the middle. Autoscaling fixes exactly that - it finds the range the image actually occupies and spreads it across the full 0..255.

It's one of the intensity_transform wrappers in bmad4ever/comfyui_cv, the pack that turns OpenCV into ComfyUI nodes. One input, no parameters, output comes back the same type it went in.

Mechanism

The author's own tooltip is the clearest statement of it: it "linearly stretches whatever range the image actually occupies to the full 0-255 (per image, not per channel)". So it's a single global min/max find, then a scale and offset applied to every channel with the same numbers. That last part matters - per-channel stretching would fix the contrast and wreck the colour balance, because each channel would get a different gain. One set of numbers for the whole frame keeps the hue where it was.

Two implementation details worth knowing. First, this comes from OpenCV's contrib intensity_transform module, not core - so it needs the contrib wheel. Second, the whole module returns its result by writing into a buffer the caller pre-allocates rather than returning a Mat, which is why this node has no dst input: the wrapper allocates and sizes that buffer for you, so it looks like every other filter.

Input is input - it takes a ComfyUI IMAGE, a MASK or an NPARRAY. Because it's format-preserving, an IMAGE link comes back as an IMAGE (and a MASK link as a MASK), which means the whole thing is a drop-in node in the middle of a photo pipeline with no conversion on either side. It's also batch-aware: hand it a 100-frame IMAGE batch and the pack loops it frame by frame, stretching each frame by its own range.

What it's for, and what it isn't

Good for: hazy or underexposed captures, scan-like inputs, screenshots with a washed-out look, and - the unglamorous but common one - normalizing a mask or a float-ish score map into a displayable 0..255 range before you threshold it.

Not for: creative exposure work. This is a global stretch with no curve. It cannot lift shadows without touching highlights, because the whole mapping is one straight line. If the picture is flat but well-exposed, the thing you actually want is a gamma or sigmoid curve - brightness additives flatten contrast and clip, gamma is a power curve and doesn't (post-processing.md has the long version of that argument, and it's right).

The trap: one outlier owns your range

Min/max is not robust. A single hot pixel, a specular highlight, a bit of sensor noise in a dark corner - any one of them sets the endpoint, and everything else gets compressed accordingly. You'll see it as "the stretch did nothing" (a stray bright pixel held the top of the range up) or "everything blew out" (a black speck held the bottom down).

The pack's own Wiener filter subgraphs wrestle with the same class of problem and solve it by saturating to 0..255 before stretching, with a comment explaining that the other order lets one outlier pixel crush the contrast of everything else. Same lesson: if autoscaling looks wrong, the range is being set by something you didn't intend. Crop or mask the outlier out, or use CV Array Statistic to look at percentiles instead of trusting min/max.

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 and a recent ComfyUI on the V3 node API; OpenCV behaviour is curated against 5.0.0.93. Get the contrib wheel. Installing plain opencv-python over a contrib install overwrites the shared cv2 in site-packages with a core-only build, and every contrib node - this one included - disappears silently rather than erroring. tools/repair_opencv_contrib.py --check will tell you, --apply fixes it.

The pack's README also says plainly that it's a personal, LLM-assisted project, not production software, with some code possibly overfitted to its own test cases. For a deterministic, one-function wrapper like this, judge it on your own output.

Categoryimage/CV/low-level/intensity_transform

Inputs (1)

NameTypeDefaultDescription
inputCOMFY_MATCHTYPE_V3 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.

Outputs (1)

NameTypeDescription
resultCOMFY_MATCHTYPE_V3Echoes the 'input' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY.