CLAHE
The fix for uneven lighting before you even think about thresholding
- src
- IMAGE
If you've ever tried to binarize a photo of a document with a lamp shining on one side of it, you already know the problem CLAHE solves. A global threshold picks one cutoff for the whole image, so the bright half reads as "white" and the shadowed half reads as "black," and your mask comes out wrong no matter what. CLAHE - Contrast Limited Adaptive Histogram Equalization - fixes contrast locally, per small tile of the image, instead of globally. It's the standard preprocessing step before thresholding or edge detection on anything photographed rather than rendered.
The name is a mouthful, but the idea is simple: divide the image into a grid of tiles, equalize the histogram of each tile so local contrast gets a boost, then blend the tile boundaries so you don't get ugly seams. The "limited" part is what keeps it sane - contrast gain is capped per tile, which stops CLAHE from amplifying noise the way plain histogram equalization does. That's the whole reason people reach for CLAHE over the equalize-everything option.
How it works here
The node wraps OpenCV's cv.createCLAHE. Mechanically it's worth knowing three things:
- It processes the image as single-channel grayscale - that's what CLAHE operates on - and hands the result back as an
IMAGE. The output is RGB (the gray values copied into all three channels), which keeps every downstream Bmad node happy. clip_limit(default 2) is the contrast ceiling per tile. Raise it for a punchier result, but the noise will find you eventually.tile_grid_x/tile_grid_y(default 8 each, minimum 2) set the grid density. Smaller tiles = more local adaptation; bigger tiles = closer to global equalization.
src is the only other input, and the output IMAGE is what you route onward.
Where it fits
The natural workflow: photograph or screenshot → CLAHE → AdaptiveThresholding (or the pack's OtsuThreshold) → mask. CLAHE evens the lighting field so the threshold step sees a flat, high-contrast image instead of a gradient. It's also handy as a detail pass before inpainting or before feeding a low-contrast image into feature detection - same trick photo editors call "local contrast."
Install
This is part of bmad4ever/comfyui_bmad_nodes, the catch-all utility pack from bmad4ever. Install via ComfyUI Manager (search comfyui_bmad_nodes) or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_bmad_nodes
cd comfyui_bmad_nodes
pip install -r requirements.txt
Restart after. The pack's requirements.txt pins opencv-python~=4.8.1.78 plus scikit-image and gray2color, so Manager's dependency install will pull those. No models, no downloads beyond that.
Gotchas
Two things bite people. First, tile_grid_x / tile_grid_y have a minimum of 2, and OpenCV itself will throw if you go below that - the UI enforces it, but if you're building workflows programmatically, respect it. Second, remember the output is grayscale-as-RGB. If you chain this into a node that needs a real single-channel mask (some ComfyUI-native nodes expect 1-channel tensors), you may need a channel-split first. For the pack's own masking chain it's seamless, which is probably where you should stay.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| src | IMAGE | — | |
| clip_limit | INT | 2 | — |
| tile_grid_x | INT | 8 | — |
| tile_grid_y | INT | 8 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |