Back-End

22 abr, 2015

Como identificar gestos com o Intel SDK RealSense e C#?

Publicidade

Você já pensou em criar um programa para reconhecer gestos através de uma câmera integrada à sua aplicação, de forma que as ações da app são disparadas através de gestos com as mãos? Este é o objetivo deste artigo: explorar passo a passo tudo o que é preciso para um desenvolvedor C# iniciar a codificação para este fantástico mundo do RealSense.

Os pré-requisitos para o artigo são: Visual Studio .NET 2013 com o Update 3 ou 4, uma câmera 3D Intel® RealSense F200 e instalar o SDK Intel RealSense.

A primeira coisa a fazer é entender o que é o mundo RealSense, apps que você pode criar, alguns showcases e uma visão geral. Tudo isto está disponível neste link. O SDK é um conjunto de bibliotecas e exemplos com documentações que você encontrará para várias tecnologias como C++, C#, Unity, JavaScript e Java. É só fazer o download e instalar. Vale a dica que você precisa ter a câmera para que o SDK já a reconheça no momento da instalação, isto tudo evitará problemas de reconhecimento do device.

Neste projeto, vamos criar desde o início uma Console Application em C#, adicionar todas as referências necessárias das bibliotecas do SDK, criar códigos para identificar a câmera, ativar o módulo da mão, verificar se a câmera reconhece a mão esquerda e quais gestos a mão está fazendo.

Projeto

Para criar o projeto, abra o Visual Studio, selecione o menu File / New Project (Ctrl + Shift + N), e na janela aberta, conforme a Figura 1, selecione a linguagem Visual C#, o template de Console Application. Coloque o nome do projeto como ArticleGesture e você pode gravar em qualquer pasta que desejar. Clique no botão OK e aguarde o VS criar o projeto.

IC792751
Figura 1 – Novo projeto de Console App

 

Como você já instalou o SDK Intel RealSense, abra a pasta C:\Program Files (x86)\Intel\RSSDK\bin\x64 e veja que há diversas DLLs. Iremos precisar apenas de duas: libpxcclr.cs.dll e libpxccpp2c.dll.

No projeto do VS vamos adicionar apenas a referência à libpxcclr.cs.dll. Sendo assim, no Solution Explorer selecione Add / Reference, aponte para a pasta citada acima, selecione a libpxcclr.cs.dll conforme a figura 2 e clique no botão OK.

Figura 2 – Adicionar referência
Figura 2 – Adicionar referência

Em seguida, no Windows Explorer, selecione a libpxccpp2c.dll e arraste para dentro do Solution Explorer do projeto. No entanto, abra a janela de propriedades (F4) e configure a propriedade “Copy to Output Directory” para “Copy Always”, conforme a figura 3. E qual a explicação disto? A biblioteca libpxcclr é escrita em C# e é apenas um wrapper para a libpxccpp2c, que está escrita em C++. Sendo assim, quando você fizer o deploy do projeto, a libpxccpp2c estará sempre presente fisicamente na pasta o qual a aplicação for instalada.

IC792752
Figura 3 – Propriedade Copy Always da biblioteca

 

Pronto, basicamente precisamos apenas destas duas e assim já temos acesso a todos os módulos que precisamos. E como você é um desenvolver pesquisador, que tal clicar com o botão direito na libpxcclr e selecionar View in Object Browser. Veja na Figura 4 que a classe PXCMHandData já mostra tudo o que precisamos para manipular as mãos e é justamente esta classe que iremos explorar nos exemplos.

IC792728

Códigos C#

Como é uma aplicação de Console, a estrutura básica do Program.cs está descrita abaixo, contendo a classe Program e o método Main. Justamente no comentário códigos é onde você deverá adicionar todo e qualquer código C#.

using System;

namespace ArticleGesture
{
    class Program
    {
        static void Main(string[] args)
        {
            // códigos
        }
    }
}

A primeira coisa é referenciar uma sessão através da classe PXCMSession e do método CreateInstance. Mas o que é uma sessão? O SDK foi construído em módulos de I/O e algoritmo que implementam as interfaces SDK. Sendo assim, entendemos que uma sessão SDK é o contexto com os módulos. É possível criar uma única ou múltiplas sessões SDK, sendo que cada sessão mantém o seu próprio contexto de I/O e modelos de algoritmo.

Uma sessão não tem conhecimento de outras sessões, ou seja, o ciclo de vida do módulo é a sua duração, afinal você irá criar, usar e destruir a instância do módulo. Esta sessão deve ser a primeira instância a ser criada antes de qualquer operação do módulo e depois você tem que destruí-la (Dispose).

