Color Theory

The Best Way to Extract Dominant Colors From a Logo

Median cut, octree and k-means were built to compress photographs. Point them at a logo and they return three whites and a gray. Here is what works instead.

Colorize6 min read
The Best Way to Extract Dominant Colors From a Logo

Reach for a color extraction library and you will almost certainly get median cut, usually through ColorThief or one of its ports. Feed it a photograph and it does a genuinely good job. Feed it a logo and it hands back something like this:

#FFFFFF #FAFAFA #EEF1F5 #A8BEDC

For a logo that is a solid blue mark on a white background. The blue is missing, and in its place is a washed out tint that appears nowhere in the file. This is not a bug in the library. It is the algorithm doing exactly what it was designed to do, on an input it was never designed for.

#What median cut optimizes for

Median cut comes from Paul Heckbert's 1982 work on displaying continuous tone images on hardware limited to 256 colors. The problem it solves is: given millions of pixels, choose N colors that minimize the total error when every pixel is mapped to its nearest chosen color.

The method is elegant. Put every pixel in one box in RGB space. Repeatedly take the box with the largest range along any axis, sort its pixels along that axis, and split it at the median. After N splits, average the pixels in each box. Octree quantization and k-means differ in mechanics but optimize the same objective.

Read that objective again with a logo in mind. Total error across all pixels. If 92% of your image is white background, then getting white exactly right is worth more to that objective than getting the brand color right at all. The algorithm is not confused. It is correctly answering a question you did not mean to ask.

Splitting at the median is what makes it acute. The first split divides the pixel population in half. When most of the population is one flat color, the first several splits are all spent carving up that flat color into slightly different versions of itself.

#Why photos and logos are opposite inputs

PhotographLogo
Distinct colorsHundreds of thousandsOften under ten, by intent
DistributionBroad and continuousA few huge spikes
BackgroundPart of the subjectUsually irrelevant
TransparencyNoneCommon and load bearing
EdgesSoft, everywhereHard, with anti-aliased ramps
What you wantA representative summaryThe exact declared values

The last row is the one that matters most. For a photograph there is no correct answer sitting in the file waiting to be found; a palette is a reasonable summary and reasonable is the goal. For a logo there is a correct answer. Someone chose those colors, wrote them in a brand guide, and would recognize them instantly. Returning an average of them is returning a wrong answer, not an approximate one.

#Four specific failure modes

#The background wins

Covered above, and it is the dominant failure. Any population weighted method spends its budget on whatever occupies the most pixels, and in a logo that is nearly always the background.

#Anti-aliasing outvotes the real color

Every diagonal and curve in a rasterized logo has a fringe of blended pixels between the mark and the background. On a small mark those blend pixels can outnumber the pure ones. Because they sit on a straight line in RGB space between the two real colors, they form a cluster that clustering algorithms happily discover and report as a color. That #A8BEDC above is exactly this: the midpoint of the blue and the white, elected by the pixels that belong to neither.

#Alpha gets thrown away

Most quantization libraries take RGB and have nowhere to put an alpha channel. Decode a transparent PNG naively and the transparent region arrives as whatever the decoder left in those bytes, frequently black. You then get black returned as a dominant brand color for a logo with no black in it.

The fix is not subtle but it does have to be explicit. Reject pixels below an alpha threshold before counting anything:

const { data, info } = await sharp(buffer)
  .ensureAlpha()
  .raw()
  .toBuffer({ resolveWithObject: true })

const counts = new Map()
let opaque = 0

for (let i = 0; i < data.length; i += 4) {
  if (data[i + 3] < 128) continue // transparent and semi-transparent
  const key = `${data[i]},${data[i + 1]},${data[i + 2]}`
  counts.set(key, (counts.get(key) || 0) + 1)
  opaque += 1
}

Rejecting rather than compositing is the right call. Compositing transparent pixels onto white invents colors the brand never chose, and compositing onto black invents different ones.

#Resizing invents colors

Nearly every pipeline downscales before analysis, for speed. For a photograph, bilinear or Lanczos resampling is correct and harmless. For a logo it is a way of manufacturing more of the blend pixels described above, and Lanczos in particular adds ringing that produces values slightly outside the original range.

If you downscale a logo, either use nearest neighbour, which preserves exact values at the cost of some aliasing, or downscale far less aggressively than you would a photo. A 256px logo analyzed at 256px is cheap.

#What works better

The structure that handles logos well inverts the assumption. Instead of clustering to find representative colors, count exact values and let the flat areas speak for themselves.

  1. Decode to raw RGBA without heavy resampling, and reject pixels below an alpha threshold.
  2. Count values with a small quantization step, not a large one. A step of four or eight collapses compression noise while keeping genuinely distinct colors apart. This is a histogram, not a clustering pass.
  3. Filter achromatics conditionally. White, near-white, near-black and desaturated grays are backgrounds, text and edges far more often than they are brand colors. Only apply the filter when chromatic candidates remain, or you will return nothing for a monochrome brand.
  4. Merge what is left perceptually. A histogram over quantized values still splits one real color across neighbouring buckets. Merging in a perceptually uniform space is what turns four near identical blues back into one, and the threshold has to sit above your bucket size. That interaction is the subject of The Complete Guide to Accurate Brand Color Palettes.
  5. Read the markup when the format has any. An SVG states its colors as numbers, including gradient stops that pixel counting cannot reach. See How to Extract Brand Colors From SVG Logos.

#Screenshots are the other case

Worth separating clearly, because the same word "extraction" covers both jobs.

A full page screenshot is much closer to a photograph than to a logo. Thousands of colors, broad distribution, photography and gradients in the hero, no meaningful transparency. Median cut is the right tool, and a flat histogram over a screenshot returns the background color of the page repeated in five slightly different tones.

The two also answer different questions. A logo palette answers "what colors did this brand choose". A screenshot palette answers "what does this page look like", which includes the stock photography and the third party embed. Both are useful. Reporting one as the other is how you end up telling somebody their brand color is the sky in their hero image.

That separation is the thing worth taking away, more than any specific algorithm. The color in a logo, the colors declared in a stylesheet and the colors visible in a screenshot are three different measurements of three different things. Averaging them into one list destroys the only information that made them useful. You can see what keeping them apart looks like in the brand color directory, or run a site through the extractor and compare the three groups it returns.

Taggedbrand colorslogoscolor theory
Share this articleXLinkedInReddit

Do you want API access?

Access our API to integrate color, brand & screenshot extraction into your app:

Brand Logos

Site Assets & Screenshot

Color Extraction & Grouping

Categorization & Company Details

Sign up for free

Keep reading

  • Engineering

    How to Extract Brand Colors From Modern CSS

    For about fifteen years you could collect every color a website used with three regular expressions. One for hex, one for rgb() and rgba(), one for hsl() and hsla(). Between them they matched…

  • Engineering

    How to Extract Brand Colors From SVG Logos

    An SVG is a text file with the colors written in it. Compared to sampling pixels out of a PNG, reading fill="#1F6FEB" out of some markup should be the easy case. In practice SVG logos are the single…

  • Color Theory

    The Complete Guide to Accurate Brand Color Palettes

    You point an extractor at a logo, ask for the brand palette, and get five colors back. Three of them are the same blue.