DEV Community

Cover image for Extracting a color palette from an image with javascript
Zygimantas Sniurevicius for Product Hackers

Posted on • Edited on

Extracting a color palette from an image with javascript

Introduction

Today I am bring you something really interesting that I think is worth sharing. Let me begin by showcasing the end result.

Project Image

If you can’t wait and want to test it yourself, here are the links to the app demo and the repository.

Explanation

We can load any image and extract a color palette and every color is accompanied by its opposed color (complementary).

Example of a similar technique can be found in Spotify, when you navigate to a song/playlist or album you get a custom color gradient on top that represents the dominant color of the picture, this gradient adds a unique feel to each page and it's actually the reason why I am doing this post.

Spotify example

There are several websites that provide this service such as coolors.co or canva.com, if you ever wondered how does it work you are in the correct place, let's find out.

📝 Steps

Now that we know what we are dealing here, let’s start by explaining the process:

  1. Load an image into a canvas.
  2. Extract image information.
  3. Build an array of RGB colors.
  4. Apply Color quantization.
BONUS TRACK
  • Order colors by luminance.
  • Create a complementary version of each color.
  • Build the HTML to display the color palette.

🖼️ Load an image into a canvas

First we create the basic HTML of our page, we need a form input of type file to upload the image and a canvas element because that’s how we gain access to the image’s data.

index.html



<form action="#">
 <input type="file" id="imgfile" />
 <input type="button" id="btnLoad" value="Load" onclick="main();" />
</form>
<canvas id="canvas"></canvas>
<div id="palette"></div>
<div id="complementary"></div>


Enter fullscreen mode Exit fullscreen mode

🚜 Extract image information

We load the image into the canvas using the event handler .onload, this allow us to access the getImageData() method from the canvas API.

index.js



