Contents
  1. What a Python palette extraction can and cannot tell you
  2. Choose a method based on your source file
  3. Before you start
  4. Step 1: Load the image and keep only logo pixels
  5. Step 2a: Count exact colors in flat artwork
  6. Step 2b: Cluster colors in noisy or shaded artwork
  7. Step 3: Convert to hex and save a swatch sheet
  8. Reading colors directly from an SVG
  9. How to confirm the palette is correct
  10. Using the palette in logo delivery

The short answer: load the logo with Pillow, convert it to RGBA, discard transparent and background pixels, then count the remaining colors with NumPy. If the artwork is flat, keep the most frequent exact colors. If it is anti-aliased, compressed or shaded, group similar pixels with k-means clustering. Convert the results to hex codes and check them against the artwork. If you have a vector file such as an SVG, read the fill and stroke values from the file instead of sampling pixels, because those values are the colors the designer actually specified.

Some terms used in this guide: a vector logo is built from mathematically defined paths, and each shape carries an explicit color value. A raster image (PNG, JPEG) is a grid of pixels. Anti-aliasing is the blending of edge pixels between a shape and its background, which creates intermediate colors that were never part of the design. Color quantization means reducing many similar colors to a small representative set. A palette is the final list of distinct colors the logo uses.

What a Python palette extraction can and cannot tell you

Pixel sampling returns screen values (RGB and hex) as they appear in that particular file. It cannot recover print specifications such as CMYK builds or spot colors like Pantone references. Those are decisions recorded in brand guidelines or in the original print artwork. Do not convert an extracted hex value to a spot color by guessing, and do not treat sampled values as the official brand colors unless the brand owner confirms them. A file extension does not guarantee clean vector content either: an SVG, AI or EPS file can contain embedded raster images, and having the file does not mean you have permission to reuse the logo.

Choose a method based on your source file

Source artwork | Method | What you get | Main limitation
SVG with explicit fills | Parse fill, stroke and stop-color values | The exact specified colors | Misses colors set through CSS classes in external files, rgb() notation or named colors unless you handle them
Flat PNG with transparency | Exact pixel counting | Real pixel values, ranked by area | Anti-aliased edges add minor colors that must be filtered out
JPEG, scan or shaded artwork | K-means clustering, then the most common real pixel in each cluster | A representative palette of chosen size | You choose the number of colors, and gradients have no single true value

Before you start

  • Python 3 with Pillow and NumPy installed; scikit-learn if you plan to cluster.
  • The highest-quality source available: vector first, then a lossless PNG, and a JPEG only as a last resort, because JPEG compression adds color noise.
  • A visual count of how many distinct colors the logo appears to use, so you can check the output against it.
  • Knowledge of whether the logo sits on a transparent background or a solid one, because a solid background will show up as the dominant color.
  • The library versions you have installed. The code below uses widely documented Pillow, NumPy and scikit-learn calls, but no version-specific behavior was verified for this guide. Check the parameters against the documentation for your versions.
pip install pillow numpy scikit-learn

Step 1: Load the image and keep only logo pixels

Converting to RGBA makes palette-mode, grayscale and RGB files behave the same way and keeps any transparency. Pixels below an alpha cutoff are dropped, which also removes most of the semi-transparent edge pixels that anti-aliasing produces. If the logo sits on a solid background, pass that color in so it is excluded.

from collections import Counter

import numpy as np
from PIL import Image


def load_logo_pixels(path, alpha_cutoff=250, background=None, bg_tolerance=8):
    img = Image.open(path).convert("RGBA")
    arr = np.asarray(img).reshape(-1, 4)
    pixels = arr[arr[:, 3] >= alpha_cutoff][:, :3].astype(int)
    if background is not None:
        distance = np.abs(pixels - np.array(background)).max(axis=1)
        pixels = pixels[distance > bg_tolerance]
    return pixels

Pillow's basic convert call does not perform color-managed conversion. If the file has an embedded ICC color profile, the raw values may differ from what a color-managed application displays. Pillow's ImageCms module can handle profile conversion when that matters for your delivery.

Step 2a: Count exact colors in flat artwork

For a clean, flat-color PNG, count every unique color and keep those that cover a meaningful share of the logo. The min_share threshold is a starting value, not a standard. Raise it if edge colors still appear, and lower it if a small but genuine accent color disappears.

