Bem provável que você já tenha ouvido falar bastante deste termo. Não a toa, é hoje uma das aplicações muito utilizadas na área de visão computacional. Podendo ser aplicado em diversos eixos como sistemas de segurança, sistemas para, entrenenimento, etc. No entanto, trata-se de uma atividade não tão trivial quando estamos falando dos computadores, pois o mesmo não entende uma imagem como nós seres humanos, através de nossa visão compreendemos, para o computador, a imagem nada mais é do que um conjunto de pixels, onde se deve realizar cálculos matemáticos para extrair alguma informação, ou realizar algum processamento em uma imagem. Neste artigo vamos nos restringir a um case sobre reconhecimento facial utilizando alguns recursos da biblioteca DLib. Trata-se de uma biblioteca recente, que já contém ótimos exemplos de reconhecimento facial, detecção de objetos, etc. A biblioteca está disponível nas linguagem C++ e Python. Antes de comentar sobre o nosso projeto, vamos falar um pouco sobre dois algoritmos de reconhecimento facial muito popular, disponíveis no OpenCV, apesar de não ser o que estaremos utilizando no projeto, é interessante falarmos um pouco sobre eles, para fins de contextualização.
- Algoritmo Eigenfaces: Esse algoritmo utiliza a técnica PCA (Principal Component Analysis - Análise dos Componentes Principais) para realizar a redução de dimensionalidade, com o intuito de passar informações mais discrimantes da face, para a avaliação do algoritmo no reconhecimento das faces. Por utilizar o PCA, estaremos trabalhando nesse algoritmo, com aprendizagem não-supervisionada, ou seja, não precisaremos informar os dados de saída, nesse caso, os labels para cada imagem de treinamento que nosso algoritmo for submetido.
- Algoritmo Fisherfaces: Esse algoritmo trabalha o seu aprendizado de forma supervisionada, precisamos apresentar os labels ou classes das imagens de treinamento para o algoritmo. A extração de características é realizada separadamente, ou seja, a iluminação de uma face, não afetará as demais faces.
Perceba que temos uma imagem de entrada e como saída teremos um conjunto de classes para que a nossa rede vai realize a associação, geralmente essa camada, geralmente utiliza a função de ativação softmax que calcula as probabilidades entre as diferentes classes. Mas em comparação com uma rede neural profunda, não temos nenhuma diferença, a diferença ocorre pela existência da convolução, onde será aplicado um filtro numa imagem que estará percorrendo-a a fim de encontrar as características da imagem. A sacada é que após a convolução, ocorre o subsampling, também chamado de pooling, isso acontece por que a CNN requer muito esforço computacional, quanto maior a imagem, mais filtros serão aplicado à imagem, para otimizar o tempo de processamento de uma CNN, podemos utilizar o pooling, como o max pooling, que vai realizar um agrupamento dos maiores valores de um filtro que foi utilizado na convolução. É perceptível pela imagem, que à medida que aplicamos convolução e subsampling, a profundidade do conjunto de dados aumentará, ao mesmo tempo que estaremos reduzindo a largura e altura. Feito essa explicação, vamos comentar um pouco sobre o projeto que estaremos mostrando aqui nesse artigo. A sequência de passos será o seguinte:
- Detecção de faces: O primeiro passo para reconhecer faces, é que consigamos inicialmente detectá-las. Para isso, vamos utilizar uma função disponível na biblioteca do DLib.
- Detecção de pontos faciais: Antes do reconhecimento facial, precisamos utilizar técnicas que permitam realizar a detecção das regiões de interesse em uma imagem, como estaremos trabalhando com detecção de faces, um exemplo de região de interesse pode ser detectar pontos faciais.
- Utilização de CNN: Por último, faremos o treinamento de um modelo de CNN pré-treinado para treinar nossas fotos a fim de estarmos aptos para realizar o reconhecimento facial.
Nas
etapas 2 e 3, estaremos alguns modelos já disponibilizados na internet, para
facilitar o processo de reconhecimento facial. Esses modelos, poderão ser
encontrados nesse repositório do github. Vamos utilizar como exemplo nesse projeto, algumas imagens da série Silicon Valley. Vamos então à primeira etapa, utilizaremos a seguinte imagem:
Posteriormente, utilizaremos essa mesma imagem, como imagem de teste para verificar se ela irá reconhecer a face do Richard, esse personagem ao meio. Como comentamos, vamos utilizar DLib, nosso código ficará assim:
Esse exemplo utiliza as bibliotecas do Opencv e do DLib. Na linha 4, apresentamos a imagem que estaremos realizando a detecção facial. Na linha 5, utilizamos um dos recursos disponíveis do DLib que é o detector de faces, através do método get_frontal_face_detector(). Existem outros algoritmos para detectar faces como o haarcascades que é baseado no Adaboost. Já esse algoritmo do DLib, é baseado no HOG (Histograma de Gradientes Orientados). A sua caixa delimitadora (bounding box) é construída utilizando os parâmetros left, top, right e bottom. Continuando no código, na linha 6, chamamos o detector passando como parâmetro a imagem que queremos detectar e o tamanha da escala. E nas linhas 7 a linha 15, são procedimentos padrões, inclusive bem similares aos que é feito com o algoritmo haarcascade, percorremos pixel a pixel e desenhamos um retângulo sob a face detectada em cada foto. A atenção especial aqui, é lembrar que o HOG trabalha com os parâmetros left, top, right e bottom. Executando o código, teremos o seguinte resultado:Posteriormente, utilizaremos essa mesma imagem, como imagem de teste para verificar se ela irá reconhecer a face do Richard, esse personagem ao meio. Como comentamos, vamos utilizar DLib, nosso código ficará assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import cv2 import dlib imagem = cv2.imread("fotos/equipe.8.jpg") detector = dlib.get_frontal_face_detector() facesDetectadas = detector(imagem, 3) print(facesDetectadas) print("Faces detectadas: ", len(facesDetectadas)) for face in facesDetectadas: e, t, d, b = (int(face.left()), int(face.top()), int(face.right()), int(face.bottom())) cv2.rectangle(imagem, (e, t), (d, b), (0, 255, 255), 2) cv2.imshow("Detector hog", imagem) cv2.waitKey(0) cv2.destroyAllWindows() |
Legal, temos o resultado esperado. Vamos avançar agora à 2º etapa, agora que temos a bounding box, vamos extrair a região de interesse. Para isso, utilizaremos um dos recursos presente no código do Github que eu coloquei o link aqui no artigo. Esse recurso é um preditor específico para trabalhar com pontos faciais, seu objetivo, é detectar 68 pontos faciais. Vamos então utilizá-lo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import dlib import cv2 import numpy as np def imprimeLinhas(imagem, pontosFaciais): p68 = [[0, 16, False], # linha do queixo [17, 21, False], # sombrancelha direita [22, 26, False], # sombancelha esquerda [27, 30, False], # ponte nasal [30, 35, True], # nariz inferior [36, 41, True], # olho esquerdo [42, 47, True], # olho direito [48, 59, True], # labio externo [60, 67, True]] # labio interno for k in range(0, len(p68)): pontos = [] for i in range(p68[k][0], p68[k][1] + 1): ponto = [pontosFaciais.part(i).x, pontosFaciais.part(i).y] pontos.append(ponto) pontos = np.array(pontos, dtype=np.int32) cv2.polylines(imagem, [pontos], p68[k][2], (255, 0, 0), 2) imagem = cv2.imread("fotos/equipe.8.jpg") detectorFace = dlib.get_frontal_face_detector() detectorPontos = dlib.shape_predictor("recursos/shape_predictor_68_face_landmarks.dat") facesDetectadas = detectorFace(imagem, 2) for face in facesDetectadas: pontos = detectorPontos(imagem, face) imprimeLinhas(imagem, pontos) cv2.imshow("Pontos faciais", imagem) cv2.waitKey(0) cv2.destroyAllWindows() |
Ótimo! Veja que essa segunda etapa foi concluída como esperávamos. Agora, partimos para a terceira etapa. Para isso, iremos utilizar o personagem Richard, que é o personagem do meio nesta foto, vamos treinar o nosso algoritmo com três fotos dele, e depois faremos testes nas imagens que contém toda a sua equipe, para ver se o nosso algoritmo reconhece o Richard. Antes de começar os trabalhos, faço aqui uma ressalva, caso você prefira utilizar algoritmos como Fisherfaces ou Eigenfaces, saiba que seu algoritmo precisará de muito mais fotos do que apenas três. Vamos usar as seguintes fotos no treinamento:
Nosso código de treinamento das imagens será:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import os import glob import _pickle as cPickle import dlib import cv2 import numpy as np detectorFace = dlib.get_frontal_face_detector() detectorPontos = dlib.shape_predictor("recursos/shape_predictor_68_face_landmarks.dat") reconhecimentoFacial = dlib.face_recognition_model_v1("recursos/dlib_face_recognition_resnet_model_v1.dat") indice = {} idx = 0 descritoresFaciais = None for arquivo in glob.glob(os.path.join("fotos/treinamento", "*.jpg")): imagem = cv2.imread(arquivo) facesDetectadas = detectorFace(imagem, 1) for face in facesDetectadas: pontosFaciais = detectorPontos(imagem, face) descritorFacial = reconhecimentoFacial.compute_face_descriptor(imagem, pontosFaciais) listaDescritorFacial = [df for df in descritorFacial] npArrayDescritorFacial = np.asarray(listaDescritorFacial, dtype=np.float64) npArrayDescritorFacial = npArrayDescritorFacial[np.newaxis, :] if descritoresFaciais is None: descritoresFaciais = npArrayDescritorFacial else: descritoresFaciais = np.concatenate((descritoresFaciais, npArrayDescritorFacial), axis=0) indice[idx] = arquivo idx += 1 np.save("recursos/descritores_r.npy", descritoresFaciais) with open("recursos/indices_r.pickle", 'wb') as f: cPickle.dump(indice, f) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import os import glob import _pickle as cPickle import dlib import cv2 import numpy as np detectorFace = dlib.get_frontal_face_detector() detectorPontos = dlib.shape_predictor("recursos/shape_predictor_68_face_landmarks.dat") reconhecimentoFacial = dlib.face_recognition_model_v1("recursos/dlib_face_recognition_resnet_model_v1.dat") indices = np.load("recursos/indices_r.pickle") descritoresFaciais = np.load("recursos/descritores_r.npy") limiar = 0.5 for arquivo in glob.glob(os.path.join("fotos", "*.jpg")): imagem = cv2.imread(arquivo) facesDetectadas = detectorFace(imagem, 2) for face in facesDetectadas: e, t, d, b = (int(face.left()), int(face.top()), int(face.right()), int(face.bottom())) pontosFaciais = detectorPontos(imagem, face) descritorFacial = reconhecimentoFacial.compute_face_descriptor(imagem, pontosFaciais) listaDescritorFacial = [fd for fd in descritorFacial] npArrayDescritorFacial = np.asarray(listaDescritorFacial, dtype=np.float64) npArrayDescritorFacial = npArrayDescritorFacial[np.newaxis, :] distancias = np.linalg.norm(npArrayDescritorFacial - descritoresFaciais, axis=1) print("Distâncias: {}".format(distancias)) minimo = np.argmin(distancias) print(minimo) distanciaMinima = distancias[minimo] print(distanciaMinima) if distanciaMinima <= limiar: nome = os.path.split(indices[minimo])[1].split(".")[0] else: nome = ' ' cv2.rectangle(imagem, (e, t), (d, b), (0, 255, 255), 2) texto = "{} {:.4f}".format(nome, distanciaMinima) cv2.putText(imagem, texto, (d, t), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.5, (0, 255, 255)) cv2.imshow("Reconhecimento facial", imagem) cv2.waitKey(0) cv2.destroyAllWindows() |
Perceba que esse código é bem similar ao anterior. Porém, temos algumas especificidades aqui, a primeira delas, é exportar os arquivos que gravamos no treinamento, que foram os índices e descritores. Definimos um valor de limiar também, por que agora, vamos comparar os valores de cada imagem de teste, com os valores dos descritores faciais que foram gerados pela imagem de treinamento. Nesse caso, a distância entre esses valores, deve ser menor ou igual ao que definimos no limiar, para que então, possamos certificar de que a face é de Richard. E por último, vamos ao que interessa. Vamos ver se como ficou nosso teste de reconhecimento facial nas imagens.
Veja que nessa primeira foto, apesar de Richard está com a face meio virada, ainda assim o algoritmo o reconheceu com aproximadamente 0.48 de distância. Lembre-se, como definimos o limiar de 0.50, os valores acima, não serão considerados como Richard. Vamos para próxima foto:
Essa segunda foto, também nosso algoritmo conseguiu reconhecer o Richard com um pouco mais de precisão, 0.42. Note que o reconhecimento se dá pelo texto que é anexado ao lado da face, e por fins de conhecimento, deixamos visível a bounding box (caixa delimitadora) de detecção de faces, nas demais pessoas também. Vamos a próxima foto:
Note que nessa terceira imagem, também o Richard foi reconhecido corretamente. Até agora três imagens que submetemos a teste, as três, obtivemos resultado desejado. Vamos a quarta foto:
Veja que nessa foto também nosso algoritmo conseguiu reconhecer o Richard e com uma precisão muito boa, inclusive.
Outra foto, e tivemos sucesso também no reconhecimento facial de Richard, perceba que tivemos uma melhor precisão nessa imagem.
Achei essa foto bem legal por dois motivos: Ela reconheceu o Richard através de sua face pessoal, e também através da imagem que está no poster ao fundo. E a outra é por ter detectado a face que está na camisa do Erlich.
Nem tudo são mar de rosas na vida. Veja que nessa imagem, o nosso algoritmo sequer detectou a face de Richard, logo ele não poderia reconhecê-lo. Essa imagem, apresenta uma dificuldade muito alta, pois a sua face está totalmente invertida.
Outra imagem que conseguimos obter êxito no reconhecimento de Richard, mesmo pelo fato de que sua face esteja um pouco curvada para baixo.
E por último, essa imagem nos mostra que o algoritmo conseguiu detectar a face de Richard, porém a distância entre os descritores faciais de treinamento e a imagem de teste foi maior que nosso limiar (0.5), nesse caso basta aumentar um pouco o nosso limiar, e teremos também o reconhecimento facial de Richard nessa imagem.Concluí-se então que tivemos uma ótima experiência com o algoritmo utilizado. De 9 imagens de teste, 7 conseguimos reconhecer o nosso alvo. E ainda vai um acréscimo, utilizamos apenas 3 imagens para treinamento, repito, apenas 3. Quando temos um dataset, é muito comum divirmos nossos dados em 75/25, sendo 75% treinamento e 25% teste, aqui fizemos o contrário, apenas para fins de avaliação mesmo, dedicamos mais imagens para teste do que para treinamento, e concluímos que nosso algoritmo se saiu muito bem. Espero que o artigo tenha sido útil e que de alguma forma possa contribuir positivamente para o conhecimento dos que vierem lê-lo. Até a próxima!