Nodes/PortraitUtils/Auto White-Balance + Color Match
ComfyUI Node

Auto White-Balance + Color Match

Fix the Cast Without Re-Rolling a Sampler

By heyburns·Created about a year ago·Updated 23 days ago· 3
Auto White-Balance + Color Match
  • image
  • reference
  • IMAGE
◄methodwb_highlight+reinhard►
◄percentile95.0►
◄strength1.00►
◄clip_gamuttrue►
◄force_sizefalse►
◄target_width1440►
◄target_height1080►
◄preserve_luminancetrue►

Scanned prints are yellowed. Flash photos are orange. Mixed lighting gives you a face that's green on one side. Most people's instinct is to fix it in the generation stack - and that's the wrong tool, because a colour cast is a statistics problem, not a detail problem. Auto White-Balance + Color Match fixes it deterministically, in milliseconds, with no model and no new seed lottery.

If you want the framing that keeps you out of trouble: this is the node you reach for before you're tempted to run an img2img pass to "fix the colours." A re-gen is expensive, non-deterministic, and rewrites faces. A channel gain is none of those things.

How it works

White balancing here is independent channel gains in linear light - sRGB in, un-gamma, gain each channel, gamma back. Not a slider in a hue space; an actual illumination estimate. Two statistical methods estimate it: wb_grayworld equalises the alpha-weighted average linear RGB of the whole frame, and wb_highlight equalises channels among the brightest eligible pixels (selected by percentile, default 95, using a 2,048-bin histogram rather than sorting the image). Highlight WB is the better default for photos with a genuine bright neutral in them; gray-world is the blunt instrument that works when the frame is broadly neutral.

Reference matching is a separate, well-understood family: reinhard_lab matches per-image mean and standard deviation in D65 Lab, lab_l_only matches Lab lightness only, and the default wb_highlight+reinhard does source highlight WB first, then matches the corrected Lab statistics to your reference. That's classic Reinhard-style colour transfer, and it does what mean/std transfer always does: it moves your palette toward the reference's statistics, not to a physically measured illuminant.

By default the selected pixels are neutralised to their existing average linear luminance, so neutral highlights don't get slammed to white - that's preserve_luminance (on by default). Turn it off and you're opting into a brightness change as well as a colour one.

Controls

image and reference, both required. Note the reference socket is required even for the WB-only methods, which ignore its pixels - connect the source image to reference and move on. It's a compatibility holdover rather than a design statement.

method is the choice above. percentile only matters for the highlight methods: higher picks fewer, brighter pixels. strength (default 1.0) blends from unchanged source to full correction in sRGB - and this is your safety valve, because 0.3 on a skin-tone-heavy photo usually looks better than 1.0. clip_gamut (on by default) clamps the final RGB to 0–1; leave it on unless you're chaining into something that expects unclamped values, because a second instance of this node assumes normalised input.

force_size with target_width/target_height (1440×1080) limits analysis sampling only - the correction is always applied to the original full-resolution pixels. Start there on big scans; it's a real speedup and the fitted transform doesn't care how many pixels it was fitted on. value-free, one output: the corrected IMAGE, same dimensions, batch, dtype and device as the input.

Statistics are alpha-weighted, so transparent regions don't drag the estimate, and RGBA survives intact.

Where it goes wrong

Both statistical WB methods assume a neutral somewhere. Theatrical lighting, a strongly coloured background, skin highlights in a warm-lit room, or a deliberately warm grade will all mislead them - there is no way to identify a true neutral patch from colour statistics alone, and the pack's docs say so plainly rather than pretending otherwise. If a photo looks worse after correction, this is why. Compare at strength 0.3 before committing.

Reference matching transfers global colour statistics, so it will happily change your exposure even with preserve_luminance on. It's also guarded: the Lab standard-deviation ratios are limited, channels that are nearly flat (below 0.05 Lab units in either image) get a mean shift only rather than an amplified std ratio, and WB gains are clamped to 0.25–4. Those guards mean an extreme reference won't be matched exactly. That's a feature; a node that matched a flat reference exactly would blot your texture.

Batch rules are worth knowing: one reference serves the whole source batch, unless the reference batch is exactly the same length, in which case it matches pairwise. Any other count is rejected. A fully transparent reference raises an explanatory error, and a fully transparent source pixel is left alone.

Install

ComfyUI Manager → search PortraitUtils → install → restart the ComfyUI server. Then it's under PortraitUtils/Analysis.

cd ComfyUI/custom_nodes
git clone https://github.com/heyburns/PortraitUtils
/path/to/ComfyUI/venv/bin/python -m pip install -r PortraitUtils/requirements.txt

Use ComfyUI's Python - or python_embeded on Windows portable builds. The pack's requirements are numpy, Pillow, opencv-python-headless and tifffile; only one OpenCV wheel variant, since they share the cv2 namespace.

The long node ID still contains Preview because the pack kept the production Python IDs after cleaning up the display names (it displays as Auto White-Balance + Color Match). Search the display name, not the ID - and don't go looking for a separate "non-preview" version, because the old AutoWBColorMatch ID has been removed.

CategoryPortraitUtils/Analysis

Inputs (10)

NameTypeDefaultDescription
imageIMAGE—
referenceIMAGE—
methodCOMBOwb_highlight+reinhard5 options: wb_grayworld, wb_highlight, reinhard_lab, lab_l_only, wb_highlight+reinhard
percentileFLOAT95.080–99.9—
strengthFLOAT1.000–1—
clip_gamutBOOLEANtrue—
force_sizeBOOLEANfalseLimit statistics sampling only; output stays full resolution.
target_widthINT144016–8192—
target_heightINT108016–8192—
preserve_luminanceoptBOOLEANtrueWB preserves the sampled linear-light luminance instead of forcing highlights to 95% white. Reference matching can still change brightness.

Outputs (1)

NameTypeDescription
IMAGEIMAGE—