Manual

do

Maker

.

com

OpenFace e um pouco de Docker

OpenFace e um pouco de Docker

Recentemente escrevi sobre reconhecimento facial utilizando rede neural com o OpenFace, que é uma caixinha mágica com tudo pronto dentro dele. O caso é que do modo mais simples, escrevi como utilizá-lo baixando a imagem dele utilizando docker. Partindo disso, iniciei o container com a porta 8000 para mostrar a streaming em tempo real, mas não mostrei um treinamento para reconhecimento por linha de comando nem mostrei como reutilizar um container. Vamos ver esses detalhes todos agora, utilizando openface e um pouco de docker.

Docker

Não sou especialista em docker, aliás, nunca vi mais gordo nem mais magro; ok, uma vez em 2014 na Alemanha, mas mal pude compreender com o que eu estava lidando na época. Para utilizar essa imagem não tive trabalho nenhum, apenas instalei o docker como descrito nesse artigo e utilizei em seguida. Só que depois fui acessar o container outra vez, mas na verdade eu acabei criando outro. Então a regra básica é a seguinte:

docker pull bamos/openface

Com esse comando, baixei a imagem bamos/openface, que o B. Amos deixou prontinha pra uso, facilitando ainda mais a vida de newbies como eu.

docker run -p 9000:9000 -p 8000:8000 -t -i bamos/openface /bin/bash

Com isso eu criei meu container baseado nessa imagem que baixei. Isso me fez entrar diretamente no console do container, porque foi o comando que dei ao final; acessar o shell. Quando sair dessa imagem, ela ainda existirá. Se eu colocar esse comando novamente, criarei um novo container.

Outra coisa interessante em relação ao container é que você pode cria-lo já com um nome específico, como por exemplo, DobitAoByte:

docker run -p 9000:9000 -p 8000:8000 -name DobitAoByte -t -i bamos/openface /bin/bash

Porém, se você esqueceu de nomear na criação do container, você pode renomeá-lo posteriormente:

docker rename VELHO_NOME NOVO_NOME

Só que se voce esqueceu de dar um nome ao criá-lo, certamente você não sabe o nome que foi atribuido. Para isso, um comando bastante simples:

docker ps -a

Se utilizar apenas 'ps' como parâmetro, apenas as imagens em execução serão mostradas. Executando esse comando você poderá ver o nome da imagem no último campo.

Mas aí você sai do container e ele para de rodar. Como supracitado, executar 'run' novamente criará um novo container, então, como reconectar ao container 'DobitAoByte'? - Simples responder - primeiro inicie o container e depois conecte-se a ele:

docker start DobitAoByte
docker attach DobitAoByte

O Docker faz isolamento de processos. Você pode criar um container pra cada tarefa que desejar fazer de forma independente e se precisar executar um comando em um container que já esteja rodando, mesmo que isso não faça parte da tarefa à qual esse container foi dedicado, você pode fazer assim, por exemplo:

docker exec DobitAoByte ls

Isso executará um ls no container e exibirá em stdout, como se o ls fosse em seu próprio shell, mas mostrando o conteúdo do container.

O último recurso sobre docker que desejo mostrar é o compartilhamento de um diretório do sistema nativo com o container, porque precisaremos disso para mandar a imagem do sistema nativo para o container para que seja feito o reconhecimento. O processo é simples também:

docker run -p 9000:9000 -p 8000:8000 -v /home/djames/shared:/home/user/shared -t -i bamos/openface /bin/bash

Nesse caso, criei o container já apontando o 'diretorio/de/origem:/diretorio/de/destino', com a flag '-v'.

Tem mais um recurso legal que é linkar containers, mas vou deixar isso pra quando for utilizar, porque esse não é um tutorial de docker, mas um complemento à sua utilização junto do OpenFace. Vamos agora criar nosso treinamento do OpenFace.

Do classificador à predição

Vou explicar todos os passos, sendo que alguns deles podem ser automatizados. Primeiro, tenha em mente que o OpenFace é um conjunto de ferramentas para reconhecimento facial e que faz uso da rede neural Torch, tendo a Intel NVidia e Google envolvidos no projeto.

Não posso entrar em detalhes sobre redes neurais porque estou gatinhando nisso, mas posso afirmar o que será necessário para fazer um treinamento com o OpenFace

Passo 1 - Filmar o rosto

A primeira fase é filmar o rosto da pessoa que deverá ser reconhecida. Um fundo neutro (preferencialmente branco) e luz controlada podem ser bons pontos de partida. Eu gravei em torno de 17 segundos cada pessoa, pedindo que fizesse expressões durante a filmagem.

Passo 2 - Disponibilização das filmagens

