Brillo y contraste. Ajuste automático.

El ajuste del brillo y del contraste se usa para mejorar la visibilidad de la imagen. El brillo añade o reduce luminosidad a la imagen y el contraste aumenta o disminuye la distancia entre los valores. No vamos a complicarnos la vida. La fórmula para ajustar el brillo y el contraste de una imagen es muy sencilla, es la unión de dos formulas. Como ya os imaginareis, la de ajuste del brillo y la de ajuste del contraste

Formula para ajustar el brillo de la imagen:

newPixelValue = pixelValue+b;

Sumando a cada pixel un valor lo hacemos más brillante, si el valor es positivo, o más oscuro, si es negativo.

Formula para ajuste del contraste:

newPixelValue = c * (pixelValue-128) + 128

Combinando ambas tenemos una formaula que nos permite ajusta ambos valores:

newPixelValue = c * (pixelValue-128) + 128 + b

Siendo el parámetro c el contraste y b el brillo. Sólo hay que tener en cuenta las diferencias entre la estructura de datos de color y de escala de grises y tenemos el código listo.

Pero como nos encanta complicarnos la vida vamos a intentar que el ajuste del brillo y el contraste sea automático. ¿Como?. Supongamos que una imagen correctamente balanceada «ocupa» todo el rango de valores de 0 a 255. Vamos a «estirar» los valores de la imagen para que el mínimo sea el 0 y el máximo el 255. Es tan «sencillo» como buscar el valor más bajo y más alto que hay en la imagen. Ahora seleccionamos los valores contraste y brillo que hacen que el mínimo valor sea 0 y el máximo 255. Suena lógico, ¿cómo lo hacemos?. ¡Resolviendo ecuaciones!. Contened vuestra emoción.

Vamos a hacer que el mínimo sea la variable low y el máximo high. Tenemos estas dos ecuaciones:

(c*(high-128))+128+b = 255

(c*(low-128))+128+b = 0

Resolvemos el valor de b en la que es igual a 0. Podríamos despejarlo de cualquier otra forma pero esta me ha parecido la más fácil.

b = 128*c - low*c - 128

Sustituimos la b en la segunda ecuación:

c = 255/(high-low)

Bueno, ¿de donde sacamos estos valores low y high? Es tan sencillo como recorrer la imagen comparando los valores de sus pixels para  encontrar el mínimo y el máximo.

Con las imágenes en escala de grises es sencillo pues solo hay un valor maximo y uno minimo. Pero con las de color hay que tomar una decisión. ¿Ajustamos los tres canales de color de forma independiente calculando los valores b y c para cada uno o usamos los mismos para todos los canales?.

Si ajustamos todos los canales con los mismos valores de b y c hay dos opciones:

  • Convertir cada pixel a escala de grises, calcular el mínimo y el máximo y luego los correspondientes b y c
  • Calcular el máximo y mínimo de cada canal y calcular el b y c de aquel que tenga mayor distancia entre el valor máximo y mínimo (high -low). Asi nos aseguramos que coger el canal que «menos hay que estirar».

¿Cual es el problema ajustar cada color por separado? Que al estirar cada canal por separado la relación entre los colores cambia cambiando el color y no sólo el contraste y la intensidad. Por ejemplo si el rango de cada canal es:

Rojo: 0-255

Verde: 10-230

Azul: 100-150

Tras aplicar nuestro ajuste todos los canales tendrán un rango de 0-255. Así que mientras que el canal rojo no ha cambiado el azul ha variado de forma desproporcionada de tal forma que un píxel RGB con valores (230, 230, 100) pasa a ser tras el ajuste (230, 255, 0). Mientras el canal R y G casi no varía el B pasa de 100 a 0. Estos cambios alteran el color. Puede dar resultados muy artísticos pero poco naturales.

Todos estos cálculos se pueden realizar usando tablas de consulta calculando el valor para cada uno de los posibles valores de cada pixel (0…255)

Evitar el ruido

