PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) U NIVERSIDAD (x, y)P OLITÉCNICA DE M ADRID d Escuela Técnica Superior de Ingenieros de Telecomunicación d eje x eje y φ uφ+ π2 Departamento de Señales, Sistemas y Radiocomunicación Grupo de Procesado de Señal y Simulación Sistema eficiente de reconocimiento de gestos de la mano Proyecto Fin de Carrera Autor: Jaime Silvela Maestre Tutor: Javier Ignacio Portillo García Madrid, Enero de 2000 Agradecimientos En primer lugar quiero dar las gracias a mi tutor, Javier Portillo, y al Grupo de Procesado de Señal y Simulación, por darme esta oportunidad y por su apoyo. Algunos amigos me han prestado ayuda específica con el proyecto: Guillermo Parodi me escuchó, me hizo sugerencias, me animó a publicar mis resultados, y es siempre una fuerte influencia. Pablo Rosales y Guillermo Díez me prestaron su tiempo y me hicieron sugerencias muy acertadas. Anabel González-Tablas encontró los libros furtivos que le pedí. Javier Gamo y Pablo Rosales me echaron una mano con LATEX. Quiero también reconocer mi deuda con dos grupos de personas que no conozco. En primer lugar, los programadores responsables de todo el software (gratis) que he utilizado para hacer este proyecto: Emacs, Linux, Perl, LATEX, etc. En segundo lugar, los autores de los excelentes libros, como Structure and Interpretation of Computer Programs [1] e Introduction to Algorithms [3], que me han descubierto el mundo de la programación. Por último, lo más importante. Quiero dar las gracias a todos mis amigos y a mi familia. Me han ayudado más de lo que piensan, y, probablemente, más de lo que pienso yo. Índice general Índice de cuadros VII Índice de figuras IX 1. Introducción 1 2. Segmentación de la imagen 7 2.1. Relaciones de vecindad 9 2.2. Algoritmos de llenado de regiones 10 2.2.1. Algoritmo intuitivo 10 2.2.2. Algoritmo iterativo 12 2.2.3. Algoritmo híbrido 13 2.2.4. Llenado “breadth-first” o a lo ancho 14 2.2.5. Algoritmo intuitivo revisado 17 2.2.6. Otros algoritmos de llenado 19 2.3. ¿Regiones 4-conexas o 8-conexas? 19 2.4. Separación en regiones 20 3. Operaciones morfológicas 23 3.1. Almacenamiento de pixels de frontera 24 3.2. Erosión 24 3.3. Dilatación 26 3.4. Transformación de distancia 28 3.5. Esqueletización 30 3.6. Otras consideraciones 33 4. Obtención del contorno 4.1. Especificación del contorno 35 35 ÍNDICE GENERAL IV 4.2. Algoritmos funcionales y mutación 36 4.3. Algoritmos de obtención de contorno 37 4.3.1. Algoritmo intuitivo 4.3.2. El contorno visto como un grafo 37 39 4.3.3. Limpieza del contorno 43 4.4. Hallando el contorno, por fin 5. Procesado del contorno 45 47 5.1. Curvatura de arcos continuos 47 5.2. Nuestra definición de curvatura 48 5.3. El grafo de curvatura 50 5.4. Caracterización de elementos del contorno 53 5.5. Detección de elementos del contorno 55 6. Extracción de características 59 6.1. Área de la mano 59 6.2. Número de agujeros 60 6.3. Perímetro 60 6.4. Curvosidad 61 6.5. Número de dedos 61 6.6. Circularidad 61 6.7. Ángulo de inclinación 62 6.8. Momentos principal y secundario 67 6.9. Relación de aspecto 68 6.10. Cuestiones de implementación 6.11. El programa clasificador 68 69 7. Resultados 71 7.1. Área 80 7.2. Perímetro 81 7.3. Circularidad 82 7.4. Curvosidad 83 7.5. Agujeros 84 7.6. Dedos 85 7.7. Ángulo de inclinación 86 7.8. Relación de aspecto 87 7.9. Momento principal 88 ÍNDICE GENERAL V 7.10. Momento secundario 89 7.11. Rendimiento del programa 90 7.12. Utilidad de los parámetros calculados 91 8. Conclusiones y mejoras 97 A. Cómo usar el programa 101 B. Dactilología 103 C. Implementación 105 C.1. Organización del código 105 C.2. Representación de la imagen 106 C.3. Operaciones sobre regiones 109 C.4. Obtención del contorno 114 C.5. Procesado del contorno 118 C.6. Inclinación y momentos de inercia 120 C.7. Operaciones morfológicas 121 C.8. El intérprete 124 C.9. Interacción con el exterior 125 Bibliografía 127 Índice de cuadros 7.1. Tabla de resultados. Primer grupo de características (1). 74 7.2. Tabla de resultados. Primer grupo de características (2). 75 7.3. Tabla de resultados. Primer grupo de características (3). 76 7.4. Tabla de resultados. Segundo grupo de características (1). 77 7.5. Tabla de resultados. Segundo grupo de características (2). 78 7.6. Tabla de resultados. Segundo grupo de características (3). 79 7.7. Tiempo de ejecución de operaciones. 90 Índice de figuras 1.1. El sistema de clasificador completo. 1 1.2. Etapas de un clasificador. 2 1.3. Un hombre y su perro. 3 1.4. Una de las figuras de entrada al programa. 4 2.1. Aplicación de un umbral. 8 2.2. 4-vecinos y 8-vecinos recorridos en orden. 10 2.3. Una sola pasada deja pixels sin colorear. 12 2.4. Una segunda pasada también deja pixels sin colorear. 13 2.5. 4-distancia y 8-distancia. 14 2.6. Equivalencia entre matriz y grafo. 15 2.7. Llenado a lo ancho. Los pixels almacenados en cola se muestran más oscuros. 16 2.8. Llenado a lo profundo. Los pixels en pila se muestran más oscuros. 18 2.9. Regiones 8-conexas pero no 4-conexas. 20 2.10. Resultado de segmentar la imagen. 22 3.1. Erosión incorrecta. 25 3.2. Erosión correcta. 25 3.3. Erosión de una región. Los pixels de frontera son grises. 27 3.4. Dilatación de una región. Los pixels de frontera son grises. 29 3.5. Transformación de distancia de la región. 31 3.6. Imagen transformada. 32 3.7. Índices de cruce de 1 y 2, respectivamente. 32 3.8. Resultado de esqueletizar. 34 4.1. Ejemplo de mutación. 36 4.2. Callejón sin salida. 38 ÍNDICE DE FIGURAS X 4.3. Contorno de la mano visto como un grafo. 39 4.4. Contorno con algoritmo “a lo ancho”. 41 4.5. Recorriendo el camino más corto evitamos callejones sin salida. 4.6. El pixel inicial es negro. Los pixels blancos sueltos son callejones 42 sin salida que el algoritmo evita. 42 4.7. Las dos mitades del contorno no casan. 43 4.8. Regiones que producen estrangulamientos de contorno. Los pixels de frontera están coloreados. 43 4.9. Erosión seguida de dilatación. 44 4.10. Un “opening” no siempre vale. 44 4.11. Acción de L IMPIAR - CONTORNO. 45 5.1. La digitalización arruga el contorno. 49 5.2. Usamos sólo algunos pixels. 49 5.3. Postura correspondiente a la letra ‘q’. 50 5.4. Análisis de la postura correspondiente a la letra ‘q’. 51 5.5. Postura correspondiente a la letra ‘ch’. 53 5.6. Análisis de la postura correspondiente a la letra ‘ch’. 54 5.7. Postura correspondiente a la letra ‘r’. 55 5.8. Análisis de la postura correspondiente a la letra ‘r’. 56 6.1. Recta de regresión. 62 6.2. El método de mínimos cuadrados no es adecuado. 65 6.3. Una distancia distinta. 65 6.4. Distancia entre punto y recta. 66 6.5. Momentos de inercia pequeño y grande. 68 7.1. El alfabeto dactilológico español (1). 72 7.2. El alfabeto dactilológico español (2). 73 7.3. Área de la mano en número de pixels. 80 7.4. Perímetro de la mano en unidades naturales (unidad de longitud = diámetro de un pixel). 7.5. Circularidad de la mano. 81 82 7.6. Curvosidad de la mano. 83 7.7. Número de agujeros de la mano. 84 7.8. Número de dedos de la mano. 85 7.9. Ángulo de inclinación de la mano en radianes. 86 ÍNDICE DE FIGURAS XI 7.10. Relación de aspecto de la mano. 87 7.11. Momento de inercia principal de la mano. 88 7.12. Momento de inercia secundario de la mano. 7.13. Diagrama del clasificador. Los grupos finales se muestran enmar- 89 cados. 94 Capítulo 1 Introducción Este Proyecto de fin de carrera se inscribe en una linea de investigación seguida en el Grupo de Procesado de Señal y Simulación (GPSS) en el campo de la Visión Artificial, con el objetivo final de desarrollar un sistema clasificador de gestos de la mano. Las posturas de la mano que debe distinguir provienen del alfabeto dactilológiPSfrag replacements co para sordos, N ULO es decir, cada postura de la mano simboliza una letra. Así, el clasificador, dada. .una . imagen de una mano, debe producir una letra, idealmente la que Recta producida por corresponde a la mano. El clasificador podría formar parte de un sistema en que mínimos cuadrados Esta es mejor unarecta cámara tomase una foto de una mano, la digitalizase y la enviase a un orde- ȳ) se podría analizar la postura en que se encuentra, y decir a qué nador, en (elx̄,que (x, y) letra corresponde, todo de forma automática (figura 1.1). d d eje x eje y φ uφ+ π2 A Figura 1.1 El sistema de clasificador completo. Este es el tipo de problema que se trata de resolver en Visión Artificial, una rama de la Inteligencia Artificial. La Visión Artificial es un campo multidisciplinar en que se mezclan los Gráficos de Computador, la Estadística, Procesado Digital de Imagen, . . . Su objetivo es reconocer los objetos que están representados en una imagen, y obtener información sobre ellos. En este sentido, es una disciplina complementaria a los Gráficos por Computador, donde el objetivo es producir una imagen que represente una serie de objetos a partir de una descripción de los mis- 2 Capítulo 1 Introducción mos. Las aplicaciones de la Visión Artificial son muchas y muy importantes, desde el reconocimiento automático de errores de fabricación en componentes al análisis de imágenes astronómicas o médicas. Pero, como en otras subdisciplinas de la Inteligencia Artificial, el progreso es más lento de lo que se esperaba, por la simple razón de que no sabemos cómo vemos. El sistema que nos concierne es, entonces un clasificador de imágenes. Tales sistemas consisten, por lo general, en una etapa inicial en que la imagen de partida es procesada para resaltar alguna de sus características más interesantes, una segunda etapa que realiza medidas sobre la imagen procesada y produce un vector de características, y una tercera etapa, en que el vector de características de la imagen se compara con vectores correspondientes a distintas clases, y se asigna a la clase que se le parezca más según algún criterio acordado (figura 1.2). I MAGEN ORIGINAL −→ I MAGEN PROCESADA −→ V ECTOR DE CARACTERÍSTICAS DEL −→ C LASE OBJETO Figura 1.2 Etapas de un clasificador. Este Proyecto se ha centrado en las dos primeras etapas, es decir, en el procesado de la imagen, y en la obtención de sus características. Propone varias características medibles sobre las imágenes de entrada, y estudia su poder discriminador sobre el grupo de imágenes que nos interesa. Si las características estudiadas toman valores claramente distintos para posturas de la mano distintas, servirán como base para el desarrollo de un sistema clasificador. El objetivo de este Proyecto es, entonces, encontrar un conjunto de características medibles sobre imágenes de manos, que sirvan para distinguir unas manos de otras, con vistas a formar parte de un sistema clasificador. Los sistemas de reconocimiento de imágenes pueden ser muy complicados y lentos, y en este Proyecto se intentó desde desde el principio obtener un sistema sencillo y rápido; lo suficientemente rápido como para poder usarlo en tiempo real, es decir, interactivamente. Para conseguir una ejecución rápida, se decidió simplificar al máximo la etapa de procesado de la imagen. Observemos, entonces, con qué tipo de imágenes nos enfrentamos. Las imágenes de ordenador se pueden crear y almacenar en memoria de diversas formas, pero la más conocida, y la que nos interesa, es la representación de raster. La imagen, en este caso, está formada por filas y columnas de puntos, es decir, es una matriz de pixels (figura 1.3), siendo un pixel un punto en la imagen; 3 el nombre pixel es una abreviatura de “picture element”, o elemento de la imagen. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 1.3 Un hombre y su perro. Una matriz de pixels puede reproducir cualquier imagen que deseemos, pero la calidad de la reproducción aumenta cuantas más filas y columnas tengamos, y cuantos más colores o tonos de gris pueda tener cada pixel. En este proyecto tratamos con imágenes de 576 filas y 768 columnas, en que cada pixel puede tener 256 tonos de gris, es decir, tiene un nivel de gris representado por 8 bits. Las imágenes de este tipo tienen ya una calidad apreciable. Vemos un ejemplo en la figura 1.4, que nos muestra, además, la postura de la mano que corresponde a la letra “s”. Podemos observar en la figura que los pixels que corresponden a la mano son más claros que los que corresponden al fondo. En la representación que usamos, un pixel con un nivel de gris de 0 es negro, y un pixel con un nivel de gris de 255 es blanco. Es decir, que un pixel es más claro cuanto mayor es su nivel de gris. Entonces, los pixels de la mano tienen un nivel de gris superior a los del fondo. Podemos aprovechar este hecho para realizar un primer procesado de la imagen: vamos a separar los pixels de la mano de los de el fondo. Para ello basta con escoger un determinado umbral, colorear de blanco los pixels con un nivel de gris mayor que el umbral, y colorear de negro los demás pixels. Deberíamos obtener una imagen en que todos los pixels de la mano son blancos, y el resto negros. Sin embargo, siempre vamos a encontrarnos con pixels de la mano coloreados de negro, y pixels del fondo coloreados de blanco. Para evitar esto usaremos una técnica normalmente empleada para otros fines. Vamos a dividir la imagen, a la que hemos aplicado el umbral, en regiones conexas, 4 Capítulo 1 Introducción PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 1.4 Una de las figuras de entrada al programa. es decir, en manchas de un solo color (nivel de gris), y vamos a borrar las regiones demasiado pequeñas, que consideraremos debidas a ruido. De esta forma conseguimos el objetivo de separar la mano del fondo. La separación de los objetos que forman la mano es lo que se conoce, en la literatura de Procesado de Imagen, como segmentación. Tratamos este tema en el capítulo 2. Una vez tenemos nuestra imagen segmentada, podemos calcular el número de pixels que pertenecen a la mano, que es proporcional a su área. Podemos calcular el ángulo de inclinación de la mano usando operaciones de estadística, como veremos en el capítulo 6. También podemos hallar de forma simple su contorno, que nos podría dar una idea del número de dedos extendidos, del perímetro de la mano, de su circularidad, y otras medidas. Pero para tomar estas medidas necesitamos que el contorno sea continuo, es decir, hay que obtener el contorno de forma ordenada, como si estuviéramos calcándolo. Esto ha resultado ser un problema complicado, como veremos en el capítulo 4, y la mejor alternativa ha resultado ser eliminar de la imagen segmentada algunos pixels que resultan problemáticos. Se buscó otras alternativas para contar el número de dedos en la mano que evitasen las dificultades de trabajar con el contorno. La mejor alternativa es usar las llamadas operaciones morfológicas, como erosión o esqueletización. La esqueletización consiste en extraer de la imagen un esqueleto, de un pixel de grosor, que idealmente debe contener toda la información relevante 5 de la imagen. A partir de este esqueleto debe ser más sencillo contar el número de dedos de la mano. Aunque este enfoque es prometedor, no se ha conseguido un procedimiento de esqueletización satisfactorio (de hecho nadie lo ha conseguido todavía). Este es el tema del capítulo 3. Cuando hayamos conseguido todo esto, tendremos ya el número de agujeros en la imagen, el área de la mano, su perímetro, su número de dedos, su pendiente media y otras características, y observaremos si estas características son muy distintas para las distintas posturas de la mano. Si es así, serán adecuadas para formar parte de un sistema clasificador. Estructura de la memoria En esta memoria se describe las etapas de procesado de imagen y extracción de características. En los primeros capítulos, las descripciones de algoritmos se hacen en pseudocódigo, es decir, lenguaje normal que usa construcciones típicas en lenguajes de programación. Cualquier persona que haya programado debería poder leerlo y entenderlo. El capítulo 2 describe los algoritmos usados para segmentar la imagen, e introduce la idea de ver la imagen como un grafo, que será de gran utilidad a la hora de desarrollar algoritmos. El capítulo 3 describe las operaciones morfológicas que se han usado en el proyecto, que aunque han resultado menos útiles que la exploración del contorno, son siempre interesantes. Además, han sido implementadas de forma muy eficiente. Los capítulos 4 y 5 describen el procesado y clasificación del contorno de la mano, la parte quizás menos precisa del Proyecto, aunque promete ser muy útil cuando se desarrolle con más detenimiento. El capítulo 6 resume las características medidas hasta ese momento, y muestra cómo hallar otras características, basadas en parámetros estadísticos, de forma sencilla. También presenta los pasos seguidos por el proyecto para generar el vector de características. Aquí vemos cuáles han sido al fin las operaciones empleadas en la extracción de características. El capítulo 7 muestra los resultados, es decir, los vectores de características obtenidos para cada una de las imágenes que se han examinado en el transcurso del Proyecto. En este capítulo vemos también si las características propuestas tienen interés para la clasificación de imágenes, y mostramos el tiempo que tarda en ejecutarse cada una de las operaciones necesarias en la obtención del vector de características. Finalmente, vemos cómo podría ser un sistema clasificador basado 6 Capítulo 1 Introducción en este proyecto. El capítulo 8 resume los resultados obtenidos, y propone direcciones de desarrollo para el futuro, pensando tanto en extender los resultados del presente Proyecto, como en obtenerlos de forma más rápida y sencilla. En los apéndices hay un resumen de las implementaciones, en C++, de los algoritmos desarrollados a lo largo del Proyecto. Será de utilidad principalmente a quien desee modificar el código fuente o añadir extensiones; pero además, presentar los algoritmos en un lenguaje de programación real da una idea de algunos detalles de “bajo nivel” que son muy importantes. También se incluye una breve explicación acerca de cómo usar el programa que se ha desarrollado, y del alfabeto dactilológico español, que es, al fin y al cabo, la fuente de las posturas de la mano que han sido estudiadas. Capítulo 2 Segmentación de la imagen En esta fase tomamos como entrada una imagen de una mano en 256 tonos de gris y debemos segmentarla, es decir, delimitar los objetos que la forman. Estamos interesados en tres tipos de objetos: la mano, el fondo, y posibles huecos o agujeros formados por los dedos. El primer paso que seguimos para esto es aplicar un umbral, de forma que un pixel con un nivel de gris superior o igual al umbral se considera perteneciente a la mano y se colorea de blanco, y un pixel con un nivel de gris inferior al umbral se considera parte del fondo y se colorea de negro. A PLICAR - UMBRAL(umbral) para cada pixel p de la imagen si color(p) < umbral color(p) ← negro si no color(p) ← blanco El problema que nos encontramos es que, para cualquier umbral, obtenemos manchas blancas que corresponden a objetos claros en el fondo, y manchas negras que corresponden a sombras en la mano, como vemos en la figura 2.1. ¿Cómo evitamos esto? Una posible solución es filtrar la imagen antes de aplicar el umbral, pero esto en primer lugar falsea nuestra imagen, y en segundo, para una implementación eficiente requiere transformadas de Fourier [2]. La solución escogida en este proyecto evita esto, y de paso resuelve también la detección de agujeros. La idea es que la mano debe ser una región conexa de pixels del mismo color. Por conexa se entiende que entre cualquier par de pixels podemos encontrar 8 Capítulo 2 Segmentación de la imagen PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 (a) Imagen original PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 (b) Imagen después de aplicar un umbral de 90 Figura 2.1 Aplicación de un umbral. 2.1 Relaciones de vecindad 9 un camino formado por pixels vecinos pertenecientes a la mano. Un pixel blanco que no podamos conectar a la mano no pertenece a ella. Lo mismo ocurre con las regiones negras. El fondo debe ser una región conexa, y también los agujeros formados por los dedos deben ser conexos. Vamos a separar nuestra imagen binaria en regiones conexas disjuntas. Para distinguir unas regiones de otras vamos a colorear cada región de un nivel de gris distinto. Para librarnos de los espúreos tan sólo tenemos que borrar las regiones demasiado pequeñas. Es decir, vamos a pintarlas de negro si su color original era blanco, y de blanco si su color original era negro. Como vemos, necesitamos un procedimiento para colorear una región conexa dado un punto inicial contenido en ella. Este problema se conoce en Visión Artificial como “object labelling” o “marking” [11], y a menudo se implementa sin colorear las regiones, asignando una etiqueta a cada pixel como característica adicional [4]. En el campo de los Gráficos por Computador este problema también es importante. Es común en programas de dibujo querer colorear una región que hemos “pinchado” con el ratón. En los libros de Gráficas por Computador [7] este problema se llama “flood-fill”. Nosotros usaremos el término “llenado de regiones”. Antes de resolver el problema necesitamos definir con precisión qué se entiende por “camino de vecinos” entre pixels, y qué es un vecino. 2.1. Relaciones de vecindad Una imagen de “raster” [7], que es el tipo más habitual en computadores, y el que nos concierne, está representada mediante una agrupación de puntos en filas y columnas o matriz. Cada punto tiene un nivel de gris (o color), codificado por un número entero. En nuestro caso, hay 256 grises posibles, por lo que cada pixel ocupa 8 bits de memoria. Los vecinos de un pixel están en filas y columnas adyacentes. En concreto, hay dos definiciones de vecindad: 4-vecindad y 8-vecindad. Los 4-vecinos de un pixel dado son los que se encuentran directamente arriba, abajo, a la izquierda y a la derecha. Los 8-vecinos añaden a los anteriores el pixel de arriba a la derecha, arriba a la izquierda, abajo a la izquierda y abajo a la derecha. Cuando en las secciones siguientes digamos “para todos los vecinos del pixel p. . . ” debemos entender que recorreremos los vecinos en el orden que especifica la figura 2.2. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados 10Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Capítulo 2 Segmentación de la imagen Figura 2.2 4-vecinos y 8-vecinos recorridos en orden. Un camino de vecinos es un camino que podemos recorrer completamente saltando de un pixel a su vecino, y al vecino de éste. . . hasta el final. Por tanto tenemos dos posibles definiciones de región conexa, según el tipo de relación de vecindad que queramos. Reformulando la definición: una región 4-conexa es un conjunto de pixels del mismo color, en que cualquier par de pixels puede ser unido por un camino de 4-vecinos pertenecientes a la región. La definición de región 8-conexa es análoga. Por el momento basta con esta definición, aunque más adelante veremos que las regiones 4-conexas nos convienen más. En las secciones siguientes diremos que los vecinos de un pixel dado están a una distancia 1 de él, los vecinos de sus vecinos a una distancia 2 y así sucesivamente. También diremos que sus vecinos están a una profundidad de 1. Ya estamos preparados para ver los algoritmos de llenado de regiones. 2.2. Algoritmos de llenado de regiones Para formalizar: queremos, dado un pixel inicial, colorear de un nivel de gris determinado la región conexa a la que pertenece el pixel. Como veremos, este es un problema complicado con muchas soluciones posibles. En las siguientes secciones, suponemos que la región de interés es blanca, y que queremos colorearla de negro. 2.2.1. Algoritmo intuitivo El algoritmo más sencillo (y el que muestran los libros de texto) que resuelve nuestro problema es el siguiente: dado un pixel inicial, digamos blanco, lo coloreamos de negro, y para cada uno de sus vecinos blancos realizamos la misma operación. Hay que tener en cuenta que algunos pixels están en un borde de la imagen, y algunos de sus vecinos quedarían “fuera” de la imagen. Por supuesto, éstos 2.2 Algoritmos de llenado de regiones 11 no nos interesan. Este algoritmo no es más que una adaptación de lo que podría ser la definición matemática de una región: La región conexa a que pertenece un punto p está determinada por dos reglas: p pertenece a esta región q pertenece a esta región si tiene el mismo color que p y tiene un vecino v que pertenece a la región L LENAR(p , gris_inicial, gris_final) color(p) ← gris_final para cada v ∈ Vecinos(p) si color(v) = gris_inicial L LENAR(v, gris_inicial, gris_final) Pero este algoritmo, siendo correcto, no funciona, ni en el ordenador personal usado para realizar el proyecto, ni en las estaciones de trabajo SPARC del Grupo de Procesado de Señal y Simulación. La razón es su enorme recursividad. Se suele decir que un algoritmo es recursivo cuando está definido en términos de sí mismo. Para implementar un procedimiento o algoritmo recursivo, los compiladores e intérpretes de la mayoría de lenguajes de programación usan una estructura de datos en memoria llamada pila o en inglés stack. Veamos cómo funciona: en nuestro procedimiento, supongamos que hemos coloreado de negro el punto p, inicialmente blanco, y que tenemos 3 vecinos, v 1 , v2 , v3 , sobre los que tenemos que hacer L LENAR(vi , blanco, negro). Cuando hayamos aplicado este procedimiento a v1 necesitamos “volver” a la situación anterior y llamar a L LENAR(v 2 , gris). Necesitamos salvar el contexto, lo que los compiladores de, entre otros lenguajes, C y C++, consiguen almacenando toda la información que se encuentra en los registros del microprocesador en la pila o stack. Esto se conoce como disciplina “framedstack” [1]. El resultado es que si en nuestro programa hay muchas llamadas recursivas a procedimientos, la pila crece mucho, y puede producir un fallo de segmentación, esto es, la pila crece tanto que invade memoria en uso, y el sistema operativo aborta el programa para que no produzca daños. Esto es exactamente lo que ocurre en nuestro caso. Si tuviéramos un ordenador con más memoria o imágenes más pequeñas (las que usamos tienen 768×576 = 442368 pixels) el algoritmo que hemos descrito 12 Capítulo 2 Segmentación de la imagen funcionaría, pero está claro que necesitamos desarrollar un algoritmo menos recursivo. 2.2.2. Algoritmo iterativo En el extremo opuesto a los procedimientos recursivos están los iterativos, que se ejecutan en espacio (memoria) constante, y que no suelen contener llamadas a sí mismos1 . Podemos formular fácilmente un procedimiento iterativo. Por ejemplo: dado el pixel inicial, lo coloreamos de gris. Recorremos lo que queda de fila coloreando de gris los pixels blancos que tengan un vecino gris. Cuando se acaba la fila vamos a la siguiente, y de nuevo la recorremos coloreando de gris los pixels blancos que tienen un vecino gris. Así hasta cubrir la imagen. Este procedimiento es iterativo: sólo necesitamos tener en memoria la fila y la columna en que nos encontramos PSfrag replacements dentro de la imagen. N ULO tiene un grave problema: es posible que en una fila dada haya dos pix. .Pero . Recta producida elspor blancos pertenecientes a la misma región, separados por pixels negros. Este mínimos cuadrados procedimiento deja uno de los dos sin colorear, como vemos en la figura 2.3. Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 2.3 Una sola pasada deja pixels sin colorear. Si queremos colorearlo debemos hacer otra “pasada” sobre la imagen, esta vez de abajo a arriba. Pero una segunda pasada deja todavía pixels sin colorear, y en general, para tener resultados aceptables debemos hacer muchas “pasadas” sobre la imagen. De hecho para todo número de pasadas podemos encontrar una región que no queda bien coloreada (por ejemplo, una espiral). Podemos pensar en un algoritmo iterativo que coloree los pixels a distancia 1 de nuestro pixel, después los que están a distancia 2, y así sucesivamente, pero 1 Es posible que un procedimiento que se llama a sí mismo se ejecute en espacio constante, es decir, genere un proceso iterativo, como se demuestra en Abelson et al. [1] PSfrag replacements N ULO ... Recta producida por mínimos cuadrados 2.2 Algoritmos de llenado de regiones Esta recta es mejor 13 (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 2.4 Una segunda pasada también deja pixels sin colorear. incluso en este caso tendremos necesidad de “volver atrás” con pases sucesivos, lo que ocurre en general con las soluciones iterativas a este problema. 2.2.3. Algoritmo híbrido Ya sin esperanza de encontrar un algoritmo iterativo, el objetivo es encontrar un procedimiento que no se llame a sí mismo tan a menudo como nuestro primer intento. Podemos conseguir esto si, dado el pixel inicial, somos capaces de colorear varios pixels antes de hacer una llamada recursiva. Un posible algoritmo es: dado el pixel inicial, lo coloreamos. Coloreamos los pixels blancos a distancia 1 de él (sus vecinos); coloreamos los pixels blancos a distancia 2, y hacemos una llamada recursiva para cada pixel blanco a distancia 3. L LENAR -2(p, gris_inicial, gris_final) color(p) ← gris_final para cada pixel v a distancia 1 de p si color(v) = gris_inicial color(v) ← gris_final para cada pixel v a distancia 2 de p si color(v) = gris_inicial color(p) ← gris_final para cada pixel v a distancia 3 de p si color(v) = gris_inicial L LENAR -2(v, gris_inicial, gris_final) ¿Por qué hacer la llamada recursiva para distancia 3? Haciendo la llamada recursiva con los pixels a distancia 2, se seguía produciendo un consumo excesivo de memoria, y fallos de segmentación. Por otra parte hemos ocultado cómo podemos implementar “para cada pixel v a distancia d”, lo que no es trivial. Podemos ver en 14 PSfrag replacements Capítulo 2 Segmentación de la imagen N ULO la figura 2.5 que si usamos 8-conexión y 8-distancia, los pixels a distancia n de uno ... dado cuadrados Rectason producida por cuyos lados son filas y columnas de la imagen, así que no es mínimos cuadrados complicado recorrerlos secuencialmente. Con 4-distancia, en cambio, los vecinos a Esta recta es mejor distancia k forman (x̄, ȳ)un cuadrado cuyos lados están en diagonal respecto de las filas y las columnas(x, dey) la imagen, así que recorrer estos pixels es más complicado. d d eje x eje y φ uφ+ π2 Figura 2.5 4-distancia y 8-distancia. En ambos casos, la solución más eficiente y elegante consiste en almacenar las posiciones relativas de los pixels a distancia k como variables globales de nuestro programa. Debemos hacerlo para distancias 1, 2, y 3, para 4-conexión y para 8conexión, porque usar 8-distancia produce siempre regiones 8 conexas. Este enfoque es claramente torpe, porque si decidiéramos hacer la llamada recursiva en los pixels a distancia 4, necesitaríamos almacenar las posiciones relativas de esos pixels. Esto es dado a errores y poco flexible. Además, es un método algo redundante. Vemos que hemos reducido el volumen de llamadas recursivas: entorno a un pixel vamos a colorear 48 pixels y vamos a realizar 24 llamadas recursivas (suponiendo que todos los pixels deben ser coloreados). Cada uno de los pixels sobre los que hemos hecho la llamada recursiva va a examinar de nuevo los pixels de su entorno, incluyendo nuestro pixel de partida. Es decir, nuestro pixel de partida va a ser coloreado una vez y examinado 24. A pesar de todas estas pegas, el método funciona, y fue el usado durante varios meses en este proyecto, hasta que un problema posterior dio la idea para un nuevo algoritmo de llenado. 2.2.4. Llenado “breadth-first” o a lo ancho La idea del siguiente algoritmo es que podemos ver nuestra matriz de puntos como un grafo, en que los nodos representan pixels, y los nexos representan vecindad. El problema de llenar una región a partir de un punto inicial se convierte en PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor 2.2 Algoritmos de llenado de regiones (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 15 Figura 2.6 Equivalencia entre matriz y grafo. recorrer su grafo de vecinos y colorear los adecuados. El problema de recorrer un grafo es de importancia enorme en informática, y está presente en ramas distintas, como la inteligencia artificial, la teoría de algoritmos, problemas de búsqueda. . . Para resolver nuestro problema vamos a adaptar una técnica de recorrido de grafos que se conoce en la jerga como “breadth-first search” [3] y que algunos autores españoles traducen como “búsqueda a lo ancho”. La explicación presentada para este algoritmo imita a Cormen [3]. Pero antes tenemos que definir unos términos: una cola, en inglés queue, es una serie de elementos puestos en fila. Podemos poner un elemento al final de la cola (en inglés enqueue), y podemos quitar el primer elemento de la cola (en inglés dequeue). En una cola, el primer elemento que metemos es el primero que borramos, y el último que metemos es el último que borramos, por lo que a veces se llama a las colas FIFOs (First In, First Out). Durante la ejecución del algoritmo examinaremos pixels que posiblemente formen parte de la región. Si en efecto forman parte de la región, los colorearemos del nivel deseado y los colocaremos en cola. A esto lo llamaremos “descubrir el pixel”. La cola alberga pixels que tienen vecinos no descubiertos; cuando todos los vecinos de un pixel han sido descubiertos, lo retiraremos de la cola. Debido a esto, se usa la memoria eficientemente, y de hecho no se producen fallos de segmentación. El algoritmo es el siguiente. Dado un pixel inicial, lo coloreamos del nivel de gris deseado, y lo ponemos en la cola (que hasta entonces estaba vacía); es decir, lo descubrimos. A partir de ese momento, mientras la cola no esté vacía hacemos lo siguiente: tomamos el primer elemento de la cola y examinamos sus vecinos. Si alguno pertenece a la región (tiene el mismo nivel de gris), lo pintamos del nivel de gris deseado, y lo ponemos en cola. Después quitamos el primer elemento de la cola. L LENAR - A - LO - ANCHO(p, gris_inicial, gris_final) color(p) ← gris_final 16 Capítulo 2 Segmentación de la imagen poner p en la cola mientras la cola no esté vacía q ← primer elemento en cola para v ∈ Vecinos(q) si color(v) = gris_inicial color(v) ← gris_final poner v en cola quitar q de la cola La mayor ventaja de este algoritmo es que recicla la memoria: una vez que un pixel no tiene vecinos sin descubrir lo podemos sacar de la cola. Veamos cómo funciona en la figura 2.7. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 2.7 Llenado a lo ancho. Los pixels almacenados en cola se muestran más oscuros. 2.2 Algoritmos de llenado de regiones 17 Los resultados de este algoritmo son excelentes: la ocupación de memoria es relativamente baja, es 3 veces más rápido que el algoritmo híbrido (tarda 0.3 seg. en segmentar una imagen), y es idéntico para 4-conexión y para 8-conexión. ¿Es este procedimiento iterativo o recursivo? Podemos ver que el procedimiento no se llama a sí mismo, pero tiene una cola que va creciendo (o decreciendo) a lo largo de la ejecución del programa. Por esta razón diremos que es recursivo, aunque no sea sintácticamente recursivo. Para verlo más claro, en este procedimiento cada punto de nuestra imagen produce una búsqueda de sus vecinos, cada punto inicia un llenado a lo ancho. Esto es reminiscente de nuestro algoritmo intuitivo. Los conocimientos adquiridos con este algoritmo nos permiten entender mejor el algoritmo intuitivo. 2.2.5. Algoritmo intuitivo revisado Después de ver el algoritmo anterior podemos retomar nuestro algoritmo inicial. Es muy parecido a la técnica de recorrido de grafos llamada “depth-first search” o “búsqueda a lo profundo” según algunos autores españoles. Veíamos antes que producía un consumo de memoria enorme debido a la disciplina “framed stack” de los lenguajes como C y C++. Podemos entonces engañar al compilador e implementar manualmente la recursión usando nuestra propia pila o stack. Una pila es una serie de elementos puestos en fila. Podemos poner un elemento al principio de la pila, y podemos borrar el primer elemento de la pila. Es notable el parecido entre la pila y la cola, excepto que en la pila, tanto las inserciones como los borrados de elementos se realizan en el mismo extremo de la fila, y en la cola, las inserciones y los borrados se realizan en los extremos opuestos de la fila. En una pila, sacamos siempre el último elemento que hemos metido, y el primer elemento que hemos metido es el último que sacamos. Por eso las pilas se llaman a veces LIFOs (Last In, First Out). De nuevo, un pixel que examinamos por primera vez es descubierto y coloreado del nivel de gris deseado, si es que pertenece a la región. La reformulación del algoritmo intuitivo es: dado el pixel inicial, lo coloreamos y lo ponemos en la pila. Mientras la pila no esté vacía, tomamos el primer elemento de la pila, y si tiene un vecino perteneciente a la región (su nivel de gris es igual que el de la región), lo coloreamos y lo ponemos en la pila. Si no tiene ningún vecino con el nivel de gris de la región, lo quitamos de la cola. L LENAR - A - LO - PROFUNDO(p, gris_inicial, gris_final) 18 Capítulo 2 Segmentación de la imagen color(p) ← gris_final poner p en la pila mientras la pila no esté vacía q ← primer elemento de la pila si q no tiene vecinos con gris_inicial quitar q de la pila si q tiene un vecino v con gris_inicial color(v) ← gris_final poner v en la pila Observemos cómo funciona en la figura 2.8: PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 2.8 Llenado a lo profundo. Los pixels en pila se muestran más oscuros. En la figura podemos ver que cuando el algoritmo llega a un callejón sin sal- 2.3 ¿Regiones 4-conexas o 8-conexas? 19 ida, vuelve atrás hasta encontrar un pixel que sí tenga vecinos blancos, y continúa el llenado a partir de ahí. Esta vuelta atrás tiene nombre propio en los libros de Inteligencia Artificial [12]: backtracking. Este procedimiento tiene un comportamiento idéntico al intuitivo, pero una utilización de memoria menor, debido a que en la pila que mantenemos por nuestra cuenta sólo almacenamos pixels, y no información de contexto que, como vemos, no es necesaria. De nuevo, este es un procedimiento recursivo, aunque no se llame a sí mismo. La utilización de memoria es muy superior a la del algoritmo “breadth-first”; de hecho en una imagen cualquiera de las usadas en este proyecto la pila llega a albergar más de 300000 pixels. Por contra, en el algoritmo de llenado a lo ancho la cola nunca supera los 6000 elementos; es decir, tiene un consumo de memoria al menos 50 veces inferior. También el llenado a lo ancho es más rápido. Tarda 0.3 seg. en segmentar una imagen, frente a 0.9 seg. del llenado a lo profundo. 2.2.6. Otros algoritmos de llenado Aunque los libros de Visión Artificial dan una visión somera del llenado de regiones (que llaman etiquetado o marcado), y presentan algoritmos ineficientes, en el campo de los Gráficos por Ordenador este es un problema muy importante. De hecho, en el artículo de Levoy [8], se comparan cuatro estrategias de llenado de regiones, una de las cuales es idéntica al llenado a lo ancho. La tendencia actual es usar algoritmos de llenado por scan-lines, en que se trata de llenar de forma iterativa una fila de pixels entera, antes de hacer una llamada recursiva o almacenar pixels en pila o cola. Pueden verse ejemplos en los libros de Gráficos [7]. 2.3. ¿Regiones 4-conexas o 8-conexas? Hemos visto que tenemos varios algoritmos de llenado para elegir, entre los que el llenado a lo ancho es el mejor. Pero ¿qué tipo de regiones nos interesa tener? La elección de este proyecto es buscar regiones 4-conexas; la razón es que estas regiones son más restrictivas y tienen contornos más suaves. Veremos que aún usando regiones 4-conexas, los contornos no se comportan suficientemente bien, y será necesario modificarlos. La figura 2.9 muestra ejemplos de la diferencia entre usar 8-conexión y 4conexión: Hay un beneficio adicional en usar 4-conexión. Usando 4-conexión, cada pixel va a examinar sus 4 vecinos, y de la misma forma va a ser examinado por ellos. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 20 Capítulo 2 Segmentación de la imagen Figura 2.9 Regiones 8-conexas pero no 4-conexas. Si usamos 8-conexión, cada pixel examina y es examinado 16 veces, es decir, el doble de veces. Por tanto, aunque la complejidad asintótica de nuestros algoritmos de llenado sea igual en 4-conexión que en 8-conexión, podemos esperar que el 4-llenado sea más rápido. Otro detalle: hemos visto que en nuestros algoritmos de llenado se descubre cada pixel una vez y se examina varias. Podemos evitar esto si añadimos a cada pixel una lista con sus vecinos no descubiertos, lo que se conoce en los libros de Algoritmos y Estructuras de Datos como representación con lista de adyacencia [3]. Cuando descubriéramos un pixel lo borraríamos de las listas de adyacencia de sus vecinos. Así evitaríamos la redundancia anterior. No está claro que esta estrategia pueda mejorar los algoritmos: evitamos redundancia pero por cada pixel tenemos que modificar cuatro u ocho listas. Además, es complicada. Quizás un proyecto posterior investigue este punto. 2.4. Separación en regiones Ya sabemos que queremos tener regiones 4-conexas, y que las podemos obtener usando, por ejemplo, L LENAR - A - LO - ANCHO. Debemos colorear cada región de la imagen con un nivel de gris distinto para poder distinguirlas, y como sabemos que vamos a borrar alguna de las regiones, es conveniente acordarnos de su color inicial. Para borrar una región cuyo color inicial era negro, la colorearemos de blanco, y si el color inicial era negro, la colorearemos de blanco. El criterio para borrar una región es que sea demasiado pequeña, es decir tenga un número de pixels menor que un umbral convenientemente elegido. Es fácil integrar en nuestros algoritmos de llenado la cuenta de pixels que posee la región. En el caso de L LENAR - A - LO ANCHO o L LENAR - A - LO - PROFUNDO, basta con sumar 1 cada vez que sacamos un pixel de la cola o de la pila. Por tanto, cada región puede quedar descrita también de forma sencilla por el número de pixels que posee. Esta representación de una región es suficiente para nuestros propósitos. Para 2.4 Separación en regiones 21 concretar, representamos una región mediante un pixel que pertenezca a ella, su color inicial, y el número de pixels que contiene. En la etapa de segmentación, construimos un mapa de regiones, es decir, una lista de regiones representadas como hemos visto. La construcción del mapa de regiones es la estrategia general a usar cuando no sabemos cuántas regiones debe tener la imagen; pero dado que en este Proyecto nos enfrentamos con un tipo de imagen particular, es más sencillo usar un método distinto. En este proyecto suponemos que sólo hay una mano, esto es, hay una sola región blanca, lo cual simplifica la segmentación. Buscamos regiones blancas hasta encontrar una con un número de pixels superior a un determinado umbral. Cuando la encontramos pintamos de negro todos los pixels que no pertenecen a ella, y después la pintamos de blanco (porque la habíamos pintado de gris). Con esto hemos eliminado las regiones blancas indeseadas. Ahora buscamos las regiones negras, y si son menores que un umbral las pintamos de blanco. Con esto, nuestra imagen contiene una región blanca correspondiente a la mano, y una o más regiones en distintos tonos de gris, correspondientes al fondo. La presencia de más de una de estas regiones indica que hay un agujero en la imagen. El número de agujeros se puede contar fácilmente sin más que sumar 1 por cada región lo suficientemente grande. Resumiendo: S EGMENTAR nivel = 0 repetir buscar siguiente pixel blanco p nivel ← nivel + 1 tamaño ← L LENAR - A - LO - ANCHO hasta que tamaño > umbral-blanco pintar pixels fuera de la última región de negro repetir buscar siguiente pixel negro p nivel ← nivel + 1 tamaño ← L LENAR - A - LO - ANCHO si tamaño < umbral-negro L LENAR - A - LO - ANCHO(p, blanco) hasta que no queden pixels negros 22 Capítulo 2 Segmentación de la imagen Podemos ver en la figura 2.10 el resultado de aplicar S EGMENTAR a la imagen de prueba mostrada al principio del capítulo. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 2.10 Resultado de segmentar la imagen. Capítulo 3 Operaciones morfológicas Una vez segmentada la imagen, el objetivo es contar el número de dedos, para lo cual procesaremos el contorno de la mano. Como veremos en los capítulos 4 y 5, la obtención y el procesado del contorno de la imagen presentan muchas complicaciones. Otra alternativa para contar el número de dedos de la mano es obtener el esqueleto de la mano. El esqueleto es una serie de lineas de 1 pixel de grosor que pasan por el centro de la región estudiada. En el caso de que logremos generar el esqueleto correctamente, tendremos una linea pasando por el centro de cada dedo, y quizás una linea pasando por el centro de la muñeca. Entonces, no tenemos más que contar el número de lineas de esqueleto, lo que podemos hacer fácilmente contando el número de extremos de linea. La obtención del esqueleto de una región es un problema difícil y no ha sido resuelto satisfactoriamente nunca. Pertenece a un tipo de operaciones llamadas morfológicas. Otras operaciones morfológicas son la erosión, la dilatación, la transformación de distancia, la abertura y el cerrado. Usaremos alguna de estas operaciones en el capítulo 4. En este capítulo se muestra la forma en que las operaciones morfológicas han sido implementadas en el proyecto. La idea de usar el esqueleto para contar dedos todavía no es practicable, pero no hay que descartarla. A lo largo de todo este capítulo, todos los pixels de la región de interés se suponen blancos, y todos los pixels de fondo se suponen negros. 24 Capítulo 3 Operaciones morfológicas 3.1. Almacenamiento de pixels de frontera Las implementaciones de los algoritmos morfológicos que veremos a continuación están basadas en el mismo principio que la búsqueda a lo ancho; es decir, vemos la matriz de pixels como un grafo, y lo recorremos a lo ancho, usando una cola para almacenar los pixels a partir de los cuales debe continuar el recorrido. En estos algoritmos, al contrario que antes, vamos a comenzar el recorrido de la región por la frontera, y vamos a progresar hacia el interior. Para ello, vamos a almacenar los pixels de frontera de la región en una cola. Veremos pronto que resulta útil colorear los pixels de frontera de un color que no se pueda confundir con el fondo ni con la mano. En nuestro caso, el fondo es negro, y la mano blanca, así que coloreamos la frontera de gris. Comprenderemos mejor esto en el capítulo 4, donde examinamos diversas formas de guardar el contorno en una cola. En este capítulo no necesitamos más que almacenar los pixel de frontera, no importa en qué orden. El siguiente algoritmo resuelve el problema. Usa el símbolo Q para denotar la cola. A partir de ahora, usamos encolar(Q, p) para decir que hemos puesto el pixel p en la cola Q, y descolar(Q) para decir que hemos borrado el primer elemento de la cola. G UARDAR - FRONTERA(Q) para cada pixel p en la imagen si ( color(p) = blanco Y existe v vecino de p tal que color(v) = negro) color(p) ← gris encolar(Q, p) 3.2. Erosión Una erosión consiste en borrar la frontera de una región. De forma intuitiva, podríamos pensar en recorrer la imagen fila por fila y colorear de negro cada pixel de frontera. Sin embargo, una vez hemos borrado un pixel, hemos convertido a sus vecinos en pixels de frontera, si no lo eran ya. El resultado es que acabamos borrando la imagen completa, como vemos en la figura 3.1. Examinamos este tipo de problema en profundidad en la sección 4.2, página 36. Hay dos formas sencillas de evitar este problema. Una es hacer una copia de la imagen original. Recorremos la imagen principal, y cuando encontramos un pixel PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor 3.2 (x̄, ȳ) (x, y) Erosión d d eje x eje y φ uφ+ π2 25 Figura 3.1 Erosión incorrecta. PSfrag replacements N ULO de frontera, borramos el pixel homólogo en la imagen auxiliar. Cuando terminamos . . . el recorrido, la región erosionada está en la imagen auxiliar. Recta producida por mínimos Otra cuadrados forma es colorear los pixels de frontera de gris. Como hemos definido un Esta recta es mejor pixel de frontera como un pixel blanco con un vecino negro, un pixel gris no pro(x̄, ȳ) duce pixels de(x,frontera falsos. Después recorremos la imagen otra vez y borramos y) los pixels grises, dcomo vemos en la figura 3.2. d eje x eje y φ uφ+ π2 Figura 3.2 Erosión correcta. Estos dos métodos son correctos y funcionan bien si queremos realizar una erosión sobre la imagen. Pero ¿y si queremos realizar varias erosiones? En ese caso, para cada nueva erosión debemos repetir el proceso, es decir, recorrer la imagen, y pintar de gris cada pixel de frontera. Esto es poco eficiente. Las ideas desarrolladas a lo largo de este proyecto sobre búsqueda a lo ancho tienen también aplicación aquí. En el siguiente algoritmo, hacemos un recorrido “a lo ancho” de la región, solo que, al contrario que en el algoritmo de llenado, vamos a empezar con los pixels de frontera y progresar hacia el interior. Empezamos por almacenar los pixels de frontera en una cola, Q, y colorearlos de gris. Después colocamos un símbolo especial, NULO, al final de la cola. La función de este símbolo es que no puede representar un pixel. Tomamos la cabeza de la cola. Si es un pixel, exploramos sus vecinos, y si alguno es blanco, lo ponemos en la cola y lo coloreamos de gris. Después borramos la cabeza de la cola. Si la cabeza de la cola es el símbolo NULO, hemos realizado una erosión. Para realizar una nueva iteración, quitamos NULO de la cabeza de la cola y lo colocamos al final. En el procedimiento mostrado primero(Q) denota al primer elemento en la cola Q. 26 Capítulo 3 Operaciones morfológicas E ROSIONAR (num-repeticiones) G UARDAR - FRONTERA(Q) mientras num-repeticiones > 0 encolar(Q, NULO) h ← primero(Q) mientras h 6= NULO para cada n ∈ vecinos(h) si color(n) = blanco color(n) ← gris encolar(Q, n) color(h) ← negro descolar(Q) h ← primero(Q) descolar(Q) ;; eliminar NULO num-repeticiones ← num-repeticiones - 1 Vemos cómo funciona en la figura 3.3. En cada iteración, comenzamos con los pixels de frontera almacenados en la cola Q, seguidos de NULO. Entonces vamos añadiendo los “nuevos” pixels de frontera, después de NULO, y vamos borrando los “viejos” pixels de frontera de la cabeza de la cola. Es decir, los pixels que añadimos después de NULO son los vecinos de los pixels almacenados antes de NULO. Este algoritmo tiene varias ventajas. En primer lugar, sólo examina los pixels que elimina y sus vecinos, lo cual es un ahorro enorme respecto de examinar todos los pixels de la imagen en cada iteración. En segundo lugar, al igual que en el algoritmo de llenado a lo ancho, la cola sólo almacena los pixels cuyos vecinos no han sido examinados, lo cual es eficiente. En cada momento la cola alberga los pixels de frontera de la región actual. Puesto que erosiones sucesivas encogen la región, almacenamos en cola como máximo tantos pixels como pixels de frontera tiene la imagen original. Este mismo método se puede aprovechar para implementar eficientemente las otras operaciones morfológicas. 3.3. Dilatación La dilatación es casi la operación inversa a la erosión, aunque una dilatación no es capaz de reponer todos los pixels que la erosión borra. Consiste en añadir 3.3 Dilatación 27 ... PSfrag replacements ... N ULO Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 N ULO Figura 3.3 Erosión de una región. Los pixels de frontera son grises. 28 Capítulo 3 Operaciones morfológicas una nueva capa de pixels a la región. La combinación de una erosión y una dilatación produce como resultado las operaciones de abertura (erosión seguida de dilatación), y cerrado (dilatación seguida de erosión). El algoritmo usado para la dilatación es idéntico al de erosión, excepto que añadimos pixels negros (de fondo) a la cola, y cuando sacamos un pixel de la cola lo coloreamos de blanco (ahora pertenece a la región). D ILATAR (num-repeticiones) G UARDAR - FRONTERA(Q) mientras num-repeticiones > 0 encolar(Q, NULO) h ← primero(Q) mientras h 6= NULO para cada n ∈ vecinos(h) si color(n) = negro color(n) ← gris encolar(Q, n) color(h) ← blanco descolar(Q) h ← primero(Q) descolar(Q) num-repeticiones ← num-repeticiones - 1 Las consideraciones de eficiencia de la erosión sirven aquí también, excepto por que la cola aumenta de tamaño con cada iteración, y por tanto almacenamos tantos pixels como pixels de frontera hay en la región final. 3.4. Transformación de distancia La transformación de distancia de una región consiste en asignar a cada pixel su distancia a la frontera, como en la figura 3.5. Esto es parecido a la erosión: una erosión borra los pixels de frontera, que están a distancia 0 de la frontera, y cada nueva erosión borra pixels a una distancia de la frontera 1 mayor que los de la iteración anterior. Esto quiere decir que los pixels que una erosión repetida borra en la iteración n son los mismos pixels a los que la transformación de distancia debe asignar n como nivel de gris. Es fácil, entonces, adaptar el algoritmo de erosión para que sirva en la transformación de distancia. 3.4 Transformación de distancia 29 N ULO PSfrag replacements ... N ULO Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 N ULO ... Figura 3.4 Dilatación de una región. Los pixels de frontera son grises. 30 Capítulo 3 Operaciones morfológicas T RANSFORMAR distancia ← 1 G UARDAR - FRONTERA(Q) para cada b en Q color(b) ← distancia mientras Q not vacia distance ← distancia + 1 encolar(Q, NULO) h ← primero(Q) mientras h 6= NULO para cada n ∈ vecinos(h) si color(n) = blanco color(n) ← distancia encolar(Q, n) descolar(Q) h ← primero(Q) descolar(Q) ;; elimina NULO Este algoritmo requiere un solo pase sobre la imagen (de hecho, ni siquiera un pase completo), en contraposición al chamfer algorithm, que requiere dos pases sobre la imagen. Vemos sus resultados en la figura 3.6. 3.5. Esqueletización La esqueletización o adelgazamiento es una técnica muy usada en reconocimiento de patrones. Consiste en eliminar pixels de la imagen hasta que queda un esqueleto de un pixel de grosor. Idealmente, este esqueleto contiene toda la información relevante de la región, y en particular, debe conservar el número de regiones de la imagen, y debe ser parecido a la región original. Esto es complicado de conseguir. De hecho, aunque ha habido muchos intentos de resolver el problema, no hay todavía una solución completamente satisfactoria. El algoritmo propuesto usa las ideas de recorrido a lo ancho, y es una versión— más eficiente—del algoritmo de Zhang y Suen [13]. La idea del algoritmo es que la esqueletización se puede ver como la eliminación de capa tras capa de pixels de frontera, como erosiones repetidas, pero teniendo cuidado de no eliminar algunos pixels que mantienen las relaciones de 3.5 Esqueletización 31 ... N ULO ... N ULO PSfrag replacements Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 ... N ULO Figura 3.5 Transformación de distancia de la región. ... 32 Capítulo 3 Operaciones morfológicas PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 3.6 Imagen transformada. conexión. Los pixels que podemos borrar se caracterizan, según Zhang y Suen, por: Su índice de cruce es distinto de 1. Tienen más de 1 y menos de 7 8-vecinos. replacements EnPSfrag el algoritmo propuesto, los pixels de frontera son coloreados de gris y alN ULO macenados en una cola. A medida que recorremos la cola vamos sustituyendo los . . pixels de frontera pos sus. vecinos, coloreados de gris. Los pixels que no debemos Recta producida por cuadrados eliminarmínimos son aquellos que producen una nueva conexión entre pixels de la fronteEsta recta es mejor ra. El índice de cruce de un pixel es, entonces, el número de regiones grises que (x̄, ȳ) existirían en los 9 pixels (x, y)que rodean a dicho pixel, suponiendo que este pixel se elimina. Este número es el d mismo que el número de transiciones de gris a blanco o negro si recorremos los vecinos en orden. En la figura 3.7 podemos ver ejemplos: d eje x eje y φ uφ+ π2 Figura 3.7 Índices de cruce de 1 y 2, respectivamente. En el algoritmo propuesto, cuando un pixel no cumple las condiciones necesarias para ser eliminado, lo coloreamos de un color distinto, digamos rojo, y no lo 3.6 Otras consideraciones 33 ponemos en cola. El algoritmo completo es: E SQUELETIZAR G UARDAR - COLA(Q) mientras Q no vacia h ← primero(Q) para cada n ∈ 8_vecinos(h) si (color(n) = blanco) si ( indice_de_cruce(n) = 1 AND num_8vecinos(n) >= 2 AND num_8vecinos(n) <= 6) color(n) ← gris encolar(Q, n) sino color(n) ← rojo descolar(Q) Este procedimiento da buenos resultados cuando la frontera de la región bajo estudio es suave; si no, produce segmentos espúreos de esqueleto. Un saliente en la frontera produce líneas de esqueleto que a menudo son falsas. Una forma de evitar esto es erosionar la región varias veces antes de hallar su esqueleto, pero no es una solución general: fronteras irregulares siguen produciendo esqueletos falsos. Por otra parte, la forma de la región queda bien reflejada en el esqueleto, es decir, el proceso es poco distorsionante. Vemos un ejemplo en la figura 3.8. 3.6. Otras consideraciones En todos los algoritmos de esta sección, el primer paso ha sido almacenar los pixels de frontera en una cola y colorearlos de gris. Esto es poco eficiente. Es más sensato almacenar los pixels de frontera en cola sólo una vez, para lo que esta cola debe estar siempre disponible, es decir, debe ser una variable global. Esta es la estrategia seguida en el proyecto. Los algoritmos, entonces, usan y modifican la cola global, y es fácil encadenar operaciones; por ejemplo, realizar 10 erosiones seguidas de una transformación de distancia no implica recomputación de la frontera. Además, al usar una cola global, sólo hace falta pedir memoria al sistema operativo una vez. 34 Capítulo 3 Operaciones morfológicas PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 3.8 Resultado de esqueletizar. Capítulo 4 Obtención del contorno Hemos visto en el capítulo 2 cómo obtener, a partir de la imagen original, una imagen con los pixels de la mano formando una región 4-conexa blanca, y con los pixels de fondo formando una o más regiones 4-conexas de diversos tonos de gris. Esto nos permite medir el número de pixels de cada región, y el número de regiones grises es 1 más que el número de agujeros en la mano. Ahora nos interesa contar el número de dedos de la mano, y para ello en este proyecto se ha seguido dos estrategias distintas. Una es usar operaciones morfológicas, como en el capítulo 3; otra, obtener el contorno de la mano, y medir su curvatura, usando las definiciones de la geometría diferencial [5]. Los dedos corresponderán a zonas con una curvatura más o menos marcada y estable. Puede parecer sencillo obtener el contorno a partir de nuestra imagen segmentada, pero hay que tener en cuenta que si queremos hallar la curvatura, debemos recorrer los pixels del contorno de forma ordenada. También debemos especificar qué tipo de pixel pertenece al contorno, y, como en el capítulo 2, tenemos que decidir entre 4-conexión y 8-conexión. 4.1. Especificación del contorno Para simplificar las cosas, en este momento vamos a colorear de negro todos los pixels de la imagen que no sean blancos. Al fin y al cabo, para hallar el contorno de la mano no hace falta distinguir entre regiones correspondientes al fondo y a agujeros en la mano. Podemos dar varias definiciones de qué pixel pertenece al contorno. En concreto, puede ser un pixel blanco que tiene al menos un 4-vecino negro, un pixel blanco con al menos un 8-vecino negro, un pixel negro con un 4- 36 Capítulo 4 Obtención del contorno vecino blanco, y un pixel negro con un 8-vecino blanco. Qué definición escojamos es cuestión en gran parte de gusto. En este proyecto se considera que un pixel es de contorno si es blanco y tiene un 8-vecino negro. Una consecuencia de esta elección es que el contorno forma una región 4-conexa. Si hubiéramos dicho que un pixel de contorno debe tener un 4-vecino negro, el contorno sería una región 8-conexa. 4.2. Algoritmos funcionales y mutación Hasta ahora hemos evitado el tema de dónde se ejecutan las operaciones sobre imágenes. Una imagen en el ordenador ocupa una determinada zona de la memoria, en la que están almacenados los pixels que la forman. ¿Sobre qué memoria PSfrag replacements ejecutamos nuestros algoritmos? Es, decir, si hay que cambiar algo en nuestra imaN ULO sobre el espacio de memoria ocupado por la imagen? La gen, ¿podemos cambiarlo ... respuesta es que no siempre es posible. Recta producida por mínimosen cuadrados Pensemos nuestro problema de obtener el contorno de una mano. En princiEsta recta es mejor pio, esto parece fácil: sólo hay que recorrer la imagen y pintar de negro cada pixel (x̄, ȳ) blanco que no tenga(x,vecinos negros. Así, al acabar, sólo los pixels de contorno y) quedan blancos. En la figura 4.1 vemos que esto puede dar problemas. d d eje x eje y φ uφ+ π2 Figura 4.1 Ejemplo de mutación. Una vez que hemos borrado el primer pixel de la imagen, la hemos mutado, y la imagen modificada tiene una frontera distinta. En particular, los vecinos de este pixel modificado forman ahora parte de la frontera. Este es un ejemplo de los peligros de modificar los objetos que maneja un procedimiento. ¿Como evitamos esto? Una solución consiste en hacer una copia de la imagen original en otra zona de memoria, y operar sobre ella. Observamos que un pixel debe ser borrado en la imagen original, y borramos su homólogo en la otra zona de memoria. Así, la imagen original no es modificada, y la imagen resultado queda almacenada en una zona de memoria distinta. Esta idea es la principal de la programación funcional [1], que pretende evitar la mutación de objetos. Tenemos otra opción: cuando encontramos un pixel blanco en la imagen que esté rodeado de pixels blancos, lo pintamos de gris sobre la imagen original. Al no 4.3 Algoritmos de obtención de contorno 37 pintarlo de negro, sus vecinos se van a comportar correctamente. Al final, sólo los pixels de frontera quedan blancos. Vamos a usar un enfoque parecido a éste. Vamos a recorrer la imagen y pintar de gris los pixels de frontera. Las ventajas de usar esta estrategia quedan claras también en el capítulo 3. 4.3. Algoritmos de obtención de contorno Hemos visto que vamos a colorear de gris los pixels del contorno y vamos a dejar el resto igual. Necesitamos una lista con los pixels del contorno recorridos de forma continua para poder estudiar la curvatura. Sabemos también que el contorno de la mano formará una región 4-conexa, o más bien, varias curvas cerradas 4conexas, ya que puede que haya agujeros en la mano. Como en el caso de el llenado de regiones, nos vamos a encontrar con más problemas de los previstos. 4.3.1. Algoritmo intuitivo Como nos interesa obtener los pixels de contorno de forma ordenada, el algoritmo más sencillo que podemos pensar es el siguiente: dado un punto inicial del contorno, lo pintamos de gris y lo almacenamos. Buscamos sus vecinos, y si alguno es un pixel de contorno, realizamos el mismo proceso recursivamente. C ONTORNO(p, contorno) colorear p de gris añadir p a contorno para cada vecino v de p si v es punto de contorno C ONTORNO(v, contorno) Este procedimiento colorea de gris todos los pixels del contorno, pero cuando examinamos la lista contorno que produce, vemos que no es continua, sino que consta de segmentos continuos inconexos unos con otros. Es decir, el procedimiento obtiene los pixels de contorno desordenadamente. ¿Por qué? Lo podemos entender mejor si modificamos el procedimiento anterior. Si vamos a rodear la mano de forma ordenada, dado un pixel de contorno, sólo hace falta saltar a un vecino de contorno. El contorno recorrerá un bucle y llegará al pixel inicial por otra dirección. 38 Capítulo 4 Obtención del contorno C ONTORNO(p, contorno) colorear p de gris añadir p a contorno para el primer vecino v de p que sea punto de contorno C ONTORNO(v, contorno) Este procedimiento da en la lista contorno un segmento continuo, pero ni mucho menos completo, y no colorea todos los pixels de frontera de la mano. Lo que ocurre es que por alguna razón, cuando el procedimiento recorre el contorno de la mano, llega a un callejón sin salida, un punto que no ofrece continuación posible. PSfrag replacements Nuestro segundo algoritmo termina aquí. Nuestro primer algoritmo, ya que busca N ULO para todos los vecinos de un pixel de contorno, realizará un retroceso un camino ... o backtracking, hasta encontrar un pixel con una continuación practicable. Por eso Recta producida por mínimosnuestro cuadrados primer algoritmo produce segmentos inconexos. Esta recta es mejor El problema está en que hay pixels de contorno que quedan “ahogados”, sin (x̄, ȳ) vecinos de contorno, y esto es lo que detiene nuestro recorrido secuencial. En la (x, y) figura 4.2d podemos ver un ejemplo. d eje x eje y φ uφ+ π2 Figura 4.2 Callejón sin salida. Es precisamente en los “pelos” de un pixel o dos de grosor donde se producen los estrangulamientos, así que la idea que surgió fue la de alisar el contorno de la mano antes de aplicar los algoritmos. En este proyecto se experimentó con cuatro formas de alisar el contorno de la mano: Filtrado previo de la imagen. Como veíamos en el capítulo 2, un filtrado eficiente requiere uso de FFT. Erosión: consiste en el borrado de todos los pixels de frontera, con lo cual eliminamos los molestos “pelos”. Dilatación: consiste en colorear de blanco los pixels adyacentes a la mano. Con esto suavizamos los picos de la imagen. Diversas combinaciones de varias erosiones y dilataciones. 4.3 Algoritmos de obtención de contorno 39 Todos los métodos anteriores tuvieron éxito parcial: conseguían aumentar la longitud de los segmentos continuos de contorno, pero nunca se podía recorrer la mano entera de forma continua. Además, las operaciones que realizan son caras, así que hacía falta otra estrategia. PSfrag replacements 4.3.2. El contorno visto como un grafo N ULO Al igual que . . .en el capítulo 2, la idea salvadora fue la de ver el contorno de la Recta producida por mano no como un conjunto de pixels sino como un grafo, en que un enlace entre mínimos cuadrados dos Estanodos recta esquiere mejor decir que son vecinos, figura 4.3. (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 4.3 Contorno de la mano visto como un grafo. De hecho, la idea surgió al tratar de resolver este problema, y fue aplicada más tarde a los problemas de llenado de regiones. El problema que estamos tratando de resolver se beneficia mucho más que los anteriores de verlo como un problema de grafos. En efecto, si construyéramos un grafo completo del contorno, los “pelos” que tanto nos estorban se ven sencillamente como fragmentos cortos del grafo que no nos llevan a ninguna parte. El camino que nos interesa en el grafo es cerrado, llega al pixel de partida, y seguramente es el más largo que contiene. Así que parece lógico dividir el problema en dos partes: A partir de la imagen obtener un grafo equivalente al contorno de la mano. Elegir como contorno el camino más largo que haya en el grafo. Este procedimiento para resolver el problema tiene dos pegas: Consume bastante memoria si en el grafo hay variedad de caminos. Requiere el manejo de estructuras de datos complejas, lo que siempre es pesado en C/C++. 40 Capítulo 4 Obtención del contorno La segunda pega hizo que en primer lugar se probase a implementar un procedimiento para enumerar los caminos de un grafo en lenguaje Scheme. (define (list-of-paths tree) (let ((descendants (node-descendants tree))) (if (null? descendants) (list (list (node-entry tree))) (accumulate append () (map (lambda (descendant) (map (lambda (path) (cons (node-entry tree) path)) (list-of-paths descendant))) descendants))))) En efecto, la ocupación de memoria resultaba alta, y la traducción a C++ parecía torpe. Por suerte, en Cormen et al. [3] hay un excelente algoritmo para obtener los caminos en un grafo, que no sólo consume poca memoria sino que tiene fácil traducción a C++. Es una versión fuerte de L LENAR - A - LO - ANCHO, o más bien, L LENAR - A - LO - ANCHO es una versión simplificada de este algoritmo. Este algoritmo es una versión completa de “breadth-first-search”, y se diferencia de L LENAR - A - LO - ANCHO precisamente en que almacena en memoria el camino seguido para llegar del pixel inicial a cada pixel. Además, no hace falta construir un grafo antes de poder aplicarlo, podemos aplicarlo directamente sobre la imagen. De nuevo vamos a usar una cola para almacenar los pixels que descubrimos, pero en esta ocasión los elementos que saquemos de ella los almacenaremos en una lista. Para recordar el camino seguido hasta llegar a un pixel, almacenaremos en cada pixel la dirección de su predecesor, es decir del pixel desde el cual lo hemos descubierto. Veamos cómo funciona: Buscamos un punto inicial del contorno, lo coloreamos de gris y lo metemos en la cola, hasta entonces vacía. A partir de ahora, mientras la cola no esté vacía, haremos lo siguiente: tomamos el primer elemento de cola, y para todos sus vecinos, si son parte del contorno, los coloreamos de gris, los metemos en cola, y asignamos el primer elemento de la cola como su predecesor. Después quitamos el elemento de la cola y lo almacenamos en una lista. C ONTORNO - BREADTH - FIRST 4.3 Algoritmos de obtención de contorno 41 buscar punto p de contorno color(p) ← gris predecesor(p) ← nulo encolar(Q, p) mientras Q no vacia q ← primero(Q) para cada v ∈ Vecinos(q) si v pertenece al contorno color(v) ← gris predecesor(v) ← p encolar(Q, v) descolar(Q) poner q en la lista de puntos de contorno La figura 4.4 nos muestra cómo funciona. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 4.4 Contorno con algoritmo “a lo ancho”. Como vemos, el resultado del algoritmo es una lista con todos los pixels de frontera. Cada pixel en la lista tiene un puntero a su predecesor, y podemos, dado un pixel de la lista, ir a su predecesor, y al predecesor de éste, y así sucesivamente, hasta llegar al punto de inicio del algoritmo. Es decir, en la lista que obtenemos como resultado podemos extraer fácilmente el camino recorrido hasta llegar a un 42 Capítulo 4 Obtención del contorno pixel. Pero hay todavía una propiedad más interesante: los caminos que enconPSfrag replacements tramos de esta forma son siempre los más cortos entre el pixel inicial y el dado, como se demuestra en Cormen N ULOet al. [3]. Así que podemos ver el problema así. .. Encontrado el punto inicial de. frontera, construimos la lista de puntos de frontera, Recta producida por cuyos dos últimos elementos serán los pixels opuestos al inicial. Recorriendo el mínimos cuadrados Esta uno rectade es mejor camino desde cada los dos hasta el pixel inicial, tenemos dos curvas que (x̄, ȳ) unidas, forman el contorno. Al ser cada una de ellas la más corta desde el pixel (x, y) dado hasta el inicial, evitamos los pelos que antes nos molestaban, como vemos en d la figura 4.5. d eje x eje y φ uφ+ π2 Figura 4.5 Recorriendo el camino más corto evitamos callejones sin salida. Vemos entonces, cuál es la estrategia: aplicar C ONTORNO - BREADTH - FIRST a la imagen para extraer la lista de pixels de frontera. Recorrer el camino desde el último pixel de la lista hasta el primero, lo que nos da una mitad del contorno. Recorrer el camino desde el penúltimo pixel de la lista hasta el primero, lo que nos da la otra mitad del contorno. Formar una lista con la primera mitad del contorno, y con la segunda invertida. Es decir tendremos la lista siguiente: (último pixel . . . PSfrag replacements pixel inicial . . . penúltimo pixel), que da un contorno continuo y completo, como N ULO queríamos. ... Recta producida por Podemos ver en la figura 4.6 el contorno resultante en una imagen de prueba. mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 4.6 El pixel inicial es negro. Los pixels blancos sueltos son callejones sin salida que el algoritmo evita. Este procedimiento tiene un comportamiento muy bueno, pero un grave problema: puede ser que el pixel en que iniciemos el recorrido sea parte de una zona 4.3 Algoritmos de obtención de contorno 43 PSfrag replacements problemática, conectada con la mano por un canal estrecho. En ese caso, las dos N ULO mitades de contorno que . . . hemos hallado antes no se reunirían en el punto inicial, y Recta producida por tendremos un contorno discontinuo, como podemos ver en la figura 4.7. mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 4.7 Las dos mitades del contorno no casan. Se podía pensar en formas de evitar este nuevo obstáculo, pero es complicar más un algoritmo de por sí complicado. Parece que la mejor alternativa es eliminar las zonas que nos molestan. PSfrag4.3.3. replacements Limpieza del contorno N ULO Dado que todos nuestros problemas se deben a que ciertas zonas producen ... Recta producidasin porsalida podemos pensar en borrar estas zonas, para lo cual debemos callejones mínimos cuadrados ser capaces, en primer lugar, de caracterizarlas: las zonas que parecen molestarnos Esta recta es mejor son canales (x̄,de ȳ)un pixel o dos de grosor, como vemos en la figura 4.8. (x, y) d d eje x eje y φ uφ+ π2 Figura 4.8 Regiones que producen estrangulamientos de contorno. Los pixels de frontera están coloreados. Anteriormente habíamos intentado erosionar la imagen para regularizar el contorno. Erosionar no es más que eliminar los pixels de frontera, y como vemos en la figura 4.8, esto borraría tanto los “canales” de un pixel de anchura como los de dos. El único problema es que convierte canales de tres pixels de ancho en canales de uno, es decir, nos borra las zonas molestas pero introduce otras. Podemos reme- PSfrag replacements N ULO 44 Capítulo 4 Obtención del contorno ... Recta producida por mínimos cuadrados diar esto si acompañamos la erosión con una dilatación, que consiste en añadir una Esta recta es mejor nueva capa de pixels alrededor de una región. Con esto, los canales de tres pixels (x̄, ȳ) su grosor original, y los de dos y un pixel no rebrotan, como vemos en la (x,recobran y) figura d 4.9. d eje x eje y φ uφ+ 2π PSfrag replacements Figura 4.9 Erosión seguida de dilatación. N ULO La secuencia de una erosión seguida de una dilatación es tan común que tiene ... nombre Recta producida por propio, opening. En nuestro caso, no siempre vale, y en particular, en el mínimos cuadrados caso de que el canal estrecho conecte con una pequeña región blanca, un “opening” Esta recta es mejor eliminaría el canal estrecho pero no la región blanca, y el contorno de la mano daría (x̄, ȳ) un salto a esa región, rompiendo la continuidad, como vemos en la figura 4.10. (x, y) d d eje x eje y φ uφ+ π2 Figura 4.10 Un “opening” no siempre vale. Por fin la solución al problema se presentó. Recordemos que al hallar la frontera hemos coloreado los pixels de contorno de gris, y los hemos almacenado en una cola. Al estar coloreados de gris, ahogan la conexión entre las regiones blancas pequeñas de la figura 4.10 y la región correspondiente a la mano propiamente dicha. Esto quiere decir que podemos borrar estas regiones blancas indeseadas usando los algoritmos de llenado del capítulo 2. Podemos ver, una vez hecho esto, que los pixels que nos molestan son aquellos pixels de frontera que no tienen vecinos blancos. Simplemente recorremos la cola de pixels de frontera y borramos estos pixels. Después, pintamos de blanco los pixels de frontera restantes, y podemos ya recorrer el contorno ordenadamente. Formalizando: L IMPIAR - CONTORNO(Q) ;;Q es la cola de frontera borrar regiones blancas ahogadas para cada p ∈ Q 4.4 Hallando el contorno, por fin 45 si p no tiene vecinos blancos color(p) ← negro para cada p ∈ Q color(p) ← blanco El procedimiento tiene un comportamiento excelente. Produce regiones que PSfrag replacements podemos recorrer de forma continua, es decir, regiones con un contorno con buenas N ULO propiedades. Como parte del proyecto se escribió también un test para verificar que ... Recta producida por sea continuo y que acabe en su punto de inicio. Después de pasar por el contorno mínimos cuadrados L IMPIAR - CONTORNO, todas las imágenes usadas en el proyecto pasaron este test. Esta recta es mejor Veamos (x̄, ȳ) cómo se comporta L IMPIAR - CONTORNO en el caso de la figura anterior: (x, y) d d eje x eje y φ uφ+ π2 Figura 4.11 Acción de L IMPIAR - CONTORNO. 4.4. Hallando el contorno, por fin La estrategia de limpiar el contorno da resultados excelentes, y en la región resultante cada pixel de frontera tiene exactamente dos vecinos de frontera, lo cual es la condición necesaria para que el contorno tenga un comportamiento intuitivo. Aplicar el primer algoritmo de este capítulo ya da un contorno ordenado y completo, pero por comodidad vamos a manejar una pila explícitamente, al igual que en el algoritmo de llenado a lo profundo. C ONTORNO(p, contorno) colorear p de gris poner p en la pila repetir q ← primer elemento de la pila buscar un vecino v de q que sea de contorno colorear v de gris 46 Capítulo 4 Obtención del contorno poner v en la pila hasta que lleguemos al punto inicial en este momento la pila contiene el contorno ordenado La razón de usar una pila tiene que ver con la torpeza de C++ para manejar listas y recursión. Cuando el algoritmo concluye, la pila contiene los pixels de contorno de la mano recorridos en orden. El problema de encontrar el contorno está por fin completamente resuelto. Capítulo 5 Procesado del contorno Ya hemos resuelto el problema de obtener una lista ordenada de los pixels de frontera de la mano. Ahora nos interesa, a partir de esta lista, contar el número de dedos extendidos en nuestra imagen. Para ello, estudiaremos la curvatura del contorno de la mano. La definición de curvatura que usaremos aquí será una adaptación de la que usan los libros de geometría diferencial [5]. 5.1. Curvatura de arcos continuos Supongamos una curva dada por su vector de posición, una función vectorial diferenciable r : I ⊂ R → R3 . r(t) = (x(t), y(t), z(t)) Cada coordenada es una función diferenciable en el intervalo de definición, I. Supongamos además que r(t) es regular, es decir, su derivada no es nunca nula, r0 (t) 6= 0 para todo t ∈ I. Podemos hallar la longitud de arco así: s(t) = Z t kr0 (t)k dt t0 Como r0 (t) no se anula, s(t) es biyectiva, y por tanto, podemos hallar t = f (s), con lo cual, podemos parametrizar la curva en función de su longitud de arco s, y definirla mediante una función v(s). Esta función tiene la propiedad de que 48 Capítulo 5 Procesado del contorno kv0 (s)k = 1. En efecto, Z s 0 0 kv (s)k ds = Z t kr0 (t)k dt = s t0 Para una curva parametrizada por su longitud de arco, v 0 (s) se denomina el vector normal, ya que es tangente a la curva y de módulo unidad. Si hallamos la derivada del vector normal obtenemos v 00 (s), que es perpendicular a v0 (s). En efecto, kv0 (s)k = 1 d 0 (v (s) · v0 (s)) = 0 ds d v0 (s) · v0 (s) = 0 ds Se suele definir como curvatura en un punto al módulo de la derivada del vector normal en ese punto, kv00 (s)k. Esta definición da los resultados esperados, en que la curvatura es el inverso del radio de la circunferencia que mejor aproxima la curva en el punto bajo estudio. Puesto que es el módulo de la derivada del vector normal, que es unitario, la curvatura mide la velocidad angular del vector normal. Esto es lo que necesitamos imitar en nuestro caso. 5.2. Nuestra definición de curvatura En este proyecto no usamos en ningún caso funciones continuas. Hemos visto en el capítulo anterior cómo obtener una lista ordenada de los pixels de contorno, y es a partir de esta lista que tenemos que encontrar la curvatura del contorno de la mano. Resultaría muy complicado encontrar una descripción del contorno mediante una función parametrizada por longitud de arco, así que la definición de curvatura vista en la sección anterior no es de utilidad. Sin embargo, si definimos la curvatura como la velocidad de giro del vector tangente al contorno, tenemos más posibilidades. Podemos hallar el vector tangente al contorno en un pixel de forma sencilla. Basta restar a las coordenadas del pixel bajo estudio las coordenadas del pixel anterior en la lista. Usando funciones trigonométricas inversas podemos hallar el ángulo del vector tangente. La diferencia de ángulos de los vectores tangentes en pixels sucesivos es la velocidad angular 5.2 Nuestra definición de curvatura 49 que queríamos calcular. En este proyecto, por eficiencia, un pixel se representa mediante un puntero a un lugar de la memoria que contiene un nivel de gris (o color). Para tener un manejo sencillo de los pixels en los procedimientos de detección de curvatura, cambiaremos a una representación del pixel como un número complejo, cuya parte real es la columna ocupada por el pixel, y cuya parte imaginaria es la fila, siendo la fila inferior en la imagen la 0. Entonces, convertimos la lista de pixels a una lista de números complejos. La diferencia (restar a cada número el anterior) de esta lista es la lista de tangentes al PSfrag replacements contorno. Formamos la lista de argumentos de los vectores (números complejos) tangentes, y N suULO diferencia nos da la lista de curvaturas en cada punto. ... sólo unpor problema con esta definición, y es que debido al proceso de digRectaHay producida mínimos cuadrados italización, el contorno es demasiado rugoso, hay aliasing [7], como vemos en la Esta recta es mejor figura 5.1. (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 PSfrag replacements N ULO Figura 5.1 La digitalización arruga el contorno. ... evitar por esto vamos a desechar algunos de los pixels de contorno. De esRectaPara producida mínimos cuadrados ta forma, perdemos detalle, pero el contorno es más regular, como vemos en la Esta recta es mejor figura 5.2. (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 5.2 Usamos sólo algunos pixels. Debemos encontrar un punto medio entre tener un contorno con mucho detalle pero con falsas rugosidades, y uno más regular y menos fiel (a la imagen digitalizada). La estrategia seguida en este proyecto es usar sólo un pixel de cada diez 50 Capítulo 5 Procesado del contorno de contorno. Es decir, dada la lista de pixels de contorno, formamos una lista con cada décimo pixel representado como número complejo. Después formamos las listas de tangentes, argumentos y curvaturas sin más complicaciones. 5.3. El grafo de curvatura Después de lo visto en las secciones anteriores estamos ya en condiciones de examinar el contorno de una mano. Como ejemplo observemos el procesado de una imagen correspondiente a la letra ‘q’, en las figuras 5.3 y 5.4. PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 5.3 Postura correspondiente a la letra ‘q’. El recorrido del contorno comienza en su punto más alto, y continúa hacia la izquierda, en sentido contrario a las agujas del reloj. En la gráfica de curvatura, las zonas con curvatura positiva corresponden a convexidades, o “giros hacia dentro”, del contorno, mientras que las zonas con curvatura negativa corresponden a concavidades. Las zonas con curvatura alta (≈ 1,5) indican una esquina o un giro muy brusco. Las zonas con curvatura nula o muy pequeña son casi rectas. Podemos ver en la gráfica que entorno a 90 en la escala horizontal (recordar que estamos observando un pixel de cada diez), hay una zona de curvatura nula. Esto corresponde a la frontera izquierda de la mano, que debido a que coincide con el límite de la imagen, es plana. A los lados de esta zona hay picos pronunciados de curvatura positiva, que se deben a que el contorno de la mano es casi perpendicular 5.3 El grafo de curvatura 51 450 400 350 300 250 200 150 100 0 100 200 300 400 500 600 (a) Puntos de contorno 2 1.5 1 0.5 0 -0.5 -1 -1.5 -2 -2.5 0 50 100 150 200 (b) Gráfica de curvatura Figura 5.4 Análisis de la postura correspondiente a la letra ‘q’. 250 52 Capítulo 5 Procesado del contorno al límite izquierdo. En la gráfica, a la izquierda de la zona plana hay una zona en que la curvatura es apreciable y alterna de signo con frecuencia. Esto corresponde a la zona plana pero rugosa de la parte superior izquierda del contorno. Las zonas rugosas se van a ver caracterizadas precisamente por una curvatura apreciable y de signo alternante. A la derecha de la zona plana hay también alternancia de signos, pero esta vez la curvatura es pequeña, debido a que el contorno es suave. Los giros muy suaves que vienen a continuación se muestran en una ligera preponderancia de valores, o bien positivos (giro hacia dentro) o bien negativos, pero pequeños. Llegados al nudillo del dedo pulgar vemos cuatro valores consecutivos de curvatura positiva, rodeados de nulos, y más adelante, en la yema del pulgar, vemos tres valores positivos de curvatura, seguidos de un pico negativo, correspondiente al giro brusco en el contorno, donde comienza un nuevo dedo. Después de este giro brusco hay una zona de curvatura positiva pequeña, debida a la postura flexionada del índice, seguida de un pico negativo (comienza un nuevo dedo). Hay otros dos giros bruscos más, correspondientes a las yemas del medio y anular. Entre los giros bruscos hay 6 o 7 valores positivos de curvatura, correspondientes a las yemas. Como vemos, contar el número de picos de curvatura no nos resulta útil. Los picos no sólo son producidos por los dedos, sino por el “choque” de la mano con los límites de la imagen, e incluso por grietas debidas a sombra, que no nos interesan. Sin embargo, parece que las yemas de dedos producen zonas de 6 o 7 puntos consecutivos de curvatura positiva, y eso sí es aprovechable. En las figuras 5.5 y 5.6, correspondientes a la letra ‘ch’, podemos ver cómo cambia esto para el caso de que haya dedos extendidos. Las yemas o nudillos de los dedos, al igual que antes, producen zonas de 5 o 6 puntos de curvatura positiva en la gráfica de curvaturas. Pero en esta ocasión, los dos dedos extendidos dan pixels consecutivos de curvatura positiva, rodeados de zonas relativamente planas. El tramo intermedio entre los dos dedos extendidos es, en esta ocasión, más achatado, aunque sigue produciendo una zona de gran curvatura negativa. De nuevo, entorno a 50 en la escala horizontal, la zona de curvatura nula corresponde al margen izquierdo de la imagen. Los nudillos del meñique y del anular son esta vez menos claros, y no llegan a producir más de 4 puntos consecutivos con curvatura positiva. Los dedos, en esta ocasión, están claramente separados, pero no siempre vamos a tener esta ventaja. Vemos en las figuras 5.7 y 5.8 el caso correspondiente a la letra ‘r’. 5.4 Caracterización de elementos del contorno 53 PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 5.5 Postura correspondiente a la letra ‘ch’. En esta ocasión, los dos dedos extendidos no están separados. Podemos ver en la gráfica que entre las yemas del índice y el medio hay un pico de curvatura negativa, correspondiente a un giro brusco entre dedo y dedo, pero no hay una zona plana apreciable. Aunque sí hay zonas planas a los lados del par de dedos. Como se puede ver, un dedo extendido no siempre está rodeado de zonas de curvatura pequeña. 5.4. Caracterización de elementos del contorno En la sección anterior hemos visto que las zonas más o menos rectas del contorno de la manos se caracterizan por que, en la gráfica de curvaturas, producen valores de curvatura pequeños, y de signo alternante. Si los valores absolutos de curvatura son pequeños, tenemos una zona recta suave. Si los valores absolutos de curvatura son mayores, tenemos una zona recta, pero rugosa (o ruidosa). Para determinar si una zona es recta, entonces, tenemos que examinar varios puntos consecutivos en la gráfica de curvaturas. En este proyecto se examinan 7 posiciones consecutivas. Tenemos que definir también qué se entiende por “curvatura pequeña”. En este proyecto se ha usado un umbral de 0.1. Una curvatura con un valor menor es “pequeña”. Las yemas de los dedos se han visto distinguidas por que producen valores 54 Capítulo 5 Procesado del contorno 450 400 350 300 250 200 150 0 100 200 300 400 500 600 700 (a) Puntos de contorno 2 1.5 1 0.5 0 -0.5 -1 -1.5 -2 -2.5 0 50 100 150 200 (b) Gráfica de curvatura Figura 5.6 Análisis de la postura correspondiente a la letra ‘ch’. 250 5.5 Detección de elementos del contorno 55 PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 5.7 Postura correspondiente a la letra ‘r’. consecutivos de curvatura apreciable (mayor que el umbral de 0.1). Puesto que hemos recorrido el contorno de la mano en sentido contrario a las agujas del reloj, las yemas, que son salientes, o convexidades, giran también en sentido contrario a las agujas del reloj. Esto quiere decir que producen curvatura positiva. Es posible también que entre dos dedos extendidos haya una zona achatada como la de la figura 5.6. En este caso tendremos varios puntos consecutivos de curvatura negativa. Los giros producen también valores consecutivos positivos (para giros que producen convexidad) o negativos, aunque no tantos como las yemas. Finalmente, las zonas rugosas del contorno no se pueden identificar con ninguno de los casos anteriores. 5.5. Detección de elementos del contorno Ahora que hemos caracterizado los elementos del contorno es necesario encontrar un método para detectarlos. Puesto que todos los elementos de contorno se caracterizan por puntos consecutivos de curvatura similar, vamos a recorrer el grafo de curvatura examinando varios puntos al mismo tiempo; en nuestro caso, 7. Para cada conjunto de 7 puntos consecutivos, vamos a contar el número de puntos con curvatura positiva, negativa, y nula. Hay que puntualizar que consideramos la curvatura positiva cuando es mayor que un umbral (en este caso 0.1), negativa 56 Capítulo 5 Procesado del contorno 400 350 300 250 200 150 100 0 100 200 300 400 500 600 700 (a) Puntos de contorno 1.5 1 0.5 0 -0.5 -1 -1.5 -2 -2.5 -3 0 50 100 150 200 (b) Gráfica de curvatura Figura 5.8 Análisis de la postura correspondiente a la letra ‘r’. 250 5.5 Detección de elementos del contorno 57 cuando es menor que el umbral cambiado de signo, y nula en el resto de los casos. Vamos a asignar a cada tipo de elemento una letra. Si en un conjunto de 7 puntos 6 o 7 de ellos tienen: curvatura positiva tenemos una yema de dedo. Letra c. curvatura negativa tenemos una zona achatada entre dedos. Letra C. curvatura nula tenemos una zona recta. Letra s. Si 4 o 5 tienen: curvatura positiva tenemos un giro suave “hacia dentro”. Letra g. curvatura negativa tenemos un giro suave “hacia fuera”. Letra G. curvatura nula tenemos una zona recta. Letra s. En cualquier otro caso, tenemos una zona ruidosa de contorno, que denotamos con la letra n. Vamos a producir una lista con los elementos de contorno denotados por sus correspondientes letras. Por ejemplo, en el caso de la postura estudiada en la figura 5.6 obtenemos: nnnnnnnnnnnnnnnsssnssnnnnnnnnnnnnssssssnnggnnssssssssssss sssssssnnnnnnssssssssssnnnnnnGnGnnnnnssssssssssssssssssss sssssssssssssssssggccccgggssssssnnnnnnnnsssssssGGGGnnssss ssssssssnnnnnnnsngggcccccggsssssssssssssssssnnnnnnnnnnggg cccggcggnnnnngggcggnnn En la última linea podemos ver varias cs separadas por dos gs. Las cs consecutivas corresponden claramente a la misma yema, pero la siguiente ¿corresponde a otra? La respuesta es que no tiene sentido tener dos yemas separadas por tan poco espacio. Para evitar estas separaciones indeseadas, vamos a convertir los puntos vecinos a una c en cs, es decir, vamos a dilatar las yemas de dedos. Después de esto, tenemos para el caso anterior: nnnnnnnnnnnnnnnsssnssnnnnnnnnnnnnssssssnnggnnssssssssssss sssssssnnnnnnssssssssssnnnnnnGnGnnnnnssssssssssssssssssss sssssssssssssssssgccccccggssssssnnnnnnnnsssssssGGGGnnssss ssssssssnnnnnnnsnggcccccccgsssssssssssssssssnnnnnnnnnnggc cccccccgnnnnnggcccgnnn 58 Capítulo 5 Procesado del contorno Ahora, todas las yemas producen zonas de cs consecutivas. Para contarlas no tenemos más que contar el número de transiciones de c a una letra distinta. Con esto ya podemos contar el número de yemas de dedo. En este caso particular, tenemos 4. Capítulo 6 Extracción de características En los capítulos anteriores hemos visto cómo segmentar la imagen, aplicar operaciones morfológicas, obtener el contorno de la mano, y extraer de él distintos elementos. Aún hay algunas características de la mano que podemos calcular de forma sencilla, como su ángulo de inclinación. En este capítulo describimos todas las características de la mano estudiadas en este proyecto, y formalizamos los métodos usados para calcularlas. 6.1. Área de la mano En el capítulo 2 vimos varios algoritmos de llenado de regiones. En todos ellos es fácil contar el número de pixels de la región llenada. En concreto, el algoritmo usado en este proyecto es: L LENAR - A - LO - ANCHO(p, gris_inicial, gris_final) area = 0 color(p) ← gris_final encolar(Q, p) mientras Q no esté vacía q ← primero(Q) para cada vecino v de q si color(v) = gris_inicial color(v) = gris_final encolar(Q, v) descolar(Q) area = area + 1 60 Capítulo 6 Extracción de características Estamos aprovechando que cada pixel de la región va a ser puesto en cola (y extraído de la cola) exactamente una vez durante el proceso de llenado. El procedimiento presentado aquí (traducido a C++) es el usado en el programa de ordenador desarrollado en el proyecto. Como veíamos en el capítulo 2, vamos a suponer que en nuestras imágenes hay una sola mano, y por tanto, después de la segmentación, y de la eliminación de ruido, una sola región habrá sido llenada de blanco. La diferencia entre las regiones blancas que debemos borrar y la mano es que la mano debe tener un área superior a un umbral, que en este proyecto es 100000 pixels. El número de pixels en esta región es el área de la mano. 6.2. Número de agujeros De forma parecida, durante la etapa de segmentación y eliminación de ruido vamos a eliminar las regiones negras menores de un umbral, en este caso 2000 pixels. De las regiones que sobrevivan, la más grande corresponderá el fondo, y si hay más regiones negras que sobrevivan, corresponderán a huecos entre los dedos, o agujeros. En otras palabras: después de la etapa de segmentación tendremos una región blanca correspondiente a la mano, y varias regiones en distintos niveles de gris. Una de estas regiones es el fondo. El resto son agujeros. Número de agujeros = Número de regiones grises − 1 6.3. Perímetro En el capítulo 5 vimos que era conveniente formar una lista con uno de cada diez pixels de contorno, representados como números complejos. Gracias a usar sólo un pixel de cada diez, evitamos el aliasing, y tenemos un contorno más fiel al original. Como los pixels están representados como números complejos, es fácil hallar el perímetro: dada la lista de frontera, hallamos la diferencia de la lista, hallamos su valor absoluto, y sumamos. P ERIMETRO (lista) perimetro ← 0 para i ← 1 hasta fin 6.4 Curvosidad 61 v ← lista[i] − lista[i−1] perimetro ← perimetro + |v| ;; En este punto, perimetro contiene el resultado 6.4. Curvosidad Este índice, de nombre inventado, mide la proporción de curvatura en el contorno. Una curvosidad alta debería indicar que el contorno es muy ondulado, una curvosidad baja, que el contorno es relativamente poco curvo, como la frontera de una figura convexa. Por desgracia, como hemos podido ver en el capítulo 5, el ruido y las sombras en la imagen pueden hacer que zonas rectas de contorno den zonas de curvatura elevada y alternante en la gráfica de curvaturas. La curvosidad no es más que la media de los valores absolutos de curvatura calculados como en el capítulo anterior. 6.5. Número de dedos El capítulo 5 ha estado dedicado a la cuenta del número de dedos. De forma resumida, dada la lista de puntos de contorno, formamos la lista de tangentes al contorno, la lista de argumentos de éstos, y la lista de diferencias de argumentos, o de curvaturas. Después recorremos los puntos de la lista de curvaturas, y asignamos a cada uno un carácter que lo identifica como parte de una recta, un giro, o una yema de un dedo. El número de dedos es el número de agrupaciones de puntos que pertenecen a una yema. 6.6. Circularidad La circularidad es una medida muy usada en la clasificación de imágenes. Su objetivo es indicar cuánto difiere la figura bajo estudio de un círculo. Un círculo de radio r un perímetro P (circunferencia) de 2πr y un área A de πr 2 . Esto quiere decir que para un círculo, P2 = 4πA Definiremos entonces la circularidad por C= P2 4πA 62 Capítulo 6 Extracción de características que es 1 en caso de un círculo, y mayor de 1 en cualquier otro caso (el círculo es la figura que encierra mayor área para un perímetro dado). Puesto que ya hemos calculado el perímetro y el área de la mano, calcular la circularidad es trivial. 6.7. Ángulo de inclinación La inclinación de la mano es posiblemente uno de los parámetros de mayor interés. Pero dado que la mano es una figura bidimensional, ¿cómo calculamos esta inclinación? La estrategia inicial del proyecto fue ajustar una recta a la mano por el método de mínimos cuadrados. Es decir, tomamos la mano como una nube dePSfrag puntos,replacements y hallamos la recta de regresión que minimiza la suma de los cuadrados ULO de las diferenciasNen altura (coordenada y) entre puntos de la nube y puntos de la . . recta que tengan igual.abscisa (coordenada x). Observar la figura 6.1. Recta producida por mínimos cuadrados Esta recta es mejor eje y (x, y) d d (x̄, ȳ) φ uφ+ π2 eje x Figura 6.1 Recta de regresión. 6.7 Ángulo de inclinación 63 Desarrollando obtenemos: y = mx + n tal que ∑[yi − mxi − n]2 es mínimo i ∂ [yi − mxi − n]2 = 0 ∂m ∑ i ∑ 2(yi − mxi − n)(−xi ) = 0 i ∑ yi xi = m ∑ x2i + n ∑ xi i i i ∂ [yi − mxi − n]2 = 0 ∂n ∑ i ∑ 2(yi − mxi − n)(−1) = 0 i ∑ yi = m ∑ xi + n ∑ 1 i i i Para resolver el sistema de ecuaciones ∑ y x = m ∑ x2 + n ∑ x i i i i i i i ∑ y = m ∑ x + n ∑ 1 i i i i i es conveniente usar una serie de números muy conocidos en estadística: las medias ∑i xi ∑i 1 ∑ yi ȳ = i ∑i 1 x̄ = (6.1) 64 Capítulo 6 Extracción de características y los momentos centrales de orden 2 µ20 = ∑(xi − x̄)2 (6.2) µ11 = ∑(xi − x̄)(yi − ȳ) (6.3) µ02 = ∑(yi − ȳ)2 (6.4) i i i Con estas definiciones, es fácil llegar a la solución del sistema de ecuaciones ( 6.1), como demuestra Papoulis [10]: m= µ11 µ20 n = ȳ − mx̄ Puesto que m es la pendiente de la recta de regresión que se ajusta a la mano, tomamos su arco-tangente para tener el ángulo de inclinación. Ángulo de inclinación = arctan(m) Por cierto, en todas las ecuaciones usadas anteriormente hemos supuesto que el peso de los pixels de la mano era 1, de los demás, 0. En el caso más general, deberíamos haber multiplicado por el peso, y obtendríamos, por ejemplo: x̄ = ∑i xi F(xi , yi ) ∑i F(xi , yi ) pero todos los resultados obtenidos antes seguirían siendo válidos. Este método tiene serias pegas. La principal es que debido a que minimiza las diferencias de ordenadas, tiende a dar preferencia a las líneas con orientación horizontal, cuando a veces es más conveniente una línea de orientación cercana a la vertical. Vemos un ejemplo en la figura 6.2. El problema de la estrategia anterior es que toma como distancia entre un punto y la recta de regresión la longitud del segmento vertical entre el punto y la recta (figura 6.1). Para regiones de orientación más o menos horizontal esto es adecuado, pero si la región bajo estudio tiene una pendiente apreciable, no funciona bien. Nos interesa, más bien, definir la distancia entre un punto y una recta como la longitud del segmento que une el punto a la recta y es perpendicular a ella (figura 6.3). La recta tal que la suma de los cuadrados de las distancias de los puntos a PSfrag replacements N ULO 6.7 ... Ángulo de inclinación 65 Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ 2π Recta producida por mínimos cuadrados PSfrag replacements Figura 6.2 El método de mínimos cuadrados no es adecuado. N ULO . .que . llamaremos eje de la región. ella sea mínima es lapor Recta producida mínimos cuadrados Esta recta es mejor eje y d d (x, y) (x̄, ȳ) φ uφ+ π2 eje x Figura 6.3 Una distancia distinta. Concretando, buscamos una recta r tal que D2 (r) = ∑ d 2 ((xi , yi ), r) i sea mínimo. Esto recuerda a la definición en física del momento de inercia de un sólido respecto de un eje. De hecho, es la misma, o sea que llamaremos I(r) a D2 (r). Reformulamos el problema: se trata de encontrar una recta respecto de la cual el momento de inercia de la región sea mínimo. Llamaremos a esta recta el eje de la región. Es un hecho conocido [6] que dado un haz de rectas paralelas, un objeto tiene el momento de inercia menor respecto de la recta que pasa por su centro de gravedad. Sabemos, entonces, que el eje de la región pasa por su centro de gravedad, cuyas coordenadas son (x̄, ȳ), y sólo necesitamos saber el ángulo del eje. Vamos a hallar el momento de inercia como función de este ángulo. Podemos calcular la distancia de un punto a una recta fácilmente, sin más que hallar el producto escalar de un vector unitario normal a la recta con la diferencia 66 Capítulo 6 Extracción de características entre el punto dado y un punto sobre la recta (figura 6.4). PSfrag replacements (x, y) N ULO ... Recta producida por d mínimos cuadrados Esta recta es mejor uφ+ 2π d eje x eje y (x̄, ȳ) φ Figura 6.4 Distancia entre punto y recta. Esto es, d((xi , yi ), r) = uφ+ π2 · (x − x̄, y − ȳ) donde uφ+ π2 es perpendicular a la recta r. Calculamos, entonces, el momento de inercia de la región respecto de una recta que pasa por su centro de masas, (x̄, ȳ), con ángulo φ, I(φ) = ∑(uφ+ π2 · (xi − x̄, yi − ȳ))2 = i ∑(cos(φ + 2 π π 2 )(xi − x̄) + sin(φ + 2 )(yi − ȳ)) = i ∑(cos(φ)(yi − ȳ) − sin(φ)(xi − x̄))2 = i ∑ cos 2 (φ)(yi − ȳ)2 + sin2 (φ)(xi − x̄)2 − 2 sin(φ) cos(φ)(xi − x̄)(yi − ȳ) = i I(φ) = µ02 cos2 φ + µ20 sin2 φ − µ11 sin 2φ donde µ20 , µ11 , µ02 son los momentos centrales de segundo orden definidos en ( 6.2)( 6.3)( 6.4), página 64. La función I(φ) = µ02 cos2 φ + µ20 sin2 φ − µ11 sin 2φ (6.5) es muy interesante. Nos muestra que el momento de inercia de la región respecto de un eje varía sinusoidalmente con el ángulo del eje. En concreto, es una sinusoide de periodo π. Esto quiere decir que tiene, a lo largo del intervalo [−π, π], 6.8 Momentos principal y secundario 67 dos máximos, en ángulos diferenciados en π (es decir, en la misma dirección y sentido opuesto), y dos mínimos, separados π 2 de los máximos. Esto quiere decir que la región tiene un eje respecto del cual su momento de inercia es mínimo, que llamaremos eje principal, y un eje respecto del cual su momento de inercia es máximo, que llamaremos eje secundario. Estos dos ejes son siempre perpendiculares, no importa de qué región se trate. Así mismo, llamaremos momento principal al momento de inercia respecto del eje principal, y momento secundario al momento respecto del eje secundario. Para hallar el ángulo de inclinación de los dos ejes, derivamos la función I(φ) respecto de φ. Usando la fórmula 6.5 obtenemos: ∂I = (µ20 − µ02 ) sin 2φ − 2µ11 cos 2φ ∂φ (6.6) que se anula para tan 2φ = 2µ11 µ20 − µ02 (6.7) Esta ecuación tiene cuatro soluciones en [−π, π]. Vamos a usar sólo las que se encuentran en [− π2 , π2 ], que son un máximo y un mínimo separados π2 . Para saber cuál es cuál, simplemente sustituimos en la fórmula 6.5 que da el momento de inercia,. El ángulo que da el momento de inercia mínimo es el ángulo de inclinación de la mano. 6.8. Momentos principal y secundario Tal y como hemos visto en la sección anterior, el momento de inercia de la región que corresponde a la mano tiene dos direcciones privilegiadas, una para la que es máximo y otra para la que es mínimo. El momento de inercia en sí es una magnitud interesante: nos da una idea de la separación entre los pixels de la región y el eje. Una región muy estrecha tendrá, respecto de su eje longitudinal, un momento de inercia muy pequeño. Una región más achatada tendrá un momento de inercia mayor (figura 6.5). Puesto que hemos calculado los ángulos de los ejes principal y secundario, calcular los correspondientes momentos es fácil. PSfrag replacements N ULO ... 68 producida por Recta mínimos cuadrados Esta recta es mejor Capítulo 6 Extracción de características (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 6.5 Momentos de inercia pequeño y grande. 6.9. Relación de aspecto Una de las magnitudes más usadas en los libros de Visión Artificial para clasificar objetos es la relación de aspecto, que es el cociente entre la “altura” y la “anchura” del objeto. Qué entendamos por altura y anchura del objeto está sujeto a variación, pero la estrategia más comúnmente utilizada es obtener el rectángulo abarcante mínimo (minimum enclosing rectangle), es decir, el rectángulo más pequeño dentro del cual cabe el objeto. Por supuesto, esto requiere alguna estrategia para calcular el ángulo de inclinación del objeto, como hemos visto antes. Pero ya que hemos calculado los momentos de inercia principal y secundario, es interesante usarlos. El momento de inercia de un objeto respecto de un eje es tanto mayor cuanto más ancho es respecto del eje. Por ejemplo, el momento de inercia de una varilla de longitud l y masa m respecto de un eje perpendicular a ella que pasa por su centro es 1 2 12 ml (ver [6]). Entonces, usaremos como relación de aspecto del objeto el cociente entre sus momentos de inercia principal y secundario. Puesto que el momento de inercia parece ser proporcional al cuadrado de la anchura del objeto, esta relación de aspecto será aproximadamente el cuadrado de la relación de aspecto tradicional. 1 6.10. Cuestiones de implementación En las secciones anteriores hemos visto que los cálculos eran muy sencillos una vez disponemos de x̄, ȳ, µ20 , µ11 , µ02 . Podemos calcular todas estas magnitudes 1 Para obtener una relación de aspecto más ortodoxa, podríamos calcular las dimensiones de un rectángulo de iguales momentos principal y secundario que nuestro objeto. 6.11 El programa clasificador 69 con un solo pase sobre la imagen. En concreto: ∑i xi ∑i 1 ∑ yi ȳ = i ∑i 1 x̄ = µ20 = ∑ x2i − x̄2 ∑ 1 i i µ11 = ∑ xi yi − x̄ȳ ∑ 1 i µ02 = ∑ i y2i − ȳ2 i ∑1 i lo que quiere decir, que a medida que recorremos la imagen, podemos formar las sumas ∑i 1 ∑i xi ∑i yi ∑i x2i ∑i xi yi ∑i y2i , donde xi , yi se obtienen en función de la fila y columna del pixel que estamos examinando. A partir de estas sumas tenemos todas las magnitudes que nos interesan. 6.11. El programa clasificador Ahora vemos la secuencia general de operaciones seguida para extraer las características. 1. Aplicar un umbral a la imagen. (capítulo 2) 2. Segmentar la imagen. (capítulo 2) 3. Calcular el área de la mano. (capítulo 2) 4. Calcular el número de agujeros. (capítulo 2) 5. Calcular los estadísticos (medias, momentos de segundo orden). (capítulo 6) 6. Hallar la pendiente de la imagen, sus momentos de inercia, y su relación de aspecto. (capítulo 6) 7. Hallar frontera desordenadamente y colorearla de gris. (capítulo 4) 8. Borrar regiones blancas ahogadas por la frontera. (capítulo 4) 9. Borrar los pixels de frontera que no tienen vecinos blancos. (capítulo 4) 10. Hallar la frontera ordenada. (capítulo 4) 70 11. Capítulo 6 Extracción de características Formar la lista de uno de cada diez pixels de frontera representados como números complejos. (capítulo 5) 12. Calcular perímetro y circularidad. (capítulo 5) 13. Hallar lista de curvaturas. (capítulo 5) 14. Calcular curvosidad y número de dedos. (capítulo 5) Capítulo 7 Resultados En este capítulo mostramos los vectores de características obtenidos para las distintas manos examinadas en el proyecto. En total son 87 imágenes, correspondientes a 23 letras y 4 manos diferentes. Estas imágenes fueron obtenidas por Marcos Pineda como parte de su Proyecto de fin de carrera [9]. Las fotografió usando dos cámaras, y produjo ficheros de imagen en formato TIFF, en escala de grises con 8 bits por pixel (256 niveles de gris), y 768 × 576 pixels. Los nombres de fichero usados están compuestos por una primera letra (o letras en el caso de ‘ch’) que indica a qué letra corresponde la imagen, seguida de un número usado para distinguir entre distintas manos en igual postura, y finalmente la extensión .tif. Así, por ejemplo a20000.tif y a30000.tif contienen dos manos en postura correspondiente a la letra ‘a’. En las gráficas, tenemos en abscisas la letra a que corresponde cada mano examinada, y en ordenadas la magnitud relevante para cada medida. No se ha hecho esfuerzo en distinguir, en las gráficas, entre las manos de distintas personas, por lo que hay varios puntos en cada abscisa representados de la misma forma. Hay que decir que 4 muestras de cada postura son pocas para tener parámetros estadísticos fiables. Para hacer estimaciones y medidas consistentes será necesario añadir imágenes de las manos de muchas personas al banco de pruebas. Aún así, podemos discernir ya algunas tendencias que seguramente serían confirmadas con un estudio de mayores dimensiones. Por completitud, he aquí una muestra de las posturas del alfabeto dactilológico español sobre las que se centra el estudio: 72 Capítulo 7 PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 7.1 El alfabeto dactilológico español (1). Resultados 73 PSfrag replacements N ULO ... Recta producida por mínimos cuadrados Esta recta es mejor (x̄, ȳ) (x, y) d d eje x eje y φ uφ+ π2 Figura 7.2 El alfabeto dactilológico español (2). 74 Capítulo 7 Fichero a20000.tif a30000.tif a40000.tif aa0000.tif b20000.tif b30000.tif b40000.tif bb0000.tif c20000.tif c20001.tif c30000.tif c40000.tif cc0000.tif ch20000.tif ch40000.tif d20000.tif d30000.tif d40000.tif dd0000.tif e20000.tif e30000.tif e40000.tif e410000.tif ee0000.tif f20000.tif f30000.tif f40000.tif g20000.tif g30000.tif g40000.tif h20000.tif h30000.tif h40000.tif hh0000.tif Área Perímetro 114497 83180 76466 107402 117539 117229 108202 148088 103624 103629 71159 79170 65023 108431 84336 87030 78648 79936 91689 94708 62430 68074 62430 71033 72089 59562 64808 93485 85284 68074 122315 84299 82705 105786 1704.04 1388.31 1327.27 1707.4 1822.86 1935.23 2231.28 1979.59 1942.3 1873.96 1887.24 1810.71 1862.73 2473.08 2004.85 1609.22 1733.46 1741.74 1799.8 1955.55 1664.69 1358.74 1664.69 1696.54 1740.97 1476.63 1509.37 1649.69 1629.55 1358.74 2417.82 1632.82 1398.91 2029.7 Circularidad Curvosidad 2.01816 1.84392 1.83332 2.15998 2.24966 2.54226 3.66154 2.10582 2.89709 2.69668 3.98305 3.29556 4.24641 4.48861 3.79264 2.36783 3.04039 3.02006 2.8114 3.21322 3.53235 2.15816 3.53235 3.22446 3.34584 2.91315 2.79739 2.31661 2.47776 2.15816 3.80328 2.51676 1.88294 3.09903 0.259001 0.241964 0.241053 0.279535 0.261673 0.221085 0.23939 0.245506 0.27673 0.245848 0.276442 0.225152 0.287914 0.240521 0.235293 0.328815 0.313787 0.265452 0.272187 0.368906 0.31945 0.237111 0.31945 0.282759 0.274707 0.296884 0.302785 0.262854 0.222516 0.237111 0.238671 0.281972 0.220813 0.261926 Resultados Agujeros Cuadro 7.1 Tabla de resultados. Primer grupo de características (1). 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 75 Fichero i20000.tif i30000.tif i40000.tif k20000.tif k30000.tif k40000.tif kk0000.tif l20000.tif l30000.tif l40000.tif m20000.tif m30000.tif m40000.tif n20000.tif n30000.tif n40000.tif n410000.tif nn0000.tif nn0001.tif o20000.tif o30000.tif o40000.tif oo0000.tif p20000.tif p30000.tif p40000.tif q20000.tif q30000.tif q40000.tif r20000.tif r30000.tif r40000.tif s20000.tif s20001.tif s30000.tif Área Perímetro 101290 85519 66195 80738 67677 70308 69833 89141 66122 72665 113200 111161 111161 102598 84387 84387 84387 65168 65197 90436 66190 71897 85069 98656 86318 83150 108921 61809 66912 106588 84085 85461 82151 82157 67533 1574.82 1471.95 1259.6 1467.13 1465.76 1344.81 1693.33 1509.19 1423.36 1344.67 1875.21 1692.1 1692.1 1744.74 1442.47 1442.47 1442.47 1312.91 1313.09 1674.35 1393.9 1465.61 1953.78 1637.1 1355.31 1670 1907.44 1605.65 1955.79 2003.02 1981 1996.89 1491.75 1541.37 1474.57 Circularidad Curvosidad 1.94842 2.01611 1.90735 2.12154 2.52624 2.04695 3.26747 2.03329 2.43821 1.98015 2.47198 2.04971 2.04971 2.36109 1.96214 1.96214 1.96214 2.10488 2.10452 2.46684 2.33594 2.37747 3.57083 2.16181 1.69341 2.66908 2.65815 3.31925 4.54916 2.99537 3.714 3.71305 2.15561 2.30122 2.56216 0.234135 0.207479 0.266923 0.305958 0.358405 0.306186 0.377422 0.282917 0.253126 0.251547 0.234513 0.259409 0.259409 0.302023 0.275277 0.275277 0.275277 0.370012 0.374464 0.345848 0.263474 0.32095 0.396523 0.22518 0.232229 0.281689 0.359582 0.343131 0.378391 0.248894 0.222245 0.195501 0.275284 0.345831 0.299983 Agujeros Cuadro 7.2 Tabla de resultados. Primer grupo de características (2). 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 1 1 0 0 1 1 1 76 Capítulo 7 Fichero s40000.tif t20000.tif t30000.tif t40000.tif t410000.tif tt0000.tif u20000.tif u30000.tif u40000.tif x20000.tif x30000.tif x310000.tif x40000.tif x410000.tif xx0000.tif y20000.tif y30000.tif y40000.tif Área Perímetro 73745 78485 55894 62092 55894 57332 88955 67669 77546 100931 85284 85284 77462 85284 78324 85261 82542 96839 1549.17 1969.7 1395.25 1510.5 1395.25 1760.34 1907.58 1619.61 1809.57 2120.1 1629.55 1629.55 1624.33 1629.55 1559.69 1415.05 1438.77 1623.48 Circularidad Curvosidad 2.58973 3.9337 2.77161 2.92414 2.77161 4.30116 3.25526 3.08477 3.36032 3.54386 2.47776 2.47776 2.7105 2.47776 2.47155 1.86888 1.99571 2.16589 0.264863 0.402413 0.273717 0.275899 0.273717 0.373229 0.270443 0.181782 0.198603 0.264832 0.222516 0.222516 0.23784 0.222516 0.229266 0.397771 0.26483 0.211695 Resultados Agujeros Cuadro 7.3 Tabla de resultados. Primer grupo de características (3). 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 77 Fichero a20000.tif a30000.tif a40000.tif aa0000.tif b20000.tif b30000.tif b40000.tif bb0000.tif c20000.tif c20001.tif c30000.tif c40000.tif cc0000.tif ch20000.tif ch40000.tif d20000.tif d30000.tif d40000.tif dd0000.tif e20000.tif e30000.tif e40000.tif e410000.tif ee0000.tif f20000.tif f30000.tif f40000.tif g20000.tif g30000.tif g40000.tif h20000.tif h30000.tif h40000.tif hh0000.tif Dedos 3 3 4 4 3 5 5 4 2 0 2 0 2 4 4 3 3 3 1 2 2 2 2 1 2 3 1 4 3 2 4 3 3 5 Pendiente (rad.) Relación de aspecto Momento Principal 0.0503233 -0.00283665 -0.0402411 -0.412405 -0.289953 -0.0189213 0.00270697 -0.317411 -0.12671 -0.12653 -0.233827 -0.232355 0.0280679 -0.0974764 -0.0414594 -1.45692 -0.287792 -0.138488 0.532317 -1.06146 -0.85062 0.0264569 -0.85062 -0.722803 -1.50678 -1.48846 1.52204 -0.127598 -0.180863 0.0264569 -0.412422 1.55537 -1.30358 -1.1303 6.36053 5.1631 4.59212 6.23208 6.79435 4.93551 5.30008 6.26682 3.64759 3.64297 3.69855 4.40853 2.81777 7.34046 7.6678 3.65525 6.20566 6.2946 6.10731 3.19675 2.7625 6.33872 2.7625 2.92846 4.85436 7.44779 5.41595 5.78154 4.51954 6.33872 5.58221 4.1064 4.37874 3.97088 4.61906e+08 2.68595e+08 2.3809e+08 4.06571e+08 4.4835e+08 5.14004e+08 4.24111e+08 7.20892e+08 5.9078e+08 5.91133e+08 3.68611e+08 3.48637e+08 3.76976e+08 4.05839e+08 2.37078e+08 3.98748e+08 2.69359e+08 2.64154e+08 3.82978e+08 6.05618e+08 3.42478e+08 1.66629e+08 3.42478e+08 3.7152e+08 2.56721e+08 1.24462e+08 1.68747e+08 3.35254e+08 3.15477e+08 1.66629e+08 5.54353e+08 3.10841e+08 2.82912e+08 5.07335e+08 Cuadro 7.4 Tabla de resultados. Segundo grupo de características (1). 78 Capítulo 7 Fichero i20000.tif i30000.tif i40000.tif k20000.tif k30000.tif k40000.tif kk0000.tif l20000.tif l30000.tif l40000.tif m20000.tif m30000.tif m40000.tif n20000.tif n30000.tif n40000.tif n410000.tif nn0000.tif nn0001.tif o20000.tif o30000.tif o40000.tif oo0000.tif p20000.tif p30000.tif p40000.tif q20000.tif q30000.tif q40000.tif r20000.tif r30000.tif r40000.tif s20000.tif s20001.tif s30000.tif Dedos 2 2 2 3 2 1 3 4 4 1 5 5 5 4 3 3 3 2 3 2 2 3 3 3 3 4 3 3 2 2 4 3 2 2 2 Resultados Pendiente (rad.) Relación de aspecto Momento Principal 1.43771 -1.50636 -1.3276 -1.40649 -1.357 1.44367 1.51508 1.32282 1.29179 -1.46544 1.52024 -1.54182 -1.54182 -1.50976 -1.38826 -1.38826 -1.38826 -1.42878 -1.4302 -1.44666 -1.41885 1.54928 1.29399 1.46626 1.52244 1.5274 0.0275684 -1.50592 -1.25503 -0.138703 -0.165632 0.0222373 -1.44073 -1.44212 -1.30656 1.32198 4.18494 3.18041 2.55576 3.47606 4.26881 2.99534 2.71336 3.09459 2.08752 2.91922 3.66927 3.66927 2.60183 4.97404 4.97404 4.97404 5.76951 5.75872 2.19182 4.69121 3.81666 3.32628 5.02395 5.29302 6.00076 4.90063 5.30054 8.37595 7.81976 6.8409 7.24288 3.33584 3.33275 3.25571 8.33738e+08 3.33366e+08 2.22313e+08 3.98869e+08 2.61787e+08 2.35824e+08 3.05113e+08 4.55035e+08 2.47783e+08 3.33217e+08 6.41687e+08 5.47567e+08 5.47567e+08 5.79058e+08 2.7522e+08 2.7522e+08 2.7522e+08 1.58705e+08 1.59167e+08 5.91253e+08 2.19392e+08 2.89979e+08 4.55127e+08 3.78148e+08 2.67802e+08 2.36714e+08 5.09132e+08 1.9375e+08 1.62594e+08 3.73501e+08 2.63047e+08 2.53322e+08 3.4813e+08 3.47934e+08 2.64746e+08 Cuadro 7.5 Tabla de resultados. Segundo grupo de características (2). 79 Fichero s40000.tif t20000.tif t30000.tif t40000.tif t410000.tif tt0000.tif u20000.tif u30000.tif u40000.tif x20000.tif x30000.tif x310000.tif x40000.tif x410000.tif xx0000.tif y20000.tif y30000.tif y40000.tif Dedos 3 1 2 3 2 3 4 3 4 3 3 3 4 3 3 3 3 4 Pendiente (rad.) Relación de aspecto Momento Principal -1.38948 -1.47429 -1.36959 -1.53347 -1.36959 -1.38381 1.46706 1.38909 -1.46859 -0.143574 -0.180863 -0.180863 -0.0643013 -0.180863 -0.186061 1.41793 -1.54767 1.46766 4.14883 4.74925 5.38599 5.9838 5.38599 3.2218 4.1098 4.42085 4.85167 5.85099 4.51954 4.51954 5.75055 4.51954 6.56948 2.35767 4.17265 4.53261 2.37421e+08 2.71992e+08 1.35921e+08 1.54053e+08 1.35921e+08 2.26625e+08 3.77278e+08 2.28863e+08 2.66775e+08 3.97794e+08 3.15477e+08 3.15477e+08 2.32011e+08 3.15477e+08 2.26192e+08 4.21072e+08 3.12549e+08 3.86448e+08 Cuadro 7.6 Tabla de resultados. Segundo grupo de características (3). 80 Capítulo 7 Resultados 7.1. Área 150000 140000 130000 120000 110000 100000 90000 80000 70000 60000 50000 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.3 Área de la mano en número de pixels. El área de por sí no es un buen parámetro para la clasificación debido a que distintas personas tienen manos de distinto tamaño, y de que incluso para las manos de una sola persona, representando la misma letra, puede haber variaciones sustanciales en el área mostrada en imagen. Aún así, podemos ver en la gráfica que para algunas letras, el área es consistentemente grande; por ejemplo, las letras b, m, ch. Para otras el área es consistentemente pequeña, como f, k, s, t. El umbral entre “pequeño” y “grande” estaría entorno a 90000 pixels. Los resultados obtenidos son lógicos: las letras de área pequeña corresponden precisamente a manos que son vistas de lado o tienen agujeros, mientras que las letras de área grande corresponden a manos totalmente extendidas. Aunque el área no deba usarse por sí sola como parámetro de clasificación, es indispensable calcularla, precisamente para tener una idea de la escala de la mano. Podemos esperar que el área sea proporcional al cuadrado de las dimensiones de la mano, así que para conseguir resultados normalizados, dividiremos por el área o su raíz cuadrada. 7.2 Perímetro 81 7.2. Perímetro 2600 2400 2200 2000 1800 1600 1400 1200 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.4 Perímetro de la mano en unidades naturales (unidad de longitud = diámetro de un pixel). La misma observación que para el área es pertinente en este caso: el perímetro medido puede depender, para una misma letra, de la persona cuya mano analizamos, de cada fotografía que tomamos, de variaciones en distancia entre la mano y la cámara. Por eso no debemos usar esta medida por sí sola, a menos que realicemos medidas sobre imágenes normalizadas en tamaño. Aún así, podemos ver algunas letras con perímetro consistentemente grande, como b, c, r, y otras con perímetro consistentemente pequeño, como a, f, i, k, s. La separación entre unas y otras estaría entorno a 1800. 82 Capítulo 7 Resultados 7.3. Circularidad 5 4.5 4 3.5 3 2.5 2 1.5 a b c d e f g h i k l m n o p q r s t u x y ch Figura 7.5 Circularidad de la mano. Esta es la primera medida robusta, en teoría independiente de la distancia entre cámara y mano, y del tamaño de la mano. Recordemos que la circularidad es tanto más próxima a 1 cuanto más circular es el objeto; para objetos poco regulares, la circularidad toma valores grandes. Podemos observar en la gráfica dos grupos: letras de baja circularidad (formas regulares) como a, g, i, l, m, n, p, s, y, frente a letras de circularidad alta, como f, q, r, t, u, ch. El umbral estaría en 2.6, aproximadamente. En algunos casos los resultados no son intuitivos, pero por lo general, las manos con circularidad alta poseen uno o dos dedos extendidos, y la palma de la mano encogida. 7.4 Curvosidad 83 7.4. Curvosidad 0.45 0.4 0.35 0.3 0.25 0.2 0.15 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.6 Curvosidad de la mano. Esta magnitud es un índice de lo curvo que es el contorno. Valores altos deberían indicar un contorno zigzagueante, valores bajos, un contorno regular. Puesto que se calcula dividiendo la suma de valores absolutos de curvatura entre el perímetro de la mano, esta medida debe ser también independiente del tamaño de la mano y de la distancia a la cámara. Fijemos un umbral de 0.28. Manos con un valor superior de curvosidad suelen ser: d, e, f, k, n, o, q, s, t. Manos con curvosidad inferior: b, g, h, i, m, p, r, u, x, ch. Los resultados son esperados, en que las manos de baja curvosidad son aquellas en que hay zonas casi rectas, como dedos totalmente extendidos. 84 Capítulo 7 Resultados 7.5. Agujeros 1.5 1 0.5 0 -0.5 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.7 Número de agujeros de la mano. Los resultados aquí son los esperados. El programa detecta correctamente las cuatro posturas de la mano que poseen agujeros: d, k, o, s. Pero por desgracia, detecta también falsos agujeros en las letras r, t, x. En realidad, estos “agujeros” son zonas de sombra más grandes que el umbral que usamos para reconocer agujeros. Modificar el umbral no resuelve el problema, porque los falsos agujeros son a veces mayores que los reales. Hay, sin embargo, dos soluciones claras a este problema: una es usar una iluminación uniforme al fotografiar la mano, con lo que se podría evitar casi por completo las sombras. Otra solución, más complicada, sería usar una técnica de aplicación de umbral a la imagen original más sofisticada. Por ejemplo, usando una técnica adaptativa (adaptative thresholding), se usaría un umbral distinto para distintas zonas de la imagen, y las sombras se verían disminuídas. 7.6 Dedos 85 7.6. Dedos 6 5 4 3 2 1 0 -1 a b c d e f g h i k l m n o p q r s t u x y ch Figura 7.8 Número de dedos de la mano. Podemos ver que el número de dedos (yemas de dedo), no es, después de todo, muy útil. Una misma postura de la mano puede presentar en distintas ocasiones distinto número de dedos. Podemos, sin embargo, usar una pequeña separación: manos con 4 o 5 dedos son a, b, g, h, l, m, n, p, r, u, x, y, ch. Con 0 o 1 dedos, c, d, e, f, k, l, t. Estos resultados no son muy intuitivos si nos fijamos en las posturas representadas, y probablemente se vean modificados si usamos un banco de pruebas mayor. Sin embargo, la técnica de cuenta de dedos usada era muy cruda; una técnica más elaborada seguramente pudiera emplear mejor la abundante información que se encuentra en la gráfica de curvatura. 86 Capítulo 7 Resultados 7.7. Ángulo de inclinación 2 1.5 1 0.5 0 -0.5 -1 -1.5 -2 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.9 Ángulo de inclinación de la mano en radianes. Esta es la magnitud que nos da la separación más clara entre manos, y es, además independiente de la escala. Podemos ver claramente que para algunas manos, la inclinación esta cerca de 1.5 o -1.5 ( π2 ) que corresponde a una postura vertical, mientras que para otras es casi 0, por lo que la postura es horizontal. Para d, h, la inclinación es intermedia. Posturas horizontales: a, b, c, ch, g, q, r, x. Posturas verticales: d, f, i, k, l, m, n, o, p, s, t, u, y. Posturas inclinadas: e, h 7.8 Relación de aspecto 87 7.8. Relación de aspecto 9 8 7 6 5 4 3 2 1 a b c d e f g h i k l m n o p q r s t u x y ch Figura 7.10 Relación de aspecto de la mano. Es el cociente entre los momentos de inercia principal y secundario. Independiente de la escala. Esta magnitud nos da una idea tanto de si la postura es “alargada”, como de si tiene un eje marcado y claro. Manos con relación de aspecto alta (>4) : a, b, ch, f, g, h, p, q, r, t, u, x. Con relación de aspecto baja: c, e, i, k, l, o, y. Los resultados son muy intuitivos. Las manos más alargadas tienen mayor relación de aspecto, las manos en postura más encogida o circular, relaciones de aspecto menores. 88 Capítulo 7 Resultados 7.9. Momento principal 9e+08 8e+08 7e+08 6e+08 5e+08 4e+08 3e+08 2e+08 1e+08 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.11 Momento de inercia principal de la mano. Es pequeño para regiones “pegadas” al eje: c, ch, d, f, g, i, p, r, s, t, u, x. Esta medida aporta poco sobre los resultados de la relación de aspecto. En efecto, una relación de aspecto alta indica que la mano es mucho más larga que ancha, y esto es prácticamente lo mismo que decir que la mano es estrecha, o pegada al eje. Podemos ver que las manos con momento principal pequeño son casi las mismas que tienen una relación de aspecto grande. 7.10 Momento secundario 89 7.10. Momento secundario 5e+09 4.5e+09 4e+09 3.5e+09 3e+09 2.5e+09 2e+09 1.5e+09 1e+09 5e+08 a b c d e f g h i k l mn o p q r s t u x y ch Figura 7.12 Momento de inercia secundario de la mano. Al igual que el momento principal, esta medida aporta poco sobre los resultados de la relación de aspecto. Un valor bajo del momento secundario indica manos “chatas”: e, k, l, s. Esto es casi lo mismo que decir que la relación de aspecto es baja: la mano no es mucho más larga que ancha. En efecto, las letras con momento secundario bajo son casi las mismas que tienen una relación de aspecto baja. 90 Capítulo 7 Resultados 7.11. Rendimiento del programa Mostramos aquí el tiempo invertido en cada una de las operaciones que se han investigado en este proyecto. Las pruebas han sido realizadas sobre el fichero q20000.tif, en el que el área de la mano es de unos 116400 pixels. El ordenador usado es un Pentium MMX a 166 MHz, corriendo Linux: Operación Tiempo (s.) Aplicar umbral Segmentar Obtener orientación Frontera desordenada Limpieza de contorno Extraer contorno Estudiar curvatura 100 erosiones 100 dilataciones Esqueletización Transformación de distancia 0.02 0.30 0.05 0.20 0.15 0.16 0.01 0.18 0.34 0.50 0.34 Total obtención de vector característico 0.79 Cuadro 7.7 Tiempo de ejecución de operaciones. 7.12 Utilidad de los parámetros calculados 91 7.12. Utilidad de los parámetros calculados Ya que hemos visto los resultados empíricos, es el momento de pensar si los parámetros estudiados en este proyecto son adecuados para desarrollar un clasificador basado en ellos. Algunos de los parámetros calculados no son útiles por sí mismos, como el área y el perímetro; otros no se comportan satisfactoriamente, como el número de dedos. Los momentos de inercia han resultado interesantes, pero no aportan nada nuevo respecto a la relación de aspecto. Sin embargo, hemos encontrado una serie de parámetros que dividen las manos estudiadas en grupos diferenciados. Son el número de agujeros, el ángulo de inclinación, la circularidad, la curvosidad y la relación de aspecto. Podemos especular cómo sería un clasificador basado en estos parámetros. Proponemos a continuación un clasificador basado en estos parámetros. Hay que decir que este clasificador es sólo una propuesta, y no ha sido todavía llevado a la práctica. Pero nos permitirá tener una idea de cómo podríamos usar los parámetros calculados en un clasificador real; además, el sistema propuesto es lo suficientemente detallado para que no sufra muchas modificaciones a la hora de ser implementado. La forma de funcionamiento del clasificador es sencilla: en una etapa dada, tenemos nuestra imagen de una mano, y sabemos que debe corresponder a una de un conjunto de letras. Nos fijamos en el valor de uno de los parámetros calculados, y lo comparamos con el valor del mismo parámetro para cada una de las posibles letras. Sólo algunas letras tienen un valor similar, de forma que reducimos el conjunto de letras a que puede corresponder el parámetro estudiado. Repetimos esta operación hasta quedarnos con una sola letra posible. Esto supone que tenemos una librería de imágenes preclasificadas, y por ejemplo, sabemos que la relación de aspecto de la letra f está cerca de 3.2. La formación de esta librería de imágenes preclasificadas se conoce como entrenamiento del sistema. Vamos a ver que el entrenamiento es posible con los parámetros que hemos aprendido a calcular en este proyecto, y para ello veremos que podemos dividir el conjunto de imágenes que hemos estudiado en subconjuntos, muchos de ellos con un solo elemento, que están completamente determinados por el valor de los parámetros. Al comenzar, tenemos un gran conjunto formado por todas las imágenes de entrada, que corresponden a las letras a, b, c, ch, d, e, f, g, h, i, k, l, m, n, o, p, q, r, s, t, u, x, y, z. 92 Capítulo 7 Resultados Ahora observamos el valor del ángulo de inclinación, que básicamente divide el conjunto de entrada en manos horizontales y manos verticales, salvo para las imágenes que corresponden a las letras e, h, que tienen una pendiente intermedia, entorno a -1 radián. Diremos que la e y la h están inclinadas. Por tanto, tenemos los siguientes grupos: Manos horizontales: a, b, c, ch, g, r, x Manos verticales: d, f, i, k, l, m, n, o, p, q, s, t, u, y Manos inclinadas: e, h Para dividir estos grupos en subgrupos menores, estudiamos la relación de aspecto. Fijémonos por separado en cada grupo. Para las manos horizontales, hay tres subgrupos distintos: Relación de aspecto baja (0–4.5): c Relación de aspecto media (4.5–6.8): a, b, g, x Relación de aspecto alta (6.8–9): r, ch Por tanto, una mano horizontal con relación de aspecto baja debe corresponder a c. Sólo hemos podido formar dos subgrupos para las manos verticales: Relación de aspecto baja (0–5): d, i, k, l, m, o, s, u, y Relación de aspecto alta (5–9): f, n, p, q, t Finalmente, para las manos inclinadas: Relación de aspecto alta (0–4): h Relación de aspecto baja (4–9): e En esta segunda etapa de clasificación ya tenemos tres grupos aislados, que corresponden a c, h, e. Podemos partir en dos el grupo de manos verticales con relación de aspecto baja contando el número de agujeros: Con un agujero: d, k, o, s Sin agujeros: i, l, m , u, y 7.12 Utilidad de los parámetros calculados 93 Ningún otro grupo contiene manos con agujeros. Como todavía quedan grupos con más de un elemento, estudiamos a continuación el valor de la circularidad para los distintos grupos. Para manos horizontales con relación de aspecto media (a, b, g, x) Circularidad baja (1–2.2): a Circularidad media (2.2–2.6): b, g Circularidad alta (2.6–9): x Para manos horizontales con relación de aspecto alta (r, ch) Circularidad alta (4–9): ch Circularidad baja (1–4): r Para manos verticales con relación de aspecto alta (f, n, p, q, t) Circularidad baja (1–2.6): n, p Circularidad alta (2.6–9): f, q, t Para manos verticales con relación de aspecto baja y sin agujeros (i, l, m, u, y), sólo u tiene una circularidad mayor de 2.7. Por desgracia, la circularidad no nos ha servido para refinar el grupo (d, k, o, s). Sin embargo, podemos estudiar la curvosidad ahora. Tomemos el grupo (f, q, t). La q tiene una curvosidad visiblemente mayor. En el grupo (n, p), la n tiene una curvosidad visiblemente mayor. Finalmente, estudiando el conjunto (i, l, m, y), vemos que la m es la única con 5 dedos. Recapitulemos: usando los parámetros calculados, hemos logrado determinar los siguientes grupos: (a), (b, g), (c), (ch), (d, k, o, s), (e), (f, t), (h), (i, l, y), (m), (n), (p), (q), (r), (u), (x). Resumimos el proceso en la figura 7.13. La clasificación es bastante buena, pero ¿podemos mejorarla aún?, ¿podemos subdividir los grupos (b, g), (d, k, o, s), (f, t), (i, l, y)? Con los parámetros que hemos calculado no podemos hacer más. Un vistazo a la gráfica del alfabeto dactilológico (páginas 72 y 73) nos sirve para comprender por qué: La f y la t son casi idénticas. Ambas son verticales, vistas de lado, con todos los dedos extendidos, y el índice extendido en ángulo recto. La única diferencia es que en la t, el pulgar pasa por debajo del índice. Ninguno de los parámetros 94 Capítulo 7 Alta ` `` ``` f, q, t ``` Vert. d, k, o, s @ @ @ Baja b b b i, l, m, u, y Manos \ D D\ D \ \ D \ Horiz. D CT D CT D CT D C D C D C D C D D D D D , D Inclin. , T T T Orientacion . .... .... ... Resultados q @ @ f, t n, p A n A A p u m A , A i, l, m, y , Z Z Z i, l, y (((( ((( ( ( h r, ch hhhh hhhh ch ((( ((( (h h a, b, g, x b hhh hh b b b b b b b c x r b, g a h e Rel. aspecto Agujeros Circularidad Curvosidad Dedos Figura 7.13 Diagrama del clasificador. Los grupos finales se muestran enmarcados. 7.12 Utilidad de los parámetros calculados 95 calculados puede detectar esto, y de hecho, es probable que sea necesaria una etapa de preprocesado de imagen más sofisticada para ello. Las manos correspondientes a i, l, y son muy parecidas, pero podemos ver fácilmente que en la l el dedo extendido es el índice, mientras que en las otras dos es el meñique. Podremos distinguir esto fácilmente con un análisis más fino del gráfico de curvatura. Si siempre recorremos el contorno de la mano en sentido contrario a las agujas del reloj, para l tendremos: “dedo, seguido de zona circular, zona recta, y de nuevo otra zona recta”, mientras que para i, y será: “dedo, seguido de zona recta, otra zona recta, y una zona curva”. Hemos visto cómo hacer tales descripciones. Las manos correspondientes a d, k, o, s son muy parecidas, pero se podrán distinguir por la forma del agujero. El de k es alargado, por lo que tendrá una relación de aspecto mayor que los otros, o una circularidad mayor. El de s es más pequeño. Los de o, d son muy parecidos, y de hecho, la única diferencia entre ellos es que en uno el agujero lo forman el pulgar y el índice, y en otro el agujero lo forman el pulgar y el medio. De nuevo, la etapa de preprocesado de este proyecto no parece suficiente. En resumen, para distinguir d de o, y f de t, seguramente hará falta un preprocesado más sofisticado, probablemente con algún operador diferencial. Para las demás letras, la etapa de preprocesado es suficiente, y en algunos casos quizás haga falta refinar el estudio de los agujeros o del gráfico de curvatura. El balance es muy positivo. Esta descripción de alto nivel no muestra con justicia la complejidad de una etapa de clasificación. Suele ser necesario emplear criterios estadísticos para decidir cuál es la clase (o posibles clases) a que pertenece una mano. Por otra parte, un estudio estadístico detallado podría aumentar el poder de resolución de los parámetros calculados. En el sistema propuesto cada parámetro ha permitido partir un grupo en dos o tres subgrupos (o ninguno), y quizás un estudio más serio muestre que algunos parámetros pueden tener un poder de resolución mayor. Para tener un clasificador completo será necesario un estudio mucho más detallado, y este estudio puede ser lo suficientemente extenso como para justificar un Proyecto de fin de carrera. En cualquier caso, los parámetros calculados en este proyecto parecen prometedores. Capítulo 8 Conclusiones y mejoras Hemos visto ya la descripción de los algoritmos de este proyecto, y los resultados obtenidos. Como se mencionaba en la introducción, el objetivo es encontrar parámetros que sean útiles para construir un clasificador de gestos de la mano. En este proyecto se ha desarrollado un programa que extrae de la imagen multitud de características, y algunas de ellas parecen tener un buen poder discriminador. Esto es una buena base a la que añadir una etapa de clasificación. En concreto, de las características obtenidas, la circularidad, curvosidad, el número de agujeros, el ángulo de inclinación y la relación de aspecto, producen una separación de los gestos en grupos (horizontales/verticales, con/sin agujeros, alargados/chatos . . . ) que seguramente resulten útiles para una clasificación completa. De las demás características, el área y el perímetro son muy dependientes del tamaño de las manos fotografiadas, y de la forma en que han sido fotografiadas. Los momentos de inercia principal y secundario son interesantes, pero aportan poco que no dé la relación de aspecto. Y el número de dedos no ha resultado ser satisfactorio. Por otra parte, el proyecto ha tenido su mayor éxito en la etapa de procesado de imagen. La etapa de procesado es robusta y efectiva, y se ejecuta en tan solo 0.79 segundos. La idea de deshacerse de los espúreos en la etapa de segmentación ha resultado muy acertada, y el procesado de regiones se ha logrado implementar de forma que sea muy rápido y consuma poca memoria. Las implementaciones de las operaciones morfológicas son también muy rápidas, como hemos visto en el capítulo anterior, y aunque no hemos usado las operaciones morfológicas aquí, son frecuentes en la literatura sobre el tema, y muy útiles. Seguramente un proyecto posterior encuentre buen uso para ellas. 98 Capítulo 8 Conclusiones y mejoras También ha sido interesante la idea de colorear los pixels de frontera de gris para llevar a cabo la limpieza del contorno. Los contornos obtenidos son realmente “dibujables” de forma continua. Esto ha posibilitado el estudio de curvatura de la silueta. Este estudio ha sido muy somero en este proyecto, y su producto, el número de yemas de dedo, no ha resultado muy afortunado. Sin embargo, el estudio de la curvatura del contorno promete ser interesante. Es concebible que pudiéramos realizar una descripción de la silueta de la mano del tipo: Zona recta larga, seguida de un giro suave y breve, y otro segmento corto, en dirección opuesta al anterior. Después viene una zona circular con radio amplio, seguida de . . . y que esta descripción resultase suficiente para la clasificación. Un clasificador basado en una descripción cualitativa como esta se suele denominar sintáctico, debido a que las descripciones cualitativas se suelen representar usando un lenguaje especializado al problema en cuestión. Por contra, la mayoría de los clasificadores son paramétricos, lo que quiere decir que se basan en el uso de vectores de parámetros, cuyas componentes son números reales. En general, para realizar estos clasificadores se recurre a la estadística y a la teoría de la decisión, y es necesario realizar un entrenamiento del clasificador, es decir, hay que formar una librería de imágenes de prueba pre-clasificadas. También se recurre a veces a técnicas modernas como redes neuronales. En este proyecto, en concreto, todos los parámetros de clasificación obtenidos, excepto el número de dedos y el número de agujeros, son números reales, y parece que se presta más a un clasificador paramétrico. En cuanto a posibles mejoras para el futuro, son innumerables. Para empezar, las implementaciones pueden hacerse más rápidas con poco esfuerzo: en este proyecto se usa las estructuras de datos, como colas y pilas, de la STL (Standard Template Library) de C++. Esta librería ha sido diseñada para proporcionar estructuras de datos genéricas, y por ello ha sacrificado algo de velocidad. Reimplementando colas y listas como estructuras de datos de C seguramente conseguiríamos un aumento considerable de velocidad. En general, reimplementar en C supone un aumento de velocidad notable. El peor defecto de este proyecto es su dependencia con las imágenes de entrada. Para manos obtenidas en otros lugares seguramente funcione con más dificultades. Esto es debido a que el sistema se ha configurado ad hoc. Por ejemplo, en la primera etapa, aplicamos un umbral de 100, que ha resultado adecuado para nues- 99 tras necesidades. Pero para tener un sistema versátil, la selección de umbral se debe hacer en función de cada imagen, usando métodos adaptativos. Lo mismo ocurre con los tamaños de regiones mínimo y máximo permitido, que se han definido con arreglo a las posturas estudiadas. En resumen, es urgente, para tener un sistema versátil, realizar la configuración del sistema de forma automática y adaptativa, no “a dedo”. Otra dirección futura es añadir más operaciones de procesado de imagen, como la obtención de la envolvente convexa, el uso de la transformada de Hough, transformadas integrales . . . También sería recomendable hacer un estudio más serio de la curvatura del contorno, que puede resultar muy interesante y debería poder implementarse de forma eficiente. En cuanto a la clasificación, es necesario ampliar el banco de imágenes de prueba, y tomar las fotos con una iluminación más uniforme que reduzca las sombras. La etapa de clasificación de vectores característicos debe aún realizarse por completo. Finalmente, el programa de ordenador desarrollado para este proyecto es de por sí interesante. Ofrece un intérprete de comandos rudimentario, y en sistemas que proporcionan la librería GTK+, produce imágenes que resultan útiles a la hora de visualizar lo que ocurre y corregir errores en los algoritmos. Con un intérprete más sofisticado, que quizás permitiese definir procedimientos y variables, y añadiendo más operaciones, tendríamos el comienzo de un buen programa de procesado de imagen. Por supuesto, sería deseable un interfaz gráfico intuitivo, pero no debe olvidarse el intérprete de comandos. Gracias a él, en este proyecto no ha sido necesario recompilar cada vez que se ha cambiado el orden de aplicación de las operaciones. Para tener la mayor portabilidad posible del programa quizás convendría usar para gráficos la librería Xlib, presente en casi todos los sistemas UNIX. GTK+ es todavía sólo una promesa. En resumen, lo mejor de este proyecto es que ha producido un programa que da acceso, mediante un sencillo intérprete, a operaciones de procesado de imagen implementadas de forma eficiente. Este programa puede usarse para producir un vector de características de la imagen, que luego se podría clasificar por métodos estadísticos. También puede formar la base de un programa general para Visión Artificial. Apéndice A Cómo usar el programa El programa desarrollado en el proyecto se llama desde la línea de comando con proyvision, y admite por el momento, dos opciones: -v modo verboso, produce cronometrado de operaciones y muestra texto producido por los comandos. En el código, estos mensajes se muestran como if (VERBOSE) std::cout < <, lo que resulta útil en la depuración de programas. -d depth-first: esta opción cambia la implementación usada para los algoritmos. Por lo general, para el llenado, erosión, obtención de contorno . . . , se usa el método “a lo ancho”, pero esta opción nos permite hacerlo de otra forma. También se le puede dar, a continuación de las opciones, el nombre de un fichero de tipo TIFF (el único tipo soportado), que el programa abrirá inmediatamente. Una vez hemos invocado el programa, introducimos comandos, y pulsamos ENTER para ejecutarlos. Los comandos admitidos son: write seguido de un nombre: almacena la imagen en memoria en un fichero de formato TIFF con el nombre dado. read seguido de un nombre. Lee el fichero TIFF nombrado. threshold seguido de un entero. Aplica un umbral a la imagen. regions segmenta la imagen. regions_bis calcula el mapa de regiones (no se ha usado en el proyecto). white_regions elimina regiones blancas pequeñas. 102 Apéndice A Cómo usar el programa black_regions elimina regiones negras pequeñas. skeleton esqueletiza la imagen (usar sólo después de tener la cola de frontera). boundary obtiene cola de frontera desordenada. contour obtiene contorno ordenado. contour_outer obtiene silueta (contorno exterior) ordenada. erode seguido de un número. Erosiona la imagen las veces deseadas. dilate seguido de un número. Dilata la imagen las veces deseadas. clean limpia la frontera. curvature estudia la curvatura de la mano. statistic realiza cálculos estadísticos y halla inclinación y momentos de inercia. results muestra el vector de características. q para salir. Una ventaja de usar un intérprete que lee del standard input es que podemos pasarle ficheros con instrucciones redirigiendo el input. Para generar los vectores de características, en este proyecto se ha usado proyvision fichero.tif < curvinput donde curvinput contiene threshold 100 regions statistic boundary white_regions clean contour_outer curvature results q Apéndice B Dactilología El origen de los lenguajes de signos no está claro, puede que sean anteriores a los lenguajes orales. Ya en el siglo XVI, Fray Pedro de León usaba gestos con el propósito de comunicarse con sus alumnos, niños sordos de familias ricas. A partir de entonces, el uso de comunicación gestual se extendió por Europa. Los lenguajes de signos se han considerado a veces como mímica. Sin embargo, a menudo los gestos de estos lenguajes no son evidentes para quien los desconoce, y además, hay grandes diferencias entre ellos (los lenguajes). El significado de un gesto en estos lenguajes depende de: La postura que la mano adopta para realizar el signo. La orientación de la palma de la mano. La posición de la mano relativa al cuerpo. El movimiento de la mano. Componentes no manuales como movimiento de los labios o expresión facial. Pero además, no hay una correspondencia estricta entre signos y palabras, lo que a veces dificulta la traducción del lenguaje de signos. Para facilitar la comunicación entre sordos y oyentes se usa a veces la dactilología, que consiste en asignar un signo a cada letra del alfabeto. Un sordo usa la dactilología para: Denominar un nuevo concepto que todavía carece de signo propio. Esto ocurre hasta que se da un signo al concepto, o hasta que el concepto cae en desuso. 104 Apéndice B Dactilología Para expresar nombres de lugares o personas a oyentes. La dactilología, por tanto, es auxiliar del lenguaje de signos convencional. Existen en el mundo varios alfabetos dactilológicos. El que nos interesa es el español, que, al igual que el irlandés o el americano, usa una sola mano (la mano dominante). El alfabeto dactilológico inglés, por otra parte, emplea dos manos. Este proyecto ha usado imágenes de manos en posturas del alfabeto dactilológico español; en concreto, sólo en aquellas posturas correspondientes a letras que se pueden representar sin movimiento. Por cierto, en este alfabeto no están recogidos los signos de puntuación ni la distinción entre mayúsculas y minúsculas. Las letras estudiadas se muestran en la figura 7.1, página 72, y en la figura 7.2, página 73. Apéndice C Implementación Ha llegado el momento de presentar la implementación de los algoritmos desarrollados en el Proyecto. Hasta ahora, las descripciones de algoritmos se han hecho en pseudocódigo, y han evitado meterse en detalles “laterales”, pero el código real está escrito en C++, y debe tener en cuenta sucesos como el acceso a zonas de memoria no asignadas, representación específica de estructuras de datos, y alternativas de diseño. C.1. Organización del código El código del proyecto se ha dividido en los siguientes ficheros: boundary-ops.cpp contiene las operaciones que usan la frontera de la mano, es decir, los procedimientos de obtención de contorno y las operaciones morfológicas. curvature.cpp contiene las funciones que extraen los elementos del contorno estudiando su curvatura y que calculan el número de dedos. eval.cpp contiene el intérprete de comandos. greylevel-ops.cpp contiene las operaciones que sólo dependen del nivel de gris de cada punto, como aplicar un umbral. gtk-image.cpp contiene la función que dibuja en pantalla la imagen contenida en memoria. gtk-main.cpp define el ejecutable con interfaz gráfico. 106 Apéndice C Implementación image-basic.cpp define las operaciones básicas con imágenes (creación, destrucción, lectura. . . ). image-main.cpp define el ejecutable sin interfaz gráfico. image.hpp contiene declaraciones de clases y variables globales. region-ops.cpp describe las operaciones sobre regiones, esto es, llenado y borrado de regiones. statistic.cpp contiene las funciones que calculan los estadísticos y los momentos de inercia de la imagen. C.2. Representación de la imagen Hemos visto varias veces a lo largo del proyecto que usamos imágenes de tipo raster, en que los pixels están organizados en filas y columnas. Cada pixel tiene un nivel de gris, con lo cual necesitamos un tipo de datos para almacenar este nivel de gris. En las imágenes que nos interesan, los pixels pueden tener 256 niveles de gris, con lo cual debemos usar 8 bits por pixel. La forma más sencilla de conseguir posiciones de memoria con 8 bits es: typedef unsigned char grey; Además, por comodidad y portabilidad, vamos a definir las siguientes constantes: const int grey_depth = 256; const grey white = 255; const grey black= 0; La razón de estas definiciones es que en algunos casos, un nivel de gris de 0 representa el color blanco. No es el caso aquí, pero conviene prevenir. Puesto que queremos una matriz de pixels, es lógico pensar en un array bidimensional en C/C++, como grey matrix[num_filas][num_columnas]; pero esto tiene la desventaja de que necesitamos saber de antemano el tamaño de la imagen. Nuestras imágenes tienen 768 columnas y 576 filas, pero vale la pena hacer que el programa no dependa de ello. Para esto, el espacio reservado para la imagen debe asignarse dinámicamente, y no podemos usar la definición anterior. C.2 Representación de la imagen 107 Cuando pedimos al sistema operativo memoria dinámica (mediante new), éste nos devuelve un puntero a una región de memoria tan grande como necesitamos, pero sin estructura. Para conseguir acceder a pixels con un número de fila y un número de columna debemos poner la estructura nosotros mismos. En cualquier libro de nivel medio sobre C/C++ se discute varias alternativas para hacer esto. La escogida aquí pretende copiar la estructura en memoria de las matrices estáticas declaradas como veíamos arriba. En estas matrices, los pixels de una fila ocupan posiciones contiguas, y unas filas están directamente al lado de otras. fila 1 fila 2 fila 3 · · · fila n Una consecuencia interesante de esto, es que, debido a que C/C++ no realizan control sobre los índices de un array, podemos acceder a cualquier elemento de la matriz usando un solo índice. Por ejemplo, matrix[0][num_columnas+5] es lo mismo que matrix[1][5] Si vamos a acceder a una fila tras otra, modificar un solo índice es más rápido y más corto de expresar. Las siguientes expresiones serían equivalentes: for (i=0; i<num_filas; ++i) for (j=0; j<num_columnas; ++j) matrix[i][j] . . . for (i=0; i<num_pixels; ++i) matrix[0][i] . . . Nos interesa, por tanto, tener todos los pixels ocupando posiciones consecutivas de memoria. Conseguimos esto con start = new grey[size]; donde size es el número de pixels de la imagen. El nombre start es debido a que new nos va a devolver la dirección del comienzo de la región libre en memoria en la que almacenaremos la imagen. Por completitud, definimos también la variable end, que nos da la posición del último pixel de la imagen. Necesitamos también saber el número de filas y columnas de la imagen para algunos de los algoritmos del programa, así como para evitar accesos ilegales a 108 Apéndice C Implementación memoria. Los almacenaremos en las variables height y width. Con esto tenemos suficiente información para acceder a cualquier pixel, pero por comodidad vamos a proporcionar la posibilidad de acceder a la matriz por fila y columna. Para ello necesitamos tener un puntero a la dirección de comienzo de cada fila, lo que conseguimos con matrix = new grey* [height]; for (int i=0; i<height; ++i) matrix[i]=start+i*width; Es decir, definimos un nuevo array que contiene punteros al array que contiene la imagen. Con dos niveles de indirección, podemos usar matrix como base de la matriz de pixels. Esto es todo lo necesario para empezar a trabajar. Nos conviene tener todas las variables que hemos definido juntas, para lo cual podemos utilizar struct o clases. Se ha escogido usar clases porque de esta forma el alcance (scope) de las variables es local a cada imagen, y global dentro de ella. La definición básica de la estructura de datos que almacena la imagen es: class Image { public: . . . private: int height; int width; int size; grey **matrix; grey *start; grey *end; int offset_8neighbor[8]; int offset_4neighbor[4]; }; Ahora examinaremos las variables offset_Xneighbor. Aunque hemos provisto acceso a la matriz por fila y columna, esto no es por lo general necesario, así que no representaremos los pixels como un par de números, sino como punteros al array start. C.3 Operaciones sobre regiones 109 typedef grey * point; Varios de los algoritmos vistos en el proyecto requerían poder recorrer los vecinos de un pixel secuencialmente. offset_Xneighbor contiene las posiciones relativas de estos vecinos respecto al pixel de origen. Recordemos que los 4-vecinos se encuentran a la derecha, a la izquierda, encima, y debajo del pixel de origen. offset_4neighbor[0] = 1; offset_4neighbor[1] = -width; offset_4neighbor[2] = -1; offset_4neighbor[3] = width; Para recorrer los vecinos secuencialmente, basta con point pixel_original, pixel_vecino; for (int i=0; i<4; ++i) pixel_vecino = pixel_original + offset_4neighbor[i]; . . . C.3. Operaciones sobre regiones Casi todas las operaciones que hemos visto necesitan conocer el valor de las variables de clase definidas en la sección anterior, así que las implementaremos como funciones miembro de la clase. Como ejemplo, veamos cómo aplicamos un umbral a la imagen. void Image::threshold(grey level) { point pt; for(pt=start; pt<=end; ++pt) if (*pt < level) *pt=black; else *pt=white; } En la función anterior no hemos usado índices para acceder a los pixels, sino que aprovechamos la capacidad de C/C++ para incrementar punteros. En muchos 110 Apéndice C Implementación casos (depende de la calidad del compilador), el código máquina producido es más rápido. También conviene fijarse en el uso del operador de indirección. Ya que un pixel pt es un puntero a un pedazo de memoria que almacena un nivel de gris, *pt nos da ese nivel de gris. Veremos a menudo esta utilización de * a lo largo del capítulo. Una posibilidad que ocurre realmente en el proyecto y que no hemos discutido hasta ahora es que podemos intentar acceder a un pixel que no pertenece a la imagen. Esto ocurre, por ejemplo, cuando tratamos de recorrer los 8-vecinos de pixels en las esquinas de la imagen. Vamos a usar un pequeño test para determinar si el pixel pertenece a la imagen. bool Image::in_range(point pt) { return (pt >= start && pt <= end); } Esta pequeña función demuestra la conveniencia de la representación escogida. En caso de que hubiéramos elegido acceder a los pixels usando dos índices, la función hubiera sido: bool Image::in_range(int x, int y) { return (x >= 0 && x < width && y >= 0 && y < height); } No es tampoco muy complicado, pero una función que va a ser ejecutada tan a menudo merece ser escrita de forma eficiente. Llegamos ahora a los algoritmos de llenado discutidos en el capítulo 2. El algoritmo intuitivo es prácticamente una traducción del pseudocódigo. int Image::fill4_recursive(point first, grey g_in, grey g_out) { point aux; int re=1; *first=g_out; for (int i=0; i<4; ++i) { aux = first + offset_4neighbor[i]; C.3 Operaciones sobre regiones 111 if (in_range(aux) && *aux==g_in) re += mark4_recursive(aux,g_in,g_out); } return re; } Sólo tiene un par de elementos reseñables. Uno es que devuelve el número de pixels que forman la región, y otro es que usa la función in_range antes de comprobar si un pixel debe ser borrado. En el bucle for podemos comprobar cómo salta de vecino en vecino. La función que implementa el algoritmo de llenado a lo ancho es más interesante: int Image::fill4_breadth_first(point first, grey g_in, grey g_out) { queue<point> Q; int re=0; int i; point aux; *first=g_out; ++re; Q.push(first); while (!Q.empty()) { first = Q.front(); for (i=0; i<4; ++i) { aux = first + offset_4neighbor[i]; if (in_range(aux) && *aux==g_in) { *aux=g_out; ++re; Q.push(aux); } } Q.pop(); } return re; 112 Apéndice C Implementación } Este es el primer ejemplo de la utilización de la Standard Template Library (STL) de C++ en este proyecto. La STL proporciona estructuras de datos genéricas, es decir, independientes del tipo de datos. Para ello, toman el tipo de datos como parámetro. Así, una cola de números de coma flotante con precisión simple se define mediante queue<float> Q; Las funciones de acceso a estructuras de la STL son funciones miembro, y tienen nombres genéricos para facilitar el cambio de una estructura de datos a otra. En la función anterior hemos usado: Q.pop() para eliminar el primer elemento de cola. Q.push() para poner un elemento al final de la cola. Q.front() para acceder al primer elemento en cola. Q.empty() para determinar si la cola está vacía. La función que implementa el algoritmo a lo profundo es, gracias a esto, casi idéntico int Image::fill4_depth_first(point first, grey g_in, grey g_out) { stack<point> S; int i, re=0; bool is_on_top; point aux; *first=g_out; S.push(first); ++re; while (!S.empty()) { first = S.top(); is_on_top = true; for (i=0; i<4; ++i) { C.3 Operaciones sobre regiones 113 aux = first + offset_4neighbor[i]; if (in_range(aux) && *aux==g_in) { is_on_top = false; *aux=g_out; ++re; S.push(aux); break; } } if (is_on_top) S.pop(); } return re; } Sólo se diferencia en que usamos stack<point> en vez de queue<point>, y en que sólo realizamos pop() en caso de que no se haya añadido nuevos pixels a la pila. Finalmente, he aquí la función que borra las regiones negras demasiado pequeñas para ser agujeros entre los dedos. void Image::get_black_regions() { int holes = 0; point pt, last; grey g_out = 1; last=start-1; while ((pt=get_next_point_with_level(black, last))) { assert(g_out < white); if (fill(pt, black, g_out) < SMALLEST_BLACK_REGION) fill(pt, g_out, white); else ++holes; ++g_out; last=pt; } feature_vector[num_holes] = holes-1; 114 Apéndice C Implementación } Esta función debería resultar clara: busca el siguiente pixel negro usando la función get_next_point_with_level, colorea la región a que pertenece, y la borra si es demasiado pequeña. En caso de que tenga el tamaño suficiente, incrementa el contador de agujeros. Vemos aquí por primera vez el vector de características, feature_vector, que es una variable global del programa. Por comodidad, accedemos a sus elementos usando una variable de tipo enum. La función que busca regiones blancas es similar, excepto que cuando encuentra una región lo suficientemente grande, pinta todas las demás de negro y concluye. C.4. Obtención del contorno El primer paso en la obtención del contorno es decidir qué pixels forman parte de él. La definición que usamos es que un pixel pertenece al contorno si es blanco y tiene un vecino negro. Naturalmente, esto quiere decir que todos los pixels del fondo deben ser negros. Recordando que en la etapa de segmentación obtenemos una región blanca y varias con distintos niveles de gris, debemos pintar de negro todo pixel que no sea blanco. Hecho esto, averiguamos que un pixel es de contorno con: bool Image::is_8boundary_point(point pt) { point aux; if (*pt == white) { for (int i=0; i<8; ++i) { aux= pt + offset_8neighbor[i]; if (!in_range(aux)) return true; else if (*aux == black) return true; } } return false; } C.4 Obtención del contorno 115 La función anterior tiene la peculiaridad de que trata a los pixel “fuera de la imagen” como pixels negros. Es decir, que los pixels blancos en un lateral de la imagen son siempre de frontera. Es como si la imagen estuviera enmarcada por bandas de pixels negros. Para almacenar los pixels de contorno en una lista empleamos la siguiente función: void Image::get_boundary_breadth_first(queue<point> & Points) { int i; queue<point> Q; point pt, aux, last = start-1; filter_by_level(white); while ( (pt=get_next_boundary_point(last)) ) { last = pt; *pt = bound; Q.push(pt); while ( !Q.empty()) { pt=Q.front(); for (i=0; i<4; ++i) { aux= pt + offset_4neighbor[i]; if (in_range(aux) && is_8boundary_point(aux)) { *aux=bound; Q.push(aux); } } Points.push(pt); Q.pop(); } } } Es idéntica a la función de llenado a lo ancho, excepto por que añadimos a la cola los vecinos del pixel en cabeza de cola que pertenecen al contorno. Otra particularidad es que los pixels borrados de la cola son almacenados en una lista. La fun- 116 Apéndice C Implementación ción get_next_boundary_point, al igual que get_next_point_with_level, usada antes, recorre la imagen a partir del pixel especificado por su argumento de entrada, y devuelve el primer pixel que satisface un test, en este caso, is_8boundary_point. Como veíamos en el capítulo 4, para tener un contorno continuo modificaremos la imagen ligeramente. Este procedimiento colorea el contorno de gris, y al hacerlo, produce “ahogos”, regiones que pierden conexión con el resto de la mano. Nos interesa borrarlas, para lo cual usamos la función Image::erase_small_whites(), que es casi idéntica a Image::get_black_regions() descrita anteriormente, excepto por que no trata de contar el número de regiones. Después de borrar las regiones ahogadas, es necesario borrar los pixels de frontera que ya no tienen vecinos blancos. Usamos para ello: void Image::clean_boundary(queue<point> & Q) { vector<point> Keep; point aux; int i, size; while (!Q.empty()) { aux = Q.front(); if (has_white_8neighbor(aux)) Keep.push_back(aux); else *aux = black; Q.pop(); } size = Keep.size(); for (i=0; i<size; ++i) *( Keep[i] ) = white; } Esta función simplemente pinta los pixels sin vecinos blancos de negro, y el resto de blanco, para hallar la frontera de nuevo, esta vez ordenada. No debemos colorear de blanco ningún pixel de frontera hasta haber borrado los pixels necesarios. De otra forma, falseamos el contorno. Ahora podemos ya obtener el contorno ordenado de forma sencilla, usando C.4 Obtención del contorno 117 void Image::get__contour_depth_first(bool once, queue<point> & Q) { int i; point pt, aux, first, last = start-1; bool is_on_top; pt = first = get_next_boundary_point(last); while ( pt != 0) { last = first = pt; stack<point> S; *pt = bound; S.push(pt); while ( !S.empty()) { pt=S.top(); is_on_top = true; for (i=0; i<4; ++i) { aux= pt + offset_4neighbor[i]; if (in_range(aux) && is_8boundary_point(aux)) { is_on_top = false; *aux=bound; S.push(aux); break; } } if (is_on_top) { assert( are_4neighbors(pt, first)); stack_to_queue(S, Q); } } if (once) break; pt = get_next_boundary_point(last); } 118 Apéndice C Implementación } Esta función de apariencia complicada es en realidad bastante simple. Busca un punto de contorno, y si lo encuentra, realiza una búsqueda a lo profundo de pixels de frontera vecinos. Es casi idéntico a fill4_depth_first que hemos visto, incluyendo la variable booleana is_on_top para saber cuándo llegamos a un callejón sin salida. En este caso, sólo llegamos a un callejón sin salida cuando hemos concluido el recorrido del contorno. Para certificarlo, la sentencia assert comprueba si el último pixel añadido a la pila es vecino del pixel inicial. Si no es así, la limpieza del contorno, después de todo, no ha sido efectiva. La sentencia assert se ha cumplido para todas las imágenes del proyecto, por lo que podemos decir que la limpieza de contorno es muy robusta. La función toma un parámetro booleano, once, que nos indica si debemos buscar un solo contorno o más. La razón de esto es que en posiciones de la mano con agujeros, tendremos al menos dos curvas cerradas de contorno. Si deseamos usar sólo el contorno exterior, daremos a once (“una vez” en inglés), el valor true. La función are_4neighbors toma dos pixels como argumentos y recorre todos los vecinos de uno de ellos, devolviendo true en caso de que el otro se encuentre entre los vecinos. La función stack_to_queue forma una cola con los elementos de la pila. Con esto queda concluida la extracción del contorno. C.5. Procesado del contorno La primera operación a hacer para procesar el contorno es cambiar los pixels de representación. Dado un pixel representado como un puntero a un byte en la imagen, debemos ser capaces de producir un número complejo cuya parte real sea el número de columna del pixel, y cuya parte imaginaria sea el número de fila. Aprovechando la posibilidad de hacer aritmética con punteros que nos ofrece C, podemos hallar el número de pixel en que nos encontramos respecto del pixel inicial de imagen, es decir, el índice de pixel. Puesto que las filas completas se almacenan una después de otra, dividiendo el índice de pixel por el número de pixels en cada fila tenemos como cociente el número de fila, y como resto, el número de columna. typedef complex<float> cmplx; cmplx Image::make_complex(point pt) C.5 Procesado del contorno 119 { int dif, row, col; dif = pt-start; row = dif / width; col = dif % width; cmplx re(col, height-1-row); return re; } Nótese que debido a que en nuestra matriz de pixels la fila 0 es la superior, damos la parte imaginaria referida a la fila inferior para tener un sistema de coordenadas más habitual. Convertir la lista de pixels de frontera en una lista de números complejos es una cuestión trivial, así como lo es hallar la lista de diferencias, de argumentos y de curvaturas. Como muestra, presentamos la función que decima (rechaza elementos de lista periódicamente) la lista de pixels de frontera. void decimate(int n, vector<cmplx> & V) { vector<cmplx> re; int i, size = V.size(); for (i=0; i<size; ++i) if (i%n == 0) re.push_back(V[i]); V = re; } Las demás funciones de este tipo son análogas. Vale la pena examinar las funciones que se ocupan de la detección de dedos. Algunas de ellas van a producir la lista de elementos de contorno (representada por una ristra de letras). Estas funciones no presentan ninguna peculiaridad, y ocupan mucho espacio, por lo que no las presentamos. Una vez tenemos la lista de elementos de contorno, la siguiente función obtiene el número de dedos. int numfingers(vector<char> letters) { int i, size = letters.size(), crossings =0; bool is_curved; 120 Apéndice C Implementación dilate_tips(letters); is_curved = (letters[size-1] == ’c’); for (i=0; i<size; ++i) if (letters[i] != ’c’ && is_curved) { is_curved = false; ++crossings; } else if (letters[i] == ’c’ && !is_curved) { is_curved = true; } return crossings; } La función realiza una llamada a dilate_tips, que dilata las yemas, es decir, convierte en c las letras adyacentes a una c, como se describe en el capítulo 5. Después, cuenta el número de dedos, que es el número de regiones de cs consecutivas. Para obtener este número cuenta los cambios de c a otra letra. C.6. Inclinación y momentos de inercia Vimos en el capítulo 6 que para definir el eje de una región es conveniente hallar una serie de estadísticos comúnmente usados, como las medias y los momentos centrales de orden 2. La función get_statistics los calcula: void Image::get_statistics() { int x, y; double sum = 0, sum_x=0, sum_y =0; double sum_xx =0, sum_xy = 0, sum_yy = 0; double cx, cy, mu_11, mu_20, mu_02; double mean_square_angle, axis_angle; double mom_1, mom_2; for (y=0; y<height; ++y) { for (x=0; x<width; ++x) { C.7 Operaciones morfológicas 121 if (matrix[height-1-y][x] == white) { ++sum; sum_x += x; sum_y += y; sum_xx += x*x; sum_xy += x*y; sum_yy += y*y; } } } cx = sum_x / static_cast<double>(sum); cy = sum_y / static_cast<double>(sum); mu_11 = sum_xy - cx*cy*sum; mu_20 = sum_xx - cx*cx*sum; mu_02 = sum_yy - cy*cy*sum; . . Para las sumas, que dan números enteros, se ha tenido que usar tipo de datos double, debido a que los números producidos son tan grandes que no caben tan siquiera en un long int. Esta es una de las pocas ocasiones en que vale la pena acceder a los pixels por fila y columna. cx y cy representan las medias, y mu_nm los momentos centrales. El procedimiento es muy grande, y no lo hemos mostrado entero. El resto contiene sentencias para hallar los ejes y los momentos, dados los estadísticos. Hacer una función grande suele ser mala política, pero en este caso, para poder usar los estadísticos hubiéramos debido definirlos como variables globales, siempre desaconsejable, o haber formado con ellos una estructura de datos. Formar una estructura de datos es lo más recomendable, y si no se ha hecho aquí es porque el procesado es tan sencillo que casi no vale la pena molestarse. C.7. Operaciones morfológicas Presentamos ahora la implementación de las operaciones morfológicas. La más sencilla de ellas es la erosión: void Image::erode(int n, queue<point> & Q) 122 Apéndice C Implementación { point pt, aux; int i; if (Q.empty()) { std::cerr << "erode: given an" << " empty boundary queue!!" << endl; return; } for ( ; n>0; --n) { aux = 0; Q.push(aux); pt = Q.front(); while (pt !=0) { for (i=0; i<8; ++i) { aux= pt + offset_8neighbor[i]; if (in_range(aux) && *aux==white) { *aux=bound; Q.push(aux); } } *pt=black; Q.pop(); pt = Q.front(); } Q.pop(); } } Incluye un par de sentencias para reaccionar en caso de que intentemos aplicar esta operación a una región cuya frontera no hemos hallado todavía. Este manejador de error está presente en las otras operaciones morfológicas. Como vemos, usamos la STL y su implementación de las colas. Las operaciones de dilatación y transformación de distancia tienen una implementación muy parecida, que no mostramos. La función que obtiene el esqueleto es más interesante: C.7 Operaciones morfológicas 123 void Image::get_skeleton(queue<point> & Q) { int i, num_neighbors; point pt, aux; if (Q.empty()) { std::cerr << "get_skeleton: given an" << " empty boundary queue!!" << endl; return; } while ( !Q.empty()) { pt=Q.front(); for (i=0; i<8; ++i) { aux= pt + offset_8neighbor[i]; if (in_range(aux) && *aux==white) { num_neighbors = get_num_8neighbors(aux, white); if (!makes_8connection(aux, bound) && num_neighbors >= 2 && num_neighbors <= 6 ) { *aux=bound; Q.push(aux); } else { *aux=skel; } } } Q.pop(); } } Colorea los pixels de esqueleto de un nivel de gris especial, llamado skel. Utiliza dos funciones auxiliares; makes_8connection comprueba si un pixel, de ser coloreado del nivel de gris de los pixels de frontera, produciría una conexión en 124 Apéndice C Implementación la frontera, o, lo que es igual, ahogaría una región blanca. get_num_8neighbors simplemente recorre los 8-vecinos de un pixel y cuenta cuántos de ellos son blancos. C.8. El intérprete Para tener una mayor flexibilidad en la aplicación de operaciones sobre imágenes, se ha escrito un intérprete de comandos rudimentario. Gracias a él nos ahorramos recompilar cada vez que queremos combinar operaciones de una nueva forma. No es más que una sentencia if ... else ... gigante, y proporciona cronometrado de operaciones: void eval(string expr, Image & im, queue<point> & Q) { clock_t start_, finish_; start_ = clock(); if (expr == "write") { char bf[40]; std::cin >> bf; im.write_TIFF(bf); } else if (expr == "read") { char bf[40]; std::cin >> bf; im.read_TIFF(bf); } else if (expr == "threshold") { int level; . . . El comportamiento del programa está determinado por ciertas variables globales, como VERBOSE o DEPTH_FIRST, que controlan cuánta información imprime en pantalla, y qué alternativas de implementación escoge. Podemos dar valores a estas variables a través de la línea de comandos, para lo cual usamos un lector específico a tal fin, parse_command_line C.9 Interacción con el exterior 125 C.9. Interacción con el exterior Las imágenes que usa el proyecto están almacenadas en disco duro, en ficheros de tipo TIFF. Por suerte, hay una librería, TIFFIO, que es gratis y proporciona funciones para leer y escribir ficheros TIFF. La documentación de esta librería, por desgracia, no es muy buena, así que mostramos la función que escribe los contenidos de la imagen en memoria en un fichero: void Image::write_TIFF(char *filename) { TIFF* tif = TIFFOpen(filename, "w"); if (!tif) { std::cerr << "Cannot write file: " << filename << endl; return; } TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height); TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width); TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8); TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); TIFFSetField(tif, TIFFTAG_MINSAMPLEVALUE, 0); TIFFSetField(tif, TIFFTAG_MAXSAMPLEVALUE, 255); TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_BOTLEFT); for (int r = 0; r < height; r++) TIFFWriteScanline(tif, matrix[r], r); TIFFClose(tif); } También se ha implementado un pequeño interfaz gráfico para poder visualizar la imagen contenida en memoria en una ventana. Para ello se usa la librería de widgets GTK+. Esta librería está basada en el sistema de ventanas X de MIT, y está escrita en C. Todas las distribuciones de Linux la incluyen, pero no suele estar disponible en sistemas UNIX más antiguos. Bibliografía [1] Harold Abelson and Gerald Jay Sussman with Julie Sussman. Structure and Interpretation of Computer Programs. MIT Press, Cambridge, MA, second edition, 1996. [2] Kenneth R. Castleman. Digital Image Processing. Prentice Hall, Upper Saddle River, NJ, 1996. [3] Thomas Cormen, Charles Leiserson, and Ronald Rivest. Introduction to Algorithms. MIT Press, Cambridge, MA, 1990. [4] E. R. Davies. Machine Vision: Theory, Algorithms, Practicalities. Academic Press, London, second edition, 1997. [5] Manfredo P. do Carmo. Geometría diferencial de curvas y superficies. Alianza Editorial, Madrid, 1995. [6] Richard Feynman. Fisica. Volumen I. Addison-Wesley Iberoamericana, Wilmington, DE, 1987. [7] James D. Foley, van Dam, Feiner, and Hughes. Computer Graphics: Principles and Practice. Addison-Wesley, Reading, MA, second edition, 1996. [8] Marc Levoy. Area flooding algorithms. Technical report, Hanna-Barbera Productions, June 1981. [9] Marcos Pineda Ortega. Desarrollo de un sistema de clasificación de gestos de la mano, 1998. Proyecto de Fin de Carrera, ETSI Telecomunicaciones. UPM. [10] Athanasios Papoulis. Probability, Random Variables, and Stochastic Processes. McGraw-Hill, New York, third edition, 1991. [11] J. R. Parker. Practical Computer Vision using C. John Wiley & Sons, New York, 1993. 128 BIBLIOGRAFÍA [12] Patrick Henry Winston. Artificial Intelligence. Addisson Wesley, Reading, MA, second edition, 1992. [13] T. Y. Zhang and C. Y. Suen. A fast parallel algorithm for thinning digital patterns. In Communications of the ACM, volume 27, pages 236–239, 1984.