A medida que desarrollábamos el sistema de menús nos encontramos con la necesidad de utilizar un gran número de timers para contabilizar tiempos y, mediante el uso de flags, poder controlar procesos.
Primero, para el juego de piedra papel o tijera, en el que se tienen que utilizar diferentes usos para la cuenta atrás antes de iniciar la partida, el tiempo que queda el resultado de cada partida, y el tiempo en el que se muestra el resultado del juego (conjunto de partidas). Luego en el menú de reloj, donde hacemos parpadear el dígito que se está utilizando en cada momento. Y por último al salir de cada uno de los submenús, dando un tiempo desde que salimos hasta que se puede entrar a cualquier otro, para evitar salir y entrar inmediatamente por error.
Necesitábamos por tanto utilizar un gran número de timers, algunos de los cuales a la vez, y la mayoría con tiempos de interrupción bastante diferentes (de entre poco menos de un segundo y hasta tres segundos). Poniendo en la práctica la solución observada en problemas de la asignatura SEDG, creamos una variable global timeOut. Cada vez que queramos hacer uso de un timer, modificamos su valor e iniciamos el mismo timer; en la interrupción del timer dicha variable se va decrementando hasta llegar a 0, en cuyo momento se desactiva el timer.
Por precisar de varios timers al mismo tiempo, no nos basta con una única variable timeOut. Creamos un array de timeouts de un total de TIMEOUTS posiciones, de tal forma que con la misma fuente de interrupción (timer3) podemos implementar varios timers diferentes. Cada vez que el timer esté activado, recorrerá el array de timeouts y disminuirá todas aquellas posiciones en las que haya un entero diferente de 0. Cuando timeOut[x], siendo x cualquiera de las posiciones posibles, llegue a 0, es que ha transcurrido el periodo previsto.
El periodo de todos estos timers, a pesar de variar, es siempre de un tiempo apreciable para el usuario. Configuramos la interrupción por timer3 a un valor de 0,2 segundos. Dado el reloj de 80MHz y el preescalado máximo de 256, el periodo de cuenta del temporizador será de 3.2 us, y por tanto lo configuraremos para que interumpa cada 62500 ciclos de cuenta. Nuestros periodos serán por tanto siempre de un múltiplo de 0,2 segundos.
Iniciamos la posición deseada del array de timeOuts mediante una función timeOut, la cual inicia el timer3 si no lo estaba ya.
Puede consultarse aquí la rutina de interrupción del timer (timer3) y la breve función para inicialización de timeOuts (función timeOut).
lunes, 14 de mayo de 2012
Periféricos y más periféricos
A la hora de diseñar el menú nos planteamos incluir las funciones de calefacción y de control de luces. Sería interesante tener algo físico que simulara estos dos sistemas. ¿Por qué no incluir un sensor de temperatura, algo que simule un calentador y unos LED's cuya luminosidad podamos variar?
La idea de la interfaz gráfica era mostrar que con los gestos se realizba alguna acción así que lo de que esa acción se realizara físicamente era algo un poco secundario. Sin embargo, nuestro PIC32 tiene una serie de periféricos muy potentes y que no estabamos utilizando para nada, así que decidimos dar uso a dos periféricos más, los módulos de PWM y el conversor analógico/digital.
El PIC32 dispone de 5 canales de PWM "independientes", y un conversor digital analógico de 10 bit de tipo SAR capaz de trabajar a hasta 1 Ms/s y con varios sistemas de multiplexación analógica para dar cobertura a varios canales. Con esto podemos controlar 5 LED's de forma independiente y un sensor de temperatura de los más simples que existen, que proporcionan una tensión proporcional a la temperatura.
En funcionamiento del PWM es bastante sencillo. Un temporizador arranca y comienza a contar a una frecuencia determinada hasta llegar a un valor preajustado de 16 bit (periodo), momento en el cual se reinicia y comienza a contar de nuevo.Con este valor ajustamos el periodo de la señal de PWM. Los módulos de PWM simplemente se ponen a vigilar a este temporizador. Cada uno de estos módulos tiene su propio registro de 16 bit con un valor al que vamos a llamar PWMi con i indicando el número del canal de PWM de 1 a 5. Cuando el timer alcanza algún valor PWMi, el correspondiente módulo PWM conmuta su salida y la pone a nivel bajo. Cuando se alcanza el valor de periodo y el temporizador se reinicia, el módulo de PWM vuelve a poner a nivel alto su salida.
El ciclo de trabajo de cada módulo será PWMi/periodo y por tanto PWMi tendrá que ser menor o igual que periodo, y justamente el valor "periodo" nos marcará la resolución disponible en el ciclo de trabajo. Nosotros trabajamos con una frecuencia de PWM (1/periodo) de 1200Hz por puro capricho (hubiera bastado con una frecuencia suficiente para no notar parpadeo), y a esa frecuencia conseguimos trabajar con 16 bit de resolución (también por capricho, ya que en realidad tanta resolución es innecesaria para esta aplicación).
En cuanto al conversor analógico/digital, la cosa resultó ser algo más complicada. Este periférico del PIC es tan sofisticado que configurarlo para realizar una tarea tan tonta como tomar una muestra cuando nosotros se lo pidamos y darnos el valor convertido sin hacer nada con él requirió leerse la parte completa de la hoja de características referente al módulo para entender el funcionamiento completo del mismo y saber qué cosas desactivar y cuales no para conseguir nuestro objetivo. Afortunadamente funcionó a la primera.
Para aquellos que tengan curiosidad, aquí está el documento relativo a este módulo del PIC32:
http://ww1.microchip.com/downloads/en/DeviceDoc/61104E.pdf
Al final, nuestra práctica utiliza varios periféricos del PIC: una UART, un puerto I2C (sí, nosotros también utilizamos este maravilloso estándar de comunicación entre integrados) , un comparador analógico, el conversor analógico/digital, los módulos de PWM , el reloj/calendario de tiempo real y 4 temporizadores.
La idea de la interfaz gráfica era mostrar que con los gestos se realizba alguna acción así que lo de que esa acción se realizara físicamente era algo un poco secundario. Sin embargo, nuestro PIC32 tiene una serie de periféricos muy potentes y que no estabamos utilizando para nada, así que decidimos dar uso a dos periféricos más, los módulos de PWM y el conversor analógico/digital.
El PIC32 dispone de 5 canales de PWM "independientes", y un conversor digital analógico de 10 bit de tipo SAR capaz de trabajar a hasta 1 Ms/s y con varios sistemas de multiplexación analógica para dar cobertura a varios canales. Con esto podemos controlar 5 LED's de forma independiente y un sensor de temperatura de los más simples que existen, que proporcionan una tensión proporcional a la temperatura.
En funcionamiento del PWM es bastante sencillo. Un temporizador arranca y comienza a contar a una frecuencia determinada hasta llegar a un valor preajustado de 16 bit (periodo), momento en el cual se reinicia y comienza a contar de nuevo.Con este valor ajustamos el periodo de la señal de PWM. Los módulos de PWM simplemente se ponen a vigilar a este temporizador. Cada uno de estos módulos tiene su propio registro de 16 bit con un valor al que vamos a llamar PWMi con i indicando el número del canal de PWM de 1 a 5. Cuando el timer alcanza algún valor PWMi, el correspondiente módulo PWM conmuta su salida y la pone a nivel bajo. Cuando se alcanza el valor de periodo y el temporizador se reinicia, el módulo de PWM vuelve a poner a nivel alto su salida.
El ciclo de trabajo de cada módulo será PWMi/periodo y por tanto PWMi tendrá que ser menor o igual que periodo, y justamente el valor "periodo" nos marcará la resolución disponible en el ciclo de trabajo. Nosotros trabajamos con una frecuencia de PWM (1/periodo) de 1200Hz por puro capricho (hubiera bastado con una frecuencia suficiente para no notar parpadeo), y a esa frecuencia conseguimos trabajar con 16 bit de resolución (también por capricho, ya que en realidad tanta resolución es innecesaria para esta aplicación).
En cuanto al conversor analógico/digital, la cosa resultó ser algo más complicada. Este periférico del PIC es tan sofisticado que configurarlo para realizar una tarea tan tonta como tomar una muestra cuando nosotros se lo pidamos y darnos el valor convertido sin hacer nada con él requirió leerse la parte completa de la hoja de características referente al módulo para entender el funcionamiento completo del mismo y saber qué cosas desactivar y cuales no para conseguir nuestro objetivo. Afortunadamente funcionó a la primera.
Para aquellos que tengan curiosidad, aquí está el documento relativo a este módulo del PIC32:
http://ww1.microchip.com/downloads/en/DeviceDoc/61104E.pdf
Al final, nuestra práctica utiliza varios periféricos del PIC: una UART, un puerto I2C (sí, nosotros también utilizamos este maravilloso estándar de comunicación entre integrados) , un comparador analógico, el conversor analógico/digital, los módulos de PWM , el reloj/calendario de tiempo real y 4 temporizadores.
Generación de gráficos
Para hacer más sencillo el uso del sistema decidimos incorporar una interfaz gráfica. Propusimos como mejora la integración de una pantalla con el sistema, para que fuera completamente autónomo. Sin embargo, el tiempo disponible no iba a ser suficiente para diseñar y fabricar la electrónica asociada a la pantalla, así que finalmente seguimos utilizando la pantalla del ordenador como medio de visualización. Eso sí, todos los gráficos se generan internamente en el PIC32 y al ordenador únicamente se le envían los valores de cada píxel.
El sistema gráfico trabaja a 48x48 píxeles debido a que ese es el tamaño al que capturamos las imágenes, y es una resolución suficiente para mostrar la información que queremos. Dado que el interés de la práctica no radica en la creación de una interfaz gráfica sofisticada, hicimos la implementación de la forma más sencilla que se nos ocurrió. Partimos de nuestra imágen de 48x48 píxeles, que recordamos que se almacena con este formato en la RAM del PIC32:
Ya que guardamos la imagen aprovechando cada bit para guardar un píxel y agrupándolos en bytes, lo más sencillo era realizar una interfaz gráfica completamente en blanco y negro, partiendo del fotograma en 48x48 que se genera tras el filtrado de mediana.
Los gráficos que se muestran por pantalla se guardan en la memoria flash del PIC como arrays con un formato similar al de la imagen de 48x48 guardando cada bit en un píxel. Estos iconos siempre tienen un tamaño horizontal múltiplo de 8 píxeles y podrán colocarse en cualquier posición vertical dentro de la imagen, y ocupando un número entero de bytes horizontales para simplificar el proceso de superposición sobre el fotograma. Esta restricción hace que incorporar un gráfico al fotograma sea tan simple como ir haciendo una operación de XOR entre un byte del fotograma y un byte del gráfico que deba caer en esa posición. Haciendo un XOR y considerando que aquellos píxeles del gráfico que lo definen valen 1 y el resto 0, tendremos que aquellas posiciones del fotograma ocupadas por el gráfico ven invertido el color de sus píxeles (recordemos que una operación de XOR con 1 invierte el bit correspondiente, y un XOR con 0 lo deja como estaba). Así, podremos mostrar la mano (o cualquier cosa que la cámara esté capturando) como fondo y superponer por encima los gráficos mientras seguimos viendo ambos elementos de la imagen simultaneamente. La siguiente foto es un simple ejemplo ilustrativo.
De esta forma conseguimos una forma muy rápida y sencilla de colocar gráficos sobre la imagen. La superposición de los gráficos se hace después de escalar el fotograma filtrado, almacenándose el fotograma completo a mostrar (gráficos incluidos) en la misma zona de memoria donde se encontraba el fotograma filtrado.
Nos hemos encargado de diseñar la mayoría de los gráficos que componen la interfaz. Algunos ejemplos son los siguientes:
El texto lo generamos con photoshop previamente en la mayoría de los casos, ya que el solo disponer de 6 posibles posiciones horizontales para colocar iconos nos limitaba a textos de máximo 6 letras, si utilizaramos una fuente y generaramos el texto dinámicamente en el PIC32. Lo que se almacenan son parches que contienen el texto a mostrar con una fuente lo más estrecha posible. Los números y algunos símbolos sí que generan utilizando una fuente de 8x8 píxeles de dominio público, que puede encontrarse aquí.
El sistema gráfico trabaja a 48x48 píxeles debido a que ese es el tamaño al que capturamos las imágenes, y es una resolución suficiente para mostrar la información que queremos. Dado que el interés de la práctica no radica en la creación de una interfaz gráfica sofisticada, hicimos la implementación de la forma más sencilla que se nos ocurrió. Partimos de nuestra imágen de 48x48 píxeles, que recordamos que se almacena con este formato en la RAM del PIC32:
Ya que guardamos la imagen aprovechando cada bit para guardar un píxel y agrupándolos en bytes, lo más sencillo era realizar una interfaz gráfica completamente en blanco y negro, partiendo del fotograma en 48x48 que se genera tras el filtrado de mediana.
Los gráficos que se muestran por pantalla se guardan en la memoria flash del PIC como arrays con un formato similar al de la imagen de 48x48 guardando cada bit en un píxel. Estos iconos siempre tienen un tamaño horizontal múltiplo de 8 píxeles y podrán colocarse en cualquier posición vertical dentro de la imagen, y ocupando un número entero de bytes horizontales para simplificar el proceso de superposición sobre el fotograma. Esta restricción hace que incorporar un gráfico al fotograma sea tan simple como ir haciendo una operación de XOR entre un byte del fotograma y un byte del gráfico que deba caer en esa posición. Haciendo un XOR y considerando que aquellos píxeles del gráfico que lo definen valen 1 y el resto 0, tendremos que aquellas posiciones del fotograma ocupadas por el gráfico ven invertido el color de sus píxeles (recordemos que una operación de XOR con 1 invierte el bit correspondiente, y un XOR con 0 lo deja como estaba). Así, podremos mostrar la mano (o cualquier cosa que la cámara esté capturando) como fondo y superponer por encima los gráficos mientras seguimos viendo ambos elementos de la imagen simultaneamente. La siguiente foto es un simple ejemplo ilustrativo.
De esta forma conseguimos una forma muy rápida y sencilla de colocar gráficos sobre la imagen. La superposición de los gráficos se hace después de escalar el fotograma filtrado, almacenándose el fotograma completo a mostrar (gráficos incluidos) en la misma zona de memoria donde se encontraba el fotograma filtrado.
Nos hemos encargado de diseñar la mayoría de los gráficos que componen la interfaz. Algunos ejemplos son los siguientes:
El texto lo generamos con photoshop previamente en la mayoría de los casos, ya que el solo disponer de 6 posibles posiciones horizontales para colocar iconos nos limitaba a textos de máximo 6 letras, si utilizaramos una fuente y generaramos el texto dinámicamente en el PIC32. Lo que se almacenan son parches que contienen el texto a mostrar con una fuente lo más estrecha posible. Los números y algunos símbolos sí que generan utilizando una fuente de 8x8 píxeles de dominio público, que puede encontrarse aquí.
sábado, 12 de mayo de 2012
Sistema de menús
Nuestra práctica en si consistía en el reconocimiento de gestos estáticos con PIC32, pero como hemos mencionado en la anterior entrada, hemos dedicado este tercer hito a desarrollar una aplicación demostrativa de su funcionamiento, orientada a control domótico.
Hemos implantado para ello un sistema de menús: un menú principal desde el que, con diferentes gestos listados en el manual de usuario, puedes acceder a diversos submenús con las funcionalidades del sistema.
Idle: menú principal, en el que se ve el reloj del sistema y desde el que se puede acceder mediante gestos específicos a cualquiera de los submenús.
Idle: menú principal, en el que se ve el reloj del sistema y desde el que se puede acceder mediante gestos específicos a cualquiera de los submenús.
Luces: submenú para control de luces. Indicando un número del 1 al 5 se accede al control de una de cinco luces posibles; con otro gesto pueden encenderse y apagarse (toggle) y moviendo el dedo de arriba abajo se calibra la intensidad lumínica de la luz seleccionada.
Clima: control de climatización. Deslizando el dedo de arriba abajo por la pantalla se selecciona la temperatura deseada, la cual se valida mediante otro gesto.
Reloj: cambia la hora del sistema. Permite, de nuevo mediante gestos, cada uno asociado a un dígito del 0 al 9, ir cambiando cada dígito de la hora.
Música: función no implantada por el momento, y que posiblemente sea retirada o sustituida.
Juego: accede al minijuego de prueba, un juego de piedra-papel-tijera contra el PIC. Al iniciar debe seleccionarse el número de partidas a desarrollar, y hace uso de varios timers para la cuenta atrás antes de cada partida, espera entre dos juegos, etc. Al final de cada juego muestra por pantalla el ganador (PIC o humano).
Hemos seleccionado los gestos asociados al acceso a cada submenú y control del mismo con cuidado para que resulten todo lo intuitivos que sea posible.
Para el desarrollo software del sistema de menús nos basamos en un funcionamiento por punteros a funciones. En el main del sistema siempre se ejecuta la función a la que apunta el puntero func_menu; de esta forma para pasar del menú a un submenú simplemente editamos la función a la que apunta el puntero. Esto se hace en idle para acceder a cualquier otro de los submenús, o a la salida del submenú para volver a idle.
func_menu define los parámetros de entrada que tendrán las funciones a las que apunta. Por tanto todas las funciones de menú tienen como argumentos el gesto reconocido en el fotograma y la información proporcionada por el tracking de dedos, dada la necesidad de homogeneizar. Esto es así a pesar de que algunas funciones como reloj o juego no hacen uso del tracking de dedos y sólo utilizan el número de gesto reconocido.
A continuación se incluye una porción de código de idle, la función que hace de menú principal y desde el que puede accederse a todos los demás, a fin de ilustrar el uso de punteros.
Para el desarrollo software del sistema de menús nos basamos en un funcionamiento por punteros a funciones. En el main del sistema siempre se ejecuta la función a la que apunta el puntero func_menu; de esta forma para pasar del menú a un submenú simplemente editamos la función a la que apunta el puntero. Esto se hace en idle para acceder a cualquier otro de los submenús, o a la salida del submenú para volver a idle.
func_menu define los parámetros de entrada que tendrán las funciones a las que apunta. Por tanto todas las funciones de menú tienen como argumentos el gesto reconocido en el fotograma y la información proporcionada por el tracking de dedos, dada la necesidad de homogeneizar. Esto es así a pesar de que algunas funciones como reloj o juego no hacen uso del tracking de dedos y sólo utilizan el número de gesto reconocido.
A continuación se incluye una porción de código de idle, la función que hace de menú principal y desde el que puede accederse a todos los demás, a fin de ilustrar el uso de punteros.
- void idle(uint8_t gesto, struct dedos *dedos_t) {
- if (!timeout[2]) {
- switch (gesto) { //pasamos a una u otra función según el gesto que elija el usuario
- case GESTO_LUCES: //el usuario quiere acceder al menú de luces
- func_menu = &luz_control; // pasamos a control de luces
- break;
- case GESTO_CLIMA:
- func_menu = &clima_control; //pasamos a control de climatizador
- break;
- case GESTO_RELOJ:
- func_menu = &hora_control;
- break;
- case GESTO_JUEGO:
- func_menu = &juegoPPT; //pasamos a juego
- break;
- case GESTO_MUSICA:
- //printf("paso a musica \n");
- break;
- }
- }
Y todo esto, ¿para qué sirve?
Al final del hito 2 ya teníamos un sistema de reconocimiento de gestos capaz de funcionar correctamente con las manos de diversas personas. Además, implementamos un medidor de confianza y un sistema basado en un buffer circular que almacena las hipótesis asociadas a los últimos N fotogramas (ahora mismo N =10) y da como gesto reconocido en cada instante aquel que haya aparecido más veces como hipótesis en los N instantes anteriores. Esto introduce un pequeño retardo de aproximadamente medio segundo, pero hace que el sistema sea robusto en las transiciones entre gestos, evitando falsos positivos. De esta forma conseguimos un sistema fiable que puede ser aplicado en una interfaz gestual para el control de algúna aplicación.
Con el sistema actual, aparte de disponer de reconocimiento de gestos también contamos con un sistema de tracking de dedos. Utilizaremos toda esa información para controlar un sistema de menús con varias funciones, como el control de luces, climatizador, reloj y posiblemente música, y aparte un pequeño juego de piedra-papel o tijeras, que resulta idoneo para demostrar nuestra tecnología de una manera divertida. Sobre la imagen de la mano en 48x48 píxeles (después del filtro de mediana, pero antes de efectuar el resto de operaciones de procesamiento, como el escalado) se muestra información de forma gráfica para poder navegar por el sistema.
A estas alturas del curso resultaba demasiado compleja la implementación de una pantalla gráfica al sistema. Al ser un sistema que trabaja a 25 fotogramas por segundo, necesitaríamos una pantalla con la sufciente velocidad de refresco para mostrar una calidad razonable, lo cual hace que hubiera que elegir una pantalla de tipo TFT-LCD u OLED. Ambos tipos de pantalla exigen un sistema electrónico extra relativamente complejo para su funcionamiento. Por ello, finalmente utilizamos la pantalla del ordenador como pantalla del sistema. Eso sí, todos los gráficos los genera el PIC32 por sí mismo, y lo único que hace es mandar en crudo el color de cada píxel (blanco o negro) de una imagen de 48x48 al ordenador, que se encarga de mostrarla por pantalla de forma totalmente pasiva. De esta forma, si en algún momento quisieramos incluir una pantalla al sistema, tan solo necesitaríamos fabricar el hardware adicional y configurar al PIC para que mandara a la pantalla exactamente la misma información que manda al ordenador actualmente.
Con el sistema actual, aparte de disponer de reconocimiento de gestos también contamos con un sistema de tracking de dedos. Utilizaremos toda esa información para controlar un sistema de menús con varias funciones, como el control de luces, climatizador, reloj y posiblemente música, y aparte un pequeño juego de piedra-papel o tijeras, que resulta idoneo para demostrar nuestra tecnología de una manera divertida. Sobre la imagen de la mano en 48x48 píxeles (después del filtro de mediana, pero antes de efectuar el resto de operaciones de procesamiento, como el escalado) se muestra información de forma gráfica para poder navegar por el sistema.
A estas alturas del curso resultaba demasiado compleja la implementación de una pantalla gráfica al sistema. Al ser un sistema que trabaja a 25 fotogramas por segundo, necesitaríamos una pantalla con la sufciente velocidad de refresco para mostrar una calidad razonable, lo cual hace que hubiera que elegir una pantalla de tipo TFT-LCD u OLED. Ambos tipos de pantalla exigen un sistema electrónico extra relativamente complejo para su funcionamiento. Por ello, finalmente utilizamos la pantalla del ordenador como pantalla del sistema. Eso sí, todos los gráficos los genera el PIC32 por sí mismo, y lo único que hace es mandar en crudo el color de cada píxel (blanco o negro) de una imagen de 48x48 al ordenador, que se encarga de mostrarla por pantalla de forma totalmente pasiva. De esta forma, si en algún momento quisieramos incluir una pantalla al sistema, tan solo necesitaríamos fabricar el hardware adicional y configurar al PIC para que mandara a la pantalla exactamente la misma información que manda al ordenador actualmente.
lunes, 7 de mayo de 2012
Tracking de dedos
Mientras trabajabamos con el entrenamiento de la red neuronal decidimos implementar un sistema de tracking de los dedos del usuario, que nos permitiera saber cuántos dedos aparecen en cada fotograma y en qué posiciones. Aunque pretendíamos utilizar esta funcionalidad para entregar un dato extra a la red neuronal, finalmente vamos a utilizarlo para ajustar algunos parámetros del sistema de control domótico que pretendemos implementar finalmente.
Nos planteamos como reto que el sistema de reconocimiento fuera lo más sencillo posible, y capaz de detectar las yemas de los dedos que aparecieran en cualquier ángulo en la imagen, y que el algoritmo solo necesitara de una iteración para conseguirlo. A priori puede parecer una tarea complicada, pero en nuestro mundo de 24x24 píxeles las cosas se simplifican.
Echemos un vistazo a algunas imágenes de manos ampliadas en las que aparezcan dedos, tal como las captura el PIC32:
Se ha marcado con rojo las zonas correspondientes a las yemas de los dedos. Puede apreciarse que en condiciones normales de funcionamiento las yemas de los dedos pueden contenerse en un cuadrado de 2x2 píxeles. De hecho, la condición de que la mano deba abarcar la pantalla entera antes de poder comenzar a realizar gestos para que el reconocimiento funcione también consigue que los dedos ocupen el tamaño deseado. Bajo esta premisa, basamos nuestro algoritmo de tracking de dedos en comparar cada zona de 4x4 píxeles de la imagen con un parche como este:
Nos planteamos como reto que el sistema de reconocimiento fuera lo más sencillo posible, y capaz de detectar las yemas de los dedos que aparecieran en cualquier ángulo en la imagen, y que el algoritmo solo necesitara de una iteración para conseguirlo. A priori puede parecer una tarea complicada, pero en nuestro mundo de 24x24 píxeles las cosas se simplifican.
Echemos un vistazo a algunas imágenes de manos ampliadas en las que aparezcan dedos, tal como las captura el PIC32:
Se ha marcado con rojo las zonas correspondientes a las yemas de los dedos. Puede apreciarse que en condiciones normales de funcionamiento las yemas de los dedos pueden contenerse en un cuadrado de 2x2 píxeles. De hecho, la condición de que la mano deba abarcar la pantalla entera antes de poder comenzar a realizar gestos para que el reconocimiento funcione también consigue que los dedos ocupen el tamaño deseado. Bajo esta premisa, basamos nuestro algoritmo de tracking de dedos en comparar cada zona de 4x4 píxeles de la imagen con un parche como este:
Nos recorremos la imagen de arriba a abajo y de izquierda a derecha, cogiendo los píxeles en grupos de 4x4. Si el grupo de 4x4 píxeles de la imagen tiene al menos 3 píxeles en negro lo comparamos con el parche mencionado, sumando un punto por cada píxel de la zona de la imagen que sea igual que el píxel correspondiente del parche. Una vez establecida la comparación, observamos la puntuación del grupo de píxeles. Hay 16 píxeles en cada grupo, y la puntuación indica cuántos de ellos eran iguales a los correspondientes píxeles del parche. Si la puntuación es 12 o más, consideramos que en ese grupo de píxeles de la imagen estaba la yema de un dedo, y marcamos un píxel negro en una imágen auxiliar en la posición del centro de ese grupo de píxeles, que consideramos como el píxel con coordenadas (1,1) situando el origen en la esquina superior izquierda.
Puede comprobarse que con un umbral de 12 píxeles los bordes de la imagen no son tenidos en cuenta, y la punta de la mayoría de estructuras protuberantes que no excedan los 2 píxeles de grosor es considerada como la yema de un dedo. Bajar el umbral a 11 implica que algunas zonas del borde son consideradas dedos, y subirlo a 13 o más hace que muchos dedos no se detecten. De hecho, es altamente improbable que un dedo de verdad obtenga una puntuación superior a 14.
En las dos imágenes anteriores se puede ver la puntuación de varias zonas de la imagen. En verde están los píxeles que suman puntuación, y en rojo los que no (oscuro = el pixel de la imagen era negro, claro = el píxel de la imagen era blanco). En la imagen de la derecha se ve cómo las 3 yemas de los dedos de la imagen se reconocen perfectamente. En la imagen izquierda se ve como los bordes se ignoran y el por qué de no realizar una comparación si la zona de la imagen tenía menos de 3 píxeles en negro (cualquier trozo de fondo blanco tiene puntuación 12). También puede apreciarse cómo el algoritmo no es perfecto, y aparte de las yemas también detecta algunas zonas de la imagen correspondientes a zonas delgadas y largas de la imagen, que en la mayoría de ocasiones pertenecen a un dedo.
El resultado del algoritmo es por lo general una imagen auxiliar con varios grupos de unos pocos
(por lo general 1, 2 ó 3) píxeles negros muy juntos, situados en zonas cercanas a donde es muy probable que esté la yema de un dedo. El paso siguiente es hacer algo para tratar de quedarse con un punto de cada grupo de píxeles, en concreto aquel que esté más arriba y a la izquierda. Para ello pasamos un filtro por la imagen que, para cada píxel negro de la imagen auxiliar, busca si hay algún otro píxel a su alrededor tal como muestra este diagrama donde el píxel negro es el que nos interesa, y el resto de píxeles son aquellas posiciones donde se comprueba si hay otro píxel negro:
Si se encuentra otro píxel negro en esa zona, el píxel de interés se borra. En nuestra implementación final, el filtro se pasa a la vez que se va pasando el parche por la imágen, antes de marcar un píxel como posible nueva yema dactilar en la imagen auxiliar (en vez de primero sacar y guardar todos los posibles puntos y luego filtrar y borrar los necesarios, aplciamos el filtro a cada nuevo punto candidato y si no lo pasa, no lo guardamos). Dado que el parche y por consiguiente el filtro se pasan de izquierda a derecha y de arriba a abajo, conseguiremos que sólo sobreviva aquel píxel de cada grupo que esté más arriba a la izquierda. El área de exclusión del filtro se diseñó a partir de observaciones de los resultados del algoritmo sin filtrar, y se obtienen muy buenos resultados, dando lugar en la inmensa mayoría de los casos a imágenes auxiliares con tantos píxeles negros como dedos hubiera en la imágen original, situados en aquellas posiciones donde se encontraban sus yemas.
Y todo esto se realiza en menos de 1 milisegundo.
martes, 1 de mayo de 2012
Resumen de la situación actual: HITO 2
Nuestra red actual tiene por entrada una imagen de 24x24, es decir 566 entradas binarias. La capa intermedia tiene un total de 25 neuronas, y la salida consiste en los 12 gestos del set de usuario, mas una salida correspondiente al fondo.
Tras un entrenamiento de 200 iteraciones obtenemos una red que clasifica correctamente el 99.58% de las imágenes del set de entrenamiento, y el 86.3% de las imágenes del set de test.
Este es un muy buen porcentaje de acierto sabiendo que el sistema funciona a tiempo real, y que el usuario puede corregir la posición de su mano si, debido a una mala colocación, el sistema no es capaz de identificarlo correctamente. Si además le añadimos el medidor de confianza y un buffer de imágenes, nos aseguramos de que cuando emita una hipótesis ésta sea correcta la gran mayoría de las veces.
A continuación el vídeo mostrado al final del hito 2, en el cual están activados tanto el medidor de confianza como el buffer. Del medidor de confianza hemos hablado en anteriores entradas; el buffer por su parte retarda la muestra por pantalla del símbolo identificado un segundo, aunque la identificación de hipótesis de la red neuronal se hace a tiempo real.
Tras un entrenamiento de 200 iteraciones obtenemos una red que clasifica correctamente el 99.58% de las imágenes del set de entrenamiento, y el 86.3% de las imágenes del set de test.
Este es un muy buen porcentaje de acierto sabiendo que el sistema funciona a tiempo real, y que el usuario puede corregir la posición de su mano si, debido a una mala colocación, el sistema no es capaz de identificarlo correctamente. Si además le añadimos el medidor de confianza y un buffer de imágenes, nos aseguramos de que cuando emita una hipótesis ésta sea correcta la gran mayoría de las veces.
A continuación el vídeo mostrado al final del hito 2, en el cual están activados tanto el medidor de confianza como el buffer. Del medidor de confianza hemos hablado en anteriores entradas; el buffer por su parte retarda la muestra por pantalla del símbolo identificado un segundo, aunque la identificación de hipótesis de la red neuronal se hace a tiempo real.
jueves, 19 de abril de 2012
Medidor de confianza
Tal como teníamos implementada la red, ésta devolvía a cada momento el número de neurona con máxima salida; este número era el número de gesto identificado. Esto no tenía en cuenta de ninguna forma si el valor de salida máximo era muy superior al segundo valor más alto, por ejemplo, así como el valor absoluto de este valor máximo.
Pudimos comprobar experimentalmente que el valor medio de las salidas máximas de la red neuronal cuando el sistema acertaba era significativamente mayor que el valor medio de las salidas máximas cuando la hipótesis emitida era incorrecta. Por así decirlo, el valor de la salida máxima indica "cómo de seguro" está la red neuronal de que su hipótesis es correcta. La diferente entre el valor más alto de salida y el segundo valor también resulta un parámetro significativo.
Debido a que es más importante que el sistema acierte al emitir una hipótesis que que emita hipótesis constantemente, la mayoría acertadas y algunas falsas, hemos añadido un medidor de confianza como segunda etapa tras la red neuronal.
Esta etapa evalúa el nivel de "confianza" de la red neuronal según:
confidence =((max_salidas - siguiente_max)/max_salidas);
Y si este nivel es mayor que 0.7/1, entonces considera la predicción de la red neuronal como válida y se la transmite al usuario. Si es menor de 0.7 consideramos que la red neuronal no está lo suficientemente "segura" de su hipótesis y la descartamos. En este estado de desarrollo, gráficamente el usuario ve una ? como gesto reconocido en la pantalla del ordenador cuando no se ha podido identificar el gesto.
La red neuronal está constantemente sacando hipótesis. Esto quiere decir que, por ejemplo, en la transición de un gesto de la mano a otro buscará en cada instante qué gesto de la base de datos es el más similar; aunque la máxima salida de la red sea baja, reconocerá gestos mientras que el usuario no está efectuando ninguno. El añadir este nivel de comprobación asegurará que estas hipótesis sean bloqueadas en el medidor de confianza, que dará un resultado inferior al umbral de 0.7, de forma que el sistema no reaccione a estos gestos.
Pudimos comprobar experimentalmente que el valor medio de las salidas máximas de la red neuronal cuando el sistema acertaba era significativamente mayor que el valor medio de las salidas máximas cuando la hipótesis emitida era incorrecta. Por así decirlo, el valor de la salida máxima indica "cómo de seguro" está la red neuronal de que su hipótesis es correcta. La diferente entre el valor más alto de salida y el segundo valor también resulta un parámetro significativo.
Debido a que es más importante que el sistema acierte al emitir una hipótesis que que emita hipótesis constantemente, la mayoría acertadas y algunas falsas, hemos añadido un medidor de confianza como segunda etapa tras la red neuronal.
Esta etapa evalúa el nivel de "confianza" de la red neuronal según:
confidence =((max_salidas - siguiente_max)/max_salidas);
Y si este nivel es mayor que 0.7/1, entonces considera la predicción de la red neuronal como válida y se la transmite al usuario. Si es menor de 0.7 consideramos que la red neuronal no está lo suficientemente "segura" de su hipótesis y la descartamos. En este estado de desarrollo, gráficamente el usuario ve una ? como gesto reconocido en la pantalla del ordenador cuando no se ha podido identificar el gesto.
La red neuronal está constantemente sacando hipótesis. Esto quiere decir que, por ejemplo, en la transición de un gesto de la mano a otro buscará en cada instante qué gesto de la base de datos es el más similar; aunque la máxima salida de la red sea baja, reconocerá gestos mientras que el usuario no está efectuando ninguno. El añadir este nivel de comprobación asegurará que estas hipótesis sean bloqueadas en el medidor de confianza, que dará un resultado inferior al umbral de 0.7, de forma que el sistema no reaccione a estos gestos.
La importancia de elegir un buen set de entrenamiento
En la entrada inmediatamente anterior se menciona el proceso de entrenamiento de las redes neuronales, así como el modo en que capturamos imágenes de entrenamiento para nuestra red neuronal.
Pensando en conseguir que el sistema sea capaz de reconocer gestos de diferentes personas, con manos en posiciones levemente distintas e inclinaciones variadas aun conservando el gesto en si, realizamos una serie de capturas para la base de datos en las que movíamos la mano, variando significativamente el ángulo, la posición del brazo, la separación de los dedos... Después de dividir la base de datos en set de entrenamiento y set de test y de entrenar la red, terminamos por encontrarnos con unos resultados algo peores de lo esperado.
No fue inmediato darnos cuenta de que nuestro exceso de celo resultó más perjudicial que otra cosa para nuestro sistema. Algunos de los movimientos capturados, a pesar de conservar características propias de su gesto original, como el número de dedos extendidos, tenían una inclinación tan exagerada que pasaban a tener características de otras imágenes e inducían a error a la red neuronal. De esta forma la red neuronal se fijaba en características como el ángulo del brazo por encima de los dedos extendidos y se equivocaba clasificando las imágenes.
En las siguientes capturas para creación de base de datos limitamos la variación de los gestos.
En este punto del desarrollo nuestra red tenía doce neuronas de salida, una por gesto; de esta forma cuando la entrada era un fondo vacío, sin mano, lo detectaba como una mano de canto (gesto que menos pixels activos tiene). Le hemos añadido una neurona más de salida correspondiente al fondo, para lo cual precisamos también de un set de entrenamiento. Realizamos una función simple de matlab que genera una serie de imágenes de fondos con algunos píxeles de ruido en posición aleatoria (uno, dos o tres por imagen) y la utilizamos para entrenar la red.
Pensando en conseguir que el sistema sea capaz de reconocer gestos de diferentes personas, con manos en posiciones levemente distintas e inclinaciones variadas aun conservando el gesto en si, realizamos una serie de capturas para la base de datos en las que movíamos la mano, variando significativamente el ángulo, la posición del brazo, la separación de los dedos... Después de dividir la base de datos en set de entrenamiento y set de test y de entrenar la red, terminamos por encontrarnos con unos resultados algo peores de lo esperado.
No fue inmediato darnos cuenta de que nuestro exceso de celo resultó más perjudicial que otra cosa para nuestro sistema. Algunos de los movimientos capturados, a pesar de conservar características propias de su gesto original, como el número de dedos extendidos, tenían una inclinación tan exagerada que pasaban a tener características de otras imágenes e inducían a error a la red neuronal. De esta forma la red neuronal se fijaba en características como el ángulo del brazo por encima de los dedos extendidos y se equivocaba clasificando las imágenes.
En las siguientes capturas para creación de base de datos limitamos la variación de los gestos.
En este punto del desarrollo nuestra red tenía doce neuronas de salida, una por gesto; de esta forma cuando la entrada era un fondo vacío, sin mano, lo detectaba como una mano de canto (gesto que menos pixels activos tiene). Le hemos añadido una neurona más de salida correspondiente al fondo, para lo cual precisamos también de un set de entrenamiento. Realizamos una función simple de matlab que genera una serie de imágenes de fondos con algunos píxeles de ruido en posición aleatoria (uno, dos o tres por imagen) y la utilizamos para entrenar la red.
miércoles, 18 de abril de 2012
Redes neuronales: recolección de base de datos y entrenamiento
Como ya comentamos anteriormente, entrenar una red neuronal mediante backpropagation consiste en buscar los valores óptimos de en nuestro caso más de 14000 parámetros libres que definen el comportamiento de las neuronas de la red, para que una función asociada, conocida como función de coste, minimice su valor. Estos parámetros libres son constantes una vez fijados, y no son las variables de las funciones matemáticas implicadas. Por tanto estos parámetros habrá que buscarlos con algún criterio determinado y para un subconjunto finito de posibles valores de las variables.
Para poder entrenar la red necesitamos una base de datos de imágenes de gestos similares a las que va a utilizar el sistema final, clasificadas de forma que el algoritmo de entrenamiento conozca a qué gesto corresponde cada imagen. Para entrenar la red se comenzará con los parámetros libres ajustados completamente al azar (y con valores pequeños) y se irán pasando una a una todas las imágenes de la base de datos de entrenamiento, observando cómo clasifica la red cada imagen y si su clasificación es correcta o errónea. La función de coste será menor cuantos más aciertos tenga la red dentro del set de entrenamiento. Una vez calculada la función de coste se aplica el algoritmo de backpropagation que calcula el gradiente de algo que tiene que ver con el error cometido al clasificar las imágenes, y conocido este gradiente calcula un nuevo set de parámetros libres mediante métodos numéricos que traten de minimizar el error cometido y la función de coste. El proceso vuelve a repetirse con los nuevos parámetros y se continúa iterando hasta un número de veces fijado por el usuario. Cada vez el coste se va reduciendo y la red va aprendiendo a clasificar mejor los gestos de su set de entrenamiento. Esto puede dar lugar a un fenómeno conocido como "overfitting" en que la red queda entrenada de tal forma que reconoce a la perfección la mayoría de las imágenes de su base de datos de entrenamiento, pero no funciona bien ante nuevas imágenes que nunca había visto antes. Por ello la función de coste también penaliza en cierta medida que los parámetros tomen valores propensos a provocar overfitting, y se debe valuar el comportamiento de la red con nuevos gestos desconocidos para determinar el número óptimo de ciclos de entrenamiento y otros parámetros hasta que el sistema funcione de forma aceptable, mostrando un buen comportamiento tanto con los gestos de entrenamiento como con nuevos gestos.
Para entrenar nuestra red nos hemos propuesto obtener una base de datos de unas 10000-15000 imágenes de unas 10 manos distintas. Como estas imágenes deben ser idénticas a las que el sistema va a recibir una vez sea autónomo, es el PIC32 el encargado de capturar las imágenes y enviarselas al ordenador donde se almacenarán. Para simplificar la tarea de la captura de imágenes hemos realizado un pequeño sketch en lenguaje Processing (un lenguaje que utiliza java como base y está optimizado para trabajar con imágenes). Este sketch toma los datos de la imágen que el PIC32 envía por puerto serie y los transforma en una imágen png, que guarda en un directorio determinado según el gesto al que corresponda, lo cual indica el usuario mediante un menú de control.
[se añadiran imagenes ilustrativas]
El asistente de cámara elige en el menú el gesto que va a grabar e indica al usuario que haga el gesto. Una vez la mano se vea estable en la pantalla, el asistente de cámara pulsará el botón de grabación, momento en el cual se empiezan a guardar las imágenes enviadas por el PIC. Durante el tiempo que dure la captura, que va a unos 15-20 fotogramas por segundo limitada por la velocidad de acceso al disco a la hora de guardar imágenes, el usuario deberá mover ligeramente la mano sin dejar de hacer el gesto, para obtener muchas imágenes distintas.
En sesiones de unos 5 minutos somos capaces de obtener algo más de 1000 imágenes para la base de datos de entrenamiento. Actualmente disponemos de unas 14000 imágenes, de las cuales solo 6000 son válidas, pues tuvimos que cambiar el alfabeto de gestos y descartar las imágenes capturadas con el alfabeto anterior.
Una vez capturadas las imágenes, un script de Matlab recorre la estructura de directorios y extrae todas las imágenes que encuentra, convirtiéndolas a una gran matriz de 576 columnas (una por pixel) y tantas filas como imágenes haya, y otra matriz de una columna y el mismo número de filas que contiene, para cada imagen, el número de gesto al que corresponde.
Cabe destacar un problema que tuvimos debido a que cuando Matlab pasa una matriz a un vector lo hace columna a columna, es decir, dada una matriz
Para poder entrenar la red necesitamos una base de datos de imágenes de gestos similares a las que va a utilizar el sistema final, clasificadas de forma que el algoritmo de entrenamiento conozca a qué gesto corresponde cada imagen. Para entrenar la red se comenzará con los parámetros libres ajustados completamente al azar (y con valores pequeños) y se irán pasando una a una todas las imágenes de la base de datos de entrenamiento, observando cómo clasifica la red cada imagen y si su clasificación es correcta o errónea. La función de coste será menor cuantos más aciertos tenga la red dentro del set de entrenamiento. Una vez calculada la función de coste se aplica el algoritmo de backpropagation que calcula el gradiente de algo que tiene que ver con el error cometido al clasificar las imágenes, y conocido este gradiente calcula un nuevo set de parámetros libres mediante métodos numéricos que traten de minimizar el error cometido y la función de coste. El proceso vuelve a repetirse con los nuevos parámetros y se continúa iterando hasta un número de veces fijado por el usuario. Cada vez el coste se va reduciendo y la red va aprendiendo a clasificar mejor los gestos de su set de entrenamiento. Esto puede dar lugar a un fenómeno conocido como "overfitting" en que la red queda entrenada de tal forma que reconoce a la perfección la mayoría de las imágenes de su base de datos de entrenamiento, pero no funciona bien ante nuevas imágenes que nunca había visto antes. Por ello la función de coste también penaliza en cierta medida que los parámetros tomen valores propensos a provocar overfitting, y se debe valuar el comportamiento de la red con nuevos gestos desconocidos para determinar el número óptimo de ciclos de entrenamiento y otros parámetros hasta que el sistema funcione de forma aceptable, mostrando un buen comportamiento tanto con los gestos de entrenamiento como con nuevos gestos.
Para entrenar nuestra red nos hemos propuesto obtener una base de datos de unas 10000-15000 imágenes de unas 10 manos distintas. Como estas imágenes deben ser idénticas a las que el sistema va a recibir una vez sea autónomo, es el PIC32 el encargado de capturar las imágenes y enviarselas al ordenador donde se almacenarán. Para simplificar la tarea de la captura de imágenes hemos realizado un pequeño sketch en lenguaje Processing (un lenguaje que utiliza java como base y está optimizado para trabajar con imágenes). Este sketch toma los datos de la imágen que el PIC32 envía por puerto serie y los transforma en una imágen png, que guarda en un directorio determinado según el gesto al que corresponda, lo cual indica el usuario mediante un menú de control.
[se añadiran imagenes ilustrativas]
El asistente de cámara elige en el menú el gesto que va a grabar e indica al usuario que haga el gesto. Una vez la mano se vea estable en la pantalla, el asistente de cámara pulsará el botón de grabación, momento en el cual se empiezan a guardar las imágenes enviadas por el PIC. Durante el tiempo que dure la captura, que va a unos 15-20 fotogramas por segundo limitada por la velocidad de acceso al disco a la hora de guardar imágenes, el usuario deberá mover ligeramente la mano sin dejar de hacer el gesto, para obtener muchas imágenes distintas.
En sesiones de unos 5 minutos somos capaces de obtener algo más de 1000 imágenes para la base de datos de entrenamiento. Actualmente disponemos de unas 14000 imágenes, de las cuales solo 6000 son válidas, pues tuvimos que cambiar el alfabeto de gestos y descartar las imágenes capturadas con el alfabeto anterior.
Una vez capturadas las imágenes, un script de Matlab recorre la estructura de directorios y extrae todas las imágenes que encuentra, convirtiéndolas a una gran matriz de 576 columnas (una por pixel) y tantas filas como imágenes haya, y otra matriz de una columna y el mismo número de filas que contiene, para cada imagen, el número de gesto al que corresponde.
Cabe destacar un problema que tuvimos debido a que cuando Matlab pasa una matriz a un vector lo hace columna a columna, es decir, dada una matriz
A=[1 2 3
4 5 6]
4 5 6]
El comando A(:) de matlab lo transformaría en [1 4 2 5 3 6]. En el PIC32 las imágenes se capturan fila a fila y se guardan como un array unidimensional fila a fila ([1 2 3 4 5 6]). Por tanto nuestra primera prueba de la red neuronal en el PIC32 resultó un fracaso, ya que al no darnos cuenta del detalle anterior la red neuronal se había entrenado en realidad para reconocer las imágenes que captura el PIC rotadas 90 grados (traspuestas). El problema se arregla simplemente trasponiendo la matriz que representa cada imágen antes de transformarla en un vector en Matlab.
Procesamiento de imagen II: resumen y añadidos
Desde que comenzamos a escribir el blog hemos ido avanzando poco a poco en las tareas de procesamiento de vídeo. Esta entrada sirve de resumen de lo ya visto, pero además añadimos una modificación a nivel del escalado y un par de añadidos posteriores a la etapa de procesamiento.
Este post será actualizado más adelante con imágenes ilustrativas del proceso.
Actualmente, a cada fotograma capturado de 48x48 píxeles se le aplican los siguientes procesos (recordamos que debido a problemas con el tiempo que debía haber entre dos capturas sucesivas, no pudimos hacer la captura a 96x96 como planeábamos inicialmente):
UNO -Filtro de mediana con radio 1 (una explicación más detallada puede consultarse en esta entrada).
DOS -Escalado a la mitad.
Utilizamos un algoritmo equivalente al box filter que ya se explicó, pero esta vez dividiendo por 2 en vez de por 4. La implementación del nuevo escalado está algo menos optimizada; se trata de dos bucles for anidados que recorren la imagen, tomando una columna de cada dos y una fila de cada dos, y para cada pixel considerado llama a una función getUnos, también empleada en el autocalibrado. Dicha función simplemente cuenta el número de pixels a 1 en una imagen, en un parche cuyas coordenadas iniciales y coordenadas finales se pasan por parámetro. Se cuenta por tanto el número de unos en un parche de 2x2 desde el pixel considerado; si hay dos o mas se sustituye el parche por un 1, y si hay menos se sustituye por un 0. La imagen obtenida es por tanto de 24x24.
TRES- Centrado de la imagen en la coordenada vertical.
Para que las imágenes con las que debe trabajar el sistema se parezcan más entre sí y la red tenga más sencillo hacer su trabajo, hemos decidido aplicar un algoritmo de centrado en la coordenada vertical a cada fotograma. El proceso consiste en calcular la coordenada vertical del centro de masas de la imagen, y ver cuánto se desvía del centro real de la imagen. Después se aplica un desplazamiento al fotograma para que ambos centros coincidan. De esta forma nos independizamos de parte de los movimientos del brazo, quedando la imagen siempre centrada verticalmente; pudimos observar que los movimientos verticales, subiendo y bajando la mano, son muy comunes e involuntarios según se van realizando los gestos. Así, mientras la mano quepa verticalmente en el campo de visión de la cámara, la imagen que le llegará a la red neuronal será muy parecida independientemente de la posición vertical de la mano.
CUATRO- Alinear a la izquierda (alignLeft).
Nuestro alfabeto de gestos implica la extensión de uno o más dedos de la mano para la mayoría de los gestos, dando lugar a imágenes "largas" que tienden a ocupar todo el espacio horizontal del campo visual de la cámara. Pero aunque el usuario tiene menos libertad de movimiento en la coordenada horizontal, la variación sí puede llegar a ser significativa, con lo que implementamos un equivalente a un centrado horizontal.
No es posible utilizar el mismo método que para el centrado vertical, basado en el cálculo del centro de masas, dado que no sería efectivo por ocupar el brazo del usuario todo un lado de la pantalla. Hemos recurrido por tanto a otra alternativa:
Dada una imagen, la recorremos desde la izquierda para encontrar la primera columna en la que tengamos imagen de mano (1s). En ese momento, comenzamos a desplazar la imagen columna a columna, de tal forma que la primera columna con 1s quede a la izquierda del todo, la segunda justo al lado, etc. Donde antes estaba la imagen que se ha desplazado, rellenamos con todo 0s.
Obtenemos de esa forma una mano siempre a la izquierda del fotograma, pero con brazo inacabado.
Hacemos notar que el bucle de recorrido de columnas comienza en la columna 1 en lugar de en la cero. Esto es porque, si la mano ocupaba toda la imagen horizontalmente, al buscar el offset a aplicar resultaba ser 0 (en la primera columna ya hay imagen). En este momento copiaba la columna en si misma y después la borraba, con lo que obteníamos imágenes consistentes en todo ceros.
En lugar de hacer uso de estructuras if, simplemente comenzamos a verificar si las columnas contienen imagen a partir de la columna 1. En tal caso podría perderse la primera columna al ocupar una imagen toda la imagen horizontalmente, pero la diferencia no es significativa.
CINCO- Extensión del brazo (armGenerator)
Volvemos a recorrer la imagen, pero ahora desde la derecha en busca de la primera columna que tenga tres unos o más (el grosor del brazo nunca será de menos de 2 pixeles, con lo que si encontramos sólo dos unos o menos en una columna se tratará de ruido). En ese momento la identificamos como brazo, copiamos la columna y realizamos copias de ella en cada columna hacia la derecha hasta llegar al final de la imagen. De esta forma obtenemos un brazo completo que llega hasta el borde del fotograma.
Esto tiene la ventaja adicional de que hasta ahora era necesario arremangarse para utilizar el sistema. Desde este momento, bastará con despejar la muñeca para que el sistema mismo haga la extensión de brazo.
Código de alingLeft y armGenerator.
Este post será actualizado más adelante con imágenes ilustrativas del proceso.
Actualmente, a cada fotograma capturado de 48x48 píxeles se le aplican los siguientes procesos (recordamos que debido a problemas con el tiempo que debía haber entre dos capturas sucesivas, no pudimos hacer la captura a 96x96 como planeábamos inicialmente):
UNO -Filtro de mediana con radio 1 (una explicación más detallada puede consultarse en esta entrada).
DOS -Escalado a la mitad.
Utilizamos un algoritmo equivalente al box filter que ya se explicó, pero esta vez dividiendo por 2 en vez de por 4. La implementación del nuevo escalado está algo menos optimizada; se trata de dos bucles for anidados que recorren la imagen, tomando una columna de cada dos y una fila de cada dos, y para cada pixel considerado llama a una función getUnos, también empleada en el autocalibrado. Dicha función simplemente cuenta el número de pixels a 1 en una imagen, en un parche cuyas coordenadas iniciales y coordenadas finales se pasan por parámetro. Se cuenta por tanto el número de unos en un parche de 2x2 desde el pixel considerado; si hay dos o mas se sustituye el parche por un 1, y si hay menos se sustituye por un 0. La imagen obtenida es por tanto de 24x24.
TRES- Centrado de la imagen en la coordenada vertical.
Para que las imágenes con las que debe trabajar el sistema se parezcan más entre sí y la red tenga más sencillo hacer su trabajo, hemos decidido aplicar un algoritmo de centrado en la coordenada vertical a cada fotograma. El proceso consiste en calcular la coordenada vertical del centro de masas de la imagen, y ver cuánto se desvía del centro real de la imagen. Después se aplica un desplazamiento al fotograma para que ambos centros coincidan. De esta forma nos independizamos de parte de los movimientos del brazo, quedando la imagen siempre centrada verticalmente; pudimos observar que los movimientos verticales, subiendo y bajando la mano, son muy comunes e involuntarios según se van realizando los gestos. Así, mientras la mano quepa verticalmente en el campo de visión de la cámara, la imagen que le llegará a la red neuronal será muy parecida independientemente de la posición vertical de la mano.
CUATRO- Alinear a la izquierda (alignLeft).
Nuestro alfabeto de gestos implica la extensión de uno o más dedos de la mano para la mayoría de los gestos, dando lugar a imágenes "largas" que tienden a ocupar todo el espacio horizontal del campo visual de la cámara. Pero aunque el usuario tiene menos libertad de movimiento en la coordenada horizontal, la variación sí puede llegar a ser significativa, con lo que implementamos un equivalente a un centrado horizontal.
No es posible utilizar el mismo método que para el centrado vertical, basado en el cálculo del centro de masas, dado que no sería efectivo por ocupar el brazo del usuario todo un lado de la pantalla. Hemos recurrido por tanto a otra alternativa:
Dada una imagen, la recorremos desde la izquierda para encontrar la primera columna en la que tengamos imagen de mano (1s). En ese momento, comenzamos a desplazar la imagen columna a columna, de tal forma que la primera columna con 1s quede a la izquierda del todo, la segunda justo al lado, etc. Donde antes estaba la imagen que se ha desplazado, rellenamos con todo 0s.
Obtenemos de esa forma una mano siempre a la izquierda del fotograma, pero con brazo inacabado.
Hacemos notar que el bucle de recorrido de columnas comienza en la columna 1 en lugar de en la cero. Esto es porque, si la mano ocupaba toda la imagen horizontalmente, al buscar el offset a aplicar resultaba ser 0 (en la primera columna ya hay imagen). En este momento copiaba la columna en si misma y después la borraba, con lo que obteníamos imágenes consistentes en todo ceros.
En lugar de hacer uso de estructuras if, simplemente comenzamos a verificar si las columnas contienen imagen a partir de la columna 1. En tal caso podría perderse la primera columna al ocupar una imagen toda la imagen horizontalmente, pero la diferencia no es significativa.
CINCO- Extensión del brazo (armGenerator)
Volvemos a recorrer la imagen, pero ahora desde la derecha en busca de la primera columna que tenga tres unos o más (el grosor del brazo nunca será de menos de 2 pixeles, con lo que si encontramos sólo dos unos o menos en una columna se tratará de ruido). En ese momento la identificamos como brazo, copiamos la columna y realizamos copias de ella en cada columna hacia la derecha hasta llegar al final de la imagen. De esta forma obtenemos un brazo completo que llega hasta el borde del fotograma.
Esto tiene la ventaja adicional de que hasta ahora era necesario arremangarse para utilizar el sistema. Desde este momento, bastará con despejar la muñeca para que el sistema mismo haga la extensión de brazo.
Código de alingLeft y armGenerator.
martes, 3 de abril de 2012
Red neuronal en PIC32
Se ha comentado que parte del interés de esta práctica reside en trabajar con redes neuronales. Una vez determinado el alfabeto de gestos y entrenada la red neuronal en Matlab, procedemos a implementarla en C.
En una red neuronal, la salida de cada neurona es una función de la suma de todas sus entradas, ponderadas por unos parámetros θ (cuyos valores hemos obtenido entrenando la red neuronal en Matlab). Para esta función, llamada función de activación, suele utilizarse una sigmoide; con el fin de implementarla en el PIC32 nos informamos sobre diversas formas de realizar aproximaciones de la función sigmoide, siendo la más sencilla por tramos de rectas.
Pero a la hora de llevarlo a la práctica determinamos finalmente hacer uso de la función exp de la librería matemática en punto flotante para PIC32 incluida en el compilador de MPLAB. La función exp requiere 133 ciclos, y para calcular la sigmoide se necesita además una división. Dado que nuestra capa oculta es de 25 neuronas y, por tener un set de 12 gestos posibles, la capa de salida consta de 12 neuronas, se realizaran un total de 37 operaciones de sigmoide por imagen.
Además de esto, para obtener la salida de cada neurona sería necesario multiplicar el valor de cada una de las entradas por un theta y sumarlas, lo cual supone un gran número de operaciones de multiplicación, costosas en tiempo. Pero debido a que trabajamos con sólo dos niveles de color, la imagen de entrada está compuesta por píxeles con valor únicamente 1 ó 0. Esto implica que, para cada neurona de la capa oculta, ponderar cada entrada por los valores theta equivale simplemente a sumar el valor de theta si la entrada vale 1, o no sumarlo si vale 0. En lugar de las 577 (incluyendo neurona de bias) * 25 operaciones de multiplicación tenemos el mismo número de estructuras if y sumas, lo cual es un ahorro muy importante de ciclos de trabajo.
Para obtener la salida de la red neuronal sí será necesario tomar el valor de cada neurona de la capa oculta (mas la de bias), ponderarla por un theta y aplicarle la sigmoide a la suma de todas ellas; pero el número de operaciones de multiplicación en este caso (12(número de neuronas de salida)*26(salidas de la capa oculta)) es mucho menor que las que tendríamos que haber efectuado en la capa anterior de no ser por la enorme simplificación que supone trabajar a dos niveles de color.
Toda la red neuronal puede implementarse de esta forma mediante dos sencillos bucles for. El método en el que está implementada devuelve además el número de neurona cuya salida es máxima, que se corresponde con el número del gesto identificado. De esta forma hemos conseguido trabajar a tiempo real, a pesar de no haber realizado una aproximación menos costosa en tiempo para la sigmoide, identificando el gesto en cada fotograma capturado.
Esta función podría ser modificada en el futuro si queremos tener en cuenta otros datos en la salida de la red neuronal, como la varianza de las salidas, la diferencia en la estimación de las dos salidas más altas, etc.
Métodos de sigmoid y predict (red neuronal).
Implementación de la red neuronal en Matlab
Para el reconocimiento de gestos hemos decidido utilizar un algoritmo basado en red neuronal. Nuestra red neuronal será un clasificador de imágenes con una estructura que intenta simular matemáticamente el comportamiento de un grupo de neuronas a las que entrenaremos para que sean capaces de clasificar las imágenes fijándose en ciertas características determinadas por el entrenamiento.
El entrenamiento de la red neuronal puede hacerse de diversas formas (algoritmos genéticos, backpropagation, manualmente...). Nosotros hemos elegido utilizar la técnica de backpropagation utilizando una gran base de datos de imágenes de gestos clasificados capturados por nosotros mismos y tratando de minimizar una función de coste que depende entre otras cosas del número de aciertos y de fallos que tiene la red al clasificar los gestos de la base de datos de entrenamiento.
La red neuronal depende de un juego de parámetros libres que serán los que ajustará el algoritmo de entrenamiento. En nuestro caso, cada neurona de la capa intermedia u oculta recibe una entrada de cada una de las neuronas de entrada, y asigna un peso distinto entre 0 y 1 a cada una de estas entradas. La suma de todas las entradas ponderadas por su correspondiente peso se pasa como entrada a una función de activación de tipo sigmoide (en nuestro caso con el sesgo siempre a 0), y el valor que tome esta función será la salida de la neurona. El funcionamiento es análogo entre las capa oculta y la capa de salida. Además, a la capa de entrada y a la capa oculta se les añade una neurona extra que no tiene entradas y siempre vale 1 y que se conoce como "bias unit", y que se incluye para evitar algunos problemas en caso de que ninguna de las neuronas de verdad se active.
Por tanto, con 576 píxeles por imagen (24x24) y sumando la bias unit, habrá 577 neuronas en la capa de entrada. Para la capa intermedia hemos fijado un total de 25 neuronas más la bias unit, total 26. Y la capa de salida tendrá tantas neuronas como gestos, en nuestro caso 12. Por tanto los parámetros libres de la red suman un total de 577x25 + 26*12 = 14737 parámetros. Entrenar la red con backpropagation significa esencialmente buscar los 14737 valores que hacen mínima una función de esos 14737 parámetros, lo cual resulta una tarea muy pesada que realizamos con Matlab.

