Nodes/BrainDead Nodes/BD Normalize Luma
ComfyUI Node

BD Normalize Luma

Fix your dynamic range before the mask sees it — BD Normalize Luma

By BizaNator·Created 8 months ago·Updated 3 days ago· 15
BD Normalize Luma
  • image
  • mask
  • image
  • found_min
  • found_max
luma_standardbt709
target_max0.95
target_min0.00
clip_percent_high1.0
clip_percent_low1.0
preserve_colortrue
proportional_scalefalse
apply_to_mask_onlytrue
luma_apply_min0.00
luma_apply_max1.00
luma_apply_feather0.15

Every luminance-based tool in ComfyUI has the same quiet failure: it computes a threshold from your image's actual min and max, and if one blown-out highlight or one dead-black corner defines those extremes, the entire range collapses and the mask comes out garbage. BD Normalize Luma is the pre-process that fixes exactly that - it rescales an image's luminance to a target range using percentile clipping, so a few outlier pixels don't get to set the goalposts.

The core question it answers is simple: "what's the darkest 1% and brightest 1% of this image, ignoring the spikes?" From those robust bounds it remaps the image so the real content spans your chosen target_min (default 0) to target_max (default 0.95). clip_percent_low and clip_percent_high (both default 1) control how much of the extreme tail gets ignored; 0 means "use absolute min/max," which is the thing that was broken to begin with.

The two scaling modes

This is where the node stops being trivial and starts being genuinely useful:

  • Range fit (default, proportional_scale off) - remaps source [min, max] to target [min, max]. Stretches both ends, lifts or drops the black floor. Good when you want full dynamic range out.
  • Proportional (proportional_scale on) - a single multiplier so the brightest non-outlier pixel lands exactly on target_max, with a hard clamp above it. Input zero stays zero, shadows don't lift. This is your "dim the whole thing without touching blacks" mode - the tooltip's example is shifting the brightest part down to mid-grey with target_max=0.5 and guaranteeing nothing exceeds it, even saturated pixels.

You also get a mask input that does double duty: it restricts the percentile calculation to a region (e.g. skin only), and by default (apply_to_mask_only on) restricts the application too, so pixels outside the mask pass through untouched. That's the "balance the skin tones without touching the background" trick.

The tone-band controls

The optional luma_apply_min / luma_apply_max / luma_apply_feather trio lets you target a tone band instead of the whole image - highlights only (0.61.0), shadows only (0.00.5), midtones only (0.30.7). The feather softens both edges so you don't get a hard transition line. The blend is computed from the original luma, which is the detail that prevents feedback loops. preserve_color (on by default) rescales RGB proportionally; turn it off and you get a grayscale using the normalized luma.

Outputs: image, plus found_min and found_max - the source luma values that mapped to your targets. Those two floats are great for diagnostics; the README's BD Luma Stats node produces the companion numbers (like the recommended outer band) for wiring into shader uniforms.

Install

In ComfyUI-BrainDead: Manager search "BrainDead", or clone + pip install -r requirements.txt, restart. V3-API pack, so update ComfyUI first if needed.

The classic mistake is turning this into a fishing expedition - if you normalize the whole image when you only needed the mask region normalized, or crank target_max to 1.0 and clip highlights you wanted to keep, you've traded one artifact for another. Start with defaults (1% clip, target_max 0.95), wire the mask if you have one, and only then reach for tone-band targeting. The typical payoff: a too-dark character render whose BD Luminance Mask came out as a black blob suddenly produces a usable mask after a single normalize pass.

Category🧠BrainDead/Segmentation

Inputs (13)

NameTypeDefaultDescription
imageIMAGESource image to normalize.
luma_standardCOMBObt709RGB → luminance weighting: bt709 (default): 0.2126 R + 0.7152 G + 0.0722 B — modern sRGB/Rec.709 standard. Use for Unity/Unreal, AI pipelines, modern display work. bt601: 0.299 R + 0.587 G + 0.114 B — legacy NTSC weights, matches Photoshop 'Desaturate' and older tools. average: (R+G+B)/3 — simple, not perceptual.
target_maxFLOAT0.950–1Brightest output value. Set lower (e.g. 0.85) to compress an over-bright image so there's headroom.
target_minFLOAT0.000–1Darkest output value. Set higher (e.g. 0.1) to lift shadows in an under-exposed image.
clip_percent_highFLOAT1.00–50Treat the top X% of valid pixels as the source-max. Higher values ignore more outliers. 1.0 = robust default. 0 = use absolute max (sensitive to single-pixel spikes).
clip_percent_lowFLOAT1.00–50Treat the bottom X% of valid pixels as the source-min. Higher values ignore more outliers.
preserve_colorBOOLEANtrueON: rescale RGB proportionally (keeps color, just darkens/brightens). OFF: convert to greyscale using the normalized luma.
proportional_scaleBOOLEANfalseOFF (default): RANGE FIT — remaps source [min, max] to target [min, max]. Stretches/compresses both ends, lifts or drops the black floor. ON: PROPORTIONAL — single multiplier (target_max / src_max), then HARD CLAMP at target_max. No pixel can exceed target_max. Brightest non-outlier pixel hits exactly target_max, outlier pixels (top clip_percent_high%) are clamped down to target_max. Everything below scales by the same factor, input=0 stays at 0. target_min is IGNORED. Use when you want to dim the whole image without lifting shadows — e.g. shift brightest part down to mid-grey (target_max=0.5) — even saturated pixels are guaranteed ≤ 0.5.
apply_to_mask_onlyBOOLEANtrueON (default): only pixels WITHIN the mask are normalized — pixels outside the mask pass through UNCHANGED from the source. OFF: normalization is applied to the whole image, even outside the mask area (mask still controls the percentile calc either way). If no mask is wired, this setting has no effect.
maskoptMASKOptional region mask. Percentile calc only considers pixels where mask > 0.5. By default (apply_to_mask_only=ON), the normalization is also only applied within this region.
luma_apply_minoptFLOAT0.000–1Tone floor — normalization fades out BELOW this luminance. 0.0 = apply to all tones (default, existing behaviour). 0.5 = only pixels at or above mid-grey receive the full normalized result; darker pixels fade back to original. Use this to bring down highlights without touching shadows. Combined with luma_apply_max to target a tone band: luma_apply_min=0.6, luma_apply_max=1.0 → highlights only luma_apply_min=0.0, luma_apply_max=0.5 → shadows only luma_apply_min=0.3, luma_apply_max=0.7 → midtones only
luma_apply_maxoptFLOAT1.000–1Tone ceiling — normalization fades out above this luminance. 1.0 = apply to all tones (default, existing behaviour). 0.5 = only pixels at or below mid-grey receive the full normalized result; brighter pixels fade back to original. Use this to lift shadows without touching highlights.
luma_apply_featheroptFLOAT0.150–1Soft transition width applied to BOTH the floor (luma_apply_min) and ceiling (luma_apply_max) edges. 0 = hard cutoff. 0.15 = blend ramps over 0.15 luma units at each edge (recommended). Blend is always computed from ORIGINAL luma so the result doesn't chase the output — a highlight stays a highlight.

Outputs (3)

NameTypeDescription
imageIMAGENormalized image (RGB preserved if preserve_color=on).
found_minFLOATThe source luma value that maps to target_min.
found_maxFLOATThe source luma value that maps to target_max.