static void Main(string[] args)
{
    //1 - criar uma session
    PXCMSession session = PXCMSession.CreateInstance();
}

Em seguida, é preciso criar o SenseManager através do método CreateSenseManager da classe PXCMSenseManager. Ele é o responsável para se conectar diretamente com a câmera e processar ações como a análise do rosto e reconhecimento de voz. Se você pensar que a câmera tem 30 frames por segundo, alguém tem que ler os frames para se tomar algumas ações e o SenseManager é que faz isto. Geralmente você precisa configurar o pipeline, inicializar, executar e disparar callbacks e/ou delegates.

static void Main(string[] args)
{
    //1 - criar uma session
    PXCMSession session = PXCMSession.CreateInstance();

    //2 - criar manager (responsável por adquirir os frames da leitura da câmera)
    PXCMSenseManager manager = session.CreateSenseManager();
}

Em seguida, é preciso dizer ao manager o que você quer controlar. No nosso caso será a mão, então, nada mais justo que ativar o módulo EnableHand. Você tem diversas opções, por exemplo, Enable3Dscan, Enable3Dseg, EnableEmotion, EnableFace, EnableModule, EnableStream e EnableTracker.

static void Main(string[] args)
{
    //1 - criar uma session
    PXCMSession session = PXCMSession.CreateInstance();

    //2 - criar manager (responsável por adquirir os frames da leitura da câmera)
    PXCMSenseManager manager = session.CreateSenseManager();

    //3 - ativar módulo da mão
    manager.EnableHand();
}

Uma vez com o objeto da mão, você pode configurar algumas funções para fazer o tracking. Para isto é preciso instanciar o QueryHand do módulo PXCMHandModule. Quando o módulo não está ocupado processando os dados de entrada, temos dois momentos:

  • Setup: ocorre entre a chamada do EnableHand e do AcquireFrame, onde o QueryHand retorna a instância módulo permitindo aplicar configurações;
  • Leitura de dados: ocorre entre a chamada do AcquireFrame e do ReleaseFrame onde o QueryHand retorna uma instância de módulo válido, se concluiu o processamento do frame atual, ou seja, leu os dados.

O EnableGesture permite a notificação de eventos para um gesto específico. Há uma sobrecarga para este método, onde você pode informar se a notificação de eventos será contínua ou não. Se for false (padrão), ativa a notificação apenas no início e para no ponto do reconhecimento do gesto.

E que tal pensar em disparar um evento de acordo com o gesto? Quem faz isto é o SubscribeGesture, o qual você atribui um evento, no nosso exemplo será o método OnGesture. Vale dizer que o ciclo de vida ocorre até a chamada do UnsubscribeGesture.

O ApplyChanges aplica as configurações para o rastreamento. Não se esqueça deste método, senão as configurações não terão efeito algum.

//4 - pegar a instância da mão para fazer configurações antes de iniciar
PXCMHandModule handModule = manager.QueryHand();
using (PXCMHandConfiguration config = handModule.CreateActiveConfiguration())
{
    // permite a notificação de eventos
    config.EnableAllGestures();
    // define um evento para o gesto, o OnGesture
    config.SubscribeGesture(OnGesture);
    // deixa em alerta todas as notificações
    config.EnableAllAlerts();
    // aplica as configurações
    config.ApplyChanges();
}

O evento OnGesture atribuído no SubscribeGesture ainda não existe. Então, no Visual Studio, dê um CTRL + ponto (.) sobre o nome do método que ele é criado automaticamente. Resta-nos atribuir o bloco de código, conforme a seguir. Note que o parâmetro é do tipo GestureData, contendo tudo sobre o gesto. Com isto, você consegue capturar as seguintes informações: timestamp – o momento em 100ns que ocorreu o gesto; handId – o ID da mão; frameNumber – número do frame; state – estado do tracking (início, em progresso ou finalizado); name – nome da string do gesto.

private static void OnGesture(PXCMHandData.GestureData gesturedata)
{
    Console.WriteLine("Gesture: {0}-{1}-{2}",
        gesturedata.name,
        gesturedata.handId,
        gesturedata.state);
}

Logo após, vamos iniciar o manager e ao mesmo tempo já verificar o seu estado – se está ou não com erro. Para você não se perder no código, esta etapa 5 está logo abaixo do using da configuração. Veja no código que, caso dê erro será mostrada uma mensagem ao usuário.

