cv2.equalizeHist
The 90s contrast trick your face detector still wants
- src
- result
Histogram equalization is the oldest trick in the contrast toolbox and it is still doing real work in 2026 - mostly as pre-processing for classic detectors, not as a "make my photo pop" button. It is a one-input node with no knobs, which makes it either the simplest thing in your graph or a quiet way to wreck an image. Knowing which one you're doing takes two minutes.
The mechanism, briefly
It counts the gray levels, builds the cumulative distribution, and remaps every pixel through that curve. Result: intensities spread out to fill the range, so a dim, compressed image comes back with the full black-to-white spread. It is a global, monotonic, memoryless transform - every pixel with the same input value gets the same output value, no matter where it sits in the frame. That is the part people miss. Equalization has no idea that a region is flat sky and another is a face; if most of your pixels are mid-gray, it will stretch the minority into the extremes and amplify whatever noise is lurking in the flat areas. It also clips highlights hard, and there is no inverse - save the original.
In a ComfyUI graph
src is the only input, and this wrapper is on the pack's gray-required list, which is doing you a favour: wire an IMAGE in and it is converted to single-channel gray before the call, then converted back and echoed in the input's format. So an IMAGE in gives you an IMAGE out - a grayscale-looking picture - a MASK in gives you a MASK out, and a raw NPARRAY stays an NPARRAY and is left exactly as you handed it over (that's the case that will hit OpenCV's "8-bit single channel" assert if you paste in an RGBA or float array; route it through Image → CV Array with GRAY or Mask → CV Array first).
It also handles batches properly: equalization is on the pack's frame-batch-safe list, so an IMAGE batch of 8 gets equalized frame by frame rather than silently only touching frame 0. Same settings across the batch means the contrast curve differs per frame - which is either exactly what you want for normalising a set of exposures, or exactly what you don't want if you're grading a video and expect consistency.
Where it earns its place: preparing dim or unevenly lit photographs for a classic detector, normalising a batch of scanned documents before thresholding, and flattening a luminance-varying mask. That first one is not folklore. Haar cascades were trained on equalized crops, which is why this pack's own CV Cascade Detect node has an equalize_hist toggle that defaults to on - a very direct statement that global equalization is still the standard pre-processing step for a 25-year-old detector.
Where you want something else: anything colour. The curated CV Contrast (CLAHE/Equalize) node in this same pack is the friendlier door - it does CLAHE (adaptive, local, and it limits noise amplification), runs on luminance only in LAB or per RGB channel, and is batch-aware. Reach for that unless you specifically need the raw global function.
Installing it
One pack install covers every cv2.* wrapper plus the curated nodes:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
ComfyUI Manager also has it - search comfyui_cv. Python ≥ 3.12 and a recent, V3-API ComfyUI. Node path: image/CV/low-level/cv2 E.
Gotchas
Contrib matters. If something else on your machine installs plain opencv-python over the contrib wheel, the contrib submodules go quiet and nodes vanish from the menu - the pack ships tools/repair_opencv_contrib.py with --check and --apply for that.
Two more. Because this is a raw auto-generated wrapper, there is no preview, no batch toggle and no normalisation - the output is whatever OpenCV produced. And the transformation is global, so on a well-exposed photo the honest outcome is often "it looks worse, but with more contrast": harsh highlights, crunchy noise in smooth areas. If you find yourself stacking it before a colour node, you wanted CLAHE or a gamma curve instead; post-processing.md has the contrast-vs-gamma distinction, and the short version is that gamma is a power curve that leaves white alone while equalization rewrites the whole histogram.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | Source 8-bit single channel image. 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)
| 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. |