Cambios yolo en gpu

This commit is contained in:
ecuellar 2026-04-08 11:00:23 -06:00
parent 6bc9a5cb44
commit aa2132f3cf
12 changed files with 2217 additions and 2206 deletions

346
.gitignore vendored
View File

@ -1,174 +1,174 @@
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock #pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
.pdm.toml .pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.env .env
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# ──────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────
# ENTORNO VIRTUAL DEL PROYECTO # ENTORNO VIRTUAL DEL PROYECTO
# ──────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────
ia_env/ ia_env/
# ──────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────
# MODELOS DE IA (Límite de GitHub: 100 MB) # MODELOS DE IA (Límite de GitHub: 100 MB)
# ──────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────
*.pt *.pt
*.onnx *.onnx

122
README.md
View File

@ -1,62 +1,62 @@
# Sistema de Identificación y Seguimiento Inteligente # Sistema de Identificación y Seguimiento Inteligente
Este repositorio contiene la arquitectura modular para el seguimiento de personas en múltiples cámaras (Re-ID) y reconocimiento facial asíncrono. Este repositorio contiene la arquitectura modular para el seguimiento de personas en múltiples cámaras (Re-ID) y reconocimiento facial asíncrono.
## Arquitectura del Proyecto ## Arquitectura del Proyecto
El sistema está dividido en tres módulos principales para garantizar la separación de responsabilidades: El sistema está dividido en tres módulos principales para garantizar la separación de responsabilidades:
* `seguimiento2.py`: Motor matemático de Tracking (Kalman + YOLO) y Re-Identificación (OSNet). * `seguimiento2.py`: Motor matemático de Tracking (Kalman + YOLO) y Re-Identificación (OSNet).
* `reconocimiento2.py`: Motor de biometría facial (YuNet + ArcFace) y síntesis de audio (Edge-TTS). * `reconocimiento2.py`: Motor de biometría facial (YuNet + ArcFace) y síntesis de audio (Edge-TTS).
* `main_fusion.py`: Orquestador principal que fusiona ambos motores mediante procesamiento multihilo. * `main_fusion.py`: Orquestador principal que fusiona ambos motores mediante procesamiento multihilo.
## Requisitos Previos ## Requisitos Previos
1. **Python 3.8 - 3.11** instalado en el sistema. 1. **Python 3.8 - 3.11** instalado en el sistema.
2. **Reproductor MPV** instalado y agregado al PATH del sistema (requerido para el motor de audio sin bloqueos). 2. **Reproductor MPV** instalado y agregado al PATH del sistema (requerido para el motor de audio sin bloqueos).
* *Windows:* Descargar de la página oficial o usar `scoop install mpv`. * *Windows:* Descargar de la página oficial o usar `scoop install mpv`.
* *Linux:* `sudo apt install mpv` * *Linux:* `sudo apt install mpv`
* *Mac:* `brew install mpv` * *Mac:* `brew install mpv`
## Guía de Instalación Rápida ## Guía de Instalación Rápida
**1. Clonar el repositorio** **1. Clonar el repositorio**
Abre tu terminal y clona este proyecto: Abre tu terminal y clona este proyecto:
```bash ```bash
git clone <URL_DE_TU_REPOSITORIO_GITEA> git clone <URL_DE_TU_REPOSITORIO_GITEA>
cd IdentificacionIA´´´ cd IdentificacionIA´´´
**2. Crear un Entorno Virtual (¡Importante!) **2. Crear un Entorno Virtual (¡Importante!)
Para evitar conflictos de librerías, crea un entorno virtual limpio dentro de la carpeta del proyecto: Para evitar conflictos de librerías, crea un entorno virtual limpio dentro de la carpeta del proyecto:
python -m venv venv python -m venv venv
3. Activar el Entorno Virtual 3. Activar el Entorno Virtual
En Windows: En Windows:
.\venv\Scripts\activate .\venv\Scripts\activate
En Mac/Linux: En Mac/Linux:
source venv/bin/activate source venv/bin/activate
(Sabrás que está activo si ves un (venv) al inicio de tu línea de comandos). (Sabrás que está activo si ves un (venv) al inicio de tu línea de comandos).
4. Instalar Dependencias 4. Instalar Dependencias
Con el entorno activado, instala todas las librerías necesarias: Con el entorno activado, instala todas las librerías necesarias:
pip install -r requirements.txt pip install -r requirements.txt
## Archivos y Carpetas Necesarias ## Archivos y Carpetas Necesarias
yolov8n.pt (Detector de personas) yolov8n.pt (Detector de personas)
osnet_x0_25_msmt17.onnx (Extractor de características de ropa) osnet_x0_25_msmt17.onnx (Extractor de características de ropa)
face_detection_yunet_2023mar.onnx (Detector facial rápido) face_detection_yunet_2023mar.onnx (Detector facial rápido)
Además, debes tener la carpeta db_institucion con las fotografías de los rostros a reconocer. Además, debes tener la carpeta db_institucion con las fotografías de los rostros a reconocer.
## Ejecución ## Ejecución
Para arrancar el sistema completo con interfaz gráfica y audio, ejecuta: Para arrancar el sistema completo con interfaz gráfica y audio, ejecuta:
python main_fusion.py python main_fusion.py

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -10,6 +10,9 @@ from queue import Queue
from deepface import DeepFace from deepface import DeepFace
from ultralytics import YOLO from ultralytics import YOLO
import warnings import warnings
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Usando dispositivo: {device}")
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
@ -295,7 +298,7 @@ def dibujar_track_fusion(frame_show, trk, global_mem):
def main(): def main():
print("\nIniciando Sistema") print("\nIniciando Sistema")
model = YOLO("yolov8n.pt") model = YOLO("yolov8n.pt").to("cuda")
global_mem = GlobalMemory() global_mem = GlobalMemory()
managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA}
cams = [CamStream(u) for u in URLS] cams = [CamStream(u) for u in URLS]
@ -303,7 +306,7 @@ def main():
for _ in range(2): for _ in range(2):
threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start() threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start()
cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) cv2.namedWindow("SmartSoft", cv2.WINDOW_NORMAL)
idx = 0 idx = 0
while True: while True:

