sábado, 26 de mayo de 2012

Vídeo final

Para cerrar nuestro blog, os presentamos el vídeo con un resumen y el resultado final de nuestro proyecto. Aunque hemos tenido problemas por la poca precisión del sensor de ultrasonidos, lo cual hace que el sistema autónomo de detección de contenedores no funcione a la perfección, sí que estamos muy contentos por el resultado obtenido con los sistemas de control por reconocimiento de gestos a través del acelerómetro del móvil y de Kinect.



No puedo acabar esta última entrada sin mostrar mi agradecimiento a todas las personas que de alguna manera han contribuido a que hoy estéis viendo este vídeo. Si estáis leyendo esto, es muy probable que seáis una de ellas. Gracias por creer en nuestro proyecto y devolverme la ilusión cuando he atravesado momentos en los que yo misma no era capaz de creer en él.

miércoles, 23 de mayo de 2012

Integración de Kinect (Parte III)

Por último, en esta entrada hablaremos de la interfaz gráfica de la aplicación que utilizamos para el control del robot Kigo a través de Kinect.

El funcionamiento de esta interfaz gráfica es muy sencillo, aunque requiere que se maneje entre 2 personas: la persona “modelo” (que se coloca delante del sensor de Kinect) y la que controla la interfaz.
En la siguiente imagen vemos cuál es su aspecto:



Los pasos que se han de realizar para poder controlar el robot de manera satisfactoria son los siguientes:
  1. La persona modelo ha de colocarse frente al sensor y quedarse en posición de reposo. En ese momento, hay que hacer click en el botón de “Capturar reposo”. Esto nos permitirá distinguir cuándo se está realizando un movimiento, pues estableceremos unos umbrales gracias a la posición de reposo.

  2. Ya se puede empezar el entrenamiento de los gestos patrón. Para ello, se selecciona en la lista desplegable a qué comando queremos asociar un gesto. 

  3. Hacemos click en el botón de “Capturar gesto” y, cuando se indique en pantalla, se puede empezar a realizar ese gesto. Automáticamente detectará cuándo termina el gesto.

  4. Con algún gesto ya grabado, cuando lo realicemos y sea detectado aparecerá un mensaje por pantalla. De esta manera, podemos probar que la detección funciona correctamente antes de realizar la conexión con el robot.

  5. Cuando estemos preparados para enviar comandos al robot, haremos click en el botón de “Conectar” para que se establezca la conexión Bluetooth con él.

  6. Ya podemos realizar los gestos que queramos y, si están asociados a algún comando, se le enviará al robot por Bluetooth y él realizará la acción correspondiente.

Otras opciones que encontramos en la interfaz para mejorar la experiencia del usuario son:
  • Guardar gestos en un archivo”: Nos permite almacenar las coordenadas de los gestos patrón que hemos grabado en un fichero de texto; de esta manera, la próxima vez que abramos la aplicación podremos volver a utilizar los mismos patrones y no perderemos tiempo en grabar unos nuevos.
  • Cargar archivo con gestos”: Podemos establecer como patrones gestos que hayamos grabado y almacenado en un fichero de texto anteriormente.
  • Mostrar archivo con gestos”: Nos abre un fichero de texto donde tenemos almacenadas coordenadas de algún gesto patrón.
  • Ajustar Kinect”: Nos permite inclinar el Kinect tantos grados como hayamos indicado en la barra de selección superior. Hay que tener en cuenta que la inclinación máxima es de ±27º.
  • Umbral”: Podemos determinar el umbral de similitud que establece si dos gestos son iguales o no. Umbrales elevados implican tener un sistema de poca sensibilidad, que establece que dos gestos son iguales a pesar de que haya grandes diferencias entre ellos. Umbrales muy bajos nos obligan a realizar los gestos con mucha precisión para que puedan ser detectados. Nosotros solemos trabajar con un umbral de 2.
  • Parada de emergencia”: Si le enviamos al robot un comando sin querer, necesitaremos pararlo haciendo click en este botón.



