Palette Harmonizer
Grade an image to a hex palette instead of re-generating it
- image
- harmonized_image
- palette_image
- normalized_hex
Here's the thing nobody tells you when you paste #39a78e into a prompt: the text encoder has no idea what it means. A guy on r/comfyui asked how to make "a zomp dress" come out teal-green and the model handed him a blue dress with the literal word ZOMP printed across the chest. That's not a bug you can prompt your way out of - hex codes aren't in the training distribution as colors, they're just tokens.
So the fix has to happen in pixels. That's the premise of the Palette Toolkit, and Palette Harmonizer is the half that does something interesting: it takes an ordinary image and pulls its colors toward a palette you specify, in a perceptual color space, deterministically, in milliseconds. No re-gen, no seed lottery, no rewritten face - the same logic as a gamma curve or a color match. Reach for the cheap deterministic primitive before you burn a diffusion pass on a color problem.
What it actually does
It converts your image from sRGB to linear, then to OKLab (a perceptual space where equal numeric distance looks like equal visual difference), and for every pixel measures its distance to each palette color. Then it builds a target color and mixes toward it:
soft(the default) - weights every palette color by a Gaussian falloff on that distance and takes the weighted average. Nearby colors pull hard, far ones barely at all.chroma_only- same blending, but the target's lightness is forced back to the source pixel's. Hue and saturation move, exposure doesn't. This is the "recolor it without relighting it" mode.nearest- snap each pixel to the single closest palette color. At full strength that's a hard posterize onto your palette: cheap pixel-art-adjacent posterizing without leaving ComfyUI.
Then mixed = source + strength * (target - source), converted back to sRGB. Alpha, if present, passes through untouched, and the math runs in chunks so a 4K batch doesn't allocate a full pixels × colors distance tensor at once.
The inputs worth setting
hex_codes is the palette: multiline text, one color per line by default. The parser takes whitespace, commas, semicolons, pipes or newlines as separators and #RRGGBB, RRGGBB, 0xRRGGBB or #RGB as formats, dedupes repeats, and caps at 32 colors.
strength is the dial you'll touch most. 0.35 is genuinely gentle - a tint pass, not a recolor. Push past 0.7 and you're overriding the image's own color story.
softness (0.005–0.5) controls how many palette colors blend into each pixel. Low values behave almost like nearest with soft edges; high values wash everything toward the palette's average, which mostly reads as a global color cast. The default 0.08 is a sane middle.
preserve_luminance defaults to true, and that's the honest default - it keeps each pixel's original lightness and only shifts hue and chroma, so you can't blow out your exposure by grading. If you want the palette's own tonal relationships too, turn it off and expect a much stronger look. Note chroma_only forces this behavior anyway.
Outputs
harmonized_image is the graded result - wire it onward like any IMAGE. palette_image is a 1024×256 horizontal swatch of the palette - a real pixel image you can feed to a style-reference path, or just preview next to the result to check the grade by eye. normalized_hex is the palette re-emitted as clean uppercase #RRGGBB, one per line - handy for chaining and for naming output files.
Two placements, both from the README and both correct:
Load Image → Palette Harmonizer → VAE Encode → KSampler (low denoise)
VAE Decode → Palette Harmonizer → Save Image
The first lets the palette reach the model through the image pathway. The second is deterministic finishing, and it's the one I'd reach for first - grade the decoded output and you know exactly what you got.
Installing it
ComfyUI Manager, search Palette Toolkit. Or by hand:
cd ComfyUI/custom_nodes
git clone https://github.com/CrimsonEarth/ComfyUI-Palette-Toolkit
# restart ComfyUI
No requirements.txt, no install.py, no models, no pip anything - it uses the PyTorch ComfyUI already ships, and pyproject.toml only asks for Python ≥3.10. This is the rare pack with nothing to download and nothing to break.
Where people get burned
The parser is strict where it matters, and the error is unhelpful: Invalid RGB hex color: '255'. Decimal RGB triples are not hex. Type 255,0,0 and you get an error; type 255 alone and something worse happens - it's a legal three-character hex code, so it silently expands to #225555. Labels break it too: red: #FF0000 won't parse, because red: isn't a color. More than 32 colors throws. And if the node doesn't show up after install, check for folder nesting (ComfyUI-Palette-Toolkit/ComfyUI-Palette-Toolkit) and read the console - the pack's classes register normally, so a missing node is an import error, not a registration quirk.
One expectation to set: this node can't add anything. It moves color, nothing else. Point it at a bad composition and it will grade it into a beautifully consistent palette.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| hex_codes | STRING | #1B2029 #66788C #E7DFC9 #A88443 #D5A33B #78A9BA | The target RGB palette. |
| strength | FLOAT | 0.350–1 | How strongly the image is pulled toward the palette. |
| mode | COMBO | soft | Soft blends nearby colors; chroma_only protects lightness; nearest can quantize. |
| softness | FLOAT | 0.0800.005–0.5 | Higher values blend influence from more palette colors. |
| preserve_luminance | BOOLEAN | true | Keep the source OKLab lightness while changing hue and chroma. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| harmonized_image | IMAGE | — |
| palette_image | IMAGE | — |
| normalized_hex | STRING | — |