Faala aí galera! Tudo certo?
Hoje vamos fazer uma aplicação para realizar reconhecimento facial e identificação de pessoas utilizando os serviços cognitivos do Azure. Como é um serviço exposto via HTTP, podemos fazer a aplicação com a linguagem que quisermos; nesse exemplo vamos utilizar Node.js e Angular. Bora lá!
O post vai ser dividido em duas partes:
- Parte 1 (esse post): vamos ver um pouco de como funciona o Azure Cognitive Services, como ele reconhece faces, como utilizar e criar esse serviço no portal do Azure e por fim vamos cadastrar faces em um “banco de faces” para serem reconhecidas posteriormente.
- Parte 2 (aqui): vamos enviar uma face para o serviço de reconhecimento e ver a mágica acontecer.
O que são os serviços cognitivos do Azure?
Não vou me alongar muito para explicar tudo, mas basicamente os serviços cognitivos do Azure são algoritmos de inteligência artificial encapsulados e expostos em endpoints via HTTP. Para mais informações, veja a documentação aqui.
Como funciona o reconhecimento facial?
Devemos seguir alguns passos após já termos o nosso serviço criado no portal do Azure:
- Criar uma lista de faces;
- Inserir faces na lista de faces que criamos;
- Treinar a lista de faces;
- Reconhecer faces (esse aqui vai ficar na parte 2);
Ou seja, com 4 chamadas de API já temos o nosso reconhecimento facial funcionando!
Como criar um serviço de reconhecimento facial?
No portal do Azure (https://portal.azure.com) vamos até o menu da esquerda, e clicamos na opção “Create a resource”, logo após em “AI + Machine Learning” e por fim em “Face”:
Após isso, preenchemos os dados necessários, como nome, local, grupo de recursos e a opção de cobrança:
*existe uma opção FREE, que tem o limite de 1 conta por assinatura, já estou utilizando ela por isso não coloquei aqui na demo, mas estou utilizando e tá bem tranquilo.
Após a criação, vamos precisar pegar a chave para utilizarmos nas chamadas dos endpoints, o serviço nos disponibiliza duas chaves, na demo vamos utilizar a chave 1:
Mãos na massa!
Agora com a conta criada é só mandar bala! 😀
Criei um cadastro de clientes, para exemplificarmos o reconhecimento facial. Como a ideia é mostrar a funcionalidade do Azure, não vou explicar os detalhes de funcionalidades do Angular e do Node.js, caso tenham alguma dúvida podem me chamar que a gente troca uma ideia.
Mas vamos lá, a tela de cadastro de clientes é bem simples, com algumas informações básicas e uma foto:
O layout é baseado no material design e é um componente que criamos na SMN chamado smn-ui, ele é open-source e está disponível para download e contribuições aqui no GitHub (https://github.com/smn-official/ng-smn-ui).
O código dessa tela ficou mais ou menos dessa forma:
HTML
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="ui-s480"> | |
<form #formCliente="ngForm" class="ui-validate on-dirty on-submit" (submit)="onSubmit(formCliente)"> | |
<ui-card class="elevate-on-toolbar" [class.loading]="loading"> | |
<div class="ui-progress accent" [class.hide]="!loading"> | |
<div class="indeterminate"></div> | |
</div> | |
<ui-toolbar class="flat"> | |
<button class="ui-button flat icon" type="button" uiRipple (click)="_location.back()"> | |
<i class="material-icons">arrow_back</i> | |
</button> | |
<span class="title">{{newRegister ? 'Novo cliente' : (loading ? 'Carregando' : 'Alterando ' + (info.nome || 'cliente'))}}</span> | |
</ui-toolbar> | |
<fieldset [disabled]="saving || loading"> | |
<ui-card-content> | |
<div> | |
<input type="file" name="input" uiInputFile [(ngModel)]="info.imagemPath" [read]="changeImagem.bind(this)" [error]="changeImagemError.bind(this)" | |
#inputNovaImagem #fieldNovaImagem="ngModel" accept="jpg,jpeg,png" max-file-size="15MB" hidden> | |
<div class="picture"> | |
<div *ngIf="!info.imagem && !info.novaImagem" (click)="inputNovaImagem.click()"> | |
<span *ngIf="info.nome">{{info.nome.substring(0, 1)}}</span> | |
<i *ngIf="!info.nome" class="material-icons">assignment_ind</i> | |
</div> | |
<div *ngIf="info.imagem || info.novaImagem" [style.background-image]="'url(' + (info.novaImagem || info.imagem) + ')'"></div> | |
<button type="button" class="ui-button icon raised accent" uiRipple [uiMenuTrigger]="menuPicture" align="left"> | |
<i class="material-icons">photo_camera</i> | |
</button> | |
</div> | |
</div> | |
<div class="ui-flex-container"> | |
<ui-input-container> | |
<input #fieldNome="ngModel" type="text" [(ngModel)]="info.nome" uiInput name="nome" required uiMaxlength="50"> | |
<label>Nome</label> | |
<div class="ui-messages"> | |
<div *ngIf="fieldNome.errors && fieldNome.dirty"> | |
<div class="ui-message error" [hidden]="!fieldNome.pristine && !fieldNome.errors.required"> | |
Nome é obrigatório | |
</div> | |
<div class="ui-message counter error" [hidden]="!fieldNome.errors.uiMaxlength"> | |
{{info.nome ? info.nome.length : 0}}/50 | |
</div> | |
</div> | |
</div> | |
</ui-input-container> | |
</div> | |
<div class="ui-flex-container"> | |
<ui-input-container> | |
<input #fieldCpf="ngModel" id="cpf" type="text" [(ngModel)]="info.cpf" uiInput uiMaskCpf name="cpf" required > | |
<label>CPF</label> | |
<div class="ui-messages"> | |
<div *ngIf="fieldCpf.errors && fieldCpf.dirty"> | |
<div class="ui-message error" [hidden]="!fieldCpf.pristine && !fieldCpf.errors.required"> | |
CPF é obrigatório | |
</div> | |
<div class="ui-message error" | |
[hidden]="!fieldCpf.pristine && !fieldCpf.errors.duplicate"> | |
Já exisite um cliente com esse CPF | |
</div> | |
</div> | |
</div> | |
</ui-input-container> | |
</div> | |
<div class="ui-flex-container"> | |
<ui-input-container> | |
<input #fieldDataNascimento="ngModel" id="dataNascimento" type="text" [(ngModel)]="info.dataNascimento" uiInput uiMaskDate name="dataNascimento" required > | |
<label>Data de Nascimento</label> | |
<div class="ui-messages"> | |
<div *ngIf="fieldDataNascimento.errors && fieldDataNascimento.dirty"> | |
<div class="ui-message error" [hidden]="!fieldDataNascimento.pristine && !fieldDataNascimento.errors.required"> | |
Data de nascimento é obrigatório | |
</div> | |
</div> | |
</div> | |
</ui-input-container> | |
</div> | |
<div class="ui-flex-container"> | |
<ui-input-container> | |
<input #fieldEmail="ngModel" id="email" type="text" [(ngModel)]="info.email" uiInput name="email" required > | |
<label>E-mail</label> | |
<div class="ui-messages"> | |
<div *ngIf="fieldEmail.errors && fieldEmail.dirty"> | |
<div class="ui-message error" [hidden]="!fieldEmail.pristine && !fieldEmail.errors.required"> | |
E-mail é obrigatório | |
</div> | |
</div> | |
</div> | |
</ui-input-container> | |
</div> | |
<div class="ui-flex-container"> | |
<ui-input-container> | |
<input #fieldTelefone="ngModel" id="telefone" type="text" [(ngModel)]="info.telefone" uiInput uiMaskPhone name="telefone" required > | |
<label>Telefone</label> | |
<div class="ui-messages"> | |
<div *ngIf="fieldTelefone.errors && fieldTelefone.dirty"> | |
<div class="ui-message error" [hidden]="!fieldTelefone.pristine && !fieldTelefone.errors.required"> | |
Telefone é obrigatório | |
</div> | |
</div> | |
</div> | |
</ui-input-container> | |
</div> | |
</ui-card-content> | |
</fieldset> | |
</ui-card> | |
<div class="ui-fab-container"> | |
<button class="ui-button success fab" uiRipple [class.hide]="loading"> | |
<ui-progress-radial class="indeterminate" *ngIf="saving"></ui-progress-radial> | |
<i class="material-icons">check</i> | |
</button> | |
</div> | |
</form> | |
<ui-menu #menuPicture> | |
<div class="ui-menu-content size-2x"> | |
<div class="ui-menu-item" uiRipple (click)="inputNovaImagem.click()"> | |
<i class="icon material-icons">add_a_photo</i> | |
{{!info.imagem && !info.novaImagem ? 'Selecionar imagem' : 'Nova imagem'}} | |
</div> | |
<div class="ui-menu-item" uiRipple *ngIf="info.imagem || info.novaImagem" (click)="info.novaImagem = null; info.imagem = null; inputNovaImagem.value = ''"> | |
<i class="icon material-icons">delete</i> | |
Remover | |
</div> | |
</div> | |
</ui-menu> | |
</div> |
Submit do formulário
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
onSubmit(form) { | |
if (!this.saving) { | |
for (const control in form.controls) { | |
if (form.controls.hasOwnProperty(control)) { | |
form.controls[control].markAsTouched(); | |
form.controls[control].markAsDirty(); | |
} | |
} | |
if (!form.valid) { | |
this.element.nativeElement.querySelectorAll('form .ng-invalid')[0].focus(); | |
return false; | |
} | |
this.saving = true; | |
this.clienteService.cadastrar(this.info).subscribe(data => { | |
this.saving = false; | |
UiSnackbar.show({ | |
text: `Cliente cadastrado com sucesso.` | |
}); | |
this.router.navigate(['/cliente'], { replaceUrl: true }); | |
}, e => { | |
this.saving = false; | |
if(e.error.statusCode == 406){ | |
form.controls.cpf.setErrors({ duplicate: true }); | |
this.element.nativeElement.querySelector('#cpf').focus(); | |
} else { | |
UiSnackbar.show({ | |
text: 'Ocorreu um erro interno, tente novamente mais tarde.' | |
}); | |
} | |
}); | |
} | |
} |
Agora, a parte onde toda a mágica acontece, na API onde cadastramos o cliente, vamos passar cada método passo-a-passo:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function inserirCliente(req, res) { | |
let clienteDb = await repository.verificaExisteCliente(req.body.cpf); | |
if (clienteDb) | |
return res.error('Já existe um cliente com esse CPF', 406); | |
let cliente = await repository.inserirCliente(req.body); | |
await _atualizarReconhecimentoFacial(cliente, req.body.novaImagem); | |
res.ok(cliente); | |
} |
Vejam que o controller está bem simples, e tem as seguintes funcionalidades:
- Verificar se existe um outro cliente com o mesmo CPF;
- Inserir um cliente no banco de dados;
- Inserir a imagem do cliente em um banco de imagens;
E é esse o nosso foco, vamos lá!
O método “_atualizarReconhecimentoFacial” faz as seguintes tarefas:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function _atualizarReconhecimentoFacial(idCliente, imagem) { | |
let imageName = `${idCliente}.jpg`; | |
await azureStorage.upload('cliente', imageName, imagem); | |
let face = await reconhecimentoFacial.uploadImagem(imageName); | |
let faceId = face.persistedFaceId; | |
await repository.atualizarFaceId(idCliente, faceId); | |
reconhecimentoFacial.treinarReconhecimento(); | |
} |
Esse método faz basicamente:
- Insere a imagem do cliente em um container utilizando o Azure Blob Storage, tem um post explicando sobre isso aqui;
- Faz upload da imagem para o grupo em que escolhi (calma aí que já explico isso daqui a pouco)
- Atualiza o cliente com o identificador da face
- Isso vai servir para reconhecermos a face na nossa base de dados posteriormente
- Treina o reconhecimento no grupo
- Esse método de treinar é assíncrono, e tem uma outra rota no serviço para verificarmos o status do treinamento. Ele varia de tempo de acordo com a quantidade de imagens no grupo.
Considerem que a variável “faceApi” tenha o seguinte valor pois nosso serviço foi criado com a localização do centro-sul dos Estados Unidos:
let faceApi = "https://southcentralus.api.cognitive.microsoft.com/face/v1.0"
E falando no grupo ou lista de faces, a seguir vamos ver como criar um grupo, vejam:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function criarLargeFaceList() { | |
let config = { | |
method: 'PUT', | |
uri: `${faceApi}/largefacelists/minha-lista`, | |
headers: { | |
'Ocp-Apim-Subscription-Key': faceApiKey | |
}, | |
json: true, | |
body: { | |
name: 'minha-lista', | |
userData: "Minha lista de faces" | |
} | |
}; | |
let response = await request(config); | |
return response; | |
}; |
O método acima criou a lista de faces chamada “minha-lista”, e nessa lista vamos inserir todas as imagens de clientes que cadastrarmos.
Para inserir uma imagem nessa lista, vamos utilizar o seguinte método:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function uploadImagem(imageName) { | |
let config = { | |
method: 'POST', | |
uri: `${faceApi}/largefacelists/minha-lista/persistedfaces`, | |
headers: { | |
'Ocp-Apim-Subscription-Key': faceApiKey | |
}, | |
json: true, | |
body: { | |
url: `${blobUrl}/${imageName}` | |
} | |
}; | |
let response = await request(config); | |
return response; | |
}; |
Vejam que no corpo da requisição estamos enviando um atributo chamado “url”, essa é a url da imagem que estamos enviando para a lista, a API irá nos responder com um status code 200, e o seguinte corpo com o identificador da face que foi inserida na lista de faces:
{
"persistedFaceId": "43897a75-8d6f-42cf-885e-74832febb055"
}
e por fim, vamos treinar a nossa lista. O treinamento serve para podermos fazer o reconhecimento facial, ele deve ser feito toda vez que uma nova face for adicionada a lista. O treino pode variar de tempo de acordo com a quantidade de imagens na lista e por isso é uma tarefa assíncrona.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function treinarReconhecimento() { | |
let config = { | |
method: 'POST', | |
uri: `${faceApi}/largefacelists/minha-lista/train`, | |
headers: { | |
'Ocp-Apim-Subscription-Key': faceApiKey | |
} | |
}; | |
let response = await request(config); | |
return response; | |
}; |
Pronto! A parte 1 está concluída, ou seja, estamos inserindo clientes e os vinculando com uma imagem em nossa lista de imagens.
Após isso treinamos a lista para poder reconhecer as faces posteriormente, que é o que vamos fazer na parte 2 desse post!
Se tiverem qualquer dúvida sobre essa parte, se algo ficou muito vago ou confuso, me enviem feedbacks que irei dar um jeito 😉
Os códigos utilizados estão disponíveis no GitHub:
Por hoje é só isso, qualquer dúvida ou sugestão, estou à disposição! Até mais 😀
Nice cara, parabéns, muito instrutivo. continue com os ótimos posts.
GostarGostar
Valeu pelo feedback Douglas! 😀
GostarGostar