Vamos a la siguiente vuelta de tuerca. ¿Qué pasa si el valor mínimo y el máximo sólo corresponden a unos pocos píxeles no representativos de la imagen? Al aplicar el ajuste de brillo y contraste podríamos hacer más mal que bien y dejar la imagen peor. ¿Pero ese caso se puede dar? Resulta que «píxeles con valores extremos no representativos de la imagen» describe muy bien cierto tipo de ruidos como el ruido de sal y pimienta. Como siempre la realidad resulta ser menos bonita que la bella teoría. Por suerte hay solución. En lugar de coger los valores mínimos y máximos vamos a descartar una porcentaje pequeño de los valores más altos y más bajos. Por ejemplos vamos a descartar el 5% de los pixeles con los valores más bajos y el 5% con los valores más altos. En caso de que la iamgen esté muy deteriorada por el ruido estos valores pueden ser mayores

Así que hemos de sumar cuantos pixeles de cada valor hay en la imagen. Un momento, eso me suena, huy si, es el histograma y ya lo tenemos hecho. Sólo hemos de ir sumando el número de píxeles de cada entrada del array del histograma hasta que el acumulado supere los valores deseados y usar esos valores como mínimo y máximo. Parece facil y lo es hasta que vemos el histograma a color, tenemos tres canales de color ¿Como elegimos los valores high y low para calcular los ajustes de brillo y contraste?. En el caso anterior hemos ignorado de forma poco elegante este punto y el resultado a veces se resiente. La solución más simple es modificar la función de ajuste de brillo y contraste para que permita modificar cada canal por separado.

Combinar cálculos de tablas de consulta e histogramas.

Ya hemos visto lo útiles que resultan las tablas de consulta para agilizar cálculos y también la utilidad del histograma que al ser una representación estadística de la imagen nos permite calcular ajustes globales de la imagen con solo 256 valores. ¿A que seria genial que ambos métodos se pudieran usar conjuntamente? La vida es bella, ya que no solo es posible sino que es realmente sencillo.

En los distintos métodos que usan el histograma (ajuste del brillo y contraste, ecualización del histograma, …) sencillo que calcular la tabla de consulta correspondiente. Pero al aplicar estas tablas la imagen cambia y por tanto el histograma también. ¿Podemos calcular el nuevo histograma sin tener que volver a recorrer toda la imagen contando pixels? Por fortuna para el interés de esta entrada la respuesta es sí. Además de una forma realmente sencilla.

  1. Creamos un nuevo histograma (newHistogram en el ejemplo) con todos sus valores a 0.
  2. Recorremos todos los posibles valores i del histograma original (por ejemplo de 0 a 255).
  3. Para cada valor i del histograma original consultamos su valor (lut[i]) en la tabla de consulta lut
  4. Se suma el valor del histogram[i] a newHistogram[lut[i]]

for(var i = 0; i < 256; ++i){
  newHistogram[lut[i]] += histogram[i];
}

Con solo recorrer 256 elementos tenemos el histograma actualizado y nos hemos ahorrado volver a recorrer la imagen.

Comparar pantallas para encontrar errores visuales durante los test del software

Uno de los problemas de los test automáticos para probar aplicaciones es que no se guían por el aspecto visual de la aplicación. Puedes tener un botón torcido, un texto que no se lee o un color que no corresponde y los test serán correctos, sin embargo para el usuario es importante que el botón que tiene que pulsar sea visible o que pueda leer ese texto ilegible.

Es una tarea difícil de automatizar. Programar código para comprobar la correcta disposición de todos los elementos es algo muy costoso. Y tener una IA que detecte posible elementos erróneos puede sonar muy interesante pero es aun más costoso y complicado. Así que vamos a optar por una solución más sencilla, comparar una imagen que sabemos que esta bien con una captura de pantalla de la aplicación durante las pruebas. Luego calcularemos las diferencias entre ambas imágenes.

Hay múltiples librerías para realizar la comparación de la imagen como pixelmatch o Resemble.js. Aunque estas librerías tienen bastante funciones el algoritmo básico de comparación es muy sencillo. Se comparan uno a uno los pixel de la imagen de referencia con los de la imagen capturada, se fija un valor de umbral. Si la diferencia entre ambos pixel es mayor que el valor umbral se considera que ese pixel es distinto y se marca.

Para que funcione bien la imagen que se usa como modelo ha de ser idéntica a la que se obtiene de la captura de pantalla. Podéis estar pensando que siempre se puede reescalar una de la dos imágenes pero generalmente eso mete «ruido» en los bordes de los elementos de la imagen que el algoritmo de comparación detecta como diferencias.

