Reto Kinect: Usar las cámaras del sensor

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” https://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: https://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