Badge Material Constraint V1 (DAELab)
Keep the model's texture, throw away its lighting
- 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
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: falseand you can't tell why. Readreport-mean_chroma_errorover ~0.02 means the model recoloured;output_visible_meannear zero means the material didn't take. Raisematerial_strengthbefore loosening the chroma limits; the limits are the guard rail.- Material looks flat and synthetic.
deterministic_fallbackkicked 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_meandoesn't fix a candidate that mostly went black; there's apreserve_opticspath for exactly this, but it's opt-in and stricter about invalid candidates.
Inputs (20)
| Name | Type | Default | Description |
|---|---|---|---|
| base_image | IMAGE | — | |
| candidate_image | IMAGE | — | |
| flat_image | IMAGE | — | |
| height_map | MASK | — | |
| region_mask | MASK | — | |
| color_policy | COMBO | 2 options: preserve, material_intrinsic | |
| intrinsic_color_hex | STRING | #808080 | — |
| mean_chroma_limit | FLOAT | 0.0200–1 | — |
| p95_chroma_limit | FLOAT | 0.0500–1 | — |
| median_low_frequency_lightness_limit | FLOAT | 0.0300–1 | — |
| high_frequency_strength | FLOAT | 0.350–1 | — |
| material_id | STRING | generic | — |
| material_strength | FLOAT | 1.000.25–1.5 | — |
| mid_frequency_strength | FLOAT | 1.000–1.5 | — |
| minimum_visible_mean | FLOAT | 0.0000–0.25 | — |
| minimum_visible_p95 | FLOAT | 0.0000–0.4 | — |
| pattern_seed | INT | 00–2147483647 | — |
| deterministic_fallback | BOOLEAN | true | — |
| preserve_opticsopt | BOOLEAN | false | — |
| preserve_base_lightnessopt | BOOLEAN | false | — |
Outputs (8)
| Name | Type | Description |
|---|---|---|
| constrained_candidate | IMAGE | — |
| strict_mask | MASK | — |
| accepted | BOOLEAN | — |
| mean_chroma_error | FLOAT | — |
| p95_chroma_error | FLOAT | — |
| median_low_frequency_lightness_error | FLOAT | — |
| outside_max_abs_diff | FLOAT | — |
| report | STRING | — |