Skip to content

measuring distances between colours?

4 messages · Michael Friendly, John Fox, Kevin Wright

#
Just a quick note:  The following two versions of your function don't 
give the same results.  I'm not sure why, and also not sure why the
criterion for 'near' should be expressed in squared distance.

# version 1
rgb2col <- local({
     hex2dec <- function(hexnums) {
         # suggestion of Eik Vettorazzi
         sapply(strtoi(hexnums, 16L), function(x) x %/% 256^(2:0) %% 256)
     }
     findMatch <- function(dec.col) {
         sq.dist <- colSums((hsv - dec.col)^2)
         rbind(which.min(sq.dist), min(sq.dist))
     }
     colors <- colors()
     hsv <- rgb2hsv(col2rgb(colors))

     function(cols, near=0.25) {
         cols <- sub("^#", "", toupper(cols))
         dec.cols <- rgb2hsv(hex2dec(cols))
         which.col <- apply(dec.cols, 2, findMatch)
         matches <- colors[which.col[1, ]]
         unmatched <- which.col[2, ] > near^2
         matches[unmatched] <- paste("#", cols[unmatched], sep="")
         matches
     }
})

# version 2
rgb2col2 <- local({
       all.names <- colors()
       all.hsv <- rgb2hsv(col2rgb(all.names))
       find.near <- function(x.hsv) {
           # return the nearest R color name and distance
           sq.dist <- colSums((all.hsv - x.hsv)^2)
           rbind(all.names[which.min(sq.dist)], min(sq.dist))
       }
       function(cols.hex, near=.25){
           cols.hsv <- rgb2hsv(col2rgb(cols.hex))
           cols.near <- apply(cols.hsv, 2, find.near)
           ifelse(cols.near[2,] < near^2, cols.near[1,], cols.hex)
       }
})

# tests
 > rgb2col(c("#010101", "#EEEEEE", "#AA0000", "#00AA00", "#0000AA", 
"#AAAA00", "#AA00AA", "#00AAAA"))
[1] "black"         "gray93"        "darkred"       "green4"
[5] "blue4"         "darkgoldenrod" "darkmagenta"   "cyan4"
 > rgb2col2(c("#010101", "#EEEEEE", "#AA0000", "#00AA00", "#0000AA", 
"#AAAA00", "#AA00AA", "#00AAAA"))
[1] "#010101"       "#EEEEEE"       "darkred"       "green4"
[5] "blue4"         "darkgoldenrod" "darkmagenta"   "cyan4"
 >
On 5/31/2013 7:42 PM, John Fox wrote:

  
    
#
Hi Michael,

This has become a bit of a comedy of errors. 

The bug is in Kevin Wright's code, which I adapted, and you too in your
version, which uses local() rather than function() to produce the closure.
The matrix which.col contains character data, as a consequence of binding
the minimum squared distances to colour names, and thus the comparison
cols.near[2,] < near^2 doesn't work properly when, ironically, the distance
is small enough so that it's rendered in scientific notation. 

Converting to numeric appears to work:
+     all.names <- colors()
+     all.hsv <- rgb2hsv(col2rgb(all.names))
+     find.near <- function(x.hsv) {
+         # return the nearest R color name and distance
+         sq.dist <- colSums((all.hsv - x.hsv)^2)
+         rbind(all.names[which.min(sq.dist)], min(sq.dist))
+     }
+     function(cols.hex, near=.25){
+         cols.hsv <- rgb2hsv(col2rgb(cols.hex))
+         cols.near <- apply(cols.hsv, 2, find.near)
+         ifelse(as.numeric(cols.near[2,]) <= near^2, cols.near[1,],
cols.hex)
+     }
+ })
+     "#AAAA00", "#AA00AA", "#00AAAA"))

[1] "black"         "gray93"        "darkred"       "green4"        "blue4"
"darkgoldenrod"
[7] "darkmagenta"   "cyan4"

The same bug is in the code that I just posted using Lab colours, so (for
posterity) here's a fixed version of that, using local():
+     all.names <- colors()
+     all.lab <- t(convertColor(t(col2rgb(all.names)), from = "sRGB", 
+         to = "Lab", scale.in = 255))
+     find.near <- function(x.lab) {
+         sq.dist <- colSums((all.lab - x.lab)^2)
+         rbind(all.names[which.min(sq.dist)], min(sq.dist))
+     }
+     function(cols.hex, near = 2.3) {
+         cols.lab <- t(convertColor(t(col2rgb(cols.hex)), from = "sRGB", 
+             to = "Lab", scale.in = 255))
+         cols.near <- apply(cols.lab, 2, find.near)
+         ifelse(as.numeric(cols.near[2, ]) < near^2, cols.near[1, ],
toupper(cols.hex))
+     }
+ })
"#AAAA00", "#AA00AA", "#00AAAA"))

[1] "black"   "gray93"  "#AA0000" "#00AA00" "#0000AA" "#AAAA00"
[7] "#AA00AA" "#00AAAA"
"#AAAA00", "#AA00AA", "#00AAAA"), near=15)

[1] "black"         "gray93"        "firebrick3"    "limegreen"    
[5] "blue4"         "#AAAA00"       "darkmagenta"   "lightseagreen"

So with Lab colours, setting near to the JND of 2.3 leaves many of these
colours unmatched. I experimented a bit, and using 15 (as above) produces
matches that appear reasonably "close" to me.

I used squared distances to avoid taking the square-roots of all the
distances. Since the criterion for "near" colours, which is on the distance
scale, is squared to make the comparison, this shouldn't be problematic.

I hope that finally this will be a satisfactory solution.

Best,
 John
#
Dear Kevin,

When computer code is bug free, we'll probably all be out of business. Thank
you for improving my original code.

Best,
 John