Los pasos a segir son los siguientes:

  1. Captura de pantalla
  2. Cargar imagen modelo correspondiente
  3. Crear una tercera imagen comparando los pixeles de las dos anteriores. Cada pixel cuya diferencia supere cierto valor fijado se marcara como «diferente», por ejemplo poniéndolo en rojo.
  4. Buscar si existe alguna diferencia en la imagen resultado de la comparación
  5. Si existe alguna diferencia se deja la imagen con el resultado de la comparación y se avisa al usuario para que la revise.

El principal problemas son los datos que cambian cada vez que se realiza el test. Por ejemplo: ids de elementos, fechas, horas, elementos generados en parte o completamente por procesos (pseudo)aleatorios. Estos elementos dejan «una mancha» en el resultado de la comparación. Una solución para evitar estas manchas es «taparlas» para ello vamos a usar plantillas con una zona de un color especial que cuando el algoritmo las lee ignora.

Otra técnica para ignorar «pequeñas cantidades de error» es dividir la imagen en cuadrados, por ejemplo de 16×16. En cada cuadrado hay un total de 256 pixeles, solo lanzaremos una advertencia si la cantidad de pixeles marcados como diferentes supera cierto número. Por ejemplo el 20%, o lo que es lo mismo 51 pixeles. Con esto logramos que solo diferencias «notables» se consideren.

Este es un sistema realmente simple para ayudar a resolver el problema de comprobar el aspecto visual de la aplicación o la web durante los test. Si bien no resuelve todo el problema y sigue siendo necesario un humano para verificar las imágenes marcadas como errores. Agiliza mucho el proceso de comprobar el correcto aspecto visual de la aplicación o web.

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.

Operaciones con el histograma

A nivel de programación podemos ver el histograma de un conjunto de valores como un array donde se asocia a cada posible valor del conjunto a un indice donde se almanacena el número de veces que aparece ese valor en el conjunto de datos. Es habitual usarlos con imágenes. Así que vamos a usarlo como ejemplo, pero estos cálculos son aplicables para cualquier histograma.

Empezando por el caso más simple, el de una imagen en escala de grises con 8 bits de profundidad. Su histograma es el número de pixeles que hay de cada valor (0-255). Se calcula con un procedimiento tan simple como recorrer la imagen e ir contando el número de pixels de cada intensidad .

Histograma de un solo canal (escala de grises)
var histograma = [];
for(var i = 0; i < pixels.length; i++){
  histograma[pixels[i]] = histograma[pixels[i]]+1 || 0;
}

Para el caso de una imagen RGB lo que habitualmente se hace es calcular un histograma por cada canal, por lo que tendremos tres histogramas. Se podria hacer un histograma contando cada una de las posibles combinaciones que hay de los tres canales RGB, pero son mas de dieciséis millones y quedaba un histograma demasiado grande para ser manejable

Histograma RGB.

Vale, ya sabemos que es y como se calcula ¿Para qué demonios sirve? Para conocer la distribución de las distintas intensidades en la imagen. Por ejemplo, si la mayoría de los pixeles están en los valores bajos la imagen será oscura y posiblemente estará subexpuesta. Por el contrario si se concentran en los valores altos la imagen será luminosa y podría estar sobreexpuesta.

El histograma normalizado es un histograma cuyos valores se han ajustado para que la suma de todos sus valores sea 1. Se calcula dividiendo cada valor del histograma entre el número total de píxeles que tiene la imagen. Resulta útil para trabajar con histogramas que procedan de distintas fuentes ya que el número de píxeles puede no ser equivalente. Lo que indica es la proporción de píxeles sobre el total. Hay que recordar que el total de píxeles de la imagen es igual a la suma de los valores de todo el histograma

var total = 0;
for(var i = 0; i < histograma.lentgh; i++)}
  total += histograma[i];
}
var normalizado = [];
for(var i = 0; i < histograma.lentgh; i++)}
  normalizado[i] = histograma[i]/total;
}

El histograma acumulado indica cuantos píxeles tienen un valor igual o inferior a uno dado. Se calcula sumando a cada posición del histograma la suma de las anteriores.

var acumulado = [];
acumulado[0] = histograma[0];
for(var i = 1; i < histograma.lentgh; i++)}
  acumulado[i] = acumulado[i-1]+histograma[i];
}