Además, podremos ver en todo momento el esqueleto que Kinect está detectando y una representación de las medidas del sensor de profundidad.




Con esto, ya conocéis los rasgos más importantes de todo nuestro proyecto. Próximamente publicaremos un vídeo en el que se podrá ver el resultado final de nuestro trabajo.

Integración de Kinect (Parte II)

En esta entrada hablaremos del software que hay detrás de la implementación de Kinect en nuestro proyecto y cómo hemos tenido que adaptar el código del que hemos partido.


Software del reconocimiento de gestos mediante Kinect


Como ya hemos mencionado, el código de este proyecto en C# no ha sido escrito por nosotros, sino que lo hemos adaptado a nuestro sistema. Inicialmente estaba adaptado a la Beta 2 del SDK y nosotros lo hemos adaptado a la versión 1.

Por este motivo, en lugar de centrarnos en los detalles del código, explicaremos de forma cualitativa la estructura del software y las modificaciones que hemos tenido que incluir en el código original.
En el siguiente diagrama vemos las 4 clases en las que se basa el software:





1. MainWindow.xaml

En esta clase es donde definimos toda la inteligencia asociada a la interfaz gráfica. En ella hemos tenido que añadir algunos métodos para poder establecer la conexión con el robot Kigo y el envío de comandos.
Además, hemos añadido algunas funcionalidades a la interfaz gráfica que teníamos inicialmente y hemos modificado algunos métodos para mejorar el sistema de reconocimiento de gestos.
Los métodos más importantes que encontramos en ella son:
  • private void WindowLoaded(object sender, RoutedEventArgs e): Una vez que se ha abierto la interfaz gráfica, se llama a este método para inicializar el Kinect.
  • private static void SkeletonExtractSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e): Cuando estamos intentando reconocer un gesto y Kinect tiene un frame del esqueleto listo, este método es llamado. Él se encarga de pasar todos los datos del esqueleto al algoritmo DTW para compararlo con los patrones.
  • private void NuiDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e): Es llamado cada vez que un frame de profundidad está listo. Nos permite mostrar en la interfaz las medidas del sensor de profundidad.
  • private Point getDisplayPosition(DepthImageFrame depthFrame, Joint joint): Obtiene la posición donde se muestra una articulación en la representación cualitativa del esqueleto que aparece en la interfaz.
  • private Polyline GetBodySegment(JointCollection joints, Brush brush, params JointType[] ids): Calcula cómo dibujar las líneas entre los puntos que representan las articulaciones en la interfaz gráfica.
  • private void NuiSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e): Cada vez que un esqueleto está listo para ser mostrado, se llama a este método para actualizar el canvas con las líneas que representan los huesos y los puntos que representan las articulaciones.
  • private void NuiColorFrameReady(object sender, ColorImageFrameReadyEventArgs e): Llamado cada vez que un frame de vídeo está listo.
  • private void DepthImageFrameReady(object sender, DepthImageFrameReadyEventArgs e): Llamado cada vez que un frame de profundidad está listo.
  • private void Connect(): Realiza la conexión con el robot Kigo a través de Bluetooth.
  • private void DtwReadClick(object sender, RoutedEventArgs e): Al hacer click en “Capturar reposo” se llama a este método, que activa el estado de reposo poniendo a “true” la variable “capturandoreposo”.
  • private void DtwCaptureClick(object sender, RoutedEventArgs e): Al hacer click en “Capturar gesto”, lanza una cuenta atrás antes de iniciar la grabación de un nuevo gesto patrón.
  • private void CaptureCountdown(object sender, EventArgs e): Es llamado por el método anterior para mostrar por pantalla la cuenta atrás.
  • private void StartCapture(): Inicializa el modo de captura de un nuevo gesto patrón, poniendo a “true” la variable “capturing”.
  • private void DtwSaveToFile(object sender, RoutedEventArgs e): Guarda los gestos patrón que se han gabrado hasta el momento en un fichero de texto.
  • private void DtwLoadFile(object sender, RoutedEventArgs e): Carga un fichero con gestos patrón que hemos grabado anteriormente.
  • private void NuiSkeleton2DdataCoordReady(object sender, Skeleton2DdataCoordEventArgs a): Es el método que contiene toda la inteligencia del reconocimiento de gestos, al cual se le llama cada vez que Kinect tiene un esqueleto preparado.Si nos encontramos en el estado de “Capturar reposo” (capturandoreposo=true), este método almacena 6 esqueletos de la posición de reposo.Si nos encontramos en el estado de “Capturar gesto” (capturing=true), empieza a guardar automáticamente todos los esqueletos que le llegan. Después eliminará todos aquellos que sean muy similares a la posición de reposo, pues no contendrán información sobre el movimiento.El resto del tiempo estará comparando los esqueletos que le llegan con los patrones y, si detecta algún gesto conocido, enviará a Kigo el comando asociado a él por Bluetooth.
  • private void elevationButton_Click(object sender, RoutedEventArgs e): Cambia la inclinación del Kinect en función de lo que indique el usuario en un slider de la interfaz gráfica.
  • private void stopButton_Click(object sender, RoutedEventArgs e): Manda al robot un comando de parada.
  • private void WindowClosed(object sender, EventArgs e): Cuando se cierra la ventana de la aplicación, se llama a este método para que se detenga el Kinect y se apague el robot.

