Clasificar colores

Si estás buscando un algoritmo “mágico” que clasifique colores en esta entrada no lo vas a encontrar, más bien vas a descubrir lo complicado que es un tema que de primeras parece muy simple y posibles alternativas para clasificar colores.

La primera pregunta que tenemos que hacernos es ¿Cuántos colores hay?. Lo más probable es que los que están acostumbrados a trabajar con colores en el ordenador digan “unos dieciséis millones” y es cierto. Pero sería duro ponerle un nombre a cada uno así que seamos un poquito menos quisquillosos y pensemos como agruparlos. Cómo vamos a usar el sistema RGB podemos empezar por los colores que no sean mezcla de ningún otro: rojo, verde y azul. Luego los que son mezcla de otros dos colores: amarillo, cían, magenta. Por último los que son mezcla de los tres: blanco, gris y negro.

Los colores “puros” en RGB serían:

rojo (255,0,0)
verde (0,255,0)
azul (0,0,255)

amarillo (255,255,0)
cian (0,255,255)
magenta (255,0,255)

gris (192,192,192)
blanco (255,255,255)
negro (0,0,0)

Pero no basta con esos colores, claramente hay más: ocre, marrón, turquesa, naranja, morado, lila, malva, rosa…No hay una definición exacta de cuántos colores hay, ni siquiera de que significa cada nombre, algunos colores se solapan. Tanto que directamente a los colores entre los dos algunos les llaman cosas como verde-amarillos, rojo-naranjas,….

Pero aunque nos parezca que los colores son algo bastante universal no es así. Hay influencias culturales que hacen que algunos colores caigan a un lado u otro de la frontera, colores que son verdes o azules dependiendo de la cultura de donde sea el que los ve.

También hay factores fisiológicos que hacen que veamos los colores de forma diferente. No hay más que recordar alguna discusión en internet sobre de qué color es algo que aparece en una foto.

Y por último nuestra percepción de un color puede cambiar por influencia de los colores que lo rodean. Tomas un verde-amarillo lo pones junto a los verdes y lo ves amarillo, lo juntas con estos y lo ves verde. Tan sorprendente como frustrante.

Conociendo todas estas pegas y sabiendo que acertar al 100% es difícil por lo que no hay una solución ideal vamos a ver algunas soluciones.

Usar colores de referencia

Lo primero que se nos ocurre cuando hablamos de clasificar colores es tomar unos colores representativos como referencia. Cuando queramos clasificar un color medimos la distancia a cada uno de estos colores de referencia y elegimos el más cercano. Básicamente es un algoritmo del vecino más cercano. Ya hemos visto como calcular la diferencia entre dos colores, solo nos queda elegir los colores de referencia.

El problema es que no hay un espacio de color con una frontera clara y definida por lo que hace falta una gran cantidad de puntos de referencia. Generar ese listado de puntos es costoso, tiene que ser generado por humanos y resolver las diferencias de opinión entre ellos. Cómo creo que no tenemos recursos para hacer eso y tenemos que trabajar con tamaños de muestra muy pequeños el funcionamiento no es el ideal.

Espacio HSL

Una ventaja del espacio HSL (matiz, saturación, luminosidad) es que es relativamente fácil de saber el color. Basta con fijarse el valor del componente H o matiz, que viene expresado en grados o radianes. Con él ya puedes distinguir entre varios colores. Los más habituales son:

  • 0 rojo
  • 30 naranja
  • 60 amarillo
  • 120 verde
  • 180 cian
  • 240 azul
  • 300 magenta
  • 330 rosa
  • 360 rojo

La componente L el indica “la luminosidad” del color. Viene expresada en %. El 50% es el tono puro del color. Por encima los colores son cada vez más claros y por debajo más oscuros. Si es muy bajo esta cerca de color negro y si es muy claro lo está del blanco. Cómo referencia podemos usar estos valores:

  • 0, 2 negro
  • 3, 8 casi negro
  • 9, 39 oscuro
  • 40, 60 [nada]
  • 61, 91 claro
  • 92, 97 casi blanco98,
  • 100 blanco

El componente S indica la saturación del color, está expresado en %. Un valor muy bajo indica que está cercano al gris:

  • 0-2 gris
  • 3-10 casi gris
  • 10-25 grisáceo
  • 25-50 pálido

Aún así hay colores problemáticos como el marrón que surge de algunos rojos o narajas (o rojo-naranjas) muy oscuros. O los tono pastel que la mayoria surgen cuando la saturación y la luminosidad estan entre el 70% y el 85%.

Lista de colores

Hay otra opción, usar un listado de colores y sus nombres, buscar el más cercano y usarlo como respuesta. Aunque usar términos como “verde menta”, “amarillo eléctrico”, “rosa pastel” nos parezca menos exacto que los métodos anteriores, para los seres humanos resulta más fácil de entenderlos y muy intuitivos.

