MSDN España

Noticias, avisos, reflexiones... del equipo que hay detrás de MSDN en España... también tendremos alguna que otra firma invitada :)

Reto Kinect: Usar las cámaras del sensor

Reto Kinect: Usar las cámaras del sensor

Rate This
  • Comments 14

Imprescindible

Descargar el SDK de Kinect para Windows y tener alguna versión de Visual Studio 2010. Te puedes bajar la versión gratuita de Visual Studio 2010 Express o de la Ultimate Trial.

Para programar con el SDK utilizaremos la herramienta de desarrollo Visual Studio 2010 y Windows 7. Para ver más información sobre requerimientos previos y qué se puede hacer con la SDK de Kinect visita el post referente al SDK de Kinect en nuestro blog de MSDN.

Estamos atentos a tus comentarios en el twitter de @esmsdn y en el hashtag #retosmsdn.

Funcionamiento de las cámaras

Kinect utiliza una cámara RGB que obtiene imágenes en color y 2 cámaras de infrarrojos para medir la distancia a la que se encuentran los elementos que están en el campo de visión.

Gracias al SDK de Kinect para Windows podemos obtener los datos de las cámaras y trabajar con ellos para utilizarlos en nuestras aplicaciones.

Las imágenes que se obtienen del sensor se codifican en un vector de bytes. Para entender esta codificación hay que saber primero como se estructura una imagen.

Una imagen se compone de un conjunto de píxeles. Cada pixel de la imagen tiene 4 componentes que representan los valores de los colores rojo, verde y azul más una componente que corresponde con el valor de transparencia (alfa), en el caso de imágenes RGBa, o un valor vacío, si es de tipo RGB.

image

Cada componente del píxel tiene un valor decimal de 0 a 254 lo que corresponde a un byte. De esta forma el vector de bytes que obtenemos del sensor, en el caso de la cámara RGB, es una representación de esos píxeles organizados de arriba abajo y de izquierda a derecha donde los 4 primeros elementos del vector serán los valores rojo, verde, azul y alfa del píxel de arriba a la izquierda mientras que los 4 últimos serán del píxel de abajo a la derecha.

image

Cuando queramos usar las cámaras de profundidad el procedimiento varía. Al igual que con la cámara RGB también obtendremos un vector de bytes pero en esta ocasión esos bytes no corresponden con los valores de los componentes de un píxel sino con la distancia del píxel al sensor.

Al tener 2 cámaras de infrarrojos cada píxel se corresponde con 2 bytes en el vector siendo éstos el valor de la distancia de ese píxel a cada cámara. La organización de los píxeles es la misma que con la cámara RGB, los 2 primeros bytes es la distancia del píxel de la posición de arriba a la izquierda al sensor y los 2 últimos son del píxel de abajo a la derecha.

image

Luces, cámara y ¡acción!

Una vez hemos explicado cómo se codifica la información obtenida de las cámaras vamos a crear una aplicación para dar nuestros primeros pasos en el uso de las cámaras del sensor. Lo primero va a ser crear nuestra aplicación WPF con Visual Studio 2010 con la estructura básica para poder usar el SDK de Kinect. Esto viene descrito en nuestro anterior reto en la sección “Preparar proyecto” http://blogs.msdn.com/b/esmsdn/archive/2011/07/07/sdk-de-kinect-desarrolla-con-kinect.aspx.

Obtener datos de la cámara RGB

En esta parte vamos a obtener los datos de la cámara RGB del sensor y mostrar las imágenes capturadas en nuestra aplicación. Para mostrar la imagen añadiremos a la ventana principal un elemento tipo Imagen para enlazar la imagen que obtenemos del sensor.

<Image Name="image1" Height="480" Width="640" Margin="12,124,350,157"/>

En el evento Loaded, donde inicializamos el sensor, activaremos la cámara de vídeo con las siguientes líneas de código.

Runtime kinect = new Runtime();

kinect.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);

kinect.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);

El método Open de la propiedad VideoStream permite abrir el flujo de la cámara de vídeo pasándole una serie de parámetros. En primer lugar el tipo de del flujo que se abre (Video, Depth o Invalid), la resolución de la imagen y el tipo de imagen (Color, Depth, DepthAndPlayerIndex, ect.).

Lo siguiente es crear el evento para capturar el frame que devuelve el flujo de la cámara cuando esté listo. De esa forma podremos obtener la imagen codificada, como hemos explicado anteriormente, a partir del frame.

kinect.VideoFrameReady += new EventHandler<ImageFrameReadyEventArgs>(kinect_VideoFrameReady);

