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

Badge GPT Structure Transfer V1 (DAELab)

Let the model suggest the relief, let geometry decide — with a veto

By TeutonicMepheles·Created 4 months ago·Updated 3 days ago· 0
Badge GPT Structure Transfer V1 (DAELab)
  • candidate_image
  • geometry_base
  • flat_image
  • height_map
  • foreground_mask
  • fused_structure
  • fused_lightness
  • accepted
  • aligned_candidate
  • detail_preview
  • report
◄form_strength0.40►
◄detail_strength0.24►
◄boundary_lock_px1►
◄maximum_translation_px8►
◄minimum_relief_contrast0.080►
◄minimum_edge_coverage0.24►
◄enforce_1024true►

What this is for

Ask an image model to render a grayscale "structure proof" of a badge - a grey picture where brightness means height - and it will often produce something genuinely lovely: soft chamfers, believable shoulders, a bit of micro-bevel on the lettering. It will also, sometimes, hallucinate a whole new outline, invert a shoulder, or return a picture with a nice gradient and no actual height in it at all.

Badge GPT Structure Transfer V1 is the adult in the room. It takes the model's grayscale candidate and a deterministic relief base built from your height map, tries to register the candidate onto the geometry, decides whether the candidate is trustworthy at all, and then transfers only bounded form and detail frequencies into the deterministic base. Your flat artwork's colours are never touched - the light/shadow ratio is reapplied to the original art, not to the model's invented colours.

This is the node that lets you use a generative model for surface nuance without letting it redraw the product. There's an accepted boolean output and, if the answer is no, you get the deterministic base back instead of the model's output. Refuse-and-fall-back, not best-effort blend - and that's the right design for something that ends up as a manufacturing reference.

How it works

The candidate first gets resized and aligned. Edge alignment searches small translations up to maximum_translation_px (default 8) to find the best fit against the target edges - which come from the height map's level boundaries, colour edges inside the foreground, and the silhouette itself.

Then two acceptance gates. If the candidate's overall relief contrast - the quantile range of its normalised lightness inside the foreground - is below minimum_relief_contrast (default 0.08), it's rejected as "too flat to be a height map". If the minimum per-image edge coverage is below minimum_edge_coverage (default 0.24), it's rejected as "aligned but not actually the same shape". Either way accepted goes false and you get the untouched geometry base.

If accepted, the candidate's lightness is split into frequency bands - a fine band, a medium band around a 10–48px gaussian difference, and a high band - and each is scaled by form_strength (default 0.40) and detail_strength (default 0.24) before being added into the geometry's OKLab lightness. Then a boundary band, boundary_lock_px wide (default 1), is replaced by the deterministic bevel geometry, so no matter how creative the model got, the silhouette band is yours. Output is converted back to colour with the flat artwork's chroma intact.

enforce_1024 defaults true: flat art, masks and the height map must be 1024, and the geometry base must match. Set it false if you're deliberately working at another square size and the candidate will be bicubic-normalised for you.

The inputs and outputs

Five images in: candidate_image (the model's greyscale proof), geometry_base (out of Badge Relief Geometry V1), flat_image, height_map, foreground_mask.

The three knobs you'll actually tune: form_strength for the broad shape contribution, detail_strength for the micro texture, and the two acceptance thresholds if you want a stricter or looser veto. maximum_translation_px matters if the model tends to re-centre things; boundary_lock_px matters if you can see seams at the silhouette.

Six outputs, and the extras are the point of the node: fused_structure (the result), fused_lightness (the mask-shaped lightness field that went in), accepted (the veto), aligned_candidate (what the model's output looked like after registration - this is how you diagnose a rejection), detail_preview, and report with the shift values, alignment scores, edge coverages and the rejection reason.

Install

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

No model of its own - the GPT call is a separate, visible native OpenAIGPTImageNodeV2 in the workflow, and the pack is explicit that this module doesn't call the API. That's a nice property: the expensive part is a node you can see and bypass.

Where people get burned

Fighting a rejection instead of reading it. accepted: false with candidate_relief_signal_too_weak means the model returned a nearly flat image. Crank form_strength all you like, it won't help - the candidate had nothing in it. Regenerate the proof with a stronger structure prompt, or accept the deterministic base, which is a legitimate output.

Strength values above ~0.6. The transfer is additive into OKLab lightness, and past a point you're no longer "transferring nuance", you're reconstructing the model's picture with extra steps. The defaults are conservative on purpose.

Feeding a coloured render as the candidate. It expects a greyscale structure proof. A colour render's lightness carries shading that belongs to the camera, not the geometry, and the frequency split will happily bake it into your relief.

Non-1024 inputs with enforce_1024 on. It errors rather than resizing, and the error names the exact tensor. Normalise upstream - the pack has a node for exactly that - and don't switch the flag off to "just get it running"; the height map's discretisation assumptions travel with the canvas.

CategoryDAELab/Badge/Height

Inputs (12)

NameTypeDefaultDescription
candidate_imageIMAGE—
geometry_baseIMAGE—
flat_imageIMAGE—
height_mapMASK—
foreground_maskMASK—
form_strengthFLOAT0.400–1—
detail_strengthFLOAT0.240–1—
boundary_lock_pxINT10–8—
maximum_translation_pxINT80–32—
minimum_relief_contrastFLOAT0.0800–1—
minimum_edge_coverageFLOAT0.240–1—
enforce_1024BOOLEANtrue—

Outputs (6)

NameTypeDescription
fused_structureIMAGE—
fused_lightnessMASK—
acceptedBOOLEAN—
aligned_candidateIMAGE—
detail_previewIMAGE—
reportSTRING—