E ai filhotes, sussa?

Agora vamos começar a fazer coisas mais interessantes com o Kinect, claro que isso também significa que nossos posts estarão um pouco mais complexos… mas não se preocupem! Pois eu vou tentar simplificar o máximo possível!

Não tão simples assim…

Tutorial

Nível: Intermediário

Objetivo: Mostrar a distancia de cada ponto na frente do kinect.

Vídeo:

Introdução

A câmera de profundidade é uma das mais usadas do Kinect e se você quer virar um programador super sayajin para Kinect é fundamental que você saiba como ela funciona. O Kinect é composto (dentre outras coisas) por um emissor de infra vermelho e por um sensor que lê esses raios infra vermelhos. São esses camaradas que fazem o Kinect “ver em 3D”:

Então o que o depth irá nos retornar exatamente? Bom, o depth “retorna pixels que contêm a distância (em milímetros)  a partir do plano da câmera e o jogador”. Mas o que isso significa? Que cada ponto que o depth retorna traz a profundidade do objeto e o Kinect (em mm), alem do numero do jogador (se for parte de um jogador).

Vamos a um exemplo mais pratico então! Usando o exemplo depth basics do Kinect developer toolkit vamos ter esse resultado:

Leve-me a seu líder.

Os objetos mais perto do Kinect vão ficando pretos enquanto mais longe vão ficando brancos. Isso só é possível porque cada pontinho (pixel) desses vai ter uma informação de posicionamento (X,Y e Z) e com isso eu mudo a cor de todos dependendo da distância.

Criando o aplicativo

É agora que o bicho pega!  Vamos dar uma olhada no passo a passo de como fazer um aplicativo assim. Primeiro passo é criar um projeto no Visual Studio 2010 ( no meu caso vou criar um WPF ). Lembrando que tem que ser .NET 4.0. E depois vou adicionar a referência da biblioteca Microsoft.Kinect ao meu projeto (tudo isso já fizemos no artigo anterior).

Começando a programar

O primeiro passo vai ser adicionar uma imagem no XAML e o evento do window_closing (ou unloaded):

<Window x:Class="CameraDepth.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="480" Width="640" Closing="Window_Closing">
    <Grid>
        <Image Height="440" HorizontalAlignment="Left" Name="imgDepth" Stretch="Fill" VerticalAlignment="Top" Width="620" />
    </Grid>
</Window>

E agora no codebehind vamos pegar o kinect ativo e já ativar a câmera de profundidade (depthstream):

        public MainWindow()
        {
            InitializeComponent();

            // Verificamos quantos sensores estão conectado
            if (KinectSensor.KinectSensors.Count > 0)
            {
                // Pega o sensor conectado
                sensor = KinectSensor.KinectSensors[0];

                // Habilita a camera de profundidade
                sensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);

                // Associo o evento ao AllFrameReady
                sensor.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(sensor_AllFramesReady);

                // Inicio o sensor
                sensor.Start();
            }
        }

E só para lembrar já vamos implementar o evento windows_closing:

 private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            // Se existir o sensor, para ele
            if (sensor != null)
            {
                sensor.Stop();
                sensor.Dispose();
            }
        }

Ótimo! Tudo normal ate agora né? Só falta nosso evento AllFramesReady agora! Bom… É aqui que o bicho pega ( só um pouquinho):

        /// <summary>
        /// Cada vez que o kinect detectar uma imagem ele passa por aqui
        /// </summary>
        void sensor_AllFramesReady(object sender, AllFramesReadyEventArgs e)
        {
            // Pegamos o DepthFrame
            using (DepthImageFrame dframe = e.OpenDepthImageFrame())
            {
                // Caso não venha nenhum stream sai do método
                if (dframe == null)
                    return;

                // Gera os bytes coloridos bonitinhos para a nossa imagem
                byte[] pixels = GerarBytesDeCor(dframe);

                // Numero de bytes por linha width * 4 (R,G,B, Vazio)
                int stride = dframe.Width * 4;

                // Cria a imagem passando o tamanho da imagem (640x480) o dpi (to passando um padrão de 96x96) o formato (32bits)
                // a paleta (nula) o array de bytes da imagem (bytesp) e o stride
                imgDepth.Source = BitmapImage.Create(640, 480, 96, 96, PixelFormats.Bgr32, null, pixels, stride);
            }
        }

Então vocês podem reparar que tem algo diferente nesse método, no ultimo artigo nós usávamos o CopyPixelDataTo mas agora em vez disso estamos chamando um método que criamos… então, o que mudou?

Depth não retorna os nossos 32 bits

