domingo, 11 de mayo de 2014

Solución a problema de sensibilidad Kinect xbox 360 en PC

En esta publicación hablare sobre la solución que se utilizó en el proyecto de piano con la Kinect que estoy realizando, solución sobre el problema de sensibilidad de la Kinect que he comentado en una publicación anterior.

Para explicar la solución a este problema explicare en que consiste una tecla del piano con la Kinect:
Una tecla es un espacio en el suelo que tiene las medidas de un cerámico normal de 30 x 30 cm. Que está ubicado aproximadamente a 2,25 mts.  de la Kinect con la Kinect a una altura de 1,30 mts. Aproximadamente.


Por lo cual al poner el pie sobre una orilla de la tecla de este piano el sonido se detenía y reproducía a una velocidad de 30 veces por segundo, lo cual distorsiona el sonido y da una experiencia muy mala sobre el piano, para solucionarlo se tomaron varias posibles soluciones, las cuales todas consistían en fórmulas matemáticas básicas y lineales, las cuales dan el mismo problema ya que seleccionan un espacio dependiendo del dato numérico de entrada, el cual se toma 30 veces en un segundo. Al analizar de mejor forma el problema después de fallar tantas veces (unas 5 soluciones se probaron fallando todas) se llegó a la solución de aplicar una herramienta de inteligencia artificial, la cual es llamada Lógica difusa.

Una definición simple de internet:

La lógica difusa (también llamada lógica borrosa o lógica heurística) se basa en lo relativo de lo observado como posición diferencial. Este tipo de lógica toma dos valores aleatorios, pero contextualizados y referidos entre sí. Así, por ejemplo, una persona que mida 2 metros es claramente una persona alta, si previamente se ha tomado el valor de persona baja y se ha establecido en 1 metro. Ambos valores están contextualizados a personas y referidos a una medida métrica lineal.


La lógica difusa imita el pensamiento de decisión de un humano por lo cual puedo hacer mención de que es parte de la inteligencia artificial, aunque implementarla es muy matemático ya que se basa en funciones de distintos planos que se cruzan para escoger la alternativa correcta o más correcta dependiendo del punto de vista que se quiere imitar. Para utilizar de buena forma la lógica difusa es necesario investigar y entender bien su forma teórica, por lo cual invito a investigar sobre ella y así podrá darle un toque más de realidad a su software, claro que todo depende del problema que se tenga.


Bueno ya llegado a la solución a implementar lo siguiente fue diseñar la solución como sigue:

Nota: para no alargar la publicación se omite la explicación de algunos conceptos relacionados a la lógica difusa y su implementación, esperando que un buen lector pueda investigar sobre el tema.

Nociones a representar:


·         El pie está dentro o fuera de la tecla en cierto grado.
·         Mientras más cercano esté el pie a la frontera de la tecla, menos grado de pertenencia tiene el pie de estar dentro de la tecla.
·         Si el pie está más cercano al centro de la tecla, mayor grado de pertenencia tiene de estar dentro de la tecla.


Diseño a implementar:

·         Se propusieron 2 variables lingüísticas de entrada:

o   Eje X.
o   Eje Z
·         El eje Y se omitió ya que no presenta problemas.
·         Para las variables lingüísticas X y Z se crearon 3 conjuntos difusos con sus respectivas funciones de pertenencia:

o   Fuera 1
o   Dentro
o   Fuera 2
·         Base de reglas para tomar las decisiones, reglas que se basan en la forma de pensar humana:

o   IF (X IS Fuera1) AND (Z IS Fuera1) THEN Exito IS NoToca
o   IF (X IS Fuera1) AND (Z IS Dentro) THEN Exito IS NoToca
o   IF (X IS Fuera1) AND (Z IS Fuera2) THEN Exito IS NoToca
o   IF (X IS Dentro) AND (Z IS Fuera1) THEN Exito IS NoToca
o   IF (X IS Dentro) AND (Z IS Dentro) THEN Exito IS Toca
o   IF (X IS Dentro) AND (Z IS Fuera2) THEN Exito IS NoToca
o   IF (X IS Fuera2) AND (Z IS Fuera1) THEN Exito IS NoToca
o   IF (X IS Fuera2) AND (Z IS Dentro) THEN Exito IS NoToca
o   IF (X IS Fuera2) AND (Z IS Fuera2) THEN Exito IS NoToca


  • Gráficos de las funciones realizadas:


Conjuntos difusos.jpeg
  • Una vez inferida la conclusión utilizando la base de conocimientos, se debe determinar la salida del sistema difuso a través de la defuzificación.
Diagrama3.jpeg


  • Se utilizará el método del centroide para defuzificar y determinar la salida del sistema.
  • Si la salida es mayor a 72.14% entonces la tecla sonará (en caso de que en el frame anterior el pie estuviese fuera)

