diff --git a/.gitignore b/.gitignore index 232b864..d214f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,174 +1,174 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# 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. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# 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: -# .python-version - -# pipenv -# 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 -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# 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 -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# 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 -# 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. -#.idea/ - - -# ──────────────────────────────────────────────────────── -# ENTORNO VIRTUAL DEL PROYECTO -# ──────────────────────────────────────────────────────── -ia_env/ - -# ──────────────────────────────────────────────────────── -# MODELOS DE IA (Límite de GitHub: 100 MB) -# ──────────────────────────────────────────────────────── - -*.pt +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# 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. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# 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: +# .python-version + +# pipenv +# 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 +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# 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 +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# 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 +# 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. +#.idea/ + + +# ──────────────────────────────────────────────────────── +# ENTORNO VIRTUAL DEL PROYECTO +# ──────────────────────────────────────────────────────── +ia_env/ + +# ──────────────────────────────────────────────────────── +# MODELOS DE IA (Límite de GitHub: 100 MB) +# ──────────────────────────────────────────────────────── + +*.pt *.onnx \ No newline at end of file diff --git a/README.md b/README.md index d10df45..4c6456d 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,62 @@ -# 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. - -## Arquitectura del Proyecto -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). -* `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. - -## Requisitos Previos -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). - * *Windows:* Descargar de la página oficial o usar `scoop install mpv`. - * *Linux:* `sudo apt install mpv` - * *Mac:* `brew install mpv` - -## Guía de Instalación Rápida - -**1. Clonar el repositorio** -Abre tu terminal y clona este proyecto: -```bash -git clone -cd IdentificacionIA´´´ - -**2. Crear un Entorno Virtual (¡Importante!) -Para evitar conflictos de librerías, crea un entorno virtual limpio dentro de la carpeta del proyecto: - -python -m venv venv - - -3. Activar el Entorno Virtual - -En Windows: -.\venv\Scripts\activate - -En Mac/Linux: - -source venv/bin/activate -(Sabrás que está activo si ves un (venv) al inicio de tu línea de comandos). - -4. Instalar Dependencias -Con el entorno activado, instala todas las librerías necesarias: - -pip install -r requirements.txt - - -## Archivos y Carpetas Necesarias - -yolov8n.pt (Detector de personas) - -osnet_x0_25_msmt17.onnx (Extractor de características de ropa) - -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. - - -## Ejecución -Para arrancar el sistema completo con interfaz gráfica y audio, ejecuta: - +# 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. + +## Arquitectura del Proyecto +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). +* `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. + +## Requisitos Previos +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). + * *Windows:* Descargar de la página oficial o usar `scoop install mpv`. + * *Linux:* `sudo apt install mpv` + * *Mac:* `brew install mpv` + +## Guía de Instalación Rápida + +**1. Clonar el repositorio** +Abre tu terminal y clona este proyecto: +```bash +git clone +cd IdentificacionIA´´´ + +**2. Crear un Entorno Virtual (¡Importante!) +Para evitar conflictos de librerías, crea un entorno virtual limpio dentro de la carpeta del proyecto: + +python -m venv venv + + +3. Activar el Entorno Virtual + +En Windows: +.\venv\Scripts\activate + +En Mac/Linux: + +source venv/bin/activate +(Sabrás que está activo si ves un (venv) al inicio de tu línea de comandos). + +4. Instalar Dependencias +Con el entorno activado, instala todas las librerías necesarias: + +pip install -r requirements.txt + + +## Archivos y Carpetas Necesarias + +yolov8n.pt (Detector de personas) + +osnet_x0_25_msmt17.onnx (Extractor de características de ropa) + +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. + + +## Ejecución +Para arrancar el sistema completo con interfaz gráfica y audio, ejecuta: + python main_fusion.py \ No newline at end of file diff --git a/cache_nombres/nombre_Jose Maria.mp3 b/cache_nombres/nombre_Jose Maria.mp3 deleted file mode 100644 index e69de29..0000000 diff --git a/cache_nombres/nombre_Rosa Maria.mp3 b/cache_nombres/nombre_Rosa Maria.mp3 deleted file mode 100644 index 8386987..0000000 Binary files a/cache_nombres/nombre_Rosa Maria.mp3 and /dev/null differ diff --git a/db_institucion/Diana Laura.jpg b/db_institucion/Diana Laura.jpg deleted file mode 100644 index 479b9fd..0000000 Binary files a/db_institucion/Diana Laura.jpg and /dev/null differ diff --git a/db_institucion/Ian Axel.jpg b/db_institucion/Ian Axel.jpg deleted file mode 100644 index e672d38..0000000 Binary files a/db_institucion/Ian Axel.jpg and /dev/null differ diff --git a/fusion.py b/fusion.py index 9878f7c..cd038d8 100644 --- a/fusion.py +++ b/fusion.py @@ -10,6 +10,9 @@ from queue import Queue from deepface import DeepFace from ultralytics import YOLO import warnings +import torch +device = "cuda" if torch.cuda.is_available() else "cpu" +print(f"Usando dispositivo: {device}") warnings.filterwarnings("ignore") @@ -295,7 +298,7 @@ def dibujar_track_fusion(frame_show, trk, global_mem): def main(): print("\nIniciando Sistema") - model = YOLO("yolov8n.pt") + model = YOLO("yolov8n.pt").to("cuda") global_mem = GlobalMemory() managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} cams = [CamStream(u) for u in URLS] @@ -303,7 +306,7 @@ def main(): for _ in range(2): 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 while True: diff --git a/generar_db_rostros.py b/generar_db_rostros.py index b0412a5..6429e5a 100644 --- a/generar_db_rostros.py +++ b/generar_db_rostros.py @@ -1,110 +1,110 @@ -import cv2 -import os -import json -import numpy as np - -# ⚡ Importamos tus motores exactos para no romper la simetría -from reconocimiento2 import detectar_rostros_yunet, gestionar_vectores - -def registrar_desde_webcam(): - print("\n" + "="*50) - print("📸 MÓDULO DE REGISTRO LIMPIO (WEBCAM LOCAL)") - print("Alinea tu rostro, mira a la cámara con buena luz.") - print("Presiona [R] para capturar | [Q] para salir") - print("="*50 + "\n") - - DB_PATH = "db_institucion" - CACHE_PATH = "cache_nombres" - os.makedirs(DB_PATH, exist_ok=True) - os.makedirs(CACHE_PATH, exist_ok=True) - - # 0 es la cámara por defecto de tu laptop - cap = cv2.VideoCapture(0) - if not cap.isOpened(): - print("[!] Error: No se pudo abrir la webcam local.") - return - - while True: - ret, frame = cap.read() - if not ret: continue - - # Espejamos la imagen para que actúe como un espejo natural - frame = cv2.flip(frame, 1) - display_frame = frame.copy() - - # Usamos YuNet para garantizar que estamos capturando una cara válida - faces = detectar_rostros_yunet(frame) - mejor_rostro = None - max_area = 0 - - for (fx, fy, fw, fh, score) in faces: - area = fw * fh - cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 2) - - if area > max_area: - max_area = area - h_frame, w_frame = frame.shape[:2] - - # Mismo margen del 30% que requiere MTCNN para alinear correctamente - m_x, m_y = int(fw * 0.30), int(fh * 0.30) - y1 = max(0, fy - m_y) - y2 = min(h_frame, fy + fh + m_y) - x1 = max(0, fx - m_x) - x2 = min(w_frame, fx + fw + m_x) - - mejor_rostro = frame[y1:y2, x1:x2] - - cv2.putText(display_frame, "Alineate y presiona [R]", (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) - cv2.imshow("Registro Webcam Local", display_frame) - - key = cv2.waitKey(1) & 0xFF - if key == ord('q'): - break - elif key == ord('r'): - if mejor_rostro is not None and mejor_rostro.size > 0: - cv2.imshow("Captura Congelada", mejor_rostro) - cv2.waitKey(1) - - print("\n--- NUEVO REGISTRO ---") - nom = input("Escribe el nombre exacto de la persona: ").strip() - - if nom: - gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower() - genero_guardado = "Woman" if gen_input == 'm' else "Man" - - # 1. Guardamos la foto pura - foto_path = os.path.join(DB_PATH, f"{nom}.jpg") - cv2.imwrite(foto_path, mejor_rostro) - - # 2. Actualizamos el caché de géneros sin usar IA - ruta_generos = os.path.join(CACHE_PATH, "generos.json") - dic_generos = {} - if os.path.exists(ruta_generos): - try: - with open(ruta_generos, 'r') as f: - dic_generos = json.load(f) - except Exception: pass - - dic_generos[nom] = genero_guardado - with open(ruta_generos, 'w') as f: - json.dump(dic_generos, f) - - print(f"\n[OK] Foto guardada. Generando punto de gravedad matemático...") - - # 3. Forzamos la creación del vector en la base de datos - gestionar_vectores(actualizar=True) - print(" Registro inyectado exitosamente en el sistema principal.") - - else: - print("[!] Registro cancelado por nombre vacío.") - - cv2.destroyWindow("Captura Congelada") - else: - print("[!] No se detectó ningún rostro claro. Acércate más a la luz.") - - cap.release() - cv2.destroyAllWindows() - -if __name__ == "__main__": +import cv2 +import os +import json +import numpy as np + +# ⚡ Importamos tus motores exactos para no romper la simetría +from reconocimiento2 import detectar_rostros_yunet, gestionar_vectores + +def registrar_desde_webcam(): + print("\n" + "="*50) + print("📸 MÓDULO DE REGISTRO LIMPIO (WEBCAM LOCAL)") + print("Alinea tu rostro, mira a la cámara con buena luz.") + print("Presiona [R] para capturar | [Q] para salir") + print("="*50 + "\n") + + DB_PATH = "db_institucion" + CACHE_PATH = "cache_nombres" + os.makedirs(DB_PATH, exist_ok=True) + os.makedirs(CACHE_PATH, exist_ok=True) + + # 0 es la cámara por defecto de tu laptop + cap = cv2.VideoCapture(0) + if not cap.isOpened(): + print("[!] Error: No se pudo abrir la webcam local.") + return + + while True: + ret, frame = cap.read() + if not ret: continue + + # Espejamos la imagen para que actúe como un espejo natural + frame = cv2.flip(frame, 1) + display_frame = frame.copy() + + # Usamos YuNet para garantizar que estamos capturando una cara válida + faces = detectar_rostros_yunet(frame) + mejor_rostro = None + max_area = 0 + + for (fx, fy, fw, fh, score) in faces: + area = fw * fh + cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 2) + + if area > max_area: + max_area = area + h_frame, w_frame = frame.shape[:2] + + # Mismo margen del 30% que requiere MTCNN para alinear correctamente + m_x, m_y = int(fw * 0.30), int(fh * 0.30) + y1 = max(0, fy - m_y) + y2 = min(h_frame, fy + fh + m_y) + x1 = max(0, fx - m_x) + x2 = min(w_frame, fx + fw + m_x) + + mejor_rostro = frame[y1:y2, x1:x2] + + cv2.putText(display_frame, "Alineate y presiona [R]", (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) + cv2.imshow("Registro Webcam Local", display_frame) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('r'): + if mejor_rostro is not None and mejor_rostro.size > 0: + cv2.imshow("Captura Congelada", mejor_rostro) + cv2.waitKey(1) + + print("\n--- NUEVO REGISTRO ---") + nom = input("Escribe el nombre exacto de la persona: ").strip() + + if nom: + gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower() + genero_guardado = "Woman" if gen_input == 'm' else "Man" + + # 1. Guardamos la foto pura + foto_path = os.path.join(DB_PATH, f"{nom}.jpg") + cv2.imwrite(foto_path, mejor_rostro) + + # 2. Actualizamos el caché de géneros sin usar IA + ruta_generos = os.path.join(CACHE_PATH, "generos.json") + dic_generos = {} + if os.path.exists(ruta_generos): + try: + with open(ruta_generos, 'r') as f: + dic_generos = json.load(f) + except Exception: pass + + dic_generos[nom] = genero_guardado + with open(ruta_generos, 'w') as f: + json.dump(dic_generos, f) + + print(f"\n[OK] Foto guardada. Generando punto de gravedad matemático...") + + # 3. Forzamos la creación del vector en la base de datos + gestionar_vectores(actualizar=True) + print(" Registro inyectado exitosamente en el sistema principal.") + + else: + print("[!] Registro cancelado por nombre vacío.") + + cv2.destroyWindow("Captura Congelada") + else: + print("[!] No se detectó ningún rostro claro. Acércate más a la luz.") + + cap.release() + cv2.destroyAllWindows() + +if __name__ == "__main__": registrar_desde_webcam() \ No newline at end of file diff --git a/prueba_video.py b/prueba_video.py index d72ea2f..c9ec0c9 100644 --- a/prueba_video.py +++ b/prueba_video.py @@ -1,43 +1,43 @@ -import cv2 -import time -from ultralytics import YOLO -from seguimiento2 import GlobalMemory, CamManager, dibujar_track - -def test_video(video_path): - print(f"Iniciando Benchmark de Video: {video_path}") - model = YOLO("yolov8n.pt") - global_mem = GlobalMemory() - manager = CamManager("TEST_CAM", global_mem) - - cap = cv2.VideoCapture(video_path) - cv2.namedWindow("Benchmark TT", cv2.WINDOW_AUTOSIZE) - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: break - - now = time.time() - frame_show = cv2.resize(frame, (480, 270)) - - # 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') - boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes else [] - - tracks = manager.update(boxes, frame_show, now, turno_activo=True) - - for trk in tracks: - if trk.time_since_update == 0: - dibujar_track(frame_show, trk) - - cv2.imshow("Benchmark TT", frame_show) - - # Si presionas espacio se pausa, con 'q' sales - key = cv2.waitKey(30) & 0xFF - if key == ord('q'): break - elif key == ord(' '): cv2.waitKey(-1) - - cap.release() - cv2.destroyAllWindows() - -if __name__ == "__main__": +import cv2 +import time +from ultralytics import YOLO +from seguimiento2 import GlobalMemory, CamManager, dibujar_track + +def test_video(video_path): + print(f"Iniciando Benchmark de Video: {video_path}") + model = YOLO("yolov8n.pt") + global_mem = GlobalMemory() + manager = CamManager("TEST_CAM", global_mem) + + cap = cv2.VideoCapture(video_path) + cv2.namedWindow("Benchmark TT", cv2.WINDOW_AUTOSIZE) + + while cap.isOpened(): + ret, frame = cap.read() + if not ret: break + + now = time.time() + frame_show = cv2.resize(frame, (480, 270)) + + # 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') + boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes else [] + + tracks = manager.update(boxes, frame_show, now, turno_activo=True) + + for trk in tracks: + if trk.time_since_update == 0: + dibujar_track(frame_show, trk) + + cv2.imshow("Benchmark TT", frame_show) + + # Si presionas espacio se pausa, con 'q' sales + key = cv2.waitKey(30) & 0xFF + if key == ord('q'): break + elif key == ord(' '): cv2.waitKey(-1) + + cap.release() + cv2.destroyAllWindows() + +if __name__ == "__main__": test_video("video.mp4") # Pon aquí el nombre de tu video \ No newline at end of file diff --git a/reconocimiento2.py b/reconocimiento2.py index b17378c..6295792 100644 --- a/reconocimiento2.py +++ b/reconocimiento2.py @@ -1,465 +1,473 @@ -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' -os.environ['CUDA_VISIBLE_DEVICES'] = '-1' - -import cv2 -import numpy as np -from deepface import DeepFace -import pickle -import time -import threading -import asyncio -import edge_tts -import subprocess -from datetime import datetime -import warnings -import urllib.request - -warnings.filterwarnings("ignore") - -# ────────────────────────────────────────────────────────────────────────────── -# CONFIGURACIÓN -# ────────────────────────────────────────────────────────────────────────────── -DB_PATH = "db_institucion" -CACHE_PATH = "cache_nombres" -VECTORS_FILE = "base_datos_rostros.pkl" -TIMESTAMPS_FILE = "representaciones_timestamps.pkl" -UMBRAL_SIM = 0.42 # Por encima → identificado. Por debajo → desconocido. -COOLDOWN_TIME = 15 # Segundos entre saludos - -USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" -RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" - -for path in [DB_PATH, CACHE_PATH]: - os.makedirs(path, exist_ok=True) - -# ────────────────────────────────────────────────────────────────────────────── -# YUNET — Detector facial rápido en CPU -# ────────────────────────────────────────────────────────────────────────────── -YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" - -if not os.path.exists(YUNET_MODEL_PATH): - print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") - 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) - print("YuNet descargado.") - -# Detector estricto para ROIs grandes (persona cerca) -detector_yunet = cv2.FaceDetectorYN.create( - model=YUNET_MODEL_PATH, config="", - input_size=(320, 320), - score_threshold=0.70, - nms_threshold=0.3, - top_k=5000 -) - -# Detector permisivo para ROIs pequeños (persona lejos) -detector_yunet_lejano = cv2.FaceDetectorYN.create( - model=YUNET_MODEL_PATH, config="", - input_size=(320, 320), - score_threshold=0.45, - nms_threshold=0.3, - top_k=5000 -) - -def detectar_rostros_yunet(roi, lock=None): - """ - Elige automáticamente el detector según el tamaño del ROI. - """ - h_roi, w_roi = roi.shape[:2] - area = w_roi * h_roi - det = detector_yunet if area > 8000 else detector_yunet_lejano - - try: - if lock: - with lock: - det.setInputSize((w_roi, h_roi)) - _, faces = det.detect(roi) - else: - det.setInputSize((w_roi, h_roi)) - _, faces = det.detect(roi) - except Exception: - return [] - - if faces is None: - return [] - - resultado = [] - for face in faces: - try: - fx, fy, fw, fh = map(int, face[:4]) - score = float(face[14]) if len(face) > 14 else 1.0 - resultado.append((fx, fy, fw, fh, score)) - except (ValueError, OverflowError, TypeError): - continue - return resultado - - -# ────────────────────────────────────────────────────────────────────────────── -# SISTEMA DE AUDIO -# ────────────────────────────────────────────────────────────────────────────── -def obtener_audios_humanos(genero): - hora = datetime.now().hour - es_mujer = genero.lower() == 'woman' - suffix = "_m.mp3" if es_mujer else "_h.mp3" - if 5 <= hora < 12: - intro = "dias.mp3" - elif 12 <= hora < 19: - intro = "tarde.mp3" - else: - intro = "noches.mp3" - cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix - return intro, cierre - - -async def sintetizar_nombre(nombre, ruta): - nombre_limpio = nombre.replace('_', ' ') - try: - comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") - await comunicador.save(ruta) - except Exception: - pass - - -def reproducir(archivo): - if os.path.exists(archivo): - subprocess.Popen( - ["mpv", "--no-video", "--volume=100", archivo], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - -def hilo_bienvenida(nombre, genero): - archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") - - if not os.path.exists(archivo_nombre): - try: - asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) - except Exception: - pass - - intro, cierre = obtener_audios_humanos(genero) - - archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] - if archivos: - subprocess.Popen( - ["mpv", "--no-video", "--volume=100"] + archivos, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - -# ────────────────────────────────────────────────────────────────────────────── -# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN) -# ────────────────────────────────────────────────────────────────────────────── -def gestionar_vectores(actualizar=False): - import json # ⚡ Asegúrate de tener importado json - - vectores_actuales = {} - if os.path.exists(VECTORS_FILE): - try: - with open(VECTORS_FILE, 'rb') as f: - vectores_actuales = pickle.load(f) - except Exception: - vectores_actuales = {} - - if not actualizar: - return vectores_actuales - - timestamps = {} - if os.path.exists(TIMESTAMPS_FILE): - try: - with open(TIMESTAMPS_FILE, 'rb') as f: - timestamps = pickle.load(f) - except Exception: - timestamps = {} - - # ────────────────────────────────────────────────────────── - # CARGA DEL CACHÉ DE GÉNEROS - # ────────────────────────────────────────────────────────── - ruta_generos = os.path.join(CACHE_PATH, "generos.json") - dic_generos = {} - if os.path.exists(ruta_generos): - try: - with open(ruta_generos, 'r') as f: - dic_generos = json.load(f) - except Exception: - pass - - print("\nACTUALIZANDO BASE DE DATOS (Alineación y Caché de Géneros)...") - imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] - nombres_en_disco = set() - hubo_cambios = False - cambio_generos = False # Bandera para saber si actualizamos el JSON - - for archivo in imagenes: - nombre_archivo = os.path.splitext(archivo)[0] - ruta_img = os.path.join(DB_PATH, archivo) - nombres_en_disco.add(nombre_archivo) - - ts_actual = os.path.getmtime(ruta_img) - ts_guardado = timestamps.get(nombre_archivo, 0) - - # Si ya tenemos el vector pero NO tenemos su género en el JSON, forzamos el procesamiento - falta_genero = nombre_archivo not in dic_generos - - if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero: - continue - - try: - img_db = cv2.imread(ruta_img) - lab = cv2.cvtColor(img_db, cv2.COLOR_BGR2LAB) - l, a, b = cv2.split(lab) - clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) - l = clahe.apply(l) - img_mejorada = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) - - # IA DE GÉNERO (Solo se ejecuta 1 vez por persona en toda la vida del sistema) - if falta_genero: - try: - analisis = DeepFace.analyze(img_mejorada, actions=['gender'], enforce_detection=False)[0] - dic_generos[nombre_archivo] = analisis.get('dominant_gender', 'Man') - except Exception: - dic_generos[nombre_archivo] = "Man" # Respaldo - cambio_generos = True - - # Extraemos el vector - res = DeepFace.represent( - img_path=img_mejorada, - model_name="ArcFace", - detector_backend="mtcnn", - align=True, - enforce_detection=True - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - - norma = np.linalg.norm(emb) - if norma > 0: - emb = emb / norma - - vectores_actuales[nombre_archivo] = emb - timestamps[nombre_archivo] = ts_actual - hubo_cambios = True - print(f" Procesado y alineado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}") - - except Exception as e: - print(f" Rostro no válido en '{archivo}', omitido. Error: {e}") - - # Limpieza de eliminados - for nombre in list(vectores_actuales.keys()): - if nombre not in nombres_en_disco: - del vectores_actuales[nombre] - timestamps.pop(nombre, None) - if nombre in dic_generos: - del dic_generos[nombre] - cambio_generos = True - hubo_cambios = True - print(f" Eliminado (sin foto): {nombre}") - - # Guardado de la memoria - if hubo_cambios: - with open(VECTORS_FILE, 'wb') as f: - pickle.dump(vectores_actuales, f) - with open(TIMESTAMPS_FILE, 'wb') as f: - pickle.dump(timestamps, f) - - # Guardado del JSON de géneros si hubo descubrimientos nuevos - if cambio_generos: - with open(ruta_generos, 'w') as f: - json.dump(dic_generos, f) - - if hubo_cambios or cambio_generos: - print(" Sincronización terminada.\n") - else: - print(" Sin cambios. Base de datos al día.\n") - - return vectores_actuales - -# ────────────────────────────────────────────────────────────────────────────── -# BÚSQUEDA BLINDADA (Similitud Coseno estricta) -# ────────────────────────────────────────────────────────────────────────────── -def buscar_mejor_match(emb_consulta, base_datos): - # ⚡ MAGIA 3: Normalización L2 del vector entrante - norma = np.linalg.norm(emb_consulta) - if norma > 0: - emb_consulta = emb_consulta / norma - - 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) - sim = float(np.dot(emb_consulta, vec)) - if sim > max_sim: - max_sim = sim - mejor_match = nombre - - return mejor_match, max_sim - -# ────────────────────────────────────────────────────────────────────────────── -# LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA) -# ────────────────────────────────────────────────────────────────────────────── -def sistema_interactivo(): - base_datos = gestionar_vectores(actualizar=False) - cap = cv2.VideoCapture(RTSP_URL) - ultimo_saludo = 0 - persona_actual = None - confirmaciones = 0 - - print("\n" + "=" * 50) - print(" MÓDULO DE REGISTRO Y DEPURACIÓN ESTRICTO") - print(" [R] Registrar nuevo rostro | [Q] Salir") - print("=" * 50 + "\n") - - faces_ultimo_frame = [] - - while True: - ret, frame = cap.read() - if not ret: - time.sleep(2) - cap.open(RTSP_URL) - continue - - h, w = frame.shape[:2] - display_frame = frame.copy() - tiempo_actual = time.time() - - faces_raw = detectar_rostros_yunet(frame) - faces_ultimo_frame = faces_raw - - for (fx, fy, fw, fh, score_yunet) in faces_raw: - fx = max(0, fx); fy = max(0, fy) - fw = min(w - fx, fw); fh = min(h - fy, fh) - if fw <= 0 or fh <= 0: - continue - - cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) - cv2.putText(display_frame, f"YN:{score_yunet:.2f}", - (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) - - if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: - continue - - m = int(fw * 0.15) - roi = frame[max(0, fy-m): min(h, fy+fh+m), - max(0, fx-m): min(w, fx+fw+m)] - - # 🛡️ FILTRO DE TAMAÑO FÍSICO - if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: - cv2.putText(display_frame, "muy pequeno", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) - continue - - # 🛡️ FILTRO DE NITIDEZ - gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) - nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() - if nitidez < 50.0: - cv2.putText(display_frame, f"blur({nitidez:.0f})", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) - continue - - # 🌙 SIMETRÍA 1: VISIÓN NOCTURNA (CLAHE) AL VIDEO EN VIVO - try: - lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) - l, a, b = cv2.split(lab) - clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) - l = clahe.apply(l) - roi_mejorado = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) - except Exception: - roi_mejorado = roi # Respaldo de seguridad - - # 🧠 SIMETRÍA 2: MOTOR MTCNN Y ALINEACIÓN (Igual que la Base de Datos) - try: - res = DeepFace.represent( - img_path=roi_mejorado, - model_name="ArcFace", - detector_backend="mtcnn", # El mismo que en gestionar_vectores - align=True, # Enderezamos la cara - enforce_detection=True # Si MTCNN no ve cara clara, aborta - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - mejor_match, max_sim = buscar_mejor_match(emb, base_datos) - - except Exception: - # MTCNN abortó porque la cara estaba de perfil, tapada o no era una cara - cv2.putText(display_frame, "MTCNN Ignorado", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) - continue - - estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" - nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" - n_bloques = int(max_sim * 20) - barra = "█" * n_bloques + "░" * (20 - n_bloques) - print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " - f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") - - if max_sim > UMBRAL_SIM and mejor_match: - color = (0, 255, 0) - texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" - - if mejor_match == persona_actual: - confirmaciones += 1 - else: - persona_actual, confirmaciones = mejor_match, 1 - - if confirmaciones >= 1: - cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) - try: - analisis = DeepFace.analyze( - roi_mejorado, actions=['gender'], enforce_detection=False - )[0] - genero = analisis['dominant_gender'] - except Exception: - genero = "Man" - - threading.Thread( - target=hilo_bienvenida, - args=(mejor_match, genero), - daemon=True - ).start() - ultimo_saludo = tiempo_actual - confirmaciones = 0 - - else: - color = (0, 0, 255) - texto = f"? ({max_sim:.2f})" - confirmaciones = max(0, confirmaciones - 1) - - cv2.putText(display_frame, texto, - (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) - - cv2.imshow("Módulo de Registro", display_frame) - key = cv2.waitKey(1) & 0xFF - - if key == ord('q'): - break - - elif key == ord('r'): - if faces_ultimo_frame: - areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] - fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] - - m_x = int(fw * 0.30) - m_y = int(fh * 0.30) - 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)] - - if face_roi.size > 0: - nom = input("\nNombre de la persona: ").strip() - if nom: - foto_path = os.path.join(DB_PATH, f"{nom}.jpg") - cv2.imwrite(foto_path, face_roi) - print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") - base_datos = gestionar_vectores(actualizar=True) - else: - print("[!] Registro cancelado.") - else: - 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() +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +os.environ['CUDA_VISIBLE_DEVICES'] = '-1' + +import cv2 +import numpy as np +from deepface import DeepFace +import pickle +import time +import threading +import asyncio +import edge_tts +import subprocess +from datetime import datetime +import warnings +import urllib.request +import torch + +if torch.cuda.is_available(): + device = "cuda" + print("GPU detectada → usando GPU 🚀") +else: + device = "cpu" + print("GPU no disponible → usando CPU ⚠️") + +warnings.filterwarnings("ignore") + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +DB_PATH = "db_institucion" +CACHE_PATH = "cache_nombres" +VECTORS_FILE = "base_datos_rostros.pkl" +TIMESTAMPS_FILE = "representaciones_timestamps.pkl" +UMBRAL_SIM = 0.42 # Por encima → identificado. Por debajo → desconocido. +COOLDOWN_TIME = 15 # Segundos entre saludos + +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" +RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" + +for path in [DB_PATH, CACHE_PATH]: + os.makedirs(path, exist_ok=True) + +# ────────────────────────────────────────────────────────────────────────────── +# YUNET — Detector facial rápido en CPU +# ────────────────────────────────────────────────────────────────────────────── +YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" + +if not os.path.exists(YUNET_MODEL_PATH): + print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") + 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) + print("YuNet descargado.") + +# Detector estricto para ROIs grandes (persona cerca) +detector_yunet = cv2.FaceDetectorYN.create( + model=YUNET_MODEL_PATH, config="", + input_size=(320, 320), + score_threshold=0.70, + nms_threshold=0.3, + top_k=5000 +) + +# Detector permisivo para ROIs pequeños (persona lejos) +detector_yunet_lejano = cv2.FaceDetectorYN.create( + model=YUNET_MODEL_PATH, config="", + input_size=(320, 320), + score_threshold=0.45, + nms_threshold=0.3, + top_k=5000 +) + +def detectar_rostros_yunet(roi, lock=None): + """ + Elige automáticamente el detector según el tamaño del ROI. + """ + h_roi, w_roi = roi.shape[:2] + area = w_roi * h_roi + det = detector_yunet if area > 8000 else detector_yunet_lejano + + try: + if lock: + with lock: + det.setInputSize((w_roi, h_roi)) + _, faces = det.detect(roi) + else: + det.setInputSize((w_roi, h_roi)) + _, faces = det.detect(roi) + except Exception: + return [] + + if faces is None: + return [] + + resultado = [] + for face in faces: + try: + fx, fy, fw, fh = map(int, face[:4]) + score = float(face[14]) if len(face) > 14 else 1.0 + resultado.append((fx, fy, fw, fh, score)) + except (ValueError, OverflowError, TypeError): + continue + return resultado + + +# ────────────────────────────────────────────────────────────────────────────── +# SISTEMA DE AUDIO +# ────────────────────────────────────────────────────────────────────────────── +def obtener_audios_humanos(genero): + hora = datetime.now().hour + es_mujer = genero.lower() == 'woman' + suffix = "_m.mp3" if es_mujer else "_h.mp3" + if 5 <= hora < 12: + intro = "dias.mp3" + elif 12 <= hora < 19: + intro = "tarde.mp3" + else: + intro = "noches.mp3" + cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix + return intro, cierre + + +async def sintetizar_nombre(nombre, ruta): + nombre_limpio = nombre.replace('_', ' ') + try: + comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") + await comunicador.save(ruta) + except Exception: + pass + + +def reproducir(archivo): + if os.path.exists(archivo): + subprocess.Popen( + ["mpv", "--no-video", "--volume=100", archivo], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + +def hilo_bienvenida(nombre, genero): + archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") + + if not os.path.exists(archivo_nombre): + try: + asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) + except Exception: + pass + + intro, cierre = obtener_audios_humanos(genero) + + archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] + if archivos: + subprocess.Popen( + ["mpv", "--no-video", "--volume=100"] + archivos, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + +# ────────────────────────────────────────────────────────────────────────────── +# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN) +# ────────────────────────────────────────────────────────────────────────────── +def gestionar_vectores(actualizar=False): + import json # ⚡ Asegúrate de tener importado json + + vectores_actuales = {} + if os.path.exists(VECTORS_FILE): + try: + with open(VECTORS_FILE, 'rb') as f: + vectores_actuales = pickle.load(f) + except Exception: + vectores_actuales = {} + + if not actualizar: + return vectores_actuales + + timestamps = {} + if os.path.exists(TIMESTAMPS_FILE): + try: + with open(TIMESTAMPS_FILE, 'rb') as f: + timestamps = pickle.load(f) + except Exception: + timestamps = {} + + # ────────────────────────────────────────────────────────── + # CARGA DEL CACHÉ DE GÉNEROS + # ────────────────────────────────────────────────────────── + ruta_generos = os.path.join(CACHE_PATH, "generos.json") + dic_generos = {} + if os.path.exists(ruta_generos): + try: + with open(ruta_generos, 'r') as f: + dic_generos = json.load(f) + except Exception: + pass + + print("\nACTUALIZANDO BASE DE DATOS (Alineación y Caché de Géneros)...") + imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] + nombres_en_disco = set() + hubo_cambios = False + cambio_generos = False # Bandera para saber si actualizamos el JSON + + for archivo in imagenes: + nombre_archivo = os.path.splitext(archivo)[0] + ruta_img = os.path.join(DB_PATH, archivo) + nombres_en_disco.add(nombre_archivo) + + ts_actual = os.path.getmtime(ruta_img) + ts_guardado = timestamps.get(nombre_archivo, 0) + + # Si ya tenemos el vector pero NO tenemos su género en el JSON, forzamos el procesamiento + falta_genero = nombre_archivo not in dic_generos + + if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero: + continue + + try: + img_db = cv2.imread(ruta_img) + lab = cv2.cvtColor(img_db, cv2.COLOR_BGR2LAB) + l, a, b = cv2.split(lab) + clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) + l = clahe.apply(l) + img_mejorada = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) + + # IA DE GÉNERO (Solo se ejecuta 1 vez por persona en toda la vida del sistema) + if falta_genero: + try: + analisis = DeepFace.analyze(img_mejorada, actions=['gender'], enforce_detection=False)[0] + dic_generos[nombre_archivo] = analisis.get('dominant_gender', 'Man') + except Exception: + dic_generos[nombre_archivo] = "Man" # Respaldo + cambio_generos = True + + # Extraemos el vector + res = DeepFace.represent( + img_path=img_mejorada, + model_name="ArcFace", + detector_backend="opencv", + align=False, + enforce_detection=True + ) + emb = np.array(res[0]["embedding"], dtype=np.float32) + + norma = np.linalg.norm(emb) + if norma > 0: + emb = emb / norma + + vectores_actuales[nombre_archivo] = emb + timestamps[nombre_archivo] = ts_actual + hubo_cambios = True + print(f" Procesado y alineado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}") + + except Exception as e: + print(f" Rostro no válido en '{archivo}', omitido. Error: {e}") + + # Limpieza de eliminados + for nombre in list(vectores_actuales.keys()): + if nombre not in nombres_en_disco: + del vectores_actuales[nombre] + timestamps.pop(nombre, None) + if nombre in dic_generos: + del dic_generos[nombre] + cambio_generos = True + hubo_cambios = True + print(f" Eliminado (sin foto): {nombre}") + + # Guardado de la memoria + if hubo_cambios: + with open(VECTORS_FILE, 'wb') as f: + pickle.dump(vectores_actuales, f) + with open(TIMESTAMPS_FILE, 'wb') as f: + pickle.dump(timestamps, f) + + # Guardado del JSON de géneros si hubo descubrimientos nuevos + if cambio_generos: + with open(ruta_generos, 'w') as f: + json.dump(dic_generos, f) + + if hubo_cambios or cambio_generos: + print(" Sincronización terminada.\n") + else: + print(" Sin cambios. Base de datos al día.\n") + + return vectores_actuales + +# ────────────────────────────────────────────────────────────────────────────── +# BÚSQUEDA BLINDADA (Similitud Coseno estricta) +# ────────────────────────────────────────────────────────────────────────────── +def buscar_mejor_match(emb_consulta, base_datos): + # ⚡ MAGIA 3: Normalización L2 del vector entrante + norma = np.linalg.norm(emb_consulta) + if norma > 0: + emb_consulta = emb_consulta / norma + + 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) + sim = float(np.dot(emb_consulta, vec)) + if sim > max_sim: + max_sim = sim + mejor_match = nombre + + return mejor_match, max_sim + +# ────────────────────────────────────────────────────────────────────────────── +# LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA) +# ────────────────────────────────────────────────────────────────────────────── +def sistema_interactivo(): + base_datos = gestionar_vectores(actualizar=False) + cap = cv2.VideoCapture(RTSP_URL) + ultimo_saludo = 0 + persona_actual = None + confirmaciones = 0 + + print("\n" + "=" * 50) + print(" MÓDULO DE REGISTRO Y DEPURACIÓN ESTRICTO") + print(" [R] Registrar nuevo rostro | [Q] Salir") + print("=" * 50 + "\n") + + faces_ultimo_frame = [] + + while True: + ret, frame = cap.read() + if not ret: + time.sleep(2) + cap.open(RTSP_URL) + continue + + h, w = frame.shape[:2] + display_frame = frame.copy() + tiempo_actual = time.time() + + faces_raw = detectar_rostros_yunet(frame) + faces_ultimo_frame = faces_raw + + for (fx, fy, fw, fh, score_yunet) in faces_raw: + fx = max(0, fx); fy = max(0, fy) + fw = min(w - fx, fw); fh = min(h - fy, fh) + if fw <= 0 or fh <= 0: + continue + + cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) + cv2.putText(display_frame, f"YN:{score_yunet:.2f}", + (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) + + if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: + continue + + m = int(fw * 0.15) + roi = frame[max(0, fy-m): min(h, fy+fh+m), + max(0, fx-m): min(w, fx+fw+m)] + + # 🛡️ FILTRO DE TAMAÑO FÍSICO + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: + cv2.putText(display_frame, "muy pequeno", + (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) + continue + + # 🛡️ FILTRO DE NITIDEZ + gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) + nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() + if nitidez < 50.0: + cv2.putText(display_frame, f"blur({nitidez:.0f})", + (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) + continue + + # 🌙 SIMETRÍA 1: VISIÓN NOCTURNA (CLAHE) AL VIDEO EN VIVO + try: + lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) + l, a, b = cv2.split(lab) + clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) + l = clahe.apply(l) + roi_mejorado = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) + except Exception: + roi_mejorado = roi # Respaldo de seguridad + + # 🧠 SIMETRÍA 2: MOTOR MTCNN Y ALINEACIÓN (Igual que la Base de Datos) + try: + res = DeepFace.represent( + img_path=roi_mejorado, + model_name="ArcFace", + detector_backend="mtcnn", # El mismo que en gestionar_vectores + align=True, # Enderezamos la cara + enforce_detection=True # Si MTCNN no ve cara clara, aborta + ) + emb = np.array(res[0]["embedding"], dtype=np.float32) + mejor_match, max_sim = buscar_mejor_match(emb, base_datos) + + except Exception: + # MTCNN abortó porque la cara estaba de perfil, tapada o no era una cara + cv2.putText(display_frame, "MTCNN Ignorado", + (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) + continue + + estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" + nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" + n_bloques = int(max_sim * 20) + barra = "█" * n_bloques + "░" * (20 - n_bloques) + print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " + f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") + + if max_sim > UMBRAL_SIM and mejor_match: + color = (0, 255, 0) + texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" + + if mejor_match == persona_actual: + confirmaciones += 1 + else: + persona_actual, confirmaciones = mejor_match, 1 + + if confirmaciones >= 1: + cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) + try: + analisis = DeepFace.analyze( + roi_mejorado, actions=['gender'], enforce_detection=False + )[0] + genero = analisis['dominant_gender'] + except Exception: + genero = "Man" + + threading.Thread( + target=hilo_bienvenida, + args=(mejor_match, genero), + daemon=True + ).start() + ultimo_saludo = tiempo_actual + confirmaciones = 0 + + else: + color = (0, 0, 255) + texto = f"? ({max_sim:.2f})" + confirmaciones = max(0, confirmaciones - 1) + + cv2.putText(display_frame, texto, + (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) + + cv2.imshow("Módulo de Registro", display_frame) + key = cv2.waitKey(1) & 0xFF + + if key == ord('q'): + break + + elif key == ord('r'): + if faces_ultimo_frame: + areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] + fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] + + m_x = int(fw * 0.30) + m_y = int(fh * 0.30) + 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)] + + if face_roi.size > 0: + nom = input("\nNombre de la persona: ").strip() + if nom: + foto_path = os.path.join(DB_PATH, f"{nom}.jpg") + cv2.imwrite(foto_path, face_roi) + print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") + base_datos = gestionar_vectores(actualizar=True) + else: + print("[!] Registro cancelado.") + else: + 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() diff --git a/requirements.txt b/requirements.txt index 1096595..e008e7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,96 +1,96 @@ -absl-py==2.4.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 -aiosignal==1.4.0 -astunparse==1.6.3 -attrs==25.4.0 -beautifulsoup4==4.14.3 -blinker==1.9.0 -certifi==2026.1.4 -charset-normalizer==3.4.4 -click==8.3.1 -contourpy==1.3.3 -cycler==0.12.1 -deepface==0.0.98 -edge-tts==7.2.7 -filelock==3.20.0 -fire==0.7.1 -Flask==3.1.2 -flask-cors==6.0.2 -flatbuffers==25.12.19 -fonttools==4.61.1 -frozenlist==1.8.0 -fsspec==2025.12.0 -gast==0.7.0 -gdown==5.2.1 -google-pasta==0.2.0 -grpcio==1.78.0 -gunicorn==25.0.3 -h5py==3.15.1 -idna==3.11 -itsdangerous==2.2.0 -Jinja2==3.1.6 -joblib==1.5.3 -keras==3.13.2 -kiwisolver==1.4.9 -lap==0.5.12 -libclang==18.1.1 -lightdsa==0.0.3 -lightecc==0.0.4 -lightphe==0.0.20 -lz4==4.4.5 -Markdown==3.10.2 -markdown-it-py==4.0.0 -MarkupSafe==2.1.5 -matplotlib==3.10.8 -mdurl==0.1.2 -ml_dtypes==0.5.4 -mpmath==1.3.0 -mtcnn==1.0.0 -multidict==6.7.1 -namex==0.1.0 -networkx==3.6.1 -numpy==1.26.4 -onnxruntime==1.24.2 -opencv-python==4.11.0.86 -opt_einsum==3.4.0 -optree==0.18.0 -packaging==26.0 -pandas==3.0.0 -pillow==12.0.0 -polars==1.38.1 -polars-runtime-32==1.38.1 -propcache==0.4.1 -protobuf==6.33.5 -psutil==7.2.2 -Pygments==2.19.2 -pyparsing==3.3.2 -PySocks==1.7.1 -python-dateutil==2.9.0.post0 -python-dotenv==1.2.1 -PyYAML==6.0.3 -requests==2.32.5 -retina-face==0.0.17 -rich==14.3.2 -scipy==1.17.0 -six==1.17.0 -soupsieve==2.8.3 -sympy==1.14.0 -tabulate==0.9.0 -tensorboard==2.20.0 -tensorboard-data-server==0.7.2 -tensorflow==2.20.0 -tensorflow-io-gcs-filesystem==0.37.1 -termcolor==3.3.0 -tf_keras==2.20.1 -torch==2.10.0+cpu -torchreid==0.2.5 -torchvision==0.25.0+cpu -tqdm==4.67.3 -typing_extensions==4.15.0 -ultralytics==8.4.14 -ultralytics-thop==2.0.18 -urllib3==2.6.3 -Werkzeug==3.1.5 -wrapt==2.1.1 -yarl==1.22.0 +absl-py==2.4.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.13.3 +aiosignal==1.4.0 +astunparse==1.6.3 +attrs==25.4.0 +beautifulsoup4==4.14.3 +blinker==1.9.0 +certifi==2026.1.4 +charset-normalizer==3.4.4 +click==8.3.1 +contourpy==1.3.3 +cycler==0.12.1 +deepface==0.0.98 +edge-tts==7.2.7 +filelock==3.20.0 +fire==0.7.1 +Flask==3.1.2 +flask-cors==6.0.2 +flatbuffers==25.12.19 +fonttools==4.61.1 +frozenlist==1.8.0 +fsspec==2025.12.0 +gast==0.7.0 +gdown==5.2.1 +google-pasta==0.2.0 +grpcio==1.78.0 +gunicorn==25.0.3 +h5py==3.15.1 +idna==3.11 +itsdangerous==2.2.0 +Jinja2==3.1.6 +joblib==1.5.3 +keras==3.13.2 +kiwisolver==1.4.9 +lap==0.5.12 +libclang==18.1.1 +lightdsa==0.0.3 +lightecc==0.0.4 +lightphe==0.0.20 +lz4==4.4.5 +Markdown==3.10.2 +markdown-it-py==4.0.0 +MarkupSafe==2.1.5 +matplotlib==3.10.8 +mdurl==0.1.2 +ml_dtypes==0.5.4 +mpmath==1.3.0 +mtcnn==1.0.0 +multidict==6.7.1 +namex==0.1.0 +networkx==3.6.1 +numpy==1.26.4 +onnxruntime==1.24.2 +opencv-python==4.11.0.86 +opt_einsum==3.4.0 +optree==0.18.0 +packaging==26.0 +pandas==3.0.0 +pillow==12.0.0 +polars==1.38.1 +polars-runtime-32==1.38.1 +propcache==0.4.1 +protobuf==6.33.5 +psutil==7.2.2 +Pygments==2.19.2 +pyparsing==3.3.2 +PySocks==1.7.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +PyYAML==6.0.3 +requests==2.32.5 +retina-face==0.0.17 +rich==14.3.2 +scipy==1.17.0 +six==1.17.0 +soupsieve==2.8.3 +sympy==1.14.0 +tabulate==0.9.0 +tensorboard==2.20.0 +tensorboard-data-server==0.7.2 +tensorflow==2.20.0 +tensorflow-io-gcs-filesystem==0.37.1 +termcolor==3.3.0 +tf_keras==2.20.1 +torch==2.10.0+cpu +torchreid==0.2.5 +torchvision==0.25.0+cpu +tqdm==4.67.3 +typing_extensions==4.15.0 +ultralytics==8.4.14 +ultralytics-thop==2.0.18 +urllib3==2.6.3 +Werkzeug==3.1.5 +wrapt==2.1.1 +yarl==1.22.0 diff --git a/vesiones_seguras.txt b/vesiones_seguras.txt index 0653fd8..0db3b2c 100644 --- a/vesiones_seguras.txt +++ b/vesiones_seguras.txt @@ -1,1258 +1,1258 @@ -############################################################ seguimiento2.py -import cv2 -import numpy as np -import time -import threading -from scipy.optimize import linear_sum_assignment -from scipy.spatial.distance import cosine -from ultralytics import YOLO -import onnxruntime as ort -import os - -# ────────────────────────────────────────────────────────────────────────────── -# CONFIGURACIÓN DEL SISTEMA -# ────────────────────────────────────────────────────────────────────────────── -USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" -SECUENCIA = [1, 7, 5, 8, 3, 6] -# 🛡️ RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg) -os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000" -URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA] -ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx" - -VECINOS = { - "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], - "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] -} - -ASPECT_RATIO_MIN = 0.5 -ASPECT_RATIO_MAX = 4.0 -AREA_MIN_CALIDAD = 1200 -FRAMES_CALIDAD = 2 -TIEMPO_MAX_AUSENCIA = 800.0 - -# ⚡ UMBRALES MAESTROS: Tolerancia altísima entre cámaras vecinas para ignorar cambios de luz -UMBRAL_REID_MISMA_CAM = 0.62 -UMBRAL_REID_VECINO = 0.53 -UMBRAL_REID_NO_VECINO = 0.72 -MAX_FIRMAS_MEMORIA = 15 - -C_CANDIDATO = (150, 150, 150) -C_LOCAL = (0, 255, 0) -C_GLOBAL = (0, 165, 255) -C_GRUPO = (0, 0, 255) -C_APRENDIZAJE = (255, 255, 0) -FUENTE = cv2.FONT_HERSHEY_SIMPLEX - -# ────────────────────────────────────────────────────────────────────────────── -# INICIALIZACIÓN OSNET -# ────────────────────────────────────────────────────────────────────────────── -print("Cargando cerebro de Re-Identificación (OSNet)...") -try: - ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) - input_name = ort_session.get_inputs()[0].name - print("Modelo OSNet cargado exitosamente.") -except Exception as e: - print(f"ERROR FATAL: No se pudo cargar {ONNX_MODEL_PATH}.") - exit() - -MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 3, 1, 1) -STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1) - -# ────────────────────────────────────────────────────────────────────────────── -# 1. EXTRACCIÓN DE FIRMAS (Deep + Color + Textura) -# ────────────────────────────────────────────────────────────────────────────── -def analizar_calidad(box): - x1, y1, x2, y2 = box - w, h = x2 - x1, y2 - y1 - if w <= 0 or h <= 0: return False - return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD) - -def preprocess_onnx(roi): - img = cv2.resize(roi, (128, 256)) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 - img = np.expand_dims(img, axis=0) - img = (img - MEAN) / STD - return img - -def extraer_color_zonas(img): - h_roi = img.shape[0] - t1, t2 = int(h_roi * 0.15), int(h_roi * 0.55) - zonas = [img[:t1, :], img[t1:t2, :], img[t2:, :]] - - def hist_zona(z): - if z.size == 0: return np.zeros(16 * 8) - hsv = cv2.cvtColor(z, cv2.COLOR_BGR2HSV) - hist = cv2.calcHist([hsv], [0, 1], None, [16, 8], [0, 180, 0, 256]) - cv2.normalize(hist, hist) - return hist.flatten() - return np.concatenate([hist_zona(z) for z in zonas]) - -def extraer_textura_rapida(roi): - if roi.size == 0: return np.zeros(16) - gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) - gray_eq = cv2.equalizeHist(gray) - gx = cv2.Sobel(gray_eq, cv2.CV_32F, 1, 0, ksize=3) - gy = cv2.Sobel(gray_eq, cv2.CV_32F, 0, 1, ksize=3) - mag, _ = cv2.cartToPolar(gx, gy) - hist = cv2.calcHist([mag], [0], None, [16], [0, 256]) - cv2.normalize(hist, hist) - return hist.flatten() - -def extraer_firma_hibrida(frame, box): - try: - x1, y1, x2, y2 = map(int, box) - fh, fw = frame.shape[:2] - x1_c, y1_c = max(0, x1), max(0, y1) - x2_c, y2_c = min(fw, x2), min(fh, y2) - - roi = frame[y1_c:y2_c, x1_c:x2_c] - if roi.size == 0 or roi.shape[0] < 20 or roi.shape[1] < 10: return None - - calidad_area = (x2_c - x1_c) * (y2_c - y1_c) - - blob = preprocess_onnx(roi) - blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32) - blob_16[0] = blob[0] - deep_feat = ort_session.run(None, {input_name: blob_16})[0][0].flatten() - norma = np.linalg.norm(deep_feat) - if norma > 0: deep_feat = deep_feat / norma - - color_feat = extraer_color_zonas(roi) - textura_feat = extraer_textura_rapida(roi) - - return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area} - except Exception: - return None - -# ⚡ EL SECRETO: 100% IA entre cámaras. Textura solo en la misma cámara. -def similitud_hibrida(f1, f2, cross_cam=False): - if f1 is None or f2 is None: return 0.0 - - sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep'])) - - if cross_cam: - # Si saltó de cámara, la luz cambia. Ignoramos color y textura. Confiamos 100% en OSNet. - return sim_deep - - # Si está en la misma cámara, usamos color y textura para separar a los vestidos de negro. - if f1['color'].shape == f2['color'].shape and f1['color'].size > 1: - L = len(f1['color']) // 3 - sim_head = max(0.0, float(cv2.compareHist(f1['color'][:L].astype(np.float32), f2['color'][:L].astype(np.float32), cv2.HISTCMP_CORREL))) - sim_torso = max(0.0, float(cv2.compareHist(f1['color'][L:2*L].astype(np.float32), f2['color'][L:2*L].astype(np.float32), cv2.HISTCMP_CORREL))) - sim_legs = max(0.0, float(cv2.compareHist(f1['color'][2*L:].astype(np.float32), f2['color'][2*L:].astype(np.float32), cv2.HISTCMP_CORREL))) - sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs) - else: sim_color = 0.0 - - if 'textura' in f1 and 'textura' in f2 and f1['textura'].size > 1: - sim_textura = max(0.0, float(cv2.compareHist(f1['textura'].astype(np.float32), f2['textura'].astype(np.float32), cv2.HISTCMP_CORREL))) - else: sim_textura = 0.0 - - return (sim_deep * 0.80) + (sim_color * 0.10) + (sim_textura * 0.10) - -# ────────────────────────────────────────────────────────────────────────────── -# 2. KALMAN TRACKER -# ────────────────────────────────────────────────────────────────────────────── -class KalmanTrack: - _count = 0 - def __init__(self, box, now): - self.kf = cv2.KalmanFilter(7, 4) - self.kf.measurementMatrix = np.array([[1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0]], np.float32) - self.kf.transitionMatrix = np.eye(7, dtype=np.float32) - self.kf.transitionMatrix[0,4] = 1; self.kf.transitionMatrix[1,5] = 1; self.kf.transitionMatrix[2,6] = 1 - self.kf.processNoiseCov *= 0.03 - self.kf.statePost = np.zeros((7, 1), np.float32) - self.kf.statePost[:4] = self._convert_bbox_to_z(box) - self.local_id = KalmanTrack._count - KalmanTrack._count += 1 - self.gid = None - self.origen_global = False - self.aprendiendo = False - self.box = list(box) - self.ts_creacion = now - self.ts_ultima_deteccion = now - self.time_since_update = 0 - self.en_grupo = False - self.frames_buena_calidad = 0 - self.listo_para_id = False - self.area_referencia = 0.0 - - def _convert_bbox_to_z(self, bbox): - w = bbox[2] - bbox[0]; h = bbox[3] - bbox[1]; x = bbox[0] + w/2.; y = bbox[1] + h/2. - return np.array([[x],[y],[w*h],[w/float(h+1e-6)]]).astype(np.float32) - - def _convert_x_to_bbox(self, x): - cx, cy, s, r = float(x[0].item()), float(x[1].item()), float(x[2].item()), float(x[3].item()) - w = np.sqrt(s * r); h = s / (w + 1e-6) - return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] - - def predict(self, turno_activo=True): - if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: self.kf.statePost[6] *= 0.0 - self.kf.predict() - if turno_activo: self.time_since_update += 1 - self.aprendiendo = False - self.box = self._convert_x_to_bbox(self.kf.statePre) - return self.box - - def update(self, box, en_grupo, now): - self.ts_ultima_deteccion = now - self.time_since_update = 0 - self.box = list(box) - self.en_grupo = en_grupo - self.kf.correct(self._convert_bbox_to_z(box)) - - if analizar_calidad(box) and not en_grupo: - self.frames_buena_calidad += 1 - if self.frames_buena_calidad >= FRAMES_CALIDAD: - self.listo_para_id = True - elif self.gid is None: - self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1) - -# ────────────────────────────────────────────────────────────────────────────── -# 3. MEMORIA GLOBAL (Anti-Robos y Físicas de Tiempo) -# ────────────────────────────────────────────────────────────────────────────── -class GlobalMemory: - def __init__(self): - self.db = {} - self.next_gid = 100 - self.lock = threading.Lock() - - def _es_transito_posible(self, data, cam_destino, now): - ultima_cam = str(data['last_cam']) - cam_destino = str(cam_destino) - dt = now - data['ts'] - - if ultima_cam == cam_destino: return True - vecinos = VECINOS.get(ultima_cam, []) - # Permite teletransportación mínima (-0.5s) para que no te fragmente en los pasillos conectados - if cam_destino in vecinos: return dt >= -0.5 - return dt >= 4.0 - - def _sim_robusta(self, firma_nueva, firmas_guardadas, cross_cam=False): - if not firmas_guardadas: return 0.0 - sims = sorted([similitud_hibrida(firma_nueva, f, cross_cam) for f in firmas_guardadas], reverse=True) - if len(sims) == 1: return sims[0] - elif len(sims) <= 4: return (sims[0] * 0.6) + (sims[1] * 0.4) - else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20) - - # ⚡ SE AGREGÓ 'en_borde' A LOS PARÁMETROS - def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True): - with self.lock: - candidatos = [] - vecinos = VECINOS.get(str(cam_id), []) - - for gid, data in self.db.items(): - if gid in active_gids: continue - dt = now - data['ts'] - if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue - if not data['firmas']: continue - - misma_cam = (str(data['last_cam']) == str(cam_id)) - es_cross_cam = not misma_cam - es_vecino = str(data['last_cam']) in vecinos - - # ⚡ FÍSICA DE PUERTAS: Si "nació" en el centro de la pantalla, NO viene caminando del pasillo adyacente. - if es_vecino and not en_borde: - es_vecino = False - - sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam) - - if misma_cam: umbral = UMBRAL_REID_MISMA_CAM - elif es_vecino: umbral = UMBRAL_REID_VECINO - else: umbral = UMBRAL_REID_NO_VECINO - - # 🛡️ PROTECCIÓN VIP: Si este ID ya tiene un nombre real asignado por ArcFace, - # nos volvemos súper estrictos (+0.08) para que un desconocido no se lo robe. - if data.get('nombre') is not None: - umbral += 0.08 - - if sim > umbral: - candidatos.append((sim, gid)) - - if not candidatos: - nid = self.next_gid; self.next_gid += 1 - self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) - return nid, False - - candidatos.sort(reverse=True) - best_sim, best_gid = candidatos[0] - - if len(candidatos) >= 2: - segunda_sim, segundo_gid = candidatos[1] - margen = best_sim - segunda_sim - if margen <= 0.02 and best_sim < 0.75: - print(f"\n[⚠️ ALERTA ROPA SIMILAR] Empate técnico entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna ID temporal nuevo.") - nid = self.next_gid; self.next_gid += 1 - self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) - return nid, False - - self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now) - return best_gid, True - - def _actualizar_sin_lock(self, gid, firma_dict, cam_id, now): - if gid not in self.db: self.db[gid] = {'firmas': [], 'last_cam': cam_id, 'ts': now} - if firma_dict is not None: - firmas_list = self.db[gid]['firmas'] - if not firmas_list: - firmas_list.append(firma_dict) - else: - if firma_dict['calidad'] > (firmas_list[0]['calidad'] * 1.50): - vieja_ancla = firmas_list[0]; firmas_list[0] = firma_dict; firma_dict = vieja_ancla - if len(firmas_list) >= MAX_FIRMAS_MEMORIA: - max_sim_interna = -1.0; idx_redundante = 1 - for i in range(1, len(firmas_list)): - sims_con_otras = [similitud_hibrida(firmas_list[i], firmas_list[j]) for j in range(1, len(firmas_list)) if j != i] - sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0 - if sim_promedio > max_sim_interna: max_sim_interna = sim_promedio; idx_redundante = i - firmas_list[idx_redundante] = firma_dict - else: - firmas_list.append(firma_dict) - self.db[gid]['last_cam'] = cam_id - self.db[gid]['ts'] = now - - def actualizar(self, gid, firma, cam_id, now): - with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now) - -# ────────────────────────────────────────────────────────────────────────────── -# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer) -# ────────────────────────────────────────────────────────────────────────────── -def iou_overlap(boxA, boxB): - xA, yA, xB, yB = max(boxA[0], boxB[0]), max(boxA[1], boxB[1]), min(boxA[2], boxB[2]), min(boxA[3], boxB[3]) - inter = max(0, xB-xA) * max(0, yB-yA) - areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1]); areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1]) - return inter / (areaA + areaB - inter + 1e-6) - -class CamManager: - def __init__(self, cam_id, global_mem): - self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] - - def update(self, boxes, frame, now, turno_activo): - for trk in self.trackers: trk.predict(turno_activo=turno_activo) - if not turno_activo: return self.trackers - - matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now) - - for t_idx, d_idx in matched: - trk = self.trackers[t_idx]; box = boxes[d_idx] - en_grupo = any(other is not trk and iou_overlap(box, other.box) > 0.10 for other in self.trackers) - trk.update(box, en_grupo, now) - - active_gids = {t.gid for t in self.trackers if t.gid is not None} - area_actual = (box[2] - box[0]) * (box[3] - box[1]) - - # IGNORAMOS VECTORES MUTANTES DE GRUPOS - if trk.gid is None and trk.listo_para_id and not trk.en_grupo: - firma = extraer_firma_hibrida(frame, box) - if firma is not None: - # ⚡ DETECCIÓN DE ZONA DE NACIMIENTO - fh, fw = frame.shape[:2] - bx1, by1, bx2, by2 = map(int, box) - # Si nace a menos de 40 píxeles del margen, entró por el pasillo - nace_en_borde = (bx1 < 80 or by1 < 80 or bx2 > fw - 80 or by2 > fh - 80) - - # Mandamos esa información al identificador - gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde) - trk.gid, trk.origen_global, trk.area_referencia = gid, es_reid, area_actual - - elif trk.gid is not None and not trk.en_grupo: - tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) - - # ⚡ APRENDIZAJE RÁPIDO: Bajamos de 1.5s a 0.5s para que llene la memoria volando - if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box): - fh, fw = frame.shape[:2] - x1, y1, x2, y2 = map(int, box) - en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) - - if not en_borde: - firma_nueva = extraer_firma_hibrida(frame, box) - if firma_nueva is not None: - with self.global_mem.lock: - if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: - - # ⚡ APRENDIZAJE EN CADENA: Comparamos contra la ÚLTIMA foto (-1), no contra la primera. - # Esto permite que el sistema "entienda" cuando te estás dando la vuelta o mostrando la mochila. - firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1] - sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente) - - # Tolerancia relajada a 0.50 para permitir la transición de la espalda - if sim_coherencia > 0.50: - es_coherente = True - for otro_gid, otro_data in self.global_mem.db.items(): - if otro_gid == trk.gid or not otro_data['firmas']: continue - sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0]) - if sim_intruso > sim_coherencia: - es_coherente = False - break - if es_coherente: - self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now) - trk.ultimo_aprendizaje = now - trk.aprendiendo = True - - for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now)) - - vivos = [] - fh, fw = frame.shape[:2] - for t in self.trackers: - x1, y1, x2, y2 = t.box - toca_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) - tiempo_oculto = now - t.ts_ultima_deteccion - - # ⚡ MUERTE DE FANTASMAS: Si toca el borde muere en 1s. Evita robo de IDs. - limite_vida = 1.0 if toca_borde else 10.0 - if tiempo_oculto < limite_vida: - vivos.append(t) - - self.trackers = vivos - return self.trackers - - def _asignar(self, boxes, now): - n_trk = len(self.trackers); n_det = len(boxes) - if n_trk == 0: return [], list(range(n_det)), [] - if n_det == 0: return [], [], list(range(n_trk)) - - cost_mat = np.zeros((n_trk, n_det), dtype=np.float32) - TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035 - - for t, trk in enumerate(self.trackers): - for d, det in enumerate(boxes): - iou = iou_overlap(trk.box, det) - cx_t, cy_t = (trk.box[0]+trk.box[2])/2, (trk.box[1]+trk.box[3])/2 - cx_d, cy_d = (det[0]+det[2])/2, (det[1]+det[3])/2 - dist_norm = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) / 550.0 - - area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1]) - area_det = (det[2] - det[0]) * (det[3] - det[1]) - ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) - castigo_tam = (ratio_area - 1.0) * 0.7 - - tiempo_oculto = now - trk.ts_ultima_deteccion - if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10: - fantasma_penalty = 5.0 - else: fantasma_penalty = 0.0 - - if iou >= 0.05 or dist_norm < 0.80: - cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tam - else: cost_mat[t, d] = 100.0 - - row_ind, col_ind = linear_sum_assignment(cost_mat) - matched, unmatched_dets, unmatched_trks = [], [], [] - - for r, c in zip(row_ind, col_ind): - # ⚡ CAJAS PEGAJOSAS: 6.0 evita que suelte el ID si te mueves rápido - if cost_mat[r, c] > 7.0: - unmatched_trks.append(r); unmatched_dets.append(c) - else: matched.append((r, c)) - - for t in range(n_trk): - if t not in [m[0] for m in matched]: unmatched_trks.append(t) - for d in range(n_det): - if d not in [m[1] for m in matched]: unmatched_dets.append(d) - - return matched, unmatched_dets, unmatched_trks - -# ────────────────────────────────────────────────────────────────────────────── -# 5. STREAM Y MAIN LOOP (Standalone) -# ────────────────────────────────────────────────────────────────────────────── -class CamStream: - def __init__(self, url): - self.url, self.cap = url, cv2.VideoCapture(url) - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1); self.frame = None - threading.Thread(target=self._run, daemon=True).start() - - def _run(self): - while True: - ret, f = self.cap.read() - if ret: - self.frame = f; time.sleep(0.01) - else: - time.sleep(2); self.cap.open(self.url) - -def dibujar_track(frame_show, trk): - try: x1, y1, x2, y2 = map(int, trk.box) - except Exception: return - - if trk.gid is None: color, label = C_CANDIDATO, f"?{trk.local_id}" - elif trk.en_grupo: color, label = C_GRUPO, f"ID:{trk.gid} [grp]" - elif trk.aprendiendo: color, label = C_APRENDIZAJE, f"ID:{trk.gid} [++]" - elif trk.origen_global: color, label = C_GLOBAL, f"ID:{trk.gid} [re-id]" - else: color, label = C_LOCAL, f"ID:{trk.gid}" - - cv2.rectangle(frame_show, (x1, y1), (x2, y2), color, 2) - (tw, th), _ = cv2.getTextSize(label, FUENTE, 0.55, 1) - cv2.rectangle(frame_show, (x1, y1-th-6), (x1+tw+2, y1), color, -1) - cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1) - -def main(): - print("Iniciando Sistema V-PRO — Tracker Resiliente (Código Unificado Maestro)") - model = YOLO("yolov8n.pt") - global_mem = GlobalMemory() - managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} - cams = [CamStream(u) for u in URLS] - cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) - - idx = 0 - while True: - now = time.time() - tiles = [] - cam_ia = idx % len(cams) - for i, cam_obj in enumerate(cams): - frame = cam_obj.frame; cid = str(SECUENCIA[i]) - if frame is None: tiles.append(np.zeros((270, 480, 3), np.uint8)); continue - frame_show = cv2.resize(frame.copy(), (480, 270)); boxes = []; turno_activo = (i == cam_ia) - if turno_activo: - res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu') - if res[0].boxes: boxes = res[0].boxes.xyxy.cpu().numpy().tolist() - tracks = managers[cid].update(boxes, frame_show, now, turno_activo) - for trk in tracks: - if trk.time_since_update <= 1: dibujar_track(frame_show, trk) - if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) - con_id = sum(1 for t in tracks if t.gid and t.time_since_update==0) - cv2.putText(frame_show, f"CAM {cid} [{con_id} ID]", (10, 28), FUENTE, 0.7, (255, 255, 255), 2) - tiles.append(frame_show) - - if len(tiles) == 6: cv2.imshow("SmartSoft", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])])) - idx += 1 - if cv2.waitKey(1) == ord('q'): break - cv2.destroyAllWindows() - -if __name__ == "__main__": - main() - - - - - - - - - - -############################################################### fusion.py -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' -os.environ['CUDA_VISIBLE_DEVICES'] = '-1' -os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000" -import cv2 -import numpy as np -import time -import threading -from queue import Queue -from deepface import DeepFace -from ultralytics import YOLO -import warnings - -warnings.filterwarnings("ignore") - -# ────────────────────────────────────────────────────────────────────────────── -# 1. IMPORTAMOS NUESTROS MÓDULOS -# ────────────────────────────────────────────────────────────────────────────── -# Del motor matemático y tracking -from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE, similitud_hibrida - -# Del motor de reconocimiento facial y audio -from reconocimiento2 import ( - gestionar_vectores, - detectar_rostros_yunet, - buscar_mejor_match, - hilo_bienvenida, - UMBRAL_SIM, - COOLDOWN_TIME -) - -# ────────────────────────────────────────────────────────────────────────────── -# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN -# ────────────────────────────────────────────────────────────────────────────── -COLA_ROSTROS = Queue(maxsize=4) -YUNET_LOCK = threading.Lock() -IA_LOCK = threading.Lock() - -# Inicializamos la base de datos usando tu función importada -print("\nIniciando carga de base de datos...") -BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) - -# ────────────────────────────────────────────────────────────────────────────── -# 3. MOTOR ASÍNCRONO -# ────────────────────────────────────────────────────────────────────────────── -def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk): - """ Toma el recorte del tracker, escala a Alta Definición, usa YuNet y hace la Fusión Mágica """ - try: - if not BASE_DATOS_ROSTROS: return - - # ────────────────────────────────────────────────────────── - # 1. VALIDACIÓN DEL FRAME HD Y ESCALADO MATEMÁTICO - # ────────────────────────────────────────────────────────── - h_real, w_real = frame_hd.shape[:2] - - # ⚡ TRAMPA ANTI-BUGS: Si esto salta, corrige la llamada en tu main_fusion.py - if w_real <= 480: - print(f"[❌ ERROR CAM {cam_id}] Le estás pasando el frame_show (480x270) a ArcFace, no el HD.") - - escala_x = w_real / 480.0 - escala_y = h_real / 270.0 - - x_min, y_min, x_max, y_max = box_480 - h_box = y_max - y_min - - y_min_expandido = max(0, y_min - (h_box * 0.15)) - y_max_cabeza = min(270, y_min + (h_box * 0.40)) # Límite máximo en la escala de 270 - - x1_hd = int(max(0, x_min) * escala_x) - y1_hd = int(y_min_expandido * escala_y) - x2_hd = int(min(480, x_max) * escala_x) - y2_hd = int(y_max_cabeza * escala_y) - - roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd] - - # Si la cabeza HD mide menos de 60x60, está demasiado lejos incluso en HD - if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 60 or roi_cabeza.shape[1] < 60: - return - - h_roi, w_roi = roi_cabeza.shape[:2] - - # ────────────────────────────────────────────────────────── - # 2. DETECCIÓN DE ROSTRO CON YUNET (Ahora operando en HD) - # ────────────────────────────────────────────────────────── - faces = detectar_rostros_yunet(roi_cabeza, lock=YUNET_LOCK) - - for (rx, ry, rw, rh, score) in faces: - rx, ry = max(0, rx), max(0, ry) - rw, rh = min(w_roi - rx, rw), min(h_roi - ry, rh) - - area_rostro_actual = rw * rh - - with global_mem.lock: - data = global_mem.db.get(gid, {}) - nombre_actual = data.get('nombre') - area_ref = data.get('area_rostro_ref', 0) - - necesita_saludo = False - if str(cam_id) == "7": - if not hasattr(global_mem, 'ultimos_saludos'): - global_mem.ultimos_saludos = {} - ultimo = global_mem.ultimos_saludos.get(nombre_actual if nombre_actual else "", 0) - if (time.time() - ultimo) > COOLDOWN_TIME: - necesita_saludo = True - - if nombre_actual is None or area_rostro_actual >= (area_ref * 1.5) or necesita_saludo: - - # ⚡ MÁRGENES MÁS AMPLIOS: ArcFace necesita ver frente y barbilla (25%) - m_x = int(rw * 0.25) - m_y = int(rh * 0.25) - - roi_rostro = roi_cabeza[max(0, ry-m_y):min(h_roi, ry+rh+m_y), - max(0, rx-m_x):min(w_roi, rx+rw+m_x)] - - if roi_rostro.size == 0 or roi_rostro.shape[0] < 60 or roi_rostro.shape[1] < 60: - continue - - # ⚡ Laplaciano ajustado para imágenes HD (30.0 es más justo) - gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY) - nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() - if nitidez < 15.0: - continue - - # ────────────────────────────────────────────────────────── - # 3. RECONOCIMIENTO FACIAL ARCFACE - # ────────────────────────────────────────────────────────── - with IA_LOCK: - try: - # ⚡ CAMBIO DRÁSTICO: Usamos RetinaFace para alinear la cabeza obligatoriamente. - # Si RetinaFace no logra enderezar la cara (ej. estás totalmente de perfil), - # lanzará una excepción y abortará, evitando falsos positivos. - # Así DEBE estar en main_fusion.py para que sea compatible con tu nueva DB - res = DeepFace.represent( - img_path=roi_cabeza, - model_name="ArcFace", - detector_backend="retinaface", # Obligatorio - align=True, # Obligatorio - enforce_detection=True # Obligatorio - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) - except Exception: - # Si falla la alineación o estás muy borroso, lo ignoramos en silencio. - continue - - print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f} (Umbral: {UMBRAL_SIM})") - - if max_sim >= UMBRAL_SIM and mejor_match: - nombre_limpio = mejor_match.split('_')[0] - - with global_mem.lock: - global_mem.db[gid]['nombre'] = nombre_limpio - global_mem.db[gid]['area_rostro_ref'] = area_rostro_actual - global_mem.db[gid]['ts'] = time.time() - - ids_a_borrar = [] - firma_actual = global_mem.db[gid]['firmas'][0] if global_mem.db[gid]['firmas'] else None - - for otro_gid, datos_otro in list(global_mem.db.items()): - if otro_gid == gid: continue - - if datos_otro.get('nombre') == nombre_limpio: - ids_a_borrar.append(otro_gid) - - elif datos_otro.get('nombre') is None and firma_actual and datos_otro['firmas']: - sim_huerfano = similitud_hibrida(firma_actual, datos_otro['firmas'][0]) - if sim_huerfano > 0.75: - ids_a_borrar.append(otro_gid) - - for id_basura in ids_a_borrar: - del global_mem.db[id_basura] - print(f"[🧹 LIMPIEZA] ID huérfano/clon {id_basura} eliminado tras reconocer a {nombre_limpio}.") - - if str(cam_id) == "7" and necesita_saludo: - global_mem.ultimos_saludos[nombre_limpio] = time.time() - try: - with IA_LOCK: - analisis = DeepFace.analyze(roi_rostro, actions=['gender'], enforce_detection=False)[0] - genero = analisis.get('dominant_gender', 'Man') - except Exception: - genero = "Man" - - threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero), daemon=True).start() - break - except Exception as e: - pass - finally: - trk.procesando_rostro = False - -def worker_rostros(global_mem): - """ Consumidor de la cola multihilo """ - while True: - frame, box, gid, cam_id, trk = COLA_ROSTROS.get() - procesar_rostro_async(frame, box, gid, cam_id, global_mem, trk) - COLA_ROSTROS.task_done() - -# ────────────────────────────────────────────────────────────────────────────── -# 4. LOOP PRINCIPAL DE FUSIÓN -# ────────────────────────────────────────────────────────────────────────────── -class CamStream: - def __init__(self, url): - self.url = url - self.cap = cv2.VideoCapture(url) - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) - self.frame = None - threading.Thread(target=self._run, daemon=True).start() - - def _run(self): - while True: - ret, f = self.cap.read() - if ret: - self.frame = f - time.sleep(0.01) - else: - time.sleep(2) - self.cap.open(self.url) - -def dibujar_track_fusion(frame_show, trk, global_mem): - try: x1, y1, x2, y2 = map(int, trk.box) - except Exception: return - - nombre_str = "" - if trk.gid is not None: - with global_mem.lock: - nombre = global_mem.db.get(trk.gid, {}).get('nombre') - if nombre: nombre_str = f" [{nombre}]" - - if trk.gid is None: color, label = (150, 150, 150), f"?{trk.local_id}" - elif nombre_str: color, label = (255, 0, 255), f"ID:{trk.gid}{nombre_str}" - elif trk.en_grupo: color, label = (0, 0, 255), f"ID:{trk.gid} [grp]" - elif trk.aprendiendo: color, label = (255, 255, 0), f"ID:{trk.gid} [++]" - elif trk.origen_global: color, label = (0, 165, 255), f"ID:{trk.gid} [re-id]" - else: color, label = (0, 255, 0), f"ID:{trk.gid}" - - cv2.rectangle(frame_show, (x1, y1), (x2, y2), color, 2) - (tw, th), _ = cv2.getTextSize(label, FUENTE, 0.55, 1) - cv2.rectangle(frame_show, (x1, y1-th-6), (x1+tw+2, y1), color, -1) - cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1) - -def main(): - print("\nIniciando Sistema") - model = YOLO("yolov8n.pt") - global_mem = GlobalMemory() - managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} - cams = [CamStream(u) for u in URLS] - - for _ in range(2): - threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start() - - cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) - idx = 0 - - while True: - now = time.time() - tiles = [] - cam_ia = idx % len(cams) - - for i, cam_obj in enumerate(cams): - frame = cam_obj.frame; cid = str(SECUENCIA[i]) - if frame is None: - tiles.append(np.zeros((270, 480, 3), np.uint8)) - continue - - frame_show = cv2.resize(frame.copy(), (480, 270)) - boxes = [] - turno_activo = (i == cam_ia) - - if turno_activo: - res = model.predict(frame_show, conf=0.50, iou=0.50, classes=[0], verbose=False, imgsz=480) - if res[0].boxes: - boxes = res[0].boxes.xyxy.cpu().numpy().tolist() - - tracks = managers[cid].update(boxes, frame_show, now, turno_activo) - - for trk in tracks: - if trk.time_since_update <= 1: - dibujar_track_fusion(frame_show, trk, global_mem) - - if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False): - if not COLA_ROSTROS.full(): - trk.procesando_rostro = True - COLA_ROSTROS.put((frame.copy(), trk.box, trk.gid, cid, trk)) - - if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) - - con_id = sum(1 for t in tracks if t.gid and t.time_since_update==0) - cv2.putText(frame_show, f"CAM {cid} [{con_id} ID]", (10, 28), FUENTE, 0.7, (255, 255, 255), 2) - tiles.append(frame_show) - - if len(tiles) == 6: - cv2.imshow("SmartSoft Fusion", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])])) - - idx += 1 - if cv2.waitKey(1) == ord('q'): - break - - cv2.destroyAllWindows() - -if __name__ == "__main__": - main() - - - - - - - - - - - - - - - - - - - -################################################################### reconocimeito2.py - -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' -os.environ['CUDA_VISIBLE_DEVICES'] = '-1' - -import cv2 -import numpy as np -from deepface import DeepFace -import pickle -import time -import threading -import asyncio -import edge_tts -import subprocess -from datetime import datetime -import warnings -import urllib.request - -warnings.filterwarnings("ignore") - -# ────────────────────────────────────────────────────────────────────────────── -# CONFIGURACIÓN -# ────────────────────────────────────────────────────────────────────────────── -DB_PATH = "db_institucion" -CACHE_PATH = "cache_nombres" -VECTORS_FILE = "base_datos_rostros.pkl" -TIMESTAMPS_FILE = "representaciones_timestamps.pkl" -UMBRAL_SIM = 0.45 # Por encima → identificado. Por debajo → desconocido. -COOLDOWN_TIME = 15 # Segundos entre saludos - -USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" -RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" - -for path in [DB_PATH, CACHE_PATH]: - os.makedirs(path, exist_ok=True) - -# ────────────────────────────────────────────────────────────────────────────── -# YUNET — Detector facial rápido en CPU -# ────────────────────────────────────────────────────────────────────────────── -YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" - -if not os.path.exists(YUNET_MODEL_PATH): - print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") - 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) - print("YuNet descargado.") - -# Detector estricto para ROIs grandes (persona cerca) -detector_yunet = cv2.FaceDetectorYN.create( - model=YUNET_MODEL_PATH, config="", - input_size=(320, 320), - score_threshold=0.70, - nms_threshold=0.3, - top_k=5000 -) - -# Detector permisivo para ROIs pequeños (persona lejos) -detector_yunet_lejano = cv2.FaceDetectorYN.create( - model=YUNET_MODEL_PATH, config="", - input_size=(320, 320), - score_threshold=0.45, - nms_threshold=0.3, - top_k=5000 -) - -def detectar_rostros_yunet(roi, lock=None): - """ - Elige automáticamente el detector según el tamaño del ROI. - """ - h_roi, w_roi = roi.shape[:2] - area = w_roi * h_roi - det = detector_yunet if area > 8000 else detector_yunet_lejano - - try: - if lock: - with lock: - det.setInputSize((w_roi, h_roi)) - _, faces = det.detect(roi) - else: - det.setInputSize((w_roi, h_roi)) - _, faces = det.detect(roi) - except Exception: - return [] - - if faces is None: - return [] - - resultado = [] - for face in faces: - try: - fx, fy, fw, fh = map(int, face[:4]) - score = float(face[14]) if len(face) > 14 else 1.0 - resultado.append((fx, fy, fw, fh, score)) - except (ValueError, OverflowError, TypeError): - continue - return resultado - - -# ────────────────────────────────────────────────────────────────────────────── -# SISTEMA DE AUDIO -# ────────────────────────────────────────────────────────────────────────────── -def obtener_audios_humanos(genero): - hora = datetime.now().hour - es_mujer = genero.lower() == 'woman' - suffix = "_m.mp3" if es_mujer else "_h.mp3" - if 5 <= hora < 12: - intro = "dias.mp3" - elif 12 <= hora < 19: - intro = "tarde.mp3" - else: - intro = "noches.mp3" - cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix - return intro, cierre - - -async def sintetizar_nombre(nombre, ruta): - nombre_limpio = nombre.replace('_', ' ') - try: - comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") - await comunicador.save(ruta) - except Exception: - pass - - -def reproducir(archivo): - if os.path.exists(archivo): - subprocess.Popen( - ["mpv", "--no-video", "--volume=100", archivo], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - -def hilo_bienvenida(nombre, genero): - archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") - - if not os.path.exists(archivo_nombre): - try: - asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) - except Exception: - pass - - intro, cierre = obtener_audios_humanos(genero) - - archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] - if archivos: - subprocess.Popen( - ["mpv", "--no-video", "--volume=100"] + archivos, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - -# ────────────────────────────────────────────────────────────────────────────── -# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN) -# ────────────────────────────────────────────────────────────────────────────── -def gestionar_vectores(actualizar=False): - vectores_actuales = {} - - if os.path.exists(VECTORS_FILE): - try: - with open(VECTORS_FILE, 'rb') as f: - vectores_actuales = pickle.load(f) - except Exception: - vectores_actuales = {} - - if not actualizar: - return vectores_actuales - - timestamps = {} - if os.path.exists(TIMESTAMPS_FILE): - try: - with open(TIMESTAMPS_FILE, 'rb') as f: - timestamps = pickle.load(f) - except Exception: - timestamps = {} - - print("\nACTUALIZANDO BASE DE DATOS (Alineación con RetinaFace)...") - imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] - nombres_en_disco = set() - hubo_cambios = False - - for archivo in imagenes: - nombre_archivo = os.path.splitext(archivo)[0] - ruta_img = os.path.join(DB_PATH, archivo) - nombres_en_disco.add(nombre_archivo) - - ts_actual = os.path.getmtime(ruta_img) - ts_guardado = timestamps.get(nombre_archivo, 0) - - if nombre_archivo in vectores_actuales and ts_actual == ts_guardado: - continue - - try: - # ⚡ MAGIA 1: RetinaFace alinea matemáticamente los rostros de la base de datos - res = DeepFace.represent( - img_path=ruta_img, - model_name="ArcFace", - detector_backend="retinaface", # Localiza ojos/nariz - align=True, # Rota la imagen para alinear - enforce_detection=True # Obliga a que haya cara válida - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - - # ⚡ MAGIA 2: Normalización L2 al guardar (Elimina el "Efecto Rosa María") - norma = np.linalg.norm(emb) - if norma > 0: - emb = emb / norma - - vectores_actuales[nombre_archivo] = emb - timestamps[nombre_archivo] = ts_actual - hubo_cambios = True - print(f" ✅ Procesado y alineado: {nombre_archivo}") - - except Exception as e: - print(f" ❌ Rostro no válido en '{archivo}', omitido. Error: {e}") - - for nombre in list(vectores_actuales.keys()): - if nombre not in nombres_en_disco: - del vectores_actuales[nombre] - timestamps.pop(nombre, None) - hubo_cambios = True - print(f" 🗑️ Eliminado (sin foto): {nombre}") - - if hubo_cambios: - with open(VECTORS_FILE, 'wb') as f: - pickle.dump(vectores_actuales, f) - with open(TIMESTAMPS_FILE, 'wb') as f: - pickle.dump(timestamps, f) - print(" Sincronización terminada.\n") - else: - print(" Sin cambios. Base de datos al día.\n") - - return vectores_actuales - -# ────────────────────────────────────────────────────────────────────────────── -# BÚSQUEDA BLINDADA (Similitud Coseno estricta) -# ────────────────────────────────────────────────────────────────────────────── -def buscar_mejor_match(emb_consulta, base_datos): - # ⚡ MAGIA 3: Normalización L2 del vector entrante - norma = np.linalg.norm(emb_consulta) - if norma > 0: - emb_consulta = emb_consulta / norma - - 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) - sim = float(np.dot(emb_consulta, vec)) - if sim > max_sim: - max_sim = sim - mejor_match = nombre - - return mejor_match, max_sim - -# ────────────────────────────────────────────────────────────────────────────── -# LOOP DE PRUEBA Y REGISTRO -# ────────────────────────────────────────────────────────────────────────────── -def sistema_interactivo(): - base_datos = gestionar_vectores(actualizar=False) - cap = cv2.VideoCapture(RTSP_URL) - ultimo_saludo = 0 - persona_actual = None - confirmaciones = 0 - - print("\n" + "=" * 50) - print(" MÓDULO DE REGISTRO Y DEPURACIÓN") - print(" [R] Registrar nuevo rostro | [Q] Salir") - print("=" * 50 + "\n") - - faces_ultimo_frame = [] - - while True: - ret, frame = cap.read() - if not ret: - time.sleep(2) - cap.open(RTSP_URL) - continue - - h, w = frame.shape[:2] - display_frame = frame.copy() - tiempo_actual = time.time() - - faces_raw = detectar_rostros_yunet(frame) - faces_ultimo_frame = faces_raw - - for (fx, fy, fw, fh, score_yunet) in faces_raw: - fx = max(0, fx); fy = max(0, fy) - fw = min(w - fx, fw); fh = min(h - fy, fh) - if fw <= 0 or fh <= 0: - continue - - cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) - cv2.putText(display_frame, f"YN:{score_yunet:.2f}", - (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) - - if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: - continue - - m = int(fw * 0.15) - roi = frame[max(0, fy-m): min(h, fy+fh+m), - max(0, fx-m): min(w, fx+fw+m)] - - if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: - cv2.putText(display_frame, "muy pequeño", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) - continue - - gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) - nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() - if nitidez < 50.0: - cv2.putText(display_frame, f"blur({nitidez:.0f})", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) - continue - - try: - # ⚡ En el modo de prueba interactivo usamos las reglas viejas - # para que sea rápido y puedas registrar fotos fácilmente. - res = DeepFace.represent( - img_path=roi, model_name="ArcFace", enforce_detection=False - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - mejor_match, max_sim = buscar_mejor_match(emb, base_datos) - - except Exception as e: - print(f"[ERROR ArcFace]: {e}") - continue - - estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" - nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" - n_bloques = int(max_sim * 20) - barra = "█" * n_bloques + "░" * (20 - n_bloques) - print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " - f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") - - if max_sim > UMBRAL_SIM and mejor_match: - color = (0, 255, 0) - texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" - - if mejor_match == persona_actual: - confirmaciones += 1 - else: - persona_actual, confirmaciones = mejor_match, 1 - - if confirmaciones >= 2: - cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) - try: - analisis = DeepFace.analyze( - roi, actions=['gender'], enforce_detection=False - )[0] - genero = analisis['dominant_gender'] - except Exception: - genero = "Man" - - threading.Thread( - target=hilo_bienvenida, - args=(mejor_match, genero), - daemon=True - ).start() - ultimo_saludo = tiempo_actual - confirmaciones = 0 - - else: - color = (0, 0, 255) - texto = f"? ({max_sim:.2f})" - confirmaciones = max(0, confirmaciones - 1) - - cv2.putText(display_frame, texto, - (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) - - cv2.imshow("Módulo de Registro", display_frame) - key = cv2.waitKey(1) & 0xFF - - if key == ord('q'): - break - - elif key == ord('r'): - if faces_ultimo_frame: - areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] - fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] - - # Le damos más margen al registro (30%) para que RetinaFace no falle - # cuando procese la foto en la carpeta. - m_x = int(fw * 0.30) - m_y = int(fh * 0.30) - 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)] - - if face_roi.size > 0: - nom = input("\nNombre de la persona: ").strip() - if nom: - foto_path = os.path.join(DB_PATH, f"{nom}.jpg") - cv2.imwrite(foto_path, face_roi) - print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") - # ⚡ Al sincronizar, RetinaFace alineará esta foto guardada. - base_datos = gestionar_vectores(actualizar=True) - else: - print("[!] Registro cancelado.") - else: - 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() +############################################################ seguimiento2.py +import cv2 +import numpy as np +import time +import threading +from scipy.optimize import linear_sum_assignment +from scipy.spatial.distance import cosine +from ultralytics import YOLO +import onnxruntime as ort +import os + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN DEL SISTEMA +# ────────────────────────────────────────────────────────────────────────────── +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" +SECUENCIA = [1, 7, 5, 8, 3, 6] +# 🛡️ RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg) +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000" +URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA] +ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx" + +VECINOS = { + "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], + "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] +} + +ASPECT_RATIO_MIN = 0.5 +ASPECT_RATIO_MAX = 4.0 +AREA_MIN_CALIDAD = 1200 +FRAMES_CALIDAD = 2 +TIEMPO_MAX_AUSENCIA = 800.0 + +# ⚡ UMBRALES MAESTROS: Tolerancia altísima entre cámaras vecinas para ignorar cambios de luz +UMBRAL_REID_MISMA_CAM = 0.62 +UMBRAL_REID_VECINO = 0.53 +UMBRAL_REID_NO_VECINO = 0.72 +MAX_FIRMAS_MEMORIA = 15 + +C_CANDIDATO = (150, 150, 150) +C_LOCAL = (0, 255, 0) +C_GLOBAL = (0, 165, 255) +C_GRUPO = (0, 0, 255) +C_APRENDIZAJE = (255, 255, 0) +FUENTE = cv2.FONT_HERSHEY_SIMPLEX + +# ────────────────────────────────────────────────────────────────────────────── +# INICIALIZACIÓN OSNET +# ────────────────────────────────────────────────────────────────────────────── +print("Cargando cerebro de Re-Identificación (OSNet)...") +try: + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + input_name = ort_session.get_inputs()[0].name + print("Modelo OSNet cargado exitosamente.") +except Exception as e: + print(f"ERROR FATAL: No se pudo cargar {ONNX_MODEL_PATH}.") + exit() + +MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 3, 1, 1) +STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1) + +# ────────────────────────────────────────────────────────────────────────────── +# 1. EXTRACCIÓN DE FIRMAS (Deep + Color + Textura) +# ────────────────────────────────────────────────────────────────────────────── +def analizar_calidad(box): + x1, y1, x2, y2 = box + w, h = x2 - x1, y2 - y1 + if w <= 0 or h <= 0: return False + return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD) + +def preprocess_onnx(roi): + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + img = np.expand_dims(img, axis=0) + img = (img - MEAN) / STD + return img + +def extraer_color_zonas(img): + h_roi = img.shape[0] + t1, t2 = int(h_roi * 0.15), int(h_roi * 0.55) + zonas = [img[:t1, :], img[t1:t2, :], img[t2:, :]] + + def hist_zona(z): + if z.size == 0: return np.zeros(16 * 8) + hsv = cv2.cvtColor(z, cv2.COLOR_BGR2HSV) + hist = cv2.calcHist([hsv], [0, 1], None, [16, 8], [0, 180, 0, 256]) + cv2.normalize(hist, hist) + return hist.flatten() + return np.concatenate([hist_zona(z) for z in zonas]) + +def extraer_textura_rapida(roi): + if roi.size == 0: return np.zeros(16) + gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) + gray_eq = cv2.equalizeHist(gray) + gx = cv2.Sobel(gray_eq, cv2.CV_32F, 1, 0, ksize=3) + gy = cv2.Sobel(gray_eq, cv2.CV_32F, 0, 1, ksize=3) + mag, _ = cv2.cartToPolar(gx, gy) + hist = cv2.calcHist([mag], [0], None, [16], [0, 256]) + cv2.normalize(hist, hist) + return hist.flatten() + +def extraer_firma_hibrida(frame, box): + try: + x1, y1, x2, y2 = map(int, box) + fh, fw = frame.shape[:2] + x1_c, y1_c = max(0, x1), max(0, y1) + x2_c, y2_c = min(fw, x2), min(fh, y2) + + roi = frame[y1_c:y2_c, x1_c:x2_c] + if roi.size == 0 or roi.shape[0] < 20 or roi.shape[1] < 10: return None + + calidad_area = (x2_c - x1_c) * (y2_c - y1_c) + + blob = preprocess_onnx(roi) + blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32) + blob_16[0] = blob[0] + deep_feat = ort_session.run(None, {input_name: blob_16})[0][0].flatten() + norma = np.linalg.norm(deep_feat) + if norma > 0: deep_feat = deep_feat / norma + + color_feat = extraer_color_zonas(roi) + textura_feat = extraer_textura_rapida(roi) + + return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area} + except Exception: + return None + +# ⚡ EL SECRETO: 100% IA entre cámaras. Textura solo en la misma cámara. +def similitud_hibrida(f1, f2, cross_cam=False): + if f1 is None or f2 is None: return 0.0 + + sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep'])) + + if cross_cam: + # Si saltó de cámara, la luz cambia. Ignoramos color y textura. Confiamos 100% en OSNet. + return sim_deep + + # Si está en la misma cámara, usamos color y textura para separar a los vestidos de negro. + if f1['color'].shape == f2['color'].shape and f1['color'].size > 1: + L = len(f1['color']) // 3 + sim_head = max(0.0, float(cv2.compareHist(f1['color'][:L].astype(np.float32), f2['color'][:L].astype(np.float32), cv2.HISTCMP_CORREL))) + sim_torso = max(0.0, float(cv2.compareHist(f1['color'][L:2*L].astype(np.float32), f2['color'][L:2*L].astype(np.float32), cv2.HISTCMP_CORREL))) + sim_legs = max(0.0, float(cv2.compareHist(f1['color'][2*L:].astype(np.float32), f2['color'][2*L:].astype(np.float32), cv2.HISTCMP_CORREL))) + sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs) + else: sim_color = 0.0 + + if 'textura' in f1 and 'textura' in f2 and f1['textura'].size > 1: + sim_textura = max(0.0, float(cv2.compareHist(f1['textura'].astype(np.float32), f2['textura'].astype(np.float32), cv2.HISTCMP_CORREL))) + else: sim_textura = 0.0 + + return (sim_deep * 0.80) + (sim_color * 0.10) + (sim_textura * 0.10) + +# ────────────────────────────────────────────────────────────────────────────── +# 2. KALMAN TRACKER +# ────────────────────────────────────────────────────────────────────────────── +class KalmanTrack: + _count = 0 + def __init__(self, box, now): + self.kf = cv2.KalmanFilter(7, 4) + self.kf.measurementMatrix = np.array([[1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0]], np.float32) + self.kf.transitionMatrix = np.eye(7, dtype=np.float32) + self.kf.transitionMatrix[0,4] = 1; self.kf.transitionMatrix[1,5] = 1; self.kf.transitionMatrix[2,6] = 1 + self.kf.processNoiseCov *= 0.03 + self.kf.statePost = np.zeros((7, 1), np.float32) + self.kf.statePost[:4] = self._convert_bbox_to_z(box) + self.local_id = KalmanTrack._count + KalmanTrack._count += 1 + self.gid = None + self.origen_global = False + self.aprendiendo = False + self.box = list(box) + self.ts_creacion = now + self.ts_ultima_deteccion = now + self.time_since_update = 0 + self.en_grupo = False + self.frames_buena_calidad = 0 + self.listo_para_id = False + self.area_referencia = 0.0 + + def _convert_bbox_to_z(self, bbox): + w = bbox[2] - bbox[0]; h = bbox[3] - bbox[1]; x = bbox[0] + w/2.; y = bbox[1] + h/2. + return np.array([[x],[y],[w*h],[w/float(h+1e-6)]]).astype(np.float32) + + def _convert_x_to_bbox(self, x): + cx, cy, s, r = float(x[0].item()), float(x[1].item()), float(x[2].item()), float(x[3].item()) + w = np.sqrt(s * r); h = s / (w + 1e-6) + return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] + + def predict(self, turno_activo=True): + if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: self.kf.statePost[6] *= 0.0 + self.kf.predict() + if turno_activo: self.time_since_update += 1 + self.aprendiendo = False + self.box = self._convert_x_to_bbox(self.kf.statePre) + return self.box + + def update(self, box, en_grupo, now): + self.ts_ultima_deteccion = now + self.time_since_update = 0 + self.box = list(box) + self.en_grupo = en_grupo + self.kf.correct(self._convert_bbox_to_z(box)) + + if analizar_calidad(box) and not en_grupo: + self.frames_buena_calidad += 1 + if self.frames_buena_calidad >= FRAMES_CALIDAD: + self.listo_para_id = True + elif self.gid is None: + self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1) + +# ────────────────────────────────────────────────────────────────────────────── +# 3. MEMORIA GLOBAL (Anti-Robos y Físicas de Tiempo) +# ────────────────────────────────────────────────────────────────────────────── +class GlobalMemory: + def __init__(self): + self.db = {} + self.next_gid = 100 + self.lock = threading.Lock() + + def _es_transito_posible(self, data, cam_destino, now): + ultima_cam = str(data['last_cam']) + cam_destino = str(cam_destino) + dt = now - data['ts'] + + if ultima_cam == cam_destino: return True + vecinos = VECINOS.get(ultima_cam, []) + # Permite teletransportación mínima (-0.5s) para que no te fragmente en los pasillos conectados + if cam_destino in vecinos: return dt >= -0.5 + return dt >= 4.0 + + def _sim_robusta(self, firma_nueva, firmas_guardadas, cross_cam=False): + if not firmas_guardadas: return 0.0 + sims = sorted([similitud_hibrida(firma_nueva, f, cross_cam) for f in firmas_guardadas], reverse=True) + if len(sims) == 1: return sims[0] + elif len(sims) <= 4: return (sims[0] * 0.6) + (sims[1] * 0.4) + else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20) + + # ⚡ SE AGREGÓ 'en_borde' A LOS PARÁMETROS + def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True): + with self.lock: + candidatos = [] + vecinos = VECINOS.get(str(cam_id), []) + + for gid, data in self.db.items(): + if gid in active_gids: continue + dt = now - data['ts'] + if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue + if not data['firmas']: continue + + misma_cam = (str(data['last_cam']) == str(cam_id)) + es_cross_cam = not misma_cam + es_vecino = str(data['last_cam']) in vecinos + + # ⚡ FÍSICA DE PUERTAS: Si "nació" en el centro de la pantalla, NO viene caminando del pasillo adyacente. + if es_vecino and not en_borde: + es_vecino = False + + sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam) + + if misma_cam: umbral = UMBRAL_REID_MISMA_CAM + elif es_vecino: umbral = UMBRAL_REID_VECINO + else: umbral = UMBRAL_REID_NO_VECINO + + # 🛡️ PROTECCIÓN VIP: Si este ID ya tiene un nombre real asignado por ArcFace, + # nos volvemos súper estrictos (+0.08) para que un desconocido no se lo robe. + if data.get('nombre') is not None: + umbral += 0.08 + + if sim > umbral: + candidatos.append((sim, gid)) + + if not candidatos: + nid = self.next_gid; self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) + return nid, False + + candidatos.sort(reverse=True) + best_sim, best_gid = candidatos[0] + + if len(candidatos) >= 2: + segunda_sim, segundo_gid = candidatos[1] + margen = best_sim - segunda_sim + if margen <= 0.02 and best_sim < 0.75: + print(f"\n[⚠️ ALERTA ROPA SIMILAR] Empate técnico entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna ID temporal nuevo.") + nid = self.next_gid; self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) + return nid, False + + self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now) + return best_gid, True + + def _actualizar_sin_lock(self, gid, firma_dict, cam_id, now): + if gid not in self.db: self.db[gid] = {'firmas': [], 'last_cam': cam_id, 'ts': now} + if firma_dict is not None: + firmas_list = self.db[gid]['firmas'] + if not firmas_list: + firmas_list.append(firma_dict) + else: + if firma_dict['calidad'] > (firmas_list[0]['calidad'] * 1.50): + vieja_ancla = firmas_list[0]; firmas_list[0] = firma_dict; firma_dict = vieja_ancla + if len(firmas_list) >= MAX_FIRMAS_MEMORIA: + max_sim_interna = -1.0; idx_redundante = 1 + for i in range(1, len(firmas_list)): + sims_con_otras = [similitud_hibrida(firmas_list[i], firmas_list[j]) for j in range(1, len(firmas_list)) if j != i] + sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0 + if sim_promedio > max_sim_interna: max_sim_interna = sim_promedio; idx_redundante = i + firmas_list[idx_redundante] = firma_dict + else: + firmas_list.append(firma_dict) + self.db[gid]['last_cam'] = cam_id + self.db[gid]['ts'] = now + + def actualizar(self, gid, firma, cam_id, now): + with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now) + +# ────────────────────────────────────────────────────────────────────────────── +# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer) +# ────────────────────────────────────────────────────────────────────────────── +def iou_overlap(boxA, boxB): + xA, yA, xB, yB = max(boxA[0], boxB[0]), max(boxA[1], boxB[1]), min(boxA[2], boxB[2]), min(boxA[3], boxB[3]) + inter = max(0, xB-xA) * max(0, yB-yA) + areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1]); areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1]) + return inter / (areaA + areaB - inter + 1e-6) + +class CamManager: + def __init__(self, cam_id, global_mem): + self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] + + def update(self, boxes, frame, now, turno_activo): + for trk in self.trackers: trk.predict(turno_activo=turno_activo) + if not turno_activo: return self.trackers + + matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now) + + for t_idx, d_idx in matched: + trk = self.trackers[t_idx]; box = boxes[d_idx] + en_grupo = any(other is not trk and iou_overlap(box, other.box) > 0.10 for other in self.trackers) + trk.update(box, en_grupo, now) + + active_gids = {t.gid for t in self.trackers if t.gid is not None} + area_actual = (box[2] - box[0]) * (box[3] - box[1]) + + # IGNORAMOS VECTORES MUTANTES DE GRUPOS + if trk.gid is None and trk.listo_para_id and not trk.en_grupo: + firma = extraer_firma_hibrida(frame, box) + if firma is not None: + # ⚡ DETECCIÓN DE ZONA DE NACIMIENTO + fh, fw = frame.shape[:2] + bx1, by1, bx2, by2 = map(int, box) + # Si nace a menos de 40 píxeles del margen, entró por el pasillo + nace_en_borde = (bx1 < 80 or by1 < 80 or bx2 > fw - 80 or by2 > fh - 80) + + # Mandamos esa información al identificador + gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde) + trk.gid, trk.origen_global, trk.area_referencia = gid, es_reid, area_actual + + elif trk.gid is not None and not trk.en_grupo: + tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) + + # ⚡ APRENDIZAJE RÁPIDO: Bajamos de 1.5s a 0.5s para que llene la memoria volando + if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box): + fh, fw = frame.shape[:2] + x1, y1, x2, y2 = map(int, box) + en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) + + if not en_borde: + firma_nueva = extraer_firma_hibrida(frame, box) + if firma_nueva is not None: + with self.global_mem.lock: + if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: + + # ⚡ APRENDIZAJE EN CADENA: Comparamos contra la ÚLTIMA foto (-1), no contra la primera. + # Esto permite que el sistema "entienda" cuando te estás dando la vuelta o mostrando la mochila. + firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1] + sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente) + + # Tolerancia relajada a 0.50 para permitir la transición de la espalda + if sim_coherencia > 0.50: + es_coherente = True + for otro_gid, otro_data in self.global_mem.db.items(): + if otro_gid == trk.gid or not otro_data['firmas']: continue + sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0]) + if sim_intruso > sim_coherencia: + es_coherente = False + break + if es_coherente: + self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now) + trk.ultimo_aprendizaje = now + trk.aprendiendo = True + + for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now)) + + vivos = [] + fh, fw = frame.shape[:2] + for t in self.trackers: + x1, y1, x2, y2 = t.box + toca_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) + tiempo_oculto = now - t.ts_ultima_deteccion + + # ⚡ MUERTE DE FANTASMAS: Si toca el borde muere en 1s. Evita robo de IDs. + limite_vida = 1.0 if toca_borde else 10.0 + if tiempo_oculto < limite_vida: + vivos.append(t) + + self.trackers = vivos + return self.trackers + + def _asignar(self, boxes, now): + n_trk = len(self.trackers); n_det = len(boxes) + if n_trk == 0: return [], list(range(n_det)), [] + if n_det == 0: return [], [], list(range(n_trk)) + + cost_mat = np.zeros((n_trk, n_det), dtype=np.float32) + TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035 + + for t, trk in enumerate(self.trackers): + for d, det in enumerate(boxes): + iou = iou_overlap(trk.box, det) + cx_t, cy_t = (trk.box[0]+trk.box[2])/2, (trk.box[1]+trk.box[3])/2 + cx_d, cy_d = (det[0]+det[2])/2, (det[1]+det[3])/2 + dist_norm = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) / 550.0 + + area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1]) + area_det = (det[2] - det[0]) * (det[3] - det[1]) + ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) + castigo_tam = (ratio_area - 1.0) * 0.7 + + tiempo_oculto = now - trk.ts_ultima_deteccion + if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10: + fantasma_penalty = 5.0 + else: fantasma_penalty = 0.0 + + if iou >= 0.05 or dist_norm < 0.80: + cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tam + else: cost_mat[t, d] = 100.0 + + row_ind, col_ind = linear_sum_assignment(cost_mat) + matched, unmatched_dets, unmatched_trks = [], [], [] + + for r, c in zip(row_ind, col_ind): + # ⚡ CAJAS PEGAJOSAS: 6.0 evita que suelte el ID si te mueves rápido + if cost_mat[r, c] > 7.0: + unmatched_trks.append(r); unmatched_dets.append(c) + else: matched.append((r, c)) + + for t in range(n_trk): + if t not in [m[0] for m in matched]: unmatched_trks.append(t) + for d in range(n_det): + if d not in [m[1] for m in matched]: unmatched_dets.append(d) + + return matched, unmatched_dets, unmatched_trks + +# ────────────────────────────────────────────────────────────────────────────── +# 5. STREAM Y MAIN LOOP (Standalone) +# ────────────────────────────────────────────────────────────────────────────── +class CamStream: + def __init__(self, url): + self.url, self.cap = url, cv2.VideoCapture(url) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1); self.frame = None + threading.Thread(target=self._run, daemon=True).start() + + def _run(self): + while True: + ret, f = self.cap.read() + if ret: + self.frame = f; time.sleep(0.01) + else: + time.sleep(2); self.cap.open(self.url) + +def dibujar_track(frame_show, trk): + try: x1, y1, x2, y2 = map(int, trk.box) + except Exception: return + + if trk.gid is None: color, label = C_CANDIDATO, f"?{trk.local_id}" + elif trk.en_grupo: color, label = C_GRUPO, f"ID:{trk.gid} [grp]" + elif trk.aprendiendo: color, label = C_APRENDIZAJE, f"ID:{trk.gid} [++]" + elif trk.origen_global: color, label = C_GLOBAL, f"ID:{trk.gid} [re-id]" + else: color, label = C_LOCAL, f"ID:{trk.gid}" + + cv2.rectangle(frame_show, (x1, y1), (x2, y2), color, 2) + (tw, th), _ = cv2.getTextSize(label, FUENTE, 0.55, 1) + cv2.rectangle(frame_show, (x1, y1-th-6), (x1+tw+2, y1), color, -1) + cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1) + +def main(): + print("Iniciando Sistema V-PRO — Tracker Resiliente (Código Unificado Maestro)") + model = YOLO("yolov8n.pt") + global_mem = GlobalMemory() + managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} + cams = [CamStream(u) for u in URLS] + cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) + + idx = 0 + while True: + now = time.time() + tiles = [] + cam_ia = idx % len(cams) + for i, cam_obj in enumerate(cams): + frame = cam_obj.frame; cid = str(SECUENCIA[i]) + if frame is None: tiles.append(np.zeros((270, 480, 3), np.uint8)); continue + frame_show = cv2.resize(frame.copy(), (480, 270)); boxes = []; turno_activo = (i == cam_ia) + if turno_activo: + res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu') + if res[0].boxes: boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + tracks = managers[cid].update(boxes, frame_show, now, turno_activo) + for trk in tracks: + if trk.time_since_update <= 1: dibujar_track(frame_show, trk) + if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) + con_id = sum(1 for t in tracks if t.gid and t.time_since_update==0) + cv2.putText(frame_show, f"CAM {cid} [{con_id} ID]", (10, 28), FUENTE, 0.7, (255, 255, 255), 2) + tiles.append(frame_show) + + if len(tiles) == 6: cv2.imshow("SmartSoft", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])])) + idx += 1 + if cv2.waitKey(1) == ord('q'): break + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() + + + + + + + + + + +############################################################### fusion.py +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +os.environ['CUDA_VISIBLE_DEVICES'] = '-1' +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000" +import cv2 +import numpy as np +import time +import threading +from queue import Queue +from deepface import DeepFace +from ultralytics import YOLO +import warnings + +warnings.filterwarnings("ignore") + +# ────────────────────────────────────────────────────────────────────────────── +# 1. IMPORTAMOS NUESTROS MÓDULOS +# ────────────────────────────────────────────────────────────────────────────── +# Del motor matemático y tracking +from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE, similitud_hibrida + +# Del motor de reconocimiento facial y audio +from reconocimiento2 import ( + gestionar_vectores, + detectar_rostros_yunet, + buscar_mejor_match, + hilo_bienvenida, + UMBRAL_SIM, + COOLDOWN_TIME +) + +# ────────────────────────────────────────────────────────────────────────────── +# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN +# ────────────────────────────────────────────────────────────────────────────── +COLA_ROSTROS = Queue(maxsize=4) +YUNET_LOCK = threading.Lock() +IA_LOCK = threading.Lock() + +# Inicializamos la base de datos usando tu función importada +print("\nIniciando carga de base de datos...") +BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) + +# ────────────────────────────────────────────────────────────────────────────── +# 3. MOTOR ASÍNCRONO +# ────────────────────────────────────────────────────────────────────────────── +def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk): + """ Toma el recorte del tracker, escala a Alta Definición, usa YuNet y hace la Fusión Mágica """ + try: + if not BASE_DATOS_ROSTROS: return + + # ────────────────────────────────────────────────────────── + # 1. VALIDACIÓN DEL FRAME HD Y ESCALADO MATEMÁTICO + # ────────────────────────────────────────────────────────── + h_real, w_real = frame_hd.shape[:2] + + # ⚡ TRAMPA ANTI-BUGS: Si esto salta, corrige la llamada en tu main_fusion.py + if w_real <= 480: + print(f"[❌ ERROR CAM {cam_id}] Le estás pasando el frame_show (480x270) a ArcFace, no el HD.") + + escala_x = w_real / 480.0 + escala_y = h_real / 270.0 + + x_min, y_min, x_max, y_max = box_480 + h_box = y_max - y_min + + y_min_expandido = max(0, y_min - (h_box * 0.15)) + y_max_cabeza = min(270, y_min + (h_box * 0.40)) # Límite máximo en la escala de 270 + + x1_hd = int(max(0, x_min) * escala_x) + y1_hd = int(y_min_expandido * escala_y) + x2_hd = int(min(480, x_max) * escala_x) + y2_hd = int(y_max_cabeza * escala_y) + + roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd] + + # Si la cabeza HD mide menos de 60x60, está demasiado lejos incluso en HD + if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 60 or roi_cabeza.shape[1] < 60: + return + + h_roi, w_roi = roi_cabeza.shape[:2] + + # ────────────────────────────────────────────────────────── + # 2. DETECCIÓN DE ROSTRO CON YUNET (Ahora operando en HD) + # ────────────────────────────────────────────────────────── + faces = detectar_rostros_yunet(roi_cabeza, lock=YUNET_LOCK) + + for (rx, ry, rw, rh, score) in faces: + rx, ry = max(0, rx), max(0, ry) + rw, rh = min(w_roi - rx, rw), min(h_roi - ry, rh) + + area_rostro_actual = rw * rh + + with global_mem.lock: + data = global_mem.db.get(gid, {}) + nombre_actual = data.get('nombre') + area_ref = data.get('area_rostro_ref', 0) + + necesita_saludo = False + if str(cam_id) == "7": + if not hasattr(global_mem, 'ultimos_saludos'): + global_mem.ultimos_saludos = {} + ultimo = global_mem.ultimos_saludos.get(nombre_actual if nombre_actual else "", 0) + if (time.time() - ultimo) > COOLDOWN_TIME: + necesita_saludo = True + + if nombre_actual is None or area_rostro_actual >= (area_ref * 1.5) or necesita_saludo: + + # ⚡ MÁRGENES MÁS AMPLIOS: ArcFace necesita ver frente y barbilla (25%) + m_x = int(rw * 0.25) + m_y = int(rh * 0.25) + + roi_rostro = roi_cabeza[max(0, ry-m_y):min(h_roi, ry+rh+m_y), + max(0, rx-m_x):min(w_roi, rx+rw+m_x)] + + if roi_rostro.size == 0 or roi_rostro.shape[0] < 60 or roi_rostro.shape[1] < 60: + continue + + # ⚡ Laplaciano ajustado para imágenes HD (30.0 es más justo) + gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY) + nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() + if nitidez < 15.0: + continue + + # ────────────────────────────────────────────────────────── + # 3. RECONOCIMIENTO FACIAL ARCFACE + # ────────────────────────────────────────────────────────── + with IA_LOCK: + try: + # ⚡ CAMBIO DRÁSTICO: Usamos RetinaFace para alinear la cabeza obligatoriamente. + # Si RetinaFace no logra enderezar la cara (ej. estás totalmente de perfil), + # lanzará una excepción y abortará, evitando falsos positivos. + # Así DEBE estar en main_fusion.py para que sea compatible con tu nueva DB + res = DeepFace.represent( + img_path=roi_cabeza, + model_name="ArcFace", + detector_backend="retinaface", # Obligatorio + align=True, # Obligatorio + enforce_detection=True # Obligatorio + ) + emb = np.array(res[0]["embedding"], dtype=np.float32) + mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) + except Exception: + # Si falla la alineación o estás muy borroso, lo ignoramos en silencio. + continue + + print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f} (Umbral: {UMBRAL_SIM})") + + if max_sim >= UMBRAL_SIM and mejor_match: + nombre_limpio = mejor_match.split('_')[0] + + with global_mem.lock: + global_mem.db[gid]['nombre'] = nombre_limpio + global_mem.db[gid]['area_rostro_ref'] = area_rostro_actual + global_mem.db[gid]['ts'] = time.time() + + ids_a_borrar = [] + firma_actual = global_mem.db[gid]['firmas'][0] if global_mem.db[gid]['firmas'] else None + + for otro_gid, datos_otro in list(global_mem.db.items()): + if otro_gid == gid: continue + + if datos_otro.get('nombre') == nombre_limpio: + ids_a_borrar.append(otro_gid) + + elif datos_otro.get('nombre') is None and firma_actual and datos_otro['firmas']: + sim_huerfano = similitud_hibrida(firma_actual, datos_otro['firmas'][0]) + if sim_huerfano > 0.75: + ids_a_borrar.append(otro_gid) + + for id_basura in ids_a_borrar: + del global_mem.db[id_basura] + print(f"[🧹 LIMPIEZA] ID huérfano/clon {id_basura} eliminado tras reconocer a {nombre_limpio}.") + + if str(cam_id) == "7" and necesita_saludo: + global_mem.ultimos_saludos[nombre_limpio] = time.time() + try: + with IA_LOCK: + analisis = DeepFace.analyze(roi_rostro, actions=['gender'], enforce_detection=False)[0] + genero = analisis.get('dominant_gender', 'Man') + except Exception: + genero = "Man" + + threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero), daemon=True).start() + break + except Exception as e: + pass + finally: + trk.procesando_rostro = False + +def worker_rostros(global_mem): + """ Consumidor de la cola multihilo """ + while True: + frame, box, gid, cam_id, trk = COLA_ROSTROS.get() + procesar_rostro_async(frame, box, gid, cam_id, global_mem, trk) + COLA_ROSTROS.task_done() + +# ────────────────────────────────────────────────────────────────────────────── +# 4. LOOP PRINCIPAL DE FUSIÓN +# ────────────────────────────────────────────────────────────────────────────── +class CamStream: + def __init__(self, url): + self.url = url + self.cap = cv2.VideoCapture(url) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + self.frame = None + threading.Thread(target=self._run, daemon=True).start() + + def _run(self): + while True: + ret, f = self.cap.read() + if ret: + self.frame = f + time.sleep(0.01) + else: + time.sleep(2) + self.cap.open(self.url) + +def dibujar_track_fusion(frame_show, trk, global_mem): + try: x1, y1, x2, y2 = map(int, trk.box) + except Exception: return + + nombre_str = "" + if trk.gid is not None: + with global_mem.lock: + nombre = global_mem.db.get(trk.gid, {}).get('nombre') + if nombre: nombre_str = f" [{nombre}]" + + if trk.gid is None: color, label = (150, 150, 150), f"?{trk.local_id}" + elif nombre_str: color, label = (255, 0, 255), f"ID:{trk.gid}{nombre_str}" + elif trk.en_grupo: color, label = (0, 0, 255), f"ID:{trk.gid} [grp]" + elif trk.aprendiendo: color, label = (255, 255, 0), f"ID:{trk.gid} [++]" + elif trk.origen_global: color, label = (0, 165, 255), f"ID:{trk.gid} [re-id]" + else: color, label = (0, 255, 0), f"ID:{trk.gid}" + + cv2.rectangle(frame_show, (x1, y1), (x2, y2), color, 2) + (tw, th), _ = cv2.getTextSize(label, FUENTE, 0.55, 1) + cv2.rectangle(frame_show, (x1, y1-th-6), (x1+tw+2, y1), color, -1) + cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1) + +def main(): + print("\nIniciando Sistema") + model = YOLO("yolov8n.pt") + global_mem = GlobalMemory() + managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} + cams = [CamStream(u) for u in URLS] + + for _ in range(2): + threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start() + + cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) + idx = 0 + + while True: + now = time.time() + tiles = [] + cam_ia = idx % len(cams) + + for i, cam_obj in enumerate(cams): + frame = cam_obj.frame; cid = str(SECUENCIA[i]) + if frame is None: + tiles.append(np.zeros((270, 480, 3), np.uint8)) + continue + + frame_show = cv2.resize(frame.copy(), (480, 270)) + boxes = [] + turno_activo = (i == cam_ia) + + if turno_activo: + res = model.predict(frame_show, conf=0.50, iou=0.50, classes=[0], verbose=False, imgsz=480) + if res[0].boxes: + boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + + tracks = managers[cid].update(boxes, frame_show, now, turno_activo) + + for trk in tracks: + if trk.time_since_update <= 1: + dibujar_track_fusion(frame_show, trk, global_mem) + + if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False): + if not COLA_ROSTROS.full(): + trk.procesando_rostro = True + COLA_ROSTROS.put((frame.copy(), trk.box, trk.gid, cid, trk)) + + if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) + + con_id = sum(1 for t in tracks if t.gid and t.time_since_update==0) + cv2.putText(frame_show, f"CAM {cid} [{con_id} ID]", (10, 28), FUENTE, 0.7, (255, 255, 255), 2) + tiles.append(frame_show) + + if len(tiles) == 6: + cv2.imshow("SmartSoft Fusion", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])])) + + idx += 1 + if cv2.waitKey(1) == ord('q'): + break + + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() + + + + + + + + + + + + + + + + + + + +################################################################### reconocimeito2.py + +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +os.environ['CUDA_VISIBLE_DEVICES'] = '-1' + +import cv2 +import numpy as np +from deepface import DeepFace +import pickle +import time +import threading +import asyncio +import edge_tts +import subprocess +from datetime import datetime +import warnings +import urllib.request + +warnings.filterwarnings("ignore") + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +DB_PATH = "db_institucion" +CACHE_PATH = "cache_nombres" +VECTORS_FILE = "base_datos_rostros.pkl" +TIMESTAMPS_FILE = "representaciones_timestamps.pkl" +UMBRAL_SIM = 0.45 # Por encima → identificado. Por debajo → desconocido. +COOLDOWN_TIME = 15 # Segundos entre saludos + +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" +RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" + +for path in [DB_PATH, CACHE_PATH]: + os.makedirs(path, exist_ok=True) + +# ────────────────────────────────────────────────────────────────────────────── +# YUNET — Detector facial rápido en CPU +# ────────────────────────────────────────────────────────────────────────────── +YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" + +if not os.path.exists(YUNET_MODEL_PATH): + print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") + 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) + print("YuNet descargado.") + +# Detector estricto para ROIs grandes (persona cerca) +detector_yunet = cv2.FaceDetectorYN.create( + model=YUNET_MODEL_PATH, config="", + input_size=(320, 320), + score_threshold=0.70, + nms_threshold=0.3, + top_k=5000 +) + +# Detector permisivo para ROIs pequeños (persona lejos) +detector_yunet_lejano = cv2.FaceDetectorYN.create( + model=YUNET_MODEL_PATH, config="", + input_size=(320, 320), + score_threshold=0.45, + nms_threshold=0.3, + top_k=5000 +) + +def detectar_rostros_yunet(roi, lock=None): + """ + Elige automáticamente el detector según el tamaño del ROI. + """ + h_roi, w_roi = roi.shape[:2] + area = w_roi * h_roi + det = detector_yunet if area > 8000 else detector_yunet_lejano + + try: + if lock: + with lock: + det.setInputSize((w_roi, h_roi)) + _, faces = det.detect(roi) + else: + det.setInputSize((w_roi, h_roi)) + _, faces = det.detect(roi) + except Exception: + return [] + + if faces is None: + return [] + + resultado = [] + for face in faces: + try: + fx, fy, fw, fh = map(int, face[:4]) + score = float(face[14]) if len(face) > 14 else 1.0 + resultado.append((fx, fy, fw, fh, score)) + except (ValueError, OverflowError, TypeError): + continue + return resultado + + +# ────────────────────────────────────────────────────────────────────────────── +# SISTEMA DE AUDIO +# ────────────────────────────────────────────────────────────────────────────── +def obtener_audios_humanos(genero): + hora = datetime.now().hour + es_mujer = genero.lower() == 'woman' + suffix = "_m.mp3" if es_mujer else "_h.mp3" + if 5 <= hora < 12: + intro = "dias.mp3" + elif 12 <= hora < 19: + intro = "tarde.mp3" + else: + intro = "noches.mp3" + cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix + return intro, cierre + + +async def sintetizar_nombre(nombre, ruta): + nombre_limpio = nombre.replace('_', ' ') + try: + comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") + await comunicador.save(ruta) + except Exception: + pass + + +def reproducir(archivo): + if os.path.exists(archivo): + subprocess.Popen( + ["mpv", "--no-video", "--volume=100", archivo], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + +def hilo_bienvenida(nombre, genero): + archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") + + if not os.path.exists(archivo_nombre): + try: + asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) + except Exception: + pass + + intro, cierre = obtener_audios_humanos(genero) + + archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] + if archivos: + subprocess.Popen( + ["mpv", "--no-video", "--volume=100"] + archivos, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + +# ────────────────────────────────────────────────────────────────────────────── +# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN) +# ────────────────────────────────────────────────────────────────────────────── +def gestionar_vectores(actualizar=False): + vectores_actuales = {} + + if os.path.exists(VECTORS_FILE): + try: + with open(VECTORS_FILE, 'rb') as f: + vectores_actuales = pickle.load(f) + except Exception: + vectores_actuales = {} + + if not actualizar: + return vectores_actuales + + timestamps = {} + if os.path.exists(TIMESTAMPS_FILE): + try: + with open(TIMESTAMPS_FILE, 'rb') as f: + timestamps = pickle.load(f) + except Exception: + timestamps = {} + + print("\nACTUALIZANDO BASE DE DATOS (Alineación con RetinaFace)...") + imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] + nombres_en_disco = set() + hubo_cambios = False + + for archivo in imagenes: + nombre_archivo = os.path.splitext(archivo)[0] + ruta_img = os.path.join(DB_PATH, archivo) + nombres_en_disco.add(nombre_archivo) + + ts_actual = os.path.getmtime(ruta_img) + ts_guardado = timestamps.get(nombre_archivo, 0) + + if nombre_archivo in vectores_actuales and ts_actual == ts_guardado: + continue + + try: + # ⚡ MAGIA 1: RetinaFace alinea matemáticamente los rostros de la base de datos + res = DeepFace.represent( + img_path=ruta_img, + model_name="ArcFace", + detector_backend="retinaface", # Localiza ojos/nariz + align=True, # Rota la imagen para alinear + enforce_detection=True # Obliga a que haya cara válida + ) + emb = np.array(res[0]["embedding"], dtype=np.float32) + + # ⚡ MAGIA 2: Normalización L2 al guardar (Elimina el "Efecto Rosa María") + norma = np.linalg.norm(emb) + if norma > 0: + emb = emb / norma + + vectores_actuales[nombre_archivo] = emb + timestamps[nombre_archivo] = ts_actual + hubo_cambios = True + print(f" ✅ Procesado y alineado: {nombre_archivo}") + + except Exception as e: + print(f" ❌ Rostro no válido en '{archivo}', omitido. Error: {e}") + + for nombre in list(vectores_actuales.keys()): + if nombre not in nombres_en_disco: + del vectores_actuales[nombre] + timestamps.pop(nombre, None) + hubo_cambios = True + print(f" 🗑️ Eliminado (sin foto): {nombre}") + + if hubo_cambios: + with open(VECTORS_FILE, 'wb') as f: + pickle.dump(vectores_actuales, f) + with open(TIMESTAMPS_FILE, 'wb') as f: + pickle.dump(timestamps, f) + print(" Sincronización terminada.\n") + else: + print(" Sin cambios. Base de datos al día.\n") + + return vectores_actuales + +# ────────────────────────────────────────────────────────────────────────────── +# BÚSQUEDA BLINDADA (Similitud Coseno estricta) +# ────────────────────────────────────────────────────────────────────────────── +def buscar_mejor_match(emb_consulta, base_datos): + # ⚡ MAGIA 3: Normalización L2 del vector entrante + norma = np.linalg.norm(emb_consulta) + if norma > 0: + emb_consulta = emb_consulta / norma + + 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) + sim = float(np.dot(emb_consulta, vec)) + if sim > max_sim: + max_sim = sim + mejor_match = nombre + + return mejor_match, max_sim + +# ────────────────────────────────────────────────────────────────────────────── +# LOOP DE PRUEBA Y REGISTRO +# ────────────────────────────────────────────────────────────────────────────── +def sistema_interactivo(): + base_datos = gestionar_vectores(actualizar=False) + cap = cv2.VideoCapture(RTSP_URL) + ultimo_saludo = 0 + persona_actual = None + confirmaciones = 0 + + print("\n" + "=" * 50) + print(" MÓDULO DE REGISTRO Y DEPURACIÓN") + print(" [R] Registrar nuevo rostro | [Q] Salir") + print("=" * 50 + "\n") + + faces_ultimo_frame = [] + + while True: + ret, frame = cap.read() + if not ret: + time.sleep(2) + cap.open(RTSP_URL) + continue + + h, w = frame.shape[:2] + display_frame = frame.copy() + tiempo_actual = time.time() + + faces_raw = detectar_rostros_yunet(frame) + faces_ultimo_frame = faces_raw + + for (fx, fy, fw, fh, score_yunet) in faces_raw: + fx = max(0, fx); fy = max(0, fy) + fw = min(w - fx, fw); fh = min(h - fy, fh) + if fw <= 0 or fh <= 0: + continue + + cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) + cv2.putText(display_frame, f"YN:{score_yunet:.2f}", + (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) + + if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: + continue + + m = int(fw * 0.15) + roi = frame[max(0, fy-m): min(h, fy+fh+m), + max(0, fx-m): min(w, fx+fw+m)] + + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: + cv2.putText(display_frame, "muy pequeño", + (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) + continue + + gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) + nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() + if nitidez < 50.0: + cv2.putText(display_frame, f"blur({nitidez:.0f})", + (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) + continue + + try: + # ⚡ En el modo de prueba interactivo usamos las reglas viejas + # para que sea rápido y puedas registrar fotos fácilmente. + res = DeepFace.represent( + img_path=roi, model_name="ArcFace", enforce_detection=False + ) + emb = np.array(res[0]["embedding"], dtype=np.float32) + mejor_match, max_sim = buscar_mejor_match(emb, base_datos) + + except Exception as e: + print(f"[ERROR ArcFace]: {e}") + continue + + estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" + nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" + n_bloques = int(max_sim * 20) + barra = "█" * n_bloques + "░" * (20 - n_bloques) + print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " + f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") + + if max_sim > UMBRAL_SIM and mejor_match: + color = (0, 255, 0) + texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" + + if mejor_match == persona_actual: + confirmaciones += 1 + else: + persona_actual, confirmaciones = mejor_match, 1 + + if confirmaciones >= 2: + cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) + try: + analisis = DeepFace.analyze( + roi, actions=['gender'], enforce_detection=False + )[0] + genero = analisis['dominant_gender'] + except Exception: + genero = "Man" + + threading.Thread( + target=hilo_bienvenida, + args=(mejor_match, genero), + daemon=True + ).start() + ultimo_saludo = tiempo_actual + confirmaciones = 0 + + else: + color = (0, 0, 255) + texto = f"? ({max_sim:.2f})" + confirmaciones = max(0, confirmaciones - 1) + + cv2.putText(display_frame, texto, + (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) + + cv2.imshow("Módulo de Registro", display_frame) + key = cv2.waitKey(1) & 0xFF + + if key == ord('q'): + break + + elif key == ord('r'): + if faces_ultimo_frame: + areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] + fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] + + # Le damos más margen al registro (30%) para que RetinaFace no falle + # cuando procese la foto en la carpeta. + m_x = int(fw * 0.30) + m_y = int(fh * 0.30) + 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)] + + if face_roi.size > 0: + nom = input("\nNombre de la persona: ").strip() + if nom: + foto_path = os.path.join(DB_PATH, f"{nom}.jpg") + cv2.imwrite(foto_path, face_roi) + print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") + # ⚡ Al sincronizar, RetinaFace alineará esta foto guardada. + base_datos = gestionar_vectores(actualizar=True) + else: + print("[!] Registro cancelado.") + else: + 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()