Extracting a color palette from an image with javascript
Tags: javascript, tutorial, webdev, beginners
Published: 2022-03-23 14:56:06

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxkd5xcgzafzjihr5n5g.JPG)

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

- [Demo app](https://zygiss22.github.io/color-palette-extraction/).
- [Repository](https://github.com/zygisS22/color-palette-extraction) (the entire codebase is **commented**).

#### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/omys9isjhkt3carj43ja.JPG)


There are several websites that provide this service such as [coolors.co](https://coolors.co/image-picker) or [canva.com](https://www.canva.com/colors/color-palette-generator/), 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
```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>
```

### 🚜 Extract image information

We load the image into the canvas using the event handler `.onload`, this allow us to access the [getImageData()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData) method from the canvas API.

index.js
```javascript
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);
    }
  }
}
```

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:

```json
{
  data: [133,38,51,255,133,38,255,120...],
  colorSpace: "srgb",
  height: 420,
  width: 320
}
```
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](https://en.wikipedia.org/wiki/RGBA_color_model) color model.

![data example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5r4wa4dfi767qu2kc8l.png)

### 🏗️ 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](https://en.wikipedia.org/wiki/RGB_color_model) mode instead of RGBA.

index.js
```javascript
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;
};
```
### 🎨 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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/Channel_(digital_image)) 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min) and [Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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
```javascript
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";
  }
};
```

#### 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
```javascript
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),
  ];
}
```

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
```javascript
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
}
```

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](https://en.wikipedia.org/wiki/Relative_luminance).
- Create [complementary](https://en.wikipedia.org/wiki/Complementary_colors) 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:

- Use [K-means algorithm](https://en.wikipedia.org/wiki/K-means_clustering) to create the color palette.
- Use [Octree algorithm](https://en.wikipedia.org/wiki/Octree) to implement the color palette.
- Watch this talk about color ["RGB to XYZ: The Science and History of Color" by John Austin](https://www.youtube.com/watch?v=AS1OHMW873s).
- Add different color combinations like [Monochromatic](https://en.wikipedia.org/wiki/Monochromatic_color) or [Triadic](https://en.wikipedia.org/wiki/Color_scheme#Triadic), check this [page](https://www.canva.com/colors/color-wheel/) for more examples.

### 👋 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlqptjm4k7kwph6on11n.gif)

###### (Cover photo by Zhang Xinxin on Unsplash)