Bueno aunque lo anterior puede ser complicado es simple una vez investigado el tema, pero la idea principal es que haremos que el software tome la decisión de tocar o no la tecla dependiendo de que si cree que esta 100% adentro de la tecla, representado en el código como 72,14%, y así la variación que se tiene del error queda fuera ya que no estaría 100% dentro el pie de la tecla puesto si el valor es menor a 72,14% quiere decir que se esta dentro del rango de error que se tiene, esto se puede afinar para que se tenga un 90% o 80% de certeza o el que uno prefiera, obviamente llegar a 50% seria malo ya que seria como tirar una moneda al aire.


Para finalizar cabe mencionar que la solución funciona mucho mejor que las posibles soluciones de matemáticas lineal que habíamos tomado, obviamente nada es perfecto por lo cual todavía quedan detalles que afinar para que la experiencia en el proyecto del piano con kinect sea 100% agradable.

Como recomendación para cualquier lector: 
dada mi experiencia en este proyecto y en proyectos anteriores, no se debe tener miedo a lo que se ve complicado en primera vista, puesto que implementar lo mas complicado se hace simple una vez se investiga y se entiende.

Problemas de sensibilidad de kinect xbox 360 en PC.

Como dice el título en esta publicación hablare sobre los problemas con la sensibilidad de la Kinect, esta será una entrada informativa no basada en investigación sino que en las pruebas realizadas mediante la detección de extremidades con el SDK1.7 de la Kinect para Windows 7.

Para realizar las pruebas se utilizó el código que se explico en una entrada anterior de este blog donde explico como reconocer las extremidades con la Kinect en C#.


Las pruebas que se realizaron fueron para los 3 ejes espaciales(x,y,z), estas se fueron visualizando en tiempo real con la interfaz que se muestra a continuación:



Las pruebas se realizaron a una distancia de 2,58 mts de la Kinect en línea recta, o sea, frente a la Kinect.
A continuación hare mención a los 3 ejes por separado, ya que cada uno tiene una variación distinta:

Eje x:


En el eje x la variación calculada es de 10 cm (+-5cm.). Por lo cual no se puede realizar una detección precisa de un cuadro de menos de 10 cm de ancho.

Eje y:


En el eje y el problema es menor, ya que el error calculado fue de 5 cm (+-2,5cm.) para lo cual levantar la pierna para realizar alguna acción en el caso de mi proyecto a realizar no es un problema (Proyecto: realizar un piano tocado con los pies utilizando la Kinect).

Eje z:


En este eje el problema es mayor, ya que el error es muy variable, aproximadamente el error calculado fue de 20 cm (+-10cm). El problema es que este error depende de la persona ya que cada uno puede tener pies de distinto tamaño de largo por lo cual el error varía mucho, este error fue tomado en una persona que calza 43 eur.

Conclusión:


Ya tomadas las medidas de error, podemos llegar a la conclusión de que la Kinect de Xbox 360 se puede utilizar para realizar proyectos que no requieran de una sensibilidad muy precisa, con estos datos se debe realizar un trabajo de depuración de ruido bastante exhaustivo (mirado desde el punto de vista de un único estudiante). Pero es muy posible si se da un tiempo para analizarlo.

Reproducir mp3 en C#

La reproducción de sonidos en c# está restringida ya que como este lenguaje pertenece a Microsoft este obviamente preferirá reproducir elementos multimedia en sus formatos tradicionales como WAV, por lo cual es necesario implementar librerías aparte para reproducir otros tipos de formatos de sonido, para lo cual no utilizaremos una librería externa a Microsoft sino que utilizaremos su mismo reproductor de mp3 y otros el cual es Windows media player.
A continuación mostrare el código que se necesita insertar para reproducir el sonido:

//llamada a la librería o dll de Windows media player
using WMPLib;

Para poder utilizar la librería antes hay que agregarla a las referencias, lo cual se puede hacer fácilmente haciendo clic derecho en las referencias, agregar la referencia buscándola en el buscador que posee o buscarla con examinar, esta dll esta en Windows/system32.
Ahora para cargar y reproducir un sonido en los formatos que reproduce wmp este sería el código:

  WindowsMediaPlayer wplayer = new WindowsMediaPlayer();
  wplayer.URL ="ubicación/sonido.mp3";
//para reproducir 
wplayer.controls.play();
//para detener la reproducción
  wplayer.controls.stop();


Y esto sería todo para agregar un sonido a nuestro programa, si se desea reproducir sonidos simultáneamente, con WMP no hay problema ya que reproduce los sonidos asíncronamente por lo cual solo sería crear varios objetos WindowsMediaPlayer y reproducirlos.

