31

I need to compute the difference between two hex color values so the output is a percentage value. The first thing I discarted was converting the hex value into decimal, as the first one will have much higher weight than the last.

The second option is to compute the difference between each of the RGB values and then add them all. However, the difference between 0, 0, 0 and 30, 30, 30 is much lower than the one between 0, 0, 0 and 90, 0, 0.

This question recommends using YUV, but I can't figure out how to use it to establish the difference.

Also, this other question has a nice formula to compute the difference and output a RGB value, but it's not quite there.

Community
  • 1
  • 1
metrobalderas
  • 5,180
  • 7
  • 40
  • 47
  • i found a good article about matching colors http://html5hub.com/exploring-color-matching-in-javascript/ – maersu Aug 09 '14 at 12:38

6 Answers6

38

For those just looking for a quick copy/paste, here's the code from this repo by antimatter15 (with some tweaks for ease of use):

function deltaE(rgbA, rgbB) {
  let labA = rgb2lab(rgbA);
  let labB = rgb2lab(rgbB);
  let deltaL = labA[0] - labB[0];
  let deltaA = labA[1] - labB[1];
  let deltaB = labA[2] - labB[2];
  let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
  let c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
  let deltaC = c1 - c2;
  let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
  deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
  let sc = 1.0 + 0.045 * c1;
  let sh = 1.0 + 0.015 * c1;
  let deltaLKlsl = deltaL / (1.0);
  let deltaCkcsc = deltaC / (sc);
  let deltaHkhsh = deltaH / (sh);
  let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
  return i < 0 ? 0 : Math.sqrt(i);
}

function rgb2lab(rgb){
  let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255, x, y, z;
  r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
  g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
  b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
  x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
  y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
  z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
  x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
  y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
  z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
  return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
}

To use it, just pass in two rgb arrays:

deltaE([128, 0, 255], [128, 0, 255]); // 0
deltaE([128, 0, 255], [128, 0, 230]); // 3.175
deltaE([128, 0, 255], [128, 0, 230]); // 21.434
deltaE([0, 0, 255], [255, 0, 0]); // 61.24

enter image description here

The above table is from here. The above code is based on the 1994 version of DeltaE.

  • Will comparing 2 colors based on their HSB values, instead of their RGB ones, yield better results? And if so, what would that function look like? :) – Kawd Oct 20 '21 at 18:42
  • This was so useful! Thanks for this! I used this as part of code to sort colors into color familes and if the delta < 10, the colors were grouped together as a family. One problem is that it didn't account for shades of gray very well, so I added a function to account for that before running my rgb array through these functions. ```const isItGrey = (rgbArr) => { const difference = Math.max(...rgbArr) - Math.min(...rgbArr); if (difference < 10) { return true; } else { return false; } };``` – kwicz Jul 29 '22 at 16:56
12

the issue is that you want something like a distance on a 3 dimensionnal world, but that rgb representation is not intuitive at all : 'near' colors can be much different that 'far' color.

Take for instance two shades of grey c1 : (120,120,120) and c2 : (150,150,150) and a now take c3 : (160,140,140) it is closer to c2 than c1, yet it is purple, and for the eye the darker grey is much closer to grey than a purple.

I would suggest you to use hsv : a color is defined by a 'base' color (hue), the saturation, and the intensity. colors having close hue are indeed very close. colors having very different hue do not relate one to another (expl : yellow and green ) but might seem closer with a (very) low saturation and (very) low intensity.
( At night all colors are alike. )

Since the hue is divided into 6 blocks, cyl = Math.floor( hue / 6 ) gives you the first step of your similarity evalution : if same part of the cylinder -> quite close. If they don't belong to same cylinder, they might still be (quite) close if (h2-h1) is small, compare it to (1/6). If (h2-h1) > 1/6 this might just be too different colors.

Then you can be more precise with the (s,v). Colors they are nearer if both low/very low saturation and/or low intensity.

Play around with a color picker supporting both rgb and hsv until you know what you would like to have as a difference value. But be aware that you cannot have a 'true' similarity measure.

