
Compartilhar:
Steve é um ex-membro da equipe da Vonage. Ele atuou como Developer Advocate .NET na Vonage, engenheiro de software full-stack poliglota, especializado em IA/ML
Detecção de rosto em tempo real no .NET com o OpenCV e a Video API da Vonage
Tempo de leitura: 12 minutos
Observação: algumas das ferramentas ou métodos descritos neste artigo podem não ter mais suporte ou estar desatualizados. Para obter conteúdo atualizado ou suporte, consulte nossas postagens mais recentes ou nossa Documentação
A Visão Computacional é minha área favorita na ciência da computação. Ela combina minhas quatro disciplinas favoritas — Programação, Álgebra Linear, Probabilidade e Cálculo — em algo prático e poderoso. Neste artigo, vamos examinar uma aplicação interessante da Visão Computacional, a detecção de rostos, e integrar esse recurso a um aplicativo OpenTok no Windows Presentation Framework (WPF).
Linha de base
Para nos ajudar a começar, vamos trabalhar com o exemplo CustomVideoRender da Vonage Video. No momento, esse exemplo adiciona um tom de azul ao quadro do vídeo quando você ativa o filtro. Vamos remover esse tom azul e, em vez disso, adicionar a detecção de rosto ao renderizador. E, acredite se quiser, esse recurso de detecção de rosto será cerca de 30 vezes mais rápido do que o filtro azul. Para conseguir isso, vamos aplicar o método Viola-Jones para detecção de características usando o Emgu CV.
Me dê logo o código
Se você não quiser seguir todo este tutorial, pode encontrar um exemplo funcional no GitHub. Basta certificar-se de substituir os parâmetros conforme as instruções do Guia de Início Rápido do repositório principal de exemplos
Uma breve visão geral de Viola-Jones
Não vou me aprofundar muito em como o método de Viola-Jones funciona, mas, para quem estiver interessado, aqui vai um breve contexto. O ponto-chave do algoritmo de Viola-Jones tem três aspectos. Primeiro, ele usa o que se chama de características do tipo Haar, que podem parecer formas engraçadas em preto e branco.
Haar-like feature shapes source: Source https://scc.ustc.edu.cn/
Mas, na verdade, eles são detectores de características muito simples que podem nos dizer muito sobre o sombreamento relativo de uma imagem:
Harr-like Features over Faces source http://www.willberger.org/cascade-haar-explained/
Quando sobrepostas a uma imagem, a soma da região branca é subtraída da soma da região preta, indicando efetivamente a diferença de tonalidade entre as regiões. Esses cálculos, quando aplicados em cascata a várias características, podem nos dar uma boa ideia de onde um rosto pode estar na imagem. Como essas características são tão simples, elas são invariantes em relação à escala, o que significa que podem identificar rostos em uma imagem, independentemente do tamanho deles.
Esse método é excelente na detecção de rostos em uma imagem, mas, se não fosse pela última grande inovação apresentada no artigo, ele seria extremamente lento, em vez de extremamente rápido. Os autores introduziram o conceito de imagem integral — uma imagem integral é aquela em que cada pixel é definido como a soma da região acima e à esquerda desse pixel. Ao calcular isso em uma imagem de entrada, conseguimos realizar cálculos em características do tipo Haar com uma complexidade temporal de O(1), em vez de O(N*M), onde N e M são, respectivamente, a altura e a largura da característica do tipo Haar. Isso faz com que a combinatória não apenas funcione, mas funcione muito bem a nosso favor, já que estamos tentando construir um detector de rostos que opere rapidamente.
Pré-requisitos
Visual Studio — Estou usando a versão 2019, mas as versões anteriores também devem funcionar
.NET Framework 4.6.1, no mínimo — você pode usar versões anteriores até a 4.5.2, mas terá que utilizar o EmguCV em vez do Emgu.CV como seu pacote NuGet do OpenCV.
Exemplo do CustomVideoRenderer - este é o exemplo que vamos adaptar.
Uma conta da Video API da Vonage — caso você ainda não tenha uma inscreva-se aqui.
Uma chave de API, um ID de sessão e um token do seu Account da Video API do Vonage — consulte o guia de início rápido no repositório para mais detalhes.
Introdução
Atualizar para a versão 4.6.1
Primeiro, vamos abrir o arquivo da solução CustomVideoRenderer. No arquivo MainWindow.xaml.cs, insira suas credenciais, caso ainda não tenha feito isso. Em seguida, atualize o csproj para que ele tenha como alvo o .NET Framework 4.6.1.
Para fazer isso, abra a solução no Visual Studio, clique com o botão direito do mouse no arquivo do projeto e selecione “Propriedades”. Em seguida, na guia “Aplicativo”, altere a estrutura de destino para 4.6.1.
Upgrade .NET version
Adicionar pacotes NuGet
Em seguida, adicione os seguintes pacotes NuGet além dos que já estão no aplicativo:
Emgu.CV.runtime.windows — Estou usando a versão 4.2.0.3662
WriteableBitmapEx - Estou usando a versão 1.6.5
Identifique as características semelhantes às de Haar
Baixe os dois arquivos a seguir de OpenCV:
haarcascade_frontalface_default.xml
haarcascade_profileface.xml
Coloque esses arquivos junto com seu projeto e configure-os para que sejam copiados para o diretório de compilação quando o projeto for compilado. Isso pode envolver a configuração de um evento pós-compilação, caso sua instância do Visual Studio seja tão pouco colaborativa quanto a minha:
copy $(ProjectDir)\haarcascade_profileface.xml $(ProjectDir)$(OutDir)
copy $(ProjectDir)\haarcascade_frontalface_default.xml $(ProjectDir)$(OutDir)
Displaying Post Build Events Screen
Neste momento, você já deve conseguir abrir o aplicativo e se conectar a uma chamada. Como o Windows impede que mais de um aplicativo acesse sua câmera ao mesmo tempo, talvez seja necessário participar da chamada a partir de outro computador usando o Vonage Video API Playground
Running in Playground
Agora, se conectarmos nossa chamada, ela deve ficar mais ou menos assim no aplicativo do Windows:
Display Without Filter Windows App
E se ativarmos o botão de filtro, ficará mais ou menos assim:
Display With Blue Filter Windows App
O que está acontecendo até agora
Até agora, tudo o que acontece é que, se você clicar no botão “ativar/desativar filtro”, o aplicativo aplicará um tom de azul a cada quadro que chegar ao renderizador.
Como isso acontece?
Em vez de usar o VideoRenderer padrão, estamos criando nosso próprio renderizador personalizado, SampleVideoRendererque estende a classe `Control` e implementa a interface `IVideoRenderer` da Video API da Vonage. Essa interface é bastante simples: possui um único método, RenderFrame, do qual estamos obtendo o quadro e desenhando-o em um bitmap no controle. Isso nos permite intervir sempre que um quadro aparece, aplicar o que quisermos a ele e fazer com que seja renderizado.
Incorporando a visão computacional
Portanto, com esse Renderizador Personalizado, temos tudo o que precisamos para começar a adicionar o CV ao nosso aplicativo. Vamos abrir o arquivo SampleVideoRenderer.cs e, antes de qualquer outra coisa, adicionar as seguintes importações:
using Emgu.CV;
using Emgu.CV.Structure;
using System.Diagnostics;
using System.Drawing;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;Já que você está aqui, renomeie EnableBlueFilter para DetectingFaces (certifique-se de usar o recurso de renomeação do seu IDE) e transforme-o em uma propriedade pública de leitura e privada de gravação, em vez de um campo público, desta forma:
public bool DetectingFaces { get; private set; }Isso vai causar alguns problemas, mas logo ficará claro como resolvê-los. Por enquanto, vamos seguir em frente.
Constantes
Adicione as seguintes constantes ao seu Renderer:
private const double SCALE_FACTOR = 4;
private const int INTERVAL = 33;
private const double PIXEL_POINT_CONVERSION = (72.0 / 96.0);A SCALE_FACTOR é a escala para a qual vamos reduzir as imagens para processamento — 4 significa que redimensionaremos as imagens para um quarto do tamanho original antes de executar a detecção. O INTERVAL é o número de milissegundos entre as imagens que tentaremos capturar do fluxo. 33 é aproximadamente o número de milissegundos entre quadros em um fluxo de 30 FPS, portanto, o parâmetro, tal como está, significa que está rodando em velocidade máxima. O PIXEL_POINT_CONVERSION é a proporção de pixels por ponto em uma tela de 96 DPI (que é a que estou usando). Naturalmente, isso pode ser calculado com mais precisão quando levarmos em conta a resolução (DPI), mas vamos usar essa proporção como referência por enquanto. Precisamos disso apenas porque, por alguma razão, a biblioteca Bitmap Extensions que estamos usando parece preferir desenhar o eixo X em pontos e o eixo Y em pixels 🤷♂️.
Criar nossos classificadores
Abordei brevemente como funcionam as características do tipo Haar anteriormente, mas, para uma análise mais aprofundada sobre elas, fique à vontade para conferir o artigo de Viola-Jones . O melhor do OpenCV (e, por extensão, do EmguCV) é justamente o quanto isso tudo é abstraído para nós.
Agora, vamos continuar com nosso SampleVideoRenderer. Desça um pouco e adicione dois CascadeClassifiers estáticos como campos:
static CascadeClassifier _faceClassifier;
static CascadeClassifier _profileClassifier;Em seguida, no construtor, inicialize-os com seus respectivos arquivos:
_faceClassifier = new CascadeClassifier(@"haarcascade_frontalface_default.xml");
_profileClassifier = new CascadeClassifier(@"haarcascade_profileface.xml");Esses arquivos XML descrevem as características de Haar para o classificador de forma suficientemente clara para treiná-lo. Portanto, neste momento, já treinamos o classificador!
Agora, vamos classificar
Algumas estruturas necessárias
Enquanto estivermos classificando, não queremos bloquear a thread principal. Por isso, vamos implementar o padrão produtor-consumidor. Vamos usar BlockingCollections. Especificamente, vamos usar um ConcurrentStack, pois os quadros mais relevantes e mais recentes são os mesmos. Adicione os seguintes campos à nossa classe:
private System.Drawing.Rectangle[] _faces = new System.Drawing.Rectangle[0];
private BlockingCollection<Image<Bgr, byte>> _images = new BlockingCollection<Image<Bgr, byte>>(new ConcurrentStack<Image<Bgr, byte>>());
private CancellationTokenSource _source;
private Stopwatch _watch = Stopwatch.StartNew();A _faces matriz conterá os rostos que detectamos com nosso classificador, enquanto a _images coleção, inicializada com um ConcurrentStack, será a coleção LIFO de imagens que iremos processar. O CancellationTokenSource é o que usaremos para sair do ciclo de processamento quando chegar a hora. O Stopwatch servirá como nosso cronômetro, impedindo que tentemos detectar quadros com muita rapidez.
Ciclo de processamento
Agora vamos implementar nosso ciclo de processamento. Adicione o método a seguir ao seu código:
private void DetectFaces(CancellationToken token)
{
System.Threading.ThreadPool.QueueUserWorkItem(delegate
{
try
{
while (true)
{
var image = _images.Take(token);
_faces = _faceClassifier.DetectMultiScale(image);
if(_faces.Length == 0)
{
_faces = _profileClassifier.DetectMultiScale(image);
}
if (_images.Count > 25)
{
_images = new BlockingCollection<Image<Bgr, byte>>(new ConcurrentStack<Image<Bgr, byte>>());
GC.Collect();
}
}
}
catch (OperationCanceledException)
{
//exit gracefully
}
}, null);
}Há muita coisa acontecendo neste método. Primeiro, vamos executar a operação em um dos Daemons disponíveis do ThreadPool. Em seguida, vamos processar em um loop contínuo. Chamamos Take a coleção bloqueante para retirar uma imagem da pilha. Essa Take chamada bloqueará se não houver nada na coleção e, quando sinalizarmos o cancelamento, ela lançará uma OperationCanceledException, que capturamos abaixo, saindo do loop de maneira controlada. Com a imagem, ela atribuirá a _faces coleção ao resultado de DetectMultiScale, que é o método de detecção de rosto. Se ele não encontrar nada, tentaremos novamente com o classificador de rosto de perfil.
Quando tudo isso estiver feito, verificamos a coleção de imagens para ver se ela ultrapassou algum limite (estamos usando apenas 25 como exemplo aqui). Se tiver excedido esse limite, porque o classificador ficou para trás, vamos limpar a coleção re-instanciando-a e, em seguida, vamos instruir o coletor de lixo a passar por lá e coletar essas imagens. Por que chamar o coletor de lixo? Bem, esse é o tema de outro post no blog, mas, essencialmente, se seus objetos forem muito grandes (acima de 85.000 bytes), eles são transferidos para a pilha de objetos grandes (Large Object Heap), à qual o coletor de lixo atribui uma prioridade menor do que a outros objetos (já que liberar a memória envolve um custo computacional bastante alto). Na prática, isso significa que, se você estiver lidando com objetos grandes com relativa frequência, convém garantir que eles sejam liberados; caso contrário, você terá um consumo de memória considerável.
Agora, se você seguir minhas diretrizes de desempenho abaixo, nunca precisará executar esse código, mas estou deixando-o lá apenas para que, quando as pessoas estiverem otimizando o código, não vejam picos enormes no uso de memória.
Ativando/desativando o ciclo de detecção
Agora adicione o código a seguir ao seu Renderer:
public void ToggleFaceDetection(bool detectFaces)
{
DetectingFaces = detectFaces;
if (!detectFaces)
{
_source?.Cancel();
}
else
{
_source?.Dispose();
_source = new CancellationTokenSource();
var token = _source.Token;
DetectFaces(token);
}
}Isso vai controlar a ativação e desativação do detector de rostos do seu renderizador. Se você configurá-lo para parar, ele instruirá a fonte de tokens a cancelar, saindo do loop de maneira controlada. Se você instruí-lo a iniciar, ele descartará o antigo CancellationTokenSource, reinicializará-o, obterá um token e iniciará o loop de processamento com esse token.
Vamos também adicionar um finalizador para garantir que a tarefa de detecção de rostos seja cancelada quando o renderizador estiver encerrando suas operações:
~SampleVideoRenderer()
{
_source?.Cancel();
} Juntando tudo isso
Até agora, preparamos toda a base necessária para realizar a detecção de rostos. A partir daqui, basta fazer com que nosso renderizador execute a detecção de rostos em cada quadro. Agora, acesse o RenderFrame método no SampleVideoRenderer. Exclua os dois loops “for” aninhados e substitua esse código por:
using (var image = new Image<Bgr, byte>(frame.Width, frame.Height, stride[0], buffer[0]))
{
if (_watch.ElapsedMilliseconds > INTERVAL)
{
var reduced = image.Resize(1.0 / SCALE_FACTOR, Emgu.CV.CvEnum.Inter.Linear);
_watch.Restart();
_images.Add(reduced);
}
}
DrawRectanglesOnBitmap(VideoBitmap,_faces);Isso vai extrair a imagem diretamente do buffer para o qual nosso filtro anterior estava copiando, em seguida, inserirá a nova imagem em nossa pilha de bloqueio e, por fim, desenhará os retângulos nas faces detectadas. Abaixo do RenderFrame método, adicione o DrawRectanglesOnBitmap método, que ficará assim:
public static void DrawRectanglesOnBitmap(WriteableBitmap bitmap, Rectangle[] rectangles)
{
foreach (var rect in rectangles)
{
var x1 = (int)((rect.X * (int)SCALE_FACTOR) * PIXEL_POINT_CONVERSION);
var x2 = (int)(x1 + (((int)SCALE_FACTOR * rect.Width) * PIXEL_POINT_CONVERSION));
var y1 = rect.Y * (int)SCALE_FACTOR;
var y2 = y1 + ((int)SCALE_FACTOR * rect.Height);
bitmap.DrawLineAa(x1, y1, x2, y1, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x1, y1, x1, y2, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x1, y2, x2, y2, strokeThickness: 5, color: Colors.Blue);
bitmap.DrawLineAa(x2, y1, x2, y2, strokeThickness: 5, color: Colors.Blue);
}
}Isso desenhará o retângulo como quatro linhas separadas no bitmap e o exibirá — observe como estamos usando o PIXEL_POINT_CONVERSION apenas no eixo x.
Uma última coisa antes de fazermos o teste
Percebi que o elemento PublisherVideo na MainWindow está um pouco pequeno para eu conseguir ver o que está acontecendo nele. Por isso, para fins de teste, dobrei ou quadrupliquei o tamanho da janela. Para fazer isso, basta ajustar a altura (Height) e a largura (Width) na linha 12 do arquivo MainWindow.xaml.
Preparar, pronto, testar
Agora estamos prontos — abra o aplicativo e clique no Toggle Filter botão no canto superior esquerdo da tela. Isso ativará o filtro. Você deverá vê-lo na pré-visualização e, ao se conectar a uma chamada, poderá observar a detecção facial funcionando também nos participantes remotos.
Display Example With Face Detection
Você verá que esse método de detecção de características é preciso e rápido. O filtro é executado em cerca de 10 ms, em comparação com os ~30 ms do filtro azul que foi modificado. E como o processamento principal é executado em uma thread de trabalho e o desenho propriamente dito leva menos de um milissegundo, isso é, na verdade, cerca de trinta vezes mais rápido, o que significa que adicionar a detecção facial é praticamente gratuito do ponto de vista da experiência do usuário.
Ajuste de parâmetros
Nenhuma discussão sobre Visão Computacional estaria completa sem uma breve menção ao ajuste de parâmetros. Existem todos os tipos de parâmetros que poderiam ser ajustados aqui, mas vou me concentrar apenas em dois:
Intervalo entre quadros
Fator de escala
Como mencionei anteriormente, os 33 milissegundos entre os quadros funcionaram bem para mim, especialmente quando defini o fator de escala adequadamente. O fator de escala foi o elemento mais importante para o desempenho. Se você definir o fator de escala como 1 — ou seja, tentar capturar uma imagem completa (no meu caso, 1280x720) —, isso significa 921.000 pixels para processar a cada 33 milissegundos, o que acarreta um custo substancial de desempenho. No meu computador, o programa rodava a cerca de 200 ms por quadro, sobrecarregava minha CPU e, sem adicionar a chamada explícita ao coletor de lixo, fazia com que o uso de memória disparasse. Lembre-se de que o fator de escala é quadrático; portanto, definir o fator de escala para 4 resulta na redução do número de pixels por um fator de 16. Pelos meus testes, não observei nenhuma perda de precisão ao redimensionar.
Indo além
Vamos deixar por aqui por enquanto, mas espero que este post inspire o leitor a reconhecer o imenso potencial que o OpenCV tem no .NET. Algumas Applications interessantes para as quais você poderia usar isso, só de cabeça:
Adicionar filtros e integrar RA aos seus aplicativos. Confira alguns artigos sobre homografias e algoritmos de rastreamento de características. Pessoalmente, gosto do o ORB (mesmo que seja apenas porque é muito mais flexível do que outros algoritmos de rastreamento de características!).
Você poderia integrar o Far End Camera Control (FECC) ao seu aplicativo e configurar os movimentos da câmera para acompanhar seu rosto!
Depois de localizar a área de interesse (ROI) do rosto na sua imagem, você poderá executar tarefas como análise de sentimentos.
Como se pode imaginar, esse é o primeiro passo do reconhecimento facial.
Recursos
Você pode encontrar um exemplo funcional deste tutorial no GitHub aqui
Para saber tudo o que você quiser sobre a Video API da Vonage, confira nosso site
Para tudo o que você quiser saber sobre o OpenCV, confira a documentação
Confira a página da wiki do Emgu para saber mais sobre como usar o Emgu especificamente. Se você é fã do OpenCV em Python como eu, não terá nenhum problema em usar o Emgu