View File

@ -1,110 +1,110 @@
import cv2 import cv2
import os import os
import json import json
import numpy as np import numpy as np
# ⚡ Importamos tus motores exactos para no romper la simetría # ⚡ Importamos tus motores exactos para no romper la simetría
from reconocimiento2 import detectar_rostros_yunet, gestionar_vectores from reconocimiento2 import detectar_rostros_yunet, gestionar_vectores
def registrar_desde_webcam(): def registrar_desde_webcam():
print("\n" + "="*50) print("\n" + "="*50)
print("📸 MÓDULO DE REGISTRO LIMPIO (WEBCAM LOCAL)") print("📸 MÓDULO DE REGISTRO LIMPIO (WEBCAM LOCAL)")
print("Alinea tu rostro, mira a la cámara con buena luz.") print("Alinea tu rostro, mira a la cámara con buena luz.")
print("Presiona [R] para capturar | [Q] para salir") print("Presiona [R] para capturar | [Q] para salir")
print("="*50 + "\n") print("="*50 + "\n")
DB_PATH = "db_institucion" DB_PATH = "db_institucion"
CACHE_PATH = "cache_nombres" CACHE_PATH = "cache_nombres"
os.makedirs(DB_PATH, exist_ok=True) os.makedirs(DB_PATH, exist_ok=True)
os.makedirs(CACHE_PATH, exist_ok=True) os.makedirs(CACHE_PATH, exist_ok=True)
# 0 es la cámara por defecto de tu laptop # 0 es la cámara por defecto de tu laptop
cap = cv2.VideoCapture(0) cap = cv2.VideoCapture(0)
if not cap.isOpened(): if not cap.isOpened():
print("[!] Error: No se pudo abrir la webcam local.") print("[!] Error: No se pudo abrir la webcam local.")
return return
while True: while True:
ret, frame = cap.read() ret, frame = cap.read()
if not ret: continue if not ret: continue
# Espejamos la imagen para que actúe como un espejo natural # Espejamos la imagen para que actúe como un espejo natural
frame = cv2.flip(frame, 1) frame = cv2.flip(frame, 1)
display_frame = frame.copy() display_frame = frame.copy()
# Usamos YuNet para garantizar que estamos capturando una cara válida # Usamos YuNet para garantizar que estamos capturando una cara válida
faces = detectar_rostros_yunet(frame) faces = detectar_rostros_yunet(frame)
mejor_rostro = None mejor_rostro = None
max_area = 0 max_area = 0
for (fx, fy, fw, fh, score) in faces: for (fx, fy, fw, fh, score) in faces:
area = fw * fh area = fw * fh
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 2) cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 2)
if area > max_area: if area > max_area:
max_area = area max_area = area
h_frame, w_frame = frame.shape[:2] h_frame, w_frame = frame.shape[:2]
# Mismo margen del 30% que requiere MTCNN para alinear correctamente # Mismo margen del 30% que requiere MTCNN para alinear correctamente
m_x, m_y = int(fw * 0.30), int(fh * 0.30) m_x, m_y = int(fw * 0.30), int(fh * 0.30)
y1 = max(0, fy - m_y) y1 = max(0, fy - m_y)
y2 = min(h_frame, fy + fh + m_y) y2 = min(h_frame, fy + fh + m_y)
x1 = max(0, fx - m_x) x1 = max(0, fx - m_x)
x2 = min(w_frame, fx + fw + m_x) x2 = min(w_frame, fx + fw + m_x)
mejor_rostro = frame[y1:y2, x1:x2] mejor_rostro = frame[y1:y2, x1:x2]
cv2.putText(display_frame, "Alineate y presiona [R]", (10, 30), cv2.putText(display_frame, "Alineate y presiona [R]", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
cv2.imshow("Registro Webcam Local", display_frame) cv2.imshow("Registro Webcam Local", display_frame)
key = cv2.waitKey(1) & 0xFF key = cv2.waitKey(1) & 0xFF
if key == ord('q'): if key == ord('q'):
break break
elif key == ord('r'): elif key == ord('r'):
if mejor_rostro is not None and mejor_rostro.size > 0: if mejor_rostro is not None and mejor_rostro.size > 0:
cv2.imshow("Captura Congelada", mejor_rostro) cv2.imshow("Captura Congelada", mejor_rostro)
cv2.waitKey(1) cv2.waitKey(1)
print("\n--- NUEVO REGISTRO ---") print("\n--- NUEVO REGISTRO ---")
nom = input("Escribe el nombre exacto de la persona: ").strip() nom = input("Escribe el nombre exacto de la persona: ").strip()
if nom: if nom:
gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower() gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower()
genero_guardado = "Woman" if gen_input == 'm' else "Man" genero_guardado = "Woman" if gen_input == 'm' else "Man"
# 1. Guardamos la foto pura # 1. Guardamos la foto pura
foto_path = os.path.join(DB_PATH, f"{nom}.jpg") foto_path = os.path.join(DB_PATH, f"{nom}.jpg")
cv2.imwrite(foto_path, mejor_rostro) cv2.imwrite(foto_path, mejor_rostro)
# 2. Actualizamos el caché de géneros sin usar IA # 2. Actualizamos el caché de géneros sin usar IA
ruta_generos = os.path.join(CACHE_PATH, "generos.json") ruta_generos = os.path.join(CACHE_PATH, "generos.json")
dic_generos = {} dic_generos = {}
if os.path.exists(ruta_generos): if os.path.exists(ruta_generos):
try: try:
with open(ruta_generos, 'r') as f: with open(ruta_generos, 'r') as f:
dic_generos = json.load(f) dic_generos = json.load(f)
except Exception: pass except Exception: pass
dic_generos[nom] = genero_guardado dic_generos[nom] = genero_guardado
with open(ruta_generos, 'w') as f: with open(ruta_generos, 'w') as f:
json.dump(dic_generos, f) json.dump(dic_generos, f)
print(f"\n[OK] Foto guardada. Generando punto de gravedad matemático...") print(f"\n[OK] Foto guardada. Generando punto de gravedad matemático...")
# 3. Forzamos la creación del vector en la base de datos # 3. Forzamos la creación del vector en la base de datos
gestionar_vectores(actualizar=True) gestionar_vectores(actualizar=True)
print(" Registro inyectado exitosamente en el sistema principal.") print(" Registro inyectado exitosamente en el sistema principal.")
else: else:
print("[!] Registro cancelado por nombre vacío.") print("[!] Registro cancelado por nombre vacío.")
cv2.destroyWindow("Captura Congelada") cv2.destroyWindow("Captura Congelada")
else: else:
print("[!] No se detectó ningún rostro claro. Acércate más a la luz.") print("[!] No se detectó ningún rostro claro. Acércate más a la luz.")
cap.release() cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
if __name__ == "__main__": if __name__ == "__main__":
registrar_desde_webcam() registrar_desde_webcam()

View File

@ -1,43 +1,43 @@
import cv2 import cv2
import time import time
from ultralytics import YOLO from ultralytics import YOLO
from seguimiento2 import GlobalMemory, CamManager, dibujar_track from seguimiento2 import GlobalMemory, CamManager, dibujar_track
def test_video(video_path): def test_video(video_path):
print(f"Iniciando Benchmark de Video: {video_path}") print(f"Iniciando Benchmark de Video: {video_path}")
model = YOLO("yolov8n.pt") model = YOLO("yolov8n.pt")
global_mem = GlobalMemory() global_mem = GlobalMemory()
manager = CamManager("TEST_CAM", global_mem) manager = CamManager("TEST_CAM", global_mem)
cap = cv2.VideoCapture(video_path) cap = cv2.VideoCapture(video_path)
cv2.namedWindow("Benchmark TT", cv2.WINDOW_AUTOSIZE) cv2.namedWindow("Benchmark TT", cv2.WINDOW_AUTOSIZE)
while cap.isOpened(): while cap.isOpened():
ret, frame = cap.read() ret, frame = cap.read()
if not ret: break if not ret: break
now = time.time() now = time.time()
frame_show = cv2.resize(frame, (480, 270)) frame_show = cv2.resize(frame, (480, 270))
# Inferencia frame por frame sin hilos (sincrónico) # Inferencia frame por frame sin hilos (sincrónico)
res = model.predict(frame_show, conf=0.40, iou=0.50, classes=[0], verbose=False, imgsz=480, device='cpu') res = model.predict(frame_show, conf=0.40, iou=0.50, classes=[0], verbose=False, imgsz=480, device='cpu')
boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes else [] boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes else []
tracks = manager.update(boxes, frame_show, now, turno_activo=True) tracks = manager.update(boxes, frame_show, now, turno_activo=True)
for trk in tracks: for trk in tracks:
if trk.time_since_update == 0: if trk.time_since_update == 0:
dibujar_track(frame_show, trk) dibujar_track(frame_show, trk)
cv2.imshow("Benchmark TT", frame_show) cv2.imshow("Benchmark TT", frame_show)
# Si presionas espacio se pausa, con 'q' sales # Si presionas espacio se pausa, con 'q' sales
key = cv2.waitKey(30) & 0xFF key = cv2.waitKey(30) & 0xFF
if key == ord('q'): break if key == ord('q'): break
elif key == ord(' '): cv2.waitKey(-1) elif key == ord(' '): cv2.waitKey(-1)
cap.release() cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
if __name__ == "__main__": if __name__ == "__main__":
test_video("video.mp4") # Pon aquí el nombre de tu video test_video("video.mp4") # Pon aquí el nombre de tu video

View File

@ -1,465 +1,473 @@
import os import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
import cv2 import cv2
import numpy as np import numpy as np
from deepface import DeepFace from deepface import DeepFace
import pickle import pickle
import time import time
import threading import threading
import asyncio import asyncio
import edge_tts import edge_tts
import subprocess import subprocess
from datetime import datetime from datetime import datetime
import warnings import warnings
import urllib.request import urllib.request
import torch
warnings.filterwarnings("ignore")
if torch.cuda.is_available():
# ────────────────────────────────────────────────────────────────────────────── device = "cuda"
# CONFIGURACIÓN print("GPU detectada → usando GPU 🚀")
# ────────────────────────────────────────────────────────────────────────────── else:
DB_PATH = "db_institucion" device = "cpu"
CACHE_PATH = "cache_nombres" print("GPU no disponible → usando CPU ⚠️")
VECTORS_FILE = "base_datos_rostros.pkl"
TIMESTAMPS_FILE = "representaciones_timestamps.pkl" warnings.filterwarnings("ignore")
UMBRAL_SIM = 0.42 # Por encima → identificado. Por debajo → desconocido.
COOLDOWN_TIME = 15 # Segundos entre saludos # ──────────────────────────────────────────────────────────────────────────────
# CONFIGURACIÓN
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" # ──────────────────────────────────────────────────────────────────────────────
RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" DB_PATH = "db_institucion"
CACHE_PATH = "cache_nombres"
for path in [DB_PATH, CACHE_PATH]: VECTORS_FILE = "base_datos_rostros.pkl"
os.makedirs(path, exist_ok=True) TIMESTAMPS_FILE = "representaciones_timestamps.pkl"
UMBRAL_SIM = 0.42 # Por encima → identificado. Por debajo → desconocido.
# ────────────────────────────────────────────────────────────────────────────── COOLDOWN_TIME = 15 # Segundos entre saludos
# YUNET — Detector facial rápido en CPU
# ────────────────────────────────────────────────────────────────────────────── USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702"
if not os.path.exists(YUNET_MODEL_PATH): for path in [DB_PATH, CACHE_PATH]:
print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") os.makedirs(path, exist_ok=True)
url = ("https://github.com/opencv/opencv_zoo/raw/main/models/"
"face_detection_yunet/face_detection_yunet_2023mar.onnx") # ──────────────────────────────────────────────────────────────────────────────
urllib.request.urlretrieve(url, YUNET_MODEL_PATH) # YUNET — Detector facial rápido en CPU
print("YuNet descargado.") # ──────────────────────────────────────────────────────────────────────────────
YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx"
# Detector estricto para ROIs grandes (persona cerca)
detector_yunet = cv2.FaceDetectorYN.create( if not os.path.exists(YUNET_MODEL_PATH):
model=YUNET_MODEL_PATH, config="", print(f"Descargando YuNet ({YUNET_MODEL_PATH})...")
input_size=(320, 320), url = ("https://github.com/opencv/opencv_zoo/raw/main/models/"
score_threshold=0.70, "face_detection_yunet/face_detection_yunet_2023mar.onnx")
nms_threshold=0.3, urllib.request.urlretrieve(url, YUNET_MODEL_PATH)
top_k=5000 print("YuNet descargado.")
)
# Detector estricto para ROIs grandes (persona cerca)
# Detector permisivo para ROIs pequeños (persona lejos) detector_yunet = cv2.FaceDetectorYN.create(
detector_yunet_lejano = cv2.FaceDetectorYN.create( model=YUNET_MODEL_PATH, config="",
model=YUNET_MODEL_PATH, config="", input_size=(320, 320),
input_size=(320, 320), score_threshold=0.70,
score_threshold=0.45, nms_threshold=0.3,
nms_threshold=0.3, top_k=5000
top_k=5000 )
)
# Detector permisivo para ROIs pequeños (persona lejos)
def detectar_rostros_yunet(roi, lock=None): detector_yunet_lejano = cv2.FaceDetectorYN.create(
""" model=YUNET_MODEL_PATH, config="",
Elige automáticamente el detector según el tamaño del ROI. input_size=(320, 320),
""" score_threshold=0.45,
h_roi, w_roi = roi.shape[:2] nms_threshold=0.3,
area = w_roi * h_roi top_k=5000
det = detector_yunet if area > 8000 else detector_yunet_lejano )
try: def detectar_rostros_yunet(roi, lock=None):
if lock: """
with lock: Elige automáticamente el detector según el tamaño del ROI.
det.setInputSize((w_roi, h_roi)) """
_, faces = det.detect(roi) h_roi, w_roi = roi.shape[:2]
else: area = w_roi * h_roi
det.setInputSize((w_roi, h_roi)) det = detector_yunet if area > 8000 else detector_yunet_lejano
_, faces = det.detect(roi)
except Exception: try:
return [] if lock:
with lock:
if faces is None: det.setInputSize((w_roi, h_roi))
return [] _, faces = det.detect(roi)
else:
resultado = [] det.setInputSize((w_roi, h_roi))
for face in faces: _, faces = det.detect(roi)
try: except Exception:
fx, fy, fw, fh = map(int, face[:4]) return []
score = float(face[14]) if len(face) > 14 else 1.0
resultado.append((fx, fy, fw, fh, score)) if faces is None:
except (ValueError, OverflowError, TypeError): return []
continue
return resultado resultado = []
for face in faces:
try:
# ────────────────────────────────────────────────────────────────────────────── fx, fy, fw, fh = map(int, face[:4])
# SISTEMA DE AUDIO score = float(face[14]) if len(face) > 14 else 1.0
# ────────────────────────────────────────────────────────────────────────────── resultado.append((fx, fy, fw, fh, score))
def obtener_audios_humanos(genero): except (ValueError, OverflowError, TypeError):
hora = datetime.now().hour continue
es_mujer = genero.lower() == 'woman' return resultado
suffix = "_m.mp3" if es_mujer else "_h.mp3"
if 5 <= hora < 12:
intro = "dias.mp3" # ──────────────────────────────────────────────────────────────────────────────
elif 12 <= hora < 19: # SISTEMA DE AUDIO
intro = "tarde.mp3" # ──────────────────────────────────────────────────────────────────────────────
else: def obtener_audios_humanos(genero):
intro = "noches.mp3" hora = datetime.now().hour
cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix es_mujer = genero.lower() == 'woman'
return intro, cierre suffix = "_m.mp3" if es_mujer else "_h.mp3"
if 5 <= hora < 12:
intro = "dias.mp3"
async def sintetizar_nombre(nombre, ruta): elif 12 <= hora < 19:
nombre_limpio = nombre.replace('_', ' ') intro = "tarde.mp3"
try: else:
comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") intro = "noches.mp3"
await comunicador.save(ruta) cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix
except Exception: return intro, cierre
pass
async def sintetizar_nombre(nombre, ruta):
def reproducir(archivo): nombre_limpio = nombre.replace('_', ' ')
if os.path.exists(archivo): try:
subprocess.Popen( comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%")
["mpv", "--no-video", "--volume=100", archivo], await comunicador.save(ruta)
stdout=subprocess.DEVNULL, except Exception:
stderr=subprocess.DEVNULL pass
)
def reproducir(archivo):
def hilo_bienvenida(nombre, genero): if os.path.exists(archivo):
archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") subprocess.Popen(
["mpv", "--no-video", "--volume=100", archivo],
if not os.path.exists(archivo_nombre): stdout=subprocess.DEVNULL,
try: stderr=subprocess.DEVNULL
asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) )
except Exception:
pass
def hilo_bienvenida(nombre, genero):
intro, cierre = obtener_audios_humanos(genero) archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3")
archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] if not os.path.exists(archivo_nombre):
if archivos: try:
subprocess.Popen( asyncio.run(sintetizar_nombre(nombre, archivo_nombre))
["mpv", "--no-video", "--volume=100"] + archivos, except Exception:
stdout=subprocess.DEVNULL, pass
stderr=subprocess.DEVNULL
) intro, cierre = obtener_audios_humanos(genero)
archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)]
# ────────────────────────────────────────────────────────────────────────────── if archivos:
# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN) subprocess.Popen(
# ────────────────────────────────────────────────────────────────────────────── ["mpv", "--no-video", "--volume=100"] + archivos,
def gestionar_vectores(actualizar=False): stdout=subprocess.DEVNULL,
import json # ⚡ Asegúrate de tener importado json stderr=subprocess.DEVNULL
)
vectores_actuales = {}
if os.path.exists(VECTORS_FILE):
try: # ──────────────────────────────────────────────────────────────────────────────
with open(VECTORS_FILE, 'rb') as f: # GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN)
vectores_actuales = pickle.load(f) # ──────────────────────────────────────────────────────────────────────────────
except Exception: def gestionar_vectores(actualizar=False):
vectores_actuales = {} import json # ⚡ Asegúrate de tener importado json
if not actualizar: vectores_actuales = {}
return vectores_actuales if os.path.exists(VECTORS_FILE):
try:
timestamps = {} with open(VECTORS_FILE, 'rb') as f:
if os.path.exists(TIMESTAMPS_FILE): vectores_actuales = pickle.load(f)
try: except Exception:
with open(TIMESTAMPS_FILE, 'rb') as f: vectores_actuales = {}
timestamps = pickle.load(f)
except Exception: if not actualizar:
timestamps = {} return vectores_actuales
# ────────────────────────────────────────────────────────── timestamps = {}
# CARGA DEL CACHÉ DE GÉNEROS if os.path.exists(TIMESTAMPS_FILE):
# ────────────────────────────────────────────────────────── try:
ruta_generos = os.path.join(CACHE_PATH, "generos.json") with open(TIMESTAMPS_FILE, 'rb') as f:
dic_generos = {} timestamps = pickle.load(f)
if os.path.exists(ruta_generos): except Exception:
try: timestamps = {}
with open(ruta_generos, 'r') as f:
dic_generos = json.load(f) # ──────────────────────────────────────────────────────────
except Exception: # CARGA DEL CACHÉ DE GÉNEROS
pass # ──────────────────────────────────────────────────────────
ruta_generos = os.path.join(CACHE_PATH, "generos.json")
print("\nACTUALIZANDO BASE DE DATOS (Alineación y Caché de Géneros)...") dic_generos = {}
imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] if os.path.exists(ruta_generos):
nombres_en_disco = set() try:
hubo_cambios = False with open(ruta_generos, 'r') as f:
cambio_generos = False # Bandera para saber si actualizamos el JSON dic_generos = json.load(f)
except Exception:
for archivo in imagenes: pass
nombre_archivo = os.path.splitext(archivo)[0]
ruta_img = os.path.join(DB_PATH, archivo) print("\nACTUALIZANDO BASE DE DATOS (Alineación y Caché de Géneros)...")
nombres_en_disco.add(nombre_archivo) imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))]
nombres_en_disco = set()
ts_actual = os.path.getmtime(ruta_img) hubo_cambios = False
ts_guardado = timestamps.get(nombre_archivo, 0) cambio_generos = False # Bandera para saber si actualizamos el JSON
# Si ya tenemos el vector pero NO tenemos su género en el JSON, forzamos el procesamiento for archivo in imagenes:
falta_genero = nombre_archivo not in dic_generos nombre_archivo = os.path.splitext(archivo)[0]
ruta_img = os.path.join(DB_PATH, archivo)
if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero: nombres_en_disco.add(nombre_archivo)
continue
ts_actual = os.path.getmtime(ruta_img)
try: ts_guardado = timestamps.get(nombre_archivo, 0)
img_db = cv2.imread(ruta_img)
lab = cv2.cvtColor(img_db, cv2.COLOR_BGR2LAB) # Si ya tenemos el vector pero NO tenemos su género en el JSON, forzamos el procesamiento
l, a, b = cv2.split(lab) falta_genero = nombre_archivo not in dic_generos
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
l = clahe.apply(l) if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero:
img_mejorada = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) continue
# IA DE GÉNERO (Solo se ejecuta 1 vez por persona en toda la vida del sistema) try:
if falta_genero: img_db = cv2.imread(ruta_img)
try: lab = cv2.cvtColor(img_db, cv2.COLOR_BGR2LAB)
analisis = DeepFace.analyze(img_mejorada, actions=['gender'], enforce_detection=False)[0] l, a, b = cv2.split(lab)
dic_generos[nombre_archivo] = analisis.get('dominant_gender', 'Man') clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
except Exception: l = clahe.apply(l)
dic_generos[nombre_archivo] = "Man" # Respaldo img_mejorada = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR)
cambio_generos = True
# IA DE GÉNERO (Solo se ejecuta 1 vez por persona en toda la vida del sistema)
# Extraemos el vector if falta_genero:
res = DeepFace.represent( try:
img_path=img_mejorada, analisis = DeepFace.analyze(img_mejorada, actions=['gender'], enforce_detection=False)[0]
model_name="ArcFace", dic_generos[nombre_archivo] = analisis.get('dominant_gender', 'Man')
detector_backend="mtcnn", except Exception:
align=True, dic_generos[nombre_archivo] = "Man" # Respaldo
enforce_detection=True cambio_generos = True
)
emb = np.array(res[0]["embedding"], dtype=np.float32) # Extraemos el vector
res = DeepFace.represent(
norma = np.linalg.norm(emb) img_path=img_mejorada,
if norma > 0: model_name="ArcFace",
emb = emb / norma detector_backend="opencv",
align=False,
vectores_actuales[nombre_archivo] = emb enforce_detection=True
timestamps[nombre_archivo] = ts_actual )
hubo_cambios = True emb = np.array(res[0]["embedding"], dtype=np.float32)
print(f" Procesado y alineado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}")
norma = np.linalg.norm(emb)
except Exception as e: if norma > 0:
print(f" Rostro no válido en '{archivo}', omitido. Error: {e}") emb = emb / norma
# Limpieza de eliminados vectores_actuales[nombre_archivo] = emb
for nombre in list(vectores_actuales.keys()): timestamps[nombre_archivo] = ts_actual
if nombre not in nombres_en_disco: hubo_cambios = True
del vectores_actuales[nombre] print(f" Procesado y alineado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}")
timestamps.pop(nombre, None)
if nombre in dic_generos: except Exception as e:
del dic_generos[nombre] print(f" Rostro no válido en '{archivo}', omitido. Error: {e}")
cambio_generos = True
hubo_cambios = True # Limpieza de eliminados
print(f" Eliminado (sin foto): {nombre}") for nombre in list(vectores_actuales.keys()):
if nombre not in nombres_en_disco:
# Guardado de la memoria del vectores_actuales[nombre]
if hubo_cambios: timestamps.pop(nombre, None)
with open(VECTORS_FILE, 'wb') as f: if nombre in dic_generos:
pickle.dump(vectores_actuales, f) del dic_generos[nombre]
with open(TIMESTAMPS_FILE, 'wb') as f: cambio_generos = True
pickle.dump(timestamps, f) hubo_cambios = True
print(f" Eliminado (sin foto): {nombre}")
# Guardado del JSON de géneros si hubo descubrimientos nuevos
if cambio_generos: # Guardado de la memoria
with open(ruta_generos, 'w') as f: if hubo_cambios:
json.dump(dic_generos, f) with open(VECTORS_FILE, 'wb') as f:
pickle.dump(vectores_actuales, f)
if hubo_cambios or cambio_generos: with open(TIMESTAMPS_FILE, 'wb') as f:
print(" Sincronización terminada.\n") pickle.dump(timestamps, f)
else:
print(" Sin cambios. Base de datos al día.\n") # Guardado del JSON de géneros si hubo descubrimientos nuevos
if cambio_generos:
return vectores_actuales with open(ruta_generos, 'w') as f:
json.dump(dic_generos, f)
# ──────────────────────────────────────────────────────────────────────────────
# BÚSQUEDA BLINDADA (Similitud Coseno estricta) if hubo_cambios or cambio_generos:
# ────────────────────────────────────────────────────────────────────────────── print(" Sincronización terminada.\n")
def buscar_mejor_match(emb_consulta, base_datos): else:
# ⚡ MAGIA 3: Normalización L2 del vector entrante print(" Sin cambios. Base de datos al día.\n")
norma = np.linalg.norm(emb_consulta)
if norma > 0: return vectores_actuales
emb_consulta = emb_consulta / norma
# ──────────────────────────────────────────────────────────────────────────────
mejor_match, max_sim = None, -1.0 # BÚSQUEDA BLINDADA (Similitud Coseno estricta)
for nombre, vec in base_datos.items(): # ──────────────────────────────────────────────────────────────────────────────
# Como ambos están normalizados, esto es Similitud Coseno pura (-1.0 a 1.0) def buscar_mejor_match(emb_consulta, base_datos):
sim = float(np.dot(emb_consulta, vec)) # ⚡ MAGIA 3: Normalización L2 del vector entrante
if sim > max_sim: norma = np.linalg.norm(emb_consulta)
max_sim = sim if norma > 0:
mejor_match = nombre emb_consulta = emb_consulta / norma
return mejor_match, max_sim mejor_match, max_sim = None, -1.0
for nombre, vec in base_datos.items():
# ────────────────────────────────────────────────────────────────────────────── # Como ambos están normalizados, esto es Similitud Coseno pura (-1.0 a 1.0)
# LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA) sim = float(np.dot(emb_consulta, vec))
# ────────────────────────────────────────────────────────────────────────────── if sim > max_sim:
def sistema_interactivo(): max_sim = sim
base_datos = gestionar_vectores(actualizar=False) mejor_match = nombre
cap = cv2.VideoCapture(RTSP_URL)
ultimo_saludo = 0 return mejor_match, max_sim
persona_actual = None
confirmaciones = 0 # ──────────────────────────────────────────────────────────────────────────────
# LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA)
print("\n" + "=" * 50) # ──────────────────────────────────────────────────────────────────────────────
print(" MÓDULO DE REGISTRO Y DEPURACIÓN ESTRICTO") def sistema_interactivo():
print(" [R] Registrar nuevo rostro | [Q] Salir") base_datos = gestionar_vectores(actualizar=False)
print("=" * 50 + "\n") cap = cv2.VideoCapture(RTSP_URL)
ultimo_saludo = 0
faces_ultimo_frame = [] persona_actual = None
confirmaciones = 0
while True:
ret, frame = cap.read() print("\n" + "=" * 50)
if not ret: print(" MÓDULO DE REGISTRO Y DEPURACIÓN ESTRICTO")
time.sleep(2) print(" [R] Registrar nuevo rostro | [Q] Salir")
cap.open(RTSP_URL) print("=" * 50 + "\n")
continue
faces_ultimo_frame = []
h, w = frame.shape[:2]
display_frame = frame.copy() while True:
tiempo_actual = time.time() ret, frame = cap.read()
if not ret:
faces_raw = detectar_rostros_yunet(frame) time.sleep(2)
faces_ultimo_frame = faces_raw cap.open(RTSP_URL)
continue
for (fx, fy, fw, fh, score_yunet) in faces_raw:
fx = max(0, fx); fy = max(0, fy) h, w = frame.shape[:2]
fw = min(w - fx, fw); fh = min(h - fy, fh) display_frame = frame.copy()
if fw <= 0 or fh <= 0: tiempo_actual = time.time()
continue
faces_raw = detectar_rostros_yunet(frame)
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) faces_ultimo_frame = faces_raw
cv2.putText(display_frame, f"YN:{score_yunet:.2f}",
(fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) for (fx, fy, fw, fh, score_yunet) in faces_raw:
fx = max(0, fx); fy = max(0, fy)
if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: fw = min(w - fx, fw); fh = min(h - fy, fh)
continue if fw <= 0 or fh <= 0:
continue
m = int(fw * 0.15)
roi = frame[max(0, fy-m): min(h, fy+fh+m), cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2)
max(0, fx-m): min(w, fx+fw+m)] cv2.putText(display_frame, f"YN:{score_yunet:.2f}",
(fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1)
# 🛡️ FILTRO DE TAMAÑO FÍSICO
if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME:
cv2.putText(display_frame, "muy pequeno", continue
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1)
continue m = int(fw * 0.15)
roi = frame[max(0, fy-m): min(h, fy+fh+m),
# 🛡️ FILTRO DE NITIDEZ max(0, fx-m): min(w, fx+fw+m)]
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() # 🛡️ FILTRO DE TAMAÑO FÍSICO
if nitidez < 50.0: if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40:
cv2.putText(display_frame, f"blur({nitidez:.0f})", cv2.putText(display_frame, "muy pequeno",
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1)
continue continue
# 🌙 SIMETRÍA 1: VISIÓN NOCTURNA (CLAHE) AL VIDEO EN VIVO # 🛡️ FILTRO DE NITIDEZ
try: gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var()
l, a, b = cv2.split(lab) if nitidez < 50.0:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) cv2.putText(display_frame, f"blur({nitidez:.0f})",
l = clahe.apply(l) (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
roi_mejorado = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) continue
except Exception:
roi_mejorado = roi # Respaldo de seguridad # 🌙 SIMETRÍA 1: VISIÓN NOCTURNA (CLAHE) AL VIDEO EN VIVO
try:
# 🧠 SIMETRÍA 2: MOTOR MTCNN Y ALINEACIÓN (Igual que la Base de Datos) lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB)
try: l, a, b = cv2.split(lab)
res = DeepFace.represent( clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
img_path=roi_mejorado, l = clahe.apply(l)
model_name="ArcFace", roi_mejorado = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR)
detector_backend="mtcnn", # El mismo que en gestionar_vectores except Exception:
align=True, # Enderezamos la cara roi_mejorado = roi # Respaldo de seguridad
enforce_detection=True # Si MTCNN no ve cara clara, aborta
) # 🧠 SIMETRÍA 2: MOTOR MTCNN Y ALINEACIÓN (Igual que la Base de Datos)
emb = np.array(res[0]["embedding"], dtype=np.float32) try:
mejor_match, max_sim = buscar_mejor_match(emb, base_datos) res = DeepFace.represent(
img_path=roi_mejorado,
except Exception: model_name="ArcFace",
# MTCNN abortó porque la cara estaba de perfil, tapada o no era una cara detector_backend="mtcnn", # El mismo que en gestionar_vectores
cv2.putText(display_frame, "MTCNN Ignorado", align=True, # Enderezamos la cara
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) enforce_detection=True # Si MTCNN no ve cara clara, aborta
continue )
emb = np.array(res[0]["embedding"], dtype=np.float32)
estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" mejor_match, max_sim = buscar_mejor_match(emb, base_datos)
nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie"
n_bloques = int(max_sim * 20) except Exception:
barra = "" * n_bloques + "" * (20 - n_bloques) # MTCNN abortó porque la cara estaba de perfil, tapada o no era una cara
print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " cv2.putText(display_frame, "MTCNN Ignorado",
f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
continue
if max_sim > UMBRAL_SIM and mejor_match:
color = (0, 255, 0) estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO"
texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie"
n_bloques = int(max_sim * 20)
if mejor_match == persona_actual: barra = "" * n_bloques + "" * (20 - n_bloques)
confirmaciones += 1 print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | "
else: f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)")
persona_actual, confirmaciones = mejor_match, 1
if max_sim > UMBRAL_SIM and mejor_match:
if confirmaciones >= 1: color = (0, 255, 0)
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})"
try:
analisis = DeepFace.analyze( if mejor_match == persona_actual:
roi_mejorado, actions=['gender'], enforce_detection=False confirmaciones += 1
)[0] else:
genero = analisis['dominant_gender'] persona_actual, confirmaciones = mejor_match, 1
except Exception:
genero = "Man" if confirmaciones >= 1:
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3)
threading.Thread( try:
target=hilo_bienvenida, analisis = DeepFace.analyze(
args=(mejor_match, genero), roi_mejorado, actions=['gender'], enforce_detection=False
daemon=True )[0]
).start() genero = analisis['dominant_gender']
ultimo_saludo = tiempo_actual except Exception:
confirmaciones = 0 genero = "Man"
else: threading.Thread(
color = (0, 0, 255) target=hilo_bienvenida,
texto = f"? ({max_sim:.2f})" args=(mejor_match, genero),
confirmaciones = max(0, confirmaciones - 1) daemon=True
).start()
cv2.putText(display_frame, texto, ultimo_saludo = tiempo_actual
(fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) confirmaciones = 0
cv2.imshow("Módulo de Registro", display_frame) else:
key = cv2.waitKey(1) & 0xFF color = (0, 0, 255)
texto = f"? ({max_sim:.2f})"
if key == ord('q'): confirmaciones = max(0, confirmaciones - 1)
break
cv2.putText(display_frame, texto,
elif key == ord('r'): (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
if faces_ultimo_frame:
areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] cv2.imshow("Módulo de Registro", display_frame)
fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] key = cv2.waitKey(1) & 0xFF
m_x = int(fw * 0.30) if key == ord('q'):
m_y = int(fh * 0.30) break
face_roi = frame[max(0, fy-m_y): min(h, fy+fh+m_y),
max(0, fx-m_x): min(w, fx+fw+m_x)] elif key == ord('r'):
if faces_ultimo_frame:
if face_roi.size > 0: areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame]
nom = input("\nNombre de la persona: ").strip() fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)]
if nom:
foto_path = os.path.join(DB_PATH, f"{nom}.jpg") m_x = int(fw * 0.30)
cv2.imwrite(foto_path, face_roi) m_y = int(fh * 0.30)
print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") face_roi = frame[max(0, fy-m_y): min(h, fy+fh+m_y),
base_datos = gestionar_vectores(actualizar=True) max(0, fx-m_x): min(w, fx+fw+m_x)]
else:
print("[!] Registro cancelado.") if face_roi.size > 0:
else: nom = input("\nNombre de la persona: ").strip()
print("[!] Recorte vacío. Intenta de nuevo.") if nom:
else: foto_path = os.path.join(DB_PATH, f"{nom}.jpg")
print("\n[!] No se detectó rostro. Acércate más o mira a la lente.") cv2.imwrite(foto_path, face_roi)
print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...")
cap.release() base_datos = gestionar_vectores(actualizar=True)
cv2.destroyAllWindows() else:
print("[!] Registro cancelado.")
if __name__ == "__main__": else:
sistema_interactivo() print("[!] Recorte vacío. Intenta de nuevo.")
else:
print("\n[!] No se detectó rostro. Acércate más o mira a la lente.")
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
sistema_interactivo()

View File

@ -1,96 +1,96 @@
absl-py==2.4.0 absl-py==2.4.0
aiohappyeyeballs==2.6.1 aiohappyeyeballs==2.6.1
aiohttp==3.13.3 aiohttp==3.13.3
aiosignal==1.4.0 aiosignal==1.4.0
astunparse==1.6.3 astunparse==1.6.3
attrs==25.4.0 attrs==25.4.0
beautifulsoup4==4.14.3 beautifulsoup4==4.14.3
blinker==1.9.0 blinker==1.9.0
certifi==2026.1.4 certifi==2026.1.4
charset-normalizer==3.4.4 charset-normalizer==3.4.4
click==8.3.1 click==8.3.1
contourpy==1.3.3 contourpy==1.3.3
cycler==0.12.1 cycler==0.12.1
deepface==0.0.98 deepface==0.0.98
edge-tts==7.2.7 edge-tts==7.2.7
filelock==3.20.0 filelock==3.20.0
fire==0.7.1 fire==0.7.1
Flask==3.1.2 Flask==3.1.2
flask-cors==6.0.2 flask-cors==6.0.2
flatbuffers==25.12.19 flatbuffers==25.12.19
fonttools==4.61.1 fonttools==4.61.1
frozenlist==1.8.0 frozenlist==1.8.0
fsspec==2025.12.0 fsspec==2025.12.0
gast==0.7.0 gast==0.7.0
gdown==5.2.1 gdown==5.2.1
google-pasta==0.2.0 google-pasta==0.2.0
grpcio==1.78.0 grpcio==1.78.0
gunicorn==25.0.3 gunicorn==25.0.3
h5py==3.15.1 h5py==3.15.1
idna==3.11 idna==3.11
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.6 Jinja2==3.1.6
joblib==1.5.3 joblib==1.5.3
keras==3.13.2 keras==3.13.2
kiwisolver==1.4.9 kiwisolver==1.4.9
lap==0.5.12 lap==0.5.12
libclang==18.1.1 libclang==18.1.1
lightdsa==0.0.3 lightdsa==0.0.3
lightecc==0.0.4 lightecc==0.0.4
lightphe==0.0.20 lightphe==0.0.20
lz4==4.4.5 lz4==4.4.5
Markdown==3.10.2 Markdown==3.10.2
markdown-it-py==4.0.0 markdown-it-py==4.0.0
MarkupSafe==2.1.5 MarkupSafe==2.1.5
matplotlib==3.10.8 matplotlib==3.10.8
mdurl==0.1.2 mdurl==0.1.2
ml_dtypes==0.5.4 ml_dtypes==0.5.4
mpmath==1.3.0 mpmath==1.3.0
mtcnn==1.0.0 mtcnn==1.0.0
multidict==6.7.1 multidict==6.7.1
namex==0.1.0 namex==0.1.0
networkx==3.6.1 networkx==3.6.1
numpy==1.26.4 numpy==1.26.4
onnxruntime==1.24.2 onnxruntime==1.24.2
opencv-python==4.11.0.86 opencv-python==4.11.0.86
opt_einsum==3.4.0 opt_einsum==3.4.0
optree==0.18.0 optree==0.18.0
packaging==26.0 packaging==26.0
pandas==3.0.0 pandas==3.0.0
pillow==12.0.0 pillow==12.0.0
polars==1.38.1 polars==1.38.1
polars-runtime-32==1.38.1 polars-runtime-32==1.38.1
propcache==0.4.1 propcache==0.4.1
protobuf==6.33.5 protobuf==6.33.5
psutil==7.2.2 psutil==7.2.2
Pygments==2.19.2 Pygments==2.19.2
pyparsing==3.3.2 pyparsing==3.3.2
PySocks==1.7.1 PySocks==1.7.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.2.1 python-dotenv==1.2.1
PyYAML==6.0.3 PyYAML==6.0.3
requests==2.32.5 requests==2.32.5
retina-face==0.0.17 retina-face==0.0.17
rich==14.3.2 rich==14.3.2
scipy==1.17.0 scipy==1.17.0
six==1.17.0 six==1.17.0
soupsieve==2.8.3 soupsieve==2.8.3
sympy==1.14.0 sympy==1.14.0
tabulate==0.9.0 tabulate==0.9.0
tensorboard==2.20.0 tensorboard==2.20.0
tensorboard-data-server==0.7.2 tensorboard-data-server==0.7.2
tensorflow==2.20.0 tensorflow==2.20.0
tensorflow-io-gcs-filesystem==0.37.1 tensorflow-io-gcs-filesystem==0.37.1
termcolor==3.3.0 termcolor==3.3.0
tf_keras==2.20.1 tf_keras==2.20.1
torch==2.10.0+cpu torch==2.10.0+cpu
torchreid==0.2.5 torchreid==0.2.5
torchvision==0.25.0+cpu torchvision==0.25.0+cpu
tqdm==4.67.3 tqdm==4.67.3
typing_extensions==4.15.0 typing_extensions==4.15.0
ultralytics==8.4.14 ultralytics==8.4.14
ultralytics-thop==2.0.18 ultralytics-thop==2.0.18
urllib3==2.6.3 urllib3==2.6.3
Werkzeug==3.1.5 Werkzeug==3.1.5
wrapt==2.1.1 wrapt==2.1.1
yarl==1.22.0 yarl==1.22.0

File diff suppressed because it is too large Load Diff