10

I try to get the most matching color name depending on an given hex-value. For example if we have the hex-color #f00 we've to get the colorname red.

'#ff0000' => 'red'
'#000000' => 'black'
'#ffff00' => 'yellow'

I use currently the levenshtein-distance algorithm to get the closest color name, works well so far, but sometimes not as expected.

For example:

'#0769ad' => 'chocolate'
'#00aaee' => 'mediumspringgreen'

So any ideas how to get the result closer?

Here's what I made to get the closest color:

Array.closest = (function () {

    // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript
    function levDist(s, t) {
        if (!s.length) return t.length;
        if (!t.length) return s.length;

        return Math.min(
            levDist(s.substring(1), t) + 1,
            levDist(t.substring(1), s) + 1,
            levDist(s.substring(1), t.substring(1)) + (s[0] !== t[0] ? 1 : 0)
        );
    }

    return function (arr, str) {
        // http://stackoverflow.com/q/11919065/1250044#comment16113902_11919065
        return arr.sort(function (a, b) {
            return levDist(a, str) - levDist(b, str);
        });
    };

}());

http://jsfiddle.net/ARTsinn/JUZVd/2/

Another thing is the performance! It seems that it there's somewehere a really big issue that makes this really slow (is it the algorithm?).

yckart
  • 32,460
  • 9
  • 122
  • 129
  • 1
    For more similar colors it would be better to use [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) colors instead. – Sirko Jun 18 '13 at 17:58
  • 1
    You could make the sort step a **lot** faster if you'd precalculate the distances before sorting. – Pointy Jun 18 '13 at 18:00
  • Also I'm not sure why you wouldn't use a simple cartesian distance computation. (Actually I guess I'd convert to an angular coordinate space and do the distance in HSL or HSV terns.) – Pointy Jun 18 '13 at 18:00
  • 1
    i would calc the diff of two colors by averaging the difference between each of the two color's on each R, G, and B channel. ex (3,4,5) and (10,14,18) have an avg diff of 10. – dandavis Jun 18 '13 at 18:01
  • I'll bet you find you have some problem colors that you can leave out of the list. I don't know your problem domain but, perhaps, less is more. If I put up a shade of green and you call it mint or light green or spring green, it might not matter. – Lee Meador Jun 18 '13 at 18:03
  • Yeah, good thoughts so far. @Pointy Can you give an example of how to *precalculate* it before `sort`? – yckart Jun 18 '13 at 18:13
  • @dandavis Whoops, I think that's the way I'll do it. Thanks for the explanation ;) – yckart Jun 18 '13 at 18:16
  • The method described by dandavis is the same approach I used in my answer. – Andrew Clark Jun 18 '13 at 18:24
  • To precalculate before sorting, just make a pass over the array where every element is transformed into an object: `{ val: value, dist: distance }`. Then your sort function can just access the distance values directly. (Of course, as was pointed out in the accepted answer, you don't really have to sort.) – Pointy Jun 18 '13 at 19:07

1 Answers1

12

Levenshtein distance isn't really appropriate here, because it will compare character by character for equality. You need to check each color separately, and you would want 79 to be much closer to 80 than 00.

The following seems to be a lot closer to what you want, with only minimal changes to your code:

Array.closest = (function () {
    function dist(s, t) {
        if (!s.length || !t.length) return 0;
        return dist(s.slice(2), t.slice(2)) +
            Math.abs(parseInt(s.slice(0, 2), 16) - parseInt(t.slice(0, 2), 16));
    }

    return function (arr, str) {
        return arr.sort(function (a, b) {
            return dist(a, str) - dist(b, str);
        });
    };
}());

Note that this will only give reasonable results when both s and t are 6-character color hex codes.

Your code is inefficient because you don't need to sort the entire array to get the closest color. You should instead just loop through the array and keep track of the shortest distance.

For example:

Array.closest = (function () {
    function dist(s, t) {
        if (!s.length || !t.length) return 0;
        return dist(s.slice(2), t.slice(2)) +
            Math.abs(parseInt(s.slice(0, 2), 16) - parseInt(t.slice(0, 2), 16));
    }

    return function (arr, str) {
        var min = 0xffffff;
        var best, current, i;
        for (i = 0; i < arr.length; i++) {
            current = dist(arr[i], str)
            if (current < min) {
                min = current
                best = arr[i];
            }
        }
        return best;
    };
}());

Note that after this change Array.closest() will return a single value rather than an array, so you will need to remove the [0] further down in your code.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • Wow, great! Thanks so far :-* *reasonable results when both s and t are 6-character color hex codes* No problem I convert 3-digit hex-colors to 6 ones :) – yckart Jun 18 '13 at 18:22
  • BTW: Pointy [mentioned](http://stackoverflow.com/questions/17175664/get-the-closest-color-name-depending-on-an-hex-color#comment24869664_17175664) that precalculating the distance before sorting makes it even faster?! – yckart Jun 18 '13 at 18:24
  • 1
    one thing to keep in mind is that the G channel is actually more important than the R and B channels.Also not every level step is the same at each of the 0-255 values, ex: 7-2 is a huge change but 132-127 is relatively minor. Screen against this by averaging the perceptually-corrected (NTSC) gray level diff with the avg rgb diff as computed above. I think it should be worth 25%, so add it into the rgb and div by 4 instead of 3. NTSC gray is computed as ((0.29*r)+(0.59*g)+(0.12*b)) / 3 in case you're wondering... – dandavis Jun 18 '13 at 18:42
  • @dandavis Sounds great, though I don't know how to implement that. However, thanks for your mentation, nonetheless :) – yckart Jun 18 '13 at 19:17