domingo, 20 de abril de 2014

Kinect: Obtener posición de las partes del cuerpo.

NOTA: los siguientes ejemplos se realizaron con: Microsoft visual studio 2010, KinectDeveloperToolkit-v1.7.0 y KinectSDK-v1.7.

NOTA: este código es parte de un proyecto, pero solo se mostraran los métodos básicos para que quien lo lea pueda implementarlo sin problemas.
 
Una de las tareas que se produce cuando se quiere realizar un proyecto con la kinect es el de reconocer el espacio y la distancia en la que está el cuerpo, para lo cual les presentare este código que me ayuda a saber dónde está el cuerpo.

Cabe mencionar que las distancias se calculan con respecto a la kinect, esto es por la obviedad que las distancias son captadas por los sensores de la kinect.

A continuación mostrare el código C# que genera la distancia en tiempo real y posteriormente explicare lo esencial para entender este código.

-------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using Microsoft.Kinect;

using System.Runtime.InteropServices;
using System.Drawing.Imaging;

namespace FullSkeletonSKD1
{
    public partial class Form1 : Form
    {
        KinectSensor sensor;
        public Form1()
        {
            InitializeComponent();
            sensor = KinectSensor.KinectSensors[0];
        }


        private void button1_Click(object sender, EventArgs e)
        {
            sensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            sensor.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30);
            sensor.SkeletonStream.Enable();

            sensor.AllFramesReady += FramesReady;
            sensor.Start();
        }

        void FramesReady(object sender, AllFramesReadyEventArgs e)
        {
            ColorImageFrame VFrame = e.OpenColorImageFrame();
            if (VFrame == null) return;
            byte[] pixelS = new byte[VFrame.PixelDataLength];
            Bitmap bmap = ImageToBitmap(VFrame);


            SkeletonFrame SFrame = e.OpenSkeletonFrame();
            if (SFrame == null) return;

            Graphics g = Graphics.FromImage(bmap);
            Skeleton[] Skeletons = new Skeleton[SFrame.SkeletonArrayLength];
            SFrame.CopySkeletonDataTo(Skeletons);

            foreach (Skeleton S in Skeletons)
            {
                if (S.TrackingState == SkeletonTrackingState.Tracked)
                {
                    
                    //body
                    DrawBone(JointType.Head, JointType.ShoulderCenter, S, g);
                    DrawBone(JointType.ShoulderCenter, JointType.Spine, S, g);
                    DrawBone(JointType.Spine, JointType.HipCenter, S, g);
                    //left leg
                    DrawBone(JointType.HipCenter, JointType.HipLeft, S, g);
                    DrawBone(JointType.HipLeft, JointType.KneeLeft, S, g);
                    DrawBone(JointType.KneeLeft, JointType.AnkleLeft, S, g);
                    DrawBone(JointType.AnkleLeft, JointType.FootLeft, S, g);
                    //Right Leg
                    DrawBone(JointType.HipCenter, JointType.HipRight, S, g);
                    DrawBone(JointType.HipRight, JointType.KneeRight, S, g);
                    DrawBone(JointType.KneeRight, JointType.AnkleRight, S, g);
                    DrawBone(JointType.AnkleRight, JointType.FootRight, S, g);
                    //Left Arm
                    DrawBone(JointType.ShoulderCenter, JointType.ShoulderLeft, S, g);
                    DrawBone(JointType.ShoulderLeft, JointType.ElbowLeft, S, g);
                    DrawBone(JointType.ElbowLeft, JointType.WristLeft, S, g);
                    DrawBone(JointType.WristLeft, JointType.HandLeft, S, g);
                    //Right Arm
                    DrawBone(JointType.ShoulderCenter, JointType.ShoulderRight, S, g);
                    DrawBone(JointType.ShoulderRight, JointType.ElbowRight, S, g);
                    DrawBone(JointType.ElbowRight, JointType.WristRight, S, g);
                    DrawBone(JointType.WristRight, JointType.HandRight, S, g);

                }



            }
            pictureBox1.Image = bmap;
        }

        void DrawBone(JointType j1, JointType j2, Skeleton S, Graphics g)
        {
            Point p1 = GetJoint(j1, S);
            if(j1 == JointType.HandLeft){
                this.label1.Text = S.Joints[j1].Position.X + " " + S.Joints[j1].Position.Y + " " + S.Joints[j1].Position.Z;
            }
                Point p2 = GetJoint(j2, S);
                if (j2== JointType.HandLeft)
                {
                    this.label1.Text = S.Joints[j2].Position.X + " " + S.Joints[j2].Position.Y + " " + S.Joints[j2].Position.Z;
                }
            g.DrawLine(Pens.Red, p1, p2);
        }