El principal problema es que no hay un estándar de que es el color “verde menta” puedes encontrar montones de listados y en cada uno puede un valor distinto o el mismo color llamarse de otra manera.

Las listas de colores se pueden conseguir de catálogos de pinturas, de la Wikipedia o de estándares como por ejemplo los colores web.

¿Por cual optar?

Si necesitar clasificar el color dentro de una lista cerrada de los mismos lo mejor es el primer método.

Si necesitas clasificar cualquier color dentro del espacio de colores sin tener colores de referencia.

La tercera resulta útil para mostrar resultado a los humanos.

Calcular la diferencia entre dos colores

Decidir si dos colores se parecen puede parecer sencillo, basta con medir la distancia entre ambos. En formato RGB la distancia entre dos colores es:

SQRT((R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2)

Desgraciadamente no funciona demasiado bien en el espacio de color RGB, que es el que se usa habitualmente en los ordenadores , por lo que tenemos que usar otro espacio. Tenemos varias alternativas las más prometedoras son:

Vamos a apostar por el último que suele ser el que mejor funciona. Esta ideado para representar el espacio de color de forma próxima a como el ojo humano los percibe.

El primer problema es que no hay una conversión directa RGB a Lab. La solución es pasar de RGB a XYZ y luego de XYZ a Lab. En esas conversiones se pierde algo de precisión pero el resultado es lo suficientemente exacto.

Debajo está el código en JavaScript para realizar la conversión de RGB a LAB

function RGBtoLAB(r,g,b){
    //RGBtoXYZ
    var x = RGBtoXYZ_RtoX[r] + RGBtoXYZ_GtoX[g] + RGBtoXYZ_BtoX[b];
    var y = RGBtoXYZ_RtoY[r] + RGBtoXYZ_GtoY[g] + RGBtoXYZ_BtoY[b];
    var z = RGBtoXYZ_RtoZ[r] + RGBtoXYZ_GtoZ[g] + RGBtoXYZ_BtoZ[b];

    if (x > 0.008856)
        x = Math.cbrt(x);
    else
        x = (7.787 * x) + 0.13793103448275862;

    if (y > 0.008856)
        y = Math.cbrt(y);
    else
        y = (7.787 * y) + 0.13793103448275862;

    if (z > 0.008856)
        z = Math.cbrt(z);
    else
        z = (7.787 * z) + 0.13793103448275862;

    L = (116 * y) - 16;
    a = 500 * (x - y);
    b = 200 * (y - z);

    return [L,a,b];
}

RGBtoXYZ_RtoX = [];
RGBtoXYZ_GtoX = [];
RGBtoXYZ_BtoX = [];
RGBtoXYZ_RtoY = [];
RGBtoXYZ_GtoY = [];
RGBtoXYZ_BtoY = [];
RGBtoXYZ_RtoZ = [];
RGBtoXYZ_GtoZ = [];
RGBtoXYZ_BtoZ = [];

for(var i = 0; i < 256; i++){  //i from 0 to 255
    r = parseFloat(i/255) ;    //r from 0 to 1

    if (r > 0.04045 )
        r = Math.pow((r+0.055)/1.055 ,  2.4);
    else
        r = r/12.92;

    r = r * 100

    var ref_X =  95.047;
    var ref_Y = 100.000;
    var ref_Z = 108.883;

    RGBtoXYZ_RtoX[i] = r * 0.4124/ref_X;
    RGBtoXYZ_GtoX[i] = r * 0.3576/ref_X;
    RGBtoXYZ_BtoX[i] = r * 0.1805/ref_X;
    RGBtoXYZ_RtoY[i] = r * 0.2126/ref_Y;
    RGBtoXYZ_GtoY[i] = r * 0.7152/ref_Y;
    RGBtoXYZ_BtoY[i] = r * 0.0722/ref_Y;
    RGBtoXYZ_RtoZ[i] = r * 0.0193/ref_Z;
    RGBtoXYZ_GtoZ[i] = r * 0.1192/ref_Z;
    RGBtoXYZ_BtoZ[i] = r * 0.9505/ref_Z;
}

En el código se usan algunas optimizaciones como usar tablas de consulta para acelerar los cálculos (RGBtoXYX_*).

Los nuevos valores obtenidos ya se pueden comparar usando la distancia euclídea.

SQRT((L1-L2)^2 + (a1-a2)^2 + (b1-b2)^2)

El resultado indica la proximidad entre dos colores, lo parecidos que son. Como referencia un resultado menor de 4 quiere decir que la diferencia entre colores apenas es perceptible a simple vista. Eso no quiere decir que este valor solo se pueda usar para saber si dos colores son iguales, también para agrupar colores similares, encontrar un color en una imagen pese a variaciones de la iluminación (buscando el color más parecido) ,…

Veamos un ejemplo, unos de los puntos débiles más fáciles de ver del espacio de color RGB son los grises. Comparamos el gris medio (128,128,128) con otros dos colores: gris oscuro (28,28,28) y dorado oscuro (128,128,0). Veamos cual de los dos sistemas es más exacto.

RGBLab
Gris oscuro173,2043,31
Dorado Oscuro12858,16
Distancias según el espacio de color que se use

Usando RGB el dorado oscuro está más cerca del gris medio que el gris oscuro, mientras que Lab da el resultado correcto.

Convertir de escala de grises a RGB/RGBA

Nos va a pasar que en muchos casos tras aplicar varios algoritmos a una imagen terminamos con una versión en escala de grises con un canal de 8 bits por pixel. Pero para visualizarla hemos de convertirla en RGB o en RGBA. ¿Como hacemos para que se siga viendo el mismo nivel de gris? Afortunadamente la respuesta es muy sencilla, damos a los tres canales RGB el valor del único canal de escala de grises.

//Red
r = gs;
//Green
g = gs;
//Blue
b = gs;
//Alpha
a = 0;

Convertir RGB a escala de grises

Una de las operaciones más habituales que se hace en visión por computador es convertir una imagen a color a una en escala de grises. Es una operación bastante simple pero hay múltiples opciones para real izarlo. Todas se basan en lo mismo, combinar los valores de los tres canales RGB para obtener un solo canal. Aunque hay distintos formatos, para este post voy a poner el caso de que los pixeles en color están en codificados como RGB con un byte por cada canal y que la información en escala de grises emplead un soo canal de un byte. Como vemos hemos de reducir tres bytes a solo uno.

Vamos usar una función que nos servirá como para la mayoría de los casos, en ella se le pasan los tres canales y los pesos asignados a cada uno, se multiplica cada canal por su correspondiente peso y se suman. Al ser un ejemplo no se realiza ningún tipo de verificación de los valores que se pasan ni del resultado.

function RGBtoGS(r,g,b,kr,kg,kb){
 return kr*r + kg*g + kb*b;
}

El caso más sencillo y rápido es tomar el valor de uno solo de los canales. Esto se puede usar cuando uno de los canales tiene más información o sufre menos el ruido. Hay que tener en cuenta que se pierde la información de los otros dos canales.

RGBtoGS(r,g,b,1,0,0); //devolver el canal rojo como gris
RGBtoGS(r,g,b,0,1,0); //devolver el canal verde como gris
RGBtoGS(r,g,b,0,0,1); //devolver el canal azul como gris

Una variación de esta técnica consiste en no coger siempre el mismo canal si no en coger el más o el menos brillante. Tienen la ventaja de ser rápido y de aportar más información que el caso anterior. Si la imagen es muy oscura (subexpuesta) elegir el canal más brillante puede ayudar a sacar detalles que de otra forma quedan ocultos en las zonas oscuras, si es demasiado luminosa (sobreexpuesta) el menos brillante puede aportar información que de otra manera quedaría quemada. Y ya que estamos podemos optar por el punto medio y calcular la media de la suma entre el canal más y el menos luminoso de cada píxel.

function greaterRGBtoGS(r,g,b){
  if(r &gt; g &amp;&amp; r &gt; b){
    return r;
  } else if(g &gt; r &amp;&amp; g &gt; b){
    return g;
  } else{
    return b;
  }
}
function lesserRGBtoGS(r,g,b){
if(r &lt; g &amp;&amp; r <b> r &amp;&amp; g &gt; b){
    return g;
  }else{
    return b;
  }
}

function averageRGBtoGS(r,g,b){
  return (greaterRGBtoGS(r,g,b)+lesserRGBtoGS(r,g,b))/2
}

Sin embargo la manera más habitual de hacerlo es ponderar los tres valores con tres constantes. La forma más fácil que se nos ocurre es simplemente calcular la media de los tres valores (o multiplicar cada uno por 0.33). Pero esta solución no es acorde con la realidad en la que por diversos motivos (desde físicos a biológicos como que nuestros ojos no son igual de sensibles a cada componente) cada canal no aporta lo mismo. Para ello existen distintas recomendaciones de que valores usar para ponderar cada canal. Incluyo algunos ejemplos.

RGBtoGS(r,g,b,0.33,0.33,0.33); //media
RGBtoGS(r,g,b,0.2126,0.7152,0.0722); //CIE 1931
RGBtoGS(r,g,b,0.299,0.587,0.114); // rec601 luma
RGBtoGS(r,g,b,0.2627,0.6780,0.593); // ITU-R BT.2100

Aunque lo recomendado es ponderar los tres canales, desde mi limitada experiencia el de máximo brillo me ha dado buenos resultados sobre todo con imágenes obtenidas con cámaras de móviles y portátiles. Además tiene la ventaja de ser muy rápido.