Back-End

15 abr, 2015

Como disparar Delegates através de gestos com o Intel SDK RealSense e C#?

Publicidade

***Artigo feito em conjunto com André Carlucci

A cada dia, as empresas lançam dispositivos com interfaces mais fáceis de se usar, acessar e obter informações. Se você observar as novas gerações de crianças, parece que nascem sabendo usar qualquer dispositivo ou, ainda, aprendem em minutos. Mas será que isso não é devido às novas maneiras de entender como as coisas funcionam?

Usar gestos para navegar em uma aplicação é normal, ainda mais que os dispositivos como celulares, monitores de vídeo, painéis de carros, gps etc. já estão preparados pra isso. Mas com certeza ter a possibilidade de reconhecimento de voz ou gestos com a mão ou a cabeça é mais intuitivo e rápido. Como sabemos, aplicações como jogos, caixas eletrônicos, gps, entre outras tantas, devem levar o usuário a atingir um objetivo. E, para isso, é preciso programar fluxos dentro da aplicação. Quando falamos de códigos em C#, nos referimos aos delegates ou callbacks, nos quais são determinados e assinados eventos para que o próprio programa saiba qual código disparar.

Sendo assim, o objetivo deste artigo é criar um projeto de Console Application no Visual Studio em C#, utilizar o SDK Intel RealSense juntamente com a câmera para ler a mão esquerda e disparar delegates que irão fazer uma ação a partir de um movimento. Por exemplo, imagine que você esteja sentado à frente da sua televisão e deseje passar os canais apenas com um gesto com a mão para direita ou esquerda! Ou melhor, se você pensar numa pessoa com deficiência física, que tal mudar o canal com um sorriso através de um ou duplo piscar de olhos? Enfim, coloque a imaginação pra funcionar e siga-nos neste projeto.

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. (https://software.intel.com/pt-br/intel-realsense-sdk).

Projeto

Abra o Visual Studio, selecione o menu File / New Project (Ctrl + Shift + N). Conforme a figura 1, selecione a linguagem Visual C#, o template de Console Application, o nome do projeto é GesturesEvents e você pode gravar em qualquer pasta que desejar. Clique no botão OK e aguarde o VS criar o projeto.

Figura 1 – Novo projeto de Console App.
Figura 1 – Novo projeto de Console App.

Em seguida, é preciso referenciar duas DLLs. Uma vez instalado o SDK Intel RealSense, abra a pasta C:\Program Files (x86)\Intel\RSSDK\bin\x64 contendo todas as DLLs do SDK. No VS, adicione (Add / Reference no Solution Explorer) a referência à libpxcclr.cs.dll, conforme a figura 2, e clique no botão OK.

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

É preciso arrastar a DLL libpxccpp2c.dll diretamente no Solution Explorer e configurar a propriedade (F4) “Copy to Output Directory” para “Copy Always”, conforme a figura 3. Isso ocorre porque a biblioteca libpxcclr é escrita em C#, é um wrapper para a libpxccpp2c, que está escrita em C++. Com isso, ao fazer o Deploy do projeto, a libpxccpp2c estará presente fisicamente na pasta em que a aplicação for instalada.

Figura 3 – Propriedade Copy Always da biblioteca.
Figura 3 – Propriedade Copy Always da biblioteca.

Códigos C# da classe Hand

Para que possamos organizar as informações, adicione uma nova classe (Add / Class no Solution Explorer) chamada Hand com visibilidade pública. Veja a seguir o início da classe, no qual temos a definição de quatro eventos, sendo Opened, Closed, Visible e NotVisible, respectivamente. No C#, temos essa notação do EventHandler, que permite atribuir um bloco de código para tal evento. Por enquanto, é apenas a definição dele, nada de assinar nenhuma chamada.

using System;

namespace GesturesEvents
{
    public class Hand
    {
        public event EventHandler Opened;
        public event EventHandler Closed;
        public event EventHandler Visible;
        public event EventHandler NotVisible;

Em seguida, defina duas propriedades IsOpen e IsVisible. Para quem está acostumado a definir propriedades no C# com o get e o set apenas no modelo simplificado (propriedades automáticas), isso não é possível aqui porque são necessárias mais informações no bloco do set {}. Veja que nos blocos dos sets há condicionais que disparam métodos (FireOpen, FireClosed, FireVisible, FireNotVisible).

private bool _isOpen;
private bool _isVisible;
public bool IsOpen
{
    get { return _isOpen; }
    set
    {
        if (value == _isOpen)
        {
            return;
        }
        _isOpen = value;
        if (_isOpen)
        {
            FireIsOpen();
        }
        else
        {
            FireClosed();
        }
    }
}

public bool IsVisible
{
    get { return _isVisible; }
    set
    {
        if (value == _isVisible)
        {
            return;
        }
        _isVisible = value;
        if (_isVisible)
        {
            FireVisible();
        }
        else
        {
            FireNotVisible();
        }
    }
}

Esses quatro métodos preparam o evento (handler) para serem chamados e invocados quando necessário. Vejam que a assinatura do handler contém dois parâmetros: o sender que é do tipo Object, é a instância do objeto que gera o evento – nesse caso, é o this; e os argumentos, se houver, nesse caso, estão declarados como EventArgs.Empty, afinal, não há dados para esse evento. Caso tivessem parâmetros, esse é um tipo derivado de EventArgs contendo os campos ou propriedades necessárias para armazenar os dados do evento. Vale dizer que o uso da assinatura de handler para o delegate de eventos define um método que não retorna um valor.

O uso do var na declaração retorna um tipo EventHandler – é só uma forma resumida de declaração do C#, que chamamos de tipo implícito.

protected virtual void FireIsOpen()
{
    var handler = Opened;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

protected virtual void FireClosed()
{
    var handler = Closed;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

protected virtual void FireVisible()
{
    var handler = Visible;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

protected virtual void FireNotVisible()
{
    var handler = NotVisible;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

Códigos C# para o Program

Como esse projeto é de Console, a classe Program é a principal a ser executada quando rodar o projeto. Precisamos apenas de dois using, conforme listado nos códigos a seguir. Depois, definimos duas variáveis: uma para a sessão (_session), que é do tipo PXCMSession, e outra para a o Manager (_manager), que é do tipo PXCMSenseManager.

A sessão é 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. O PXCMSenseManager é o responsável para se conectar diretamente com a câmera e processar as ações.

Em seguida, definimos uma propriedade chamada LeftHand do tipo Hand, que é a classe criada anteriormente por nós, contendo os delegates que usaremos aqui. No construtor da classe Program, instanciamos um novo objeto do tipo Hand.

using System;
using System.Threading.Tasks;

namespace GesturesEvents
{
    class Program
    {
        private PXCMSession _session;
        private PXCMSenseManager _manager;

        public Hand LeftHand { get; private set; }

        public Program()
        {
            LeftHand = new Hand();
        }

O método Main é o principal do Program. Aqui é onde tudo inicia, ou seja, ao instanciar a classe Program na variável camera, o objeto Hand já é criado – afinal, está no construtor. Já a variável hand (do tipo Hand) atribui a propriedade LeftHand do Program.

static void Main(string[] args)
{
    var camera = new Program();
    var hand = camera.LeftHand;

Agora veja que fantástico a sintaxe de atribuir os delegates. Nessa sintaxe, você digita o objeto hand, o Intellisense do Visual Studio mostra todos os eventos EventHandler disponíveis (Opened, Closed, Visible e NotVisible), você escolhe o handler, seguido do += que expressa a assinatura, a atribuição do método, depois você informa quem é o objeto sender e o parâmetro de argumentos eventArgs.

E os códigos a serem executados, onde ficam? Você já ouviu falar em expressão Lambda? Se não, então corre, porque jamais um desenvolvedor C# pode ficar sem codificar com Lambda. O símbolo => atribui o bloco de código a ser executado para esse delegate, no qual o bloco está inserido entre chaves {}. No nosso exemplo, simplesmente serão mostradas mensagens de acordo com cada delegate. Resumindo, os delegates assinam os eventos que ficam de plantão para serem executados quando chamados, e cada delegate tem um bloco de código conforme o contexto.

Ao final desse código, temos o método Start (a ser criado) do objeto camera para iniciar a câmera, ler a mão e mostrar as informações. E o Console.Readline é apenas uma forma de parar o código C# numa janela de Command prompt.

hand.Opened += (sender, eventArgs) =>
    {
        Console.WriteLine("hand open");
    };

    hand.Closed += (sender, eventArgs) =>
    {
        Console.WriteLine("hand close");
    };

    hand.Visible += (sender, eventArgs) =>
    {
        Console.WriteLine("hand visible");
    };

    hand.NotVisible += (sender, eventArgs) =>
    {
        Console.WriteLine("hand not visible");
    };

    camera.Start();

    Console.ReadLine();
}

Iniciar a câmera e ler os dados

Veja o bloco completo do método Start. Lá, criamos a sessão, o manager com o CreateSenseManager, ativamos o módulo de mão no EnableHand, configuramos o módulo para pegar todos os gestos (EnableAllGestures), qual evento (OnGesture) será disparado quando um gesto for feito, ativamos as notificações (EnableAllAlerts) e, ao final, aplicamos as configurações (ApplyChanges).

public void Start()
{
    //1 - criar session
    _session = PXCMSession.CreateInstance();
    //2 - criar manager (responsável por adquirir os frames da leitura da câmera
    _manager = _session.CreateSenseManager();
    //3 - ativar módulo da mão
    _manager.EnableHand();
    //4 - pegar a instância da mão para fazer configurações antes de iniciar
    PXCMHandModule handModule = _manager.QueryHand();
    using (PXCMHandConfiguration config = handModule.CreateActiveConfiguration())
    {
        config.EnableAllGestures();
        config.SubscribeGesture(OnGesture);
        config.EnableAllAlerts();
        config.ApplyChanges();
    }

    //5 - inicia o pipeline
    if (_manager.Init() != pxcmStatus.PXCM_STATUS_NO_ERROR)
    {
        Console.WriteLine("Error initing camera");
    }

A partir deste momento, é criada uma tarefa (Task) que irá executar (Run) Threads de forma assíncrona. O código Task.Run aceita uma Action (delegate) no C#, ou seja, ele pega uma thread da thread pool e executa o código atribuído no bloco do => {} nela.

Já no bloco de código, o objeto mão é criado com o PXCMHandData, e é disparado um looping While até que não dê erro na leitura da câmera. O AcquireFrame e o ReleaseFrame leem cada frame e processam, ou seja, o AcquireFrame lê o frame atual, processa o que for preciso e o ReleaseFrame libera o frame. Lembre-se de liberar o frame o mais rápido possível.

O QueryHand e o Update irão ler os dados da mão, e o Ihand pega apenas a mão esquerda. Dependendo da visibilidade da mão na frente da câmera, a propriedade IsVisible é setada como verdadeira ou falsa. Lembre-se de que o handler irá mostrar uma mensagem na tela informando esse estado. E caso estiver visível (LeftHand.IsVisible = true), verificamos através do valor do QueryOpenness o quanto que a mão está aberta ou fechada. Esse número varia de 0 (mão fechada) a 100 (mão totalmente aberta) e, se for maior que 80, atribui a propriedade IsOpen para true. Caso contrário, false. A cada propriedade setada, é mostrada uma mensagem na tela que foi disparada pelo delegate. Quando você executar esse código, coloque breakpoints em todos os delegates e propriedades, use o F11 para executar o Debug passo a passo e entender o fluxo do código. E, para finalizar, cada delegate é exatamente um bloco de códigos que você deve atribuir para quando executar uma ação, seja num jogo, numa aplicação etc.

//6 - Executa a task
    Task.Run(() =>
    {
        //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)
            {
                handData.Update();
                PXCMHandData.IHand ihand;
                if (handData.QueryHandData(PXCMHandData.AccessOrderType.ACCESS_ORDER_LEFT_HANDS, 0, out ihand) != pxcmStatus.PXCM_STATUS_NO_ERROR)
                {
                    LeftHand.IsVisible = false;
                }
                else
                {
                    LeftHand.IsVisible = true;
                    if (ihand.QueryOpenness() > 80)
                    { 
                        //esse numero varia de 0 (mão fechada) até 100 (mão bem aberta)
                        LeftHand.IsOpen = true;
                    }
                    else
                    {
                        LeftHand.IsOpen = false;
                    }
                }
            }
            _manager.ReleaseFrame();
        }
    });
}

Veja o método OnGesture atribuído no SubscribeGesture. Como não precisamos fazer nada com ele nesse contexto, deixe-o sem nenhum código.

private void OnGesture(PXCMHandData.GestureData gesturedata)
{
    
}

Executar a aplicação.

Como ainda não configuramos o Build deste projeto, no Solution Explorer, dê um duplo clique em Properties, abra a guia Build e desmarque o checkbox Prefer 32-bit, conforme a figura 4.

Figura 4 – Build sem 32-bit.
Figura 4 – Build sem 32-bit.

Compile a aplicação (F6 ou Ctrl + Shift + B). Se estiver tudo compilado com sucesso, certifique-se de que a câmera esteja conectada ao USB e pressione F5 para executar a aplicação. Faça com que a mão esquerda fique visível à câmera, abra e feche a mão, retire a mão da frente da câmera e, ao final, veja as mensagens disparadas pelos delegates que criamos, conforme a figura 5.

Figura 5 – Mensagens conforme os gestos da mão.
Figura 5 – Mensagens conforme os gestos da mão.

Conclusão

Interagir com gestos nas aplicações é uma conquista que temos que explorar ao máximo dos dispositivos que permitem esse recurso. Leia bastante a documentação do SDK Intel RealSense e da linguagem C# (delegates, action, expressões Lambda) a fim de criar cada vez mais códigos que atendam às expectativas das aplicações e dos usuários.

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

***

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