        Point GetJoint(JointType j, Skeleton S)
        {
            SkeletonPoint Sloc = S.Joints[j].Position;
            ColorImagePoint Cloc = sensor.MapSkeletonPointToColor(Sloc, ColorImageFormat.RgbResolution640x480Fps30);

            return new Point(Cloc.X, Cloc.Y);
        }



        Bitmap ImageToBitmap(ColorImageFrame Image)
        {
            byte[] pixelData =new byte[Image.PixelDataLength];
            Image.CopyPixelDataTo(pixelData);
            Bitmap bmap = new Bitmap( Image.Width,Image.Height,PixelFormat.Format32bppRgb);
            BitmapData bmapS = bmap.LockBits(new Rectangle(0, 0,Image.Width, Image.Height),ImageLockMode.WriteOnly,bmap.PixelFormat);
            IntPtr ptr = bmapS.Scan0;
            Marshal.Copy(pixelData, 0, ptr,Image.PixelDataLength);
            bmap.UnlockBits(bmapS);
            return bmap;
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            sensor.Stop();
        }





    }
}
----------------------------------------------------------------------------------------------------

Lo que el anterior código hace es mostrar la imagen de video captada por la cámara de la kinect y captar un esqueleto si es que lo hay, después en un label muestra los tres puntos X-Y-Z que expresan la distancia en metros, tomando como punto de referencia el centro de la kinect.

EXPLICACIÓN DE CÓDIGO:

  • KinectSensor sensor;
          sensor = KinectSensor.KinectSensors[0];

          Con esta pequeña parte del código se toma la referencia del sensor kinect para poder utilizarlo.

  • sensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);   sensor.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30); sensor.SkeletonStream.Enable();
          Con esto se le dice al sensor que es lo que se requiere usar, así los tendrá preparados para su uso.
  •  sensor.AllFramesReady += FramesReady;
            sensor.Start();
          Con esto se asigna el evento que se requerirá y se inicia el sensor.
  •  void FramesReady(object sender, AllFramesReadyEventArgs e)                                                        
           Esta es la función que toma el evento que se asignó anteriormente, aquí se hacen todos los pasos gráficos y la representación de cada parte del cuerpo reconocida.
  •  void DrawBone(JointType j1, JointType j2, Skeleton S, Graphics g)
          Esta es la función que dibuja un hueso del cuerpo, se necesita el puto de referencia de inicio, el de fin, el arreglo desde donde se obtiene la posición de cada parte y el grafico para dibujar el hueso (en este caso no se dibuja un uso exactamente, sino una line que lo representa).

  • Point GetJoint(JointType j, Skeleton S)
          Esta función obtiene la ubicación de los puntos del cuerpo con respecto a la resolución de la imagen en la que se quiere dibujar.

  •  Bitmap ImageToBitmap(ColorImageFrame Image)
          Esta función transforma la imagen que se obtiene desde la cámara a una imagen Bitmap para poder utilizarla y representarla en el cuadro de imagen.

OBTENCIÓN DE LA UBICACIÖN DE LAS PARTES DEL CUERPO:

Como se dice en el título de este artículo lo que se quiere probar con este ejemplo es la ubicación en tiempo real de alguna parte del cuerpo, lo que realiza esta tarea se encuentra en la función DrawBone() dentro de esta función se encuentra este código: 

S.Joints[j1].Position.X + " " + S.Joints[j1].Position.Y + " " + S.Joints[j1].Position.Z;

el cual obtiene la posición de una parte del cuerpo, S representa el esqueleto del que se quiere sacar las posiciones, en el caso de que hubiera una sola persona este esqueleto seria el número 0 del arreglos de esqueletos que se obtiene así:

            SkeletonFrame SFrame = e.OpenSkeletonFrame();
            Skeleton[] Skeletons = new Skeleton[SFrame.SkeletonArrayLength];
            SFrame.CopySkeletonDataTo(Skeletons);

            foreach (Skeleton S in Skeletons)
            {
Así se obtendría cada esqueleto dentro del arreglo de esqueletos que crea el sensor de la kinect.

Joints[] es un arreglo en el cual están las partes del cuerpo y Position es donde se guardan las posiciones X-Y_Z. si yo quisiera obtener la posición en el eje x de la  mano izquierda de un esqueleto seria así:

 S.Joints[JointType.HandLeft].Position.X.

Entonces en el código lo que se hace es cada vez que se quiera dibujar un hueso se revisa la condición de que si es una mano izquierda, si lo es lo es se escribe su posición en un label (se puede visualizar esto en el código ejemplo mostrado arriba en la función DrawBone() ).

Para finalizar mostramos una imagen que comprueba que este código funciona correctamente, mi mano izquierda esta levantada a x= -0.29 mts, y=-0.08 mts y z=1.7mts de la kinect.