sábado, 23 de março de 2019

Comparação de rostos com Java e C++ utilizando HOG


#C++ #java #opencv #dlib #reconhecimentoFacial #faceRecognition #IA #deepLearning

HOG - Histogram of Oriented Gradients (histograma de gradientes orientados) é um descritor de imagens, capaz de resumir as principais características de uma imagem, como rostos por exemplo, permitindo comparação com imagens semelhantes.

Este artigo e tutorial é de dois anos atrás e eu resolvi atualizar e modernizar o código-fonte para publicar novamente.




Java / C++ vs Python


Nesta demonstração utilizarei a biblioteca dlib, em um programa C++, para comparar a matriz HOG de duas imagens de rostos, retornando o grau de semelhança entre elas. Usarei também Java como "encapsulamento" da minha função C++, pois a integração JNI - Java Native Interface é feita inprocess e bastante performática.

Tenho visto várias soluções de processamento de imagens, em especial de comparação de rostos ou até de reconhecimento facial, baseadas em Python. Estas soluções utilizam Python como linguagem principal, invocando funções da dlib ou OpenCV. Praticamente, todas estas soluções são baseadas em algumas bibliotecas Python disponibilizadas no Github, como estas:

Apesar de possuírem o mérito de facilitar o desenvolvimento, estas bibliotecas podem comprometer o desempenho de uma solução de processamento de imagens, especialmente se o computador hospedeiro for desprovido de GPU. É sabido que os principais interpretadores Python, como o CPython e o PyPy, possuem o GIL - Global Interpreter Lock, como eu já citei em artigo anterior. Além deste aspecto, o desempenho das aplicações Python, comparado com as aplicações Java, pode ser mais um ponto problemático.

Portanto, faz muito mais sentido implementar a função de reconhecimento em C++, encapsulando-a em código Java, para expor como RESTful service, afinal de contas, fazer isso em C++ não agregará valor à solução, apenas complexidade.

Além da melhor performance, Java é a linguagem de programação mais popular do mundo, segundo a lista TIOBE (https://www.tiobe.com/tiobe-index/).

HOG


Voltando a esta técnica, vamos ver como extrair o descritor HOG de uma imagem e fazer comparações entre descritores de imagens diferentes, a base para uma aplicação de comparação facial.

Resumidamente, extraímos uma matriz de direção e magnitude de mudança de intensidade dos pixels (gradientes), e geramos um histograma com estes dados. Há várias maneira de extrair o HOG de uma imagem, mas o artigo original é este:


Método


O primeiro passo é converter uma imagem original em tons de cinza e depois filtrar as linhas de modo a remover o fundo e outras características que não nos interessam. Podemos fazer isto com bibliotecas como a OpenCV ou a dlib, ou até com o Gimp:






Nesta figura, vemos a imagem original, no canto esquerdo, depois a convertida em monocromática, e, finalmente, a imagem com o filtro de margens. Pode ser Sobel ou qualquer outro que saliente as linhas. Para ter um melhor resultado, é recomendado cortar e trabalhar apenas o rosto, pois o resto não tem relevância e poderá atrapalhar a comparação.




Para cada gradiente extraído, calculamos a direção da mudança de intensidade e a magnitude, por exemplo, neste artigo há uma boa imagem explicativa:




Então calculamos um histograma, no qual as classes são os ângulos de inclinação (0,20,40,60,80,100,120,140,160) e os valores (votos) são as magnitudes (mudanças de intensidade).

Plotando isso (não faz sentido, mas demonstra melhor) teríamos esta versão da imagem:




Aqui vemos a melhor representação possível do que seriam as características HOG.

Utilizando a dlib


Para começar a trabalhar com a dlib, temos que ver quais são os objetos e funções que nos auxiliam no trabalho de analisar uma imagem e extrair sua matriz HOG.

1 – Detetar rostos:

A dlib tem o frontal_face_detector, que é um modelo treinado com HOG e SVG, utilizando o dataset iBUG 300-W. Ele retorna um vetor de retângulos contendo os rostos encontrados na imagem.

dlib::frontal_face_detector detector = dlib::get_frontal_face_detector();
for (auto face : detector(dlibImage))

2 – Extrair e preparar rostos:

É preciso pegar os retângulos resultantes e extrair da imagem original os rostos, rotacionando e escalando de maneira apropriada. Para isto, utilizamos um modelo com 68 características faciais, ou “face landmarks”, previamente treinados:

...
dlib::frontal_face_detector detector = dlib::get_frontal_face_detector();
dlib::shape_predictor sp;
dlib::deserialize(path + "/shape_predictor_5_face_landmarks.dat") >> sp;
...
matrix<rgb_pixel> face_chip;
dlib::extract_image_chip(dlibImage, dlib::get_face_chip_details(shape,150,0.25), face_chip);

3 – Extração de vetor de características:

A dlib tem um exemplo bem interessante que extrai um vetor HOG de uma imagem, utilizando uma rede neural implementada em código e o modelo ResNet v1 pré-treinado ("dlib_face_recognition_resnet_model_v1.dat"). O código-fonte original que utiliza esta técnica pode ser acessado em: http://dlib.net/dnn_face_recognition_ex.cpp.html

Eis a extração da matriz 128 características:

Modelo ResNet que gera a saída:

template <template <int,template<typename>class,int,typename> class block, int N, template<typename>class BN, typename SUBNET>
using residual = add_prev1<block<N,BN,1,tag1<SUBNET>>>;

template <template <int,template<typename>class,int,typename> class block, int N, template<typename>class BN, typename SUBNET>
using residual_down = add_prev2<avg_pool<2,2,2,2,skip1<tag2<block<N,BN,2,tag1<SUBNET>>>>>>;

template <int N, template <typename> class BN, int stride, typename SUBNET>
using block = BN<con<N,3,3,1,1,relu<BN<con<N,3,3,stride,stride,SUBNET>>>>>;

template <int N, typename SUBNET> using ares = relu<residual<block,N,affine,SUBNET>>;
template <int N, typename SUBNET> using ares_down = relu<residual_down<block,N,affine,SUBNET>>;

template <typename SUBNET> using alevel0 = ares_down<256,SUBNET>;
template <typename SUBNET> using alevel1 = ares<256,ares<256,ares_down<256,SUBNET>>>;
template <typename SUBNET> using alevel2 = ares<128,ares<128,ares_down<128,SUBNET>>>;
template <typename SUBNET> using alevel3 = ares<64,ares<64,ares<64,ares_down<64,SUBNET>>>>;
template <typename SUBNET> using alevel4 = ares<32,ares<32,ares<32,SUBNET>>>;

using anet_type = loss_metric<fc_no_bias<128,avg_pool_everything<
alevel0<
alevel1<
alevel2<
alevel3<
alevel4<
max_pool<3,3,2,2,relu<affine<con<32,7,7,2,2,
input_rgb_image_sized<150>
>>>>>>>>>>>>;

Agora, vamos carregar o modelo ResNet pré-treinado:
anet_type net;
dlib::deserialize(path + "/dlib_face_recognition_resnet_model_v1.dat") >> net;

Finalmente, vamos extrair a matriz de características de uma imagem de rosto:

std::vector<matrix<float,0,1>> face_descriptors1 = net(faces1);

4 – Comparar vetores

Se você quiser comparar rostos de modo a saber se são da mesma pessoa, pode calcular a distância euclidiana dos vetores da matriz. Se for menor que 0,6, então, provavelmente, as imagens são da mesma pessoa:

std::vector<sample_pair> edges;
for (size_t i = 0; i < face_descriptors.size(); ++i)
{
for (size_t j = i; j < face_descriptors.size(); ++j)
{
// Faces are connected in the graph if they are close enough. Here we check if
// the distance between two face descriptors is less than 0.6, which is the
// decision threshold the network was trained to use. Although you can
// certainly use any other threshold you find useful.
if (length(face_descriptors[i]-face_descriptors[j]) < 0.6)
edges.push_back(sample_pair(i,j));
}
}


Você pode calcular previamente e armazenar as matrizes das imagens das pessoas conhecidas, e depois, quando precisar reconhecer um rosto, pesquisar em um banco de dados. Na verdade, eu desenvolvi e implementei um sistema destes utilizando minhas câmeras de segurança de casa. Funciona bem, com razoável precisão.

Código exemplo

Este artigo é acompanhado de um código exemplo, com partes em Java e C++, que compara duas imagens e diz se são da mesma pessoa. Veja a execução com imagens da mesma pessoa:





São duas imagens minhas, tiradas com pelo menos 7 anos de diferença e em uma delas estou usando cavanhaque e bigode, o que não impediu o reconhecimento. O retorno da função C++ foi “true”, ou seja, considerou acertadamente que as duas imagens são da mesma pessoa.

Agora, vejamos um exemplo com imagens diferentes:




Aqui, utilizei uma imagem de Thomas Edison, da Wikipedia (https://nn.wikipedia.org/wiki/Thomas_Edison) e o resultado foi negativo. Testei com várias outras imagens, obtendo os mesmos resultados.

Eu poderia ter utilizado somente a biblioteca OpenCV, que também faz a mesma coisa, mas achei o código exemplo da dlib mais preciso.


Como compilar e rodar o projeto




Cara, você vai precisar de paciência… MUUUUIIIIITA PACIÊNCIA! Minha máquina é um laptop Samsung, I7 oitava geração, com 12 GB RAM e chipset Nvidia, embora eu não tenha utilizado a dlib nem a OpenCV compiladas para isto. Se você quiser desenvolver uma solução “production grade”, nem perca tempo: Compile ambas com Instruction set AVX e utilizando GPU!



Nem perca seu tempo tentando compilar isso em outro sistema operacional! O original foi feito em MacOS, mas eu adaptei tudo para rodar em Ubuntu (18.xx). Foram menos problemas! Tentei rodar em MS Windows, porém, dá mais trabalho para adaptar e o desempenho não ficou tão bom.



1 – Clone o repositório:
git clone https://github.com/cleuton/hogcomparator.git



O código da dlib já está incluído. Ele tem o código da aplicação Java e da função C++ que implementa o método nativo invocado.



2 – A aplicação Java:
para compilar a aplicação basta rodar:
mvn clean package



Ou então, importe o projeto Maven para uma workspace do Eclipse. Esta aplicação usa JNI para invocar um método nativo:
static {
nu.pattern.OpenCV.loadShared();
System.loadLibrary("hogcomparator");
}

// Native method implemented by a C++ library:
private native boolean compareFaces(long addressPhoto1, long addressPhoto2);



Para que o Java invoque o método “compareFaces”, precisamos criar uma Shared Library (ou DLL, se você insistir no MS Windows), chamada “hogcomparator”. Esta library deverá implementar o método nativo “compareFaces” de maneira estabelecida pela interface JNI – Java Native Interface. Para isto, precisamos criar um header C++ que contenha a declaração do método. Neste caso, tudo isso já foi feito, mas, se você precisar criar outra aplicação, é melhor ver como eu fiz.



Para criar o header, costumávamos utilizar o programa javah:
Javah -jni -classpath C:\ProjectName\src com.abc.YourClassName



Só que o javah não existe mais desde o Java 10! Agora, utilizamos a opção -h do compilador javac. Mas, como estou usando Maven, é só configurar o build plugin corretamente no pom.xml:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<compilerArgs>
<arg>-h</arg>
<arg>target/headers</arg>
</compilerArgs>
<source>11</source>
<target>11</target>
</configuration>
</plugin>



Ao compilar o programa (mvn clean package ou pelo eclipse) veremos uma pasta target/headers com o nosso arquivo: “com_obomprogramador_hog_HogComparator.h”. Este arquivo precisa ser copiado para a plasta hog/cplusplus, e será importado no fonte cpp.



Estou usando o OpenCV para ler as imagens e passá-las para o método nativo. Eu uso a classe Mat, do OpenCV para ler as imagens com a função imread, também da OpenCV:



Mat photo1 = imread(args[0]);
Mat photo2 = imread(args[1]);
HogComparator hg = new HogComparator();
System.out.println("Images are from the same person? "
+ hg.compareFaces(photo1.getNativeObjAddr(),photo2.getNativeObjAddr()));

O método nativo recebe os endereços das estruturas Mat na memória, o que pode ser obtido com o método getNativeObjAddr(). Facilita muito a comunicação com o C++.



3 – A parte C++



Na verdade, dava para fazer tudo diretamente em Java, sem necessitar do C++. Poderíamos ter utilizado a própria OpenCV para calcular a matriz HOG. Mas, por questões de performance e por praticidade, certas coisas ficam melhor em C++.



Eu criei um “Java binding”, ou seja, um pequeno código C++ que compila gerando uma Shared Library. Para que possa se comunicar com a parte Java, eu preciso importar aquele header gerado no passo anterior:
#include <jni.h>
#include <iostream>
#include <cstdlib>
#include "com_obomprogramador_hog_HogComparator.h"

O código C++ recebe os endereços das estruturas Mat, transformando-as no tipo array2d, utilizado pela dlib:



JNIEXPORT jboolean JNICALL Java_com_obomprogramador_hog_HogComparator_compareFaces
(JNIEnv * env, jobject obj, jlong addFoto1, jlong addFoto2) {
const char* pPath = getenv ("HOGCOMPARATOR_PATH");
std::string path(pPath);
cv::Mat* pInputImage = (cv::Mat*)addFoto1;
cv::Mat* pInputImage2 = (cv::Mat*)addFoto2;
dlib::array2d<rgb_pixel> dlibImage;
dlib::array2d<rgb_pixel> dlibImage2;
dlib::assign_image(dlibImage, dlib::cv_image<bgr_pixel>(*pInputImage));
dlib::assign_image(dlibImage2, dlib::cv_image<bgr_pixel>(*pInputImage2));

Um detalhe importante é que eu precisarei carregar os dois arquivos de modelo, que podem ser obtidos de:
http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2
http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2



Basta descompactar e depois criar uma variável de ambiente chamada HOGCOMPARATOR_PATH, apontando para o path onde você descompactou os dois arquivos.



O resto, já foi explicado quando eu falei da dlib: Detecta os rostos, extrai os rostos, calcula as matrizes e depois compara as distâncias:
bool thereIsAmatch = false;
for (size_t i = 0; i < face_descriptors1.size(); ++i)
{
for (size_t j = i; j < face_descriptors2.size(); ++j)
{
if (length(face_descriptors1[i]-face_descriptors2[j]) < 0.6)
thereIsAmatch = true;
}
}

return thereIsAmatch;




Compilar a parte C++ é meio doloroso… Como eu disse, a dlib já está embutida no nosso CmakeLists.txt, mas é preciso instalar a OpenCV em sua estação. Como eu disse, estou utilizando Ubuntu 18 e OpenCV 3.4.2-1. Se quiser utilizar uma versão mais nova de OpenCV, saiba que não existe a biblioteca Java para ela. Eu utilizo o projeto org.openpnp, do repositório Maven, para facilitar a integração do código Java com a OpenCV.



O site abaixo ensina a instalar e compilar a OpenCV:
https://docs.opencv.org/3.4.2/d7/d9f/tutorial_linux_install.html



Uma vez que a OpenCV esteja instalada, você pode compilar a parte C++. Para isto, copie o arquivo header gerado para dentro da pasta cplusplus (se você o alterou), e abra um terminal:
  1. cd hog/cplusplus
  2. mkdir build
  3. cd build
  4. cmake ..
  5. cmake --build . --config Release



Quando terminar de compilar, você terá um arquivo “libhogcomparator.so” dentro da pasta build. Esta é a biblioteca que implementa o método nativo.



Para executar o projeto no eclipse, abra o menu RUN e depois RUN CONFIGURARIONS. Crie uma configuração para executar “Java Application”, selecione a classe principal (HogComparator) e adicione dois argumentos, que são os paths das imagens que deseja comparar. Acrescente também um argumento para a JVM, que é o -Djava.library.path, apontando para a pasta cplusplus/build. Finalmente, crie a variável de ambiente apontando para o path onde você descompactou os dois arquivos de modelos.
  • Argumentos de linha de comando, por exemplo: /home/cleuton/Documentos/projetos/hog/etc/cleuton.jpg /home/cleuton/Documentos/projetos/hog/etc/thomas_edison.jpg
  • Argumento de localização da “libhogcomparator”: -Djava.library.path=/home/cleuton/Documentos/projetos/hog/cplusplus/build
  • Variável de ambiente: HOGCOMPARATOR_PATH=/home/cleuton/Documentos/projetos/hog/cplusplus/build

Conclusão

Este breve tutorial mostrou a você como pode incluir reconhecimento facial em sua aplicação, utilizando Java e C++, com excelente desempenho. Agora, você pode transformar a parte Java em um RESTful service e colocar como parte de uma aplicação mobile sua, oferecendo reconhecimento facial como meio de autenticação.

Cleuton Sampaio, M.Sc.






Nenhum comentário:

Postar um comentário