lunes, 14 de mayo de 2012

Timers en menús

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).

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.






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í.






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.

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.


  1. void idle(uint8_t gesto, struct dedos *dedos_t) {
  2.     if (!timeout[2]) {
  3.         switch (gesto) { //pasamos a una u otra función según el gesto que elija el usuario
  4.             case GESTO_LUCES: //el usuario quiere acceder al menú de luces
  5.                 func_menu = &luz_control; // pasamos a control de luces
  6.                 break;
  7.             case GESTO_CLIMA:
  8.                 func_menu = &clima_control; //pasamos a control de climatizador
  9.                 break;
  10.             case GESTO_RELOJ:
  11.                 func_menu = &hora_control;
  12.                 break;
  13.             case GESTO_JUEGO:
  14.                 func_menu = &juegoPPT; //pasamos a juego
  15.                 break;
  16.             case GESTO_MUSICA:
  17.                 //printf("paso a musica \n");
  18.                 break;
  19.         }
  20.     }

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.


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