Una vez entrenada la red tendremos un juego de parámetros entrenados, que llamaremos Theta1 (una matriz de 25 x 577) y Theta2 (12x26). La imagen se pasa a la red como un vector de 576 componentes (más la bias unit) correspondiendo cada componente del vector al valor de un pixel de la imagen. Realizando las operaciones que aparecen en la imágen (g(x) es la función sigmoide 1/(1+exp(-x)) ) obtendremos una hipótesis (un valor real entre 0 y 1) para cada neurona de salida. Las neuronas de salida van numeradas del 1 al 12 igual que nuestros gestos, y el número del gesto reconocido se corresponderá con el número de la neurona de salida cuya hipótesis sea máxima.
La implementación del sistema de entrenamiento está basado en un ejercicio propuesto para un curso online impartido por la universidad de Stanford sobre aprendizaje de máquinas. En este ejercicio se proponía implementar de forma más o menos guiada todo el algoritmo de backpropagation, programando entre otras cosas las funciones que determinan el gradiente de la función de coste y la función que clasifica una imagen una vez esté entrenada la red. El propósito de esta red era el reconocimiento de imágenes de números manuscritos.
Las modificaciones realizadas incluyen la eliminación de toda la parte del código del ejercicio que no tenía que ver con la red neuronal y la programación de todo el sistema que procesa y transforma las imágenes capturadas de la base de datos al formato adecuado (esencialmente idéntico al formato en que se capturan las imágenes en el PIC32) para trabajar con ellas.
También hemos creado un sistema de evaluación del comportamiento de la red que muestra el porcentaje de aciertos para cada uno de los gestos, y te permite visualizar cómo se ha reconocido cada gesto, para ver con cuales tiene más dificultades y poder tomar las decisiones adecuadas para mejorar el comportamiento.
Para pasarle los parámetros entrenados al PIC32 utilizamos un fichero de datos que creamos automáticamente con matlab gracias a un script que genera el fichero con el formato adecuado, listo para copiar, pegar y compilar.
El entrenamiento de la red neuronal puede hacerse de diversas formas (algoritmos genéticos, backpropagation, manualmente...). Nosotros hemos elegido utilizar la técnica de backpropagation utilizando una gran base de datos de imágenes de gestos clasificados capturados por nosotros mismos y tratando de minimizar una función de coste que depende entre otras cosas del número de aciertos y de fallos que tiene la red al clasificar los gestos de la base de datos de entrenamiento.
La red neuronal depende de un juego de parámetros libres que serán los que ajustará el algoritmo de entrenamiento. En nuestro caso, cada neurona de la capa intermedia u oculta recibe una entrada de cada una de las neuronas de entrada, y asigna un peso distinto entre 0 y 1 a cada una de estas entradas. La suma de todas las entradas ponderadas por su correspondiente peso se pasa como entrada a una función de activación de tipo sigmoide (en nuestro caso con el sesgo siempre a 0), y el valor que tome esta función será la salida de la neurona. El funcionamiento es análogo entre las capa oculta y la capa de salida. Además, a la capa de entrada y a la capa oculta se les añade una neurona extra que no tiene entradas y siempre vale 1 y que se conoce como "bias unit", y que se incluye para evitar algunos problemas en caso de que ninguna de las neuronas de verdad se active.
Por tanto, con 576 píxeles por imagen (24x24) y sumando la bias unit, habrá 577 neuronas en la capa de entrada. Para la capa intermedia hemos fijado un total de 25 neuronas más la bias unit, total 26. Y la capa de salida tendrá tantas neuronas como gestos, en nuestro caso 12. Por tanto los parámetros libres de la red suman un total de 577x25 + 26*12 = 14737 parámetros. Entrenar la red con backpropagation significa esencialmente buscar los 14737 valores que hacen mínima una función de esos 14737 parámetros, lo cual resulta una tarea muy pesada que realizamos con Matlab.