2. DTWGestureRecognizer

Es la clase donde se encuentra implementado el algoritmo de alineamiento temporal dinámico (DTW) para la comparación de un gesto realizado y los gestos patrón.

La principal diferencia respecto al algoritmo que implementamos en la clase DTW.java para el reconocimiento de gestos con el acelerómetro del móvil es que ahora comparamos vectores de 12 elementos: las coordenadas (x,y) de 6 partes distintas de nuestro esqueleto.

Otra diferencia es que ahora tenemos una variable llamada “globalThreshold”, un umbral que nos determina la máxima distancia DTW que puede haber entre dos gestos para que puedan ser considerados iguales. Si la distancia de un gesto realizado a todos los patrones está por encima de este umbral, el gesto no será reconocido. Podremos determinar el valor de esta variable desde la interfaz gráfica.


3. Skeleton2DDataExtract

Se encarga de obtener las coordenadas de la posición de 6 partes distintas de nuestro cuerpo, gracias a la funcionalidad de “skeletal tracking” del SDK de Kinect, y almacenarla en un array “p” de objetos de la clase “Point”.

Las partes del esqueleto con las que trabajaremos y el orden en el que las tendremos almacenadas se pueden ver en la siguiente tabla:

Posición del array
Parte del cuerpo
p[0]
Mano izquierda
p[1]
Muñeca izquierda
p[2]
Codo izquierdo
p[3]
Codo derecho
p[4]
Muñeca derecha
p[5]
Mano derecha


Además, obtiene las posiciones de ambos hombros y las almacena en las variables “shoulderRight” y “shoulderLeft”. Estas coordenadas serán utilizadas para establecer el origen de nuestro sistema de referencia en el punto medio del segmento que une ambos hombros.

Por último, para completar la definición del sistema de coordenadas, normaliza la distancia de cada punto al nuevo origen de coordenadas dividiendo entre la distancia que separa a los hombros.


4. Skeleton2DdataCoordEventArgs

Coge todas las coordenadas que habíamos obtenido y expresado según el nuevo sistema de referencia en la clase Skeleton2DDataExtract y las prepara para poder aplicar el algoritmo DTW sobre ellas.

Para ello, crea un nuevo vector de 12 doubles en el que va almacenando las coordenadas x en las posiciones pares (consideramos que el 0 es par) y las coordenadas y en las posiciones impares.

Este vector será el que pasemos como parámetro a la clase DTWGestureRecognizer para calcular distancias.



Adaptación a la versión 1 del SDK

