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
<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
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:
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:
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:
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:
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.
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