Estadística

El histograma facilita los cálculos estadísticos sobre los valores de la imagen.

La media aritmética:

var media = total/histograma.lentgh;

La varianza:

var varianza = 0;
for(var i = 1; i < histograma.lentgh; i++)}
  varianza += Math.pow(histograma[i], 2);
}
varianza /= histograma.lentgh;
varianza -= Math.pow(media, 2);

La desviación tipica:

var desviacion = Math.sqrt(varianza)

La moda:

var moda = 0;
for(var i = 0; i < histograma.lentgh; i++)}
    if(histograma[i] > histograma[moda]){
        moda = i;
    }
}

La mediana:

var suma = 0;
for(var i = 0; i < histogram.lentgh; i++)}
    suma = histograma[i];
    if(suma > total/2){
        return i;
    }
}

Probabilidad

Para la probabilidad usaremos el histograma normalizado. Que representa como de probable es que un pixel cogido al azar tenga el valor indicado por el indice (0-255). Es decir normalizado[3] es la probabilidad que de que un pixel elegido al azar tenga valor 3

Probabilidad de que el pixel elegido al azar tenga un valor x:

var probabilidad = normalizado[x];

Probabilidad de que el pixel elegido al azar tenga un valor x, y o z:

var probabilidad = normalizado[x] + normalizado[y] + normalizado[z];

Probabilidad de que un pixel elegido al azar tenga un valor que sea distinto de x, y o z:

var probabilidad = 1 - (normalizado[x] + normalizado[y] + normalizado[z]);

Para calcular la probabilidad acumulada podemos calcular el histograma acumulado del histograma normalizado:

var acumuladoNormalizado = [];
acumuladoNormalizado[0] = normalizado[0];
for(var i = 1; i < normalizado.lentgh; i++)}
  acumuladoNormalizado[i] = acumuladoNormalizado[i-1]+normalizado[i];
}

Ahora pasa saber la probabilidad de que un pixel tomado al azar sea menor o igual que un valor x:

var probabilidad = acumuladoNormalizado[x];

Si queremos calcular que sea mayor que el valor x:

var probabilidad = 1 - acumuladoNormalizado[x];

Todos estos casos en lugar de como probabilidad se pueden interpretar como «porcentaje de pixeles de la imagen». Por ejemplo: «Porcentaje de pixeles de la imagen que son mayores que 100» Seria:

var probabilidad = 1 - acumuladoNormalizado[100];

Unir histogramas

Una de las ventajas del histograma es que su cálculo es fácilmente paralelizable. Se puede dividir la imagen en varias partes y calcular el histograma de cada una de ellas en paralelo. Luego esos histogramas se pueden unir en uno solo de forma fácil. Simplemente basta con sumar cada uno de los indices del histograma:

var union = [];
 for(var i = 0; i < histograma1.lentgh; i++)}
   union[i] = histograma1[i]+histograma2[i]
 }

En el caso del histograma normalizado hay que sumar y dividir entre 2 para conservar al propiedad de que sume 1 en total.

var union = [];
 for(var i = 0; i < normalizado1.lentgh; i++)}
   union[i] = (normalizado1[i]+normalizado2[i])/2
 }

Tablas de consulta (lookup table)

Más que un algoritmo de visión por computador es una forma de acelerar los cálculos. La idea es reemplazar todos los cálculos aplicados a un pixel por un simple acceso a memoria. Suena bien ¿verdad?. Se puede ganar bastante velocidad pero por desgracia está limitado y solo sirve para operaciones que afecten a un solo canal del pixel. Si el resultado se ve influido por algo más como la posición, el valor de los vecinos o del resto de los canales este método no sirve. Aun con estas limitaciones resulta útil para optimizar operaciones como el umbral, el ajuste de brillo y/o contraste o corrección del color.

Empecemos por la idea básica. A partir de ahora cuando hable de pixel me refiero a un pixel de un solo canal en escala de grises, para el caso de color RGB seria en realidad uno de los canales de color. Y habría que tener una tabla de consulta por cada canal

Un pixel tiene un número limitado de valores, de 0 a 255. Eso significa que podemos precalcular esos 256 valores en una tabla y luego simplemente consultar el valor del pixel en esta en lugar de repetir los calculos para cada pixel.