Como todo el código estaba adaptado a la Beta 2 del SDK, hemos tenido que realizar algunas modificaciones para conseguir que funcionase con la versión 1, que es con la que nosotros trabajamos:
  • El primer cambio es el nombre de la librería. Antes había que importarla con:
    usingMicrosoft.Research.Kinect.Nui;
    Ahora hay que importarla con:
    usingMicrosoft.Kinect;
  • Algunas clases cambian de nombre al pasar de una versión a la otra. Las dos que nos afectan a nosotros son:

Antiguo nombre
Nuevo nombre
JointID
JointType
Runtime
KinectSensor

  • Ahora la obtención de frames de las cámaras de vídeo y profundidad es más compleja, pues ImageFrameReadyEventArgs se divide en ColorImageFrameReadyEventArgs para la cámara de video y DepthImageFrameReadyEventArgs para la cámara de profundidad.
  • La forma de sacar los datos de los “ReadyEventArgs”  también cambia considerablemente:
    AntesSkeletonFrameskeletonFrame = e.SkeletonFrame;
    Ahora : SkeletonFrameskeletonFrame = e.OpenSkeletonFrame();
  • También ha cambiado la forma de recorrer los esqueletos:
    Antes: foreach (SkeletonData data inskeletonFrame.Skeletons) {}
    AhoraSkeleton[] skeletons = newSkeleton[skeletonFrame.SkeletonArrayLength];skeletonFrame.CopySkeletonDataTo(skeletons);foreach (Skeleton data in skeletons) {}
  • Antes era necesario realizar una conversión manual en los bytes de la cámara de profundidad, pero ahora no.
  • Por último, la forma de inicializar el Kinect ha cambiado.

martes, 22 de mayo de 2012

Integración de Kinect (Parte I)

Como última mejora de nuestro sistema, hemos integrado en él el control del robot Kigo a través de Kinect.

Para ello, hemos partido de un proyecto open source para el reconocimiento de gestos a través del Software Development Kit (SDK) de Kinect que nos ha facilitado nuestro tutor Juan Manuel Montero.
El código de este proyecto está escrito en C#, a diferencia del código con el que hemos estado trabajando en el resto de las fases de desarrollo de nuestro sistema, que estaba escrito en Java.

Al igual que ocurría en el reconocimiento de gestos a través del acelerómetro de un dispositivo Android, el algoritmo usado para calcular distancias es el alineamiento temporal dinámico (DTW).

La principal diferencia que encontramos entre lo explicado para el acelerómetro del móvil y Kinect es que, en esta ocasión, en lugar de trabajar con 3 coordenadas de la aceleración sufrida por el móvil, trabajamos con posiciones de nuestro esqueleto. Estudiaremos la posición de 6 partes de nuestro cuerpo: mano izquierda y derecha, muñeca izquierda y derecha y codo izquierdo y derecho. Nuestro nuevo sistema de coordenadas será de 2 dimensiones, por lo que sólo tendremos coordenadas (x,y). Además, el origen de coordenadas lo tendremos en el punto medio del segmento que une ambos hombros.

En primer lugar, hablaremos en esta entrada brevemente del hardware del que dispone Kinect y las funcionalidades que nos proporciona Microsoft mediante el SDK para Kinect; posteriormente, en la siguiente entrada, nos centraremos en todo el software relacionado con el sistema de reconocimiento de gestos que hemos integrado y en la interfaz gráfica que usamos.


Breve descripción del hardware de Kinect

Kinect es un controlador de videojuegos desarrollado por Microsoft para la videoconsola XBOX 360. Este controlador permite al usuario interactuar con la consola sin necesidad de tener contacto físico, mediante el reconocimiento de gestos, de voz o de imágenes.

El hardware de Kinect está contenido en una barra horizontal de unos 23 cm de largo, conectado a una base circular con un eje de articulación. Dispone de un mecanismo de inclinación motorizado que le permite inclinarse ±27º.

