#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()));
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"
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));
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:
-
cd hog/cplusplus
-
mkdir build
-
cd build
-
cmake ..
-
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