Files
carinha-control-panel/tests/test_reconhecimento.py
julioandClaude Opus 5.5 938467e318 feat(pessoas): reconhecimento de quem chega, só na placa
- reconhecimento.py: cada foto de evento passa pelo YuNet (acha rostos)
  e pelo SFace (impressão de 128 números) no OpenCV da placa (~0,3 s por
  foto); compara com as amostras marcadas (cosseno >= 0,40). Rostos sem
  nome ficam agrupados por semelhança para marcar de uma vez; marcar uma
  pessoa reconhece também os rostos antigos parecidos. Procura nas fotos
  guardadas, renomear, apagar (apaga recortes e impressões), sem nome
  somem em 7 dias, liga/desliga.
- modo vida: espera até 8 s a foto para cumprimentar pelo nome ("Ana!
  Senti sua falta!"), saudade de cada pessoa, "Oi, Bia!" para quem chega
  com alguém já por perto, afinidade com quem dá carinho logo depois.
- painel: tela Pessoas e tópico no guia; console mostra "Reconheceu Ana".
- sistema/reconhecimento/instalar.sh: python3-opencv e os modelos do
  opencv_zoo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:38:10 -03:00

178 lines
7.8 KiB
Python

"""Reconhecimento de pessoas: marcar rostos, reconhecer, saudade por pessoa, afinidade e privacidade."""
import random
import sys
import tempfile
import time
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from reconhecimento import DIAS_SEM_NOME, Reconhecimento, cosseno # noqa: E402
from vida import Vida # noqa: E402
DIA = time.mktime((2026, 10, 12, 15, 0, 0, 0, 0, -1))
def rosto_de(semente, ruido=0.25):
"""Impressão de 128 números: a mesma pessoa varia pouco, pessoas diferentes são bem diferentes."""
base = random.Random(semente)
r = random.Random()
return [base.gauss(0, 1) + r.gauss(0, ruido) for _ in range(128)]
class VisaoFalsa:
motivo = ''
def __init__(self):
self.fotos = {}
def pronta(self):
return True
def rostos(self, jpeg):
return [(v, b'\xff\xd8recorte\xff\xd9', 0.95, 120) for v in self.fotos.get(jpeg, [])]
class ReconhecimentoTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.visao = VisaoFalsa()
self.r = Reconhecimento(self.tmp.name, visao=self.visao, iniciar=False)
self.vistos, self.linhas = [], []
self.r.ao_reconhecer = lambda p, conf, agora: self.vistos.append((p['nome'], round(conf, 2), p.get('visto_antes')))
self.r.console = lambda origem, texto, motivo='', nivel='info', agora=None: self.linhas.append(texto)
def tearDown(self):
self.tmp.cleanup()
def foto(self, evento, *pessoas, agora=DIA):
jpeg = ('foto-%d' % evento).encode()
self.visao.fotos[jpeg] = [rosto_de(p) for p in pessoas]
return self.r.analisar(evento, jpeg, agora)
def test_impressoes_da_mesma_pessoa_se_parecem(self):
self.assertGreater(cosseno(rosto_de('ana'), rosto_de('ana')), 0.8)
self.assertLess(abs(cosseno(rosto_de('ana'), rosto_de('bia'))), 0.3)
def test_marcar_e_depois_reconhecer_pelo_nome(self):
(pessoa, _), = self.foto(1, 'ana') # ninguém cadastrado: rosto sem nome
self.assertIsNone(pessoa)
self.foto(2, 'ana'); self.foto(3, 'ana')
sem_nome = [s['id'] for s in self.r.status()['sem_nome']]
self.assertEqual(len(sem_nome), 3)
self.r.marcar(sem_nome, nome='Ana')
st = self.r.status()
self.assertEqual([p['nome'] for p in st['pessoas']], ['Ana'])
self.assertEqual(st['pessoas'][0]['amostras'], 3)
self.assertEqual(st['sem_nome'], [])
# foto nova: reconhece a Ana e não a Bia
(pessoa, conf), = self.foto(4, 'ana', agora=DIA + 60)
self.assertIsNotNone(pessoa)
self.assertEqual(self.vistos[-1][0], 'Ana')
self.assertIn('Reconheceu Ana', self.linhas)
(pessoa, _), = self.foto(5, 'bia', agora=DIA + 120)
self.assertIsNone(pessoa)
self.assertEqual(len(self.r.status()['sem_nome']), 1)
def test_marcar_a_pessoa_reconhece_os_rostos_antigos_sem_nome(self):
for i in range(4):
self.foto(10 + i, 'bia')
ids = [s['id'] for s in self.r.status()['sem_nome']]
self.r.marcar(ids[:1], nome='Bia') # marcou um: os outros parecidos já passam a ser dela
st = self.r.status()
self.assertEqual(st['sem_nome'], [])
self.assertEqual(st['pessoas'][0]['amostras'], 1)
def test_saudade_conta_quando_cada_um_foi_visto(self):
self.foto(1, 'ana')
self.r.marcar([s['id'] for s in self.r.status()['sem_nome']], nome='Ana')
self.foto(2, 'ana', agora=DIA + 100)
self.foto(3, 'ana', agora=DIA + 5000)
self.assertEqual(self.vistos[-1][2], DIA + 100) # visto antes desta foto
def test_rostos_parecidos_ficam_no_mesmo_grupo(self):
self.foto(1, 'ana'); self.foto(2, 'bia'); self.foto(3, 'ana'); self.foto(4, 'ana')
grupos = sorted(len(g) for g in self.r.status()['grupos'])
self.assertEqual(grupos, [1, 3])
def test_descartar_renomear_apagar(self):
self.foto(1, 'ana'); self.foto(2, 'gato')
ana, gato = sorted(s['id'] for s in self.r.status()['sem_nome'])
self.r.marcar([gato]) # não é ninguém
self.r.marcar([ana], nome='ana')
self.r.marcar([ana], nome='ANA') # mesmo nome: mesma pessoa
pid = self.r.status()['pessoas'][0]['id']
self.r.renomear(pid, 'Ana Paula')
self.assertEqual(self.r.status()['pessoas'][0]['nome'], 'Ana Paula')
self.assertEqual(len(list((Path(self.tmp.name) / 'fotos' / 'rostos').glob('*.jpg'))), 1)
self.r.apagar_pessoa(pid) # apaga a pessoa e tudo dela
self.assertEqual(self.r.status()['pessoas'], [])
self.assertEqual(self.r.status()['rostos'], 0)
self.assertEqual(list((Path(self.tmp.name) / 'fotos' / 'rostos').glob('*.jpg')), [])
with self.assertRaises(ValueError):
self.r.marcar([], nome='x')
def test_rostos_sem_nome_somem_depois_de_uns_dias(self):
self.foto(1, 'ana', agora=DIA)
self.foto(2, 'bia', agora=DIA)
self.r.marcar([self.r.status()['sem_nome'][0]['id']], nome='Bia')
self.assertEqual(self.r.limpar_antigos(DIA + DIAS_SEM_NOME * 86400 + 1), 1) # a amostra da Bia fica
self.assertEqual(self.r.status()['rostos'], 1)
def test_desligado_nao_analisa_fotos_novas(self):
self.r.configurar(dict(ligado=False))
self.r.nova_foto(1, 'x.jpg')
self.assertTrue(self.r.fila.empty())
with self.assertRaises(ValueError):
self.r.configurar(dict(outra=True))
class VidaComNomesTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1))
self.ordens, self.afinidade = [], []
self.v.enfileirar = self.ordens.append
self.v.celular_online = lambda: True
self.v.celular_dormindo = lambda: False
self.v.madrugada = lambda agora: False
self.v.reconhece = lambda: True
self.v.gostou_de = lambda pessoa, quanto=1.0: self.afinidade.append((pessoa, quanto))
self.v.saudou_dia = '2026-10-12'
self.perto = False
self.v.alguem_perto = lambda: self.perto
def tearDown(self):
self.tmp.cleanup()
def falas(self):
return [o[6:] for o in self.ordens if o.startswith('dizer ')]
def test_espera_saber_quem_chegou_para_cumprimentar_pelo_nome(self):
self.v.ultimo_perto, self.perto = DIA - 2 * 3600, True
self.v.tick(DIA)
self.assertEqual(self.falas(), []) # esperando a foto
self.v.reconhecido(dict(id=1, nome='Ana', afinidade=25, visto_antes=DIA - 6 * 3600), 0.8, DIA + 3)
self.assertEqual(self.falas(), ['Ana! Senti sua falta!']) # 6 h sem a Ana, mesmo a casa tendo gente há 2 h
self.assertIsNone(self.v.reencontro_pendente)
def test_sem_reconhecer_cumprimenta_sem_nome_depois_de_esperar(self):
self.v.ultimo_perto, self.perto = DIA - 2 * 3600, True
self.v.tick(DIA)
self.v.tick(DIA + 5)
self.assertEqual(self.falas(), [])
self.v.tick(DIA + 9)
self.assertEqual(self.falas(), ['Oi! Você voltou!'])
def test_outra_pessoa_chega_com_alguem_ja_por_perto(self):
self.v.reconhecido(dict(id=2, nome='Bia', afinidade=0, visto_antes=DIA - 3 * 3600), 0.7, DIA)
self.assertEqual(self.falas(), ['Oi, Bia! Você voltou!'])
self.v.reconhecido(dict(id=2, nome='Bia', afinidade=0, visto_antes=DIA), 0.7, DIA + 60) # acabou de ver
self.assertEqual(len(self.falas()), 1)
self.v.estimulo('carinho', DIA + 90) # carinho logo depois: afinidade com a Bia
self.assertEqual(self.afinidade, [(2, 1.0)])
if __name__ == '__main__':
unittest.main()