Una vez entrenada la red tendremos un juego de parámetros entrenados, que llamaremos Theta1 (una matriz de 25 x 577) y Theta2 (12x26). La imagen se pasa a la red como un vector de 576 componentes (más la bias unit) correspondiendo cada componente del vector al valor de un pixel de la imagen. Realizando las operaciones que aparecen en la imágen (g(x) es la función sigmoide 1/(1+exp(-x)) ) obtendremos una hipótesis (un valor real entre 0 y 1) para cada neurona de salida. Las neuronas de salida van numeradas del 1 al 12 igual que nuestros gestos, y el número del gesto reconocido se corresponderá con el número de la neurona de salida cuya hipótesis sea máxima.
La implementación del sistema de entrenamiento está basado en un ejercicio propuesto para un curso online impartido por la universidad de Stanford sobre aprendizaje de máquinas. En este ejercicio se proponía implementar de forma más o menos guiada todo el algoritmo de backpropagation, programando entre otras cosas las funciones que determinan el gradiente de la función de coste y la función que clasifica una imagen una vez esté entrenada la red. El propósito de esta red era el reconocimiento de imágenes de números manuscritos.
Las modificaciones realizadas incluyen la eliminación de toda la parte del código del ejercicio que no tenía que ver con la red neuronal y la programación de todo el sistema que procesa y transforma las imágenes capturadas de la base de datos al formato adecuado (esencialmente idéntico al formato en que se capturan las imágenes en el PIC32) para trabajar con ellas.
También hemos creado un sistema de evaluación del comportamiento de la red que muestra el porcentaje de aciertos para cada uno de los gestos, y te permite visualizar cómo se ha reconocido cada gesto, para ver con cuales tiene más dificultades y poder tomar las decisiones adecuadas para mejorar el comportamiento.
Para pasarle los parámetros entrenados al PIC32 utilizamos un fichero de datos que creamos automáticamente con matlab gracias a un script que genera el fichero con el formato adecuado, listo para copiar, pegar y compilar.
Determinando el alfabeto de gestos
Es una parte importante de nuestra práctica el determinar el alfabeto de gestos a los que podrá recurrir el usuario para interactuar con el sistema. Inicialmente el objetivo de nuestra práctica era poder escribir un texto por pantalla a base de hacer gestos con la mano, para lo cual serían necesarios 27 gestos diferentes (uno por cada letra del alfabeto), mas opcionalmente otros 10 para los dígitos del 0 al 9.
Por ser demasiados gestos diferentes, pronto comenzamos a considerar la posibilidad de reducir el alfabeto para hacerlo más abordable para el usuario; una primera opción fue limitarlo a sonidos y no a letras en sí: por ejemplo, que no haya h, hacer equivalentes la qu y la k, etc. Pero más adelante reconsideramos la orientación de nuestra práctica; en lugar de escribir textos mediante gestos, un proyecto complejo y vistoso pero poco útil, decidimos orientarnos a control domótico.
Así, los gestos que sean identificados por el sistema servirán para encender y apagar luces, variar luminosidad, encender una televisión, o similares según el material disponible del laboratorio. Esto requiere un set de gestos mucho más reducido, lo que tiene dos ventajas: es más fácil para el usuario aprenderlos y más sencillo para el sistema diferenciarlos si están bien escogidos.
Debido a la forma en la que capturamos la entrada de vídeo ("Captura de vídeo"), la imagen sufre una compresión horizontal (la imagen tiene una relación de aspecto 4/3 y la capturamos cuadrada), lo que hace que los gestos sean más difíciles de distinguir los unos de los otros si colocamos la mano en vertical frente a la cámara. Teniendo esto en cuenta hemos definido los gestos en horizontal.
Determinamos una serie de gestos básicos simples que pudieran sernos útiles para el control del sistema (números del 1 al 5 con la mano, palma cerrada, signo de OK...), y reunimos a conocidos y familiares para probar con ellos lo intuitivos que les resultaban, así como para obtener una base de datos de gestos de diferentes personas. La dividimos en set de entrenamiento y set de test y la utilizamos para entrenar la red neuronal; comprobamos en MATLAB los porcentajes de acierto en el set de test para cada uno de los gestos después de tan sólo 50 ciclos de entrenamiento.
Estas pruebas nos permitieron modificar o sustituir algunos de los 12 gestos que teníamos pensados inicialmente. Un ejemplo es el del número 3: Inesperadamente encontramos gente incapaz de hacer este gesto correctamente y sin forzar la mano con los dedos índice, corazón y anular. Además, la red neuronal tenía una tendencia elevada a confundirlo con el 2, el 4, u otros gestos. En este caso hemos resuelto el problema definiendo que el gesto para el 3 se haga con lo dedos pulgar, índice y corazón.
Los gestos actualmente definidos se recogen a continuación:
Por ser demasiados gestos diferentes, pronto comenzamos a considerar la posibilidad de reducir el alfabeto para hacerlo más abordable para el usuario; una primera opción fue limitarlo a sonidos y no a letras en sí: por ejemplo, que no haya h, hacer equivalentes la qu y la k, etc. Pero más adelante reconsideramos la orientación de nuestra práctica; en lugar de escribir textos mediante gestos, un proyecto complejo y vistoso pero poco útil, decidimos orientarnos a control domótico.
Así, los gestos que sean identificados por el sistema servirán para encender y apagar luces, variar luminosidad, encender una televisión, o similares según el material disponible del laboratorio. Esto requiere un set de gestos mucho más reducido, lo que tiene dos ventajas: es más fácil para el usuario aprenderlos y más sencillo para el sistema diferenciarlos si están bien escogidos.
Debido a la forma en la que capturamos la entrada de vídeo ("Captura de vídeo"), la imagen sufre una compresión horizontal (la imagen tiene una relación de aspecto 4/3 y la capturamos cuadrada), lo que hace que los gestos sean más difíciles de distinguir los unos de los otros si colocamos la mano en vertical frente a la cámara. Teniendo esto en cuenta hemos definido los gestos en horizontal.
Determinamos una serie de gestos básicos simples que pudieran sernos útiles para el control del sistema (números del 1 al 5 con la mano, palma cerrada, signo de OK...), y reunimos a conocidos y familiares para probar con ellos lo intuitivos que les resultaban, así como para obtener una base de datos de gestos de diferentes personas. La dividimos en set de entrenamiento y set de test y la utilizamos para entrenar la red neuronal; comprobamos en MATLAB los porcentajes de acierto en el set de test para cada uno de los gestos después de tan sólo 50 ciclos de entrenamiento.
Ejemplo de porcentajes de acierto en el set de test obtenidos para cada gesto, después de 50 ciclos de entrenamiento.
En la imagen anterior puede observarse cómo algunos gestos son reconocidos prácticamente siempre, mientras que otros tienden a ser confundidos sistemáticamente con otros. En este ejemplo vemos cómo ha confundido un puño cerrado (gesto tipo 1) con un dos (gesto 3). Esto puede deberse también a una base de datos o a un entrenamiento insuficiente.
Estas pruebas nos permitieron modificar o sustituir algunos de los 12 gestos que teníamos pensados inicialmente. Un ejemplo es el del número 3: Inesperadamente encontramos gente incapaz de hacer este gesto correctamente y sin forzar la mano con los dedos índice, corazón y anular. Además, la red neuronal tenía una tendencia elevada a confundirlo con el 2, el 4, u otros gestos. En este caso hemos resuelto el problema definiendo que el gesto para el 3 se haga con lo dedos pulgar, índice y corazón.
Set actual de gestos del usuario
Autocalibrado
Un comparador interno del PIC32 es el encargado de determinar si un píxel será blanco o negro en la imagen capturada. Para fijar el umbral de dicho comparador, utilizamos un conversor digital/analógico externo de 8 bit controlado por el bus I2C, modelo MCP4706. La elección del DAC vino únicamente determinada por su bajo coste, ya que para la aplicación no necesitamos ninguna característica especial. Para fijar la tensión de referencia positiva del DAC utilizamos un potenciómetro ajustable manualmente. De esta forma, podremos ajustar la tensión de referencia al nivel de tensión máximo de nuestra señal de vídeo, disponiendo de un umbral ajustable con 256 niveles distribuidos uniformemente en casi todo el rango dinámico de la parte visible de la señal de vídeo.
Con la tensión de referencia convenientemente ajustada el PIC podrá variar el umbral de su propio comparador en 256 niveles.
A la hora de capturar vídeo imponemos que haya un contraste entre el color de la piel de la mano y el color del fondo, y que el fondo sea lo más uniforme posible, o que al menos todo el fondo mantenga un nivel aceptable de contraste, sin que haya claros en caso de fondo oscuro o sombras oscuras en caso de fondo claro.
Para capturar correctamente la mano primero hay que calibrar el umbral del comparador, para que solo la mano aparezca como píxeles activos dentro de la imagen y el fondo no sea tomado en consideración. El filtro de mediana relaja la tolerancia del umbral, al eliminar eliminar píxeles sueltos o pequeños grupos de píxeles que puedan tomar un color similar al de la mano de forma aleatoria dentro de la imagen, o a causa de pequeñas variaciones en la iluminación. No obstante sigue siendo crucial elegir adecuadamente el umbral, y para ello hemos creado un sistema de autocalibrado.
Para realizar el autocalibrado, se comienza colocando el umbral en su nivel más bajo, se captura un forograma en el que únicamente aparezca el fondo de la imagen y se cuenta el número de píxeles activos (a 1) que contenga. Si este número es superior a un umbral, que hemos fijado en 25, se incrementa el umbral y se repite la operación hasta que finalmente prácticamente todos los píxeles del fondo se consideren como inactivos, momento en que el umbral se deja fijo. También cabe la posibilidad de incrementar un poco el umbral más allá del valor calculado, para que el sistema sea más robusto frente a las pequeñas variaciones de iluminación. De momento no nos ha hecho falta en las condiciones en que hemos probado el sistema.
La orden de calibrado se manda a través de dos botones incluidos en el hardware. Uno de ellos es para realizar la calibración con fondo oscuro y mano clara, y otro para mano oscura y fondo claro. El sistema de calibrado funciona de forma que en ambos casos las imagenes capturadas son totalmente equivalentes, ya que en caso de mano oscura y fondo claro invertimos la salida del comparador y fijamos el umbral en sentido contrario, comenzando por el nivel de tensión más alto y disminuyendolo hasta que el fondo quede eliminado.
Con la tensión de referencia convenientemente ajustada el PIC podrá variar el umbral de su propio comparador en 256 niveles.
A la hora de capturar vídeo imponemos que haya un contraste entre el color de la piel de la mano y el color del fondo, y que el fondo sea lo más uniforme posible, o que al menos todo el fondo mantenga un nivel aceptable de contraste, sin que haya claros en caso de fondo oscuro o sombras oscuras en caso de fondo claro.
Para capturar correctamente la mano primero hay que calibrar el umbral del comparador, para que solo la mano aparezca como píxeles activos dentro de la imagen y el fondo no sea tomado en consideración. El filtro de mediana relaja la tolerancia del umbral, al eliminar eliminar píxeles sueltos o pequeños grupos de píxeles que puedan tomar un color similar al de la mano de forma aleatoria dentro de la imagen, o a causa de pequeñas variaciones en la iluminación. No obstante sigue siendo crucial elegir adecuadamente el umbral, y para ello hemos creado un sistema de autocalibrado.
Para realizar el autocalibrado, se comienza colocando el umbral en su nivel más bajo, se captura un forograma en el que únicamente aparezca el fondo de la imagen y se cuenta el número de píxeles activos (a 1) que contenga. Si este número es superior a un umbral, que hemos fijado en 25, se incrementa el umbral y se repite la operación hasta que finalmente prácticamente todos los píxeles del fondo se consideren como inactivos, momento en que el umbral se deja fijo. También cabe la posibilidad de incrementar un poco el umbral más allá del valor calculado, para que el sistema sea más robusto frente a las pequeñas variaciones de iluminación. De momento no nos ha hecho falta en las condiciones en que hemos probado el sistema.
La orden de calibrado se manda a través de dos botones incluidos en el hardware. Uno de ellos es para realizar la calibración con fondo oscuro y mano clara, y otro para mano oscura y fondo claro. El sistema de calibrado funciona de forma que en ambos casos las imagenes capturadas son totalmente equivalentes, ya que en caso de mano oscura y fondo claro invertimos la salida del comparador y fijamos el umbral en sentido contrario, comenzando por el nivel de tensión más alto y disminuyendolo hasta que el fondo quede eliminado.
sábado, 24 de marzo de 2012
Vídeo del hito 1
En este video se muestra la captura de vídeo analógico. Se puede apreciar como el filtro de mediana suaviza la imagen y consigue eliminar píxeles sueltos del fondo y minimizar el efecto de las luces y sombras que se producen en la mano al moverla.
La captura es a 48x48 píxeles a un poco menos de 25 fotogramas por segundo, velocidad limitada únicamente por el puerto serie que utilizamos para enviar las imágenes al ordenador.
La captura es a 48x48 píxeles a un poco menos de 25 fotogramas por segundo, velocidad limitada únicamente por el puerto serie que utilizamos para enviar las imágenes al ordenador.
Captura de vídeo
El vídeo analógico proveniente de la cámara es una señal analógica con una determinada temporización que nosotros no podemos controlar. Por tanto, para poder capturarla deberemos sincronizarnos adecuadamente con ella y realizar todas las tareas necesarias en tiempo real. Trabajaremos con vídeo compuesto en formato PAL.
La señal de vídeo tiene una estructura pensada para facilitar su visualización en los antiguos televisores con tubo de rayos catódicos. En el estándar PAL, cada fotograma mostrado en pantalla se divide en 625 líneas, y se envía en dos cuadros de 312,5 líneas, correspondiendo el primer cuadro a las líneas impares de la imagen y el segundo cuadro a las líneas pares. Se envían 50 cuadros por segundo, lo que corresponde a 25 fotogramas completos por segundo.

