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>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user