Los sensores que dotan a Kinect de su funcionalidad son principalmente:
  • Sensor de profundidad: Mediante un proyector de infrarrojos y un sensor CMOS monocromático, Kinect puede capturar datos de vídeo en 3D sean cuales sean las condiciones de luz ambiental. Los datos de vídeo tienen una resolución VGA (640x480 píxels) a 11 bits, proporcionando así 2.048 niveles de sensibilidad. Se capturan 30 frames por segundoSu campo de visión es de unos 57º horizontalmente y 43º verticalmente. Además, puede detectar un rango de 1,2-3,5 metros.
  • Cámara de color: Utiliza una resolución VGA (640x480 píxels) a 8 bits
  • Un array de 4 micrófonos, con audio representado por 16 bits y enviado a 16 KHz.


Software Development Kit para Kinect

En febrero del 2011, Microsoft lanzó el SDK (Software Development Kit) oficial para Kinect, gracias al cual se pueden desarrollar aplicaciones basadas en Kinect en C++, C# o Visual Basic, utilizando Microsoft Visual Studio 2010.

Nosotros hemos trabajado con la versión 1.0 del SDK para Kinect. Algunas de las posibilidades que nos ofrece son:
  • Acceder a los datos de todos los sensores de Kinect: el de profundidad, la cámara de color y el micrófono.
  • Skeletal tracking: la habilidad de detectar la posición de cada una de las partes del cuerpo de una persona situada en el campo de visión del Kinect.
  • Técnicas de procesado de audio, como la cancelación de ecos, supresión de ruido o identificación de la fuente de sonido.
  • Documentación y código de prueba.

De todas las funcionalidades de Kinect, la que nos interesa especialmente a nosotros para el reconocimiento de gestos es el “skeletal tracking”. Kinect es capaz de detectar la posición de hasta 20 partes de nuestro cuerpo mediante el siguiente algoritmo:
  1. Con el sensor de profundidad, mide la distancia a la que se encuentra la persona que se coloca delante y dibuja de forma rudimentaria un esqueleto de las zonas donde ha detectado presencia.
  2. El cerebro de Kinect empieza a averiguar cuál es cada parte del cuerpo, basándose en su experiencia reconociendo otros cuerpos y en modelos que lleva implementados. Asigna una serie de probabilidades a los distintos píxels. Por ejemplo, si está seguro de que una zona es el hombro izquierdo, asignará a esos píxels una probabilidad elevada.
  3. Con este conjunto de probabilidades, dibuja un esqueleto con las partes del cuerpo que ha reconocido, escogiendo la combinación que tiene una mayor probabilidad.

El SDK nos proporciona métodos para acceder a la posición de cada parte del cuerpo, obtenida a través de este algoritmo de “skeletal tracking”.

viernes, 18 de mayo de 2012

Algoritmo DTW (Dynamic Time Warping)

El algoritmo de alineamiento temporal dinámico nos permite medir la similitud entre dos secuencias que pueden variar en el tiempo o en el espacio.

Gracias a él, podremos calcular la distancia entre un gesto que realicemos y cada uno de los gestos patrón almacenados. El gesto reconocido será aquel que se encuentre a menor distancia.

Aunque podríamos haber elegido no utilizar este algoritmo, hay que tener en cuenta que, cuando realizamos un gesto, no siempre haremos un movimiento de la misma duración. Si calculásemos distancias euclídeas y tuviésemos dos vectores idénticos, pero uno desplazado ligeramente en el tiempo respecto al otro, al hacer la comparación punto a punto pensaríamos que ambos vectores son distintos. Sin embargo, con el DTW podremos realizar gestos con mayor o menor velocidad y darnos cuenta de que son idénticos.




Originalmente,  el algoritmo DTW se usaba para comparar distintos patrones acústicos en el reconocimiento automático del habla. Otras aplicaciones son el reconocimiento de gestos o de escritura.

Con esta entrada aprenderemos cuál es el fundamento matemático de este algoritmo y cómo ha sido programado en Java.



Fundamento matemático

Sean las secuencias X=(x1,x2,...,xN), de longitud N, e Y=(y1,y2,...,yM), de longitud M. Estas secuencias pueden ser señales discretas o señales muestreadas en puntos equidistantes en el tiempo.

