Realidad Aumentada (2/7)

Juan Mellado, 24 Mayo, 2011 - 17:07

Los dos primeros pasos para localizar cuadriláteros candidatos que puedan contener marcadores son: convertir la imagen original a escala de grises, y crear una nueva imagen desenfocada. El primer paso tiene la finalidad de simplificar la cantidad de información a procesar, ya que en realidad los colores no son significativos para el proceso. El segundo paso se realiza con el objetivo de eliminar ruidos, aunque en realidad es sólo una parte de un proceso posterior que se explicará más adelante.

Escala de grises (Grayscale)
GrayscaleEl proceso de conversión de una imagen en color a otra en escala de grises es bastante conocido, todo un clásico dentro del mundillo del procesamiento de imágenes por ordenador. Consiste en recorrer todos los pixels de la imagen, y para cada pixel de forma individual multiplicar sus componente RGB (rojo, verde y azul), cuyos valores oscilan entre 0 y 255, por unos coeficientes y sumarlos.

Los componentes RGB originales del pixel se sustituyen por el valor resultante de esa suma. Los tres componentes con el mismo valor, para lograr así el efecto de escala de grises, ya que cuando los tres componentes tienen un mismo valor lo que se obtiene es un tono gris, excepto en los extremos, donde (0, 0, 0) es negro y (255, 255, 255) es blanco.

Los coeficientes que se utilizan normalmente son 0.299 para el rojo (R), 0.587 para el verde (G), y 0.114 para el azul (B). Si se suman los tres coeficientes se observa que el total vale 1 (= 0.299 + 0.587 + 0.114), lo que garantiza que el valor resultante de la suma de los productos por los componentes originales del pixel será un valor comprendido también entre 0 y 255.

En JavaScript se puede acceder a las imágenes a través del canvas 2D introducido con HTML5. La función "getImageData" devuelve un objeto "ImageData" que contiene un array que proporciona acceso directo a los pixels. Es un array plano que empieza con los componentes RGBA del primer pixel en las posiciones 0, 1, 2 y 3, continúa con los componentes RGBA del segundo pixel en las posiciones 4, 5, 6 y 7, y así sucesivamente. En nuestro caso concreto el componente A (alpha), utilizado para las transparencias, puede ignorarse tranquilamente, ya que no tiene sentido considerarlo para las imágenes típicas obtenidas de una webcam.

En la práctica, el proceso de conversión suele adquirir un aspecto similar al siguiente:

dst[j] = src[i] * 0.299 + src[i + 1] * 0.587 + src[i + 2] * 0.114;

Aunque es muy sencillo, hay un par de cosas a considerar. El primero es que no necesitamos realmente generar una nueva imagen con todos sus componentes RGBA, ya que no es algo que se vaya a mostrar por pantalla, excepto quizás para labores de depuración. Sólo necesitamos un componente de los cuatro, por lo que la cantidad de información a procesar se reduce al 25% de su tamaño original. El segundo punto a considerar es que este proceso hay que realizarlo para cada imagen, y que será más lento cuanto mayor sea la imagen. Una posible solución es utilizar imágenes más pequeñas a las originales proporcionadas por la webcam. Otras soluciones a estudiar son tratar de optimizar el cálculo precalculando los productos en una tabla, o incluso ejecutarlo mediante un shader utilizando WebGL. Pero hablar de estas cosas en este punto es cometer un error de "optimización temprana".

Desenfoque (Gaussian Blur)
Gaussian BlurEste segundo proceso, el de desenfoque utilizando un filtro gaussiano, es otro clásico. Buscando información me encontré una página que me gustó mucho por la gran cantidad de técnicas y efectos que aborda, y lo bien que están expuestos. Con un particular sentido del humor además.

Aplicar un filtro sobre una imagen normalmente consiste en recorrer todos los pixels de la imagen, y para cada pixel de forma individual aplicar una operación que genere un nuevo valor para el pixel. En nuestro caso concreto, para conseguir difuminar la imagen de forma coherente, lo que se utiliza como base para calcular el nuevo valor para cada pixel son los pixels más cercanos que tiene a su alrededor. Es decir, si tenemos un pixel negro rodeado de pixels blancos, lo que se quiere obtener es un nuevo pixel gris que difumine la imagen, eliminando ese "ruido" que representa el pixel negro aislado.