Cada línea de vídeo dura 64 microsegundos, de los cuales solo 52 se corresponden a vídeo, siendo el resto necesarios para sincronización. Un pulso de polaridad negativa respecto al nivel de tensión base de la señal indica el comienzo de cada línea, y en los 8 microsegundos siguientes se aprovecha para mandar varios ciclos de la portadora que modula las componentes de color de la señal compuesta de vídeo, para sincronizar el oscilador local del televisor. Este espacio estaba reservado para que el haz de electrones que dibujaba la imágen tuviera tiempo para desplazarse a la izquierda antes de comenzar a dibujar la siguiente línea.
Los 52us de vídeo son una señal continua cuya envolvente es la componente de luminancia, que es la que nos interesa a nosotros pues incluye la información de "blanco y negro" del vídeo. La información de color se envía modulada en una zona vacía del espectro de luminancia. La señal de luminancia adquiere niveles mayores de tensión cuanto más luminosa sea la imagen.

En la vida real, una parte de las líneas de video de la imagen no son visibles y al final de cada cuadro se incluyen una serie de pulsos de sincronismo vertical para dar tiempo a que el haz de electrones volviera a situarse en la esquina superior izquierda tras acabar de dibujar un cuadro. También hay parte de los 52us de vídeo por linea que no son visibles. Finalmente tendremos 288 líneas visibles por cuadro y unos 50,5 us visibles por línea de vídeo. En la imagen anterior se puede ver la temporización completa del video en formato NTSC, equivalente a la que utiliza PAL salvo por la diferencia en el número de líneas y la duración de cada línea. Comprobamos empíricamente que en el caso de PAL el vídeo visible comienza a partir de la línea 28, momento en que comenzamos la captura del cuadro.
Para capturar la señal aprovechamos los recursos disponibles del microcontrolador, ayudados por un circuito integrado LM1881 que extrae la información de sincronismo de la señal de vídeo y se la proporciona al PIC. Puede verse en el datasheet que se extraen los pulsos de sincronismo horizontal (al comienzo de cada línea de vídeo), las ráfagas de portadora que se mandan al principio de cada línea, la información referente a si el cuadro contiene líneas pares o impares y los pulsos de sincronismo vertical.
Conectamos las salidas de este integrado a varias entradas del PIC32 con capacidad para interrupción externa, según el siguiente esquema:

De las cuatro señales de sincronismo, utilizamos la que nos indica si el cuadro es par o impar (aparte de dar información de cuándo comienza el cuadro) y la señal que se sincroniza con las ráfagas de ciclos de portadora. También podría utilizarse de forma equivalente la señal de sincronismo horizontal si el sistema debiera lidiar con cámaras que no enviasen información de color.
Utilizamos un sistema basado en interrupciones y en el uso de los temporizadores para capturar la imagen, según el siguiente diagrama de flujo:

El PIC32 utiliza una arquitectura en la cual el tiempo que tarda en ejecutarse una instrucción no es determinista, debido a que depende de varios factores como los estados de espera que se hayan configurado para la flash y del estado de la caché de instrucciones en cada instante. Es por ello que debemos emplear un sistema basado en los periféricos del microcontrolador, y no en la cuenta de instrucciones, que es lo que suele hacerse en arquitecturas con tiempo de instrucción determinista.
Nuestra primera idea era capturar imágenes a 96x96 píxeles. Asiumiendo un tiempo de vídeo visible de 50,4us por línea, esto nos da que habría que muestrear la zona de vídeo cada 525ns. A 80MHz, esto nos da 42 ciclos de reloj entre cada interrupción para ejecutar el código que guarde el píxel capturado y de servicio a las interrupciones. Pudimos comprobar empíricamente que con este periodo de muestreo había casos en los que no daba tiempo a ejecutar todo el código y por tanto el tiempo de vídeo muestreado en la realidad era superior a 50,4us, saliéndose de la línea y muestreando erróneamente parte de la siguiente.
Ya que nuestra resolución de trabajo final iban a ser 24x24 píxeles, comprobamos si el filtro de mediana seguía trabajando bien a 48x48 píxeles, y tras comprobar que sí, decidimos muestrear finalmente la imagen a 48x48 píxeles, donde el tiempo entre muestras es suficiente, consiguiendo muestrear correctamente la línea completa de vídeo a intervalos regulares y deterministas.