Al crear este evento se nos genera el método kinect_VideoFrameReady. Ese método sirve para obtener el frame desde el que podremos extraer el objeto tipo PlanarImage que será con el que trabajaremos en nuestra aplicación. El PlanarImage lo utilizaremos para crear un BitmapSource con el método Create pasando a éste como parámetros la anchura y altura de la imagen, el dpi, el formato de píxeles de la imagen (brg32, brga32, blackWhite…), la representación en bytes de la imagen y por último el stride.

El resultado de este método lo enlazaremos con el origen de datos del elemento Imagen que nos hemos creado en la ventana principal, así que cada vez que el sensor capture una imagen se activará el evento kinect_VideoFrameReady que obtendrá la imagen, creará un BitmapSource por medio del PlanarImage y actualizará la imagen que se tiene que mostrar en la ventana de nuestra aplicación.

void kinect_VideoFrameReady(object sender, ImageFrameReadyEventArgs e)

{

PlanarImage Image = e.ImageFrame.Image;

image1.Source = BitmapSource.Create(Image.Width, Image.Height, 96, 96, PixelFormats.Bgr32, null, Image.Bits, Image.Width * Image.BytesPerPixel);

}

Los datos de profundidad a fondo

Antes de ver cómo obtener los datos de la cámara de profundidad y trabajar con ellos vamos a explicar un poco mejor cómo están codificados los datos que obtenemos del sensor.

Los datos de la cámara de profundidad nos sirven para saber cómo de lejos se encuentra un píxel del sensor con lo que tendremos que calcular la distancia a partir de esos datos. Para calcular la distancia debemos de realizar una serie de operaciones dependiendo del tipo de imagen de profundidad que hayamos elegido.

Existen 2 tipos: Depth y DepthAndPlayerIndex.

Cuando elegimos Depth simplemente tenemos los datos de profundidad mientras que con DepthAndPlayerIndex a parte de la profundidad tenemos 3 bits que hacen referencia al índice del jugador. Así sabremos si ese píxel corresponde a parte del cuerpo de un jugador o no.

Para calcular la distancia de un píxel en una imagen tipo Depth hay que hacer una operación lógica OR con los bytes correspondientes al píxel realizando antes un desplazamiento de 8 bits en el segundo byte.

Distancia (0,0) = (int)(Bits[0] | Bits[1] << 8);

En el caso de tener una imagen tipo DepthAndPlayerIndex el procedimiento cambia un poco debido a que los 3 primeros bits corresponden al índice del jugador y no a la distancia, con lo que tendremos que realizar un desplazamiento en el primer byte para hacer la OR sólo con los datos de distancia y reducir el desplazamiento del segundo byte.

Distancia (0,0) =(int)(Bits[0] >> 3 | Bits[1] << 5);

El rango de distancias que acepta el sensor es de 850 mm a 4000 mm.

Obtener datos de profundidad

Una vez que hemos entendido mejor la codificación de los datos que obtenemos del sensor y cómo calcular la distancia dependiendo del tipo de imagen de profundidad que tengamos vamos a coger los datos de profundidad y traducirlos para poder mostrarlos en nuestra aplicación. Esta traducción va a consistir en crear un rango de colores dependiendo de la distancia. Serán 3 rangos de colores: azul (muy cerca del sensor), verde (distancia media) y rojo (puntos lejanos).

Lo primero es añadir a nuestra ventana principal un elemento Image para mostrar la imagen traducida que vamos a crear.

<Image Name="image2" Height="240" Width="320" Margin="658,110,24,411" />

Al igual que con la cámara RGB tenemos que abrir el flujo de datos y crear el evento para capturarlos.

kinect.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.Depth);

kinect.DepthFrameReady += new EventHandler<ImageFrameReadyEventArgs>(kinect_DepthFrameReady);

Lo siguiente es obtener la PlanarImage, traducir los datos y crear el BitmapSource para enlazarlo con la imagen.

void kinect_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)

{

PlanarImage Image = e.ImageFrame.Image;

byte[] convertedFrame = convertDepthFrame(Image.Bits);

image2.Source = BitmapSource.Create(Image.Width, Image.Height, 96, 96, PixelFormats.Bgr32, null, convertedFrame, Image.Width * 4);

}

Lo que va a diferenciar este código del realizado anteriormente es la función convertDepthFrame, que va a recibir los datos de profundidad y los va a convertir en datos de imagen RGB para poder mostrarlos.