Por tanto necesitamos generar una tabla con indices de 0 a 255. Calculamos el resultado para cada uno de los 256 posibles valores de un pixel. Cada resultado se almacenará en la posición correspondiente al valor original del pixel.

Por ejemplo si nuestra funcion suma diez al valor del pixel, la tabla seria:

T[0]=10;
T[1]=11;

T[254]=255;
T[255]=255;

Si os fijais hay un detalle a tener en cuenta. Los valores de la tabla no pueden ser mayores de 255 que es el valor máximo de un pixel, tampoco pueden ser menores de 0, que es el valor mínimo.

Vale, ya tenemos los 256 valores posibles. Para usar esta tabla solo hemos de recorrer la imagen leyendo el valor de cada pixel y remplazarlo el valor que tenga la tabla para ese índice. Por ejemplo, para un pixel P con valor I y una tabla de consulta T

Image[P] = T[I]

Pero esto no es todo, puedes combinar varias de estas tablas en una sola tabla y calcular varias operaciones con un coste en tiempo ridiculo. Vamos a ver cómo.

Cuando operamos varias veces sobre un pixel de valor p realmente lo que estamos haciendo es.

funN(…func2(func1(p)))

Para cada cada pixel solo hay 256 valores posibles (del 0 al 255), eso quiere decir para cada función habra como mucho 256 resultados distintos. Si ademas limitamos que cada una de esas funciones solo pueda devolver valores entre 0 y 255. Podemos usar el resultado de una función como entrada de la siguiente. Por lo tanto podriamos precalcular esos 256 valores y convertir todas esas funciones en una sola que relacione cada valor del pixel con su resultado…efectivamente eso es lo mismo que hacer un array con los 256 resultados y usar el valor original como indice.

Repito, esto solo se cumple cuando sobre la función que aplicamos solo influye el valor del propio pixel y los valores que devuelve están comprendidos entre 0 y 255.

Si tenemos dos tablas de T1 y T2 calcular una tabla T3 que combine ambas en una sola operación es muy sencillo:

for(var i = 0; i < 256; i++){
    T3[i]=T2[T1[i]];
}

Se pueden combinar tantas tablas como se quiera.

Aunque en los procesadores actuales resulta casi igual de rápido realizar un par de operaciones simples que recurrir a estas tablas, cuando se acumulan varias funciones en una sola tabla el aumento de rendimiento es más que apreciable.

Imagen integral

La imagen integral es una técnica para acelerar el calculo de operaciones que incluyan la suma del valor de los pixeles de un área. Para calcular la imagen integral hay que reemplazar cada píxel por la suma de todos pixeles contenidos en un rectángulo cuya esquina superior izquierda es el vértice 0,0 de la imagen . Y cuya esquina inferior derecha es el propio pixel.

Veamoslo con un ejemplo, partimos de una imagen de 4×4 con lo siguientes pixeles:

1 1 1 1
2 2 2 2
3 3 3 3
4 4 4 4

La forma más intuitiva de verlos es como un proceso en dos iteraciones. en la primera se suman todas las celdas de cada fila de izquierda a derecha:

1 1+1=2 2+1=3 3+1=4
2 2+2=4 4+2=6 6+2=8
3 3+3=6 6+3=9 9+3=12
4 4+4=8 8+4=12 12+4=16
1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 16

Luego sumamos las columnas de arriba hacia abajo:

1 2 3 4
2+1=3 4+2=6 6+3=9 8+4=12
3+3=6 6+6=12 9+9=18 12+12=24
4+6=10 8+12=20 12+18=30 16++24=40

El resultado:

1 2 3 4
3 6 9 12
6 12 18 24
10 20 30 40

Vale, tenemos una imagen con la suma de los valores de los pixeles. ¿Para qué nos sirve?. Para obtener con solo cuatro operaciones el total de la suma de de todos los pixeles de cualquier rectángulo de la imagen. Su uso es muy sencillo. Tomamos las cuatro esquinas A-B-C-D. El valor del pixel D en la imagen integral es el valor del área desde la esquina superior izquierda hasta el punto D. Le restamos el valor del área C (valor del punto C en la imagen integral) y el valor del área B (valor del punto B en la imagen integral). El problema es que ahora hemos restado una parte dos veces, por fortuna esa parte corresponde con el valor del área de A (seguro que ya sabes lo que va en estos paréntesis) solo hay que sumarlo. En resumen:

