Este projeto implementa reconhecimento facial a partir da base Olivetti/AT&T
sem usar biblioteca pronta de reconhecimento, sem OpenCV, sem face_recognition,
sem scikit-learn, sem modelo pre-treinado e sem classificador escondido em API
de alto nivel.
A ideia central e construir, do zero, uma geometria para faces:
- cada imagem vira um vetor em um espaco de dimensao alta;
- a nuvem de faces e centralizada pela face media;
- procuramos as direcoes de maior variacao dessa nuvem;
- projetamos cada rosto nessas direcoes;
- classificamos uma face nova por proximidade nesse espaco reduzido.
O metodo classico resultante e conhecido como Eigenfaces.
A base esta em:
data/olivetti_faces.mat
Ela contem:
- 400 imagens;
- 40 pessoas;
- 10 imagens por pessoa;
- imagens em escala de cinza;
- resolucao
64 x 64no arquivo.matusado aqui.
Fonte historica da base Olivetti/AT&T:
https://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html
Use reconhecimento facial apenas com dados coletados e processados com consentimento.
Uma imagem 64 x 64 em escala de cinza tem
d = 64 * 64 = 4096
pixels. Podemos empilhar esses pixels em um vetor coluna:
Aqui:
iidentifica a imagem;d = 4096;p_{ij}e a intensidade do pixeljda imagemi;- neste projeto, normalizamos os pixels para o intervalo
[0, 1].
Se temos n imagens de treino, montamos a matriz:
No split padrao do projeto:
n = 280
d = 4096
Ou seja, cada rosto vive originalmente em um espaco de 4096 dimensoes. A pergunta e: precisamos mesmo de 4096 coordenadas para distinguir pessoas?
Eigenfaces responde: nao. A maior parte da variacao relevante da base vive em um subespaco muito menor.
A primeira quantidade fundamental e a media das faces de treino:
Cada imagem e entao centralizada:
Em matriz:
onde 1 in R^n e o vetor coluna de uns.
Centralizar nao e detalhe cosmetico. Sem isso, a primeira direcao de maior energia tenderia a apontar para o brilho medio/global das imagens, nao para a variacao entre rostos. A PCA deve explicar variacao ao redor da media, nao a media em si.
Queremos encontrar uma direcao unitaria w in R^d tal que, ao projetar todas as
faces centralizadas nessa direcao, a variancia dos coeficientes projetados seja
maxima.
A projecao escalar da face centralizada \phi_i sobre w e:
Como \phi_i ja tem media zero ao longo da base, a variancia amostral nessa
direcao e proporcional a:
Reescrevendo:
Definimos a matriz de covariancia:
Entao o problema do primeiro componente principal e:
sujeito a:
Essa restricao e obrigatoria. Sem ela, se w fosse uma solucao, 1000w teria
valor objetivo um milhao de vezes maior. O problema ficaria sem sentido.
Para resolver
usamos multiplicadores de Lagrange.
Defina:
Derivando em relacao a w:
No ponto otimo:
logo:
Ou seja: a melhor direcao w e um autovetor da matriz de covariancia. O valor
da variancia nessa direcao e o autovalor correspondente:
Portanto:
- o primeiro componente principal e o autovetor de maior autovalor;
- o segundo e o autovetor de segundo maior autovalor;
- e assim por diante.
Uma unica direcao nao basta. Queremos um subespaco de dimensao k. Escreva:
com:
A projecao de uma face centralizada e:
A energia/variancia preservada pelo subespaco e:
Usando traco:
O problema passa a ser:
Como C e simetrica semidefinida positiva, ela admite decomposicao espectral:
onde:
e assumimos:
Pelo principio de Rayleigh-Ritz, o maximo da variancia preservada por qualquer
subespaco k-dimensional e:
e e atingido escolhendo w_1, ..., w_k como os autovetores associados aos k
maiores autovalores. Assim, PCA nao escolhe um subespaco plausivel; ela escolhe
o subespaco otimo para preservar variancia sob restricao de ortonormalidade.
No nosso caso:
C in R^{4096 x 4096}
Isso significa mais de 16 milhoes de entradas. Ainda e possivel em uma maquina moderna, mas e desnecessario e piora conforme a imagem cresce.
O script usa SVD diretamente na matriz centralizada:
onde:
A in R^{n x d};U in R^{n x r}tem colunas ortonormais;Sigma in R^{r x r}contem valores singulares;V in R^{d x r}tem colunas ortonormais;r = rank(A)na SVD fina.
Agora observe:
Substituindo a SVD:
Como U^T U = I:
Logo:
Isso mostra que as colunas de V sao autovetores da covariancia C, e os
autovalores sao:
onde \sigma_j e o j-esimo valor singular.
No codigo:
_, _, vt = np.linalg.svd(centered, full_matrices=False)
components = vt[:n_components]vt e V^T. Portanto, as primeiras linhas de vt sao exatamente as direcoes
principais transpostas. Cada linha pode ser remodelada como uma imagem 64 x 64; visualmente, essas direcoes sao as eigenfaces.
Se guardamos apenas k componentes, representamos a face por:
Podemos reconstruir uma aproximacao:
Como W tem linhas ortonormais, W^T W e o projetor ortogonal no subespaco
gerado pelas k eigenfaces:
O erro de reconstrucao e:
e sua norma quadratica:
A PCA escolhe o subespaco de dimensao k que minimiza a soma dos erros
quadraticos de reconstrucao sobre o conjunto de treino. Equivalentemente, ela
maximiza a variancia preservada nas projecoes.
Esse e um fato profundo: maximizar variancia projetada e minimizar erro quadratico de reconstrucao linear sao duas faces do mesmo problema.
Rostos humanos alinhados e iluminados de forma parecida nao ocupam
arbitrariamente R^4096. Eles formam uma estrutura muito mais concentrada.
Variacoes como:
- formato geral do rosto;
- posicao dos olhos;
- sombra;
- presenca de oculos;
- expressao;
- orientacao leve da cabeca;
criam direcoes dominantes de variacao. Eigenfaces descobre essas direcoes sem receber semantica alguma. O algoritmo nao sabe o que e um olho, nariz ou boca. Ele so ve vetores e variancia.
Depois da projecao, cada imagem vira um ponto em R^k. Se duas imagens sao da
mesma pessoa, esperamos que seus pontos fiquem relativamente proximos nesse
espaco. Se sao de pessoas diferentes, esperamos distancia maior.
Depois do treino, para cada imagem de treino x_i, guardamos:
e seu rotulo c_i, que indica a pessoa.
Para uma imagem nova x, calculamos:
Entao procuramos o exemplo de treino mais perto:
e predizemos:
No codigo, isso aparece como:
distances = np.linalg.norm(
projected[:, None, :] - model.train_projection[None, :, :],
axis=2,
)
nearest = distances.argmin(axis=1)
return model.train_labels[nearest], distances[np.arange(len(images)), nearest]Isso e deliberadamente simples. Nao ha rede neural, nao ha detector facial, nao ha descritor pre-treinado. A geometria e construida a partir da propria base.
Tambem existe uma alternativa:
python3 face_recognition_from_scratch.py eval --classifier centroidPara cada classe c, calculamos o centroide no espaco projetado:
onde:
Para uma face nova:
O centroide e mais compacto, pois guarda um vetor por pessoa. O vizinho mais proximo guarda todos os exemplos de treino. Na base atual, o vizinho mais proximo funciona melhor porque cada pessoa tem poucas imagens e as variacoes internas de expressao, oculos e iluminacao podem ser grandes demais para um unico centroide representar bem.
O parametro k e --components.
Se k e pequeno demais:
- descartamos informacao discriminativa;
- rostos diferentes podem colapsar para pontos parecidos;
- a acuracia cai por subajuste.
Se k e grande demais:
- preservamos ruido e detalhes acidentais;
- a distancia pode passar a depender de iluminacao ou pixels especificos;
- a acuracia pode cair por sobreajuste.
O padrao atual e:
k = 80
Com split fixo de 7 imagens por pessoa para treino e 3 para teste, o resultado observado foi:
Imagens de treino: 280
Imagens de teste: 120
Componentes PCA: 80
Classificador: nearest-neighbor
Acuracia: 89.17%
Esse numero nao deve ser tratado como verdade universal. Ele depende do split, do pre-processamento, do numero de componentes e da metrica de distancia.
PCA pode ser vista como escolher uma base ortonormal adaptada aos dados.
A base canonica de R^4096 olha para pixels individuais:
Cada e_j pergunta: "qual e a intensidade do pixel j?"
Eigenfaces troca essa base por direcoes globais:
Cada w_j pergunta: "quanto esta face se parece com este padrao global de
variacao?"
Por isso uma coordenada PCA nao e um pixel. Ela e um coeficiente de semelhanca com uma direcao visual aprendida.
Se w_j for remodelado para 64 x 64, ele pode parecer uma face fantasmagorica
com regioes claras e escuras. Matematicamente, isso e apenas um autovetor da
covariancia. Visualmente, virou uma eigenface.
Os pixels originais sao inteiros. O codigo converte para float64 e divide por
255:
faces = mat["faces"].T.astype(np.float64) / 255.0Isso coloca todas as intensidades em [0, 1].
Como a distancia euclidiana e sensivel a escala, essa normalizacao e importante.
Se os pixels estivessem em [0, 255], as distancias seriam 255 vezes maiores.
A classificacao por argmin nao mudaria necessariamente se todos os vetores
fossem escalados igualmente, mas a escala numerica dos limiares e distancias
ficaria menos interpretavel.
Entrada:
X: matriz de imagens de treino
c: rotulos das imagens de treino
k: numero de componentes
Treino:
1. Calcule mu = media das linhas de X.
2. Calcule A = X - mu.
3. Calcule A = U Sigma V^T.
4. Guarde W = primeiras k linhas de V^T.
5. Projete Y = A W^T.
6. Guarde Y e os rotulos c.
Predicao:
1. Receba uma imagem x.
2. Centralize: phi = x - mu.
3. Projete: y = W phi.
4. Ache i* = argmin_i ||y - y_i||_2.
5. Retorne c_i*.
Carregamento:
mat = loadmat(path)
faces = mat["faces"].T.astype(np.float64) / 255.0
labels = np.repeat(np.arange(faces.shape[0] // IMAGES_PER_PERSON), IMAGES_PER_PERSON)Media e centralizacao:
mean_face = x_train.mean(axis=0)
centered = x_train - mean_faceSVD:
_, _, vt = np.linalg.svd(centered, full_matrices=False)
components = vt[:n_components]Projecao:
train_projection = centered @ components.TPredicao:
projected = (images - model.mean_face) @ model.components.TDistancia:
distances = np.linalg.norm(
projected[:, None, :] - model.train_projection[None, :, :],
axis=2,
)Treinar e avaliar:
python3 face_recognition_from_scratch.py evalMudar o numero de componentes:
python3 face_recognition_from_scratch.py eval --components 40
python3 face_recognition_from_scratch.py eval --components 120Comparar com classificador por centroide:
python3 face_recognition_from_scratch.py eval --classifier centroidTreinar e salvar o modelo:
python3 face_recognition_from_scratch.py train --model models/eigenfaces.npzPredizer uma imagem da propria base:
python3 face_recognition_from_scratch.py predict --model models/eigenfaces.npz --index 0Salvar uma imagem da base como PNG:
python3 face_recognition_from_scratch.py export-image --index 0 --out outputs/face_0.pngnumpy: arrays, SVD e operacoes vetoriais.scipy: apenas para ler o arquivo.mat.matplotlib: apenas para exportar imagens de exemplo.
O reconhecimento em si e implementado no arquivo:
face_recognition_from_scratch.py
Este projeto esta sob a licenca MIT. Veja LICENSE.
Eigenfaces e historicamente importante e matematicamente elegante, mas tem limites claros:
- depende de faces razoavelmente alinhadas;
- sofre com mudancas fortes de iluminacao;
- sofre com pose muito diferente;
- nao detecta rosto na imagem;
- nao lida naturalmente com multiplas faces;
- nao aprende invariancias complexas como redes neurais modernas.
Ainda assim, como primeiro sistema completo de reconhecimento, ele e excelente: mostra como uma ideia limpa de algebra linear transforma pixels crus em uma representacao discriminativa.
Eigenfaces encontra uma base ortonormal que captura as direcoes de maior variancia das faces de treino; cada rosto vira um vetor curto de coeficientes nessa base, e o reconhecimento acontece por proximidade geometrica nesse espaco reduzido.