Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save dantetesta/891c3e88c88769f384f49e383883d3a7 to your computer and use it in GitHub Desktop.

Select an option

Save dantetesta/891c3e88c88769f384f49e383883d3a7 to your computer and use it in GitHub Desktop.
Sistema de Detecção e Reconhecimento Facial

Documentação: Sistema de Detecção e Reconhecimento Facial

Autor: Dante Testa
Data: 29/11/2024
Versão: 1.0.0


Índice

  1. Visão Geral
  2. Bibliotecas Utilizadas
  3. Modelos de IA
  4. Configuração Inicial
  5. Captura de Webcam
  6. Detecção de Faces
  7. Extração de Landmarks
  8. Extração de Descriptors
  9. Reconhecimento Facial
  10. Desenho no Canvas
  11. Exemplo Completo
  12. Dicas de Performance

Visão Geral

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.


Bibliotecas Utilizadas

face-api.js

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>

Modelos de IA

Os modelos precisam ser carregados antes de usar a detecção. Eles são arquivos .json e .bin hospedados em CDN.

URL dos Modelos

const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/model';

Modelos Disponíveis

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

Carregando os Modelos

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!');
}

Configuração Inicial

HTML Básico

<!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>

CSS Importante

/* Posicionar canvas sobre o vídeo */
#video, #canvas {
    position: absolute;
    top: 0;
    left: 0;
}

Captura de Webcam

Ativando a Câmera

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);
}

Parando a Câmera

function pararCamera() {
    const video = document.getElementById('video');
    
    if (video.srcObject) {
        video.srcObject.getTracks().forEach(track => track.stop());
    }
}

Detecção de Faces

Detectar Uma Face

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;
}

Detectar Múltiplas Faces

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;
}

Objeto Detection

// 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
    }
}

Extração de Landmarks

Landmarks são 68 pontos do rosto (olhos, nariz, boca, contorno).

Detectar com Landmarks

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 dos Landmarks (68 pontos)

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

Extração de Descriptors

O descriptor é um array de 128 números que representa unicamente uma face. É usado para comparação e reconhecimento.

Detectar com Descriptor

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;
}

Salvando Descriptor

// 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));

Reconhecimento Facial

O reconhecimento compara descriptors para identificar pessoas.

Criando LabeledFaceDescriptors

// 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]
    )
];

Usando FaceMatcher

// 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);
        }
    }
}

Entendendo a Distância

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

Desenho no Canvas

Configurando o Canvas

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');
}

Desenhando Bounding Box

function desenharBox(ctx, box, cor = '#00ff00') {
    ctx.strokeStyle = cor;
    ctx.lineWidth = 3;
    ctx.strokeRect(box.x, box.y, box.width, box.height);
}

Desenhando Cantos em L (Estilo Scanner)

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();
}

Desenhando Texto

function desenharTexto(ctx, texto, x, y, cor = '#ffffff') {
    ctx.fillStyle = cor;
    ctx.font = 'bold 16px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(texto, x, y);
}

Desenhando Landmarks

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();
    });
}

Exemplo Completo

<!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>

Dicas de Performance

1. Escolha do Detector

// TinyFaceDetector = Rápido, menos preciso
new faceapi.TinyFaceDetectorOptions({ inputSize: 320 })

// SsdMobilenetv1 = Lento, mais preciso
new faceapi.SsdMobilenetv1Options({ minConfidence: 0.5 })

2. InputSize do TinyFaceDetector

// 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

3. Intervalo de Detecção

// Opção 1: requestAnimationFrame (melhor fluidez)
function loop() {
    detect();
    requestAnimationFrame(loop);
}

// Opção 2: setInterval (controle de FPS)
setInterval(detect, 100); // ~10 FPS

4. Reduzir Resolução do Vídeo

// Menor resolução = processamento mais rápido
await navigator.mediaDevices.getUserMedia({
    video: { width: 640, height: 480 }
});

5. Usar Web Workers (Avançado)

Para não travar a UI, a detecção pode rodar em Web Worker separado.


Referências


Licença

Este documento foi criado por Dante Testa para fins educacionais.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment