Nodes/ComfyUI-JH-PixelPro/Color: Palette Extract
ComfyUI Node

Color: Palette Extract

What Colours Is This Image Actually Made Of?

By jetthuangai·Created 6 months ago·Updated a day ago· 6
Color: Palette Extract
  • image
  • mask
  • palette
  • palette_json
  • quantized
◄colors6►

Ask three people what colour a photo is and you'll get "warm" or "blue-ish". Ask Color: Palette Extract (JHPixelProPaletteExtract) and you get six hex codes and the share of the frame each one occupies. That's a different kind of answer, and it's the one that's actually useful when you're trying to match a grade.

Three outputs, and each solves a different problem: a palette swatch strip for looking at, palette_json for feeding other things, and quantized - the image rebuilt in nothing but those palette colours.

Why LAB, and why that matters

The obvious way to find dominant colours is to average pixels, and the obvious way is terrible: averaging a face's highlights and shadows gives you grey mud. So this does k-means clustering - but in CIE LAB, a space built around how the eye perceives difference, so a cluster actually corresponds to "one colour" the way you'd name it, and dark and light versions of the same hue separate when they look different to you.

It's k-means++ with a fixed seed, so the result is deterministic: same image, same settings, same palette, every run. That sounds like a small thing until you want to compare two versions of an edit - you can diff the hex codes.

The two inputs you set

image, then colors - 2 to 16, default 6. The practical range is narrower than the widget suggests. For a portrait, 4 to 8 reads well; 2 or 3 is a great way to see the main split of a grade (warm skin versus cool shadow) and 12 or more mostly finds near-duplicates, because a photograph really does live in about six colours.

The optional mask is where this stops being a toy. Only pixels inside the mask (value above 0.5) count toward the palette and the shares - so mask the subject and a huge sky or backdrop stops eating five of your six slots. One documented behaviour worth knowing: if one item in a batch has an empty mask, that item falls back to its whole frame and logs a warning rather than failing the batch.

The three outputs

palette is an image - 1024 × 192, one equal-width band per colour, largest share first. Wire it to a Preview Image or a Save Image. One per image in the batch.

palette_json is a string: [{"hex": "#c8a58b", "rgb": [200, 165, 139], "share": 0.31}, ...], largest share first. Batch of one gives you a flat list; a bigger batch gives a list of lists. This is the output you actually build on - share is your "how much of this picture is this colour" number, and hex codes drop straight into a web palette, a LoRA caption, or a colour-scheme tool.

quantized is the image with every pixel replaced by its nearest palette colour. It always covers the whole frame, even when you masked. Two uses: as a posterize/flat-colour look on purpose, and as a structural read - quantize to 4 and suddenly you see the tonal blocks of your image without the texture arguing with you.

This is the same primitive the pixel-art people reach for, by the way. Palette quantization is step two of "make it actually pixel art" (after grid detection and integer downscale), and it's widely agreed that the model doesn't do this part - a deterministic pass does. This node is that pass, minus the grid detection.

How it works

Convert to CIE LAB via Kornia, subsample to at most 200,000 pixels, run seeded k-means++ plus up to 25 Lloyd iterations (stopping when no centre moves more than 0.05 LAB units), then assign every pixel - or every masked pixel, for the shares - to its nearest centre in chunks of about a million. Centres round-trip back to sRGB at 8 bits, and those rounded values drive all three outputs, so the strip, the JSON and the posterized image agree exactly. If the image has fewer distinct colours than you asked for, the extra palette entries have a share of 0 - not an error.

Install

Manager → search ComfyUI-JH-PixelPro → install → restart. Manual:

cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/ComfyUI-JH-PixelPro.git
cd ComfyUI-JH-PixelPro
pip install -r requirements.txt

Then Add Node → ComfyUI-JH-PixelPro → color. The pack (64 nodes, Apache-2.0, one maintainer) wants Python 3.10+, kornia>=0.7, MediaPipe, OpenCV headless and SciPy. Nothing to download for this node - it's clustering, not a model.

Common issues

The palette is all background. You didn't mask. A sky or studio backdrop will dominate any unmasked portrait palette, and rightly so - it is most of the pixels.

A cluster is empty / two bands look identical. More colours than the image has, or a very flat image. Drop colors and the duplicates disappear.

The shares don't sum to 1. They sum to the masked fraction when you use a mask, since only masked pixels count. That's the documented behaviour, not a rounding bug.

Two runs give different palettes. They shouldn't - the seed is fixed. If they do, the input changed (a different image in the batch, or an upstream node with its own randomness).

It's not instant on huge images. The 200k-pixel subsample keeps the clustering bounded, but the final nearest-centre assignment touches every pixel, chunked. A 6K image is fine; don't put it in a 100-item batch loop and expect it to be free.

One transparency note: this pack has essentially no community footprint yet - nothing on the usual subreddits - so per-node docs are your support channel. Which is the trade: no folklore, but unusually concrete docs that name the iteration counts and the seed.

CategoryComfyUI-JH-PixelPro/color

Inputs (3)

NameTypeDefaultDescription
imageIMAGEImage to read the colours from.
colorsINT62–16How many palette colours to find.
maskoptMASKOnly pixels inside this mask count towards the palette (e.g. the subject). The flat-colour image still covers the whole frame. If a batch item's mask is empty, that item uses the full image (a warning is logged).

Outputs (3)

NameTypeDescription
paletteIMAGESwatch strip, one equal-width band per colour, largest share first.
palette_jsonSTRINGJSON text. Batch of 1: list of {hex, rgb, share}. Batch > 1: list of such lists, one per image.
quantizedIMAGEThe image with every pixel replaced by its nearest palette colour.