Eu filmei com a boa câmera do Galaxy S6 com boa resolução. Depois fiz upload para o Dropbox e apanhei as filmagens em meu notebook.

Passo 3 - Extração dos frames

Nesse passo, converti todas as filmagens de rosto para imagens png, de modo a garantir que não se percam frames para o próximo passo. Para tal, utilizei o ffmpeg com o seguinte comando:

ffmpeg -i arquivo_de_entrada X_%02d.png

Troque 'X' por um ID correspondente à pessoa, ou o nome, se preferir.

passo 4 - Fazer a separação das faces e redimensionamento

Obviamente você não vai abrir o gimp e ficar recortando imagem por imagem. Nesse ponto, você deverá tirar as faces utilizando o face detection com Haar Cascades.

# -*- coding: utf-8 -*-
from  __future__ import print_function
import numpy as np
import cv2
import time
import PIL
import datetime
import countourManipulation
import os

class faceCropper:
    def __init__(self,facesLocationTosave="/tmp/temp/"):
        self.face_cascade        = cv2.CascadeClassifier('/home/djames/opencv/data/haarcascades/haarcascade_frontalface_default.xml')
        self.frame               = ""
        self.FACES_LOCATION      = facesLocationTosave
        if not os.path.isdir(self.FACES_LOCATION):
            os.mkdir(self.FACES_LOCATION)

    def saveToFile(self,img,TARGET,imgOriginalName):
        filename = imgOriginalName.split(".")[0]
        if len(filename) < 10:
            for i in range(10-len(filename)):
                filename += "0"
        filename += ".jpg"
        cv2.imwrite(TARGET+filename,img)

    """
    faceDetect
    Faz a deteccao da face e salva em self.FACES_LOCATION com o nome original. O nome original eh o timestamp pego com long(time.time()).

    if w < 868 eh um gatilho falso para permitir faces maiores. Talvez seja necessario limitar o tamanho minimo,
    dependendo da reação do faceRecognition
    """
    def faceDetect(self,img,nameToSave):
        faces = self.face_cascade.detectMultiScale(img, scaleFactor=1.3, minNeighbors=5, flags=cv2.CASCADE_SCALE_IMAGE,minSize=(50, 50), maxSize=None)

        if len(faces) > 0:
            print("Pessoa detectada!")
        for (x, y, w, h) in faces:
            if w < 868:
                print(x, y)
                print(w, h)

                roi_gray = img[y - 20:y + h + 20, x - 20:x + w + 20]
                self.saveToFile(roi_gray,self.FACES_LOCATION,nameToSave)
                cv2.rectangle(img, (x - 10, y - 20), (x + w + 10, y + h + 10), (0, 255, 0), 2)

Em outra classe Python, faça o loop do diretório de imagens raw e chame o método faceDetect dessa classe.

Fiz isso em pessoa por pessoa, separado em diretórios na estrutura do próximo passo. Por razões muito específicas, não salvei diretamente como grayscale, mas será necessário converter e redimensionar as imagens. Primeiro, converter tudo pra grayscale:

cd diretorio_da_pessoa
ls| while read line; do convert $line.png -colorspace Gray $line.jpg;done

Esse resizer é um tesouro que confesso, não queria compartilhar,  mas não vejo outra forma de escalonar proporcionalmente a imagem, que será fundamental para o próximo passo (perdão mestre Lontra, por compartilhar um segredo das artes ninjaCV).

import cv2
import sys