Cada línea de vídeo dura 64 microsegundos, de los cuales solo 52 se corresponden a vídeo, siendo el resto necesarios para sincronización. Un pulso de polaridad negativa respecto al nivel de tensión base de la señal indica el comienzo de cada línea, y en los 8 microsegundos siguientes se aprovecha para mandar varios ciclos de la portadora que modula las componentes de color de la señal compuesta de vídeo, para sincronizar el oscilador local del televisor. Este espacio estaba reservado para que el haz de electrones que dibujaba la imágen tuviera tiempo para desplazarse a la izquierda antes de comenzar a dibujar la siguiente línea.
Los 52us de vídeo son una señal continua cuya envolvente es la componente de luminancia, que es la que nos interesa a nosotros pues incluye la información de "blanco y negro" del vídeo. La información de color se envía modulada en una zona vacía del espectro de luminancia. La señal de luminancia adquiere niveles mayores de tensión cuanto más luminosa sea la imagen.

En la vida real, una parte de las líneas de video de la imagen no son visibles y al final de cada cuadro se incluyen una serie de pulsos de sincronismo vertical para dar tiempo a que el haz de electrones volviera a situarse en la esquina superior izquierda tras acabar de dibujar un cuadro. También hay parte de los 52us de vídeo por linea que no son visibles. Finalmente tendremos 288 líneas visibles por cuadro y unos 50,5 us visibles por línea de vídeo. En la imagen anterior se puede ver la temporización completa del video en formato NTSC, equivalente a la que utiliza PAL salvo por la diferencia en el número de líneas y la duración de cada línea. Comprobamos empíricamente que en el caso de PAL el vídeo visible comienza a partir de la línea 28, momento en que comenzamos la captura del cuadro.
Para capturar la señal aprovechamos los recursos disponibles del microcontrolador, ayudados por un circuito integrado LM1881 que extrae la información de sincronismo de la señal de vídeo y se la proporciona al PIC. Puede verse en el datasheet que se extraen los pulsos de sincronismo horizontal (al comienzo de cada línea de vídeo), las ráfagas de portadora que se mandan al principio de cada línea, la información referente a si el cuadro contiene líneas pares o impares y los pulsos de sincronismo vertical.
Conectamos las salidas de este integrado a varias entradas del PIC32 con capacidad para interrupción externa, según el siguiente esquema:

De las cuatro señales de sincronismo, utilizamos la que nos indica si el cuadro es par o impar (aparte de dar información de cuándo comienza el cuadro) y la señal que se sincroniza con las ráfagas de ciclos de portadora. También podría utilizarse de forma equivalente la señal de sincronismo horizontal si el sistema debiera lidiar con cámaras que no enviasen información de color.
Utilizamos un sistema basado en interrupciones y en el uso de los temporizadores para capturar la imagen, según el siguiente diagrama de flujo:

El PIC32 utiliza una arquitectura en la cual el tiempo que tarda en ejecutarse una instrucción no es determinista, debido a que depende de varios factores como los estados de espera que se hayan configurado para la flash y del estado de la caché de instrucciones en cada instante. Es por ello que debemos emplear un sistema basado en los periféricos del microcontrolador, y no en la cuenta de instrucciones, que es lo que suele hacerse en arquitecturas con tiempo de instrucción determinista.
Nuestra primera idea era capturar imágenes a 96x96 píxeles. Asiumiendo un tiempo de vídeo visible de 50,4us por línea, esto nos da que habría que muestrear la zona de vídeo cada 525ns. A 80MHz, esto nos da 42 ciclos de reloj entre cada interrupción para ejecutar el código que guarde el píxel capturado y de servicio a las interrupciones. Pudimos comprobar empíricamente que con este periodo de muestreo había casos en los que no daba tiempo a ejecutar todo el código y por tanto el tiempo de vídeo muestreado en la realidad era superior a 50,4us, saliéndose de la línea y muestreando erróneamente parte de la siguiente.
Ya que nuestra resolución de trabajo final iban a ser 24x24 píxeles, comprobamos si el filtro de mediana seguía trabajando bien a 48x48 píxeles, y tras comprobar que sí, decidimos muestrear finalmente la imagen a 48x48 píxeles, donde el tiempo entre muestras es suficiente, consiguiendo muestrear correctamente la línea completa de vídeo a intervalos regulares y deterministas.
Procesado de imagen en C: filtro de mediana
- Filtrado de mediana
El filtro de mediana nos causó más complicaciones, debido fundamentalmente al uso de punteros en C.
El objetivo del filtro de mediana es eliminar ruido y suavizar la imagen; sus efectos pueden comprobarse en anteriores entradas. Debido a que este filtro tiene en cuenta el valor de los vecinos de cada píxel para determinar si éste deberá ser blanco o negro, para implementarlo fue necesario crear primero una subrutina "getUnos". Dado un array bidimensional de imagen y unas coordenadas de posición inicial y final, coge el parche correspondiente de dicha imagen y cuenta el número de bits 1 en el mismo. Este método incluye un paso por referencia de la variable en la que guardaremos el número de unos, y fue esto lo que nos causó una serie de problemas iniciales debido a nuestra falta de familiaridad con punteros.
Una vez creada esta subrutina, el método de filtro de mediana se simplifica: recorremos la imagen a procesar píxel a píxel, y para cada uno cogemos un parche de la imagen alrededor de ese píxel (el radio del parche se especifica en la llamada al método); el método getUnos nos devuelve el número de unos en ese parche y según superen o no un umbral reescribimos el píxel central como 0 o 1. De esta forma determinamos si un píxel debe ser blanco o negro según sus vecinos.
Por supuesto, tenemos en cuenta que no debemos salirnos de la imagen en ningún momento (si cogemos la coordenada (0,0) con radio 2, el parche irá de (0,0) a (2,2), sin coger coordenadas negativas). Además, en todo este proceso tenemos que tener en cuenta la agrupación por bytes de los bits de imagen, con lo cual realizamos varias multiplicaciones y divisiones por ocho a lo largo del código; para una mayor rapidez efectuamos estas operaciones mediante desplazamientos, y no como multiplicaciones per se.
Es quizás necesario hacer una especial mención a las dos últimas líneas del código. A fin de modificar únicamente el bit deseado en cada momento, lo editamos mediante el uso de una máscara. Así, si se determina que debe ser un 1, se produce:
if(unos>umbral)dstImg[i][j>>3] |= 0x80>>(j%8);
El filtro de mediana nos causó más complicaciones, debido fundamentalmente al uso de punteros en C.
El objetivo del filtro de mediana es eliminar ruido y suavizar la imagen; sus efectos pueden comprobarse en anteriores entradas. Debido a que este filtro tiene en cuenta el valor de los vecinos de cada píxel para determinar si éste deberá ser blanco o negro, para implementarlo fue necesario crear primero una subrutina "getUnos". Dado un array bidimensional de imagen y unas coordenadas de posición inicial y final, coge el parche correspondiente de dicha imagen y cuenta el número de bits 1 en el mismo. Este método incluye un paso por referencia de la variable en la que guardaremos el número de unos, y fue esto lo que nos causó una serie de problemas iniciales debido a nuestra falta de familiaridad con punteros.
Una vez creada esta subrutina, el método de filtro de mediana se simplifica: recorremos la imagen a procesar píxel a píxel, y para cada uno cogemos un parche de la imagen alrededor de ese píxel (el radio del parche se especifica en la llamada al método); el método getUnos nos devuelve el número de unos en ese parche y según superen o no un umbral reescribimos el píxel central como 0 o 1. De esta forma determinamos si un píxel debe ser blanco o negro según sus vecinos.
Por supuesto, tenemos en cuenta que no debemos salirnos de la imagen en ningún momento (si cogemos la coordenada (0,0) con radio 2, el parche irá de (0,0) a (2,2), sin coger coordenadas negativas). Además, en todo este proceso tenemos que tener en cuenta la agrupación por bytes de los bits de imagen, con lo cual realizamos varias multiplicaciones y divisiones por ocho a lo largo del código; para una mayor rapidez efectuamos estas operaciones mediante desplazamientos, y no como multiplicaciones per se.
Es quizás necesario hacer una especial mención a las dos últimas líneas del código. A fin de modificar únicamente el bit deseado en cada momento, lo editamos mediante el uso de una máscara. Así, si se determina que debe ser un 1, se produce:
if(unos>umbral)dstImg[i][j>>3] |= 0x80>>(j%8);
Donde las coordenadas del píxel considerado son (i,j). En suma, este píxel pertenece al byte[i][j/8], y su posición dentro del mismo será desde la izquierda el resto de j/8. Realizamos por tanto un OR del byte actual con una máscara con un 1 en la posición del píxel considerado, de forma que su valor quede en 1 sin variar el de los demás en el byte.
Código de medianFilter y getUnos.
Suscribirse a:
Comentarios (Atom)



























