[XNA] Detección de movimientos con una webcam (I de II)

Este artículo comienza como una serie de experimentos con la interacción jugador-XNA mediante webcam. En esta primera parte simplemente procederé a detectar movimientos en ciertas zonas de la pantalla con lo que denominaremos “sensores”. Una vez calibrados y controlados estos sensores, podremos experimentar con formas de interacción poco habituales. Por ejemplo, podremos hacer que una nave en 3D gire hacia la derecha o hacia la izquierda, dependiendo de los sensores que “activemos”. Estos sensores podrían ser dos, uno a cada lado de la pantalla, de forma que sean sensibles a los movimientos de la mano del usuario. Así levantando la mano hacia la derecha, nuestra nave rotará en esa dirección, y viceversa si levantamos la mano izquierda. En definitiva, tendremos una respuesta virtual a tiempo real a nuestra interacción. Todo esto supondría trabajar algunos aspectos básicos de la realidad virtual.

Dicho así todo esto puede sonar un poco complicado… pero como siempre, una imagen puede aclararnos más que mil palabras -nótese que la captura está tomada cerca de las 3 de la madrugada, y uno comienza a estar roto a esas horas…-. En la captura se ve rápidamente que hay dos cuadros en la pantalla, el de la izquierda está en color blanco, y el de la derecha está en rojo. Esto es así porque hay un cambio en el color en esa zona dentro del recuadro (a continuación veremos el algoritmo utilizado en detalle).

Antes de entrar en mayores detalles, indicaré que no explicaré la entrada de vídeo desde la Webcam a XNA, básicamente porque estoy reutilizando una clase implementada por Javi Cantón para ello. Para mi este componente es una “caja negra”, en mi aplicación tengo una entrada de vídeo y listos, esta, va actualizando una textura y sus datos relacionados. Si queréis utilizar ese componente está dentro de mi código (tenéis un link al final del artículo), o de forma alternativa podéis bajarlo en XNA Community. Otra clase de terceros que utilizo en el proyecto es Primitive2D, que básicamente permite dibujar gráficos simples 2D basados en primitivas (como su descriptivo nombre indica), no pongo el enlace al autor porque desconozco quién es :-). Y dicho esto… podemos comenzar.

La clase principal de esta aplicación sin duda es “SensorColores“. Esta clase, será responsable de controlar los cambios de la primera captura de pantalla tomada mediante webcam con la actual en cada llamada al método Update().

 

Para entender su funcionamiento será interesante explicar los campos de la clase:

  • colors2DPrevio: Guarda los datos de la textura anterior a la actual, es decir, contra la que queremos comprobar si han habido cambios. En mi implementación, sólo tomaremos una captura de pantalla al inicio de la ejecución, y contra esa haremos el resto de comprobaciones de si hay movimiento o no. Este campo es un array de dos dimensiones, que contiene la información de todos los píxels de la textura.
  • densidadZona: Hemos dicho que definiríamos unas zonas “sensibles”, o sensores, y en ellas se comprobaría si el color de los píxels son distintos respecto el frame anterior. Lo que ocurre es que si tenemos una zona o sensor muy grande, recorrer todos los píxels de la misma sería un proceso demasiado lento, y podría afectar al rendimiento. La densidad indica el número de píxels que nos “saltaremos” en el momento de recorrerlos en busca de cambios. Es decir, si el valor es 5, iremos de 5 en 5 píxels, en lugar de ir de 1 en 1. Esto hace que recorrer todo el array sea mucho más rápido. En realidad nos limitamos a hacer un muestreo de la zona, en lugar de un análisis completo, que entendemos que nos daría unos resultados similares.
  • primitiva: Sólo se utiliza para pintar los cuadros en pantalla, así que aquí no tiene mucha importancia, solo diremos que el color de la misma variará en función de si se han detectado cambios o no en el frame actual respecto el frame anterior.
  • tiempo: Indica el tiempo transcurrido, en milisegundos, desde la primera llamada al método Update de esta clase. Esta variable podría ser útil para variaciones en la implementación del algoritmo, que veremos luego.
  • toleranciaCambio: Este sí que es un atributo interesante. La diferencia de los colores entre dos fotogramas a color, que enfoquen a una persona inmóvil pueden parecer prácticamente iguales a la vista humana, pero la realidad es que las imágenes están absolutamente llenas de “ruido” -exceptuando que tengamos una super cámara de vídeo de muy alta resolución-. Para decidir si dos píxeles son distintos, seremos algo “tolerantes”, y no miraremos que sus códigos de color sean exactamente iguales, sinó que tengan un cierto parecido. La permisividad en este parecido será el valor de esta variable. Si descargas el código y lo pruebas en tu máquina, deberás jugar con el valor de esta propiedad, incrementándolo y decrementándolo hasta alcanzar un nivel adecuado para la iluminación del espacio en el que te encuentres (en la próxima versión puede que incluya un calibrador automático).
  • zonaSensor: Define el área de pantalla que se analizará para combprobar las diferencias en los colores.

Explicado todo esto… ya podemos ver el código del método Update de la clase SensorColores, que donde está lo realmente interesante. Este método será llamado a su vez por el método Update de la clase Game.

El primer punto destacado es este método:

Color[,] colors2D = TextureTo2DArray(color, textura);