Definimos un espacio vectorial F formado por estas muestras, de manera que:

Si queremos comparar dos secuencias, necesitamos una medida de costes o distancias locales; esta función se define como: 

Esta función nos devolverá un coste c(x,y) pequeño si x e y son similares y un coste c(x,y) elevado si son muy distintas. Si utilizamos esta función para calcular la distancia entre cada par de elementos de las secuencias X e Y, obtenemos una matriz de costes:

Cada elemento de esta matriz se define como c(n,m)=c(xn,ym). La matriz C nos permite alinear las secuencias X e Y, buscando un camino de alineación de coste mínimo a lo largo de ella.

Formalmente, un camino de alineación (warping path) se define como una secuencia:



 Esta secuencia debe satisfacer tres condiciones:

  1. Condición de frontera:

     para que los primeros  y los últimos elementos de X e Y estén alineados y así las secuencias completas estén alineadas.
  2.  
    Condición de monotonía:
     
    Con ella nos aseguramos de que elementos que se sucedan en el tiempo también se sucedan al ser alineados.
  3. Condición de salto:

    Nos indica que no se puede omitir ningún elemento de X e Y y que el camino de alineamiento es continuo.
En la siguiente figura vemos claramente en qué consiste cada una de estas condiciones para una secuencia X de longitud N=7 y una secuencia Y de longitud M=9. Mientras que en (a) se cumplen las 3, en (b) se viola la condición de frontera, en (c) la de monotonía y en (d) la de salto.


El coste total cp(X,Y) de un camino de alineamiento p será la suma de todos los costes locales:

El camino de alineamiento óptimo entre X e Y es un camino de alineamiento p* que, entre todos los posibles caminos de alineamiento, tiene el coste total mínimo. Se define la distancia DTW entre X e Y como el coste total de p*:


La distancia DTW es la que calcularemos para comparar los gestos que han de ser reconocidos con los patrones.

Para determinar cuál es el camino óptimo p* entre dos secuencias, se podrían probar todos los caminos de alineamiento entre X e Y; sin embargo, la complejidad de este problema crecería de forma exponencial con N y M. Para simplificarlo, trabajaremos con la matriz D de costes acumulados:

En esta expresión, aparecen unas secuencias prefijo:

La distancia acumulada es la distancia entre la celda actual y la mínima de las distancias acumuladas para los elementos adyacentes:

Por lo tanto, el cálculo de distancia acumulada se podrá programar de manera recursiva y podremos calcular la distancia DTW en función de la acumulada:


Clase DTW.java

Una vez que conocemos el fundamento matemático del algoritmo DTW, es muy fácil traducirlo al lenguaje de programación y ver los pasos que sigue la clase DTW.java para el cálculo de distancias.

El código de esta clase no ha sido escrito por nosotros, sino que su autor es Cheol-Woo Jung y nosotros lo hemos adaptado para poder calcular la distancia DTW entre dos arrays de objetos de la clase Data (donde se almacenan las coordenadas de la aceleración sufrida por el acelerómetro del móvil al realizar un gesto).

Las variables que utilizamos son:
  • Dos arrays de Data seq1 (coordenadas del gesto a reconocer) y seq2 (coordenadas del patrón).
  • Un array bidimensional de enteros warpingPath, que representa un camino de alineamiento.
  • 3 enteros n, m y k, que serán usados como índices para recorrer bucles.
  • El double warpingDistance, donde quedará almacenada finalmente la distancia DTW que calculemos.
Los métodos de los que disponemos para este complejo cálculo de distancias son:

  • protected double distanceBetween(Data p1, Data p2): Calcula la distancia euclídea entre 2 puntos cualesquiera que le pasemos como parámetro. Como en nuestro caso trabajamos con 3 coordenadas de aceleración, se calculará esta distancia como:
  • protected int getIndexOfMinimum(double[] array): Devuelve el índice del elemento con valor mínimo en un array de doubles.
  • public double getDistance(): Simplemente devuelve el valor de la variable “warpingDistance”. Tras realizar el cálculo de la distancia DTW, el valor final estará almacenado en esta variable y lo podremos recuperar con este getter.
  • protected void reversePath(int[][] path): Invierte el orden de un camino de alineamiento, es decir, hace que su primer punto se convierta en el último y viceversa. Para ello, parte del array bidimensional “path” que le pasamos como parámetro y crea uno nuevo llamado “newPath” del mismo tamaño y con los elementos en orden decreciente. Finalmente almacena el nuevo camino de alineamiento en la variable “warpingPath”.
  • public void compute(): Es el método más importante de la clase, que utiliza a todos los demás para poder determinar el camino de alineamiento óptimo y la distancia DTW asociada a él. Este proceso lo realiza a través de la matriz de distancias acumuladas.
Conociendo las variables y los métodos de los que disponemos, explicaremos de forma sencilla qué pasos sigue esta clase para obtener distancias DTW:

  1. Se llama al constructor de la clase, pasando como parámetro un array de Datasample” con las coordenadas del gesto a reconocer y otro “template” con las coordenadas del patrón con el que se quiere comparar.
  2. Se inicializan las variables “seq1” con el array “sample” y “n” con su longitud; de la misma manera, “seq2” es inicializado con el array patrón “template” y “m” con su longitud.
  3. Se inicializa el array bidimensional “warpingPath” con un tamaño de (N+M)x2 y establecemos que la distancia del camino óptimo “warpingDistance” inicialmente es 0.
  4. El constructor invoca al método compute() para que calcule la distancia DTW. En primer lugar, éste crea una matriz “d”, que contiene la distancia euclídea entre cada punto de la primera y de la segunda secuencia que se están comparando (por lo tanto, es una matriz NxM). También crea la matriz “D”, que será la matriz de costes acumulados.
  5. A partir de la matriz de distancias “d”, se va completando la matriz de costes acumulados “D”.
  6. Gracias a la matriz de costes acumulados, se va construyendo el camino de alineamiento óptimo rellenando el array bidimensional “warpingPath”, de tamaño Kx2 (siendo K<N+M). El cálculo de este camino p*=(p1,p2,...,pK) se realiza en orden inverso, empezando por el punto final pK=(N,M). Cada elemento se obtiene como:



  7.  Como tendremos el camino de alineamiento óptimo calculado en orden inverso, tendremos que invertirlo llamando al método reversePath().
  8. La distancia DTW entre las dos secuencias, que almacenaremos en la variable “warpingDistance” y será el resultado final de nuestro cálculo, se obtiene como la distancia acumulada al llegar al último punto de este camino de alineamiento óptimo.

Con esta entrada damos por terminada la explicación de todo lo relacionado con nuestro sistema de reconocimiento de gestos mediante el acelerómetro de un dispositivo Android. En los próximos días os hablaremos de cómo hemos implementado el control del robot Kigo mediante Kinect.



jueves, 17 de mayo de 2012

Modificaciones en la aplicación Android para el reconocimiento de gestos

Para integrar el sistema de reconocimiento de gestos en la aplicación Android, hemos tenido que realizar ciertas modificaciones en su software. En esta entrada explicaremos cuáles son los cambios que ha sufrido la aplicación.


Actividad SettingsActivity.java

El cambio más significativo es la creación de una nueva clase: SettingsActivity.java. Con ella, la estructura del software de la aplicación queda de la siguiente manera:

 


La clase SettingsActivity es una actividad encargada del menú de la aplicación, desde el cual se lleva a cabo la fase de entrenamiento para el reconocimiento de gestos.

