Computer Vision num serviço escalável em tempo real

Rui Machado, BI consultant, BI4ALL

A equipa de AI & Data Science da BI4ALL frequentemente enfrenta desafios que requerem uma mentalidade multidisciplinar para os superar. O mais recente exemplo foi uma solução que nos envolveu no mundo da computação concorrente e visão computacional.

Embora a solução tenha englobado várias partes desde aplicações web até interfaces físicas, o maior desafio foi possivelmente o serviço principal que exigiu o desenvolvimento de uma arquitetura resiliente e escalável que pudesse ser aplicada a cenários em que múltiplas famílias de sensores trabalham em conjunto como base para a tomada de decisões em tempo real.

Exemplos de casos de uso que beneficiariam de uma tal arquitetura incluem deteção de anomalias em linhas de produção, gestão de filas em serviços públicos ou redireccionamento de tráfego.

No nosso caso específico o sistema tinha de tomar uma ação tendo por base vídeo em tempo real, muito resumidamente o serviço tinha de:

  • Monitorizar vários streams de vídeo em tempo real, detetando a existência de objetos em múltiplas regiões pré-definidas da imagem
  • Combinar os resultados actuais e passados dessa mesma deteção, aplicando regras, de modo a decidir que acção tomar

Bastante simples, não?

Bem, não havia um limite predefinido para a quantidade de streams de vídeo, os objetos podiam ser tão distintamente variados quanto seres humanos e bananas, e as regras de decisão a aplicar aos resultados da deteção descreviam uma máquina de estados razoavelmente complexa.

COMPUTER VISION

Começando pela parte de processamento de vídeo sabíamos que teríamos de manter o tempo de processamento o mais curto possível de modo a não impactar a responsividade do serviço e reduzindo em simultâneo a carga exercida na máquina. Isso implicaria não depender de serviços externos de computer vision assim como não utilizar algoritmos de machine learning demasiado complexos. Também não seria de todo viável processar todas as imagens de cada stream vídeo sendo melhor analisar apenas uma amostra periódica.

Optámos então por analisar apenas uma imagem por segundo e por stream, o que daria ao processo um tempo de resposta rápido o suficiente enquanto poupava enormemente os recursos da máquina. Também nos permitiria ignorar o facto de os streams não terem todos a mesma cadência o que simplificaria a solução.

De seguida comparámos várias técnicas para detetar a presença de objetos em 3 regiões distintas de cada imagem:

  • Um classificador em cascata do tipo Haar que ofereceu boa performance mas cuja precisão se provou insuficiente.
  • Uma rede neuronal pré treinada que por outro lado resultou numa boa precisão, mas que se tornou demasiado ineficiente conforme a quantidade de streams a analisar aumentou.
  • Um algoritmo adaptativo com base na técnica de Background Subtraction que obteve boa performance e precisão geral, mas que resultava em inúmeros falsos negativos quando alguns objetos se mantinham imóveis por longos períodos.

Atendendo aos nossos requisitos não podíamos ceder nem a nível de performance nem de precisão e acabámos então por desenvolver o nosso próprio algoritmo baseado em Background Subtraction que compara cada imagem a uma outra imagem de referência utilizando dois limites configuráveis para decidir se existe pelo menos um objeto na imagem ou não.

O primeiro limite decide que pixéis são diferentes da imagem de referência, o segundo decide se a quantidade de diferenças é suficiente para desencadear a classificação positiva:

1

Além disso, o limite ao nível do pixel permite-nos ignorar diferenças de luminosidade enquanto que o limite de ocupação pode ser usado para prevenir que pequenos artefactos ou obstruções na imagem causem uma classificação positiva.

A imagem de referência seria definida na interface de utilizador e caso o ambiente se alterasse suficientemente de modo a afetar a performance do algoritmo, o próprio utilizador podia facilmente definir uma nova imagem. Previa-se que uma alteração dessas seria um evento raro. Neste caso sacrificámos um pouco em automação em troca de uma performance e precisão muito altas, que eram consideradas críticas para este caso de uso.

O resultado acabou por ser um algoritmo muito simples e fácil de personalizar, sendo não só leve a nível de recursos, mas também perfeitamente enquadrado nos nossos requisitos.

CONCORRÊNCIA COMPUTACIONAL

O serviço tinha de realizar uma ação assim que certas condições fossem satisfeitas. Adicionalmente haveria uma quantidade desconhecida de streams vídeo a monitorizar, aliás essa quantidade era variável! Num dado momento podiam ser 2 e uma hora depois mais de 20.

