Autor: Dante Testa
Data: 29/11/2024
Versão: 1.0.0
- Visão Geral
- Bibliotecas Utilizadas
- Modelos de IA
- Configuração Inicial
- Captura de Webcam
- Detecção de Faces
- Extração de Landmarks
- Extração de Descriptors
- Reconhecimento Facial
- Desenho no Canvas
- Exemplo Completo
- Dicas de Performance
Este sistema utiliza inteligência artificial no navegador para:
- Detectar rostos em tempo real via webcam
- Extrair landmarks faciais (68 pontos do rosto)
- Gerar descriptors únicos para cada face (128 valores)
- Comparar faces e identificar pessoas cadastradas
Tudo roda 100% client-side (no navegador), sem necessidade de servidor para processamento de IA.
A biblioteca principal é a face-api.js, que é um wrapper JavaScript sobre o TensorFlow.js, facilitando o uso de modelos de detecção facial.
<!-- CDN Principal -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>Repositório: https://github.com/justadudewhohacks/face-api.js
Alternativa (vladmandic fork - mais atualizado):
<script src="https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/dist/face-api.min.js"></script>Os modelos precisam ser carregados antes de usar a detecção. Eles são arquivos .json e .bin hospedados em CDN.
const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/model';| Modelo | Função | Tamanho |
|---|---|---|
tinyFaceDetector |
Detecção rápida de faces | ~190KB |
ssdMobilenetv1 |
Detecção precisa de faces | ~5.4MB |
faceLandmark68Net |
68 pontos do rosto | ~350KB |
faceRecognitionNet |
Gera descriptor único | ~6.2MB |
faceExpressionNet |
Detecta emoções | ~310KB |
ageGenderNet |
Estima idade e gênero | ~420KB |
async function carregarModelos() {
const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/model';
// Detector de faces (escolha um)
await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL); // Rápido
// await faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL); // Preciso
// Landmarks (pontos do rosto)
await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
// Reconhecimento (descriptor)
await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
console.log('Modelos carregados!');
}<!DOCTYPE html>
<html>
<head>
<title>Detecção Facial</title>
</head>
<body>
<!-- Elemento de vídeo para webcam -->
<video id="video" autoplay playsinline></video>
<!-- Canvas para desenhar sobre o vídeo -->
<canvas id="canvas"></canvas>
<!-- Biblioteca -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
<script src="app.js"></script>
</body>
</html>/* Posicionar canvas sobre o vídeo */
#video, #canvas {
position: absolute;
top: 0;
left: 0;
}async function iniciarCamera() {
const video = document.getElementById('video');
// Solicita acesso à câmera
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user' // Câmera frontal
}
});
// Conecta stream ao elemento video
video.srcObject = stream;
// Aguarda o vídeo estar pronto
await video.play();
console.log('Câmera ativada!');
console.log('Resolução:', video.videoWidth, 'x', video.videoHeight);
}function pararCamera() {
const video = document.getElementById('video');
if (video.srcObject) {
video.srcObject.getTracks().forEach(track => track.stop());
}
}async function detectarUmaFace() {
const video = document.getElementById('video');
// Opções do TinyFaceDetector
const options = new faceapi.TinyFaceDetectorOptions({
inputSize: 320, // 128, 160, 224, 320, 416, 512, 608
scoreThreshold: 0.5 // Confiança mínima (0-1)
});
// Detecta uma face
const detection = await faceapi.detectSingleFace(video, options);
if (detection) {
console.log('Face detectada!');
console.log('Posição:', detection.box);
console.log('Confiança:', detection.score);
}
return detection;
}async function detectarTodasFaces() {
const video = document.getElementById('video');
const options = new faceapi.TinyFaceDetectorOptions({
inputSize: 320,
scoreThreshold: 0.5
});
// Detecta todas as faces
const detections = await faceapi.detectAllFaces(video, options);
console.log(`${detections.length} face(s) detectada(s)`);
return detections;
}// Estrutura do objeto retornado
detection = {
score: 0.95, // Confiança (0-1)
box: {
x: 150, // Posição X
y: 80, // Posição Y
width: 200, // Largura
height: 250, // Altura
top: 80,
left: 150,
bottom: 330,
right: 350
}
}Landmarks são 68 pontos do rosto (olhos, nariz, boca, contorno).
async function detectarComLandmarks() {
const video = document.getElementById('video');
const options = new faceapi.TinyFaceDetectorOptions();
// Encadeia detecção + landmarks
const detection = await faceapi
.detectSingleFace(video, options)
.withFaceLandmarks();
if (detection) {
console.log('Landmarks:', detection.landmarks);
console.log('Posições:', detection.landmarks.positions);
}
return detection;
}Pontos 0-16: Contorno do rosto
Pontos 17-21: Sobrancelha esquerda
Pontos 22-26: Sobrancelha direita
Pontos 27-35: Nariz
Pontos 36-41: Olho esquerdo
Pontos 42-47: Olho direito
Pontos 48-67: Boca
O descriptor é um array de 128 números que representa unicamente uma face. É usado para comparação e reconhecimento.
async function detectarComDescriptor() {
const video = document.getElementById('video');
const options = new faceapi.TinyFaceDetectorOptions();
// Encadeia: detecção + landmarks + descriptor
const detection = await faceapi
.detectSingleFace(video, options)
.withFaceLandmarks()
.withFaceDescriptor();
if (detection) {
// Descriptor é um Float32Array com 128 valores
console.log('Descriptor:', detection.descriptor);
console.log('Tipo:', detection.descriptor.constructor.name);
console.log('Tamanho:', detection.descriptor.length);
}
return detection;
}// Converter para JSON (para salvar no banco)
const descriptorJSON = JSON.stringify(Array.from(detection.descriptor));
// Recuperar do JSON
const descriptorArray = new Float32Array(JSON.parse(descriptorJSON));O reconhecimento compara descriptors para identificar pessoas.
// Estrutura: nome + array de descriptors
const labeledDescriptors = [
new faceapi.LabeledFaceDescriptors(
'João', // Nome/Label
[descriptor1, descriptor2] // Array de Float32Array
),
new faceapi.LabeledFaceDescriptors(
'Maria',
[descriptor3, descriptor4, descriptor5]
)
];// Cria o matcher com threshold de distância
// Quanto menor o threshold, mais restritivo (0.6 é o padrão)
const faceMatcher = new faceapi.FaceMatcher(labeledDescriptors, 0.5);
// Compara um descriptor
async function identificarPessoa() {
const detection = await faceapi
.detectSingleFace(video, options)
.withFaceLandmarks()
.withFaceDescriptor();
if (detection) {
const match = faceMatcher.findBestMatch(detection.descriptor);
console.log('Label:', match.label); // 'João' ou 'unknown'
console.log('Distância:', match.distance); // 0-1 (menor = mais similar)
// Calcular confiança
const confianca = Math.round((1 - match.distance) * 100);
console.log('Confiança:', confianca + '%');
if (match.label !== 'unknown') {
console.log('Pessoa identificada:', match.label);
}
}
}Distância 0.0 = Mesma pessoa (100% similar)
Distância 0.4 = Muito provável ser a mesma pessoa
Distância 0.5 = Threshold padrão
Distância 0.6 = Limite aceitável
Distância 1.0 = Completamente diferente
function configurarCanvas() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
// Canvas deve ter o mesmo tamanho do vídeo
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
return canvas.getContext('2d');
}function desenharBox(ctx, box, cor = '#00ff00') {
ctx.strokeStyle = cor;
ctx.lineWidth = 3;
ctx.strokeRect(box.x, box.y, box.width, box.height);
}function desenharCantos(ctx, box, tamanho = 20, cor = '#ffffff') {
const { x, y, width: w, height: h } = box;
ctx.strokeStyle = cor;
ctx.lineWidth = 3;
// Canto superior esquerdo
ctx.beginPath();
ctx.moveTo(x, y + tamanho);
ctx.lineTo(x, y);
ctx.lineTo(x + tamanho, y);
ctx.stroke();
// Canto superior direito
ctx.beginPath();
ctx.moveTo(x + w - tamanho, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + tamanho);
ctx.stroke();
// Canto inferior esquerdo
ctx.beginPath();
ctx.moveTo(x, y + h - tamanho);
ctx.lineTo(x, y + h);
ctx.lineTo(x + tamanho, y + h);
ctx.stroke();
// Canto inferior direito
ctx.beginPath();
ctx.moveTo(x + w - tamanho, y + h);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w, y + h - tamanho);
ctx.stroke();
}function desenharTexto(ctx, texto, x, y, cor = '#ffffff') {
ctx.fillStyle = cor;
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.fillText(texto, x, y);
}function desenharLandmarks(ctx, landmarks) {
ctx.fillStyle = '#00ff00';
landmarks.positions.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
ctx.fill();
});
}<!DOCTYPE html>
<html>
<head>
<title>Detecção Facial Completa</title>
<style>
.container { position: relative; display: inline-block; }
#video, #canvas { position: absolute; top: 0; left: 0; }
#canvas { pointer-events: none; }
</style>
</head>
<body>
<div class="container">
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
</div>
<button id="btnStart">Iniciar</button>
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
<script>
const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/model';
let video, canvas, ctx;
let isRunning = false;
let faceMatcher = null;
// Dados de pessoas cadastradas (normalmente vem do banco)
const pessoasCadastradas = [
// { nome: 'João', descriptors: [Float32Array, Float32Array] }
];
// Inicialização
async function init() {
video = document.getElementById('video');
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
// Carrega modelos
console.log('Carregando modelos...');
await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
console.log('Modelos carregados!');
// Prepara matcher se houver pessoas cadastradas
if (pessoasCadastradas.length > 0) {
const labeled = pessoasCadastradas.map(p =>
new faceapi.LabeledFaceDescriptors(p.nome, p.descriptors)
);
faceMatcher = new faceapi.FaceMatcher(labeled, 0.5);
}
}
// Inicia câmera
async function startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 }
});
video.srcObject = stream;
await video.play();
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
isRunning = true;
detectLoop();
}
// Loop de detecção
async function detectLoop() {
if (!isRunning) return;
const options = new faceapi.TinyFaceDetectorOptions({
inputSize: 320,
scoreThreshold: 0.5
});
// Detecta todas as faces com landmarks e descriptors
const detections = await faceapi
.detectAllFaces(video, options)
.withFaceLandmarks()
.withFaceDescriptors();
// Limpa canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Escala para o tamanho do canvas
const scaleX = canvas.width / video.videoWidth;
const scaleY = canvas.height / video.videoHeight;
// Processa cada detecção
detections.forEach(detection => {
const box = detection.detection.box;
const x = box.x * scaleX;
const y = box.y * scaleY;
const w = box.width * scaleX;
const h = box.height * scaleY;
let nome = 'Desconhecido';
let cor = '#ef4444'; // Vermelho
let confianca = 0;
// Tenta identificar
if (faceMatcher) {
const match = faceMatcher.findBestMatch(detection.descriptor);
if (match.label !== 'unknown') {
nome = match.label;
cor = '#22c55e'; // Verde
confianca = Math.round((1 - match.distance) * 100);
}
}
// Desenha cantos em L
desenharCantos(ctx, { x, y, width: w, height: h }, 20, '#ffffff');
// Desenha nome
ctx.fillStyle = cor;
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.fillText(nome, x + w/2, y - 10);
if (confianca > 0) {
ctx.fillText(confianca + '%', x + w/2, y + h + 20);
}
});
// Próximo frame
requestAnimationFrame(detectLoop);
}
function desenharCantos(ctx, box, tamanho, cor) {
const { x, y, width: w, height: h } = box;
ctx.strokeStyle = cor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x, y + tamanho);
ctx.lineTo(x, y);
ctx.lineTo(x + tamanho, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + w - tamanho, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + tamanho);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y + h - tamanho);
ctx.lineTo(x, y + h);
ctx.lineTo(x + tamanho, y + h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + w - tamanho, y + h);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w, y + h - tamanho);
ctx.stroke();
}
// Eventos
document.getElementById('btnStart').onclick = startCamera;
// Inicializa ao carregar
init();
</script>
</body>
</html>// TinyFaceDetector = Rápido, menos preciso
new faceapi.TinyFaceDetectorOptions({ inputSize: 320 })
// SsdMobilenetv1 = Lento, mais preciso
new faceapi.SsdMobilenetv1Options({ minConfidence: 0.5 })// Valores possíveis: 128, 160, 224, 320, 416, 512, 608
// Menor = mais rápido, menos preciso
// Maior = mais lento, mais preciso
// Para tempo real, use 224 ou 320
inputSize: 320// Opção 1: requestAnimationFrame (melhor fluidez)
function loop() {
detect();
requestAnimationFrame(loop);
}
// Opção 2: setInterval (controle de FPS)
setInterval(detect, 100); // ~10 FPS// Menor resolução = processamento mais rápido
await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});Para não travar a UI, a detecção pode rodar em Web Worker separado.
- face-api.js: https://github.com/justadudewhohacks/face-api.js
- vladmandic fork: https://github.com/vladmandic/face-api
- TensorFlow.js: https://www.tensorflow.org/js
- MediaDevices API: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
Este documento foi criado por Dante Testa para fins educacionais.