The pipeline: image to palette in three steps
Every extraction tool, whether it's a website or a library, runs the same three-stage pipeline. You feed it an image, it samples and clusters the pixels, and it returns a compact palette of the most representative colors.
The key insight is in step two. Naive extraction would just count how often each exact RGB value appears — but a photo has thousands of near-identical shades of "blue," so raw counting produces a palette of imperceptibly different blues. Quantization solves this by grouping similar colors into clusters and representing each cluster with a single color, so the palette spans the image instead of clumping on its most common hue.
Which method should I use?
The right tool depends on whether you want a one-off palette, a repeatable script, or something built into an app. Here is the honest comparison.
| Method | How it works | Best for | Skill needed | Runs where |
|---|---|---|---|---|
| Manual color picker | You eyedrop individual pixels by hand | A few exact brand/key colors you must control | None | Any design app |
| Web tools (Coolors, Adobe Color, Canva, Colormind) | Upload image → algorithmic quantization → editable palette | Quick, no-code palettes; saving to a library | None | Browser (often server-side upload) |
| Color Thief (JS library) | Canvas API + modified median cut (MMCQ) | Adding extraction to a web app; client-side privacy | Basic JS | Browser / Node |
| Vibrant.js (JS library) | Quantization + swatches labeled by role (Vibrant, Muted, etc.) | Themeing a UI to an image (album art, hero photos) | Basic JS | Browser / Node |
| scikit-learn + Pillow (Python) | KMeans clustering over the pixel array | Batch analysis, data pipelines, custom k | Intermediate Python | Local / server |
ImageMagick (-colors) | Median-cut quantization from the CLI | Scripting, automation, no coding language | CLI basics | Command line |
| Design software (Photoshop, Figma, Affinity) | Built-in sampling + palette panels | Designers already in the tool | None | Desktop app |
Which should you pick? For a one-off palette, use a web tool — Adobe Color and Coolors are the fastest. To extract inside your own website without sending user images to a server, use Color Thief or Vibrant.js (both are client-side). For batch-processing many images or full control over the algorithm and k, use Python with scikit-learn. For shell scripts and CI, use ImageMagick.
Color quantization: k-means vs. median cut
Two algorithms dominate. Understanding the difference explains why tools disagree.
K-means clustering treats each pixel as a point in 3D RGB space. You choose k (how many colors you want). The algorithm places k starting centers, assigns every pixel to its nearest center, moves each center to the average of its assigned pixels, and repeats until the centers stop moving. The final centers are your palette. Because it starts from random centers, two runs can produce slightly different results — and it can be slow on large images unless you sample a subset of pixels first.
Median cut (used by Color Thief's MMCQ) takes the opposite approach. It puts every pixel in one big box in color space, then repeatedly finds the box with the widest range of color, sorts by that dimension, and splits it at the median — halving the boxes until you have the number you want. Each final box's average is a palette color. It's deterministic and fast, which is why it powers most in-browser extractors.
Neither is "correct." K-means tends to give colors weighted toward large uniform regions; median cut gives more even coverage of the color range. If a tool's palette feels off, trying one that uses the other algorithm often fixes it.
Extracting colors in JavaScript (Canvas API + Color Thief)
You don't need a library at all — the browser can read pixels directly. Draw the image to a canvas, then getImageData() returns a flat Uint8ClampedArray of [R, G, B, A, R, G, B, A, …] values you can process however you like.
// Read raw pixels with the Canvas API (the foundation every JS extractor builds on)
function getPixels(img) {
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// data is [R, G, B, A, R, G, B, A, ...] for every pixel
return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
}
// A minimal "average color" — the crudest possible extraction
function averageColor(img) {
const data = getPixels(img);
let r = 0, g = 0, b = 0, n = data.length / 4;
for (let i = 0; i < data.length; i += 4) {
r += data[i]; g += data[i + 1]; b += data[i + 2];
}
return `rgb(${Math.round(r / n)}, ${Math.round(g / n)}, ${Math.round(b / n)})`;
}
Averaging every pixel gives you one muddy color — useful as a placeholder, useless as a palette. For a real palette you want quantization, and that's exactly what Color Thief provides. It runs the Canvas step and MMCQ for you:
// Color Thief handles the canvas read + median-cut quantization
import ColorThief from "colorthief";
const img = document.querySelector("#source");
const thief = new ColorThief();
img.addEventListener("load", () => {
const dominant = thief.getColor(img); // [r, g, b] single dominant color
const palette = thief.getPalette(img, 8); // array of 8 [r, g, b] colors
const toHex = ([r, g, b]) =>
"#" + [r, g, b].map(v => v.toString(16).padStart(2, "0")).join("");
console.log("Dominant:", toHex(dominant));
console.log("Palette:", palette.map(toHex));
});
Two important gotchas: the image must be fully loaded before you read it (hence the load listener), and if the image comes from another domain it must be served with permissive CORS headers and loaded with crossOrigin = "anonymous" — otherwise the canvas becomes "tainted" and getImageData() throws a security error.
If you want swatches labeled by role instead of a flat list, Vibrant.js returns named results like Vibrant, Muted, DarkVibrant, and LightMuted — ideal for theming a UI to whatever image is on screen.
Doing it in Python (k-means with scikit-learn)
For batch jobs or precise control over k, Python is the cleanest path. KMeans does the clustering; Pillow loads and downsamples the image so it runs quickly.
from PIL import Image
from sklearn.cluster import KMeans
import numpy as np
def extract_palette(path, k=6):
img = Image.open(path).convert("RGB")
img.thumbnail((200, 200)) # downsample for speed
pixels = np.array(img).reshape(-1, 3) # flatten to a list of RGB rows
kmeans = KMeans(n_clusters=k, n_init=10).fit(pixels)
centers = kmeans.cluster_centers_.astype(int)
return ["#%02x%02x%02x" % tuple(c) for c in centers]
print(extract_palette("inspiration.jpg", k=6))
# -> ['#2a3d66', '#e8b04b', '#0f9d8c', ...]
Downsampling to a couple hundred pixels on the long edge barely changes the palette but makes clustering dramatically faster — a full-resolution photo has millions of pixels and k-means visits each one every iteration.
From raw extraction to a usable palette
Extraction gives you a starting point, not a finished palette. The colors that dominate an image are optimized for that image — not for legibility, harmony, or your brand. Turn the raw output into something usable:
- Curate by role. From eight extracted colors, pick one or two dominant colors, two or three supporting colors, and one accent. A flat list of eight equal HEX codes isn't a design system.
- Check accessibility. Extraction knows nothing about contrast. Test every text-on-background pairing against WCAG ratios (4.5:1 for normal text, 3:1 for large text and UI components) and nudge lightness where it fails. See our guide to accessible color combinations and WCAG standards.
- Apply color theory. Ask whether the extracted colors actually form a coherent scheme — complementary, analogous, triadic — or just happen to co-occur. Our color harmonies guide covers how to adjust an extracted set into an intentional harmony.
- Generate variations. Derive lighter and darker tints of each color for hover, disabled, and emphasis states rather than extracting them separately.
- Store in a real format. Save the palette as HEX, RGB, and HSL — HSL makes lightness/saturation tweaks trivial. See color models (HEX, RGB, HSL, HSV) and modern CSS color formats for how to express them in code.
Generating palettes without a source image
Extraction isn't the only route. If you don't have an inspiring image, you can generate a palette from color theory instead: pick a base color and derive complementary, analogous, or triadic partners by rotating hue on the color wheel. Tools like Coolors (in generation mode), Colormind, and Khroma produce harmonious or ML-suggested schemes you can lock and regenerate. The strongest workflow combines both: extract a palette from an image for its natural harmony, then refine it with theory and accessibility checks so the result is intentional rather than accidental.
Tips for better extraction
- Use high-quality images. Compression artifacts and low resolution muddy the clusters and produce off colors.
- Match the image to your goal. Extract from an image whose mood matches the design you're building — a muddy source gives a muddy palette.
- Tune the color count. Five to eight is the practical range; try a couple of values and compare.
- Try two tools. Because algorithms differ, running the same image through a k-means tool and a median-cut tool sometimes surfaces an accent color one of them missed.
- Keep private images local. Prefer client-side extractors (Color Thief, Vibrant.js, desktop software) when the image is confidential or copyrighted, so pixels never leave your device.
Color palette extraction turns the natural harmony already present in an image into a reusable design foundation. The tools make it a two-second operation; the value you add is knowing what the algorithm did, curating the raw output by role, and tuning it for accessibility so an inspired palette becomes an intentional one.