Lo que hacemos es pasar la información de la textura, que es un array unidimensional (color) a un array de dos dimensiones. Vosotros diréis lo que os parezca, pero a mi me es más fácil tratar un array en dos dimensiones cuando tengo que referirme a posiciones X e Y de una textura. Lo realmente importante del algoritmo es que estamos transformando sólo los píxels de interés para el análisis, es decir, aquellos que se encuentran en la zona del “sensor”, y además aquellos que se encuentran afectados por el factor de “densidad” (esto se puede observar en el incremento de las variables al final de cada bucle while).

private Color[,] TextureTo2DArray(Color[] color, Texture2D textura)
{
    
Color[,] colors2D = new Color[textura.Width, textura.Height];
    
int p = zonaSensor.X;
    
int q = zonaSensor.Y; 

    // Incrementamos segn la densidad de la zona, para no perder rendimiento
   
while
  
 (p < (textura.Width – 1) – densidadZona)
    {
       
while
        (
q < (textura.Height – 1) – densidadZona)
        {
            colors2D[p, q] = color[p + q * textura.Width];
            q += densidadZona;
        }
        p += densidadZona;
    }
   
return colors2D;

Finalmente entramos en el “core” del algoritmo. Un aviso: este algoritmo me lo he inventado yo, no puedo garantizar ni que sea “el método” de hacer esto (puede que existan otras formas de hacerlo) ni tampoco el mejor  -si existen otras formas, es posible que alguna de ellas sea mejor 🙂

 // Solo entramos en el bucle si hemos pasado por un frame previamente,
 
// sin no tiene sentido hacer los clculos
if (colors2DPrevio != null)
{
   
int pixelsCambio = 0;
   
int pixelsIguales = 0;
   
int xMin = zonaSensor.X;
   
int xMax = zonaSensor.X + zonaSensor.Width;
    int yMin = zonaSensor.Y;
   
int yMax = zonaSensor.Y + zonaSensor.Height;

    // Recorremos los pxels del sensor en busca de cambios
   
int x = zonaSensor.X;
   
int y = zonaSensor.Y;

    while
   (x < (xMax – 1) – densidadZona)
   {
        
while
       
(y < (yMax – 1) – densidadZona)
        {
           
int color1 = Math.Abs((int)colors2D[x, y].PackedValue);
           
int color2 = Math.Abs((int)colors2DPrevio[x, y].PackedValue);
           
           
// Si superamos la tolerancia al cambio… incrementamos el nmero de pxels con cambio
          
if (Math.Abs(color1 – color2) > toleranciaCambio)
           {
                pixelsCambio++;
           }
          
else
          
{
               pixelsIguales++;
           }

           // Incrementamos Y segn la densidad definida
         
y += densidadZona;
       }

   // Incrementamos X segn la densidad definida
   x += densidadZona;
   }

    // Si hay ms pxeles distintos que iguales, entonces dedidimos que hay un cambio
    if(pixelsCambio > pixelsIguales)
    {
        hayCambio =
true;
        primitiva.Colour = 
Color.Red;
    }
    else
    {
        hayCambio =
false;
        primitiva.Colour =
Color.White;
    }
}

// Finalizado el proceso… guardamos los datos en coloresPrevios
if (colors2DPrevio == null && tiempo >= 1000)  
{
    colors2DPrevio =
new Color[colors2D.GetLength(0), colors2D.GetLength(1)];
}

Aquí os dejo un vídeo de como quedaría todo:

[View:http://www.youtube.com/watch?v=fKZ1Vsbi7pM:550:0]

 Nota del vídeo: El vídeo aparece en blanco y negro, eso es porqué en el momento de grabar el vídeo hice unas pruebas con un shader de escala de grises. En la versión de código que os podéis descargar la imagen de la webcam aparece en color.

Como habréis observado, más que detección de movimiento, lo que hago en este experimento sería detectar los cambios de color o los cambios en la imagen desde “fotograma actual” – N al “fotograma actual”. Y nada… en el próximo “episodio” haremos lo más fácil, pero bonito a la vez, mover o rotar un objeto en la pantalla en función de los movimientos que haga el jugador delante de la cámara. Además prometo no salir en el vídeo 😛

8 comentarios en “[XNA] Detección de movimientos con una webcam (I de II)”

  1. Ja no me’n recordava del redundant que arriba a ser el C# però ta interessant el tema.

    Més que realitat virtual això té aplicacions en la realitat augmentada (s’estan fent veritables avenços en aquest camp a més d’estar molt interessant).

    Has pensat en posar una segona webcam per capturar la profunditat i poder detectar moviments en 3D? Potser estarà en l’entrega III?

    Saluts, ta guapo el blog

  2. Tendré que rebuscar por donde puse las webcams para probarlo, por el poco tiempo que tengo últimament, creo que esperaré al capítulo 2 para abrir un nuevo frente de investigación :).

    Gràcies!

  3. Hola Saludos.

    Muchisimas gracias por este, me ayudara mucho para poder crear juegos para el nuevo sistema de xbox Kinect. Aunque mi pregunta es,
    Donde yo puedo encontrar estos archivos : using Emgu.CV.CvEnum y Emgu.CV?

    que no me aparecen.

    Espero que me puedas ayudar, por favor…

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *