miércoles, 23 de mayo de 2012

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.

1 comentario:

  1. Muy bueno!! se ve interesante de hecho quiero hacer algo parecido, como hago para obtener el código competo de la aplicación. como te contacto. mi correo es : jramirez1009@hotmail.com

    ResponderEliminar