Valor del área ABCD = D – C – B + A

 

ABCD BD BD 4
CD D D 12
CD D D 24
10 20 30 40

¿Para qué sirve esto?. Se usa generalmente cuando tenemos algoritmos de ventana deslizante que necesitan calcular el valor de la suma del área comprendida por la ventana. Por ejemplo que necesiten el valor medio de un área o algún otro valor estadístico. No es necesario que lo que se sume sea el valor del pixel puede ser su cuadrado o simplemente 0 y 1 si es blanco o negro.

Es una herramienta útil para reducir los cálculos de distintos algoritmos, sobre todo de los que usan ventana deslizante.

Ventana deslizante y pirámide de imágenes

Se llama venta deslizante a seleccionar un rectángulo dentro de la imagen (la ventana) aplicar sobre él un conjunto de operaciones y luego desplazar (deslizar) la ventana un pixel y repetir el proceso, cuando llega al final de la linea se vuelve al principio y se baja una fila de pixels. Con un ejemplo se ve fácilmente, siendo los cuadros rojos la ventana seleccionada.

* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *

Se usa principalmente para la búsqueda de características. O para aplicar algoritmos que solo tiene en cuenta la vecindad. La convolución seria un ejemplo.

Es habitual su uso con una pirámide de imágenes que no es más que usar la misma imagen pero escalandola en cada paso para reducir su tamaño. Se usa cuando el algoritmo que se aplica en la ventana deslizante es dependiente de la escala, por ejemplo un histograma no lo es, pero un detector de caras si que lo es (no se lo mismo que la ventana abarque toda la cara que que abarque solo media cara). Cada iteración de la pirámide la imagen se reduce en un porcentaje pequeño y sobre la imagen resultante se aplica la ventana deslizante.

Una explicación más visual, con una reducción del 50% (generalmente es más pequeña)

8×8

* * * * * * * *
* * * * * * * *
* * * * * * * *
* * * * * * * *
* * * * * * * *
* * * * * * * *
* * * * * * * *
* * * * * * * *

4×4

* * * *
* * * *
* * * *
* * * *

2×2

* *
* *

Estos algoritmos suelen ser buenos candidatos a ser optimizados usando una o varias imágenes integrales.

Capturar vídeo de la cámara del dispositivo en HTML5

Para dotar a nuestro agente de ojos debemos poder acceder a las cámaras del dispositivo. Para ellos nuestra web va a necesitar un elemento vídeo y otro canvas. Enlazamos el vídeo con la webcam del dispositivo y cada cierto tiempo tomamos una captura que copiaremos sobre el canvas para poder acceder a los pixels y aplicar nuestros algoritmos de visión por computador.
Empecemos por incluir una etiqueta vídeo y una canvas en nuestra web:



Las funciones que nos da HTML5 para acceder al vídeo se basan en la API navigator.mediaDevices. En versiones antiguas de los navegadores pierdes encontrarte el problema de que los nombres no están unificados y cada uno lo llamaba de una manera. Actualmente ya lo están. Algo parecido nos pasa con la API window.URL que vamos a usar para poder conectar la webcam a nuestro elemento vídeo. Para que no haya problemas entre navegadores usaremos:

window.URL = window.URL
|| window.webkitURL
|| window.mozURL
|| window.msURL;

Nos hace falta acceder a cada uno de los elementos. El vídeo, el canvas y el contexto del canvas del cual leeremos la imagen capturada del vídeo. Para ello usaremos el id del tag.

video = document.getElementById(videoId);
canvas = document.getElementById(canvasId);
context = canvas.getContext('2d');

Para enlazar la webcam con el elemento vídeo antes hay que saber que webcam es ya que un dispositivo puede tener varias, por ejemplo un móvil tiene frontal y trasera. Para saber que dispositivos hay puedes usar la API navigator.mediaDevices.enumerateDevices . Una vez seleccionado el dispositivo que quieras usar se emplea el metodo getUserMedia() pasandole una estructura del tipo MediaStreamConstraints donde describes los requisitos que necesitas que tenga el dispositivo.  En nuestro caso va ser mas simple vamos a contemplar solo la cámara frontal, la trasera y la por defecto.