Uma abordagem modular e concorrente seria necessária para que cada thread pudesse monitorizar e analisar o seu stream vídeo sem impactar a thread principal, essa responsável por agir. Para comunicar os resultados da deteção de objetos entre as threads em tempo real bastava armazená-los em memória partilhada.

Concorrência é a capacidade de um programa executar múltiplas operações em simultâneo, ao invés de sequencialmente, dividindo as tarefas entre diferentes threads.

Embora esta capacidade permita em geral aproveitar melhor os recursos da máquina, traz consigo uma série de potenciais dores de cabeça se não for implementada cautelosamente.

A título de exemplo imagine-se múltiplas threads a tentarem alterar um mesmo recurso partilhado em simultâneo, sem que exista qualquer mecanismo de sincronia. Existe um risco muito real de que esse recurso acabe num estado inválido. Com um pouco de sorte esse estado inválido manifesta-se imediatamente como um erro na aplicação, mas caso contrário o estado inválido poderá passar despercebido por muito tempo, propagando-se para outras partes da aplicação até que algum componente eventualmente dê erro ou se comporte de uma maneira inesperada.

Encontrar a real causa de um erro dessa natureza é difícil e moroso e, portanto, um estilo de programação mais defensivo é recomendado para evitar alguns dos riscos mais frequentes inerentes à concorrência. Garantir que recursos partilhados são devidamente protegidos por mecanismos de bloqueio de acesso e ter em conta que a maioria das operações que realizamos nesses recursos não é atómica são duas ideias chave a ter em mente ao desenvolver sistemas concorrentes.

Após decidir que cada stream vídeo devia ser processado por uma thread dedicada, chamada monitor, faltava responder à questão de como aplicar as regras e agregar os resultados, processo esse que se fosse feito numa só thread não seria escalável com o aumento do número de streams vídeo. Bem, podíamos acoplar uma máquina de estados a cada monitor que aplicaria parte das regras, reduzindo a necessidade de processamento na thread que corre o controlador principal.

MÁQUINAS DE ESTADOS

Uma máquina de estados é um modelo que representa um processo como um conjunto de estados discretos, ligados por transições. Uma máquina de estados só pode estar num único estado num dado momento, mas pode transitar para vários estados diferentes. É um modelo particularmente útil quando queremos considerar o estado atual ao decidir o próximo.

Um exemplo básico de uma máquina de estados é um que modela o comportamento de um interruptor simples composto por apenas dois estados e duas transições. Se o interruptor se encontra ON e pressionarmos OFF, o estado altera-se. Se, no entanto, pressionarmos ON enquanto o interruptor já se encontra no estado ON nada acontece.

2

No nosso caso a máquina de estados era semelhante à seguinte com 3 estados principais e múltiplas transações:

3

Ao reparar que ambos os estados Idle Waiting podem transitar para qualquer um dos outros estados podíamos argumentar que bastava serem um único estado. Mas tal não seria possível porque as transições em si são diferentes e baseadas em condições distintas. Essas diferenças não se encontram aqui representadas por motivos de simplicidade. Os estados também afetam as decisões tomadas pelo controlador principal, que pode escolher executar ações diferentes conforme o significado de uma unidade se encontrar Idle ou Waiting.

JUNTANDO OS COMPONENTES

Após os passos anteriores a arquitetura passou a ter uma forma muito semelhante à seguinte:

4

Cada monitor aplica a deteção de objetos ao vídeo e armazena os resultados em tempo real num espaço acessível à sua máquina de estados respetiva. A máquina de estados aplica regras de negócio que traduzem esses resultados num único estado. O controlador principal verifica o estado de todas as unidades e decide que ação tomar.

Quando um novo vídeo a processar é introduzido no sistema, a arquitetura escala ao instanciar um novo par monitor/máquina de estados, aloca um novo espaço de memória partilhada entre o controlador principal e a máquina de estados, e pronto! A nova unidade começa a fazer parte da lógica de decisão do controlador principal.

A partir daí o controlador principal tem apenas de verificar se estão reunidas as condições para atuar:

5

Uma enorme vantagem desta arquitetura é que com um pequeno esforço de programação podemos configurá-la para diferentes tipos de sensores e lógica de regras. Ao desenvolver um monitor diferente podemos facilmente passar de deteção de objectos para deteção de defeitos de fabrico através de câmaras de infravermelhos. Se integrarmos o controlador principal com o atuador de uma correia industrial, e desenvolvermos um monitor que verifica o estado de vários contentores podemos criar um distribuidor de carga que redireciona produtos para os contentores. Desta forma, temos uma arquitectura que adapta-se facilmente a múltiplas situações.

Últimos artigos