Como Disparar Delegates através de Gestos com o Intel SDK RealSense e C#?

 

Renato Haddad, André Carlucci

Abril, 2015

A cada dia as empresas lançam novos dispositivos com interfaces mais fáceis de se usar, acessar e se 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 isto 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 isto. 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 isto, é preciso programar fluxos dentro da aplicação. Quando falamos de códigos em C#, nos referimos aos delegates ou callbacks, onde 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 a 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 (https://software.intel.com/pt-br/RealSense/F200Camera), 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.

Novo projeto de Console App

Figura 1 – Novo projeto de Console App

Em seguida é preciso referenciar duas DLLs, as quais já explicamos detalhadamente no artigo “Como Identificar Gestos com o Intel SDK RealSense e C#?”. No entanto, como são importantes e sem elas você não consegue nada, vale a pena relembrar. 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.

Adicionar referência

Figura 2 – Adicionar referência

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

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, onde temos a definição de quatro eventos, sendo Opened, Closed, Visible e NotVisible respectivamente. No C# temos esta notação do EventHandler que permite atribuir um bloco de código para tal evento. Por enquanto é apenas a definição do mesmo, 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), isto não é possível aqui porque é preciso 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();
        }
    }
}

Estes 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 do gera o evento, neste caso é o this; e os argumentos, se houver, neste caso está declarado como EventArgs.Empty, afinal, não há dados para este evento. Caso tivessem parâmetros, este é 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#, onde 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 este projeto é de Console, a classe Program é a principal a ser executada quando rodar o projeto. Precisamos apenas de dois using, conforme listados 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, o qual é a classe criada anteriormente por nós, contendo os delegates que usaremos aqui. Já 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. Nesta 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 este delegate, onde 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 deste 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). Não quero ser repetitivo nas explicações, mas até o item 5 tem todos os detalhes no artigo anterior “Como Identificar Gestos com o Intel SDK RealSense e C#?”.

    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 lê cada frame e processa, 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 que o handler irá mostrar uma mensagem na tela informando este estado. E caso estiver visível (LeftHand.IsVisible = true) verificamos através do valor do QueryOpenness o quanto que mão está aberta ou fechada. Este 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 este 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 executar quando 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 neste 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.

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 que a câmera esteja conectada à 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.

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 este 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 as expectativas das aplicações e dos usuários.

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

Sobre os Autores

Renato Haddad (rehaddad@msn.com  – www.renatohaddad.com ) é MVP, MS Regional Director, MCPD e MCTS, Intel Innovator, palestrante em eventos da Microsoft em diversos países, ministra treinamentos focados em produtividade com o VS.NET 2013/2015, ASP.NET 4/5, Entity Framework, Windows Phone e Windows 8.1. Visite o blog https://weblogs.asp.net/renatohaddad.

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.