El proceso consiste es definir un tamaño de ventana, por ejemplo de 3x3 pixels, a cada celda de esta ventana asignarle un coeficiente numérico, y mover la ventana por encima de cada pixel de la imagen original de forma individual, multiplicando los coeficientes de la ventana por los valores de los pixels que cubre cada una de las celdas.

Lógicamente la gracia de todo esto está en los coeficientes que se utilicen. Para una ventana de 3x3 (= 9 celdas), si se utilizara un valor de 1/9 en cada celda se obtendría una nueva imagen donde cada pixel sería la media ponderada de todos sus pixels vecinos inmediatos (incluido el mismo). No obstante, parece lógico pensar que unos mejores coeficientes serían aquellos que dieran más importancia al pixel original, y fueran decrementado la importancia a medida que se fueran alejando de él. Y eso es precisamente lo que persigue el "Gaussian Blur", lo que consigue usando unos coeficientes que se calculan con la siguiente fórmula:

(1 / (2 * PI * sigma^2) ) * e^( -(x^2 + y^2) / (2 * sigma^2) )

Donde x e y son la distancia al pixel original, y sigma la desviación típica de la distribución gaussiana.

¿Asustado? ¡Bienvenido al club! No obstante, en la práctica resulta que no hay por que preocuparse por entender nada de esto. Como ya comenté en el artículo anterior, estoy siguiendo el código de ArUco para tratar de portarlo a JavaScript, y para obtener los coeficientes de la ventana, que técnicamente recibe el nombre de "kernel", se utiliza la función getGaussianKernel de OpenCV. Mirando en la documentación y el código fuente se ve que en realidad implementa una fórmula un poco distinta a la original, y que además, para ventanas de tamaño 3x3, 5x5 y 7x7 tiene unos valores precalculados.

Como ArUco utiliza una ventana fija de 7x7, entonces siempre utiliza los mismos coeficientes:

[0.03125, 0.109375, 0.21875, 0.28125, 0.21875, 0.109375, 0.03125]

De igual forma que en el proceso de conversión a escala de grises, se verifica que la suma de los coeficientes es igual a 1. Y como se observa los coeficientes centrales tienen mucho más peso que los de los extremos. Y aunque sólo he puesto una fila, en la práctica ha de verse como una matriz de 7x7 con los coeficientes distribuidos de manera uniforme alrededor del elemento central. Pero como se verá inmediatamente, el resto de coeficientes no son importantes.

Implementar el filtro resulta sencillo, pero muy lento, ya que por cada pixel hay que realizar 49 (= 7*7) multiplicaciones. No obstante, el filtro tiene una característica particular. Es un filtro "separable", lo que quiere decir que se puede aplicar en un primer paso en horizontal, multiplicando cada pixel sólo por la fila central de la ventana, y al resultado de ese primer paso aplicarle el filtro en vertical, multiplicando cada pixel nuevamente sólo por la fila central. De esta forma se reduce a 14 (= 7*2) multiplicaciones por pixel. Lo que de todas formas sigue siendo bastante trabajo para JavaScript dependiendo del tamaño de la imagen que se utilice. Sería interesante en un futuro probar con otro tipo de filtros, e incluso tratar de implementarlo en un shader mediante WebGL.

Por último, comentar que el filtro plantea un problema de implementación en los bordes, donde la ventana abarca pixels que no existen, y que se ha resuelto duplicando los pixels de los bordes para cubrir la ventana. Aunque existen otras estrategias posibles.

Actualizado 19/03/2012: Actualmente js-aruco implementa un Stack Box Blur que resulta más eficiente que el Gaussian Blur.

Realidad Aumentada
1. Introducción
2. Escala de grises (Grayscale & Gaussian Blur)
3. Umbralización (Adaptive Thresholding)
4. Detección de contornos (Suzuki)
5. Aproximación a polígonos (Douglas-Peucker)
6. Transformación de perspectiva (Homography & Otsu)
7. Detección de marcadores
8. js-aruco: Realidad Aumentada en JavaScript

¿No encontró lo que buscaba?

Utilice el buscador para encontrar más páginas en esta web o en toda Internet.
 
Web www.inmensia.com