Tendremos un vector, que será el vector resultado, con el tamaño de la imagen a mostrar. En este caso queremos mostrar una imagen de 320x240 y esto lo multiplicaremos por 4 (ya que cada píxel tendrá 4 componentes, RGB).

byte[] depthFrame32 = new byte[320 * 240 * 4];

byte[] convertDepthFrame(byte[] depthFrame16)

{

Lo siguiente es ir recorriendo el vector de datos de profundidad calculando la distancia. Hay que tener en cuenta que este vector se recorre de 2 en 2 elementos (i16+=2) y el otro de 4 en 4 (i32+=4).

for (int i16 = 0, i32 = 0;i16<depthFrame16.Length && i32<depthFrame32.Length;i16+= 2,i32+=4)

{

//desplazar el segundo byte 8 posiciones y hacer la OR con el primer byte

int distance = ( (depthFrame16[i16]) | (depthFrame16[i16+1] << 8));

En la variable distance tenemos la distancia de cada pixel al sensor y con ella iremos creando los datos en la variable depthFrame32. Usando distance crearemos un píxel de color azul si la distancia es menor de 900, verde si está entre 900 y 2000 y rojo si es mayor de 2000.

if (distance <= 900)

{

depthFrame32 [i32 + RED_IDX] = 0;

depthFrame32 [i32 + GREEN_IDX] = 0;

depthFrame32 [i32 + BLUE_IDX] = 255;

}

else if (distance > 900 && distance < 2000)

{

depthFrame32 [i32 + RED_IDX] = 0;

depthFrame32 [i32 + GREEN_IDX] = 255;

depthFrame32 [i32 + BLUE_IDX] = 0;

}

else if (distance > 2000)

{

depthFrame32 [i32 + RED_IDX] = 255;

depthFrame32 [i32 + GREEN_IDX] = 0;

depthFrame32 [i32 + BLUE_IDX] = 0;

}

Habremos declarado anteriormente las 3 constantes que utilizamos como índice para hacer referencia a la posición del vector correspondiente con ese color.

const int BLUE_IDX = 0;

const int GREEN_IDX = 1;

const int RED_IDX=2;

Por último devolvemos el valor depthFrame32 que se utilizará para crear el BitmapSource y mostrar la imagen en la ventana principal de nuestra aplicación.

}

return depthFrame32;

}

Tu turno… ¡el Reto! – Identifícate en amarillo

Ya sabes cómo obtener los datos de distancia del sensor. Como reto te proponemos que utilizando el tipo de imagen depthAndPlayerIndex añadas al código anterior la funcionalidad de mostrar los píxeles que correspondan con un jugador en color amarillo. Acuérdate de utilizar la función correcta para calcular la distancia.

Pista: int jugador=depthFrame16[i16]&0x07;

Recursos: Post sobre Kinect en el blog de MSDN, documentación sobre el SDK de Kinect y centro de desarrollo de WPF.

Solución: http://msdn.microsoft.com/es-es/windows/hh487018

Participa: twitter de @esmsdn o hashtag #retosmsdn.

¡A Kinectear!

José Perona – @JVPerona - Developer Evangelist Jr.

 

Ver el primer Reto SDK de Kinect: Desarrolla con Kinect

Ver el tercer Reto SDK de Kinect: Detectar posturas con Skeletal tracking

Ver el cuarto Reto SDK de Kinect: Reconocer gestos con Skeletan tracking

