BD Normalize Luma
Fix your dynamic range before the mask sees it — BD Normalize Luma
- image
- mask
- image
- found_min
- found_max
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_scaleoff) - 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_scaleon) - a single multiplier so the brightest non-outlier pixel lands exactly ontarget_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 withtarget_max=0.5and 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.6 → 1.0), shadows only (0.0 → 0.5), midtones only (0.3 → 0.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.
Inputs (13)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Source image to normalize. | |
| luma_standard | COMBO | bt709 | RGB → 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_max | FLOAT | 0.950–1 | Brightest output value. Set lower (e.g. 0.85) to compress an over-bright image so there's headroom. |
| target_min | FLOAT | 0.000–1 | Darkest output value. Set higher (e.g. 0.1) to lift shadows in an under-exposed image. |
| clip_percent_high | FLOAT | 1.00–50 | Treat 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_low | FLOAT | 1.00–50 | Treat the bottom X% of valid pixels as the source-min. Higher values ignore more outliers. |
| preserve_color | BOOLEAN | true | ON: rescale RGB proportionally (keeps color, just darkens/brightens). OFF: convert to greyscale using the normalized luma. |
| proportional_scale | BOOLEAN | false | OFF (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_only | BOOLEAN | true | ON (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. |
| maskopt | MASK | Optional 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_minopt | FLOAT | 0.000–1 | Tone 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_maxopt | FLOAT | 1.000–1 | Tone 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_featheropt | FLOAT | 0.150–1 | Soft 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)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | Normalized image (RGB preserved if preserve_color=on). |
| found_min | FLOAT | The source luma value that maps to target_min. |
| found_max | FLOAT | The source luma value that maps to target_max. |