//Cámara frontal
device.video = {facingMode: "user", deviceId: ""};
//Cámara trasera
device.video = {facingMode: "environment", deviceId: ""};
//Cámara por defecto (frontal en los móviles)
device.video = {deviceId: "default"};

También podemos desear ajustar la resolución:

device.video.width = 320;
device.video.height = 240;

y el framerate.

this.configuration.framerate = 25;

Hay que decir que realmente estas condiciones no obligan que devolver un dispositivo que las cumpla, solo aconsejan que lo sea, así que no puedes confiar que la resolución sea la deseada y debes de verificarlos.

navigator.mediaDevices.getUserMedia(device)
.then(
  function(stream) { ...}
).catch(...)

Una vez conectada a la webcam hemos de conectar esta con el elemento vídeo de la web para ello usamos window.URL.createObjectURL :

navigator.mediaDevices.getUserMedia(device)
.then(
  function(stream) {
    var src = window.URL.createObjectURL(stream);
    video.src = src;
  }
).catch(
  function(e) { console.log("Video error: "+e); }
);

Y por ultimo cada cierto tiempo hemos de capturar el frame que se esta mostrando en el video y volcarlo al canvas, desde donde podremos acceder a todos sus pixeles. El proceso es muy sencillo, tan sencillo como copiarla al canvas usando el método drawimage

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.context.drawImage(video, 0, 0, this.video.videoWidth, this.video.videoHeight);

Lo mismo con videoToCanvas

Para hacer todo esto más sencillo se ha creado la librería videoToCanvas

//id del tag canvas y video
var v2c = new VideoToCanvas("canvas", "video");
//por defecto es 320x240
v2c.configuration.width=640;
v2c.configuration.height=480;
v2c.webcam();

Para realizar la captura al canvas es tan simple como:

v2c.snap();

Ademas cuenta con gran cantidad de funciones para controlar la reproducción del vídeo de la cámara o para cargar en su lugar un vídeo (útil para pruebas).

Acceder a los pixels de un canvas

Con esto tenemos ya el primer paso dado. Capturar la imagen. Antes de empezar a manipular los datos de un canvas tenemos que ver cómo trabajar con ellos.
Para recuperar los datos del canvas usamos la función getImageData del contexto

var imageData = canvas.context.getImageData(0, 0, width, height);
var data = imageData.data;

getImageData nos permite recuperar solo parte de la imagen indicando las coordenadas X e Y de la esquina superior izquierda del rectángulo de la imagen a recuperar y el ancho y el alto. En caso de que con las medidas especificadas recuperemos parte de de fuera de la imagen se devolverán pixels negros transparentes

Usando videoToCanvas tienes dos métodos getImageData(x, y, width, height) y getBoxes(rows, cols). el primero actúa igual que el getimageData del canvas.context y devuelve un ImageData. El segundo divide la imagen en rows*cols partes y devuelve un array de ImageData. Este método es útil cuando se va a trabajar en paralelo sobre distintas partes de la imagen.

En el ImageData devuelto encontramos las siguientes propiedades:

  • ImageData.height Alto de la imagen
  • ImageData.width Ancho de la imagen
  • ImageData.data Un array de bytes que contiene los pixels de la imagen en formato RGBA

Los píxels de la imagen se recuperan en formato RGBA. Que significa que cada píxel ocupa 4 bytes. Los 3 primeros se dedican a los componentes de color: rojo, verde y azul. El cuarto al nivel de transparencia, también llamado alpha. De esa forma data[0] corresponde al valor de la componente roja del primer píxel, data[1] a la verde, data[2] a la azul y data[3] a la alpha. Después repetimos esa misma distribución con data[4], data[5], data[6] y data[7]. Y así una y otra vez durante toda el array.

Los valores de cada pixel varían entre 0 y 255 e indican la intensidad de ese color en el pixel. Una ventaja del tipo de datos Uint8ClampedArray es que no hace falta comprobar los límites al asignarle un valor, todo valor menor que 0 se convierte a 0 y todo valor mayor de 255 se convierte a 255. Su mayor problema es que es un tipo bastante lento para operar con él así que vamos a tratar de reducir el número de operaciones sobre el mismo.