DEV Community

qingwancong
qingwancong

Posted on

Palette quantization notes: reducing colors without making an image muddy

I’ve been thinking about a small image-processing problem lately: how to reduce an image to a limited palette without making it look muddy.

This comes up in a lot of places:

  • pixel art tools
  • printable pattern generators
  • low-color previews
  • LED matrix displays
  • icons and small thumbnails
  • craft or grid-based workflows

The easy version is: pick the nearest color for every pixel.

The hard version is: keep the important shapes readable after the palette gets much smaller.

Nearest color is only the baseline

A simple nearest-color pass usually works like this:

  1. Take each pixel.
  2. Compare it with every color in the target palette.
  3. Pick the closest one.
  4. Replace the pixel.

That gives you a valid output, but not always a good one.

The problem is that closest is local. It does not know whether the whole image still reads well.

A face can lose warm midtones. A shadow can turn into a flat dark blob. A small highlight can disappear. Skin, fur, fabric, and background colors can collapse into the same bucket.

So palette reduction is not just a color problem. It is also a structure problem.

RGB distance can be misleading

A common first attempt is Euclidean distance in RGB:

function rgbDistance(a, b) {
  return Math.sqrt(
    (a.r - b.r) ** 2 +
    (a.g - b.g) ** 2 +
    (a.b - b.b) ** 2
  );
}
Enter fullscreen mode Exit fullscreen mode

This is easy to implement, but it does not match human perception very well.

Two colors can be numerically close in RGB and still feel different. Other colors can be farther apart numerically but visually acceptable.

A better approach is to compare colors in a more perceptual color space, such as Lab or OKLab. You still have to be careful, but the distance metric starts closer to what the eye notices.

Dithering helps, but it changes the style

Error diffusion, like Floyd-Steinberg dithering, can preserve gradients and perceived detail with fewer colors.

That is useful when the output is meant to look like a low-color image.

But dithering is not always desirable. In grid-based outputs, it can create scattered single-pixel noise. That may preserve tone, but it can make the result harder to read or harder to build.

So the question is not: should I dither?

Is texture more important here, or are clean shapes more important?

For a tiny icon, clean shapes often win. For a photograph-like preview, dithering may win.

Palette size is a UX control, not just a parameter

A 24-color image and a 6-color image are not just different compression levels. They can feel like different art directions.

When users control palette size, they are really controlling tradeoffs:

  • realism vs simplicity
  • smoothness vs readability
  • detail vs editability
  • texture vs clean regions

This is why I like showing a preview before committing to the palette. A numeric color count alone does not tell the user what the image will feel like.

Preserve important regions differently

One useful trick is to treat the whole image less uniformly.

For example:

  • preserve contrast around eyes and facial features
  • simplify background colors more aggressively
  • keep outlines cleaner than texture regions
  • protect small high-salience details from being averaged away

This does not require a huge ML pipeline. Even simple masks, edge detection, or manually marked regions can help.

The main idea: not every part of the image deserves the same color budget.

A practical pipeline

A basic but workable pipeline might look like this:

resize image
-> optional subject/background separation
-> smooth tiny noise
-> choose or load target palette
-> map pixels using perceptual distance
-> optionally apply controlled dithering
-> clean isolated pixels
-> preview multiple color counts
-> let the user pick the best tradeoff
Enter fullscreen mode Exit fullscreen mode

The cleanup step matters more than it sounds.

After quantization, isolated pixels often appear because the algorithm is optimizing locally. A small post-pass that removes lonely pixels or merges tiny regions can make the result feel much more intentional.

What I watch for

When reviewing a reduced-palette result, I usually check:

  • Do the main shapes still read at a glance?
  • Did skin or fur become too gray?
  • Did important contrast disappear?
  • Are there noisy single pixels everywhere?
  • Did the background steal colors from the subject?
  • Would a human understand why each color exists?

That last question is fuzzy, but useful.

If a color appears only a few times and does not clarify the image, it may be adding complexity without adding meaning.

Closing thought

Palette quantization looks simple because the first version is easy to code.

But the useful version is more about choosing what to preserve.

The algorithm can map colors. The product decision is deciding which details are worth keeping.

Top comments (0)