Mapping Dominant Image Colors in Javascript

Photo of a cloud in the sky showing color breakdown

Written by Justin Levinsohn, Technical Director

On a recent project, a designer came to me and asked if it were possible to detect the dominant color in an image and map it to a predefined color palette. The use case was when a user uploaded an image of their company logo, the subsequent dashboard page would detect the dominant color and match that to the client's color palette to style the components on the page accordingly. This provides a subtle but eye-catching bit of personalization for a user and helps them feel the site is tailored for them.

Getting Started

In the past, I’ve explored using canvas to find a color within an image so my first thought was to visit Codepen to try to find an example. Sure enough, there were a few examples to use as a starting point. I chose to fork a pen by Danail Hadjiatanasov (@AtomicForce). The code is pretty straightforward: it creates a canvas, adds the image to the canvas, and moves along the image every 5 pixels to assess the RGB values to define a dominant color.

Digging In

Having that code as my base, I tried to map the dominant RGB values to the closest match in the predefined set of colors. I set up an array of RGB values of my defined palette and an identifying name.

const colorPaletteOptions = [
  {
    name: "theme--gray",
    r: 70,
    g: 90,
    b: 105
  },
  {
    name: "theme--blue",
    r: 0,
    g: 91,
    b: 148
  }
  // etc...
];
colorPaletteOptions.js

Next, I use the code from below to iterate over the images on the page and find the dominant color of each, running the 'getAverageRGB' function on each image.

imagesArray.forEach((image) => {
  const color = getAverageRGB(image);
  const imageParent = image.closest(`.${CLASSES.IMAGE}`);

  imageParent.classList.add(color.name);
});
getAverageRGB.js

After this step, I faced the biggest challenge of this proof of concept. RGB values are difficult to use to find the closest match to a different color. The RGB color model is often depicted as a cube by mapping the red, green, and blue dimensions onto the x, y, and z axis in 3D space thus making it not an especially intuitive model for creating colors in code. Humans do not think about colors as mixes of red, green, and blue lights. In my instance, the R values may be close to matching an existing palette color, but if the G and B values aren’t then the colors will be vastly different. Comparing each of the R, G, and B values and their proximity to each R, G, and B value of a defined color seemed too difficult and fraught with potential inaccuracy.

HSL FTW

Fortunately, when working with color on the web, we can also use HSL (hue, saturation, lightness) values, specifically, the hue value. HSL is a cylindrical color model that remaps the RGB primary colors into dimensions that are easier for humans to understand. This provides an easier way to calculate the proximity of the color to a close match.

Since I didn’t have time to write my own RGB to HSL conversion, I did some searching and found https://css-tricks.com/converting-color-spaces-in-javascript/. I used the function for RGB to HSL found here for my proof of concept. Finally, I had all of the numbers necessary to find my match.

Finishing It Off

The final piece of color matching code loops through the predefined color palette, converts the RGB values into HSL, finds the difference between the hue value of the dominant image color and the palette colors to find the closest match. The color with the smallest difference is the best match. After that I simply add the palette name as a class to an element in the DOM.

colorPaletteOptions.forEach((color, index) => {
    const palletHSL = RGBToHSL(color.r, color.g, color.b);

    colorDifference = Math.abs(hslColor[0] - palletHSL[0]);

    if (colorDifference < minDiff) {
      minDiff = colorDifference;
      currentOption = index;
    }
  });
findClosestMatch.js

Final Thoughts

For quick proofs of concept like this, I think it’s useful to leverage code others have written (legally, of course) from sites like Stack Overflow, CSS Tricks, or Codepen. It helps get quickly from, “is this possible” to “yes, this can be done!” Once the challenge has been proven and you start writing production code, you can decide to write your own version or appropriately credit the original author.

(Note, the images in the markup are in base64 because I couldn’t figure out how to upload images within Codepen to allow an image to be used in canvas from their CDN. If you try this locally or on your own servers, you can better define image permissions to allow access for canvas while using a standard image src definition in the markup).

Related Reading