you have a rgb --> hsv javascript convertor here : http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c

GameAlchemist
  • 18,995
  • 7
  • 36
  • 59
11

Just compute an Euclidean distance:

var c1 = [0, 0, 0],
    c2 = [30, 30, 30],
    c3 = [90, 0, 0],
    distance = function(v1, v2){
        var i,
            d = 0;

        for (i = 0; i < v1.length; i++) {
            d += (v1[i] - v2[i])*(v1[i] - v2[i]);
        }
        return Math.sqrt(d);
    };

console.log( distance(c1, c2), distance(c1, c3), distance(c2, c3) );
//will give you 51.96152422706632 90 73.48469228349535
user2226755
  • 12,494
  • 5
  • 50
  • 73
bjornd
  • 22,397
  • 4
  • 57
  • 73
  • have you read the comment by game alchemist? I think he points out some reasons your answer isn't very good. Basically further apart doesn't mean more distinct to the human eye. – xaxxon Apr 01 '15 at 23:55
  • 4
    Euclidean distance does not work in the RGB color space, but does for LAB color space. However, while the LAB space was meant for perceptual uniformity in perceived color, it falls short! So dE76 (euclidean distance, literally) isn't very accurate, particularly with saturation. de94 and de00 are better for accuracy. More info: http://zschuessler.github.io/DeltaE/learn/ – Zachary Schuessler Oct 01 '15 at 18:32
10

I released an npm/Bower package for calculating the three CIE algorithms: de76, de94, and de00.

It's public domain and on Github:

http://zschuessler.github.io/DeltaE/

Here's a quickstart guide:

Install via npm

npm install delta-e

Usage

// Include library
var DeltaE = require('delta-e');

// Create two test LAB color objects to compare!
var color1 = {L: 36, A: 60, B: 41};
var color2 = {L: 100, A: 40, B: 90};

// 1976 formula
console.log(DeltaE.getDeltaE76(color1, color2));

// 1994 formula
console.log(DeltaE.getDeltaE94(color1, color2));

// 2000 formula
console.log(DeltaE.getDeltaE00(color1, color2));

You will need to convert to LAB color to use this library. d3.js has an excellent API for doing that - and I'm sure you can find something adhoc as well.

Zachary Schuessler
  • 3,644
  • 2
  • 28
  • 43
2

The 3rd rule for color comparisons on ColorWiki is "Never attempt to convert between color differences calculated by different equations through the use of averaging factors". This is because colors that are mathematically close to each other aren't always visually similar to us humans.

What you're looking for is probably delta-e, which is a single number that represents the 'distance' between two colors.

The most popular algorithms are listed below, with CIE76 (aka CIE 1976 or dE76) being the most popular.

Each one goes about things in a different way, but for the most part they all require you to convert to a better (for comparison) color model than RGB.

Wikipedia has all the formulae: http://en.wikipedia.org/wiki/Color_difference

You can check your work with online color calculators:

Finally, it's not javascript but there's an open-source c# library I started will do some of these conversions and calculations: https://github.com/THEjoezack/ColorMine

Joe Zack
  • 3,268
  • 2
  • 31
  • 37
  • Hi, Joe. How come there is no conversion between radians and degrees in your c# library? such as `Math.Atan2(lab1.B, aPrime1) % 360;` is a radian value mod 360 deg, which really confused me... let me know what you think, thanks. – Mark Ni Jan 12 '14 at 03:03
1

Using Color.js:

let Color = await import("https://cdn.jsdelivr.net/npm/colorjs.io@0.0.5/dist/color.esm.js").then(m => m.default);
let color1 = new Color(`rgb(10,230,95)`);
let color2 = new Color(`rgb(100,20,130)`);
let colorDistance = color1.deltaE2000(color2);

A distance of 0 means the colors are identical, and a value of 100 means they're opposite.

deltaE2000 is the current industry standard (it's better than the 1994 version mentioned in top answer), but Color.js also has other algorithms like deltaE76, deltaECMC and deltaEITP.

joe
  • 3,752
  • 1
  • 32
  • 41