def exact_palette(pixels, min_share=0.005):
    colors, counts = np.unique(pixels, axis=0, return_counts=True)
    shares = counts / counts.sum()
    order = np.argsort(shares)[::-1]
    return [(tuple(colors[i]), float(shares[i])) for i in order if shares[i] >= min_share]

Step 2b: Cluster colors in noisy or shaded artwork

When compression or shading spreads one design color across many slightly different pixel values, cluster them. A k-means cluster center is an average, so it may not match any pixel in the file. To keep each swatch tied to the artwork, the function below reports the most common real pixel in each cluster instead of the average. Sampling keeps large images fast, and a fixed seed makes repeated runs return the same result.

from sklearn.cluster import KMeans


def clustered_palette(pixels, n_colors=5, sample=50000, seed=0):
    rng = np.random.default_rng(seed)
    if len(pixels) > sample:
        pixels = pixels[rng.choice(len(pixels), sample, replace=False)]
    km = KMeans(n_clusters=n_colors, n_init=10, random_state=seed).fit(pixels)
    palette = []
    for label in range(n_colors):
        members = pixels[km.labels_ == label]
        if len(members) == 0:
            continue
        common = Counter(map(tuple, members)).most_common(1)[0][0]
        palette.append((common, len(members) / len(pixels)))
    return sorted(palette, key=lambda item: item[1], reverse=True)

Set n_colors to the number of colors you can see in the logo. If two swatches come out nearly identical, the value is too high. If a visible color is missing or merged with another, the value is too low.

Step 3: Convert to hex and save a swatch sheet

def to_hex(rgb):
    return "#{:02X}{:02X}{:02X}".format(*(int(c) for c in rgb))


def save_swatches(palette, path="palette.png", size=80):
    sheet = Image.new("RGB", (size * len(palette), size), "white")
    for i, (rgb, _) in enumerate(palette):
        sheet.paste(tuple(int(c) for c in rgb), (i * size, 0, (i + 1) * size, size))
    sheet.save(path)


pixels = load_logo_pixels("logo.png")
palette = exact_palette(pixels)
for rgb, share in palette:
    print(to_hex(rgb), f"{share:.1%}")
save_swatches(palette)

Reading colors directly from an SVG

If the logo is an SVG, its colors are stored as text, so you can read them without rendering the file. The function below collects hex values from fill, stroke and gradient stop-color attributes, from inline style attributes, and from embedded style elements. It counts how many times each color is declared, not how much area it covers. It also ignores rgb() notation, named colors such as red, and stylesheets linked from outside the file. If the result looks incomplete, open the SVG and check for those cases.

import re
import xml.etree.ElementTree as ET

HEX = re.compile(r"#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b")


def normalize_hex(value):
    value = value.upper()
    if len(value) == 4:
        value = "#" + "".join(ch * 2 for ch in value[1:])
    return value


def svg_colors(path):
    found = Counter()
    for el in ET.parse(path).iter():
        values = [el.get(attr) for attr in ("fill", "stroke", "stop-color", "style")]
        if el.tag.endswith("style"):
            values.append(el.text)
        for value in filter(None, values):
            found.update(normalize_hex(m) for m in HEX.findall(value))
    return found.most_common()

Gradients are defined by their stop colors, so a gradient logo has no single fill value. Record every stop, and note that the colors belong to a gradient rather than listing them as separate flat brand colors.

How to confirm the palette is correct

  • The number of swatches matches the number of distinct colors you can see in the logo.
  • No swatch is a mid-tone between two neighboring colors, such as a gray between black type and a white background. A swatch like that is an anti-aliasing artifact, so raise min_share or the alpha cutoff.
  • The background color does not appear in the palette unless it is part of the design.
  • Next to the logo, the swatch sheet shows each color as visibly the same.
  • Running the clustered method twice with the same seed gives the same hex values.
  • If a vector source exists, the hex values from the SVG match the raster results or explain where they differ. Where they disagree, the vector values take precedence.
  • Print values (CMYK or spot) come from brand guidelines or the original print file, not from the extracted screen colors.

Using the palette in logo delivery

Label the extracted palette for what it is: RGB and hex values for screen use, taken from a specific source file. When you rebuild or trace the logo as vector artwork, apply these values as solid fills on clean paths instead of sampling colors again from a traced result, because auto-tracing often creates extra near-duplicate colors. For print delivery, pair each screen color with its documented CMYK or spot specification. If no specification exists, note that gap in the hand-off rather than filling it with an estimate.