Leave a Comment
  • Please add 4 and 7 and type the answer here:
  • Post
  • Hola a todos!

    Tengo una pregunta relacionada con lo visto en el reto: ¿Que diferencia hay, o que ventajas tiene utilizar uno u otro de los siguientes metodos, para mostrar en un componente "imagen" de la interfaz, la imagen que captura la camara?

    image1.Source = BitmapSource.Create(Image.Width, Image.Height, 96, 96, PixelFormats.Bgr32, null, Image.Bits, Image.Width * Image.BytesPerPixel);

    --> Frente a:

    image1.Source = e.ImageFrame.ToBitmapSource();

    Un saludo.

  • "ToBitmapSource()" es una extensión añadida al utilizar el "Coding4Fun Kinect Toolkit" que se puede descargar desde aquí http://c4fkinect.codeplex.com

    Ese método es igual que llamar a "Create" con los parámetros que vienen en el ejemplo. No hay ninguna diferencia o no debería.

    La ventaja de utilizar "Create" es que lo puedes personalizar. Puedes poner otro formato de píxel, etc. También puedes usarlo con cualquier vector de bytes, mientras que "ToBitmapSource()" sólo lo puedes utilizar con ImageFrame.

    La ventaja de "ToBitmapSource()" es que te ahorras código :D

    Un saludo!

  • Una duda más:

    Tengo algunas dudas sobre el parámetro "stride", sobre para que sirve y el porque al crear una imagen normal es:

    Image.Width * Image.BytesPerPixel

    ...y en una imagen Depth es:

    Image.Width * 4

    Un saludo y gracias.

  • Hola!

    Como he explicado en el reto cada píxel de las imágenes RGB ocupan 4 bytes, mientras que las imágenes que obtenemos de la Depth ocupan 2 bytes.

    El Stride es lo que va a ocupar una fila de píxeles en memoria (en bytes). Entonces, tenemos el ancho de la imagen (número de píxeles por fila) y lo que ocupa cada píxel en bytes (Image.BytesPerPixel=4).

    En la parte del Depth no podemos poner Image.BytesPerPixel ya que esa PlanarImage que obtenemos no son 4 bytes por píxel, son 2. Por ello al generar la nueva imagen RGB tenemos que poner 4.

    ¿Me he explicado bien? :D

    No dudes en comentarnos todas las dudas que tengas.

    Un saludo y muchas gracias por tu participación.

    Resumiendo, el stride es el número de píxeles de una fila multiplicado por lo que ocupe cada píxel en bytes.

  • Hola,

    Bueno creo que ya tengo una solución. Aún así tengo una pregunta:

    El valor del "player" varia entre 6 valores,¿que patrón utiliza Kinect para detectar si es el jugador 1, el 3 o el 4?, es decir, ¿por qué unas veces detecta que es ha entrado el jugador 1, otras el 3, etc ?.

    ¿lo hace aleatoriamente de la colección de Skeleton que puede detectar?

    Para terminar, solamente anotar para que sirva de ayuda a otros Kinecticos, los valores de inicialización de las constantes de color:

               const int BLUE_IDX = 0;

               const int GREEN_IDX = 1;

               const int RED_IDX=2;

    Un saludo y muchas gracias José ;)

  • Gracias por el apunte de las constantes! He editado el post para ponerlo.

    Al hacer Skeletal Tracking obtenemos un vector de 6 elementos con datos de esqueletos. El indexplayer corresponde con una posición en ese vector, así que hay relación entre la imagen depth y el vector de esqueletos.

    Si el indexplayer de un conjunto de píxeles es igual a 1 tendremos en la posición 0 del vector de esqueletos el que corresponde con el jugador detectado en esos píxeles.

    Lo que no sabemos es qué criterio se sigue para la asignación de posiciones en el vector de esqueletos.

    Hablaremos sobre el Skeletal Tracking en el próximo reto :D

  • Hola

    Yo estoy comenzando a trabajar con el kinect  , ya entendiendo que me entrega el kinect quiero saber cada cuanto me entrega esos datos de profundidad  , o si es por demanda (cada que yo se lo pida) , también he estado leyendo y entiendo que el kinect cuando ya tiene la imagen de profundidad la segmenta en partes (los puntos de interes) mi pregunta es si uno trabaja con cada punto  por separado o hace el reconocimiento según la posición del conjunto de puntos y también como hace la unión de estos puntos .

  • hola alguien sabe como hacer que kinect solo reconozca solo uno y el primer esqueleto??, es que cuando pasa alguien cerca el reconosimiento se pasa a la otra persona

  • Hola a todos.

    Quisiera preguntar si alguien me puede explicar por que la imagen se ve, como si fuese en espejo no se ve normal gracias por la atención prestada de ante mano

  • Hola.

    ¿Donde se encuentran las imagenes que utilizas en el reto? ¿Son públicas?

  • Hola Omar,

    Con la versión anterior del SDK solo identificaba un esqueleto, por ello cuando entraba otra persona en pantalla te reconoce a esa nueva persona y deja de detectar a la anterior. Ahora ya detecta 2 esqueletos.

    No puedes elegir qué personas quieres detectar, el algoritmo identifica a las personas que encuentra.

  • Hola Jose,

    No sé a qué imágenes te refieres.

    ¿Puedes especificar un poco más?

    Jose Perona - Development Advisor en Plain Concepts

  • Hola José quisiera preguntarte si de casualidad tienes información de como utilizar EMGUcv con el SDK ya que he intentado pero no he logrado utilizarlo.

    Un saludo

  • bueno a mi me parecio muy tonto putos y a la vez estupido jjajajaj

Page 1 of 1 (14 items)