def resize(frame, downscale=None, size=None, fx=0, fy=0, fixedWidth=None, fixedHeight=None, interpolation=cv2.INTER_CUBIC):
	'''
	Resize image
	size          = Resize image with fixed dimension
	downscale         = If set, ignores the value of `size` and resize dividing the image dimensions
	fx            = downscale factor along the horizontal axis
	fy            = downscale factor along the vertical axis
	fixedWidth    = resize image by width keeping aspect ratio
	fixedHeight   = resize image by height keeping aspect ratio
	interpolation = default is bicubic interpolation over 4x4 pixel neighborhood
	'''
	sz = None if not size else size
    
	if fixedWidth:
		imh, imw = frame.shape[:2]
		new_height = int(float(fixedWidth) * (float(imh) / float(imw)))
		sz = (fixedWidth, new_height)
		
	if fixedHeight:
		imh, imw = frame.shape[:2]
		new_width  = int(float(fixedHeight) * (float(imw) / float(imh)))
		sz = (new_width, fixedHeight)
    
	if downscale:
		sz = (int(frame.shape[1] // downscale), int(frame.shape[0] // downscale))
		
	resizedim = cv2.resize(frame, sz, fx=fx, fy=fy, interpolation=interpolation)
	return resizedim 

img = cv2.imread(sys.argv[1], 0)

# tamanho fixo
# resize(img, size=(20, 20))

## fixando a largura
imagem = resize(img, fixedWidth=100)
cv2.imwrite(sys.argv[1],imagem)
print imagem.shape
## fixando a altura
#img = resize(img, fixedHeight=100)
#print img.shape[:2]

# por factor (%)
#img = resize(img, fx=2.5, fy=1.5)
#print img.shape[:2]

# downscale
# resize(img, downscale=2)

Passo 5 - Montar a estrutura

Para o treinamento, você deve montar uma estrutura de diretórios com cada indivíduo, não importanto o nome dos diretórios. Eu estou utilizando ID para casar com um sistema que estou desenvolvendo (cujo sistema não posso dar detalhes, infelizmente).

A estrutura de diretórios ficou assim:

RAW
|---Pessoa_1
        |-----1-00.png
        |-----1-01.png
        |-----1....
|---Pessoa_2
        |-----2-00.png
        |-----2-....
|---Pessoa_N
        |......

Não importa o nome nem o número de imagens, mas quanto mais imagens treinadas, melhor a interpretação da rede neural. Eu passei uma média de 600 imagens por pessoa, por isso preferi fazer a filmagem e extrair frames invés de fotografar.

Passo 6 - Copiar a estrutura para o container

Faça o compartilhamento com o docker conforme exemplificado acima. Você precisará criar seu container como descrito, com as portas etc. Só não esqueça de adicionar o diretório compartilhado. No meu caso, criei em /tmp/mydataset e expus esse diretório para o container. De outro modo, você poderia duplicar os dados, fazendo copia para dentro do container:

docker cp RAW DobitAoByte:/tmp/

 Passo 7 - Fazer o alinhamento das imagens

Pois bem, esse é mais um ponto importantíssimo a considerar; o alinhamento das imagens. Todas devem estar alinhadas à altura dos olhos. E para fazer isso manualmente? Uma eternidade! E é aí que entra em ação uma das ferramentas do OpenFace, que utiliza a dlib para fazer o alinhamento. Nesse ponto, as imagens já estão dentro do container e a ferramenta está em /home/openface. Entre no diretório e execute o comando (ex.: /tmp/mydataset dentro do container, iniciado com apenas 3 pessoas):

mkdir /tmp/mydataset/aligned && for N in {1..9}; do ./home/openface/util/align-dlib.py /tmp/mydataset/RAW/ align outerEyesAndNose /tmp/mydataset/aligned --size 96 ; done

Esse processo demora de forma proporcional ao número de imagens no diretório, mas não é um processo demorado por imagem e você tem um feedback do alinhamento, fique tranquilo.

Passo 8 - Criar representações

As representações e labels são criadas a partir das imagens alinhadas. Como você já criou uma estrutura de diretórios com cada indivíduo dentro, o processo será simples aqui:

mkdir /tmp/mydataset/feature && ./home/openface/batch-represent/main.lua -outDir /tmp/mydataset/feature -data /tmp/mydataset/aligned

Passo 9 - Criar o classificador

Esse é o último passo do processo antes de poder fazer predição. O script que treina é o script que você utilizará para a predição. O treino é feito com o seguinte comando:

./home/openface/demos/classifier.py train /tmp/mydataset/feature

Quando terminar o processo, divirta-se fazendo a predição.

Mesmo que seja a imagem de uma pessoa que não esteja treinada para o reconhecimento, o OpenFace fará uma predição, mas o índice de confidência deverá ser baixo. Considere a margem que lhe seja confiável para "sugerir" que seja realmente a pessoa que foi identificada pela rede neural.

O arquivo gerado no diretório 'feature' terá a extensão 'pkl'.

Passo 10 - Testando a predição

Agora é hora de experimentar!

./home/openface/demos/classifier.py infer ./tmp/mydataset/arquivo.pkl /tmp/mydataset/arquivo_de_teste.png

O resultado de um exemplo:

root@37e02358331e:~/openface# ./demos/classifier.py infer /tmp/featureDir/classifier.pkl /tmp/mydataset/velho-gray.jpg 
/usr/local/lib/python2.7/dist-packages/sklearn/lda.py:4: DeprecationWarning: lda.LDA has been moved to discriminant_analysis.LinearDiscriminantAnalysis in 0.17 and will be removed in 0.19
  "in 0.17 and will be removed in 0.19", DeprecationWarning)

=== /tmp/mydataset/velho-gray.jpg ===
Predict Jonas with 0.97 confidence.

Gostaria de ter tirado pelo menos um print screen disso, mas por acaso o print screen está congelando tudo, e só consigo voltar a usar o notebook depois do reboot (casa de ferreiro, espeto de pau).

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.