From aa2132f3cfaa90e34d5fa3f67f5b9cc597e17941 Mon Sep 17 00:00:00 2001 From: ecuellar Date: Wed, 8 Apr 2026 11:00:23 -0600 Subject: [PATCH] Cambios yolo en gpu --- .gitignore | 346 ++-- README.md | 122 +- cache_nombres/nombre_Jose Maria.mp3 | 0 cache_nombres/nombre_Rosa Maria.mp3 | Bin 10512 -> 0 bytes db_institucion/Diana Laura.jpg | Bin 4810 -> 0 bytes db_institucion/Ian Axel.jpg | Bin 3120 -> 0 bytes fusion.py | 7 +- generar_db_rostros.py | 218 +-- prueba_video.py | 84 +- reconocimiento2.py | 938 +++++----- requirements.txt | 192 +- vesiones_seguras.txt | 2516 +++++++++++++-------------- 12 files changed, 2217 insertions(+), 2206 deletions(-) delete mode 100644 cache_nombres/nombre_Jose Maria.mp3 delete mode 100644 cache_nombres/nombre_Rosa Maria.mp3 delete mode 100644 db_institucion/Diana Laura.jpg delete mode 100644 db_institucion/Ian Axel.jpg 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 838698711cc909beb9dd3a64613cdb9b05f0b79a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10512 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 diff --git a/db_institucion/Diana Laura.jpg b/db_institucion/Diana Laura.jpg deleted file mode 100644 index 479b9fde94b33be0c51e9a429a7d38ac43c16bdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4810 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 diff --git a/db_institucion/Ian Axel.jpg b/db_institucion/Ian Axel.jpg deleted file mode 100644 index e672d388fb0ac6064fafdbb0687864932709cf5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3120 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 E15Pcg?f?J) 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()