Nodes/ComfyUI-DAELab-Custom-Nodes-Library/Badge Material Constraint V1 (DAELab)
ComfyUI Node

Badge Material Constraint V1 (DAELab)

Keep the model's texture, throw away its lighting

By TeutonicMepheles·Created 4 months ago·Updated 6 days ago· 0
Badge Material Constraint V1 (DAELab)
  • base_image
  • candidate_image
  • flat_image
  • height_map
  • region_mask
  • constrained_candidate
  • strict_mask
  • accepted
  • mean_chroma_error
  • p95_chroma_error
  • median_low_frequency_lightness_error
  • outside_max_abs_diff
  • report
◄color_policy▾►
◄intrinsic_color_hex#808080►
◄mean_chroma_limit0.020►
◄p95_chroma_limit0.050►
◄median_low_frequency_lightness_limit0.030►
◄high_frequency_strength0.35►
◄material_idgeneric►
◄material_strength1.00►
◄mid_frequency_strength1.00►
◄minimum_visible_mean0.000►
◄minimum_visible_p950.000►
◄pattern_seed0►
◄deterministic_fallbacktrue►
◄preserve_opticsfalse►
◄preserve_base_lightnessfalse►

What it is and why you'd reach for it

Ask GPT-Image-2 to "make this region brushed bronze" and you get brushed bronze - at a different exposure, with a new light coming from somewhere else, and with the underlying enamel colour quietly shifted. You wanted the material. You got a re-render.

Badge Material Constraint V1 is a frequency-domain referee between the model's candidate and your immutable base. It assumes the model is good at the thing you asked for (mid- and high-frequency surface detail: brush grain, glitter sparkle, lacquer ripple) and bad at the thing the rest of your pipeline owns (macro lightness from the height map, base colour from the design). So it keeps the first and discards the second.

How it works

Everything happens in OKLab, inside the region mask only. The candidate's lightness delta versus the base is band-passed with two box blurs - a tight one to isolate fine detail and a wide one to isolate the low-frequency drift - and only the mid and high bands survive, clamped to ±0.14/±0.16 so a wild candidate can't produce spikes. That signal is scaled by a per-material render profile (dark brushed bronze, baked enamel, satin gold/silver, glitter, rhinestone, transparent lacquer each have their own mid/high gains and strengths) times your material_strength.

The candidate's low-frequency lightness is thrown away entirely. Base lightness stays, which is exactly right: macro light belongs to the deterministic relief render, so a material change never re-lights the badge.

Chroma is re-anchored rather than copied. With color_policy: preserve the target chroma is your flat design's chroma at that pixel - so your enamel blue stays enamel blue and only the surface response changes. With material_intrinsic, the target comes from intrinsic_color_hex instead, which is how satin gold and satin silver get to be gold and silver rather than tinted copies of the source colour. Residual chroma detail from the candidate is capped at 0.018 radius, so glitter shimmers without recolouring.

Finally the blend: weighted by distance from the mask edge, with a guard ring at the boundary that takes zero contribution. That's why a material region doesn't end in a visible seam - the rim of your mask keeps the base pixels and the change ramps in over a couple of pixels. Pixels outside the mask are untouched by construction, and the node measures outside_max_abs_diff to prove it.

Inputs and outputs that matter

Required image side: base_image (the immutable master), candidate_image (the GPT result), flat_image (colour truth), height_map, region_mask. Then color_policy, which is the only one that changes the look meaningfully, and intrinsic_color_hex.

The rest are limits: mean_chroma_limit (0.02), p95_chroma_limit (0.05), median_low_frequency_lightness_limit (0.03) are acceptance gates. material_id, material_strength, mid_frequency_strength, high_frequency_strength, pattern_seed shape the texture. minimum_visible_mean / minimum_visible_p95 are a visibility floor - if the model returned something imperceptibly faint, they force the material to actually read. deterministic_fallback (on by default) means an invalid or imperceptible candidate gets replaced with a deterministic material render instead of failing the run.

Outputs: constrained_candidate is what you composite. strict_mask is the region mask, ready for Badge Deterministic Composite. accepted is the boolean verdict. The three error floats plus outside_max_abs_diff and report are the audit trail.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/TeutonicMepheles/ComfyUI-DAELab-Custom-Nodes-Library

Restart. V3-schema node (comfy_api.latest), so old ComfyUI cores fail at import. OpenCV is used for the distance-transform weighting and isn't in the pack's requirements.txt: pip install opencv-python-headless if the node doesn't appear.

Common issues

  • accepted: false and you can't tell why. Read report - mean_chroma_error over ~0.02 means the model recoloured; output_visible_mean near zero means the material didn't take. Raise material_strength before loosening the chroma limits; the limits are the guard rail.
  • Material looks flat and synthetic. deterministic_fallback kicked in. That's the safe outcome, not a bug - but check whether the model actually returned a change.
  • Seam at the mask edge. You're seeing the zero-weight guard ring against a much darker base. Make the mask follow a real artwork boundary.
  • Gold/silver turned dark. Raising minimum_visible_mean doesn't fix a candidate that mostly went black; there's a preserve_optics path for exactly this, but it's opt-in and stricter about invalid candidates.
CategoryDAELab/Badge/Strict

Inputs (20)

NameTypeDefaultDescription
base_imageIMAGE—
candidate_imageIMAGE—
flat_imageIMAGE—
height_mapMASK—
region_maskMASK—
color_policyCOMBO2 options: preserve, material_intrinsic
intrinsic_color_hexSTRING#808080—
mean_chroma_limitFLOAT0.0200–1—
p95_chroma_limitFLOAT0.0500–1—
median_low_frequency_lightness_limitFLOAT0.0300–1—
high_frequency_strengthFLOAT0.350–1—
material_idSTRINGgeneric—
material_strengthFLOAT1.000.25–1.5—
mid_frequency_strengthFLOAT1.000–1.5—
minimum_visible_meanFLOAT0.0000–0.25—
minimum_visible_p95FLOAT0.0000–0.4—
pattern_seedINT00–2147483647—
deterministic_fallbackBOOLEANtrue—
preserve_opticsoptBOOLEANfalse—
preserve_base_lightnessoptBOOLEANfalse—

Outputs (8)

NameTypeDescription
constrained_candidateIMAGE—
strict_maskMASK—
acceptedBOOLEAN—
mean_chroma_errorFLOAT—
p95_chroma_errorFLOAT—
median_low_frequency_lightness_errorFLOAT—
outside_max_abs_diffFLOAT—
reportSTRING—