O motivo de eu não fazer do mesmo jeito que antes é porque o depth não me retorna uma imagem de 32 bits, bom na verdade ele quase não me retorna imagem nenhuma. “Quase”. Bom, veja bem (e essa é uma parte complicada)  o depth me retorna uma imagem de 16bits (cada pixel tem 16bits) em branco e nós que colorimos ela baseada na proximidade de cada ponto.
Certo… e como nós pegamos a proximidade do ponto?  Bom ai que ta! Ele esta “embutido” nos bits do ponto junto com o jogador a qual (se) pertence!

É… nem queria programar para kinect mesmo.

Calma filhão! Só parece complicado mas na verdade é muito fácil (mentira!*).
Vamos montar a nossa função GerarBytesDeCor parte por parte e você vai entender tudo que eu falei ai em cima:

* É fácil mesmo. 

        /// <summary>
        /// Função que convertera o resultado do depth para RGB
        /// </summary>
        /// <param name="dFrame"></param>
        /// <returns></returns>
        private byte[] GerarBytesDeCor(DepthImageFrame dFrame)
        {
            // tamanho do frame do Depth
            // cada ponto tem 16bits
            short[] rawDepthData = new short[dFrame.PixelDataLength];

            // Copia a informação para o nosso raw
            dFrame.CopyPixelDataTo(rawDepthData);

            //Nosso array de byte que sera a imagem
            //Height x Width x 4 (Red, Green, Blue, vazio)
            Byte[] pixels = new byte[dFrame.Height * dFrame.Width * 4];

Então, essa é a primeira parte da função, o importante aqui é que o CopyPixelDataTo do depth retorna um array de short e não de bytes como a do RGB, porque isso? Bom ai chega a parte que eu expliquei la em cima, para cada ponto/pixel que o depth detectar ele vai retornar esses 16 bits (tamanho do short), entendeu?
Então se a resolução for 80×80 ele vai retornar 6400 pontos e cada um com 16 bits de informação. Agora vai a bomba: os 13 últimos bits representam a profundidade do ponto enquanto os 3 primeiros representam o jogador. Como o Kinect só reconhece 6 pessoas você só precisa de 3 bits ( 00000111 = 7) para poder representar essa quantidade.

Então beleza, vai ficar ainda mais claro no código. Vamos ver o resto da função:

            //Nosso array de byte que sera a imagem
            //Height x Width x 4 (Red, Green, Blue, vazio)
            Byte[] pixels = new byte[dFrame.Height * dFrame.Width * 4];

            // Posição das cores
            int colorPixelIndex = 0;

            // Nossa profundidade
            short depth = 0;

            // Variaveis que vamos jogar os valores das cores
            byte blue, red, green;

            // Um loop para converter cada ponto
            for (int i = 0; i < rawDepthData.Length; ++i)
            {
                // Pega só a parte que contem a informação da profundidade
                depth = (short)(rawDepthData[i] >> DepthImageFrame.PlayerIndexBitmaskWidth);

                // Apaga os valores
                blue = 0;
                red = 0;
                green = 0;

                // 90 CM
                if (depth <= 900)
                    blue = 255;

                // 90 cm a 2 metros
                else if (depth > 900 && depth < 2000)
                    green = 255;

                // maior que 2 metros
                else if (depth > 2000)
                    red = 255;                

                // Escreve o azul
                pixels[colorPixelIndex++] = blue;

                // Escreve o verde
                pixels[colorPixelIndex++] = green;

                // Escreve o vermelho
                pixels[colorPixelIndex++] = red;

                //O ultimo byte é vazio
                ++colorPixelIndex;
            }            

            return pixels;

Agora, está vendo essa linha aqui?

depth = (short)(rawDepthData[i] >> DepthImageFrame.PlayerIndexBitmaskWidth);

O operador “<<”  (Shift) serve para deslocar os bits para esquerda ou direita ( << ou >>), e o DepthImageFrame.PlayerIndexBitmaskWidth é uma constante com o valor de 3. Então fazemos isso para mover os bits 3 casas para esquerda, ignorando assim os 3 bits do jogador e só pegando o da profundidade.

E depois eu converto o valor da profundidade para short.
Depois disso basta verificar a distância, lembrando que o resultado é em milímetros (1000mm = 1 metro). Mas e as cores e esse colorPixelIndex ?
Como nos vamos converter para uma imagem 32BIT, temos que preencher as cores (RGB). Alias nossa imagem vai ser BGR32 e cada pixel é formato por 32 bits (4 bytes): azul, verde, vermelho e vazio. E por isso que temos o colorPixelIndex ele vai ser o nosso contador e cada volta no loop ele aumenta em 4 preenchendo todos os bytes coloridos e vazios.

Então, pronto! Basta você executar e o resultado será algo assim:

Pois é, só fico na mesma posição

Vou adicionar o vídeo o mais rápido possível, seguindo o mesmo modelo do outro.

Espero que tenham gostado, até mais.

[EDITADO]

Segue a coleção completa de artigos (até agora):

  1. Camera RGB
  2. Camera de Profundidade
  3. Inclinação
  4. Detectando Esqueletos