Sistema eficiente de reconocimiento de gestos de la mano

Anuncio
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.
Descargar