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

Badge Structure Constraint V1 (DAELab)

Keeps GPT's lighting, throws away its colors

By TeutonicMepheles·Created 4 months ago·Updated 3 days ago· 0
Badge Structure Constraint V1 (DAELab)
  • candidate_image
  • fallback_image
  • flat_image
  • height_map
  • foreground_mask
  • structure_image
  • structure_lightness
  • accepted
  • report
◄edge_protection_px1►
◄minimum_relief_contrast0.080►
◄minimum_boundary_contrast0.015►
◄minimum_boundary_coverage0.60►
◄enforce_grayscaletrue►
◄enforce_1024true►

Enamel badges are a lighting problem disguised as a design problem. You want the relief - the raised rim, the bevel, the tiny shadow under the lettering - and GPT Image 2 is genuinely good at inventing that from a flat grayscale height map. It's also constitutionally incapable of leaving your colours alone. Ask for relief and you get relief plus a new palette, new contrast, slightly different edges.

This node exists to take exactly the useful half. Badge Structure Constraint V1 reads a GPT relief proof, keeps the light and shadow, and reapplies only that to your flat artwork. The colors are yours again by construction.

The mechanism

Four inputs do the work: candidate_image (the GPT grayscale proof), flat_image (your untouched artwork), height_map (the discrete height map from the height-layer node) and foreground_mask. There's also fallback_image, which is what comes out when the candidate is rejected.

The candidate gets normalised to 1024 first if it isn't already - enforce_1024 is on by default, internally a bicubic resize - while the flat artwork, masks and height map stay strictly 1024 and become the geometric authority. Then the node strips colour out of the proof (enforce_grayscale), checks that the proof actually carries relief signal, and transfers the accepted light/shadow ratio onto the flat artwork's lightness. Silhouette coordinates are preserved; the locked boundary band is replaced with deterministic bevel geometry rather than flattened.

Two thresholds decide whether anything gets through. minimum_relief_contrast (default 0.08) asks "is there enough tonal variation in this proof to be relief at all?" - a flat grey rectangle fails. minimum_boundary_contrast (0.015) with minimum_boundary_coverage (0.6) asks the narrower question "does the signal sit where the height boundaries are?" - a proof that's beautifully shaded in the wrong places fails too. edge_protection_px (default 1) is the band around region edges that the transfer isn't allowed to smear.

No network call, no model, no cache tricks. Deterministic pixel maths on the lightness channel, which is why it's cheap to run twenty times while you tune.

Outputs

structure_image is the result you composite. structure_lightness is the accepted lightness channel as a MASK - handy for debugging, and for anything that wants to know "how much relief did we actually get here". accepted is a BOOLEAN, and report is a JSON string with the measured contrasts and coverage plus, when it fails, why.

Wire accepted into a switch or a display. The pack's whole design philosophy is that a rejected candidate should degrade into something usable rather than quietly ship a wrong image, and accepted is how you find out that happened. If you never look at it, you'll eventually ship a badge that's accidentally the deterministic fallback.

When to reach for it

This is the 8.5 height branch's gatekeeper. The intent is: generate a global grayscale "this is what the relief looks like" proof once per design, run it through here, and then hand the resulting structure image to the material stages. It's the node that makes a generative step safe to put in the middle of a print pipeline, because the thing that can go wrong (colour drift) has been physically removed from the data path.

If you're doing anything else - a single one-off badge, a mockup, a quick client preview - this is overkill. The value shows up when the same design template has to render reliably dozens of times.

Install

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

Restart ComfyUI and hard-refresh the browser tab. The node lands in DAELab/Badge/Height; it's also findable through ComfyUI Manager by searching the pack title. Clone the whole repo - root __init__.py does the registration, so a single node folder on its own registers nothing.

Gotchas

If accepted is false every time, the most common causes, in order: the candidate isn't grayscale (a colour render will fail the grayscale gate, which is the point of enforce_grayscale); the proof is too flat because you asked GPT for "a subtle relief" and it obliged; or the height map and the candidate disagree about where the boundaries are, usually because one of them came from a different canvas size and got resized rather than regenerated.

Resizing is tolerated but never free. A 1024 candidate that arrives as 1023×1023 gets bicubic-normalised and is usually fine; a 1536×1536 candidate scaled down loses exactly the fine bevel detail you were paying for, so ask the image node for 1024 in the first place.

Put this after the pack's canvas normaliser, not before. The node validates that the flat artwork, masks and height authority are all 1024 and will not stretch them for you - a dimension mismatch is an error, not a warning. That strictness is the reason the branch is trustworthy, and the reason the error messages read a little terse.

One last thing: it does not call the API. The GPT node stays visible in your graph and you pay for it separately. This is purely the referee that decides whether the thing you paid for was any good.

CategoryDAELab/Badge/Height

Inputs (11)

NameTypeDefaultDescription
candidate_imageIMAGE—
fallback_imageIMAGE—
flat_imageIMAGE—
height_mapMASK—
foreground_maskMASK—
edge_protection_pxINT10–8—
minimum_relief_contrastFLOAT0.0800–1—
minimum_boundary_contrastFLOAT0.0150–1—
minimum_boundary_coverageFLOAT0.600–1—
enforce_grayscaleBOOLEANtrue—
enforce_1024BOOLEANtrue—

Outputs (4)

NameTypeDescription
structure_imageIMAGE—
structure_lightnessMASK—
acceptedBOOLEAN—
reportSTRING—