//4 - pegar a instância da mão para fazer configurações antes de iniciar
PXCMHandModule handModule = manager.QueryHand();
using (PXCMHandConfiguration config = handModule.CreateActiveConfiguration()) {
    …
}
//5 – verifica e inicia o pipeline
if (manager.Init() != pxcmStatus.PXCM_STATUS_NO_ERROR) {
    Console.WriteLine("Error initing camera");                    
}

Lendo informações da mão

Agora vem o melhor de tudo: ler os dados da mão. Para isto é preciso instanciar o método CreateOutput do PXCMHandData. Em seguida, montar um looping While para rodar enquanto não der erro. Imagine que a câmera irá adquirir diversos frames e enquanto isto estiver ativo (true) ela estará lendo informações. O código está descrito a seguir, assim como a explicação detalhada.

O método principal aqui é o AcquireFrame, o qual o parâmetro true indica para aguardar até que todos os dados sejam concluídos (são as solicitações de I/O do módulo) para o frame atual. Agora, se durante a leitura houver algum pedido de leitura de módulos diferentes como cor, áudio ou profundidade, o método se encarrega de aguardar tais leituras de forma sincronizada. Se o parâmetro for false, o método aguarda os pedidos de I/O pendentes, pois pode ser que ocorram várias solicitações.

O AcquireFrame trabalha diretamente com o ReleaseFrame, afinal cada frame deve ser processado e liberado para continuar a leitura dos próximos frames. Então, como regra, use o ReleaseFrame o mais rápido possível, nada de processamentos demorados. Tudo o que estiver no meio destes dois métodos é o processamento que o frame irá realizar.

E no caso de um processamento de streaming ao vivo, como fazer? Sabemos que isto pode ser uma tarefa longa, então é possível que haja perda do frame. Você terá que efetuar testes e usar o SetRealtime (false).

O AcquireFrame tem os seguintes estados:

PXCM_STATUS_NO_ERROR – dipsositivo está conectado e disponível para ler dados;

PXCM_STATUS_EXEC_TIMEOUT – ocorreu o timeout antes de ler dados; PXCM_STATUS_DEVICE_LOST – dispositivo está desconectado;

PXCM_STATUS_EXEC_INPROGRESS – roda uma threading paralela para adquirir streaming;

PXCM_STREAM_CONFIG_CHANGED – a configuração do dispositivo foi alterada, é preciso reiniciar;

PXCM_STATUS_DEVICE_BUSY – o I/O do dipsositivo está sendo usado por outra aplicação.

Sendo assim, teremos um looping onde cada frame será executado o bloco de códigos. Veja que o objeto QueryHand do manager é testado se é diferente de nulo. Caso verdadeiro, o Update preenche o objeto com todos os dados da mão, ou seja, pense que um scanner lê a sua mão e obtém todos os dados.

Para isto, temos a classe PXCMHandData, o qual contém a interface IHand que gerencia os dados da mão rastreada. E já que temos duas mãos, você pode definir rastrear dados de apenas uma ou das duas. Quem define isto é o enumerador AccessOrderType do QueryHandData, sendo:

ACCESS_ORDER_BY_ID – captura os IDs das mãos;

ACCESS_ORDER_LEFT_HANDS – acessa somente a mão esquerda;

ACCESS_ORDER_RIGHT_HANDS – acessa somente a mão direita;

ACCESS_ORDER_NEAR_TO_FAR – ordem das mãos que vai da mais próxima para a mais distante na cena;

ACCESS_ORDER_BY_TIME – ordem do mais velho para o mais novo na cena;

ACCESS_ORDER_FIXED – se identificadas as mãos, pega pelo índice 0 ou 1.

Enfim, no nosso código vamos focar apenas na mão esquerda (ACCESS_ORDER_LEFT_HANDS). Veja que a sintaxe no C# é no estilo TryParse, você diz qual o objeto a ser lido (mão esquerda), o índice que pega todas as mãos, mas neste caso o zero (0) representa o ACCESS_ORDER_BY_ID e o out é o tipo de objeto que será retornado do tipo ihand. É claro que tudo isto está numa condicional se for diferente de erro, mostrando a mensagem que a mão esquerda não está visível e caso contrário que encontrou a mão, seguido do QueryOpenness.

O QueryOpenness retorna um valor de 0 a 100, sendo 0 para mão fechada (dedos completamente dobrados) e 100 para mão totalmente aberta (dedos totalmente esticados).

//6 - cria objeto com dados da mão
PXCMHandData handData = handModule.CreateOutput();
while (manager.AcquireFrame(true) >= pxcmStatus.PXCM_STATUS_NO_ERROR) {
    handModule = manager.QueryHand();
    if (handModule != null) {
        //popula objeto de dados da mão com valores atuais
        handData.Update();
        //pega dados de uma mão especifica
        PXCMHandData.IHand ihand;
        if (handData.QueryHandData(PXCMHandData.AccessOrderType.ACCESS_ORDER_LEFT_HANDS, 0, out ihand) != pxcmStatus.PXCM_STATUS_NO_ERROR) {
            Console.WriteLine("mão esquerda não visivel");
        }
        //nesse ponto sabemos que a mão está visivel
        else {
            Console.WriteLine("mão esquerda visivel: " + ihand.QueryOpenness());
        }
    }
    manager.ReleaseFrame();
}
manager.Dispose();

Pronto! Já temos tudo o que precisamos para testar o reconhecimento da mão esquerda e se ela está aberta ou fechada. Resta apenas fazer um ajuste no Build do projeto. No Solution Explorer, dê um duplo clique em Properties, abra a guia Build e desmarque o checkbox Prefer 32-bit, conforme a figura 5. Isso serve porque o SDK só vai funcionar em x64.

IC792729
Figura 5 – Build sem 32-bit

 

Compile a aplicação (F6 ou Ctrl + Shift + B). Se estiver tudo compilado com sucesso, certifique-se que a câmera esteja conectada à USB e pressione F5 para executar a aplicação. Deixe a mão esquerda fora do alcance da câmera e veja conforme a figura 6, que a mão não está visível.

Figura 6 – Mão esquerda invisível
Figura 6 – Mão esquerda invisível

Já na figura 7, temos as mensagens que a mão esquerda está visível e os dados do método OnGesture, que mostra o nome, o ID e o estado do gesto.

Agora que está funcionando e reconhecendo a mão esquerda, altere o bloco de códigos do else, conforme o código a seguir, de forma que seja identificado pelo número do QueryOpenness o quanto que a mão está aberta ou fechada. Se estiver maior que 80, mostra a mensagem que a mão esquerda está aberta.

Figura 7 – Mão esquerda identificada
Figura 7 – Mão esquerda identificada

Agora que está funcionando e reconhecendo a mão esquerda, altere o bloco de códigos do else, conforme o código a seguir, de forma que seja identificado pelo número do QueryOpenness o quanto que a mão está aberta ou fechada. Se estiver maior que 80, mostra a mensagem que a mão esquerda está aberta.

if (handData.QueryHandData(PXCMHandData.AccessOrderType.ACCESS_ORDER_LEFT_HANDS, 0, out ihand) != pxcmStatus.PXCM_STATUS_NO_ERROR)
{
    Console.WriteLine("mão esquerda não visivel");
}
//nesse ponto sabemos que a mão está visivel
else
{
    //Console.WriteLine("mão esquerda visivel: " + ihand.QueryOpenness());
    if (ihand.QueryOpenness() > 80)
    { 
        //esse numero varia de 0 (mão fechada) até 100 (mão bem aberta)
        Console.WriteLine("mão esquerda está aberta");
    }
}

Execute a aplicação novamente (F5) e veja que na figura 8 há um breakpoint dentro do IF, a janela imediata aberta com o código ihand.QueryOpenness() retornando o valor 92, e a janela do command prompt exibindo a mensagem “mão esquerda está aberta”. Inserir breakpoints no projeto é uma boa prática para se explorar estes novos objetos e métodos, ver quais os retornos, os tipos de dados, enfim, explorar.

Figura 8 – Análise dos dados com um Breakpoint no código
Figura 8 – Análise dos dados com um Breakpoint no código

Conclusão

Este mundo do RealSense está presente em diversos dispositivos e as necessidades de soluções para ajudar as pessoas é constante. Utilizar deste recurso de leitura de gestos, com certeza irá agregar muito valor à sua aplicação e facilitar a usabilidade, que tanto se fala em UI inteligíveis. Este SDK da Intel com a câmera está disponível no mercado e a implementação necessita de poucos códigos, como vimos neste artigo.

Agradecemos a oportunidade de poder compartilhar o conhecimento deste artigo com todos os desenvolvedores.

***

Artigo escrito em co-autoria com André Carlucci (andrecarlucci@gmail.comwww.andrecarlucci.com) é MVP, Intel Innovator, Diretor de Tecnologia da Way2 e palestrante nos principais eventos de desenvolvimento do país. André é apaixonado por metodologias ágeis e projetos open-source. Siga-o no twitter @andrecarlucci.