Cuando la actividad se inicia, se invoca al método onCreate(), que crea:

  • Una lista desplegablesp” de la clase “Spinner”, que contiene todos los posibles comandos a los que se les puede asociar un gesto.
  • Cuatro botonesstartLeartingBtn”, “startIterationBtn”, “stopIterationBtn” y “stopLearningBtn” para poder realizar el entrenamiento (inicialmente sólo estará habilitado el de “Empezar Entrenamiento”.
Además, este método define cómo debe responder la actividad al evento de hacer click sobre uno de los botones:
  • Empezar entrenamiento:
    Llama al método public void startLearning(int type, int iterations, int idGesture) de la clase GestureFactory, pasándole como parámetro que el tipo de sensor con el que tiene que trabajar es el acelerómetro (type=0), que se han de realizar 3 iteraciones para completar el entrenamiento (iterations=3) y el número de identificación idGesture asociado al comando que se ha seleccionado en la lista desplegable “sp”.
    Además, al hacer click sobre este botón se habilita el de “Empezar gesto” y se inhabilita él mismo, pues ya se entra en la fase de entrenamiento.
  • Empezar gesto:
    Simplemente invoca al método public void startIteration() de la clase GestureFactory, para que asocie un listener al acelerómetro y almacene las coordenadas de la aceleración que sufre el dispositivo Android al realizar el gesto.
    Tras hacer click sobre él, él mismo se inhabilita y sólo queda habilitado el botón de “Finalizar gesto”.

  • Finalizar gesto:
    Llama al método public void stopIteration() de la clase GestureFactory para eliminar la asociación entre el listener y el acelerómetro.
    Además de inhabilitarse a sí mismo, si el número de iteraciones realizadas de un mismo gesto ya es 3, habilita el botón “Finalizar entrenamiento”; si es inferior a 3, vuelve a habilitar el de “Empezar gesto”.

  • Finalizar entrenamiento:
    En primer lugar, resetea el contador de iteraciones e inhabilita todos los botones excepto el de “Empezar entrenamiento”.
    Invoca al método public Gesture stopLearning() de la clase GestureFactory y añade el objeto de la clase Gesture que este método devuelve a nuestro ArrayList de gestos patrón.
    La aplicación queda preparada para comenzar un nuevo entrenamiento o pasar a la fase de realización de gestos y comparación con los patrones.


Cambios en NXJAndroidActivity.java

Hemos tenido que introducir algunas modificaciones en la actividad principal NXJAndroidActivity.java para poder comenzar la detección de movimientos del usuario y enviar los comandos correspondientes al robot.

Ahora esta actividad crea un objeto “ad” de la clase AndroidDevice, que se encargará de eliminar el efecto de la gravedad en las lecturas que tome el acelerómetro en la fase de realización de gestos.

Inicializa los dos nuevos botones que antes no teníamos en nuestra aplicación (que se encuentran en la nueva página de “Gestos”) y define cómo deben responder al evento de que el usuario haga click sobre ellos:
  • Iniciar gesto:
    Añade un GestureListener asociado al acelerómetro y entramos en la fase de realización de gestos. A partir de aquí, es la clase GestureManager la que se encarga de comparar los movimientos detectados con sus umbrales de energía y de tiempo para establecer en qué momento se está realizando un gesto, tal y como explicamos en la entrada “Software para el reconocimiento de gestos (parte II)”.
    Cuando la clase GestureManager detecta que se ha terminado de realizar el gesto, compara sus coordenadas con las de todos los patrones mediante el algoritmo DTW y decide cuál de ellos se parece más, calculando la distancia mínima; se almacena en una variable el número de identificación  del gesto patrón más parecido.
    Al finalizar el gesto, se invoca al método onGestureDetected() de la interfaz GestureListener, al que se le pasa como parámetro el número de identificación “idGesture”. Dependiendo de este número, se le enviará por Bluetooth al robot Kigo un comando u otro a través del método public static void send(byte[] bytearray) de la clase Robot. En esta tabla podemos ver la correspondencia establecida:

  • Detener gesto:
    El listener de gestos estará continuamente detectando posibles movimientos del dispositivo Android en busca de nuevos gestos hasta que hagamos click sobre este botón. Lo único que hace al detectar que el usuario ha hecho click sobre él es inhabilitar el AndroidDevice y, por consecuencia, eliminar la asociación entre el SensorEventListener y el acelerómetro.
    Con él salimos del sistema de reconocimiento de gestos.