const main = () => {
  const imgFile = document.getElementById("imgfile");
  const image = new Image();
  const file = imgFile.files[0];
  const fileReader = new FileReader();

  fileReader.onload = () => {
    image.onload = () => {
      const canvas = document.getElementById("canvas");
      canvas.width = image.width;
      canvas.height = image.height;
      const ctx = canvas.getContext("2d");
      ctx.drawImage(image, 0, 0);

      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

The information returned from getImageData() represents all the pixels that compose the image, meaning that we have an humongous array of values in the following format:



{
  data: [133,38,51,255,133,38,255,120...],
  colorSpace: "srgb",
  height: 420,
  width: 320
}


Enter fullscreen mode Exit fullscreen mode

Each value inside data represents a channel of a pixel R (red), G (Green), B (Blue) and A (Alpha), Every four elements of the data array form the RGBA color model.

data example

🏗️ Build an array of RGB colors

Immediately after obtaining the image data we have to parse it to something more readable, this will make our live easier in the future.

We loop through the image data every four elements and return an array of color objects in RGB mode instead of RGBA.

index.js



const buildRgb = (imageData) => {
  const rgbValues = [];
  for (let i = 0; i < imageData.length; i += 4) {
    const rgb = {
      r: imageData[i],
      g: imageData[i + 1],
      b: imageData[i + 2],
    };
    rgbValues.push(rgb);
  }
  return rgbValues;
};


Enter fullscreen mode Exit fullscreen mode

🎨 Color quantization

After building the rgb colors array we need to somehow know which colors are the most representative of the image, to obtain this we use color quantization.

Wikipedia describes color quantization as

A process that reduces the number of distinct colors used in an image, usually with the intention that the new image should be as visually similar as possible to the original image.

Median cut algorthm

To achieve color quantization we are gonna use an algorithm called median-cut, the process is the following:

  1. Find the color channel ( red, green or blue) in the image with the biggest range.
  2. Sort pixels by that channel.
  3. Divide the list in half.
  4. Repeat the process for each half until you have the desired number of colors.

It sounds easy but it is a little bit complex, so I am gonna try my best to explain the code below.

Let's begin by creating a function that finds the color channel with the biggest range.

Initialize the min rgb values to the maximum number and the max rgb values to the minimum, this way we can determine what is the lowest and highest accurately.

Then, loop through every pixel and compare it with our current values using Math.min and Math.max.

Subsequently, we check the difference between every channels min and max results and return the letter of the channel with the biggest range.

index.js



const findBiggestColorRange = (rgbValues) => {
  let rMin = Number.MAX_VALUE;
  let gMin = Number.MAX_VALUE;
  let bMin = Number.MAX_VALUE;

  let rMax = Number.MIN_VALUE;
  let gMax = Number.MIN_VALUE;
  let bMax = Number.MIN_VALUE;

  rgbValues.forEach((pixel) => {
    rMin = Math.min(rMin, pixel.r);
    gMin = Math.min(gMin, pixel.g);
    bMin = Math.min(bMin, pixel.b);

    rMax = Math.max(rMax, pixel.r);
    gMax = Math.max(gMax, pixel.g);
    bMax = Math.max(bMax, pixel.b);
  });

  const rRange = rMax - rMin;
  const gRange = gMax - gMin;
  const bRange = bMax - bMin;

  const biggestRange = Math.max(rRange, gRange, bRange);
  if (biggestRange === rRange) {
    return "r";
  } else if (biggestRange === gRange) {
    return "g";
  } else {
    return "b";
  }
};


Enter fullscreen mode Exit fullscreen mode

Recursion time

Now that we have the component with the biggest range of colors in it (R, G or B), sort it and then split it by half, using the two halves we repeat the same process and call the function again, each time adding a value to depth.

index.js



const quantization = (rgbValues, depth) => {
  // base code goes here

const componentToSortBy = findBiggestColorRange(rgbValues);
  rgbValues.sort((p1, p2) => {
    return p1[componentToSortBy] - p2[componentToSortBy];
  });

  const mid = rgbValues.length / 2;
  return [
    ...quantization(rgbValues.slice(0, mid), depth + 1),
    ...quantization(rgbValues.slice(mid + 1), depth + 1),
  ];
}


Enter fullscreen mode Exit fullscreen mode

As for the base case, we enter it when our depth is equal to the MAX_DEPTH, in our case 4, then add up all the values and divide by half to get the average.

Note: Depth in this case means how many colors we want by power of 2.

index.js



const quantization = (rgbValues, depth) => {

 const MAX_DEPTH = 4;
  if (depth === MAX_DEPTH || rgbValues.length === 0) {
    const color = rgbValues.reduce(
      (prev, curr) => {
        prev.r += curr.r;
        prev.g += curr.g;
        prev.b += curr.b;

        return prev;
      },
      {
        r: 0,
        g: 0,
        b: 0,
      }
    );

    color.r = Math.round(color.r / rgbValues.length);
    color.g = Math.round(color.g / rgbValues.length);
    color.b = Math.round(color.b / rgbValues.length);
    return [color];
  }
  // recursion code goes below
}


Enter fullscreen mode Exit fullscreen mode

This is it, we are done with median-cut and the palette extraction.

📑 Extra steps

There are a lot of things that we could do here but i don't want to abuse of your precious time, if you are interested in expanding a little bit the scope of the project, check the repository, it contains all the extra code.

  • Order colors by luminance. There are different ways of doing this, depending of your needs, here we use the relative luminance.
  • Create complementary version of each color.
  • Build the HTML to display the color palette.

🗃️ Resources

If you want to go further into the topic I suggest trying different algorithms to create the color palette, find the dominant dolor, understand how color spaces work or add different color schemes, here are some examples to help you out:

👋 Final remarks

Thank you for your time, I hope you enjoyed this article and have learned something along the way, have a nice day :)

Bob Ross goodbye

(Cover photo by Zhang Xinxin on Unsplash)

Top comments (13)

Collapse
 
pzh20 profile image
Pete Harrison

I'm trying to work out how to return the number of occurrences of each returned colour as well as the sorted value. I can't quite see how to do it when using recursive algorithms. Can anyone suggest a possibility?

Many thanks

Collapse
 
tr11 profile image
Tiago Rangel

very helpful! If only the function was faster :)

Collapse
 
devvsakib profile image
Sakib Ahmed

Amazing tutorial

Collapse
 
undead34 profile image
Gabriel Maizo

How well explained, thank you.

Collapse
 
youssefa9 profile image
Youssef Ahmed

Would you suggest to process the image at the client side or the server side

Collapse
 
zygiss22 profile image
Zygimantas Sniurevicius

Its something you should want to have separated from your main services since its really heavy in the processing, it can take a while, for small tests like the one I did here it doesn't matter but for bigger apps take it into consideration.

if you try to use it with higher resolution images it will take several seconds, during that time your client will be blocked.

Cheers

Collapse
 
dannyc123 profile image
Danny Cummings

Cover art from Music Albums tends to come in 300x300 max size. So I think this was intended for smaller image sizes.

Thinking outloud...
Larger images, perhaps user interaction should initiate the action to process the image to extract a palette for a specific purpose. If not in a flow of user interaction, I would question why the image needs to be so large. You could show the user a large image and process the palette from the same image, scaled down significantly, in the background. If you need something to automagically happen based on the user seeing a rather large image, with performance. IDK, just a thought, unless there is a faster way to retrieve the palette from an image. I am always about performance, even if just for 300x300 images in my scenario :)

Collapse
 
viktor1953 profile image
Viktor1953

Great job! Thank you very much!

Collapse
 
capscode profile image
capscode

Amazing article.
Thank you for this.

Collapse
 
quocbahuynh profile image
Quoc Huynh Website

very helpful <3

Collapse
 
geraldo916 profile image
Geraldo Munhika

Thank you for sharing this. That's will help me finish my project.

Collapse
 
iceflower profile image
Iceflower • Edited

There is a small issue.

rgbValues.length === 0

You still return RGB(0, 0, 0) as average, even if there was no color. I think it is better to return an empty array in that case.