diff --git a/ElevenLabs.mp3 b/ElevenLabs.mp3 deleted file mode 100644 index 9f67682..0000000 Binary files a/ElevenLabs.mp3 and /dev/null differ diff --git a/ElevenLabs1.mp3 b/ElevenLabs1.mp3 deleted file mode 100644 index 50546c1..0000000 Binary files a/ElevenLabs1.mp3 and /dev/null differ diff --git a/Rodrigo Cahuantzi_1.jpg b/Rodrigo Cahuantzi_1.jpg deleted file mode 100644 index 82215bb..0000000 Binary files a/Rodrigo Cahuantzi_1.jpg and /dev/null differ diff --git a/audio_iot.py b/audio_iot.py index 4b6a54e..dac40a4 100644 --- a/audio_iot.py +++ b/audio_iot.py @@ -1,77 +1,350 @@ import asyncio -import websockets -import json from pydub import AudioSegment import os - -# Configuración de tu Bocina IP -IP_BOCINA = "192.168.1.142" -PUERTO_WS = 81 - -async def _enviar_audio_ws(pcm_data): - url = f"ws://{IP_BOCINA}:{PUERTO_WS}" - chunk_size = 1024 - +import threading +import http.server +import socketserver +import socket +import time +import subprocess +import urllib.request # Para enviar HTTP GET a la ESP32 (sin librerías extra) + + +# ================= CONFIGURACIÓN ================= +IP_BOCINA = "192.168.15.135" +PUERTO_ESP32 = 80 +PUERTO_HTTP = 8000 +CARPETA_AUDIO = "public_audio" + +if not os.path.exists(CARPETA_AUDIO): + os.makedirs(CARPETA_AUDIO) + +# ================= AUTO-DETECTAR MI IP ================= +def obtener_mi_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: - async with websockets.connect(url, ping_timeout=None) as websocket: - # Esperamos el mensaje de bienvenida - await websocket.recv() - - chunks_enviados = 0 - for i in range(0, len(pcm_data), chunk_size): - chunk = pcm_data[i:i + chunk_size] - await websocket.send(chunk) - chunks_enviados += 1 - - # ⚡ EL TRUCO MAESTRO: CONTROL DE FLUJO - if chunks_enviados % 10 == 0: - try: - # Python se pausa hasta que el ESP32 mande el "ACK" - await asyncio.wait_for(websocket.recv(), timeout=1.0) - except asyncio.TimeoutError: - pass # Si hay lag en la red, forzamos a seguir para no cortar - else: - await asyncio.sleep(0.002) # Micro-pausa entre paquetes individuales - - # Le damos 1 segundo al ESP32 para reproducir los últimos datos en su memoria - await asyncio.sleep(1.0) - await websocket.send(json.dumps({"cmd": "STOP"})) - return True - + s.connect((IP_BOCINA, 80)) + IP = s.getsockname()[0] + except Exception: + IP = '127.0.0.1' + finally: + s.close() + return IP + +MI_IP = obtener_mi_ip() +print(f"[RED] IP auto-detectada para el servidor de audio: {MI_IP}") + + +def iniciar_servidor_http(): + class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=CARPETA_AUDIO, **kwargs) + + def log_message(self, format, *args): + print(f"[SERVIDOR HTTP] Petición de ESP32: {args[0]}") + + def handle(self): + try: + super().handle() + except (BrokenPipeError, ConnectionResetError): + pass + + def copyfile(self, source, outputfile): + try: + super().copyfile(source, outputfile) + except (BrokenPipeError, ConnectionResetError): + pass + + class ReusableTCPServer(socketserver.TCPServer): + allow_reuse_address = True + + try: + with ReusableTCPServer(("", PUERTO_HTTP), Handler) as httpd: + print(f"Servidor de Audio activo en http://{MI_IP}:{PUERTO_HTTP}") + httpd.serve_forever() except Exception as e: - print(f" Error conectando a la bocina IP {IP_BOCINA}: {e}") + print(f"ERROR CRÍTICO AL LEVANTAR EL SERVIDOR: {e}") + + +# ================= ORDEN A LA ESP32 ================= +# La ESP32 usa HTTP GET en /play?url=..., NO WebSocket. +# Antes usábamos WebSocket al puerto 81 → la ESP32 nunca recibió nada. +def _mandar_orden_reproduccion(nombre_archivo): + url_mp3 = f"http://{MI_IP}:{PUERTO_HTTP}/{nombre_archivo}" + # Construimos la URL exacta que espera el handlePlay() de la ESP32 + url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/play?url={url_mp3}" + + try: + print(f"Ordenando a ESP32 reproducir: {url_mp3}") + with urllib.request.urlopen(url_orden, timeout=5) as respuesta: + estado = respuesta.read().decode() + print(f"ESP32 respondió: {estado}") + return True + except Exception as e: + print(f" Error enviando orden a la bocina: {e}") + return False + +# ================= CONTROL DE VOLUMEN ================= + +def _mandar_orden_volumen(nivel=18): + """Establece volumen directamente (0-21)""" + url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume?level={nivel}" + try: + with urllib.request.urlopen(url_orden, timeout=5) as respuesta: + print(f"Volumen: {respuesta.read().decode()}") + return True + except Exception as e: + print(f"Error cambiando volumen: {e}") return False +def subir_volumen(): + """Sube 1 nivel de volumen""" + url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume_up" + try: + with urllib.request.urlopen(url_orden, timeout=5) as respuesta: + print(f" {respuesta.read().decode()}") + return True + except Exception as e: + print(f"Error: {e}") + return False + +def bajar_volumen(): + """Baja 1 nivel de volumen""" + url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/volume_down" + try: + with urllib.request.urlopen(url_orden, timeout=5) as respuesta: + print(f"{respuesta.read().decode()}") + return True + except Exception as e: + print(f"Error: {e}") + return False + + +# ================= LÓGICA PRINCIPAL ================= def enviar_archivos_bocina(lista_archivos): - """ - Recibe una lista de rutas de archivos MP3, los une en un solo track, - los convierte al formato del ESP32 y los transmite. - """ if not lista_archivos: return - + try: + # 1. Unimos los audios audio_final = AudioSegment.empty() for archivo in lista_archivos: if os.path.exists(archivo): - segmento = AudioSegment.from_file(archivo) - audio_final += segmento - - audio_mono = audio_final.split_to_mono()[0] - - # ⚡ 2. SUBIMOS LA CALIDAD (A 22050 Hz) - audio_final = audio_mono.set_frame_rate(22050).set_sample_width(2) - - # 3. Quitamos el clipping (saturación). - # Como ya no estamos sumando canales, podemos dejarle más volumen. Le bajaremos solo 2 dB. - audio_final = audio_final - 2 - - # 4. Obtenemos los datos puros - pcm_data = audio_final.raw_data - - # ⚡ 4. TRANSMITIMOS A LA BOCINA (¡Te faltaba esta parte!) - print(f" Transmitiendo audio a {IP_BOCINA}...") - asyncio.run(_enviar_audio_ws(pcm_data)) - + audio_final += AudioSegment.from_file(archivo) + + # 2. WAV temporal + ruta_temp = os.path.join(CARPETA_AUDIO, "temp.wav") + audio_final.export(ruta_temp, format="wav") + + # 3. Nombre único anti-caché + timestamp = int(time.time()) + nombre_salida = f"saludo_{timestamp}.mp3" + ruta_salida = os.path.join(CARPETA_AUDIO, nombre_salida) + + # 4. MP3 ligero para reducir la presión sobre el buffer de Audio.h + # El fix completo del OOM va en el .ino (ver abajo) + comando_ffmpeg = [ + "/usr/bin/ffmpeg", "-y", + "-i", ruta_temp, + "-codec:a", "libmp3lame", + "-b:a", "48k", + "-ar", "24000", + "-ac", "1", + # FILTRO ACÚSTICO: Corta todo lo que no sea voz humana + "-af", "highpass=f=150, lowpass=f=7500", + "-map_metadata", "-1", + "-id3v2_version", "0", + "-write_xing", "0", + ruta_salida + ] + subprocess.run(comando_ffmpeg, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # 5. Limpieza del temporal + if os.path.exists(ruta_temp): + os.remove(ruta_temp) + + # 6. Enviamos la orden en un hilo separado + # (evita conflicto con el runtime de InsightFace/ONNX) + def _lanzar_orden(): + time.sleep(0.5) # Aseguramos que el archivo ya está en disco + _mandar_orden_reproduccion(nombre_salida) + + # Limpiamos MP3 viejos DESPUÉS de que la ESP32 confirmó + for archivo in os.listdir(CARPETA_AUDIO): + if archivo.startswith("saludo_") and archivo != nombre_salida: + try: + os.remove(os.path.join(CARPETA_AUDIO, archivo)) + except Exception: + pass + + hilo = threading.Thread(target=_lanzar_orden, daemon=True) + hilo.start() + except Exception as e: - print(f"Error procesando el audio para la bocina: {e}") \ No newline at end of file + print(f"Error procesando el audio: {e}") + +# ============================================================================== +# AUTO-ARRANQUE DEL SERVIDOR (Hilo en segundo plano) +# ============================================================================== +servidor_thread = threading.Thread(target=iniciar_servidor_http, daemon=True) +servidor_thread.start() + + +""" +import asyncio +from pydub import AudioSegment +import os +import threading +import http.server +import socketserver +import socket +import time +import subprocess +import urllib.request # Para enviar HTTP GET a la ESP32 (sin librerías extra) + + +# ================= CONFIGURACIÓN ================= +IP_BOCINA = "192.168.1.77" # La IP de la ESP32 que se definio como estatica +PUERTO_ESP32 = 80 # La ESP32 escucha HTTP en el puerto 80 +PUERTO_HTTP = 8000 +CARPETA_AUDIO = "public_audio" + +if not os.path.exists(CARPETA_AUDIO): + os.makedirs(CARPETA_AUDIO) + + +# ================= SERVIDOR HTTP ================= +def obtener_mi_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(('10.255.255.255', 1)) + IP = s.getsockname()[0] + except Exception: + IP = '127.0.0.1' + finally: + s.close() + return IP + + +MI_IP = obtener_mi_ip() + + +def iniciar_servidor_http(): + class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=CARPETA_AUDIO, **kwargs) + + def log_message(self, format, *args): + print(f"[SERVIDOR HTTP] Petición de ESP32: {args[0]}") + + def handle(self): + try: + super().handle() + except (BrokenPipeError, ConnectionResetError): + pass + + def copyfile(self, source, outputfile): + try: + super().copyfile(source, outputfile) + except (BrokenPipeError, ConnectionResetError): + pass + + class ReusableTCPServer(socketserver.TCPServer): + allow_reuse_address = True + + try: + with ReusableTCPServer(("", PUERTO_HTTP), Handler) as httpd: + print(f"Servidor de Audio activo en http://{MI_IP}:{PUERTO_HTTP}") + httpd.serve_forever() + except Exception as e: + print(f"ERROR CRÍTICO AL LEVANTAR EL SERVIDOR: {e}") + + +# ================= ORDEN A LA ESP32 ================= +# La ESP32 usa HTTP GET en /play?url=..., NO WebSocket. +# Antes usábamos WebSocket al puerto 81 → la ESP32 nunca recibió nada. +def _mandar_orden_reproduccion(nombre_archivo): + url_mp3 = f"http://{MI_IP}:{PUERTO_HTTP}/{nombre_archivo}" + # Construimos la URL exacta que espera el handlePlay() de la ESP32 + url_orden = f"http://{IP_BOCINA}:{PUERTO_ESP32}/play?url={url_mp3}" + + try: + print(f"Ordenando a ESP32 reproducir: {url_mp3}") + with urllib.request.urlopen(url_orden, timeout=5) as respuesta: + estado = respuesta.read().decode() + print(f"ESP32 respondió: {estado}") + return True + except Exception as e: + print(f" Error enviando orden a la bocina: {e}") + return False + + +# ================= LÓGICA PRINCIPAL ================= +def enviar_archivos_bocina(lista_archivos): + if not lista_archivos: + return + + try: + # 1. Unimos los audios + audio_final = AudioSegment.empty() + for archivo in lista_archivos: + if os.path.exists(archivo): + audio_final += AudioSegment.from_file(archivo) + + # 2. WAV temporal + ruta_temp = os.path.join(CARPETA_AUDIO, "temp.wav") + audio_final.export(ruta_temp, format="wav") + + # 3. Nombre único anti-caché + timestamp = int(time.time()) + nombre_salida = f"saludo_{timestamp}.mp3" + ruta_salida = os.path.join(CARPETA_AUDIO, nombre_salida) + + # 4. MP3 ligero para reducir la presión sobre el buffer de Audio.h + # El fix completo del OOM va en el .ino (ver abajo) + comando_ffmpeg = [ + "/usr/bin/ffmpeg", "-y", + "-i", ruta_temp, + "-codec:a", "libmp3lame", + "-b:a", "48k", + "-ar", "24000", + "-ac", "1", + # FILTRO ACÚSTICO: Corta todo lo que no sea voz humana + "-af", "highpass=f=150, lowpass=f=7500", + "-map_metadata", "-1", + "-id3v2_version", "0", + "-write_xing", "0", + ruta_salida + ] + subprocess.run(comando_ffmpeg, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # 5. Limpieza del temporal + if os.path.exists(ruta_temp): + os.remove(ruta_temp) + + # 6. Enviamos la orden en un hilo separado + # (evita conflicto con el runtime de InsightFace/ONNX) + def _lanzar_orden(): + time.sleep(0.5) # Aseguramos que el archivo ya está en disco + _mandar_orden_reproduccion(nombre_salida) + + # Limpiamos MP3 viejos DESPUÉS de que la ESP32 confirmó + for archivo in os.listdir(CARPETA_AUDIO): + if archivo.startswith("saludo_") and archivo != nombre_salida: + try: + os.remove(os.path.join(CARPETA_AUDIO, archivo)) + except Exception: + pass + + hilo = threading.Thread(target=_lanzar_orden, daemon=True) + hilo.start() + + except Exception as e: + print(f"Error procesando el audio: {e}") + +# ============================================================================== +# AUTO-ARRANQUE DEL SERVIDOR (Hilo en segundo plano) +# ============================================================================== +servidor_thread = threading.Thread(target=iniciar_servidor_http, daemon=True) +servidor_thread.start() +""" \ No newline at end of file diff --git a/base_datos_rostros.pkl b/base_datos_rostros.pkl index 3db050b..4106a21 100644 Binary files a/base_datos_rostros.pkl and b/base_datos_rostros.pkl differ diff --git a/bocina_esp32.ino b/bocina_esp32.ino new file mode 100644 index 0000000..ba83e40 --- /dev/null +++ b/bocina_esp32.ino @@ -0,0 +1,225 @@ +#include +#include +#include +#include "Audio.h" + +const char* ssid = "SmartEstancia"; +const char* password = "Smart12345"; + +IPAddress local_IP(192, 168, 1, 77); +IPAddress gateway(192, 168, 1, 254); +IPAddress subnet(255, 255, 255, 0); +IPAddress primaryDNS(8, 8, 8, 8); +IPAddress secondaryDNS(8, 8, 4, 4); + +#define I2S_BCLK 26 +#define I2S_LRC 25 +#define I2S_DOUT 33 + +Audio audio; +WebServer server(80); + +void handlePlay() { + if (server.hasArg("url")) { + String url = server.arg("url"); + Serial.print("🎵 Python ordenó reproducir: "); + Serial.println(url); + audio.stopSong(); + audio.connecttohost(url.c_str()); + server.send(200, "text/plain", "Reproduciendo OK"); + } else { + server.send(400, "text/plain", "Error: Falta la URL"); + } +} + +// --- NUEVO: Control de Volumen --- +void handleVolume() { + if (server.hasArg("level")) { + int nivel = server.arg("level").toInt(); + if (nivel >= 0 && nivel <= 21) { + audio.setVolume(nivel); + Serial.print("🔊 Volumen cambiado a: "); + Serial.println(nivel); + server.send(200, "text/plain", "Volumen OK: " + String(nivel)); + } else { + server.send(400, "text/plain", "Error: Nivel debe ser 0-21"); + } + } else { + server.send(400, "text/plain", "Error: Falta parametro 'level'"); + } +} + +void handleVolumeUp() { + int actual = audio.getVolume(); + if (actual < 21) audio.setVolume(actual + 1); + server.send(200, "text/plain", "Volumen: " + String(audio.getVolume())); +} + +void handleVolumeDown() { + int actual = audio.getVolume(); + if (actual > 0) audio.setVolume(actual - 1); + server.send(200, "text/plain", "Volumen: " + String(audio.getVolume())); +} + +void setup() { + Serial.begin(115200); + Serial.println("\n--- INICIANDO SISTEMA DE AUDIO ESP32 ---"); + + if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) { + Serial.println("Fallo al configurar la IP Estatica"); + } + + WiFi.begin(ssid, password); + Serial.print("Conectando a WiFi"); + + int intento = 0; + while (WiFi.status() != WL_CONNECTED && intento < 20) { + delay(500); + Serial.print("."); + intento++; + } + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("\nFallo de conexion."); + while(true){ delay(1000); } + } + + Serial.println("\nWiFi Conectado!"); + Serial.print("IP FIJA: "); + Serial.println(WiFi.localIP()); + + audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); + audio.setVolume(15); // Volumen inicial medio + + server.on("/play", HTTP_GET, handlePlay); + server.on("/volume", HTTP_GET, handleVolume); + server.on("/volume_up", HTTP_GET, handleVolumeUp); + server.on("/volume_down", HTTP_GET, handleVolumeDown); + + server.begin(); + Serial.println("Listo para recibir audio..."); +} + +void loop() { + audio.loop(); + server.handleClient(); + + if (Serial.available()) { + String url = Serial.readStringUntil('\n'); + url.trim(); + if (url.length() > 5 && url.startsWith("http")) { + audio.stopSong(); + audio.connecttohost(url.c_str()); + } + } +} + +void audio_info(const char *info){ Serial.print("INFO AUDIO: "); Serial.println(info); } +void audio_id3data(const char *info){ Serial.print("METADATOS: "); Serial.println(info); } +void audio_eof_mp3(const char *info){ Serial.print("FIN DEL AUDIO: "); Serial.println(info); } + + + +""" +#include +#include +#include +#include "Audio.h" + +// ======================================================= +// 1. CONFIGURACIÓN DE RED +// ======================================================= +const char* ssid = "SmartEstancia"; +const char* password = "Smart12345"; + +// --- CONFIGURACIÓN DE IP ESTÁTICA --- +IPAddress local_IP(192, 168, 1, 77); // La IP que le asiganamos a la bocina +IPAddress gateway(192, 168, 1, 254); // la IP del router +IPAddress subnet(255, 255, 255, 0); // Máscara de subred +IPAddress primaryDNS(8, 8, 8, 8); // DNS primario (Google) +IPAddress secondaryDNS(8, 8, 4, 4); // DNS secundario (Google) + +// ======================================================= +// 2. CONFIGURACIÓN DE PINES CORRECTOS +// ======================================================= +#define I2S_BCLK 26 +#define I2S_LRC 25 +#define I2S_DOUT 33 + +Audio audio; +WebServer server(80); + +void handlePlay() { + if (server.hasArg("url")) { + String url = server.arg("url"); + Serial.print("🎵 Python ordenó reproducir: "); + Serial.println(url); + audio.stopSong(); + audio.connecttohost(url.c_str()); + server.send(200, "text/plain", "Reproduciendo OK"); + } else { + server.send(400, "text/plain", "Error: Falta la URL"); + } +} + +void setup() { + Serial.begin(115200); + Serial.println("\n--- INICIANDO SISTEMA DE AUDIO ESP32 ---"); + + if (psramFound()) { + Serial.println("PSRAM encontrada."); + } else { + Serial.println(" Sin PSRAM. Usando RAM interna."); + } + + // ⚡ APLICAR IP ESTÁTICA ANTES DE CONECTAR + if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) { + Serial.println(" Fallo al configurar la IP Estática"); + } + + WiFi.begin(ssid, password); + Serial.print("Conectando a WiFi"); + + int intento = 0; + while (WiFi.status() != WL_CONNECTED && intento < 20) { + delay(500); + Serial.print("."); + intento++; + } + + if (WiFi.status() != WL_CONNECTED) { + Serial.println("\n Fallo de conexión."); + while(true){ delay(1000); } + } + + Serial.println("\n WiFi Conectado de verdad!"); + Serial.print("Tu IP es FIJA: "); + Serial.println(WiFi.localIP()); + + audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); + audio.setVolume(18); + + server.on("/play", HTTP_GET, handlePlay); + server.begin(); + Serial.println("Listo para recibir audio..."); +} + +void loop() { + audio.loop(); + server.handleClient(); + + if (Serial.available()) { + String url = Serial.readStringUntil('\n'); + url.trim(); + if (url.length() > 5 && url.startsWith("http")) { + audio.stopSong(); + audio.connecttohost(url.c_str()); + } + } +} + +void audio_info(const char *info){ Serial.print("INFO AUDIO: "); Serial.println(info); } +void audio_id3data(const char *info){ Serial.print("METADATOS: "); Serial.println(info); } +void audio_eof_mp3(const char *info){ Serial.print("FIN DEL AUDIO: "); Serial.println(info); } + +""" \ No newline at end of file diff --git a/cache_nombres/alerta_101.jpg b/cache_nombres/alerta_101.jpg new file mode 100644 index 0000000..1595725 Binary files /dev/null and b/cache_nombres/alerta_101.jpg differ diff --git a/cache_nombres/alerta_102.jpg b/cache_nombres/alerta_102.jpg new file mode 100644 index 0000000..3873a1f Binary files /dev/null and b/cache_nombres/alerta_102.jpg differ diff --git a/cache_nombres/alerta_103.jpg b/cache_nombres/alerta_103.jpg new file mode 100644 index 0000000..51827ca Binary files /dev/null and b/cache_nombres/alerta_103.jpg differ diff --git a/cache_nombres/alerta_104.jpg b/cache_nombres/alerta_104.jpg new file mode 100644 index 0000000..fcb5773 Binary files /dev/null and b/cache_nombres/alerta_104.jpg differ diff --git a/cache_nombres/alerta_105.jpg b/cache_nombres/alerta_105.jpg new file mode 100644 index 0000000..f76d0b3 Binary files /dev/null and b/cache_nombres/alerta_105.jpg differ diff --git a/cache_nombres/alerta_106.jpg b/cache_nombres/alerta_106.jpg new file mode 100644 index 0000000..c8eeb48 Binary files /dev/null and b/cache_nombres/alerta_106.jpg differ diff --git a/cache_nombres/alerta_107.jpg b/cache_nombres/alerta_107.jpg new file mode 100644 index 0000000..e4757b3 Binary files /dev/null and b/cache_nombres/alerta_107.jpg differ diff --git a/cache_nombres/alerta_108.jpg b/cache_nombres/alerta_108.jpg new file mode 100644 index 0000000..7a57414 Binary files /dev/null and b/cache_nombres/alerta_108.jpg differ diff --git a/cache_nombres/alerta_110.jpg b/cache_nombres/alerta_110.jpg new file mode 100644 index 0000000..b798a6b Binary files /dev/null and b/cache_nombres/alerta_110.jpg differ diff --git a/cache_nombres/alerta_111.jpg b/cache_nombres/alerta_111.jpg new file mode 100644 index 0000000..2449568 Binary files /dev/null and b/cache_nombres/alerta_111.jpg differ diff --git a/cache_nombres/alerta_113.jpg b/cache_nombres/alerta_113.jpg new file mode 100644 index 0000000..8242021 Binary files /dev/null and b/cache_nombres/alerta_113.jpg differ diff --git a/cache_nombres/alerta_114.jpg b/cache_nombres/alerta_114.jpg new file mode 100644 index 0000000..222ed9a Binary files /dev/null and b/cache_nombres/alerta_114.jpg differ diff --git a/cache_nombres/alerta_116.jpg b/cache_nombres/alerta_116.jpg new file mode 100644 index 0000000..2487589 Binary files /dev/null and b/cache_nombres/alerta_116.jpg differ diff --git a/cache_nombres/alerta_118.jpg b/cache_nombres/alerta_118.jpg new file mode 100644 index 0000000..8e34d1b Binary files /dev/null and b/cache_nombres/alerta_118.jpg differ diff --git a/cache_nombres/alerta_119.jpg b/cache_nombres/alerta_119.jpg new file mode 100644 index 0000000..42057ae Binary files /dev/null and b/cache_nombres/alerta_119.jpg differ diff --git a/cache_nombres/alerta_123.jpg b/cache_nombres/alerta_123.jpg new file mode 100644 index 0000000..fa3ce3d Binary files /dev/null and b/cache_nombres/alerta_123.jpg differ diff --git a/cache_nombres/alerta_124.jpg b/cache_nombres/alerta_124.jpg new file mode 100644 index 0000000..4fbaa91 Binary files /dev/null and b/cache_nombres/alerta_124.jpg differ diff --git a/cache_nombres/alerta_125.jpg b/cache_nombres/alerta_125.jpg new file mode 100644 index 0000000..74ee0ad Binary files /dev/null and b/cache_nombres/alerta_125.jpg differ diff --git a/cache_nombres/alerta_126.jpg b/cache_nombres/alerta_126.jpg new file mode 100644 index 0000000..8f79e9c Binary files /dev/null and b/cache_nombres/alerta_126.jpg differ diff --git a/cache_nombres/alerta_163.jpg b/cache_nombres/alerta_163.jpg new file mode 100644 index 0000000..23bf213 Binary files /dev/null and b/cache_nombres/alerta_163.jpg differ diff --git a/cache_nombres/alerta_164.jpg b/cache_nombres/alerta_164.jpg new file mode 100644 index 0000000..8d591c7 Binary files /dev/null and b/cache_nombres/alerta_164.jpg differ diff --git a/cache_nombres/auditoria_caras/106_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/106_cam7_2026-06-25.jpg new file mode 100644 index 0000000..c8eeb48 Binary files /dev/null and b/cache_nombres/auditoria_caras/106_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/107_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras/107_cam3_2026-06-25.jpg new file mode 100644 index 0000000..81c47ce Binary files /dev/null and b/cache_nombres/auditoria_caras/107_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/107_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras/107_cam5_2026-06-25.jpg new file mode 100644 index 0000000..e4757b3 Binary files /dev/null and b/cache_nombres/auditoria_caras/107_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/107_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/107_cam7_2026-06-25.jpg new file mode 100644 index 0000000..54488fa Binary files /dev/null and b/cache_nombres/auditoria_caras/107_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/107_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras/107_cam8_2026-06-25.jpg new file mode 100644 index 0000000..37a0096 Binary files /dev/null and b/cache_nombres/auditoria_caras/107_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/108_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras/108_cam6_2026-06-25.jpg new file mode 100644 index 0000000..7a57414 Binary files /dev/null and b/cache_nombres/auditoria_caras/108_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Adriana Lopez_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/Adriana Lopez_cam7_2026-06-25.jpg new file mode 100644 index 0000000..fd5a776 Binary files /dev/null and b/cache_nombres/auditoria_caras/Adriana Lopez_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Emanuel Flores_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras/Emanuel Flores_cam3_2026-06-25.jpg new file mode 100644 index 0000000..83f8841 Binary files /dev/null and b/cache_nombres/auditoria_caras/Emanuel Flores_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg new file mode 100644 index 0000000..d5d0548 Binary files /dev/null and b/cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Jesus Eduardo_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras/Jesus Eduardo_cam6_2026-06-25.jpg new file mode 100644 index 0000000..1004091 Binary files /dev/null and b/cache_nombres/auditoria_caras/Jesus Eduardo_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam3_2026-06-25.jpg new file mode 100644 index 0000000..e459127 Binary files /dev/null and b/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam7_2026-06-25.jpg new file mode 100644 index 0000000..356313e Binary files /dev/null and b/cache_nombres/auditoria_caras/Jose Luis Tenchil_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam5_2026-06-25.jpg new file mode 100644 index 0000000..8c6e97c Binary files /dev/null and b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam6_2026-06-25.jpg new file mode 100644 index 0000000..b825147 Binary files /dev/null and b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam7_2026-06-25.jpg new file mode 100644 index 0000000..dbcde28 Binary files /dev/null and b/cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras1/100_cam6.jpg b/cache_nombres/auditoria_caras1/100_cam6.jpg new file mode 100644 index 0000000..56e2b5c Binary files /dev/null and b/cache_nombres/auditoria_caras1/100_cam6.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam1.jpg b/cache_nombres/auditoria_caras1/101_cam1.jpg new file mode 100644 index 0000000..ea3fd96 Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam1.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam3.jpg b/cache_nombres/auditoria_caras1/101_cam3.jpg new file mode 100644 index 0000000..c91114d Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam3.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam5.jpg b/cache_nombres/auditoria_caras1/101_cam5.jpg new file mode 100644 index 0000000..899baa8 Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam5.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam6.jpg b/cache_nombres/auditoria_caras1/101_cam6.jpg new file mode 100644 index 0000000..b98730d Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam6.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam7.jpg b/cache_nombres/auditoria_caras1/101_cam7.jpg new file mode 100644 index 0000000..b1e51c2 Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/101_cam8.jpg b/cache_nombres/auditoria_caras1/101_cam8.jpg new file mode 100644 index 0000000..ef894e9 Binary files /dev/null and b/cache_nombres/auditoria_caras1/101_cam8.jpg differ diff --git a/cache_nombres/auditoria_caras1/102_cam5.jpg b/cache_nombres/auditoria_caras1/102_cam5.jpg new file mode 100644 index 0000000..657e956 Binary files /dev/null and b/cache_nombres/auditoria_caras1/102_cam5.jpg differ diff --git a/cache_nombres/auditoria_caras1/102_cam7.jpg b/cache_nombres/auditoria_caras1/102_cam7.jpg new file mode 100644 index 0000000..2f223c2 Binary files /dev/null and b/cache_nombres/auditoria_caras1/102_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/103_cam3.jpg b/cache_nombres/auditoria_caras1/103_cam3.jpg new file mode 100644 index 0000000..a42ced6 Binary files /dev/null and b/cache_nombres/auditoria_caras1/103_cam3.jpg differ diff --git a/cache_nombres/auditoria_caras1/103_cam5.jpg b/cache_nombres/auditoria_caras1/103_cam5.jpg new file mode 100644 index 0000000..588a30f Binary files /dev/null and b/cache_nombres/auditoria_caras1/103_cam5.jpg differ diff --git a/cache_nombres/auditoria_caras1/103_cam6.jpg b/cache_nombres/auditoria_caras1/103_cam6.jpg new file mode 100644 index 0000000..9c0e091 Binary files /dev/null and b/cache_nombres/auditoria_caras1/103_cam6.jpg differ diff --git a/cache_nombres/auditoria_caras1/103_cam7.jpg b/cache_nombres/auditoria_caras1/103_cam7.jpg new file mode 100644 index 0000000..c0fb02a Binary files /dev/null and b/cache_nombres/auditoria_caras1/103_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/104_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras1/104_cam5_2026-06-24.jpg new file mode 100644 index 0000000..743b98e Binary files /dev/null and b/cache_nombres/auditoria_caras1/104_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/104_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras1/104_cam6_2026-06-24.jpg new file mode 100644 index 0000000..ab3492b Binary files /dev/null and b/cache_nombres/auditoria_caras1/104_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/105_cam3.jpg b/cache_nombres/auditoria_caras1/105_cam3.jpg new file mode 100644 index 0000000..959f2bf Binary files /dev/null and b/cache_nombres/auditoria_caras1/105_cam3.jpg differ diff --git a/cache_nombres/auditoria_caras1/105_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/105_cam3_2026-06-24.jpg new file mode 100644 index 0000000..29fc779 Binary files /dev/null and b/cache_nombres/auditoria_caras1/105_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/105_cam6.jpg b/cache_nombres/auditoria_caras1/105_cam6.jpg new file mode 100644 index 0000000..380fde7 Binary files /dev/null and b/cache_nombres/auditoria_caras1/105_cam6.jpg differ diff --git a/cache_nombres/auditoria_caras1/106_cam3.jpg b/cache_nombres/auditoria_caras1/106_cam3.jpg new file mode 100644 index 0000000..fc8cd24 Binary files /dev/null and b/cache_nombres/auditoria_caras1/106_cam3.jpg differ diff --git a/cache_nombres/auditoria_caras1/106_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras1/106_cam6_2026-06-24.jpg new file mode 100644 index 0000000..d2b3509 Binary files /dev/null and b/cache_nombres/auditoria_caras1/106_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/106_cam8_2026-06-24.jpg b/cache_nombres/auditoria_caras1/106_cam8_2026-06-24.jpg new file mode 100644 index 0000000..41e7a2d Binary files /dev/null and b/cache_nombres/auditoria_caras1/106_cam8_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/107_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/107_cam3_2026-06-24.jpg new file mode 100644 index 0000000..55eb809 Binary files /dev/null and b/cache_nombres/auditoria_caras1/107_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/107_cam5.jpg b/cache_nombres/auditoria_caras1/107_cam5.jpg new file mode 100644 index 0000000..3f2280f Binary files /dev/null and b/cache_nombres/auditoria_caras1/107_cam5.jpg differ diff --git a/cache_nombres/auditoria_caras1/107_cam7.jpg b/cache_nombres/auditoria_caras1/107_cam7.jpg new file mode 100644 index 0000000..783d35c Binary files /dev/null and b/cache_nombres/auditoria_caras1/107_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/111_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras1/111_cam7_2026-06-24.jpg new file mode 100644 index 0000000..334195e Binary files /dev/null and b/cache_nombres/auditoria_caras1/111_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/113.jpg b/cache_nombres/auditoria_caras1/113.jpg new file mode 100644 index 0000000..a719550 Binary files /dev/null and b/cache_nombres/auditoria_caras1/113.jpg differ diff --git a/cache_nombres/auditoria_caras1/114_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras1/114_cam7_2026-06-24.jpg new file mode 100644 index 0000000..2433811 Binary files /dev/null and b/cache_nombres/auditoria_caras1/114_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/117_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/117_cam3_2026-06-24.jpg new file mode 100644 index 0000000..a0dbb37 Binary files /dev/null and b/cache_nombres/auditoria_caras1/117_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/119_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras1/119_cam5_2026-06-24.jpg new file mode 100644 index 0000000..17bb2ea Binary files /dev/null and b/cache_nombres/auditoria_caras1/119_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Brayan Mendoza_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Brayan Mendoza_cam6_2026-06-24.jpg new file mode 100644 index 0000000..011b149 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Brayan Mendoza_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Fernando_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Fernando_cam3_2026-06-24.jpg new file mode 100644 index 0000000..63f61d6 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Fernando_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Jair Gregorio_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Jair Gregorio_cam3_2026-06-24.jpg new file mode 100644 index 0000000..240bc4b Binary files /dev/null and b/cache_nombres/auditoria_caras1/Jair Gregorio_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Oscar Atriano Ponce_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Oscar Atriano Ponce_cam7_2026-06-24.jpg new file mode 100644 index 0000000..9f374cc Binary files /dev/null and b/cache_nombres/auditoria_caras1/Oscar Atriano Ponce_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Oscar_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Oscar_cam1_2026-06-24.jpg new file mode 100644 index 0000000..107f6c3 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Oscar_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rafael.jpg b/cache_nombres/auditoria_caras1/Rafael.jpg new file mode 100644 index 0000000..01b65c8 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rafael.jpg differ diff --git a/cache_nombres/auditoria_caras1/Raul Sanchez_cam7.jpg b/cache_nombres/auditoria_caras1/Raul Sanchez_cam7.jpg new file mode 100644 index 0000000..9fd95ed Binary files /dev/null and b/cache_nombres/auditoria_caras1/Raul Sanchez_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C.jpg b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C.jpg new file mode 100644 index 0000000..49f494a Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam3.jpg b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam3.jpg new file mode 100644 index 0000000..7d9a610 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam3.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam6.jpg b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam6.jpg new file mode 100644 index 0000000..c3d97c1 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam6.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam7.jpg b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam7.jpg new file mode 100644 index 0000000..aa79cf0 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rodrigo Cahuantzi C_cam7.jpg differ diff --git a/cache_nombres/auditoria_caras1/Rubisela Barrientos_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras1/Rubisela Barrientos_cam1_2026-06-24.jpg new file mode 100644 index 0000000..913a0b8 Binary files /dev/null and b/cache_nombres/auditoria_caras1/Rubisela Barrientos_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras1/lore_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras1/lore_cam3_2026-06-24.jpg new file mode 100644 index 0000000..82a649d Binary files /dev/null and b/cache_nombres/auditoria_caras1/lore_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/100_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/100_cam1_2026-06-24.jpg new file mode 100644 index 0000000..5958110 Binary files /dev/null and b/cache_nombres/auditoria_caras2/100_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/101_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/101_cam1_2026-06-25.jpg new file mode 100644 index 0000000..1595725 Binary files /dev/null and b/cache_nombres/auditoria_caras2/101_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/102_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/102_cam1_2026-06-24.jpg new file mode 100644 index 0000000..81d7f65 Binary files /dev/null and b/cache_nombres/auditoria_caras2/102_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/102_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/102_cam7_2026-06-25.jpg new file mode 100644 index 0000000..26da512 Binary files /dev/null and b/cache_nombres/auditoria_caras2/102_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/103_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras2/103_cam5_2026-06-24.jpg new file mode 100644 index 0000000..fa8d752 Binary files /dev/null and b/cache_nombres/auditoria_caras2/103_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/103_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/103_cam5_2026-06-25.jpg new file mode 100644 index 0000000..51827ca Binary files /dev/null and b/cache_nombres/auditoria_caras2/103_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/104_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/104_cam1_2026-06-24.jpg new file mode 100644 index 0000000..dd21a07 Binary files /dev/null and b/cache_nombres/auditoria_caras2/104_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/104_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/104_cam3_2026-06-25.jpg new file mode 100644 index 0000000..b528ec6 Binary files /dev/null and b/cache_nombres/auditoria_caras2/104_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/104_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras2/104_cam6_2026-06-24.jpg new file mode 100644 index 0000000..e176559 Binary files /dev/null and b/cache_nombres/auditoria_caras2/104_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/104_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/104_cam6_2026-06-25.jpg new file mode 100644 index 0000000..fcb5773 Binary files /dev/null and b/cache_nombres/auditoria_caras2/104_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/104_cam8_2026-06-24.jpg b/cache_nombres/auditoria_caras2/104_cam8_2026-06-24.jpg new file mode 100644 index 0000000..3d115ea Binary files /dev/null and b/cache_nombres/auditoria_caras2/104_cam8_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/105_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras2/105_cam5_2026-06-24.jpg new file mode 100644 index 0000000..f76d0b3 Binary files /dev/null and b/cache_nombres/auditoria_caras2/105_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/106_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/106_cam3_2026-06-25.jpg new file mode 100644 index 0000000..7f8a0d8 Binary files /dev/null and b/cache_nombres/auditoria_caras2/106_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/107_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras2/107_cam3_2026-06-24.jpg new file mode 100644 index 0000000..3f4b6da Binary files /dev/null and b/cache_nombres/auditoria_caras2/107_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/107_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/107_cam6_2026-06-25.jpg new file mode 100644 index 0000000..fee7299 Binary files /dev/null and b/cache_nombres/auditoria_caras2/107_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/107_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/107_cam7_2026-06-24.jpg new file mode 100644 index 0000000..af7882e Binary files /dev/null and b/cache_nombres/auditoria_caras2/107_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/107_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras2/107_cam8_2026-06-25.jpg new file mode 100644 index 0000000..e4d077d Binary files /dev/null and b/cache_nombres/auditoria_caras2/107_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/108_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/108_cam1_2026-06-25.jpg new file mode 100644 index 0000000..191085a Binary files /dev/null and b/cache_nombres/auditoria_caras2/108_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/110_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/110_cam7_2026-06-25.jpg new file mode 100644 index 0000000..b798a6b Binary files /dev/null and b/cache_nombres/auditoria_caras2/110_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/111_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras2/111_cam6_2026-06-24.jpg new file mode 100644 index 0000000..2449568 Binary files /dev/null and b/cache_nombres/auditoria_caras2/111_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/113_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/113_cam7_2026-06-24.jpg new file mode 100644 index 0000000..021e44e Binary files /dev/null and b/cache_nombres/auditoria_caras2/113_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/113_cam8_2026-06-24.jpg b/cache_nombres/auditoria_caras2/113_cam8_2026-06-24.jpg new file mode 100644 index 0000000..8242021 Binary files /dev/null and b/cache_nombres/auditoria_caras2/113_cam8_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/114_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/114_cam7_2026-06-25.jpg new file mode 100644 index 0000000..222ed9a Binary files /dev/null and b/cache_nombres/auditoria_caras2/114_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/116_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras2/116_cam3_2026-06-24.jpg new file mode 100644 index 0000000..cef5065 Binary files /dev/null and b/cache_nombres/auditoria_caras2/116_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/118_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/118_cam7_2026-06-24.jpg new file mode 100644 index 0000000..8e34d1b Binary files /dev/null and b/cache_nombres/auditoria_caras2/118_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/119_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras2/119_cam6_2026-06-24.jpg new file mode 100644 index 0000000..42057ae Binary files /dev/null and b/cache_nombres/auditoria_caras2/119_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/123_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/123_cam1_2026-06-24.jpg new file mode 100644 index 0000000..d59430f Binary files /dev/null and b/cache_nombres/auditoria_caras2/123_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/124_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras2/124_cam5_2026-06-24.jpg new file mode 100644 index 0000000..4fbaa91 Binary files /dev/null and b/cache_nombres/auditoria_caras2/124_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/125_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/125_cam1_2026-06-24.jpg new file mode 100644 index 0000000..607ee75 Binary files /dev/null and b/cache_nombres/auditoria_caras2/125_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/125_cam8_2026-06-24.jpg b/cache_nombres/auditoria_caras2/125_cam8_2026-06-24.jpg new file mode 100644 index 0000000..74ee0ad Binary files /dev/null and b/cache_nombres/auditoria_caras2/125_cam8_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/126_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras2/126_cam6_2026-06-24.jpg new file mode 100644 index 0000000..8f79e9c Binary files /dev/null and b/cache_nombres/auditoria_caras2/126_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/163_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/163_cam5_2026-06-25.jpg new file mode 100644 index 0000000..fcba1c3 Binary files /dev/null and b/cache_nombres/auditoria_caras2/163_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/164_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/164_cam3_2026-06-25.jpg new file mode 100644 index 0000000..8d591c7 Binary files /dev/null and b/cache_nombres/auditoria_caras2/164_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-24.jpg new file mode 100644 index 0000000..4f5eb3b Binary files /dev/null and b/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-25.jpg new file mode 100644 index 0000000..c383ac9 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Adriana Lopez_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Alfredo Martinez_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Alfredo Martinez_cam5_2026-06-25.jpg new file mode 100644 index 0000000..295a158 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Alfredo Martinez_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Cristian Lopez Garcia_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Cristian Lopez Garcia_cam3_2026-06-25.jpg new file mode 100644 index 0000000..028f967 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Cristian Lopez Garcia_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-24.jpg new file mode 100644 index 0000000..db95cc2 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-25.jpg new file mode 100644 index 0000000..3b1dc57 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam3_2026-06-25.jpg new file mode 100644 index 0000000..8fe6d10 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam5_2026-06-25.jpg new file mode 100644 index 0000000..08aa3a6 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam6_2026-06-25.jpg new file mode 100644 index 0000000..afe0919 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam7_2026-06-25.jpg new file mode 100644 index 0000000..ca37dbb Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Emanuel Flores_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Emanuel Flores_cam8_2026-06-25.jpg new file mode 100644 index 0000000..691aeb6 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Emanuel Flores_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam1_2026-06-25.jpg new file mode 100644 index 0000000..8b0c32a Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam3_2026-06-25.jpg new file mode 100644 index 0000000..8008a6b Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-24.jpg new file mode 100644 index 0000000..ba7fce7 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-25.jpg new file mode 100644 index 0000000..6a76bc1 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam6_2026-06-25.jpg new file mode 100644 index 0000000..9ce4dc1 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-24.jpg new file mode 100644 index 0000000..1629bca Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-25.jpg new file mode 100644 index 0000000..e5296ef Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-24.jpg new file mode 100644 index 0000000..2a1688c Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-25.jpg new file mode 100644 index 0000000..02c66df Binary files /dev/null and b/cache_nombres/auditoria_caras2/Fernando_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jair Gregorio_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Jair Gregorio_cam7_2026-06-25.jpg new file mode 100644 index 0000000..c1518d4 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jair Gregorio_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jesus Eduardo_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Jesus Eduardo_cam3_2026-06-25.jpg new file mode 100644 index 0000000..1722890 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jesus Eduardo_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jose Luis Tenchil_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Jose Luis Tenchil_cam5_2026-06-25.jpg new file mode 100644 index 0000000..663b6c5 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jose Luis Tenchil_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jose Luis_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Jose Luis_cam7_2026-06-24.jpg new file mode 100644 index 0000000..ad5078a Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jose Luis_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam5_2026-06-25.jpg new file mode 100644 index 0000000..251a46f Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam7_2026-06-25.jpg new file mode 100644 index 0000000..45ce52e Binary files /dev/null and b/cache_nombres/auditoria_caras2/Jose Miguel Albarado_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam1_2026-06-25.jpg new file mode 100644 index 0000000..ea0276b Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-24.jpg new file mode 100644 index 0000000..4257976 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-25.jpg new file mode 100644 index 0000000..4c7012f Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam3_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam5_2026-06-25.jpg new file mode 100644 index 0000000..56c2b9a Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam6_2026-06-25.jpg new file mode 100644 index 0000000..bfc6378 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Josue Muñoz_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Josue Muñoz_cam7_2026-06-25.jpg new file mode 100644 index 0000000..001ec85 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Josue Muñoz_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Miguel Angel Sanchez Papalotzi_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Miguel Angel Sanchez Papalotzi_cam5_2026-06-25.jpg new file mode 100644 index 0000000..655ddce Binary files /dev/null and b/cache_nombres/auditoria_caras2/Miguel Angel Sanchez Papalotzi_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Omar_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Omar_cam7_2026-06-24.jpg new file mode 100644 index 0000000..b73c95f Binary files /dev/null and b/cache_nombres/auditoria_caras2/Omar_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam1_2026-06-25.jpg new file mode 100644 index 0000000..e593e24 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam5_2026-06-25.jpg new file mode 100644 index 0000000..2be4ca9 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam7_2026-06-25.jpg new file mode 100644 index 0000000..69de773 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam8_2026-06-25.jpg new file mode 100644 index 0000000..e4397dd Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar Atriano Ponce_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar_cam5_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar_cam5_2026-06-25.jpg new file mode 100644 index 0000000..9c59548 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar_cam5_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar_cam6_2026-06-25.jpg new file mode 100644 index 0000000..f0e8df6 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar_cam7_2026-06-25.jpg new file mode 100644 index 0000000..fdaa9c8 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Oscar_cam8_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Oscar_cam8_2026-06-25.jpg new file mode 100644 index 0000000..4fd4bcb Binary files /dev/null and b/cache_nombres/auditoria_caras2/Oscar_cam8_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Rodrigo Cahuantzi C_cam6_2026-06-24.jpg b/cache_nombres/auditoria_caras2/Rodrigo Cahuantzi C_cam6_2026-06-24.jpg new file mode 100644 index 0000000..9653ed2 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Rodrigo Cahuantzi C_cam6_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/Sergio_cam6_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Sergio_cam6_2026-06-25.jpg new file mode 100644 index 0000000..e81f492 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Sergio_cam6_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/Sergio_cam7_2026-06-25.jpg b/cache_nombres/auditoria_caras2/Sergio_cam7_2026-06-25.jpg new file mode 100644 index 0000000..ff18801 Binary files /dev/null and b/cache_nombres/auditoria_caras2/Sergio_cam7_2026-06-25.jpg differ diff --git a/cache_nombres/auditoria_caras2/aridai montiel zistecatl_cam7_2026-06-24.jpg b/cache_nombres/auditoria_caras2/aridai montiel zistecatl_cam7_2026-06-24.jpg new file mode 100644 index 0000000..8a770fc Binary files /dev/null and b/cache_nombres/auditoria_caras2/aridai montiel zistecatl_cam7_2026-06-24.jpg differ diff --git a/cache_nombres/auditoria_caras2/lore_cam1_2026-06-25.jpg b/cache_nombres/auditoria_caras2/lore_cam1_2026-06-25.jpg new file mode 100644 index 0000000..9ade29f Binary files /dev/null and b/cache_nombres/auditoria_caras2/lore_cam1_2026-06-25.jpg differ diff --git a/cache_nombres/generos.json b/cache_nombres/generos.json index 78659c4..00c3bab 100644 --- a/cache_nombres/generos.json +++ b/cache_nombres/generos.json @@ -1 +1 @@ -{"Emanuel Flores": "Man", "Vikicar Aldana": "Woman", "Rodrigo Cahuantzi C": "Man", "Cristian Hernandez Suarez": "Man", "Omar": "Man", "Oscar Atriano Ponce_1": "Man", "Miguel Angel": "Man", "Carlos Eduardo Cuamatzi": "Man", "Rosa maria": "Woman", "Ximena": "Woman", "Ana Karen Guerrero": "Woman", "Yuriel": "Man", "Diana Laura": "Woman", "Diana Laura Tecpa": "Woman", "aridai montiel zistecatl": "Woman", "Aridai montiel": "Woman", "Vikicar": "Woman", "Ian Axel": "Man", "Rafael": "Man", "Rubisela Barrientos": "Woman", "ian axel": "Man", "Adriana Lopez": "Woman", "Oscar Atriano Ponce": "Man", "Xayli Ximena": "Woman", "Victor Manuel Ocampo Mendez": "Man", "Victor": "Man"} \ No newline at end of file +{"Emanuel Flores": "Man", "Vikicar Aldana": "Woman", "Rodrigo Cahuantzi C": "Man", "Omar": "Man", "Oscar Atriano Ponce_1": "Man", "Carlos Eduardo Cuamatzi": "Man", "Rosa maria": "Woman", "Ana Karen Guerrero": "Woman", "Yuriel": "Man", "Ian Axel": "Man", "Rubisela Barrientos": "Woman", "Adriana Lopez": "Woman", "Oscar Atriano Ponce": "Man", "Xayli Ximena": "Woman", "Victor": "Man", "Wendoly": "Woman", "Bertha": "Woman", "Jesus Eduardo": "Man", "Miguel Angel Sanchez Papalotzi": "Man", "Victor Manuel Ocampo": "Man", "Raul Sanchez": "Man", "Edwin Santa ana": "Man", "Jair Gregorio": "Man", "Mileidy Perez": "Woman", "Cristian Lopez Garcia": "Man", "Josue Mu\u00f1oz": "Man", "Cristian Hernandez": "Woman", "Alfredo Martinez": "Man", "Daniel Vasquez Ramirez": "Man", "Mauricio Aguilar": "Man", "Rafael Lopez Preza": "Man", "Ana Lidia Gaspariano": "Woman", "Brayan Mendoza": "Man", "Eedwin Santa ana": "Woman", "Jose Miguel Albaradoo": "Man", "lore": "Woman", "Jose Luis Tenchil": "Woman", "Jose Miguel Albarado": "Woman", "Jose Luis": "Woman", "Abigail": "Woman", "Judith": "Woman", "Martha": "Woman", "Martha_1": "Woman", "Yoseimy": "Woman", "Cintia": "Woman", "Leticia": "Woman", "Fernando": "Woman", "Sergio": "Woman", "Xayli Ximena_1": "Woman", "Aridai Montiel _1": "Woman", "Ian Axel_1": "Woman", "Cristian Hernandez_1": "Woman", "Vikicar Aldana_1": "Woman", "Victor Manuel Ocampo_1": "Woman", "Rafael Lopez Preza_1": "Woman", "Aridai Montiel": "Woman"} \ No newline at end of file diff --git a/cache_nombres/memoria_ids.json b/cache_nombres/memoria_ids.json new file mode 100644 index 0000000..4015598 --- /dev/null +++ b/cache_nombres/memoria_ids.json @@ -0,0 +1 @@ +{"100": {"galeria_deep": [[0.0007756247068755329, 0.07523926347494125, 0.0, 0.0, 0.06352678686380386, 0.06728233397006989, 0.0343339666724205, 0.04311477765440941, 0.003607881488278508, 0.034833360463380814, 0.0, 0.07554744184017181, 0.053316630423069, 0.010681545361876488, 0.0, 0.006426693871617317, 0.03696855157613754, 0.03047468699514866, 0.007078243885189295, 0.01709827408194542, 0.0395125113427639, 0.0, 0.08719927072525024, 0.0414666123688221, 0.08514876663684845, 0.07740306109189987, 0.0003666872507892549, 0.009646364487707615, 0.0218186192214489, 0.06295068562030792, 0.010821892879903316, 0.0, 0.0, 0.01641269400715828, 0.05748259276151657, 0.03624500706791878, 0.022549092769622803, 0.017670586705207825, 0.09184975177049637, 0.0976821556687355, 0.04992065578699112, 0.0, 0.011741571128368378, 0.0, 0.03050464205443859, 0.02333035320043564, 0.0029198015108704567, 0.11957036703824997, 0.03564629703760147, 0.0, 0.0, 0.05349459871649742, 0.0, 0.0, 0.08351349085569382, 0.06895405054092407, 0.0, 0.17599144577980042, 0.0, 0.05616419389843941, 0.041744980961084366, 0.017395829781889915, 0.0, 0.0, 0.0036705571692436934, 0.04623759165406227, 0.04610023647546768, 0.0093178516253829, 0.0, 0.0, 0.056115586310625076, 0.0, 0.0, 0.0, 0.0, 0.006890125572681427, 0.02763885259628296, 0.0, 0.018765658140182495, 0.006405083928257227, 0.17945268750190735, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.055002253502607346, 0.0, 0.03845798596739769, 0.0, 0.037928372621536255, 0.0, 0.06324571371078491, 0.0, 0.0, 0.003091202350333333, 0.0009739461820572615, 0.0, 0.06876027584075928, 0.0, 0.024196328595280647, 0.06509821861982346, 0.0, 0.0, 0.0, 0.04558701068162918, 0.0, 0.07967596501111984, 0.0, 0.0, 0.0, 0.06002606078982353, 0.0, 0.0, 0.02841086871922016, 0.026804283261299133, 0.042316749691963196, 0.0, 0.01751444861292839, 0.0, 0.056225549429655075, 0.0, 0.054703596979379654, 0.0, 0.012803048826754093, 0.0016488551627844572, 0.0, 0.100243479013443, 0.0, 0.0, 0.0, 0.008072897791862488, 0.0, 0.036550622433423996, 0.00030690321000292897, 0.06182991340756416, 0.0, 0.03240085393190384, 0.0650952160358429, 0.0, 0.03206874430179596, 0.0, 0.0017320435727015138, 0.08128206431865692, 0.08936860412359238, 0.0, 0.0, 0.0547938197851181, 0.0, 0.0, 0.054994259029626846, 0.0, 0.0, 0.005784756503999233, 0.02230566181242466, 0.050092343240976334, 0.08733353018760681, 0.09619789570569992, 0.0, 0.0, 0.0, 0.09009328484535217, 0.002475749235600233, 0.0, 0.029285980388522148, 0.03598109260201454, 0.018279630690813065, 0.0, 0.0, 0.019365645945072174, 0.005605562590062618, 0.0, 0.007624468766152859, 0.0, 0.008048494346439838, 0.0, 0.0, 0.03233081102371216, 0.04117491841316223, 0.018155911937355995, 0.05467529594898224, 0.09050442278385162, 0.047616567462682724, 0.023717882111668587, 0.0019571403972804546, 0.05899913236498833, 0.0, 0.05063536763191223, 0.0, 0.0, 0.0, 0.02697407826781273, 0.020651068538427353, 0.09744293242692947, 0.0, 4.2214334825985134e-05, 0.09213877469301224, 0.006226751487702131, 0.05851738899946213, 0.05051378533244133, 0.11754947155714035, 0.0, 0.0578872412443161, 0.0, 0.0, 0.0006828558980487287, 0.0, 0.042052216827869415, 0.0, 0.10856305062770844, 0.0, 0.0, 0.057296283543109894, 0.036264754831790924, 0.11772807687520981, 0.03984122350811958, 0.0015550939133390784, 0.061598025262355804, 0.0, 0.034893687814474106, 0.09797047823667526, 0.0, 0.0227202195674181, 0.04576612263917923, 0.14104001224040985, 0.10456576943397522, 0.0012766593135893345, 0.06650672852993011, 0.0, 0.029972592368721962, 0.0, 0.0, 0.0, 0.0, 0.11788832396268845, 0.0, 0.00460422458127141, 0.007355911191552877, 0.058183662593364716, 0.08028358966112137, 0.04002523049712181, 0.03790492191910744, 0.0, 0.04696797579526901, 0.0, 0.03275999426841736, 0.024136899039149284, 0.0, 0.07797089964151382, 0.0022875573486089706, 0.010569710284471512, 0.0, 0.08610828965902328, 0.09959892183542252, 0.0, 0.06180938705801964, 0.02473422698676586, 0.0361466109752655, 0.02875639870762825, 0.04193829372525215, 0.013437984511256218, 0.062464747577905655, 0.0, 0.0004444307996891439, 0.0771310031414032, 0.0, 0.04509371519088745, 0.06745009869337082, 0.0, 0.02118641883134842, 0.0, 0.008552911691367626, 0.03632049262523651, 0.0, 0.0, 0.017068471759557724, 0.08996690809726715, 0.08269556611776352, 0.0, 0.034546755254268646, 0.0, 0.018105026334524155, 0.1124783530831337, 0.0, 0.05570222809910774, 0.0, 0.060028135776519775, 0.00094537966651842, 0.0, 0.0, 0.07340942323207855, 0.06341014802455902, 0.046446554362773895, 0.05082036182284355, 0.0004277795960661024, 0.0, 0.09252361953258514, 0.0, 0.004808245692402124, 0.020798254758119583, 0.07626776397228241, 0.08730863034725189, 0.0, 0.017567958682775497, 0.03799521178007126, 0.0, 0.013093335554003716, 0.0, 0.05394043028354645, 0.012359483167529106, 0.05419940501451492, 0.01630464196205139, 0.03461474925279617, 0.13652969896793365, 0.0, 0.0, 0.031059781089425087, 0.0, 0.0, 0.0, 0.029033470898866653, 0.09002247452735901, 0.0, 0.1649572253227234, 0.05038075894117355, 0.020163623616099358, 0.05104552209377289, 0.004173334687948227, 0.035534195601940155, 0.0, 0.10697478801012039, 0.014094311743974686, 0.04195960611104965, 0.09793013334274292, 0.0, 0.0006537561421282589, 0.005783979315310717, 0.0, 0.0013654876966029406, 0.10963118076324463, 0.02242618426680565, 0.0, 0.0, 0.0, 0.0, 0.0, 0.028417430818080902, 0.0, 0.020586468279361725, 0.0, 0.0, 0.0, 0.0, 0.00961475819349289, 0.0, 0.01926172897219658, 0.0, 0.0, 0.0, 0.01832626201212406, 0.026758437976241112, 0.0, 0.013101361691951752, 0.0, 0.0006247679120860994, 0.017147121950984, 0.0, 0.09976564347743988, 0.0, 0.06810528039932251, 0.0, 0.0, 0.0, 0.0016844253987073898, 0.057653602212667465, 0.0, 0.07109750062227249, 0.055926255881786346, 0.058049075305461884, 0.0, 0.12249602377414703, 0.019769897684454918, 0.02339661307632923, 0.004142691381275654, 0.0, 0.0, 0.0, 0.0, 0.0, 0.006376393139362335, 0.03563988208770752, 0.004871964920312166, 0.00015140912728384137, 0.0, 0.0, 0.0, 0.07123470306396484, 0.0, 0.0, 0.0, 0.1703227460384369, 0.05623645335435867, 0.06041965261101723, 0.04734151065349579, 0.01976088620722294, 0.0, 0.0, 0.0, 0.003707153955474496, 0.0, 0.0, 0.0, 0.10302101075649261, 0.04584950953722, 0.08885154873132706, 0.0, 0.008155690506100655, 0.0, 0.0, 0.017999470233917236, 0.11803174018859863, 0.0, 0.029371002689003944, 0.014784574508666992, 0.07557599246501923, 0.05654589459300041, 0.0, 0.002376961288973689, 0.0, 0.06650203466415405, 0.0006795708904974163, 0.004372467286884785, 0.0, 0.0, 0.0, 0.0005185758927837014, 0.0, 0.0, 0.0168735533952713, 0.005008733831346035, 0.0, 0.0, 0.09043114632368088, 0.0, 0.0, 0.09507762640714645, 0.015831518918275833, 0.002230654237791896, 0.0, 0.10689400881528854, 0.0, 0.0, 0.0, 0.02428283728659153, 0.0, 0.08405730128288269, 0.05604316294193268, 0.007325305137783289, 0.0, 0.0, 0.029536517336964607, 0.02481449767947197, 0.10149305313825607, 0.0, 0.0, 0.0, 0.0, 0.02148423157632351, 0.0, 0.0010346383787691593, 0.0, 0.0, 0.0, 0.0, 0.06377290189266205, 0.053876493126153946, 0.04181045666337013, 0.008047950454056263, 0.0, 0.0, 0.020448239520192146, 0.001149773714132607, 0.009438999928534031, 0.0, 0.0333813801407814, 0.035399798303842545, 0.0017065665451809764, 0.0, 0.0, 0.0, 0.05624059960246086, 0.012649383395910263, 0.0, 0.000963774393312633, 0.00017594716337043792, 0.0, 0.03273020684719086, 0.0, 0.052631378173828125, 0.1778833270072937, 0.016365578398108482, 0.0, 0.030120283365249634, 0.1359178125858307, 0.0, 0.014764497056603432, 0.0, 0.060786109417676926, 0.09315519034862518], [0.03801485896110535, 0.058776695281267166, 0.002970317378640175, 0.0, 0.10350173711776733, 0.0473068505525589, 0.0, 0.05324600264430046, 0.020385175943374634, 0.02136458456516266, 0.0, 0.09472449868917465, 0.016360171139240265, 0.015263967216014862, 0.0, 0.0, 0.04701421409845352, 0.01683516427874565, 0.008468248881399632, 0.0, 0.017493801191449165, 0.003757598577067256, 0.0, 0.018118124455213547, 0.025289759039878845, 0.0671956017613411, 0.022359244525432587, 0.0487544946372509, 0.028112897649407387, 0.057953864336013794, 0.01876492239534855, 0.051067907363176346, 0.0, 0.07338593155145645, 0.01772249862551689, 0.0, 0.0, 0.0, 0.07803121209144592, 0.11971308290958405, 0.0, 0.0, 0.032844990491867065, 0.0, 0.031609710305929184, 0.021326208487153053, 0.0572136789560318, 0.14478977024555206, 0.003169003641232848, 0.0, 0.0, 0.09971736371517181, 0.0, 0.0, 0.12211553752422333, 0.11453519761562347, 0.0059648375026881695, 0.1891109198331833, 0.004469619132578373, 0.016020746901631355, 0.0373697467148304, 0.008453387767076492, 0.0, 0.0, 0.021512694656848907, 0.08112689107656479, 0.07892804592847824, 0.005151414778083563, 0.0, 0.03035624511539936, 0.03982463479042053, 0.02445288375020027, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.021052857860922813, 0.0740252286195755, 0.0, 0.0, 0.000295614154310897, 0.0, 0.0370296910405159, 0.0, 0.015243768692016602, 0.0, 0.02399897761642933, 0.0, 0.0, 0.07604340463876724, 0.07632768154144287, 0.0, 0.05096520110964775, 0.0, 0.0007937788031995296, 0.0, 0.0, 0.002441579708829522, 0.0681970864534378, 0.01597384363412857, 0.022720923647284508, 0.0, 0.02059192955493927, 0.0, 0.0002369520370848477, 0.055159810930490494, 0.0, 0.11030255258083344, 0.025947026908397675, 0.08273503929376602, 0.0, 0.02257625199854374, 0.028154470026493073, 0.0, 0.0285622701048851, 0.0033326849807053804, 0.08397482335567474, 0.046046413481235504, 0.0, 0.0, 0.10555505752563477, 0.0, 0.024278435856103897, 0.013991277664899826, 0.044359736144542694, 0.0, 0.0, 0.12855327129364014, 0.0, 0.01856773905456066, 0.03585495054721832, 0.06492318212985992, 0.012540406547486782, 0.0008305793744511902, 0.012501492165029049, 0.035672806203365326, 0.0, 0.005828903056681156, 0.0010072548175230622, 0.04147746041417122, 0.004708387888967991, 0.016820603981614113, 0.0, 0.15527379512786865, 0.12169479578733444, 0.0, 0.0, 0.006553300190716982, 0.00029271290986798704, 0.045380108058452606, 0.0, 0.0, 0.0, 0.01914876140654087, 0.026187576353549957, 0.1367223709821701, 0.08057407289743423, 0.061864592134952545, 0.0, 0.0, 0.022117024287581444, 0.03298656642436981, 0.03232823312282562, 0.0, 0.07065366953611374, 0.00044008714030496776, 0.06362215429544449, 0.020772220566868782, 0.0, 0.04485063627362251, 0.0, 0.0, 0.01791257970035076, 0.0017339669866487384, 0.0, 0.017233755439519882, 0.0, 0.0, 0.004480673931539059, 0.005487584974616766, 0.04525135084986687, 0.10587099939584732, 0.03089437447488308, 0.0050439005717635155, 0.057694725692272186, 0.0, 0.0, 0.09087683260440826, 0.0, 0.001733750686980784, 0.0, 0.011556675657629967, 0.01035462599247694, 0.029656263068318367, 0.06522668898105621, 0.0, 0.061947744339704514, 0.019642451778054237, 0.08133289217948914, 0.0, 0.04948536306619644, 0.002251808997243643, 0.06598865240812302, 0.0, 0.0, 0.02369767241179943, 0.0, 0.0055773151107132435, 0.009638861753046513, 0.0782831609249115, 0.0, 0.04798007011413574, 0.0, 0.026796387508511543, 0.1487714797258377, 0.039226654917001724, 0.002151913708075881, 0.0, 0.05600174888968468, 0.07020062953233719, 0.15121401846408844, 0.014245167374610901, 0.0019415798597037792, 0.0, 0.12444718927145004, 0.09385828673839569, 0.0, 0.021736759692430496, 0.002545709488913417, 0.0561128631234169, 0.0, 0.0034116848837584257, 0.0, 0.0, 0.039912011474370956, 0.0, 0.0014399001374840736, 0.08829115331172943, 0.010279069654643536, 0.13386979699134827, 0.01435901876538992, 0.0884082242846489, 0.0, 0.04368656128644943, 0.0, 0.004953604657202959, 0.017270991578698158, 0.0, 0.045511242002248764, 0.07117175310850143, 0.0, 0.04325385019183159, 0.07481226325035095, 0.08081905543804169, 0.0019172992324456573, 0.1128736287355423, 0.04419143125414848, 0.003993406426161528, 0.026053883135318756, 0.07569483667612076, 0.024025363847613335, 0.06289394199848175, 0.0, 0.0, 0.05397432669997215, 0.09678760915994644, 0.012229726649820805, 0.056154873222112656, 0.0, 0.03304700553417206, 0.0004002226050943136, 0.03187217563390732, 0.03773408755660057, 0.0, 0.002636735560372472, 0.01219731755554676, 0.14089281857013702, 0.030459381639957428, 0.0390574149787426, 0.0, 0.0, 0.0, 0.09533927589654922, 0.0, 0.038306668400764465, 0.0, 0.060014575719833374, 0.0, 0.0, 0.0, 0.019764643162488937, 0.04351620748639107, 0.0486406534910202, 0.10986023396253586, 0.000304404238704592, 0.024735484272241592, 0.06861207634210587, 0.0, 0.0012467049527913332, 0.0, 0.08700992166996002, 0.06061930954456329, 0.016042586416006088, 0.00030476797837764025, 0.0, 0.0, 0.003139086300507188, 0.0, 0.012902961112558842, 0.0586976557970047, 0.009037083014845848, 6.780971307307482e-05, 0.002800640184432268, 0.06164746358990669, 0.002291215118020773, 0.0049368212930858135, 0.012098081409931183, 0.0, 0.0009178908076137304, 0.0, 4.861821071244776e-05, 0.030103836208581924, 0.0005680443719029427, 0.024939704686403275, 0.08028975874185562, 0.02589266560971737, 0.09775160998106003, 0.021285464987158775, 0.005482194013893604, 0.0, 0.08852788805961609, 0.08247333765029907, 0.0, 0.04467865824699402, 0.0, 0.031848303973674774, 0.05057317763566971, 0.0, 0.0006596246385015547, 0.11814151704311371, 0.027626747265458107, 0.0, 0.037197474390268326, 0.0239417664706707, 0.0, 0.0, 0.006128818728029728, 0.0, 0.000914999982342124, 0.0, 0.0007466671522706747, 0.0, 0.0, 0.024577181786298752, 0.0018647682154551148, 0.045966800302267075, 0.0, 0.0, 0.0, 0.0013390424428507686, 0.0021067396737635136, 0.0002642692707013339, 0.08445198833942413, 0.0, 0.0, 0.0, 0.0009342531557194889, 0.10781043022871017, 0.0, 0.03537558391690254, 0.0, 0.0, 0.014337888918817043, 0.0, 0.0, 0.0, 0.015264433808624744, 0.03755806386470795, 0.049171123653650284, 0.0, 0.07860264182090759, 0.019312048330903053, 0.0, 0.0005520011764019728, 0.018223462626338005, 0.025681965053081512, 0.004154992755502462, 0.0, 0.0, 0.10454661399126053, 0.0, 0.0, 0.09343250840902328, 0.0017301017651334405, 0.0009704236872494221, 0.0, 0.07155995070934296, 0.0050645554438233376, 0.014321076683700085, 0.0, 0.1175667941570282, 0.05213796719908714, 0.00045758046326227486, 0.0036562264431267977, 0.020479362457990646, 0.0, 0.014405894093215466, 0.0, 0.0, 0.02020888216793537, 0.0, 0.0, 0.15385635197162628, 0.010358802042901516, 0.03996962308883667, 0.0, 0.0, 0.0, 0.0, 0.0529211089015007, 0.046911392360925674, 0.0, 0.09122069180011749, 0.03275280445814133, 0.04383184760808945, 0.021652312949299812, 0.0, 0.03308628499507904, 0.0, 0.06984388828277588, 0.08265048265457153, 0.05208206549286842, 0.0, 0.0, 0.0, 0.023642929270863533, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009265619330108166, 0.014897928573191166, 0.0022563517559319735, 0.0, 0.043007608503103256, 0.0, 0.0, 0.0010282463626936078, 0.0867876186966896, 0.005777499172836542, 0.0, 0.0, 1.2759780474880245e-05, 0.026696979999542236, 0.0802038237452507, 0.007356258109211922, 0.00020403761300258338, 0.030366582795977592, 0.0, 0.002260497072711587, 0.010879551991820335, 0.07920811325311661, 0.0, 0.0, 0.0006828350597061217, 0.0, 0.0921153649687767, 0.0, 0.02536064013838768, 0.028188789263367653, 0.0, 0.015999184921383858, 0.0018210114212706685, 0.011963030323386192, 0.0555889867246151, 0.0419500358402729, 0.0839085727930069, 0.008392701856791973, 0.0, 0.0033422186970710754, 0.002470495644956827, 0.024197151884436607, 6.854011735413224e-05, 0.011731741949915886, 0.06576665490865707, 0.0, 0.0, 0.0010467128595337272, 0.036404531449079514, 0.05757656693458557, 0.003189833601936698, 0.0, 0.010679679922759533, 0.00019200565293431282, 0.0015135115245357156, 0.05452932417392731, 0.0, 0.0036070342175662518, 0.1416216492652893, 0.00012799317482858896, 0.027388839051127434, 0.027386007830500603, 0.12243950366973877, 0.0, 0.053306564688682556, 0.0, 0.034049492329359055, 0.06356721371412277]], "last_cam": "6", "nombre": null, "ts": 1782405099.2351289, "actualizaciones_globales": 30}, "101": {"galeria_deep": [[0.005036068148910999, 0.013228831812739372, 0.0007661394192837179, 0.0, 0.027209283784031868, 0.0049942852929234505, 0.009005557745695114, 0.007901671342551708, 0.015205412171781063, 0.0003064557386096567, 0.00910006184130907, 0.06319396942853928, 0.0, 0.0, 0.021997002884745598, 0.03325258195400238, 0.015456962399184704, 0.021177073940634727, 0.013286762870848179, 0.0, 0.05088546499609947, 0.08603036403656006, 0.0016875744331628084, 0.0, 0.006129351910203695, 0.003106361487880349, 0.0067086247727274895, 0.014096534810960293, 0.04465575888752937, 0.022699041292071342, 0.0033047664910554886, 0.030930954962968826, 0.0, 0.03547298163175583, 0.0, 0.0, 0.0035189827904105186, 0.0066852448508143425, 0.0861002579331398, 0.039559006690979004, 0.0008914473000913858, 0.054511670023202896, 0.020602291449904442, 0.0, 0.0005794336320832372, 0.004015262238681316, 0.006486796773970127, 0.004585126880556345, 0.009999381378293037, 0.0, 0.0, 0.08042576164007187, 0.03740011900663376, 0.00929438415914774, 0.2049064040184021, 0.1202898919582367, 0.05275975912809372, 0.05802189186215401, 0.016703030094504356, 0.01660067029297352, 0.016654565930366516, 0.018723564222455025, 0.0, 0.03797726333141327, 0.0, 0.06654710322618484, 0.004913349635899067, 0.020301947370171547, 0.04158449545502663, 0.00137780187651515, 0.0026343422941863537, 0.010103069245815277, 0.09389103204011917, 0.0026295173447579145, 0.09700895845890045, 0.0, 0.0, 0.0, 0.003222895786166191, 0.09675049036741257, 0.019260132685303688, 0.006960043217986822, 0.00821277592331171, 0.07988929003477097, 0.0, 0.0027019509579986334, 0.0, 0.013008634559810162, 0.049298644065856934, 0.0, 0.0, 0.0, 0.08203958719968796, 0.12050500512123108, 0.000520284113008529, 0.008587918244302273, 0.0, 0.0, 0.0, 0.06066877394914627, 0.03634079545736313, 0.042568325996398926, 0.018534677103161812, 0.0040116081945598125, 0.0, 0.0011534547666087747, 0.06788208335638046, 0.004188355058431625, 0.10368795692920685, 0.02709408476948738, 0.08281280845403671, 0.03358277678489685, 0.09628313034772873, 0.0, 0.029316574335098267, 0.008125259540975094, 0.0, 0.01252578105777502, 0.0, 0.0954766646027565, 0.04081710800528526, 0.0, 0.0012318488443270326, 0.0007308519561775029, 0.015487086959183216, 0.0017736065201461315, 0.024806056171655655, 0.05774639546871185, 0.00022407504729926586, 0.056800272315740585, 0.05844325199723244, 0.011580056510865688, 0.0, 0.0690435916185379, 0.04406505450606346, 0.001256705611012876, 0.001170655945315957, 0.007300699129700661, 0.003036167472600937, 0.08941430598497391, 0.04874173551797867, 0.0012349883327260613, 0.07801392674446106, 0.0, 0.033461399376392365, 0.0, 0.13556058704853058, 0.002411882858723402, 0.0038479731883853674, 0.0, 0.01044473797082901, 0.028725719079375267, 0.04048891365528107, 0.0, 0.0, 5.0111561904486734e-06, 0.01725115068256855, 0.031187433749437332, 0.11254946142435074, 0.0006444492028094828, 0.07721097022294998, 0.0, 0.0, 0.0, 0.0034411612432450056, 0.07740294188261032, 0.012242249213159084, 0.013917198404669762, 0.024209797382354736, 0.08991436660289764, 0.012910657562315464, 0.0, 0.04157126694917679, 0.0, 0.020256757736206055, 0.0551183708012104, 0.0, 0.007736782543361187, 0.06819295883178711, 0.030007576569914818, 0.000262997520621866, 0.06962171196937561, 0.05666113644838333, 0.004000961314886808, 0.008636928163468838, 0.037944529205560684, 0.039372194558382034, 0.045113056898117065, 0.0, 0.0, 0.10630320757627487, 0.0, 0.0, 0.014486238360404968, 0.008713824674487114, 0.013525138609111309, 0.03740626946091652, 0.007688168901950121, 0.0, 0.024472873657941818, 0.08515273779630661, 0.08060963451862335, 0.0, 0.04831571504473686, 0.0, 0.0, 0.0, 0.0042897979728877544, 0.06425509601831436, 0.0, 0.0034418106079101562, 0.07580222934484482, 0.02195901982486248, 0.0, 0.044741738587617874, 0.021693717688322067, 0.08430129289627075, 0.09020300209522247, 0.09190789610147476, 0.0, 0.0023071812465786934, 0.0, 0.058800552040338516, 0.06901153177022934, 0.023100482299923897, 0.007218169514089823, 0.0, 0.0413883738219738, 0.03207078576087952, 0.0, 0.0634242370724678, 0.04155431687831879, 0.09964866936206818, 0.018119754269719124, 0.02682434767484665, 0.0, 0.0037327799946069717, 0.030989181250333786, 0.0, 0.02083722874522209, 0.04911651834845543, 0.03881990909576416, 0.09650266915559769, 0.0534779392182827, 0.04729590564966202, 0.0, 0.04605737701058388, 0.0, 0.0599476620554924, 0.08383467048406601, 0.018970975652337074, 0.0010960728395730257, 0.10598521679639816, 0.021103087812662125, 0.05939235910773277, 0.08351656049489975, 0.0022142725065350533, 0.03503836318850517, 0.05097361281514168, 0.12440387159585953, 0.055564917623996735, 0.0, 0.028858914971351624, 0.03762052580714226, 0.10590104013681412, 0.01243199035525322, 0.0011370028369128704, 0.02735382318496704, 0.054755799472332, 0.00897541269659996, 0.04361600801348686, 0.006820692680776119, 0.01489818375557661, 0.05273859575390816, 0.03696048632264137, 0.04628828540444374, 0.04746796190738678, 0.020024403929710388, 0.03327865153551102, 0.08160649985074997, 0.08303214609622955, 0.026214657351374626, 0.0015311369206756353, 0.02263128012418747, 0.022921351715922356, 0.08219609409570694, 0.0, 0.01869863085448742, 0.0, 0.07984606921672821, 0.01983208954334259, 0.0, 8.210122905438766e-05, 0.02283325605094433, 0.07259396463632584, 0.019953133538365364, 0.13365961611270905, 0.00014745355292689055, 0.03434467315673828, 0.06502852588891983, 0.01701853610575199, 0.02514091320335865, 0.00028920709155499935, 0.05599387362599373, 0.0007090558647178113, 0.002184879034757614, 0.0, 0.0, 0.02419152669608593, 0.04304518923163414, 0.03236471489071846, 0.0009862689767032862, 0.060581304132938385, 0.07572098076343536, 0.021687129512429237, 0.043776173144578934, 0.052929122000932693, 0.008073583245277405, 0.0009180197375826538, 0.0029952682089060545, 0.007056452799588442, 0.03218322619795799, 0.0008003671537153423, 0.01135573536157608, 0.015952197834849358, 0.012520174495875835, 0.0, 0.056143853813409805, 0.040437161922454834, 0.02651357278227806, 0.005524565931409597, 0.03168513625860214, 0.0, 0.0009413589141331613, 0.13294586539268494, 0.0017611033981665969, 0.0, 0.0, 0.0, 0.001665076008066535, 0.010630778037011623, 0.06765636056661606, 0.11145743727684021, 0.00029396332683973014, 0.05140789598226547, 0.02770048938691616, 0.0, 0.0002458858070895076, 0.0, 0.0018971695099025965, 0.005549172405153513, 0.030344603583216667, 0.046836502850055695, 0.060483165085315704, 0.0, 0.028696082532405853, 0.0687229186296463, 0.09539971500635147, 0.01735100708901882, 0.02276511862874031, 0.00011982325668213889, 0.003745001507923007, 0.0, 0.0010530055733397603, 0.06485258042812347, 0.0, 0.00029407002148218453, 0.0, 0.0, 0.03449057787656784, 0.0538494698703289, 0.003857825882732868, 0.050827134400606155, 0.13157255947589874, 0.00215744087472558, 0.0, 4.57096604122853e-07, 0.005549585446715355, 0.0, 0.03210141882300377, 0.029067063704133034, 0.07095662504434586, 0.0, 0.0, 0.02343156188726425, 0.07470612227916718, 0.0, 0.008524321019649506, 0.019425002858042717, 0.0230465829372406, 0.06630115211009979, 0.015545648522675037, 0.12432500720024109, 0.07214214652776718, 0.0, 0.11967763304710388, 0.0012703328393399715, 0.06802897900342941, 0.006563369184732437, 0.05213252827525139, 0.0890432596206665, 0.0, 0.0007407329394482076, 0.08937100321054459, 0.06894905865192413, 0.0, 3.969743920606561e-05, 0.031098466366529465, 0.0, 0.0007773575489409268, 0.05193732678890228, 0.0, 0.007207084912806749, 0.0, 0.0, 0.038970544934272766, 0.0, 0.08118493854999542, 0.01486877165734768, 0.005060944240540266, 0.0, 0.019971458241343498, 0.1083856001496315, 0.008424238301813602, 0.0, 0.08618175983428955, 0.021491991356015205, 0.0658821240067482, 0.04472165182232857, 0.03416098654270172, 0.0, 0.04424256831407547, 0.029744774103164673, 0.038122281432151794, 0.003614019136875868, 0.0805177316069603, 0.0, 0.002564377849921584, 0.04057939350605011, 0.0019078137120231986, 0.0, 0.0, 0.001596354995854199, 0.011082503013312817, 0.00010364718036726117, 0.003090781392529607, 0.03394554927945137, 0.0, 0.005043416284024715, 0.0, 0.042309876531362534, 0.11613767594099045, 0.06337566673755646, 0.02724919095635414, 0.0, 0.024568011984229088, 0.04432961344718933, 0.03388995677232742, 0.028493264690041542, 0.005471395794302225, 0.006962941028177738, 0.008706851862370968, 0.00012413685908541083, 0.026652855798602104, 0.0602177157998085, 0.03594847768545151, 0.0018648537807166576, 0.0, 0.10562217235565186, 0.024445777758955956, 0.022736910730600357, 0.02861139364540577, 0.0, 0.051984746009111404, 0.0024116728454828262, 0.11047032475471497, 0.057677894830703735, 0.0, 0.01275329664349556, 0.05516242980957031, 0.054781001061201096, 0.09828668087720871, 0.0, 0.004559535067528486, 0.0008169795037247241, 0.0014908420853316784, 0.04111560434103012, 0.0, 0.07839883863925934, 0.0019913185387849808, 0.0021374537609517574, 0.0, 0.0, 0.10601574927568436, 0.002188138198107481, 0.0, 0.0, 0.059620074927806854, 0.0, 0.007794643752276897, 0.007785521447658539, 0.0018412275239825249, 0.1329372376203537, 0.051453832536935806, 0.08459850400686264, 0.0011146219912916422, 0.1201401799917221, 0.0, 0.011154614388942719, 0.008357920683920383, 0.006042884662747383, 0.035763196647167206], [0.0005771206342615187, 0.11096213757991791, 0.0, 0.0, 0.056758612394332886, 0.013664823025465012, 0.018008224666118622, 0.15926644206047058, 0.01741008087992668, 0.0, 0.0, 0.08593825250864029, 0.0006326616276055574, 0.0, 0.0016690798802301288, 0.018522225320339203, 0.0, 0.0007673432119190693, 0.015359027311205864, 0.0, 0.09513206779956818, 0.04131694510579109, 0.03089982457458973, 0.002597456332296133, 0.0013700667768716812, 0.022222476080060005, 0.07564722746610641, 0.11994513869285583, 0.041492775082588196, 0.01990133337676525, 0.10312356054782867, 0.06216878071427345, 0.0, 3.8928592402953655e-05, 0.00722303194925189, 0.003152860328555107, 0.014074080623686314, 0.017861556261777878, 0.09670409560203552, 0.12097027897834778, 0.003695633029565215, 0.06927323341369629, 0.06732901930809021, 0.0006977267330512404, 0.02447068691253662, 0.0019957786425948143, 0.047207314521074295, 0.050211101770401, 0.05237312614917755, 0.0009926343336701393, 0.0, 0.0874774232506752, 0.008704130537807941, 0.0, 0.13824963569641113, 0.11306639015674591, 0.021702729165554047, 0.09434375911951065, 0.00940253958106041, 0.05484609305858612, 0.0, 0.002588947769254446, 0.01487109437584877, 0.0, 0.0022449837997555733, 0.0, 0.03499436751008034, 0.004192069172859192, 0.01941065490245819, 0.00641661137342453, 0.04306444153189659, 0.006362659856677055, 0.007922809571027756, 0.00891401432454586, 0.0, 0.001653984421864152, 0.0, 0.0, 0.0008614115067757666, 0.0, 0.0074903760105371475, 0.0, 0.0, 0.01726454310119152, 0.0, 0.0, 0.019294654950499535, 0.05095865577459335, 0.0, 0.0, 0.0027616031002253294, 0.002191984560340643, 0.06205699220299721, 0.13405421376228333, 0.0, 0.05512236803770065, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0022406394127756357, 0.007843272760510445, 0.0766940489411354, 0.09508922696113586, 0.012581372633576393, 0.014304665848612785, 0.0, 0.01930323988199234, 0.0014841988449916244, 0.019005021080374718, 0.0009038152638822794, 0.06286076456308365, 0.0, 0.03572673723101616, 0.06558828800916672, 0.0, 0.06606274843215942, 0.0070419395342469215, 0.0, 0.0, 0.0, 0.01222830731421709, 0.026759864762425423, 0.0023317637387663126, 0.010310987941920757, 0.038419194519519806, 0.03904514014720917, 0.001960839843377471, 0.0006871746154502034, 0.10069068521261215, 0.0, 0.04839278757572174, 0.03450526297092438, 0.0009896695846691728, 0.010819317772984505, 0.00691669387742877, 0.00586765306070447, 0.048227742314338684, 0.013546932488679886, 0.0, 0.016217656433582306, 0.027464957907795906, 0.0, 0.03309817239642143, 0.040723059326410294, 0.12475833296775818, 0.10462314635515213, 0.0, 0.0, 0.024131134152412415, 0.009403335861861706, 0.0624641552567482, 0.00527974171563983, 0.06701924651861191, 0.002260074019432068, 0.049251895397901535, 0.004094715695828199, 0.07239504158496857, 0.029367556795477867, 0.07410545647144318, 0.0, 0.03397171571850777, 0.0, 0.011828540824353695, 0.03677118197083473, 0.0, 0.11368106305599213, 0.0032363771460950375, 0.03331255167722702, 0.027263374999165535, 0.0, 0.0, 0.002067621098831296, 0.019948771223425865, 0.05073932930827141, 6.390589260263368e-05, 0.02320457063615322, 0.06423252820968628, 0.021678514778614044, 0.00600203825160861, 0.029815921559929848, 0.0016139163635671139, 0.0890488550066948, 0.010927419178187847, 0.08813157677650452, 0.02872426249086857, 0.002009996911510825, 0.004579047206789255, 0.0, 0.09846830368041992, 0.0, 0.0015555134741589427, 0.015895063057541847, 0.0, 0.02176073007285595, 0.05796157941222191, 0.0519975982606411, 0.0012172943679615855, 0.008814981207251549, 0.08059782534837723, 0.06637585163116455, 0.0006863432936370373, 0.058365192264318466, 0.029099442064762115, 0.0006822461728006601, 0.0022294949740171432, 0.0, 0.029638562351465225, 0.0, 0.0, 0.10079587250947952, 0.03620025888085365, 0.0, 0.05159507319331169, 0.05955608934164047, 0.004881501663476229, 0.09538833796977997, 0.11880733072757721, 0.0008737917523831129, 0.00039439238025806844, 0.038318198174238205, 0.029950100928544998, 0.10728711634874344, 0.0, 0.04553529620170593, 0.001630795537494123, 0.021387213841080666, 0.047717414796352386, 0.011545702815055847, 0.00015597924357280135, 0.0, 0.0609932579100132, 0.04856590926647186, 0.050699323415756226, 0.002910671755671501, 0.05417401343584061, 0.0, 0.0, 0.0, 0.017876241356134415, 0.08231646567583084, 0.1306743025779724, 0.022523388266563416, 0.015131938271224499, 0.0, 0.03249071165919304, 0.0012441182043403387, 0.0, 0.009368929080665112, 0.0, 0.10816264897584915, 0.0013085338287055492, 0.0, 0.0959506407380104, 0.05105489119887352, 0.02161787822842598, 0.0, 0.029268959537148476, 0.04972019046545029, 0.08167580515146255, 0.0, 0.06346739083528519, 0.003373386338353157, 0.0546305775642395, 0.023095475509762764, 0.0, 0.06697630882263184, 0.003311581676825881, 0.00040975172305479646, 0.0007806011126376688, 0.0, 0.03179365396499634, 0.020233161747455597, 0.021663561463356018, 0.04142795130610466, 0.0406002439558506, 0.0, 0.05949630215764046, 0.04464401677250862, 0.10705789923667908, 0.045961230993270874, 0.01714530773460865, 0.0, 0.09340275079011917, 0.01140083558857441, 0.00024052431399468333, 0.04605329781770706, 0.0032373880967497826, 0.0006917549762874842, 0.0006269754958339036, 0.0, 0.00166584353428334, 0.08266665786504745, 0.07715342938899994, 0.0024232000578194857, 0.15407805144786835, 0.0, 0.04486661031842232, 0.08756211400032043, 0.0, 0.09529359638690948, 0.008932328782975674, 0.07988428324460983, 0.003167705610394478, 0.03146110475063324, 0.0, 0.0008025512215681374, 0.0, 0.04647499695420265, 0.05633527785539627, 0.002776443725451827, 0.08468101918697357, 0.01733597368001938, 0.016442324966192245, 0.00608130032196641, 0.0015455078100785613, 0.0, 0.0, 0.01670903153717518, 0.05644846707582474, 0.01627512276172638, 0.0002841172681655735, 0.006288579665124416, 0.027853362262248993, 0.0, 0.00580513384193182, 0.05897574871778488, 0.00881815142929554, 0.07675480842590332, 0.04281893000006676, 0.003922375850379467, 0.0, 0.001310001127421856, 0.03967544436454773, 0.0, 0.01608482375741005, 0.0, 0.00834153313189745, 0.009974266402423382, 0.03470248728990555, 0.07878708839416504, 0.10314007103443146, 0.0, 0.01980278454720974, 0.0565212182700634, 0.0, 0.022704146802425385, 0.0, 0.0, 0.0, 0.019341932609677315, 0.0, 0.0007305288454517722, 0.014590957202017307, 4.63959040644113e-05, 0.002419681055471301, 0.0, 0.0906568318605423, 0.0, 0.0011865325504913926, 0.0, 0.0016214953502640128, 0.027538560330867767, 0.13379710912704468, 0.0026411397848278284, 0.000363727449439466, 0.0, 0.0038900417275726795, 0.01866956613957882, 0.07386590540409088, 0.007793571334332228, 0.049085039645433426, 0.11235479265451431, 0.0, 0.0, 0.0, 0.0, 0.0, 0.036348845809698105, 0.00024301612575072795, 0.08735582232475281, 0.007952735759317875, 0.0017827653791755438, 0.03617047891020775, 0.0, 0.0, 0.06018322706222534, 0.0, 0.0, 0.016597919166088104, 0.0, 0.0361994206905365, 0.0, 0.0, 0.06407364457845688, 0.0, 0.0, 0.0034378431737422943, 0.06188252195715904, 0.0, 0.008363191038370132, 0.012744762003421783, 0.10327240079641342, 0.07947025448083878, 0.006827449891716242, 0.0, 0.004504320677369833, 0.0, 0.0363796204328537, 0.03193366900086403, 0.0, 0.04730455204844475, 0.0, 0.03168658912181854, 0.047870080918073654, 0.0, 0.07486862689256668, 0.0026852190494537354, 0.02808084525167942, 0.0012659336207434535, 0.02185789681971073, 0.09106319397687912, 0.014089148491621017, 0.0014878662768751383, 0.11800520867109299, 0.0856558084487915, 0.04250194504857063, 0.023982170969247818, 0.0, 0.0036220925394445658, 0.00040410427027381957, 0.03012145310640335, 0.00048347137635573745, 0.048197101801633835, 0.0, 0.0, 0.0, 0.14548197388648987, 0.03263521566987038, 0.0, 0.0, 0.004652633331716061, 0.009721451438963413, 0.03529008850455284, 0.03520939126610756, 0.0, 0.0, 0.12774032354354858, 0.001944269984960556, 0.0021493963431566954, 0.029655860736966133, 0.0839001014828682, 0.0, 0.005183492787182331, 0.08155925571918488, 0.03419173136353493, 0.04748832434415817, 0.023423397913575172, 0.05162283778190613, 0.0, 0.0012039394350722432, 0.05148620530962944, 0.00150217954069376, 0.00042079438571818173, 0.05935712158679962, 0.00043524138163775206, 0.0, 0.08166775852441788, 0.02338477596640587, 0.09977502375841141, 0.0, 0.035421498119831085, 0.004074994008988142, 0.0, 0.05047067254781723, 0.015096158720552921, 0.0, 0.0, 0.051500603556632996, 0.015354232862591743, 0.10971471667289734, 0.0006133125862106681, 0.0, 0.030646417289972305, 0.079534612596035, 0.022075524553656578, 0.027501866221427917, 0.005029171705245972, 0.024234216660261154, 0.009680536575615406, 0.008407066576182842, 0.001318325288593769, 0.08563745021820068, 0.05744118243455887, 0.0, 0.0, 0.06688028573989868, 0.0, 0.01358580682426691, 0.0, 0.0, 0.11030366271734238, 0.04759393259882927, 0.041411008685827255, 0.003829869907349348, 0.0934455543756485, 0.010302447713911533, 0.04607062041759491, 0.0, 0.06981415301561356, 0.005655255168676376], [0.0038391491398215294, 0.07413502037525177, 0.001936536282300949, 0.0, 0.053768645972013474, 0.07169542461633682, 0.0, 0.043884288519620895, 0.015222075395286083, 0.0, 0.0, 0.06731496006250381, 0.003192674368619919, 0.0, 0.0, 0.0, 0.0038789769168943167, 0.044204920530319214, 0.0, 0.0, 0.03435780107975006, 0.0030935746617615223, 0.0, 0.011140068992972374, 0.0, 0.03708462417125702, 0.03220977634191513, 0.04270175099372864, 0.01534076128154993, 0.0, 0.0062794797122478485, 0.09573189914226532, 0.0, 0.06428152322769165, 0.05583006143569946, 0.008824356831610203, 0.0, 0.003751168493181467, 0.05828438699245453, 0.1176593080163002, 0.009615604765713215, 0.03691146895289421, 0.0, 0.0, 0.005346063058823347, 0.0, 0.029666602611541748, 0.07896307855844498, 0.0, 0.0, 0.0, 0.08373894542455673, 0.0, 0.0, 0.18725387752056122, 0.12443554401397705, 0.0, 0.14257845282554626, 0.014938256703317165, 0.0031203858088701963, 0.0, 0.062168944627046585, 0.011862601153552532, 0.000883808359503746, 0.0317193977534771, 0.03291897103190422, 0.10502191632986069, 0.03342041000723839, 0.0, 0.014275242574512959, 0.06856821477413177, 0.01533783134073019, 0.0017269740346819162, 0.0010822498006746173, 0.02127368189394474, 0.007348181679844856, 0.0, 0.0, 0.0, 0.005903243087232113, 0.05946333333849907, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.012189080938696861, 0.0, 0.0, 0.018085824325680733, 0.00456005847081542, 0.07318522036075592, 0.07825616002082825, 0.00399235961958766, 0.010051512159407139, 0.0004370180540718138, 0.0, 0.0, 0.0, 0.031109321862459183, 0.02255365438759327, 0.02224818430840969, 0.014910475350916386, 0.008631095290184021, 0.062264714390039444, 0.0028657661750912666, 0.0, 0.014684720896184444, 0.04170622304081917, 0.030653709545731544, 0.04684244096279144, 0.0615864060819149, 0.0, 0.0216915775090456, 0.025321079418063164, 0.0, 0.027527134865522385, 0.031917180866003036, 0.0037777661345899105, 0.0034065882209688425, 0.0, 0.0, 0.09462697058916092, 0.0, 0.0, 0.0006978100282140076, 0.04342954605817795, 0.02888016775250435, 0.002026119502261281, 0.03554851561784744, 0.0, 0.013650690205395222, 0.001874218345619738, 0.040472179651260376, 0.0, 0.0, 0.0, 0.0438140444457531, 0.0, 0.019341757521033287, 0.008983290754258633, 0.030390294268727303, 0.009646393358707428, 0.06851433217525482, 0.0, 0.14500583708286285, 0.10387987643480301, 0.0, 0.0, 0.1045704111456871, 0.0007192844641394913, 0.04157837852835655, 0.03807181492447853, 0.0, 0.0, 0.04651549458503723, 0.03603678196668625, 0.11864320933818817, 0.006870495155453682, 0.025397013872861862, 0.0, 0.0, 0.04810658097267151, 0.031924907118082047, 0.046826425939798355, 0.0, 0.010472976602613926, 0.00974301714450121, 0.11608067154884338, 0.002735299291089177, 0.0, 0.024164725095033646, 0.0, 0.011784215457737446, 0.013095925562083721, 0.029249614104628563, 0.0015240042703226209, 0.0, 0.0, 0.0, 0.06222807988524437, 0.0, 0.09453608095645905, 0.0, 0.036892130970954895, 0.03318321332335472, 0.014917812310159206, 0.0, 0.0, 0.12655404210090637, 0.0, 0.012541702948510647, 0.0, 0.0, 0.0001649227342568338, 0.1175905093550682, 0.09872918576002121, 0.0005975041422061622, 0.08430010080337524, 0.04131211340427399, 0.07466322928667068, 0.0009337591473013163, 0.06973419338464737, 0.0024743813555687666, 0.009957999922335148, 0.0, 0.0006196028552949429, 0.0367228128015995, 0.0, 0.0, 0.048879824578762054, 0.024848248809576035, 0.0, 0.03800833970308304, 0.027982477098703384, 0.0022627064026892185, 0.13396859169006348, 0.09674245864152908, 0.004463541321456432, 0.021246805787086487, 0.024146266281604767, 0.05055861547589302, 0.13925164937973022, 0.0, 0.031582750380039215, 0.0, 0.0893654152750969, 0.09593977779150009, 0.0, 0.03412504866719246, 0.0025328858755528927, 0.09166993200778961, 0.0, 0.014166666194796562, 0.0, 0.002007326576858759, 0.07220914959907532, 0.004927433095872402, 0.0, 0.061406973749399185, 0.09476245194673538, 0.09835004061460495, 0.034063052386045456, 0.02324821427464485, 0.0, 0.05633813887834549, 0.0, 0.05319844186306, 0.052318938076496124, 0.0, 0.1197965070605278, 0.012128392234444618, 0.0, 0.011846565641462803, 0.06305845081806183, 0.008187581785023212, 0.003916880581527948, 0.04524879902601242, 0.026240425184369087, 0.0257717315107584, 0.0004983471008017659, 0.024007996544241905, 0.0012405137531459332, 0.08719592541456223, 0.018562663346529007, 0.0, 0.05161534622311592, 0.12360244989395142, 0.015723835676908493, 0.011608919128775597, 0.0, 0.06132015585899353, 0.0, 0.02978699840605259, 0.03718503192067146, 0.001998047111555934, 0.019453056156635284, 0.04437493905425072, 0.14024227857589722, 0.06763672828674316, 0.037373561412096024, 0.0, 0.0, 0.08765210211277008, 0.13353194296360016, 0.0, 0.03423002362251282, 0.0, 0.051154281944036484, 0.006850643083453178, 0.0, 0.0016828583320602775, 0.008204340934753418, 0.048590000718832016, 0.043797772377729416, 0.056422967463731766, 0.0, 0.0, 0.08207046240568161, 0.0, 0.03372587263584137, 0.016262216493487358, 0.060954414308071136, 0.03188253939151764, 0.002905439818277955, 0.0009063057950697839, 0.01663861982524395, 0.0, 0.03634687513113022, 0.060282301157712936, 0.0009393829968757927, 0.03443675488233566, 0.08506625145673752, 0.043231744319200516, 0.036735761910676956, 0.06092323735356331, 0.0, 0.009413609281182289, 0.021059202030301094, 0.023098118603229523, 0.0, 0.0004242606519255787, 0.03816971555352211, 0.030423130840063095, 0.0, 0.0, 0.05495814234018326, 0.03242320567369461, 0.10650936514139175, 0.06009075418114662, 0.030053604394197464, 0.0, 0.03982730954885483, 0.06794992089271545, 0.0009056437411345541, 0.0, 0.00021218883921392262, 0.02213594689965248, 0.06827382743358612, 0.03239395469427109, 0.024169467389583588, 0.12795495986938477, 0.0068872966803610325, 0.06122392416000366, 0.08557259291410446, 0.0, 0.0023135291412472725, 0.0, 0.0033627203665673733, 0.0, 0.008588609285652637, 0.0, 0.002140936441719532, 0.0, 0.00029799260664731264, 0.028186041861772537, 0.00024505177862010896, 0.05876452848315239, 0.0, 0.0, 0.0, 0.0, 0.04202224686741829, 0.0, 0.02128290757536888, 0.0, 0.0014937884407117963, 0.0, 0.03403095901012421, 0.08272264897823334, 0.0022732331417500973, 0.024931564927101135, 0.002902884501963854, 0.0, 0.010421394370496273, 0.0, 0.0, 0.0, 0.12678882479667664, 0.02276265062391758, 0.0719652846455574, 0.01589207351207733, 0.03594743460416794, 0.05513203889131546, 0.007015231065452099, 0.0, 0.030231529846787453, 0.0, 0.0017485229764133692, 0.0, 0.0, 0.06279121339321136, 0.000328902096953243, 0.0, 0.034263066947460175, 0.0, 0.049668774008750916, 0.0, 0.010147037915885448, 0.0, 0.0, 0.0044036447070539, 0.12869560718536377, 0.059994835406541824, 0.003936815541237593, 0.0, 0.021497875452041626, 0.0, 0.0, 0.03548256307840347, 0.0, 0.000868394912686199, 0.0, 0.003118836088106036, 0.10206278413534164, 0.0021259007044136524, 0.029277298599481583, 0.0, 0.03690459206700325, 0.0, 0.0, 0.06357144564390182, 0.09223978221416473, 0.0, 0.10951461642980576, 0.02271542325615883, 0.0860603079199791, 0.058139074593782425, 0.0, 0.026800354942679405, 1.3480223060469143e-05, 0.09317255765199661, 0.01085728034377098, 0.01975850760936737, 0.000937167031224817, 0.0, 0.0, 0.05833657458424568, 0.004390262998640537, 0.0, 0.0018162913620471954, 0.004150435794144869, 0.00046900336747057736, 0.025150472298264503, 0.0019936123862862587, 0.0, 0.0, 0.08737723529338837, 0.0, 0.015268119983375072, 0.0015112595865502954, 0.11089950054883957, 0.0, 0.0018968809163197875, 0.002200530841946602, 0.0568612739443779, 0.0, 0.05369189754128456, 0.006428651977330446, 0.0, 0.03752404451370239, 0.0, 0.01247743796557188, 0.036515697836875916, 0.07224908471107483, 7.73149513406679e-05, 0.0, 0.0035114926286041737, 0.010440072976052761, 0.10249852389097214, 0.0, 0.027970490977168083, 0.009359138086438179, 0.0, 0.03188947215676308, 0.0, 0.015596743673086166, 0.002927017631009221, 0.09322316944599152, 0.10426491498947144, 0.04647910222411156, 0.0, 0.0003072729741688818, 0.031568050384521484, 0.00039712092257104814, 0.03669421747326851, 0.0, 0.012359038926661015, 0.03986131772398949, 0.026894692331552505, 0.0010691032512113452, 0.009043158032000065, 0.10277950763702393, 0.018480662256479263, 0.0, 0.0, 0.0, 0.019060909748077393, 0.038699131458997726, 0.0, 3.9009055399219505e-06, 0.18833595514297485, 0.008940823376178741, 0.0010295013198629022, 0.0, 0.10132581740617752, 0.0, 0.0, 0.0, 0.06165667250752449, 0.018021486699581146]], "last_cam": "3", "nombre": "Rodrigo Cahuantzi C", "ts": 1782405086.9396744, "actualizaciones_globales": 9}, "102": {"galeria_deep": [[0.05575927346944809, 0.058149706572294235, 0.0, 0.0, 0.05288271605968475, 0.03857571259140968, 0.06753519177436829, 0.0815374106168747, 0.05307483673095703, 0.0007948208949528635, 3.4220462111989036e-05, 0.043450742959976196, 0.0, 0.0, 0.032428693026304245, 0.044062890112400055, 0.0012099390150979161, 0.0, 0.008084342814981937, 0.030131710693240166, 0.09854640811681747, 0.02317470870912075, 0.07916199415922165, 0.02650921419262886, 0.06671373546123505, 0.06278342008590698, 0.03023063950240612, 0.07297002524137497, 0.03884827718138695, 0.0009267465211451054, 0.058781784027814865, 0.01572565734386444, 0.0, 0.005408217199146748, 0.031186480075120926, 0.0, 0.02246241644024849, 0.03175525739789009, 0.07691226154565811, 0.027795664966106415, 0.0, 0.05083177238702774, 0.06488640606403351, 0.023671535775065422, 0.02646561898291111, 0.0, 0.0741327777504921, 0.02216408960521221, 0.048477787524461746, 0.03230508416891098, 0.0, 0.05207708850502968, 0.0068165939301252365, 0.007101963274180889, 0.21323823928833008, 0.0786663144826889, 7.326922059291974e-05, 0.10319448262453079, 0.01978391595184803, 0.0, 0.0, 0.006219032686203718, 0.05763395130634308, 0.01636987179517746, 0.014814089983701706, 0.01226872205734253, 0.022932633757591248, 0.0, 0.0, 0.0725807398557663, 0.007727856282144785, 0.04309632629156113, 0.03787161782383919, 0.0036706740502268076, 0.05468727648258209, 0.0, 0.0, 0.0066389902494847775, 0.00011342126526869833, 0.0034597953781485558, 0.0052136327140033245, 0.0034139372874051332, 0.0, 0.0009607762913219631, 0.0, 0.0, 0.0, 0.02547040395438671, 0.0002316597237950191, 0.0, 0.025113683193922043, 0.0, 0.016063755378127098, 0.13019222021102905, 0.012000301852822304, 0.03407292068004608, 0.008050857111811638, 0.0, 0.0, 0.026201311498880386, 0.03456356003880501, 0.03292294219136238, 0.021077247336506844, 0.027313394472002983, 0.05952104926109314, 0.04750994220376015, 0.020500026643276215, 0.007025664206594229, 0.07123575359582901, 0.042625222355127335, 0.022320915013551712, 0.015818631276488304, 0.13622818887233734, 0.004085395019501448, 0.05162884667515755, 0.05856718122959137, 0.0, 0.04376029595732689, 0.0, 0.03581623360514641, 0.0, 0.0, 0.0, 0.057896386831998825, 0.0, 0.0, 0.05780649930238724, 0.0972561463713646, 0.024761194363236427, 0.025659753009676933, 0.050799742341041565, 0.0, 0.009973973967134953, 0.03548811376094818, 0.00035090785240754485, 0.00017974164802581072, 0.0010016387095674872, 0.017796631902456284, 0.08497034758329391, 0.0, 0.005729539319872856, 0.030702371150255203, 0.06297487020492554, 0.008532870560884476, 0.06450328230857849, 0.04587745666503906, 0.11589150130748749, 0.03571820259094238, 0.006089756730943918, 0.04386104643344879, 0.018064573407173157, 0.033268898725509644, 0.05417827516794205, 0.0007219479302875698, 0.10888736695051193, 0.0, 0.07796519994735718, 0.01695084013044834, 0.050506796687841415, 0.03172767534852028, 0.008672474883496761, 0.0, 0.007327163126319647, 0.0, 0.00015232917212415487, 0.12333419919013977, 0.0, 0.06500009447336197, 0.01760433427989483, 0.08100704103708267, 0.10190391540527344, 0.014623544178903103, 0.0, 0.00028175258194096386, 0.005746391136199236, 0.06672977656126022, 0.009199696592986584, 0.034266259521245956, 0.06608577072620392, 0.010888403281569481, 0.03233912214636803, 0.03389522060751915, 0.014671930111944675, 0.08092198520898819, 0.001439196988940239, 0.011595833115279675, 0.067788265645504, 0.0035551905166357756, 0.0, 0.0, 0.04692136496305466, 0.0030124851036816835, 0.06107345223426819, 0.002471837680786848, 0.0, 0.0148301487788558, 0.005199391860514879, 0.0914275199174881, 0.02248138189315796, 0.050875645130872726, 0.008476410061120987, 0.037808146327733994, 0.0017021764069795609, 0.0956725999712944, 0.0, 0.06354556232690811, 0.0030903194565325975, 0.0, 0.025448307394981384, 0.0, 0.01015256904065609, 0.024609461426734924, 0.06418469548225403, 0.0, 0.054969485849142075, 0.03560197353363037, 0.0, 0.029652437195181847, 0.05182068794965744, 0.0, 0.00518754543736577, 0.05405158922076225, 0.006012170109897852, 0.08449438959360123, 0.01985815539956093, 0.005817314609885216, 0.030072400346398354, 0.07944323867559433, 0.023493556305766106, 0.029860857874155045, 0.068153515458107, 0.06018506735563278, 0.048785481601953506, 0.0, 0.11501596122980118, 0.0010195232462137938, 0.0, 0.060421764850616455, 0.0, 0.0017362706130370498, 0.05265737324953079, 0.08371550589799881, 0.07520779967308044, 0.03958655521273613, 0.058180347084999084, 0.0019329874776303768, 0.06737904250621796, 0.0, 0.021258613094687462, 0.005536530166864395, 0.00034187582787126303, 0.09061704576015472, 0.015683388337492943, 0.00878309365361929, 0.031989842653274536, 0.024765383452177048, 0.025073368102312088, 0.0, 0.03815993294119835, 0.09326203912496567, 0.08467461913824081, 0.0, 0.1048111617565155, 0.05935036391019821, 0.005814556498080492, 0.052678465843200684, 0.000618202961049974, 0.031912315636873245, 0.026912923902273178, 0.03547893837094307, 0.0006366723100654781, 0.0, 0.056688092648983, 0.05445951595902443, 0.063590869307518, 0.005030992906540632, 0.07954823970794678, 0.0, 0.08426132798194885, 0.002094610594213009, 0.10841120779514313, 0.06834083050489426, 0.0008691448601894081, 0.0, 0.049732744693756104, 0.0007589010056108236, 0.001901765470393002, 0.0, 0.013782226480543613, 0.05516207963228226, 0.0, 0.024579182267189026, 0.03212646767497063, 0.054500970989465714, 0.10173714905977249, 0.0018198275938630104, 0.06914778798818588, 0.0, 0.010388538241386414, 0.02477172762155533, 0.002401032717898488, 0.08298604935407639, 0.0, 0.08566106110811234, 0.0021199691109359264, 0.007550410460680723, 0.00745619460940361, 0.0, 0.0, 0.04365727677941322, 0.11302392929792404, 0.01579076237976551, 0.10712607204914093, 0.002887170296162367, 0.02816847339272499, 0.005936564411967993, 0.0, 0.0, 0.0, 0.056342024356126785, 0.06519949436187744, 0.018312156200408936, 0.0, 0.014960216358304024, 0.013291635550558567, 0.0016268332256004214, 0.049986351281404495, 0.03857523575425148, 0.009054454043507576, 0.06814492493867874, 0.05932047590613365, 0.0, 0.0, 0.04053177312016487, 0.05636688321828842, 0.0, 0.013580338098108768, 0.0, 0.0, 0.047586798667907715, 0.02661469765007496, 0.027166735380887985, 0.09045850485563278, 0.0, 0.07933442294597626, 0.01450427807867527, 0.036324430257081985, 0.0, 0.0, 0.0, 0.014905438758432865, 0.0, 0.0, 0.035511016845703125, 0.002685300540179014, 0.00718991132453084, 0.027103103697299957, 0.04672320932149887, 0.06305675953626633, 0.0, 0.020725589245557785, 0.0016517727635800838, 0.01860879734158516, 0.041967276483774185, 0.13218499720096588, 0.011736895889043808, 0.004882265347987413, 0.0, 0.0, 0.007491894997656345, 0.09124841541051865, 0.06920624524354935, 0.031170399859547615, 0.15419863164424896, 0.029810652136802673, 0.0026164192240685225, 0.0, 0.004064425826072693, 0.0, 0.04583580791950226, 0.0, 0.007384265307337046, 0.0, 0.011685461737215519, 0.08670508861541748, 0.0013579343212768435, 0.0, 0.012514442205429077, 0.0, 0.0020175757817924023, 0.026832034811377525, 0.03480366989970207, 0.01271045207977295, 0.006396494340151548, 0.0, 0.04207099601626396, 0.0007644414436072111, 0.0, 0.006293436046689749, 0.0, 0.04255733639001846, 0.03498898446559906, 0.0, 0.0696755200624466, 0.0486598014831543, 0.02894384041428566, 0.0, 0.0032459793146699667, 0.0, 0.01732558384537697, 0.018704961985349655, 0.0, 0.028224118053913116, 0.0, 0.0, 0.053114041686058044, 0.001407042844220996, 0.1104683130979538, 0.0018607709789648652, 0.0, 0.0, 0.005992761813104153, 0.07400025427341461, 0.007022495847195387, 0.0, 0.08186221122741699, 0.027962781488895416, 0.00334549299441278, 0.061116982251405716, 0.006052980199456215, 0.0, 0.01449926383793354, 0.0, 0.0006899562431499362, 0.03896068036556244, 0.0061242482624948025, 0.00032069309963844717, 0.0, 0.08302848041057587, 0.0001280039723496884, 0.0, 0.028832942247390747, 0.0, 0.0, 0.0003192255098838359, 0.0035749725066125393, 0.0, 2.5076913516386412e-05, 0.010139000602066517, 0.023432599380612373, 0.0038669684436172247, 0.047919563949108124, 0.04845833033323288, 0.0, 0.0024264876265078783, 0.011738454923033714, 0.05185370892286301, 0.012106796726584435, 0.05753771960735321, 0.1036110371351242, 0.0, 0.014234060421586037, 0.014156855642795563, 0.04778651148080826, 0.006241819355636835, 0.025151750072836876, 0.0, 0.00012876880646217614, 0.10804930329322815, 0.0035244820173829794, 0.07700596004724503, 0.0, 0.018241465091705322, 0.04147544503211975, 0.0, 0.07356042414903641, 0.0057076443918049335, 0.0601058267056942, 0.045728351920843124, 0.07015693187713623, 0.11434436589479446, 0.1741582155227661, 0.0, 0.030709732323884964, 0.010569592006504536, 0.030300447717308998, 0.004522322677075863, 0.0021978975273668766, 0.009910278022289276, 0.0, 0.004286590032279491, 0.01675914041697979, 0.0, 0.07341136783361435, 0.03452762961387634, 0.03812875226140022, 0.0007839417667128146, 0.026694688946008682, 0.0, 0.005463183857500553, 0.00286567653529346, 0.029960718005895615, 0.06866859644651413, 0.06554312258958817, 0.07934358716011047, 0.0, 0.13227766752243042, 0.0, 0.03967294096946716, 0.0, 0.09085845947265625, 0.006347671616822481]], "last_cam": "5", "nombre": null, "ts": 1782405072.1281576, "actualizaciones_globales": 2}, "103": {"galeria_deep": [[0.03622346371412277, 0.0900062695145607, 0.0, 0.0, 0.04468990117311478, 0.07681258767843246, 0.012804735451936722, 0.0368616059422493, 0.09521733969449997, 0.04424721747636795, 0.04871426895260811, 0.06192762032151222, 0.0, 0.0020492670591920614, 0.0014693108387291431, 0.032336775213479996, 0.0, 0.01193145290017128, 0.017004672437906265, 0.003397369524464011, 0.017755279317498207, 0.07329287379980087, 0.0, 0.0, 0.0, 0.07980989664793015, 0.08896930515766144, 0.07696510851383209, 0.00237938086502254, 0.00858265534043312, 0.0, 0.009131766855716705, 0.0, 0.04117182269692421, 0.0019233617931604385, 0.0, 0.0023069775197654963, 0.04141226038336754, 0.067928746342659, 0.07322516292333603, 0.011805993504822254, 0.027413012459874153, 0.03569314628839493, 0.001933425897732377, 0.06954657286405563, 0.008583598770201206, 0.0014225225895643234, 0.039849404245615005, 0.01046257559210062, 0.0016187175642699003, 0.0, 0.12095294892787933, 0.01676429994404316, 0.009760839864611626, 0.14409327507019043, 0.09293151646852493, 0.023519344627857208, 0.03355392813682556, 0.0005902796401642263, 0.007586880121380091, 0.018728481605648994, 0.0, 0.028518982231616974, 0.0167490653693676, 0.0014311186969280243, 0.0, 0.1035962849855423, 0.019567396491765976, 0.001105276751331985, 0.010733521543443203, 0.07486660033464432, 0.005915156565606594, 0.0, 0.0012544470373541117, 0.04819021373987198, 0.0008374163298867643, 0.0, 0.0, 0.0026974643114954233, 0.0, 0.0001960017398232594, 0.06814707070589066, 0.0, 0.0008130992064252496, 0.0, 0.0, 0.00037176921614445746, 0.11552655696868896, 0.0, 0.0, 0.011056436225771904, 0.009306148625910282, 0.039949726313352585, 0.022767890244722366, 0.0, 0.07481230050325394, 0.0011020086240023375, 0.0, 0.0, 0.000698567891959101, 0.0005985100287944078, 0.09165673702955246, 0.06206005439162254, 0.046998392790555954, 0.07017955183982849, 0.02357708290219307, 0.004960949532687664, 0.023775991052389145, 0.02601517364382744, 0.027094382792711258, 0.08854086697101593, 0.0, 0.06294387578964233, 0.002040670718997717, 0.03451990336179733, 0.0008000563248060644, 0.0, 0.0006615250604227185, 0.006923755165189505, 0.0068583241663873196, 0.008219825103878975, 0.0, 0.024286679923534393, 0.008409579284489155, 0.0, 0.04244203120470047, 0.029492469504475594, 0.0002890339237637818, 0.027319248765707016, 0.05194373056292534, 0.024745993316173553, 0.0, 0.041548099368810654, 0.059711772948503494, 0.0, 0.0, 0.0, 0.03548445180058479, 0.011155049316585064, 0.0024986069183796644, 0.0, 0.0, 0.0469859316945076, 0.0, 0.0004884425434283912, 0.02554752118885517, 0.16360127925872803, 0.056145064532756805, 0.002025760244578123, 0.0, 0.0, 0.03963736072182655, 0.0, 0.049538787454366684, 0.06394674628973007, 0.04525357857346535, 0.011341031640768051, 0.0020075980573892593, 0.08431034535169601, 0.0008158980635926127, 0.049975138157606125, 0.0, 0.01632688380777836, 0.030383095145225525, 0.0, 0.05982986465096474, 0.001094631850719452, 0.019280165433883667, 0.05026097595691681, 0.025550521910190582, 0.007244509179145098, 0.0, 0.04802298918366432, 0.0025555361062288284, 0.0, 0.002587022492662072, 0.010727585293352604, 0.010050241835415363, 0.0, 0.005760688800364733, 0.003079380840063095, 0.0904984176158905, 0.12490134686231613, 0.20535589754581451, 0.04794313758611679, 0.0, 0.029909171164035797, 0.014782668091356754, 0.0, 0.0, 0.07183368504047394, 0.001627463847398758, 0.006106621120125055, 0.003094564424827695, 0.006767262704670429, 0.03143634274601936, 0.01934555545449257, 0.02344057336449623, 0.0014731885166838765, 0.015087191015481949, 0.06001165881752968, 0.09132502228021622, 0.004093700088560581, 0.0485367551445961, 0.04239373281598091, 0.0362652912735939, 0.04310537129640579, 0.0003639411588665098, 0.0, 0.0021608364768326283, 0.02792707271873951, 0.061532456427812576, 0.06340932101011276, 0.0, 0.02125866338610649, 0.031097913160920143, 0.007921622134745121, 0.002489473670721054, 0.0021734770853072405, 0.0, 0.018499083817005157, 0.0, 0.07667980343103409, 0.11604706943035126, 0.05376074090600014, 0.062079597264528275, 0.002129903296008706, 0.007073308806866407, 0.011011609807610512, 0.025682741776108742, 0.03557467460632324, 0.04836731031537056, 0.10283321142196655, 0.06641263514757156, 0.017355704680085182, 0.05021926388144493, 0.007504437118768692, 0.0, 0.014248895458877087, 0.02600974775850773, 0.012677934020757675, 0.06441371142864227, 0.030872531235218048, 0.05933002755045891, 0.14354456961154938, 0.0021057676058262587, 0.018676893785595894, 0.001716665574349463, 0.03829626739025116, 0.0364728644490242, 0.006528475787490606, 0.0016416017897427082, 0.010607987642288208, 0.0028895882423967123, 0.155156210064888, 0.07230547815561295, 0.0, 0.0, 0.0, 0.07517211139202118, 0.09313096106052399, 0.061926089227199554, 0.07226882874965668, 0.026700284332036972, 0.0, 0.04473690688610077, 0.0, 0.0011469966266304255, 0.003827781183645129, 0.013469222001731396, 0.06959396600723267, 0.00039580161683261395, 0.01367269828915596, 0.014218715019524097, 0.006561308167874813, 0.0033764962572604418, 0.0, 0.0, 0.001854293281212449, 0.032879818230867386, 0.012641954235732555, 0.03325573354959488, 0.04130316898226738, 0.0, 0.11174928396940231, 0.04895338416099548, 0.0, 0.12508785724639893, 0.04173210263252258, 0.004481804557144642, 0.00028263762942515314, 0.0, 0.0008772297878749669, 0.027070844545960426, 0.08052661269903183, 0.15772372484207153, 0.07011943310499191, 0.001860404503531754, 0.012825085781514645, 0.041320353746414185, 0.0007962847012095153, 0.13366392254829407, 0.0, 0.0815722644329071, 0.028650837019085884, 0.022902844473719597, 0.0, 0.0019421675242483616, 0.0008527596946805716, 0.0023185941390693188, 0.004940485116094351, 0.004572485573589802, 0.06464256346225739, 0.044637542217969894, 0.029189860448241234, 0.030432380735874176, 0.051673054695129395, 0.03256017342209816, 0.006452691741287708, 0.0029739190358668566, 0.053724586963653564, 0.002400048077106476, 0.0, 0.00607974361628294, 0.0035474735777825117, 0.00919690914452076, 0.05054736137390137, 0.050728507339954376, 0.0, 0.10638587921857834, 0.02358810231089592, 0.08663517236709595, 0.0, 0.1232575923204422, 0.0524788498878479, 0.0, 0.026584718376398087, 0.011763577349483967, 0.01912837289273739, 0.014750739559531212, 0.00833083689212799, 0.0794718787074089, 0.005369651131331921, 0.0, 0.02068738266825676, 0.07113360613584518, 0.0, 0.0027677789330482483, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07168159633874893, 0.0026567024178802967, 0.0, 0.09711897373199463, 0.00034714958746917546, 0.005177691578865051, 0.0, 0.001035440480336547, 0.0, 0.0, 0.04337320476770401, 0.06560364365577698, 0.0, 0.004559792578220367, 0.0, 0.000341193750500679, 0.0, 0.055208176374435425, 0.0016413899138569832, 0.0, 0.019583532586693764, 0.01710810698568821, 0.0, 0.0, 0.01357653085142374, 0.006576078478246927, 0.07287987321615219, 0.0, 0.10486838221549988, 0.004897817969322205, 0.01804928667843342, 0.0, 0.0014305389486253262, 0.0, 0.0012920351000502706, 0.000990461092442274, 0.04231725260615349, 0.004912198521196842, 0.0493522547185421, 0.05124911293387413, 0.004176627844572067, 0.009652554988861084, 0.10554705560207367, 0.014034943655133247, 0.001566641149111092, 0.004149120766669512, 0.0, 0.0587133914232254, 0.03139427304267883, 0.0, 0.14023107290267944, 0.07044054567813873, 0.020726248621940613, 0.015678461641073227, 0.012373204343020916, 0.00804253201931715, 0.053824685513973236, 0.009616720490157604, 0.0, 0.04369957372546196, 0.0, 0.002073974348604679, 0.08749280124902725, 0.08006919920444489, 0.09911499917507172, 0.031823646277189255, 0.021470902487635612, 0.020263466984033585, 0.0015170848928391933, 0.0665431022644043, 0.0758921280503273, 0.0, 0.04606449976563454, 0.09839384257793427, 0.0, 0.0, 0.0005121961003169417, 0.0, 0.0230796430259943, 0.03549317270517349, 0.05609989911317825, 0.047565773129463196, 0.015001615509390831, 0.0028795390389859676, 0.0, 0.09052717685699463, 0.11786908656358719, 0.0, 0.007664019707590342, 0.038962528109550476, 0.0, 0.0, 0.09529313445091248, 0.002957681193947792, 0.0, 0.07739249616861343, 0.0, 0.02917810156941414, 0.04675506055355072, 0.08619514107704163, 0.0, 0.0, 0.08443476259708405, 0.07348597794771194, 0.033861350268125534, 0.06607307493686676, 0.06067376956343651, 0.0, 0.0, 0.00030363554833456874, 0.03242301940917969, 0.0011713944841176271, 0.0023300873581320047, 0.0, 0.03427682816982269, 0.022911641746759415, 0.0, 0.02080313302576542, 0.0, 0.019367173314094543, 0.008808399550616741, 0.0, 0.015118543989956379, 0.006944265682250261, 0.0, 0.04470156133174896, 0.005257823970168829, 0.008057298138737679, 0.01040717214345932, 0.0, 0.0, 0.034064881503582, 0.07412401586771011, 0.0, 0.0007609457825310528, 0.051961399614810944, 0.0018888558261096478, 0.0015639335615560412, 0.008649885654449463, 0.005415629595518112, 0.06624621897935867, 0.00025766907492652535, 0.0, 0.0, 0.08198494464159012, 0.0, 0.12170468270778656, 0.0, 0.0, 0.0954447016119957, 0.008418316952884197, 0.04056035727262497, 0.0066919224336743355, 0.0961153507232666, 0.0, 0.05995432659983635, 0.0, 0.03256474435329437, 0.02445368655025959], [0.0, 0.079125314950943, 0.0, 0.0, 0.0996609479188919, 0.05964931473135948, 0.015352041460573673, 0.01068847719579935, 0.001592382905073464, 0.0, 0.0, 0.0033887838944792747, 0.0, 0.0, 0.016363076865673065, 0.008608795702457428, 0.0, 0.014923986047506332, 0.011233827099204063, 0.01469734963029623, 0.06714286655187607, 0.03664403408765793, 0.0, 0.007952706888318062, 0.0034069521352648735, 0.06612806767225266, 0.05095645412802696, 0.0004163676348980516, 0.0143189188092947, 0.0027390094473958015, 0.02028459496796131, 0.004552748054265976, 0.0, 0.03388068452477455, 0.037070583552122116, 0.0070625776425004005, 0.022615980356931686, 0.017382383346557617, 0.1201210469007492, 0.009404966607689857, 0.01351952739059925, 0.047857411205768585, 0.04541324824094772, 0.0, 0.001245717634446919, 0.01670124940574169, 0.035026226192712784, 0.11065961420536041, 0.0014002852840349078, 0.0, 0.0, 0.12081398814916611, 0.0, 0.03751154989004135, 0.08366343379020691, 0.12972182035446167, 0.061528317630290985, 0.07869838923215866, 0.027172505855560303, 0.021109357476234436, 0.0003976993029937148, 0.0314224548637867, 0.0, 0.0248255617916584, 0.010398870334029198, 0.046429622918367386, 0.11924219876527786, 0.0, 0.0, 0.001938055851496756, 0.06710071116685867, 0.03481951728463173, 0.008932562544941902, 0.0, 0.0073011647909879684, 0.012509217485785484, 0.0, 0.0025738379918038845, 0.00026907541905529797, 0.009082804434001446, 0.0454195998609066, 0.014423448592424393, 0.0, 0.0, 0.012071371078491211, 0.0, 0.0005761408247053623, 0.012911194935441017, 0.0, 0.03264450654387474, 0.05187823623418808, 0.0401163250207901, 0.04845302179455757, 0.05723532289266586, 0.05201954394578934, 0.033114947378635406, 0.0, 0.0, 0.0005236243596300483, 0.0, 0.0, 0.027077659964561462, 0.10852709412574768, 0.028984064236283302, 0.0717000886797905, 0.006219740491360426, 0.059105031192302704, 0.06448224186897278, 0.03474428877234459, 0.07265755534172058, 0.02148379385471344, 0.026079785078763962, 0.029860731214284897, 0.0, 0.06146373227238655, 0.04460204020142555, 0.0, 0.019887464120984077, 0.0028750719502568245, 0.015755562111735344, 0.0020746742375195026, 0.0, 0.0005153567180968821, 0.060402464121580124, 0.0, 0.005342933349311352, 0.025862911716103554, 0.0492657832801342, 0.0011741830967366695, 0.02930077724158764, 0.06483499705791473, 0.0, 0.0, 0.01841353066265583, 0.0, 0.005008952226489782, 0.026676740497350693, 0.0, 0.07998242229223251, 0.0009162586065940559, 0.0, 0.010389872826635838, 0.12128771096467972, 0.01724345237016678, 0.028269227594137192, 0.017244011163711548, 0.12897321581840515, 0.05583221837878227, 0.0, 0.0, 0.04771766811609268, 0.00357190053910017, 0.03069734387099743, 0.011922079138457775, 0.002192581305280328, 0.0, 0.04218268394470215, 0.0, 0.048211123794317245, 0.005139187444001436, 0.05663422495126724, 0.0, 0.0, 0.0012615255545824766, 0.0006325258291326463, 0.04428708553314209, 0.0, 0.00590825080871582, 0.05036719888448715, 0.06276977062225342, 0.004869590979069471, 0.0, 0.038263361901044846, 0.00047513708705082536, 0.011483382433652878, 0.0, 0.012295376509428024, 0.034023918211460114, 0.03250514715909958, 0.0004497791815083474, 0.001216016593389213, 0.02402086742222309, 0.038808900862932205, 0.09854354709386826, 0.0, 0.0, 0.04358375817537308, 0.007382506038993597, 0.0, 0.025839215144515038, 0.12867531180381775, 0.035337094217538834, 0.0075270128436386585, 0.0, 0.017242513597011566, 0.0, 0.12818099558353424, 0.04181912541389465, 0.0, 0.02900705672800541, 0.05914890021085739, 0.11188597232103348, 0.048094507306814194, 0.07791709899902344, 0.09598561376333237, 0.05020468682050705, 0.0008488791063427925, 0.002171775558963418, 0.0, 0.0, 0.004997870419174433, 0.07467611134052277, 0.02078535221517086, 0.0, 0.033152464777231216, 0.0, 0.0, 0.038985077291727066, 0.016140185296535492, 0.0, 0.03395223990082741, 0.024127135053277016, 0.017331011593341827, 0.12209669500589371, 0.0, 0.05103888735175133, 0.0, 0.11683689057826996, 0.07142915576696396, 0.0, 0.10385308414697647, 0.009505207650363445, 0.07712175697088242, 0.005220683291554451, 0.028284771367907524, 0.0, 0.04098239168524742, 9.999902977142483e-05, 0.0, 0.013617321848869324, 0.006805488374084234, 0.08839721232652664, 0.02262727916240692, 0.05596137046813965, 0.06049247831106186, 0.0, 0.08506164699792862, 0.03655005618929863, 0.05802498012781143, 0.01355767622590065, 0.030929245054721832, 0.05141772702336311, 0.012209645472466946, 0.0, 0.099424809217453, 0.04699469357728958, 0.006980862468481064, 0.0, 0.0009806547313928604, 0.04165405035018921, 0.1238873302936554, 0.0755954384803772, 0.10985250025987625, 0.0371394008398056, 0.06319795548915863, 0.012143250554800034, 0.0003984564682468772, 0.0025583470705896616, 0.06895951926708221, 0.0, 0.043381065130233765, 0.0011762093054130673, 0.05123848095536232, 0.0, 0.017547214403748512, 0.0, 0.0, 0.0006275460473261774, 0.010857381857931614, 0.07236497104167938, 0.022963251918554306, 0.06024733558297157, 0.01914246194064617, 0.0, 0.05717373639345169, 0.1616799384355545, 0.0, 0.036959584802389145, 0.0, 0.037033773958683014, 0.0, 0.00017318184836767614, 0.017147371545433998, 0.03391331061720848, 0.06397836655378342, 0.11238930374383926, 0.05520941689610481, 0.01944604702293873, 0.003252458991482854, 0.15801066160202026, 0.0, 0.038535226136446, 0.004904455970972776, 0.0389714315533638, 0.07848214358091354, 0.051864854991436005, 0.0, 0.006339427083730698, 0.0, 0.03147187829017639, 0.0, 0.028544390574097633, 0.008824272081255913, 0.037982434034347534, 0.009610758163034916, 0.004439215641468763, 0.07568391412496567, 0.0003635851899161935, 0.024347204715013504, 0.005516630597412586, 0.06494555622339249, 0.00687794154509902, 0.0, 0.03845324367284775, 0.0029292693361639977, 0.012579512782394886, 0.01858474127948284, 0.03919358551502228, 0.001415830454789102, 0.007319097872823477, 0.06485654413700104, 0.028684260323643684, 0.0, 0.06721329689025879, 0.058803439140319824, 0.017315957695245743, 0.01089497096836567, 0.003916214220225811, 0.07042308151721954, 0.0, 0.015334916301071644, 0.0682353749871254, 0.14505736529827118, 0.0, 0.09553035348653793, 0.015803299844264984, 0.0, 0.0, 0.0, 0.00027788858278654516, 0.0, 0.0035196272656321526, 0.0008520151604898274, 0.01440610084682703, 0.0, 0.002323934342712164, 0.016069676727056503, 0.0, 0.046613067388534546, 0.0, 0.0, 0.07077225297689438, 0.0, 0.06562076508998871, 0.0, 0.0008165848557837307, 0.0, 0.0, 0.006007369142025709, 0.0, 0.060930706560611725, 0.0, 0.056534282863140106, 0.011662991717457771, 0.0022836204152554274, 0.03969229757785797, 0.0, 0.10234356671571732, 0.0, 0.038094013929367065, 0.0, 0.06486345827579498, 0.06140308827161789, 0.01922956481575966, 0.06041285768151283, 0.005273331422358751, 0.0, 0.0029419013299047947, 0.0, 0.006816809996962547, 0.003768415190279484, 0.010554377920925617, 0.048543013632297516, 0.00879640318453312, 0.05215994641184807, 0.017994049936532974, 0.005884266924113035, 0.04958858713507652, 0.038677576929330826, 0.0016566476551815867, 0.04805885627865791, 0.0, 0.0, 0.09406530112028122, 0.13799266517162323, 0.005664851050823927, 0.0, 0.013577509671449661, 0.03697776794433594, 0.0, 0.07932396233081818, 0.0, 0.001841559074819088, 0.0, 0.0007277652621269226, 0.09871670603752136, 0.03299291059374809, 0.16563519835472107, 0.00044367800001055, 0.000566228583920747, 0.07855816930532455, 0.0005465953727252781, 0.001484658452682197, 0.07709383219480515, 0.004389403387904167, 0.054365333169698715, 0.03582891449332237, 0.004881829023361206, 0.035465024411678314, 0.0, 0.034037571400403976, 0.0018029100028797984, 0.11494100838899612, 0.0, 0.07906362414360046, 0.04408978670835495, 0.0, 0.0, 0.05880194529891014, 0.060713592916727066, 0.0009728101431392133, 0.06775050610303879, 0.031427789479494095, 3.710454620886594e-05, 0.04956386983394623, 0.04649755358695984, 0.0, 0.0, 0.0, 0.0, 0.003323138225823641, 0.034408632665872574, 0.062246184796094894, 0.0177493654191494, 0.0, 0.05825076252222061, 0.037415239959955215, 0.0, 0.016129331663250923, 0.02407136932015419, 0.0, 0.0005145458853803575, 0.0, 0.0, 0.009071960113942623, 0.016762087121605873, 0.0, 0.0, 0.0, 0.0032398574985563755, 0.04789910838007927, 0.002978966338559985, 0.0028753713704645634, 0.02764276973903179, 0.0006052899989299476, 0.027648568153381348, 0.00472834100946784, 0.0009152468992397189, 0.0, 0.032331231981515884, 0.081898994743824, 0.03227800503373146, 0.0, 0.0013803322799503803, 0.06814644485712051, 0.0, 0.0, 0.004737415816634893, 0.09491395950317383, 0.02430402860045433, 0.008990887552499771, 0.028598032891750336, 0.013865642249584198, 0.0652688667178154, 0.0, 0.0, 0.0, 0.0001629480393603444, 0.0, 0.03520169481635094, 0.0, 0.08283014595508575, 0.19853121042251587, 0.002800670685246587, 0.015521001070737839, 0.08462203294038773, 0.0783579871058464, 0.0, 0.021870248019695282, 0.0, 0.03960254788398743, 0.05208267644047737]], "last_cam": "3", "nombre": null, "ts": 1782405090.4395273, "actualizaciones_globales": 6}, "104": {"galeria_deep": [[0.011239697225391865, 0.12024366855621338, 0.0, 0.0, 0.07633821666240692, 0.03196236863732338, 0.055173568427562714, 0.10563457757234573, 0.1059003621339798, 0.0, 0.0, 0.04151725023984909, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07549120485782623, 0.0019120064098387957, 0.0, 0.04220160096883774, 0.043893344700336456, 0.0, 0.013337678276002407, 0.01045046467334032, 0.08499283343553543, 0.0686148852109909, 0.08026869595050812, 0.05461426079273224, 0.0, 0.0, 0.03641431778669357, 0.006442595273256302, 0.054585669189691544, 0.034248899668455124, 0.0, 0.0, 0.04073459282517433, 0.054144617170095444, 0.007500986102968454, 0.00940093956887722, 0.059001531451940536, 0.08198212087154388, 0.0, 0.004781424067914486, 0.0024024485610425472, 0.059913720935583115, 0.07541805505752563, 0.0, 0.00183211755938828, 0.0, 0.07820966094732285, 0.006574000231921673, 0.019413337111473083, 0.04545338824391365, 0.09591332077980042, 0.00043861998710781336, 0.04323263093829155, 0.029114726930856705, 0.04634993150830269, 0.02370353229343891, 0.01252411026507616, 0.0, 0.013341612182557583, 0.015601631253957748, 0.000626272929366678, 0.06436959654092789, 0.0012081547174602747, 0.0, 0.0, 0.06199130043387413, 0.00015569746028631926, 0.0004566564748529345, 0.0071393707767128944, 0.020082078874111176, 0.0, 0.0007153154001571238, 0.0, 0.0005495371297001839, 0.00225600297562778, 0.007329338695853949, 0.0041744704358279705, 0.026134001091122627, 0.0, 0.012004227377474308, 0.0, 0.0, 0.09492897242307663, 0.019938873127102852, 0.0, 0.005597269628196955, 0.02913542091846466, 0.0033736329060047865, 0.04212791845202446, 0.017316341400146484, 0.1014183983206749, 0.0144466208294034, 0.0, 0.0009569967514835298, 0.10056868940591812, 0.08174849301576614, 0.03532015159726143, 0.021747393533587456, 0.03088284656405449, 0.10310982912778854, 0.0022966230753809214, 0.0, 0.01770014688372612, 0.0142583716660738, 0.043504662811756134, 0.06452610343694687, 0.0, 0.007112028542906046, 0.0, 0.061830732971429825, 0.05872903764247894, 0.00787897314876318, 0.029558656737208366, 0.02610144391655922, 0.061036285012960434, 0.007485398091375828, 0.0007016185554675758, 0.0458625890314579, 0.0056138960644602776, 0.003586419392377138, 0.0, 0.038447991013526917, 0.06384904682636261, 0.060273781418800354, 0.060208648443222046, 0.05018754303455353, 0.0, 0.019026076421141624, 0.0017708558589220047, 0.0, 0.030629867687821388, 0.003337580244988203, 0.05413367599248886, 0.05419754236936569, 0.030031904578208923, 0.0, 0.02463369630277157, 0.07881131768226624, 0.017957227304577827, 0.06108176335692406, 0.0328475758433342, 0.0264971312135458, 0.090458445250988, 0.0, 0.0020589905325323343, 0.12424322217702866, 0.0, 0.0005032540648244321, 0.0, 0.059391364455223083, 0.0021720286458730698, 0.06774122267961502, 0.08311713486909866, 0.1269473284482956, 0.00012901048467028886, 0.05166866257786751, 0.0, 0.0, 0.09359177947044373, 0.02352403663098812, 0.07922890037298203, 0.0, 0.0, 0.05721443518996239, 0.08694567531347275, 0.04114101082086563, 0.0024522377643734217, 0.0006036837003193796, 0.0397990420460701, 0.022622158750891685, 0.0015034732641652226, 0.00016291276551783085, 0.03462586551904678, 0.010879922658205032, 0.03046361170709133, 0.0, 0.08288205415010452, 0.011650152504444122, 0.0888245701789856, 0.0259653702378273, 0.0832173153758049, 0.0032252580858767033, 0.04007403925061226, 0.0, 0.028485940769314766, 0.10394466668367386, 0.0, 0.0, 0.0043069650419056416, 0.00926093477755785, 0.0399165116250515, 0.10456398874521255, 0.07767938822507858, 0.03173964470624924, 0.03455599769949913, 5.9763733588624746e-06, 0.07951866090297699, 0.09900076687335968, 0.0, 0.0, 0.055885735899209976, 0.050290580838918686, 0.0, 0.04223346337676048, 0.0, 0.0, 0.05430446192622185, 0.059904664754867554, 0.0, 0.03955813869833946, 0.0047939070500433445, 0.08238211274147034, 0.016303695738315582, 0.05257910117506981, 0.0, 0.025745419785380363, 0.0, 0.025369537994265556, 0.0744902715086937, 0.025721251964569092, 0.03631613031029701, 0.012231430038809776, 0.040662359446287155, 0.0683833435177803, 0.00028639863012358546, 0.09952101856470108, 0.0, 0.0734875276684761, 0.010231191292405128, 0.06642793118953705, 0.0008129989146254957, 0.014097923412919044, 0.0627511665225029, 0.006365678738802671, 0.026209130883216858, 0.03174414485692978, 0.09501120448112488, 0.016477501019835472, 0.006092856638133526, 0.07198872417211533, 0.006395092699676752, 0.03947260603308678, 0.008506575599312782, 0.04216574504971504, 0.024161437526345253, 0.031231189146637917, 0.13215728104114532, 0.08796858042478561, 0.0024490340147167444, 0.002054297598078847, 0.00892387330532074, 0.00749726127833128, 0.003020225092768669, 0.0005072577623650432, 0.01726767048239708, 0.05212528631091118, 0.090244822204113, 0.04735158756375313, 0.0, 0.054231226444244385, 0.053681470453739166, 0.0, 0.02257024683058262, 0.0582265704870224, 0.0, 0.011929754167795181, 0.0, 0.028937404975295067, 0.006235945969820023, 0.11240272969007492, 0.007885855622589588, 0.021677153185009956, 0.0011316919699311256, 0.03607930243015289, 0.0016140200896188617, 0.12479490786790848, 0.02766489051282406, 0.11189611256122589, 0.0, 0.05464950203895569, 0.12062238901853561, 0.0029737292788922787, 0.03616541996598244, 0.0056417169980704784, 0.0070344447158277035, 0.003422750160098076, 0.00012662312656175345, 0.05857044458389282, 0.010856359265744686, 0.06543143838644028, 0.05712632089853287, 0.061517808586359024, 0.0, 0.011090187355875969, 0.0, 0.013616065494716167, 0.03288618102669716, 0.03550894185900688, 0.05895072966814041, 0.020148716866970062, 0.020785097032785416, 0.0, 0.06383297592401505, 0.0, 0.049435704946517944, 0.00896245613694191, 0.012353401631116867, 0.07575608789920807, 0.0354500338435173, 0.0013125198893249035, 0.03373856097459793, 0.012370853684842587, 0.0020844608079642057, 0.0, 0.006945466157048941, 0.04999265447258949, 0.004660983104258776, 0.0, 0.026315825060009956, 0.003337710862979293, 0.017287801951169968, 0.014620780944824219, 0.10014268755912781, 0.00020010297885164618, 0.030029483139514923, 0.04099274054169655, 0.013769831508398056, 0.0, 0.04346095770597458, 0.06381229311227798, 0.0, 0.0, 0.0, 0.03808281570672989, 0.0012322802795097232, 0.05053091421723366, 0.0018330503953620791, 0.029915228486061096, 0.0, 0.10533210635185242, 0.03487849608063698, 0.02073577791452408, 0.001458478975109756, 0.0, 0.026381826028227806, 0.0, 0.0, 0.001550795161165297, 0.02664666250348091, 0.0, 0.03154674917459488, 0.02632039599120617, 0.02236180379986763, 0.07711922377347946, 0.0, 0.0, 0.0016973984893411398, 0.0, 0.04869402199983597, 0.010596374981105328, 0.0, 0.036724962294101715, 0.03831888735294342, 0.00490665202960372, 0.0, 0.04457034170627594, 0.01097314152866602, 0.057112958282232285, 0.010855148546397686, 0.0, 0.0009784906869754195, 0.0011679823510348797, 0.006247041281312704, 0.0, 0.06727201491594315, 0.0, 0.06044692173600197, 0.0, 0.025387192144989967, 0.0002541396825108677, 0.07543247193098068, 0.0, 0.027073930948972702, 0.0, 0.0, 0.043280377984046936, 0.009930465370416641, 0.007252342067658901, 0.0006432636873796582, 0.0, 0.015482112765312195, 0.0, 0.0, 0.09344402700662613, 0.03145007789134979, 0.011279595084488392, 0.0, 0.0, 0.0818597674369812, 0.03077644668519497, 0.027327463030815125, 0.0, 0.0057282112538814545, 0.007459145970642567, 8.506995072821155e-05, 0.09989386796951294, 0.0045137181878089905, 0.010571182705461979, 0.0, 0.00029568144236691296, 0.07569219917058945, 0.0038624494336545467, 0.11894731968641281, 0.030999213457107544, 0.04723864048719406, 0.0456928126513958, 0.0, 0.003613486886024475, 0.08625438809394836, 0.04470741003751755, 0.13310861587524414, 0.07193438708782196, 0.0, 0.002988541964441538, 0.03645942360162735, 0.008441268466413021, 0.033185455948114395, 0.035710323601961136, 0.02853451669216156, 0.0, 0.08903227746486664, 0.011603054590523243, 0.0, 0.11607372015714645, 0.009561196900904179, 0.00019543016969691962, 0.008007854223251343, 0.059638023376464844, 0.012788744643330574, 0.05023624375462532, 0.025513242930173874, 0.05828218534588814, 0.0, 0.03564019873738289, 0.0, 0.0, 0.07670191675424576, 0.07606431096792221, 0.042499132454395294, 0.01570935919880867, 0.011400156654417515, 0.03110153041779995, 0.0, 0.1670733094215393, 0.07953546196222305, 0.06316499412059784, 0.005958751309663057, 0.05777980014681816, 0.015827395021915436, 0.0, 0.0822523757815361, 0.0, 0.0, 0.0011680291499942541, 0.0, 0.13185128569602966, 0.0, 0.03376561403274536, 0.09062103182077408, 3.1256367947207764e-05, 0.0091188820078969, 0.0, 0.009952076710760593, 0.001971911871805787, 0.0024381165858358145, 0.06733743846416473, 0.06277678161859512, 0.0, 0.008933376520872116, 3.540315810823813e-05, 0.00020111013145651668, 0.0, 0.0018874747911468148, 0.052098650485277176, 0.0, 0.0, 0.0, 0.0024614883586764336, 0.13425765931606293, 0.026218323037028313, 0.0, 0.0, 0.0012811812339350581, 0.0, 0.0279171634465456, 0.0, 0.0, 0.14603084325790405, 0.0, 0.026787705719470978, 0.056937847286462784, 0.03713049367070198, 0.0, 0.0, 0.0, 0.0, 0.005958127789199352]], "last_cam": "6", "nombre": "Jesus Eduardo", "ts": 1782405167.1195412, "actualizaciones_globales": 6}, "105": {"galeria_deep": [[0.022779086604714394, 0.0913081169128418, 0.0, 0.0, 0.05206873640418053, 0.12465067207813263, 0.0, 0.0929533913731575, 0.02405940368771553, 0.016110222786664963, 0.0, 0.007904919795691967, 0.0, 0.0, 0.012871664017438889, 0.0038020138163119555, 0.004072236828505993, 0.007297885604202747, 0.036460451781749725, 0.0, 0.08754558861255646, 0.05287310481071472, 0.0015461008297279477, 0.023792479187250137, 0.0, 0.05532640218734741, 0.04378723353147507, 0.13197854161262512, 0.0668247640132904, 0.0, 0.0722557082772255, 0.05294344574213028, 0.0, 0.019594706594944, 0.039645202457904816, 0.0, 0.03472461178898811, 0.04805365204811096, 0.05852961167693138, 0.061375733464956284, 0.03715880587697029, 0.09024462848901749, 0.03229903802275658, 0.0, 0.06570065766572952, 0.0, 0.0, 0.07745618373155594, 0.06737317889928818, 0.0, 0.0, 0.09615735709667206, 0.009530129842460155, 0.02822953648865223, 0.051319509744644165, 0.12824136018753052, 0.07776749134063721, 0.06206163763999939, 0.07629009336233139, 0.0, 0.0, 0.0, 0.0, 0.03770237788558006, 0.0, 0.0, 0.010921082459390163, 0.0, 0.00048770123976282775, 0.018958531320095062, 0.010898648761212826, 0.029231388121843338, 0.0, 0.0, 0.030314728617668152, 0.03682350739836693, 0.0, 0.004534073639661074, 0.0, 0.019223570823669434, 0.03960511460900307, 0.012102149426937103, 0.0, 0.001645567361265421, 0.0, 0.0, 0.0231185182929039, 0.020122593268752098, 0.0, 0.0, 0.06494702398777008, 0.0, 0.10352285951375961, 0.12133447825908661, 0.0, 0.08785931020975113, 0.0, 0.0, 0.0, 0.0, 0.0, 0.13544070720672607, 0.05401775985956192, 0.07780132442712784, 0.04990697279572487, 0.000604183878749609, 0.0033043576404452324, 0.0, 0.04580121487379074, 0.055908385664224625, 0.039020732045173645, 0.06546393036842346, 0.03133971989154816, 0.0, 0.0004876027232967317, 0.06676221638917923, 0.0, 0.00362491887062788, 0.008919253014028072, 0.0, 0.013679307885468006, 0.0, 0.0, 0.0001336437271675095, 0.0, 0.00023981793492566794, 0.0, 0.08495733886957169, 0.0476888082921505, 0.00043718377128243446, 0.017483513802289963, 0.0, 0.005835030227899551, 0.025730038061738014, 0.0, 0.0, 0.06284614652395248, 0.019963324069976807, 0.07793185859918594, 0.0, 0.004219017922878265, 0.0005267348606139421, 0.02155189961194992, 0.028821103274822235, 0.0, 0.0, 0.06345902383327484, 0.03606278449296951, 0.0, 0.0, 0.05117002874612808, 0.014893988147377968, 0.0050458721816539764, 0.0, 0.0, 0.020461641252040863, 0.12246806919574738, 0.04370948299765587, 0.10008470714092255, 0.0, 0.03359183296561241, 0.0, 0.0, 0.014255891554057598, 0.015492923557758331, 0.04226381331682205, 0.0, 0.023625878617167473, 0.0009346305159851909, 0.1097697839140892, 0.02073032595217228, 0.0, 0.000562771747354418, 0.0, 0.042493004351854324, 0.0, 0.003295137081295252, 0.0, 0.026412878185510635, 0.01640244759619236, 0.034943364560604095, 0.0, 0.023850830271840096, 0.0652509555220604, 0.010522309690713882, 0.034491173923015594, 0.05909471586346626, 0.008303125388920307, 0.0, 0.0013178852386772633, 0.06436236947774887, 0.0, 0.0, 0.0, 0.0, 0.0, 0.08693557232618332, 0.08207879215478897, 0.0, 0.021771026775240898, 0.04895917698740959, 0.07977113127708435, 0.0, 0.11429788917303085, 0.07795477658510208, 0.013165625743567944, 0.0, 0.0, 0.05041079223155975, 0.0, 0.007121768780052662, 0.03170206397771835, 0.021606607362627983, 0.0, 0.06170421466231346, 0.09972531348466873, 0.0, 0.04839775338768959, 0.10884075611829758, 0.0007300804136320949, 0.02299349755048752, 0.0, 0.009696496650576591, 0.0012973938137292862, 0.021338777616620064, 0.06556965410709381, 0.0, 0.045105475932359695, 0.022776314988732338, 0.000642809143755585, 0.039629898965358734, 0.0009100724128074944, 0.13968174159526825, 2.262181806145236e-05, 0.008548718877136707, 0.0, 0.018255161121487617, 0.022639235481619835, 0.0, 0.0, 0.0, 0.12126477807760239, 0.09711069613695145, 0.0508326031267643, 0.08908769488334656, 0.0, 0.14526374638080597, 0.0, 0.026686321943998337, 0.03658774495124817, 0.0, 0.0693143904209137, 0.09758935868740082, 0.0, 0.09264688193798065, 0.05004327371716499, 0.016228480264544487, 0.032808221876621246, 0.03554404154419899, 0.04156947508454323, 0.07662024348974228, 0.0024939097929745913, 0.0751204565167427, 0.03909376636147499, 0.05777471885085106, 0.04210447892546654, 0.0, 0.0, 0.011006711050868034, 0.06408361345529556, 0.003316554706543684, 0.0, 0.04879818111658096, 0.049939729273319244, 0.11779609322547913, 0.0, 0.07874689996242523, 0.0, 0.037935227155685425, 0.046621475368738174, 0.09578012675046921, 0.0563698373734951, 0.04480468109250069, 0.0, 0.0005984922172501683, 0.068837970495224, 0.0, 0.08727765083312988, 0.02726716920733452, 0.00020405276154633611, 0.008659888990223408, 0.008349664509296417, 0.04286601021885872, 0.046466581523418427, 0.038319218903779984, 0.06368590891361237, 0.08298926800489426, 0.0, 0.0, 0.12048421055078506, 0.02768680639564991, 0.08833695203065872, 0.022756947204470634, 0.04738244041800499, 0.005058963317424059, 0.05883779376745224, 0.0, 0.024608388543128967, 0.0, 0.09030693024396896, 0.0, 0.03723662719130516, 0.07797753810882568, 0.046054236590862274, 0.004002605564892292, 0.08514644205570221, 0.0, 0.021173525601625443, 0.0009875854011625051, 0.0, 0.03452864661812782, 0.0, 0.0, 0.002283862791955471, 0.0, 0.0, 0.031167589128017426, 0.0681111067533493, 0.0, 0.06832549721002579, 0.07163186371326447, 0.04093040153384209, 0.0, 0.07616261392831802, 0.06133196875452995, 0.0, 0.0, 0.0, 0.013413016684353352, 0.061513762921094894, 0.029922837391495705, 0.0411105640232563, 0.03730115294456482, 0.0, 0.026284178718924522, 0.07656650990247726, 0.0002718613250181079, 0.0, 0.0, 0.00041976539068855345, 0.0012007486075162888, 0.0, 0.0, 0.013123812153935432, 0.0, 0.003927083685994148, 0.012569401413202286, 0.06638795137405396, 0.11445699632167816, 0.0, 0.0, 0.0, 0.0, 0.01008879579603672, 0.05722426250576973, 0.002838470507413149, 0.0, 0.008623238652944565, 0.06413587927818298, 0.0, 0.029307540506124496, 0.01660626195371151, 0.08852662891149521, 0.04529598355293274, 0.024618037045001984, 0.0, 0.0, 0.010350110940635204, 0.003235119627788663, 0.033057548105716705, 0.05011395737528801, 0.08171573281288147, 0.011668454855680466, 0.062375448644161224, 0.010870278812944889, 0.09730898588895798, 0.0, 0.01955367997288704, 0.0, 0.005785297602415085, 0.0031010659877210855, 0.0, 0.04679791256785393, 0.001237447839230299, 0.0020414518658071756, 0.0017066483851522207, 0.023851564154028893, 0.036163993179798126, 0.0, 0.0, 0.008958152495324612, 0.0, 0.02917611040174961, 0.11986230313777924, 0.07141502946615219, 0.0, 0.005440502893179655, 0.0, 0.0, 0.0, 0.11132220923900604, 0.0, 0.034574780613183975, 0.0, 0.0022026689257472754, 0.10407533496618271, 0.0, 0.08820595592260361, 0.003849416971206665, 0.011658144183456898, 0.024257536977529526, 0.0, 0.1096685454249382, 0.0649605393409729, 0.00362410512752831, 0.09245583415031433, 0.0, 0.02336321584880352, 0.011606519110500813, 0.0, 0.019254030659794807, 0.0, 0.01339785847812891, 0.006473632995039225, 0.04142671823501587, 0.0, 0.0, 0.0, 0.10377579927444458, 0.02421237714588642, 0.0, 0.0, 0.015052597038447857, 0.0, 0.11512929201126099, 0.009618666023015976, 0.001348158111795783, 0.0, 0.05853208899497986, 0.0, 0.08732671290636063, 0.029229668900370598, 0.09442770481109619, 0.010314217768609524, 0.0, 0.09643077850341797, 0.02349824644625187, 0.012878955341875553, 0.07707180082798004, 0.02532980591058731, 0.0, 0.02837076410651207, 0.0, 0.028732305392622948, 0.0037911576218903065, 0.010371836833655834, 0.0, 0.0, 0.041941866278648376, 0.0031200270168483257, 0.007852048613131046, 0.0, 0.017908168956637383, 7.725320756435394e-05, 0.0, 0.059880007058382034, 0.000942476384807378, 0.0, 0.0, 0.04807337000966072, 0.0026486115530133247, 0.023760344833135605, 0.008052240125834942, 0.003223209409043193, 0.0034821105655282736, 0.023711515590548515, 0.01009816862642765, 0.0015451451763510704, 0.07151050865650177, 0.009400703944265842, 0.09157084673643112, 0.0, 0.0, 0.03664393350481987, 0.03231404721736908, 0.0, 0.0, 0.0, 0.0, 0.0031456691212952137, 0.0, 0.0018606899539008737, 0.1324770748615265, 0.001152743468992412, 0.019793204963207245, 0.0, 0.0890156701207161, 0.0, 0.0007479143096134067, 0.0, 0.03814258798956871, 0.0032865749672055244], [0.02402110956609249, 0.012708605267107487, 0.0, 0.0, 0.08472250401973724, 0.06866466999053955, 0.026125721633434296, 0.08774059265851974, 0.0051437439396977425, 0.0, 0.0007823521737009287, 0.06829871237277985, 0.0002718206087592989, 0.0012144146021455526, 0.00679121445864439, 0.02313183806836605, 0.0, 0.030508598312735558, 0.06794316321611404, 0.0, 0.029762422665953636, 0.11974970251321793, 0.016207464039325714, 0.008052357472479343, 0.00509814964607358, 0.10700541734695435, 0.06666216999292374, 0.05266803875565529, 0.014655635692179203, 0.018482407554984093, 0.012617110274732113, 0.0008310033590532839, 0.0, 0.02099192887544632, 0.01475859060883522, 0.012703204527497292, 0.05471117049455643, 0.0712742879986763, 0.055203117430210114, 0.013357304967939854, 0.0, 0.019577480852603912, 0.015565956942737103, 0.0028675715439021587, 0.054292041808366776, 0.006235679145902395, 0.006151505280286074, 0.06656383723020554, 0.03118649311363697, 0.0, 0.0, 0.08792459219694138, 0.08974950760602951, 0.03850868344306946, 0.03495349735021591, 0.10356488078832626, 0.0005747326067648828, 0.09827939420938492, 0.010922207497060299, 0.01368887722492218, 0.026759512722492218, 0.04549209401011467, 0.002783190691843629, 0.0, 0.009691677987575531, 0.04930439963936806, 0.0018115906277671456, 0.0, 0.0, 0.043445851653814316, 0.03742087259888649, 0.004274651408195496, 0.0, 0.00980871170759201, 0.0027398881502449512, 0.0, 0.0, 0.0, 0.007236850913614035, 0.0023644440807402134, 0.02557351067662239, 0.07647919654846191, 0.0044289203360676765, 0.0, 0.0, 0.0, 9.673809836385772e-05, 0.05901869386434555, 0.045904796570539474, 0.039772287011146545, 0.011260345578193665, 0.007279952988028526, 0.013262275606393814, 0.06080252677202225, 0.00023418762430083007, 0.02352420799434185, 0.00018190899572800845, 0.0012360933469608426, 0.0, 0.006090248003602028, 0.001703424146398902, 0.020075585693120956, 0.0925767570734024, 0.03628498315811157, 0.0500536747276783, 0.03595195338129997, 0.03390873223543167, 0.03931106626987457, 0.03180791810154915, 0.003436062019318342, 0.04903925582766533, 0.045658648014068604, 0.03345983475446701, 0.028015276417136192, 0.018102634698152542, 0.03190668299794197, 0.0, 0.055176153779029846, 0.03441339731216431, 0.0007200834224931896, 0.0, 0.0022755854297429323, 0.0009336227085441351, 0.047997619956731796, 0.0, 0.0037063839845359325, 0.004204822704195976, 0.0475236251950264, 0.0713810920715332, 0.05044825002551079, 0.09306897968053818, 0.0, 0.009924249723553658, 0.015658803284168243, 0.0, 0.0, 0.022436361759901047, 0.0035072483588010073, 0.08062094449996948, 0.051701225340366364, 0.0, 0.03173556923866272, 0.002977140247821808, 0.0004135537601541728, 0.005242493469268084, 0.000506559677887708, 0.09824172407388687, 0.0213508028537035, 0.012514304369688034, 0.004276483319699764, 0.058746129274368286, 0.0023874458856880665, 0.002406373852863908, 0.013711447827517986, 0.0032491423189640045, 0.001533436356112361, 0.054743025451898575, 0.0033781013917177916, 0.10397889465093613, 0.04363606870174408, 0.06538786739110947, 0.0, 0.001120534259825945, 0.002146295737475157, 0.012203595601022243, 0.02116132713854313, 0.0, 0.10100217163562775, 0.002548093441873789, 0.0006243922398425639, 0.018775859847664833, 0.010155318304896355, 0.0, 0.011709174141287804, 0.034134313464164734, 0.005455673206597567, 0.0011051369365304708, 0.04526045545935631, 0.05516532436013222, 0.054999180138111115, 0.01968013308942318, 0.001094319624826312, 0.08014282584190369, 0.03268671780824661, 0.028631258755922318, 0.04624819755554199, 0.0733821764588356, 0.013633963651955128, 0.0, 0.002494163578376174, 0.08943655341863632, 0.011502635665237904, 0.06676063686609268, 0.04371242597699165, 0.0032713310793042183, 0.03813832253217697, 0.11891389638185501, 0.07895062863826752, 0.0, 0.04372233897447586, 0.09001919627189636, 0.08069770783185959, 0.05004161223769188, 0.07991084456443787, 0.01715335249900818, 0.05320851132273674, 0.028939979150891304, 0.03847102075815201, 0.04032516106963158, 0.0, 0.0, 0.07163846492767334, 0.058417003601789474, 0.0, 0.0028335568495094776, 0.041834767907857895, 0.0500066913664341, 0.036901574581861496, 0.07379642874002457, 0.0, 0.03515337407588959, 0.0, 0.01840892806649208, 0.08661314100027084, 0.04070200398564339, 0.036723800003528595, 0.0019883515778928995, 0.024770986288785934, 0.07152155786752701, 0.0005352305597625673, 0.05339113995432854, 0.0, 0.04432138428092003, 0.0, 0.08395322412252426, 0.0, 0.05904864892363548, 0.033355142921209335, 0.0032814147416502237, 0.06687518954277039, 0.026683606207370758, 0.05545477569103241, 0.07561341673135757, 0.05425596237182617, 0.046977147459983826, 0.0019267973257228732, 0.09130866080522537, 0.0062543670646846294, 0.03140827640891075, 0.02556668035686016, 0.062388744205236435, 0.009782475419342518, 0.048684727400541306, 0.003057463327422738, 0.062331922352313995, 0.016631679609417915, 0.05273473262786865, 0.00107889948412776, 0.0321119949221611, 0.12563247978687286, 0.11486822366714478, 0.03705953061580658, 0.02012905478477478, 0.04595327004790306, 0.06049128621816635, 0.023702004924416542, 0.017338158562779427, 0.0, 0.06792370975017548, 0.0398668497800827, 0.016445547342300415, 0.0, 0.06211812049150467, 0.0008684471831656992, 0.044518694281578064, 0.0, 0.01678488962352276, 0.0, 0.01294662058353424, 0.05696459859609604, 0.07177122682332993, 0.025372488424181938, 0.03411322459578514, 0.0, 0.08676311373710632, 0.07845205068588257, 0.016217565163969994, 0.04570245370268822, 0.0019429908134043217, 0.010031692683696747, 0.014588532969355583, 0.003392105456441641, 0.058083970099687576, 0.06637212634086609, 0.006927100941538811, 0.026051782071590424, 0.10845999419689178, 0.003119375556707382, 0.0, 0.05689910426735878, 0.002722518751397729, 0.03881683200597763, 0.06275958567857742, 0.13606539368629456, 0.00401864992454648, 0.009939807467162609, 1.5211053323582746e-05, 0.04930294677615166, 0.0, 0.09771360456943512, 0.051470816135406494, 0.0081614525988698, 0.03645012155175209, 0.08177529275417328, 0.0, 0.010492256842553616, 0.08049353957176208, 0.06040295213460922, 0.0, 0.031059391796588898, 0.006487654522061348, 0.034508369863033295, 0.034730829298496246, 0.029926281422376633, 0.05957736074924469, 0.0016542448429390788, 0.010173982940614223, 0.043504226952791214, 0.0, 0.10731536895036697, 0.07987532764673233, 0.07869108021259308, 0.0022444704081863165, 0.024755442515015602, 0.07430249452590942, 0.026628553867340088, 0.04376545175909996, 0.0, 0.058109838515520096, 0.10336514562368393, 0.08470474183559418, 0.016275007277727127, 0.09925027191638947, 0.010663311928510666, 0.025155959650874138, 0.0396963469684124, 8.760232594795525e-05, 0.02725210227072239, 0.0, 0.07426731288433075, 0.0007950174040161073, 0.0002985177270602435, 0.024800779297947884, 0.008777843788266182, 0.0021324821282178164, 0.005711854435503483, 0.001365167205221951, 0.04981440305709839, 0.05249665305018425, 0.0, 0.0, 0.0, 0.0017118186224251986, 0.05669331178069115, 0.05758874863386154, 0.0018879646668210626, 0.03583110123872757, 0.0, 0.03267477825284004, 0.03199828788638115, 0.07270143181085587, 0.005615629721432924, 0.023174218833446503, 0.046558208763599396, 0.030417904257774353, 0.008934381417930126, 0.0, 0.024874145165085793, 0.00211721146479249, 0.0850895494222641, 0.024214236065745354, 0.0818084254860878, 0.041185617446899414, 0.04239411652088165, 0.03786283731460571, 0.03344784677028656, 0.0015672347508370876, 0.05849454924464226, 0.07774654030799866, 0.0, 0.008064297027885914, 0.0, 0.033594049513339996, 0.0, 0.0, 0.0026185212191194296, 0.03859784081578255, 0.01901203952729702, 0.018994007259607315, 0.0, 2.1851423298357986e-05, 0.0037795521784573793, 0.03465220332145691, 0.15219834446907043, 0.0, 0.0, 0.001493049319833517, 0.026917776092886925, 0.008319777436554432, 0.001475221710279584, 0.10617804527282715, 0.002165829064324498, 0.0, 0.0, 0.00037302405689843, 0.03157112002372742, 0.0445437990128994, 0.07730644941329956, 0.07800048589706421, 0.049805041402578354, 0.10832222551107407, 0.0, 0.06287944316864014, 0.12433823198080063, 0.03615845739841461, 0.08544153720140457, 0.011096099391579628, 0.04790273308753967, 0.002897425089031458, 0.004101587459445, 0.0, 0.0, 0.052500683814287186, 0.0, 0.020104752853512764, 0.0107262022793293, 0.0, 0.0, 0.0075871944427490234, 0.050261788070201874, 0.0, 0.06328611820936203, 0.03932271525263786, 0.00036228957469575107, 0.07569930702447891, 0.06367280334234238, 0.00020258880977053195, 0.029544107615947723, 0.062456242740154266, 0.03371260315179825, 0.029537664726376534, 0.012137810699641705, 0.11519722640514374, 0.040638264268636703, 0.002517038257792592, 0.03730713203549385, 0.0036679180338978767, 0.0069280099123716354, 0.03769182041287422, 0.07045091688632965, 0.0039056220557540655, 0.0222402885556221, 0.023947764188051224, 0.04086007922887802, 0.04737212508916855, 0.11966869980096817, 0.0, 0.001462735584937036, 0.043525610119104385, 0.029796024784445763, 0.05611134693026543, 0.0, 0.022078080102801323, 0.0019614661578089, 0.01092530321329832, 0.03491051867604256, 0.002068497706204653, 0.0012501179007813334, 0.009643113240599632, 0.07596693933010101, 0.000511088699568063, 0.07201042771339417, 0.0016456677112728357, 0.013637058436870575, 0.021378880366683006, 0.0407317690551281, 0.01220827829092741, 0.0062877107411623, 0.049848902970552444, 0.08333076536655426, 0.0275981817394495, 0.033762454986572266, 0.0, 0.14890678226947784, 0.02430499903857708, 0.0, 0.0, 0.03988788649439812, 0.0008060616091825068, 0.05286159738898277, 0.0013681907439604402, 0.0, 0.06692507863044739, 0.032690148800611496, 0.0018222560174763203, 4.208479731460102e-05, 0.09192214906215668, 0.0023843739181756973, 0.0, 0.020990898832678795, 0.06699599325656891, 0.05169891566038132], [0.06014927476644516, 0.056686047464609146, 0.0, 0.0, 0.11302854865789413, 0.1064377874135971, 0.0, 0.12668052315711975, 0.03219360113143921, 0.0, 0.0, 0.0, 0.028389260172843933, 0.0, 0.0, 0.0, 0.0, 0.009343216195702553, 0.0, 0.0, 0.08114024996757507, 0.002324228873476386, 0.02191321738064289, 0.046686574816703796, 0.04094914346933365, 0.08133994042873383, 0.026673687621951103, 0.06628323346376419, 0.04533917456865311, 0.03819357231259346, 0.05158013477921486, 0.0316491574048996, 0.0, 0.04926539957523346, 0.04671759158372879, 0.0, 0.0076695759780704975, 0.04727188125252724, 0.017721286043524742, 0.0480511449277401, 0.03698944300413132, 0.057517219334840775, 0.014686844311654568, 0.0, 0.004891977179795504, 0.0, 0.0, 0.09085921198129654, 0.011420517228543758, 0.0, 0.0, 0.1067730188369751, 0.0, 0.0, 0.015877792611718178, 0.16079528629779816, 0.04893852025270462, 0.1548669934272766, 0.028803225606679916, 0.03350939601659775, 0.008880700916051865, 0.0, 0.0, 0.0, 0.06748304516077042, 0.00987818744033575, 0.0, 0.0, 0.0, 0.02455248311161995, 0.005821920931339264, 0.0, 0.0, 0.002396735129877925, 0.023944104090332985, 0.011746788397431374, 0.0, 0.0, 0.0, 0.0, 0.039208728820085526, 0.03410213440656662, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09030855447053909, 0.006689737550914288, 0.010781324468553066, 0.010198676958680153, 0.0, 0.026378383859992027, 0.051572758704423904, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009244540706276894, 0.06400325894355774, 0.0, 0.022063082084059715, 0.0, 0.0, 0.018413972109556198, 0.1287512183189392, 0.0, 0.06222262233495712, 0.054865673184394836, 0.011754235252737999, 0.0, 0.011994357220828533, 0.07070516794919968, 0.0, 0.05283471569418907, 0.0832139179110527, 0.004156508948653936, 0.0, 0.0, 0.0, 0.06240919977426529, 0.0, 0.009894237853586674, 0.0, 0.01338514219969511, 0.047156963497400284, 0.03683388605713844, 0.018731141462922096, 0.0, 0.01191408559679985, 0.0, 0.0, 0.02682306058704853, 0.06734564900398254, 0.09629122167825699, 0.08443846553564072, 0.0, 0.0, 0.0, 0.003177607897669077, 0.025694549083709717, 0.0, 0.0, 0.06963469833135605, 0.014156538061797619, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07133197039365768, 0.0, 0.0, 0.06884437799453735, 0.0, 0.04923248663544655, 0.055402450263500214, 0.02764883264899254, 0.0, 0.0, 0.0, 0.011656558141112328, 0.09805317223072052, 0.0, 0.06213857978582382, 0.006434939336031675, 0.0019115936011075974, 0.0, 0.0, 0.0, 0.0, 0.03242520987987518, 0.0, 0.0, 0.0, 0.0, 0.08596517890691757, 0.00426350487396121, 0.0, 0.0, 0.0760791003704071, 0.0059913876466453075, 0.0, 0.04102534055709839, 0.013785545714199543, 0.0013353853719308972, 0.010418184101581573, 0.07205185294151306, 0.0, 0.002640696708112955, 0.0, 0.0, 0.0, 0.07563073188066483, 0.12237054109573364, 0.0, 0.08598608523607254, 0.06962642818689346, 0.0606679767370224, 0.004794485401362181, 0.06670619547367096, 0.021719515323638916, 0.1129223108291626, 0.0035131273325532675, 0.011825242079794407, 8.266571967396885e-05, 0.0, 0.0, 0.05392765626311302, 0.04150780662894249, 0.0, 0.009552424773573875, 0.01072228979319334, 0.006380815990269184, 0.0417182631790638, 0.102825827896595, 0.0, 0.09685825556516647, 0.0, 0.013784406706690788, 0.0369938462972641, 0.000311477662762627, 0.014967809431254864, 0.0, 0.13469301164150238, 0.10862870514392853, 0.0, 0.059974536299705505, 0.048080846667289734, 0.08686818927526474, 0.0, 0.0, 0.0, 0.04684403911232948, 0.0, 0.0, 0.023482922464609146, 0.004639517981559038, 0.03510454669594765, 0.058061715215444565, 0.0, 0.04366897791624069, 0.0, 0.09965164214372635, 0.008248275145888329, 0.01652528904378414, 0.022916452959179878, 0.02464299462735653, 0.07578933984041214, 0.03130755200982094, 0.0, 0.10772722214460373, 0.02014479972422123, 0.0, 0.0, 0.029403110966086388, 0.08213881403207779, 0.08686309307813644, 0.016767170280218124, 0.08443084359169006, 0.04920794442296028, 0.061212167143821716, 0.025918882340192795, 0.0, 0.08958305418491364, 0.060027699917554855, 0.03455261513590813, 0.01948656141757965, 0.0, 0.0799809917807579, 0.0, 0.021943489089608192, 0.008511120453476906, 0.03623910993337631, 0.0, 0.07261104881763458, 0.044724054634571075, 0.09115029871463776, 0.0, 0.0, 0.0, 0.029880302026867867, 0.045381151139736176, 0.0, 0.06871773302555084, 0.0, 0.0, 0.0, 0.0, 0.0581958144903183, 0.09266982227563858, 0.08621934056282043, 0.019708197563886642, 0.12340708822011948, 0.0, 0.0, 0.012092811986804008, 0.0, 0.029782036319375038, 0.0, 0.08324885368347168, 0.051184266805648804, 0.00846211425960064, 0.0, 0.044891659170389175, 0.0, 0.042319633066654205, 0.04543929919600487, 0.018904058262705803, 0.06699157506227493, 0.09661968797445297, 0.016103902831673622, 0.0, 0.07985983043909073, 0.0065932744182646275, 0.0, 0.0251548383384943, 0.026503881439566612, 0.0, 0.0, 0.027499502524733543, 0.01265292800962925, 0.0, 0.009138094261288643, 0.08846752345561981, 0.0, 0.10213790088891983, 0.13371217250823975, 0.05924756079912186, 0.0, 0.06135481595993042, 0.10621434450149536, 0.017541484907269478, 0.0, 0.0, 0.05181572958827019, 0.10366609692573547, 0.06391182541847229, 0.0, 0.12664556503295898, 0.0, 0.050755761563777924, 0.06900358200073242, 0.0, 0.004729973617941141, 0.0, 0.06158395856618881, 0.0, 0.0, 0.0, 0.028664948418736458, 0.0, 0.0, 0.049285370856523514, 0.0, 0.11293864250183105, 0.0, 0.0, 0.0, 0.0, 0.09840472042560577, 0.0, 0.02902446687221527, 0.0, 0.003403973998501897, 0.045662011951208115, 0.0, 0.05466432869434357, 0.0, 0.05724130943417549, 0.024249250069260597, 0.0, 0.0, 0.0, 0.04031853750348091, 0.0, 0.039044395089149475, 0.008772647008299828, 0.05509043484926224, 0.0, 0.052691321820020676, 0.02400452084839344, 0.0, 0.0, 0.05318170785903931, 0.010172703303396702, 0.0, 0.003408274846151471, 0.0, 0.052595607936382294, 0.0, 0.0, 0.0, 0.010478366166353226, 0.034112270921468735, 0.01000566128641367, 0.0, 0.0, 0.012175381183624268, 0.0, 0.12543000280857086, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07307247072458267, 0.0, 0.01940549910068512, 0.0, 0.0, 0.036625489592552185, 0.0, 0.047856405377388, 0.05801234394311905, 0.022153303027153015, 0.05309415981173515, 0.0, 0.10262481123209, 0.14679670333862305, 0.0, 0.07974985241889954, 0.0, 0.0, 0.021602949127554893, 0.0, 0.02982165291905403, 0.0, 0.049787748605012894, 0.0, 0.04587148129940033, 0.0, 0.0, 0.0, 0.08685339987277985, 0.014185535721480846, 0.0, 0.05691061168909073, 0.0, 0.01761142909526825, 0.087130106985569, 0.023531179875135422, 0.0, 0.0, 0.04554223641753197, 0.10929357260465622, 0.0, 0.0012368044117465615, 0.06716810166835785, 0.0, 0.008076170459389687, 0.026092419400811195, 0.0, 0.0, 0.02680039033293724, 0.0016669044271111488, 0.0, 0.04996234178543091, 0.007971853949129581, 0.0, 0.03342272341251373, 0.04986797645688057, 0.0, 0.0, 0.0, 0.0, 0.05049293488264084, 0.0, 0.09879473596811295, 0.0, 0.0, 0.0, 0.0, 0.06568463146686554, 0.020337926223874092, 0.05993235856294632, 0.039813101291656494, 0.05020198971033096, 0.0, 0.07206424325704575, 0.034790702164173126, 0.029508430510759354, 0.013944309204816818, 0.0, 0.00952792540192604, 0.012734436430037022, 0.0, 0.012634976767003536, 0.0, 0.08515376597642899, 0.04623683914542198, 0.0, 0.0, 0.0, 0.0, 0.05363859608769417, 0.0, 0.0, 0.11901659518480301, 0.08117201179265976, 0.050768520683050156, 0.0, 0.14723822474479675, 0.0, 0.0, 0.0, 0.05955604091286659, 0.0]], "last_cam": "3", "nombre": "Jose Luis Tenchil", "ts": 1782406221.1874743, "actualizaciones_globales": 8}, "106": {"galeria_deep": [[0.016346456483006477, 0.08857869356870651, 0.0, 0.0, 0.07253731787204742, 0.06270920485258102, 0.0268650334328413, 0.09895219653844833, 0.049562130123376846, 0.0, 0.0, 0.03781307861208916, 0.027973275631666183, 0.0, 0.03697581961750984, 0.0, 0.03248514607548714, 0.02371961437165737, 0.0, 0.0, 0.09006814658641815, 0.07592955976724625, 4.510680082603358e-05, 0.014053291641175747, 0.006877675652503967, 0.0799640640616417, 0.06776256114244461, 0.09431443363428116, 0.005075956229120493, 0.022809892892837524, 0.09279201924800873, 0.08740238100290298, 0.0, 0.056325215846300125, 0.048427071422338486, 0.0, 0.011890889145433903, 0.000764169089961797, 0.07085108757019043, 0.036486998200416565, 0.03056045062839985, 0.06491482257843018, 0.055006735026836395, 0.0, 0.034725677222013474, 0.0034909844398498535, 0.0419103242456913, 0.10205589979887009, 0.00767978560179472, 0.0, 0.0, 0.09631418436765671, 0.0, 0.0028877786826342344, 0.06859869509935379, 0.13394244015216827, 0.0709894523024559, 0.09775742888450623, 0.03935384750366211, 0.0, 0.0, 0.0, 0.001868784544058144, 0.010294740088284016, 0.0, 0.00821752194315195, 0.07274837046861649, 0.0, 0.0, 0.006972942501306534, 0.07005652040243149, 0.0, 0.0009805737063288689, 0.03265685588121414, 0.06449493020772934, 0.0031089603435248137, 0.0, 0.0, 0.00026911398163065314, 0.0012233658926561475, 0.023691659793257713, 0.007686476223170757, 0.0, 0.0, 0.0, 0.0, 0.0, 0.014604315161705017, 0.0, 0.0, 0.033149585127830505, 0.022226432338356972, 0.0974084734916687, 0.07697973400354385, 0.002361423335969448, 0.002581964014098048, 0.0, 0.0, 0.0013323535677045584, 0.0007238373509608209, 0.0, 0.03198923543095589, 0.08352085202932358, 0.001806517830118537, 0.022346902638673782, 0.0001240081328433007, 0.004040925297886133, 0.0012215729802846909, 0.023719193413853645, 0.0615367516875267, 0.12031625211238861, 0.0, 0.013311132788658142, 0.0, 0.006057642865926027, 0.06239963322877884, 0.0, 0.011955252848565578, 0.11553766578435898, 0.0007617791998200119, 0.0014586515026167035, 0.0, 0.005901058204472065, 0.04769579693675041, 0.0, 0.0, 5.318480543792248e-05, 0.059556882828474045, 0.0019998059142380953, 0.022221792489290237, 0.04874526709318161, 0.0, 0.0006593796424567699, 0.0, 0.0, 0.028231730684638023, 0.03269965574145317, 0.039251282811164856, 0.02383323945105076, 0.0, 9.55803770921193e-05, 0.015936389565467834, 0.018949899822473526, 0.0, 0.00015969356172718108, 0.008020389825105667, 0.13798972964286804, 0.048606354743242264, 0.0, 0.0, 0.04513134807348251, 0.0, 0.0, 0.04237363114953041, 0.0014145002933219075, 0.004542549606412649, 0.06826946884393692, 0.028659185394644737, 0.07724618166685104, 0.021168097853660583, 0.03499826416373253, 0.0, 0.0, 0.03809969499707222, 0.06634846329689026, 0.04017405956983566, 0.0, 0.03467283025383949, 0.00588208669796586, 0.09832090884447098, 0.0, 0.0, 0.0021727357525378466, 0.0, 0.08695823699235916, 0.004297804087400436, 0.0, 0.0019141287775710225, 0.03617559373378754, 0.09038805961608887, 0.0, 0.0005693751736544073, 0.02253846824169159, 0.07325179874897003, 0.0025468666572123766, 0.0008716110605746508, 0.025902708992362022, 0.06416814029216766, 0.0, 0.02655216120183468, 0.10059039294719696, 0.0018470920622348785, 0.00501670315861702, 0.0, 0.009408141486346722, 0.0, 0.1263057291507721, 0.07253510504961014, 0.0, 0.0185845959931612, 0.07962504774332047, 0.0488080233335495, 0.01035032793879509, 0.10582946985960007, 0.01830485835671425, 0.037291377782821655, 0.0016426158836111426, 0.0, 0.03082340583205223, 0.0, 0.0, 0.04597559571266174, 0.05987783521413803, 0.0, 0.07244715839624405, 0.04997571185231209, 0.017961492761969566, 0.03702998161315918, 0.1541908085346222, 0.009051158092916012, 0.03293443098664284, 0.0, 0.0002653330739121884, 0.08122801780700684, 0.0, 0.04813660681247711, 0.0, 0.0412793830037117, 0.05611639842391014, 0.0, 0.027459295466542244, 0.007478936109691858, 0.11299506574869156, 0.0, 0.03365615755319595, 0.0, 0.02380896732211113, 0.0, 0.0, 0.0, 0.014310202561318874, 0.13391625881195068, 0.06862620264291763, 0.04240874573588371, 0.03124595619738102, 0.014409272000193596, 0.10737878829240799, 0.02303319238126278, 0.04828203096985817, 0.0, 0.0, 0.051380354911088943, 0.04457346349954605, 0.009386822581291199, 0.11584258824586868, 0.0595504529774189, 0.009205514565110207, 0.020961910486221313, 0.010341883637011051, 0.028161998838186264, 0.03746148571372032, 0.0031392567325383425, 0.08092265576124191, 0.0, 0.06547106802463531, 0.0636681616306305, 0.0, 0.014953315258026123, 0.00668348791077733, 0.06692501157522202, 0.009407681412994862, 0.0, 0.09286995977163315, 0.0, 0.002517384709790349, 0.0, 0.04443824291229248, 0.0, 0.021754508838057518, 0.028487347066402435, 0.10563568025827408, 0.009228203445672989, 0.015191895887255669, 0.0, 0.07953812927007675, 0.06198427453637123, 0.0, 0.04941191524267197, 0.005272518377751112, 0.0024875362869352102, 0.0, 0.0, 0.01284749060869217, 0.03848419338464737, 0.09958403557538986, 0.09611911326646805, 0.16121318936347961, 0.000998650910332799, 0.01668611355125904, 0.06744313985109329, 0.00024149594537448138, 0.07641912996768951, 0.006161568220704794, 0.04785201698541641, 0.024822169914841652, 0.08779647201299667, 0.00020724737260024995, 0.004607000853866339, 0.000894891214556992, 0.031883228570222855, 0.04097740352153778, 0.036662742495536804, 0.07353339344263077, 0.10511665046215057, 0.00853120256215334, 0.05321464315056801, 0.0015326854772865772, 0.04334266856312752, 0.018870003521442413, 0.022134533151984215, 0.022434361279010773, 0.005699686706066132, 0.0, 0.0008942317217588425, 0.0, 0.01781323365867138, 0.02488725632429123, 0.09818359464406967, 0.0060391961596906185, 0.07754193991422653, 0.07186786830425262, 0.04167727380990982, 0.0, 0.04089803621172905, 0.05774275213479996, 0.007673346903175116, 0.0, 0.0, 0.05138036236166954, 0.07506231963634491, 0.0639750063419342, 0.019491322338581085, 0.05483917519450188, 0.0004951754817739129, 0.07754212617874146, 0.07880904525518417, 0.0026246579363942146, 0.01918201893568039, 0.0, 0.0, 0.002106213243678212, 0.0, 0.0, 0.0, 0.0, 0.002206276636570692, 0.03184398263692856, 0.007967352867126465, 0.086912140250206, 0.0, 0.0, 0.0, 0.011089961044490337, 0.03731013461947441, 0.01744464598596096, 0.0, 0.015381108038127422, 0.0, 0.0, 0.0, 0.02743513509631157, 0.0, 0.06174377351999283, 0.055642060935497284, 0.0, 0.0, 0.0, 0.0011746619129553437, 0.0, 0.022054357454180717, 0.008249970152974129, 0.057296719402074814, 0.00015063259343151003, 0.015345239080488682, 0.014065226539969444, 0.0028362201992422342, 0.0, 0.09367095679044724, 0.0, 0.0, 0.01845512166619301, 0.0, 0.012284932658076286, 0.0, 0.0, 0.00413111224770546, 0.008217131718993187, 0.030817652121186256, 0.002825702540576458, 0.0, 0.006282575894147158, 0.0, 0.03086239844560623, 0.1145402267575264, 0.012030716054141521, 0.0, 0.0, 0.055959273129701614, 0.0, 0.0, 0.14426881074905396, 0.0, 0.01885414309799671, 0.0, 0.020167039707303047, 0.0681430846452713, 0.0, 0.1019221767783165, 0.00011572225048439577, 0.0, 0.022964322939515114, 0.0, 0.04562500864267349, 0.10264072567224503, 0.0, 0.07693648338317871, 0.023550257086753845, 0.037565261125564575, 0.0367862768471241, 0.0, 0.08498549461364746, 0.0, 0.03438446298241615, 0.0, 0.058781448751688004, 0.0027202835772186518, 0.0, 0.0, 0.13829171657562256, 0.0, 0.0, 0.03543921187520027, 0.02252422459423542, 0.0, 0.04777269437909126, 0.0, 0.0, 0.0, 0.023922724649310112, 0.0234425850212574, 0.037911854684352875, 0.0001542800455354154, 0.07370397448539734, 0.004787997808307409, 0.0, 0.07808985561132431, 0.031488724052906036, 0.0, 0.08152014017105103, 0.06299317628145218, 0.0, 0.033919595181941986, 0.036903902888298035, 0.0017465600976720452, 0.04330498352646828, 0.03465595096349716, 0.0, 0.0, 0.020709482952952385, 0.0, 0.09340929239988327, 0.00025336016551591456, 0.045334238559007645, 0.013155068270862103, 0.0, 0.06153436005115509, 0.024941636249423027, 0.0, 0.0, 0.09461654722690582, 0.024285994470119476, 0.038448575884103775, 0.0, 0.0036295864265412092, 0.00017789033881854266, 0.0166291743516922, 0.0, 0.001987940166145563, 0.11420348286628723, 0.008999372832477093, 0.008063257671892643, 0.021936479955911636, 0.0, 0.06928955018520355, 0.08772259205579758, 0.0, 0.00035975297214463353, 0.0, 0.011091531254351139, 0.02267599105834961, 0.0, 0.0, 0.12369570136070251, 0.037029724568128586, 0.03204839676618576, 0.004497797228395939, 0.09799015522003174, 0.0, 0.03774130716919899, 0.0, 0.026283934712409973, 0.01043742522597313]], "last_cam": "7", "nombre": "Desconocido", "ts": 1782406200.5437799, "actualizaciones_globales": 2}, "107": {"galeria_deep": [[0.10124744474887848, 0.05243450030684471, 0.0, 0.0, 0.006594582460820675, 0.16852186620235443, 0.0010495273163542151, 0.10770609229803085, 0.018613474443554878, 0.014418400824069977, 0.00436389772221446, 0.025794321671128273, 0.0, 0.0, 0.028119714930653572, 0.0001360669411951676, 0.03120715171098709, 0.01599765196442604, 0.09954182058572769, 0.001121650799177587, 0.10299821197986603, 0.07315997779369354, 0.029684439301490784, 0.034706536680459976, 0.029232701286673546, 0.08910799771547318, 0.034824438393116, 0.06878166645765305, 0.003213822143152356, 0.07689504325389862, 0.09879399091005325, 0.019453110173344612, 0.0, 0.024500910192728043, 0.0, 0.0, 0.028128594160079956, 0.0028919363394379616, 0.06284036487340927, 0.06328894197940826, 0.04633228853344917, 0.06446172297000885, 0.074280746281147, 0.0, 0.04343200847506523, 0.0, 0.06749923527240753, 0.11904540657997131, 0.030607054010033607, 0.0, 0.0009814695222303271, 0.09430697560310364, 0.030048754066228867, 5.3172020670899656e-06, 0.1122364029288292, 0.14130687713623047, 0.004421336110681295, 0.03978210687637329, 0.06465284526348114, 0.00036423461278900504, 0.0, 0.007783740293234587, 0.01575639843940735, 0.004274919629096985, 0.007686770986765623, 0.0, 0.051878657191991806, 0.0, 0.0, 0.0801301896572113, 0.0906291976571083, 0.004506343975663185, 0.02777109667658806, 0.044887151569128036, 0.08650780469179153, 0.0, 0.0, 0.0, 0.0, 0.0013630285393446684, 0.023088660091161728, 0.0041803959757089615, 0.0, 0.0, 0.0, 0.0017836145125329494, 0.0002921597915701568, 0.029506461694836617, 0.001907560508698225, 0.0, 0.04898114502429962, 0.009987270459532738, 0.021986734122037888, 0.08510005474090576, 0.027147503569722176, 0.026634080335497856, 0.001477685640566051, 0.0, 0.0, 0.007341187912970781, 0.0, 0.054619766771793365, 0.0, 0.011158115230500698, 0.05037781968712807, 0.026098789647221565, 0.01044407393783331, 0.061339087784290314, 0.03800913691520691, 0.05824665352702141, 0.04581742361187935, 0.004795809276401997, 0.08053664118051529, 0.013801627792418003, 0.0009539894526824355, 0.009598445147275925, 0.0, 0.05971040204167366, 0.013130221515893936, 0.0252230241894722, 0.0, 0.0, 0.0, 0.05685519054532051, 0.004751336295157671, 0.01719939522445202, 0.003049330087378621, 0.024644946679472923, 0.06312676519155502, 0.0498194545507431, 0.048231013119220734, 0.0, 0.0, 0.042978547513484955, 0.0, 0.0, 0.0010781316086649895, 0.045605190098285675, 0.11429262906312943, 0.0, 0.0036339133512228727, 0.02952420711517334, 0.031535133719444275, 0.008008127100765705, 0.006094628944993019, 0.03919504955410957, 0.16105639934539795, 0.03739210590720177, 0.00809300597757101, 0.015296746976673603, 0.02088974602520466, 0.0, 0.026892460882663727, 0.0029343627393245697, 0.050251420587301254, 0.0, 0.0997592955827713, 0.010695409961044788, 0.10673592984676361, 0.01755617745220661, 0.012630765326321125, 0.0, 0.0, 0.008886883966624737, 0.05690528079867363, 0.0730605497956276, 0.0, 0.08598677068948746, 0.00839854683727026, 0.07551689445972443, 0.06155731901526451, 0.03056946210563183, 0.0027442709542810917, 0.016372786834836006, 0.03926919400691986, 0.06090851128101349, 0.028691787272691727, 0.03436918929219246, 0.0, 0.06127871200442314, 0.02734493836760521, 0.001357236527837813, 0.030133184045553207, 0.13917598128318787, 0.006387521047145128, 0.007181744556874037, 0.0, 0.05901894345879555, 0.0, 0.0030647320672869682, 0.016722695901989937, 0.03841026499867439, 0.0389646477997303, 0.014362950809299946, 0.0, 0.005032522603869438, 0.028899213299155235, 0.07225680351257324, 0.0009898045100271702, 0.053172603249549866, 0.02724970132112503, 0.07058899849653244, 0.002757954178377986, 0.102308489382267, 0.013952433131635189, 0.09274212270975113, 0.0015623437939211726, 0.0, 0.0010505839018151164, 0.0, 0.0, 0.007303236518055201, 0.06142521649599075, 0.0, 0.05333676189184189, 0.02537848986685276, 0.002799962880089879, 0.011055340059101582, 0.0901033952832222, 0.01289683673530817, 0.0, 0.008606248535215855, 0.028777487576007843, 0.10298861563205719, 0.003739458741620183, 0.04754921421408653, 0.0, 0.05193456634879112, 0.04837999865412712, 0.007723932154476643, 0.08335409313440323, 0.02873987704515457, 0.027536610141396523, 0.004136737436056137, 0.058634936809539795, 0.0, 0.0014088897733017802, 0.003251437097787857, 0.0, 0.00821188185364008, 0.038011111319065094, 0.09440064430236816, 0.015689825639128685, 0.013309406116604805, 0.04902565851807594, 0.016007153317332268, 0.08615023642778397, 0.03968621790409088, 0.01089178491383791, 0.0, 0.0, 0.06883260607719421, 0.09780584275722504, 0.0016126028494909406, 0.0477910116314888, 0.029961461201310158, 0.0, 0.03253323957324028, 0.01206205040216446, 0.04593389853835106, 0.04324263706803322, 0.0024015626404434443, 0.05147206410765648, 0.0, 0.04055123031139374, 0.05804393067955971, 0.0, 0.005768885836005211, 0.030410978943109512, 0.06750132888555527, 0.004303759895265102, 0.0, 0.07340585440397263, 0.050341859459877014, 0.03829871863126755, 0.0, 0.010650279000401497, 0.0, 0.029801705852150917, 0.008794408291578293, 0.12686817348003387, 0.02189185656607151, 0.036375731229782104, 0.0, 0.07568715512752533, 0.001723972731269896, 0.0, 0.009672204032540321, 0.05408554524183273, 0.004074926022440195, 0.004731352441012859, 0.00031710590701550245, 0.026560671627521515, 0.0013619402889162302, 0.12055632472038269, 0.06293179094791412, 0.10845490545034409, 0.007192819379270077, 0.00025440187891945243, 0.04786171019077301, 0.0, 0.058839768171310425, 0.009161483496427536, 0.035288065671920776, 0.0028063005302101374, 0.03166292980313301, 0.0003653250460047275, 0.04881554841995239, 0.0003951648832298815, 0.001748205628246069, 0.06182198226451874, 0.00482723256573081, 0.0732707753777504, 0.06584949791431427, 0.009662168100476265, 0.030169593170285225, 0.0, 0.05178925767540932, 0.017670348286628723, 0.0709768682718277, 0.03331979364156723, 0.01568898744881153, 0.0006727456930093467, 0.028044836595654488, 0.002479391638189554, 0.027326205745339394, 0.010968899354338646, 0.05028924718499184, 0.0, 0.0573037825524807, 0.0868203341960907, 0.030973346903920174, 0.0, 0.04790728911757469, 0.071970634162426, 0.0, 0.0, 0.011968454346060753, 0.00698069715872407, 0.08306042104959488, 0.056372590363025665, 0.008780629374086857, 0.07163841277360916, 0.0, 0.014926256611943245, 0.04877162352204323, 0.004542914684861898, 0.0, 0.0, 0.0, 0.0, 0.0006688690627925098, 0.0, 0.004036508966237307, 0.009963326156139374, 0.018125362694263458, 0.030045920982956886, 0.04404432326555252, 0.0654854029417038, 0.0, 0.02899738773703575, 0.0, 0.0, 0.05193687230348587, 0.006744221318513155, 0.0017395360628142953, 0.0155442263931036, 0.003458252176642418, 0.04625190049409866, 0.0077931834384799, 0.028031444177031517, 0.029557257890701294, 0.01569899544119835, 0.1071242168545723, 0.031768448650836945, 0.0, 0.0, 0.05138210579752922, 0.0, 0.059816356748342514, 0.0, 0.06032898649573326, 0.0, 0.030171679332852364, 0.04024951159954071, 0.0, 0.0, 0.06683936715126038, 0.0, 0.0, 0.09712464362382889, 0.00734293507412076, 0.01527705043554306, 0.0, 0.030294151976704597, 0.02977091632783413, 0.05452140048146248, 0.0, 0.004312239587306976, 0.0, 0.0, 0.0017899149097502232, 0.039688922464847565, 0.06087980046868324, 0.005893146153539419, 0.003886178368702531, 0.0, 0.026113338768482208, 0.011681598611176014, 0.0, 0.08646532148122787, 0.0005008330917917192, 0.0552557073533535, 0.0, 0.03387874364852905, 0.09967207163572311, 0.05119176581501961, 0.09799730777740479, 0.006643190979957581, 0.000849420961458236, 0.04470973089337349, 0.0, 0.030904825776815414, 0.11099092662334442, 0.0014293339336290956, 0.08903814107179642, 0.004546282812952995, 0.04517862945795059, 0.0012911680387333035, 0.008202528581023216, 0.04543159902095795, 0.008041520603001118, 0.013406243175268173, 0.00018458481645211577, 0.05717380717396736, 0.00037520728074014187, 0.0, 0.0, 0.11937063187360764, 0.029010865837335587, 0.0, 0.04083719477057457, 0.04806993901729584, 0.0, 0.0003099109453614801, 0.038557268679142, 0.00223555532284081, 0.0, 0.0018720657099038363, 0.000584071094635874, 0.06944577395915985, 0.004649778828024864, 0.12142611294984818, 0.002429818967357278, 0.013693698681890965, 0.022964762523770332, 0.059983979910612106, 0.0, 0.08349165320396423, 0.07180643081665039, 0.0, 0.06748849153518677, 0.007325721904635429, 0.055828895419836044, 0.02507319673895836, 0.029818208888173103, 0.0, 0.0, 0.05709363892674446, 0.0, 0.10644054412841797, 0.0, 0.07054745405912399, 0.06531496345996857, 0.0, 0.07374869287014008, 0.0019033566350117326, 0.0, 0.04270218312740326, 0.07681223750114441, 0.010523869656026363, 0.05586952343583107, 0.0, 0.030964281409978867, 0.021049652248620987, 0.09813325107097626, 0.0, 0.041466306895017624, 0.08394820988178253, 0.0, 0.036590177565813065, 0.06339136511087418, 0.0, 0.008245880715548992, 0.02516239695250988, 0.0023180979769676924, 0.0, 0.023337330669164658, 0.0026005308609455824, 0.011055905371904373, 0.0, 0.0495033822953701, 0.025590011849999428, 0.07645585387945175, 0.03551577404141426, 0.01007344014942646, 0.05302588641643524, 0.0005459549138322473, 0.042300425469875336, 0.0, 0.10435444861650467, 0.00818823091685772], [0.0, 0.06638559699058533, 0.0, 0.0, 0.036771684885025024, 0.09453603625297546, 0.0006330271135084331, 0.030878230929374695, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1022573709487915, 0.0, 0.000635736680123955, 0.09602397680282593, 0.008812778629362583, 0.009326614439487457, 0.038364920765161514, 0.059858109802007675, 0.029918426647782326, 0.039448101073503494, 0.038223959505558014, 0.007032537367194891, 0.0610208697617054, 0.039796825498342514, 0.01259851548820734, 0.0, 0.05750296264886856, 0.028767649084329605, 0.0015881673898547888, 0.0, 0.05388675630092621, 0.12263524532318115, 0.008721986785531044, 0.0003113041166216135, 0.10497980564832687, 0.0479644238948822, 0.0, 0.009301387704908848, 0.009881403297185898, 0.017439935356378555, 0.05931755155324936, 0.0, 0.0, 0.0, 0.08280739933252335, 0.004959647078067064, 0.050309259444475174, 0.10566911846399307, 0.13743454217910767, 0.04594935104250908, 0.08101187646389008, 0.04962501674890518, 0.0, 0.0, 0.03828613460063934, 0.0, 0.020428748801350594, 0.000727439415641129, 0.031455960124731064, 0.11415305733680725, 0.0026140541303902864, 0.0, 0.0003347159072291106, 0.0007408098317682743, 0.0, 0.012863792479038239, 0.0021930201910436153, 0.022106323391199112, 0.0, 0.0, 0.0, 0.0, 0.04222869127988815, 0.059908922761678696, 0.01664370857179165, 0.0020262529142200947, 0.001546778716146946, 0.0, 0.0, 0.0, 0.038364097476005554, 0.009945477358996868, 0.0, 0.013378066010773182, 0.002582255518063903, 0.09372349828481674, 0.11355950683355331, 0.018918713554739952, 0.023739533498883247, 0.0, 0.0, 5.005218554288149e-05, 0.012314940802752972, 0.003573823720216751, 0.025598203763365746, 0.10913809388875961, 0.045531995594501495, 0.01599237136542797, 0.009184315800666809, 0.012176115065813065, 0.0006998947355896235, 0.039341360330581665, 0.03719333931803703, 0.07020165771245956, 0.0, 0.017301656305789948, 0.0, 0.018255991861224174, 0.008434824645519257, 0.0, 0.023572620004415512, 0.009840126149356365, 0.0083318455144763, 0.010316566564142704, 0.0, 0.008623341098427773, 0.03969650715589523, 0.003701076377183199, 0.0016846992075443268, 0.041352175176143646, 0.0018441769061610103, 0.004817770328372717, 0.04519886523485184, 0.03985346853733063, 0.0, 0.00041035134927369654, 0.06709415465593338, 0.00275072967633605, 0.0005936268134973943, 0.003323623677715659, 0.011695430614054203, 0.02498503029346466, 0.00538589945062995, 0.0, 0.023368557915091515, 0.06071435287594795, 0.026314683258533478, 0.05368850752711296, 0.0, 0.1817903369665146, 0.054052308201789856, 0.0, 0.0, 0.0945565477013588, 0.0, 0.02924363873898983, 0.0002295015729032457, 0.0, 0.0, 0.016074426472187042, 0.019965143874287605, 0.1272212415933609, 0.0, 0.06356889754533768, 0.0, 0.0, 0.039852164685726166, 0.033585477620363235, 0.06138651445508003, 0.0, 0.0012379188556224108, 0.01522617507725954, 0.08307594060897827, 0.009608902037143707, 0.0, 0.0003487261710688472, 0.0, 0.0356065034866333, 0.04222891107201576, 0.006279941648244858, 0.03319162502884865, 0.0, 0.015121367760002613, 0.0, 0.016907736659049988, 0.007156837731599808, 0.10388533771038055, 0.0, 0.036691807210445404, 0.005799953360110521, 0.0051210736855864525, 0.0, 0.009192954748868942, 0.12839041650295258, 0.0, 0.0, 0.007548024412244558, 0.010766187682747841, 3.647982566690189e-06, 0.12704873085021973, 0.053822826594114304, 0.0, 0.06879542768001556, 0.026522701606154442, 0.12692515552043915, 0.019270841032266617, 0.03512577340006828, 0.004837072920054197, 0.013374402187764645, 0.0002769224811345339, 1.2980280189367477e-05, 0.011765771545469761, 0.0, 0.0, 0.07904692739248276, 0.03850327059626579, 0.0, 0.0012836437672376633, 0.0025250816252082586, 0.0222341138869524, 0.0026179528795182705, 0.05512303113937378, 0.0, 0.006741106044501066, 0.0, 0.04436932131648064, 0.10728015750646591, 0.02694275602698326, 0.09218436479568481, 0.0, 0.060714200139045715, 0.12751834094524384, 0.0, 0.04505804181098938, 0.006877160165458918, 0.12487412244081497, 0.0060316650196909904, 0.010297741740942001, 0.0244603231549263, 0.0, 0.005079285707324743, 0.0020245700143277645, 0.02717754989862442, 0.02988407388329506, 0.09088834375143051, 0.038782764226198196, 0.07890607416629791, 0.0608643963932991, 0.0, 0.0721927136182785, 0.0, 0.03465821221470833, 0.03499838337302208, 0.011433521285653114, 0.024416107684373856, 0.06120824068784714, 0.023123513907194138, 0.06519627571105957, 0.035809360444545746, 0.0, 0.0033043597359210253, 0.04897197335958481, 0.07171902060508728, 0.09295244514942169, 0.04084090143442154, 0.023901954293251038, 0.0016764859901741147, 0.09027985483407974, 0.00903597753494978, 0.0, 0.010222666896879673, 0.022061370313167572, 0.0, 0.0, 0.0, 0.01693926937878132, 0.00042985175969079137, 0.025929640978574753, 0.0, 0.002257348969578743, 0.0, 0.03894472122192383, 0.0831218734383583, 0.11981301009654999, 0.0, 0.0, 0.0, 0.10149340331554413, 0.13589270412921906, 0.0, 0.0, 0.0077330986969172955, 0.016666611656546593, 0.0035657607950270176, 0.0, 0.04250870272517204, 0.005849023349583149, 0.10478487610816956, 0.051298294216394424, 0.1246822252869606, 0.0, 0.0, 0.09911829233169556, 0.002431694185361266, 0.0, 3.4573029552120715e-05, 0.046579401940107346, 0.00012978179438505322, 0.043227504938840866, 0.0, 0.010901134461164474, 0.0009060868178494275, 0.01964089646935463, 0.0013656220398843288, 0.009439255110919476, 0.02415061555802822, 0.05976911634206772, 0.021769210696220398, 0.033451467752456665, 0.03675859048962593, 0.004108250606805086, 0.0, 0.0015133374836295843, 0.050437234342098236, 0.015423580072820187, 0.006487049162387848, 0.024957796558737755, 0.0021870224736630917, 0.0, 0.030507225543260574, 0.15093034505844116, 0.0, 0.10218586027622223, 0.035693779587745667, 0.07623881101608276, 0.005182419903576374, 0.07556135207414627, 0.10591752827167511, 0.0, 0.0, 0.0, 0.04897366836667061, 0.01309389527887106, 0.02231336385011673, 0.019917942583560944, 0.16496069729328156, 0.0, 0.05269347131252289, 0.036822862923145294, 9.477896674070507e-05, 0.000987207400612533, 0.0, 0.0062337168492376804, 0.0, 0.0436604805290699, 0.0, 0.02014559879899025, 0.0, 0.013696682639420033, 0.04383938014507294, 0.0086892768740654, 0.05029283091425896, 0.0, 0.0, 0.0077035133726894855, 0.0, 0.04815084859728813, 0.035891637206077576, 0.0, 0.0047150724567472935, 0.02399173192679882, 0.0, 0.0, 0.07801982760429382, 0.0, 0.01065265666693449, 0.0016483984654769301, 0.0020610911305993795, 0.0, 0.0, 0.0027441245038062334, 0.0, 0.07679536193609238, 0.013065783306956291, 0.031285740435123444, 0.008976947516202927, 0.0, 0.054433468729257584, 0.0208676028996706, 0.0, 0.03489236906170845, 0.0, 0.0018107093637809157, 0.005041793920099735, 0.006357213947921991, 0.08086556941270828, 0.004762999247759581, 0.01244857907295227, 0.09436879307031631, 0.010097571648657322, 0.01940232515335083, 0.06009369343519211, 0.0, 0.008238156326115131, 0.0, 0.0, 0.0889384001493454, 0.004645252134650946, 0.0017851117299869657, 0.0, 0.016418976709246635, 0.016117021441459656, 0.0, 0.06268134713172913, 0.0, 0.005481563974171877, 0.0, 0.0, 0.08232559263706207, 0.006691697984933853, 0.08691953867673874, 0.005691339261829853, 2.490416818545782e-06, 0.010392853990197182, 0.0009077033610083163, 0.024013638496398926, 0.12237251549959183, 0.0001260458375327289, 0.07530970126390457, 0.02483021654188633, 0.01649392955005169, 0.0015618405304849148, 0.0, 0.004697645548731089, 0.0006318659288808703, 0.060426242649555206, 0.0, 0.04932703822851181, 0.02970324456691742, 0.0, 0.0, 0.1383500099182129, 0.06639878451824188, 0.0, 0.033564284443855286, 0.009360366500914097, 0.0001678856642683968, 0.005852448754012585, 0.01989474892616272, 0.0, 0.0, 0.013576958328485489, 0.0, 0.08361978083848953, 0.13958726823329926, 0.11152965575456619, 0.032044414430856705, 0.0012754767667502165, 0.012948214076459408, 0.050112102180719376, 0.00019935036834795028, 0.0011573039228096604, 0.02003515511751175, 0.0008309902041219175, 0.0, 0.0, 0.035427194088697433, 0.00013723452866543084, 0.03856987506151199, 0.0, 0.0, 0.015999682247638702, 0.007474880199879408, 0.07550404965877533, 0.009588073007762432, 0.05355680733919144, 0.038309089839458466, 0.0005381004884839058, 0.0438547283411026, 0.0012425718596205115, 0.0003945390635635704, 0.0, 0.09493079781532288, 0.06915537267923355, 0.025021780282258987, 0.0, 0.0030812511686235666, 0.003102890681475401, 0.0, 0.00537538155913353, 0.0, 0.04901902377605438, 0.017224790528416634, 0.025600919499993324, 0.0, 0.0, 0.12375874817371368, 0.043821245431900024, 0.0, 0.0, 0.0, 0.0, 0.007595944218337536, 0.0, 0.0, 0.18770140409469604, 0.04594063758850098, 0.026703491806983948, 0.039259716868400574, 0.0725615918636322, 0.0, 0.007320860866457224, 0.0002928482717834413, 0.04160194844007492, 0.019684500992298126], [0.038277000188827515, 0.07511908560991287, 0.0, 0.0, 0.10244771093130112, 0.0708463042974472, 0.004368195775896311, 0.09819576889276505, 0.03998960182070732, 0.0008290966507047415, 0.0, 0.041159819811582565, 0.013917924836277962, 0.0, 0.0, 0.00035837682662531734, 0.03492850065231323, 0.02305717207491398, 0.04762815311551094, 0.002005749847739935, 0.07673881947994232, 0.014275696128606796, 0.008742389269173145, 0.0, 0.0, 0.0828327164053917, 0.07759462296962738, 0.06119837611913681, 0.008520325645804405, 0.1101134717464447, 0.023455910384655, 0.05116817355155945, 0.0, 0.027629893273115158, 0.014825793914496899, 0.00021281727822497487, 0.05293149873614311, 0.0561380535364151, 0.08570902794599533, 0.07926364988088608, 0.03502173349261284, 0.02522115409374237, 0.10875818133354187, 0.0, 0.0, 0.0, 0.0318421870470047, 0.1475164145231247, 0.00029386236565187573, 0.0, 0.0, 0.036192212253808975, 0.019246984273195267, 0.009989307262003422, 0.026966463774442673, 0.11691378802061081, 0.0, 0.1135963574051857, 0.0016174042830243707, 0.024796733632683754, 0.001628325553610921, 2.8330636268947273e-05, 0.026800720021128654, 0.0, 0.006036538630723953, 0.0002672542177606374, 0.09480970352888107, 0.0, 0.0, 0.007213766220957041, 0.06861738860607147, 0.04997560381889343, 0.0, 0.01262398436665535, 0.0, 0.0008493701461702585, 0.0, 0.0, 0.0, 0.0, 0.04921518266201019, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09512346982955933, 0.0, 0.02975187823176384, 0.0, 0.014505133032798767, 0.013273661956191063, 0.09959366172552109, 0.0, 0.032930053770542145, 0.0, 0.0, 0.0, 0.0, 0.0004196784575469792, 0.060026802122592926, 0.036598194390535355, 0.013312453404068947, 0.040057793259620667, 0.000547547300811857, 0.0, 0.00838405266404152, 0.029791390523314476, 0.019470887258648872, 0.05197775363922119, 0.014910664409399033, 0.0026302451733499765, 0.003408391959965229, 0.05802195891737938, 0.0853428915143013, 0.0, 0.09452318400144577, 0.09232964366674423, 0.002419533906504512, 0.007693548686802387, 0.0007925002137199044, 0.0, 0.05685828998684883, 0.0, 0.0, 0.026838375255465508, 0.0658978819847107, 0.0, 0.002560417866334319, 0.09540434926748276, 0.0, 0.031902264803647995, 0.0, 0.0, 0.0, 0.057224348187446594, 0.026607921347022057, 0.047940514981746674, 0.0, 0.0, 0.0037222974933683872, 0.050187867134809494, 0.0018680167850106955, 0.020967399701476097, 0.05061054974794388, 0.05507611855864525, 0.03470948711037636, 0.0, 0.0, 0.022332575172185898, 0.0, 0.0, 0.0015118122100830078, 0.00095785764278844, 0.030240975320339203, 0.05440991371870041, 0.0009229924180544913, 0.04451034218072891, 0.06092967092990875, 0.07424210757017136, 0.0, 0.0, 0.011227447539567947, 0.008608663454651833, 0.06549479812383652, 0.0, 0.08515896648168564, 0.021508842706680298, 0.03847804665565491, 0.0, 0.0, 0.014916195534169674, 0.020087797194719315, 0.05679470673203468, 0.0, 0.003828709712252021, 0.006989211309701204, 0.06171084940433502, 0.0008125347085297108, 0.0011775235179811716, 0.0016798988217487931, 0.004760005045682192, 0.0050206962041556835, 0.0, 0.0647101029753685, 0.026568561792373657, 0.004178434144705534, 0.045197222381830215, 0.0011663328623399138, 0.10765835642814636, 0.0, 0.0, 0.0, 0.0, 0.0, 0.10514626652002335, 0.1397029608488083, 0.00043757687672041357, 0.07574427127838135, 0.053509604185819626, 0.0690382719039917, 0.002334417775273323, 0.1300399750471115, 0.07711007446050644, 0.08260361105203629, 0.00025726458989083767, 0.0, 0.0164357740432024, 0.0, 0.0, 0.06642376631498337, 0.0644989162683487, 0.0, 0.057952482253313065, 0.02491176128387451, 0.06984996050596237, 0.0518902912735939, 0.1086014062166214, 0.0, 0.04985350742936134, 0.0, 0.007209024857729673, 0.034966688603162766, 0.0, 0.04049668088555336, 0.0197143517434597, 0.072602778673172, 0.10863924771547318, 0.0, 0.05260597541928291, 0.02779676951467991, 0.029237115755677223, 0.0, 0.02253739908337593, 0.0, 0.04435071721673012, 0.0, 0.034917112439870834, 0.0011462111724540591, 0.006835233885794878, 0.10846205800771713, 0.10623764246702194, 0.04286682605743408, 0.0018357208464294672, 0.0, 0.09661223739385605, 0.027619419619441032, 0.03523208200931549, 0.010064486414194107, 0.0, 0.15856985747814178, 0.0, 0.0040131681598722935, 0.05116874352097511, 0.04753110557794571, 0.006049516145139933, 0.0, 0.04201481118798256, 0.048456933349370956, 0.0887010470032692, 0.0, 0.07635800540447235, 0.0007674592197872698, 0.05795363709330559, 0.027600744739174843, 0.0, 0.0286953654140234, 0.03940729424357414, 0.010810256004333496, 0.0, 0.0, 0.06554364413022995, 0.0, 0.0023055998608469963, 0.0, 0.01580154523253441, 0.0, 0.06804051995277405, 0.0, 0.11062664538621902, 0.004994380287826061, 0.05542122945189476, 0.0, 0.024800755083560944, 0.05359075590968132, 0.0, 0.05331893265247345, 0.0, 0.014766053296625614, 0.018119866028428078, 0.0, 0.033309247344732285, 0.06590617448091507, 0.04539242014288902, 0.0, 0.1741570234298706, 0.0, 0.0, 0.10486308485269547, 0.0, 0.06290885806083679, 0.01870639994740486, 0.03107806295156479, 0.04690725356340408, 0.09223122149705887, 0.0007862791535444558, 0.00029198205447755754, 0.0, 0.06251699477434158, 0.04583277553319931, 0.020563706755638123, 0.11089527606964111, 0.052616462111473083, 0.005264782812446356, 0.06359732896089554, 0.07069490104913712, 0.012766488827764988, 0.040580980479717255, 0.023158181458711624, 0.05762961134314537, 0.0026634426321834326, 0.0, 0.000885473215021193, 0.004099220968782902, 0.0009176181629300117, 0.0654546469449997, 0.13854573667049408, 0.0, 0.1255435049533844, 0.09978139400482178, 0.030759084969758987, 0.0, 0.08300407230854034, 0.03196709603071213, 0.021200593560934067, 1.1365489626768976e-05, 0.0, 0.001725812442600727, 0.10591872036457062, 0.013256208971142769, 0.0, 0.10765697062015533, 0.0, 0.054403115063905716, 0.09786894172430038, 0.00015376733790617436, 0.01644594594836235, 0.0, 0.0, 0.027682533487677574, 0.0, 0.0, 0.0, 0.0, 0.00653033796697855, 0.05505022779107094, 0.0, 0.14707991480827332, 0.0, 0.0, 0.0, 0.0003189661365468055, 0.04682626202702522, 0.0017616081750020385, 0.047871701419353485, 0.04727698117494583, 0.0, 0.0, 0.0, 0.02593984641134739, 0.0020617975387722254, 0.09477540850639343, 0.016758138313889503, 0.0, 0.0, 0.0, 0.011991270817816257, 0.0, 0.001618199166841805, 0.004590923897922039, 0.08640295267105103, 0.004693334922194481, 0.028280118480324745, 0.029816551133990288, 0.0023556046653538942, 0.0, 0.050882451236248016, 0.001858129515312612, 0.0, 0.026906879618763924, 0.0, 0.024275343865156174, 0.0, 0.005057081114500761, 0.0, 0.0017671805107966065, 0.041705477982759476, 0.003351434599608183, 0.003776905592530966, 0.005592073779553175, 0.0, 0.07986294478178024, 0.08705680072307587, 0.003908869810402393, 0.0, 0.0, 0.0, 0.000783279596362263, 0.0, 0.061273735016584396, 0.0, 0.003016785718500614, 0.0, 0.0, 0.07844416052103043, 0.0, 0.06469586491584778, 0.033898770809173584, 0.0, 0.024184564128518105, 0.0, 0.08003224432468414, 0.10605242848396301, 0.0, 0.08298053592443466, 0.015612552873790264, 0.02103460021317005, 0.019940286874771118, 0.0, 0.010980330407619476, 0.0, 0.0007881848723627627, 0.0, 0.049765560775995255, 0.0, 0.0, 0.0, 0.07262017577886581, 0.04536927118897438, 0.0, 0.05314166098833084, 0.0, 0.05026179924607277, 0.06148291379213333, 0.03531477600336075, 0.0, 0.0, 0.08165489137172699, 0.017235523089766502, 0.0, 6.222905358299613e-05, 0.06896507740020752, 0.0, 0.03606493026018143, 0.03224235400557518, 0.006689764559268951, 0.006666827946901321, 0.01756225898861885, 0.010640578344464302, 0.008199305273592472, 0.09397415071725845, 0.006599176675081253, 0.005203017964959145, 0.0014776230091229081, 0.08991487324237823, 0.0, 0.0, 0.0, 0.00019108520064037293, 0.09524884819984436, 0.0, 0.08393512666225433, 0.0, 0.0, 0.0, 0.0, 0.0, 0.006977206561714411, 0.04582805559039116, 0.0, 0.04181025177240372, 0.0009024810278788209, 0.08232796937227249, 0.00796498078852892, 0.005949033424258232, 0.0, 0.028066426515579224, 0.04002613201737404, 0.01044737920165062, 0.004159548319876194, 0.038082484155893326, 0.014074047096073627, 0.06458137929439545, 0.021113740280270576, 0.027979405596852303, 0.0, 0.002057125326246023, 0.010829600505530834, 0.0063916523940861225, 0.0020482356194406748, 0.0, 0.05733681097626686, 0.003032253822311759, 0.002843803958967328, 0.008132586255669594, 0.04838063567876816, 0.0009546587825752795, 0.002547877375036478, 0.0, 0.05208221822977066, 0.03320915251970291], [0.0, 0.12706442177295685, 0.0, 0.0, 0.0617978610098362, 0.08982294052839279, 0.0, 0.06375712156295776, 0.0, 0.0, 0.0, 0.0010066742543131113, 0.02549801580607891, 0.0, 0.0, 0.0, 0.0, 0.05747232586145401, 0.006781035102903843, 0.0, 0.06938865035772324, 0.046503376215696335, 0.01674732007086277, 0.03219857066869736, 0.0, 0.09197497367858887, 0.05787806957960129, 0.11736168712377548, 0.0, 0.10983168333768845, 0.07919714599847794, 0.0, 0.0, 0.018378272652626038, 0.0, 0.0, 0.006549353711307049, 0.04507504031062126, 0.037058476358652115, 0.027835549786686897, 0.013727848418056965, 0.0771283507347107, 0.07786482572555542, 0.013978932984173298, 0.041225865483284, 0.0, 0.0, 0.039888642728328705, 0.025105707347393036, 0.0, 0.0, 0.0825432538986206, 0.07149746268987656, 0.0, 0.1118907481431961, 0.15712784230709076, 0.07498329132795334, 0.06376960128545761, 0.002777879824861884, 0.006465183105319738, 0.0, 0.0, 0.045998625457286835, 0.0, 0.016079647466540337, 0.01174563355743885, 0.0053497059270739555, 0.0, 0.0, 0.05930091068148613, 0.021150855347514153, 0.011121757328510284, 0.0, 0.0, 0.0, 0.06736424565315247, 0.0, 0.0, 0.0, 0.0, 0.03752254694700241, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04740680381655693, 0.0, 0.0, 0.054626140743494034, 0.0016109285643324256, 0.0499187670648098, 0.07013549655675888, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06803091615438461, 0.0886249914765358, 0.008370047435164452, 0.06812837719917297, 0.006162557285279036, 0.027258051559329033, 0.007604341488331556, 0.023358263075351715, 0.0, 0.04310524836182594, 0.0, 0.0685834139585495, 0.0, 0.0034256104845553637, 0.08623292297124863, 0.0, 0.0, 0.06528131663799286, 0.018479540944099426, 0.0, 0.0, 0.0, 0.07593976706266403, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06702486425638199, 0.0, 0.027180619537830353, 0.0, 0.0757201686501503, 0.0, 0.0, 0.030390119180083275, 0.07969740033149719, 0.0, 0.0, 0.04969162866473198, 0.03978859633207321, 0.016642969101667404, 0.0, 0.05289304256439209, 0.10364636778831482, 0.02659887634217739, 0.002985325874760747, 0.0, 0.015889616683125496, 0.012265347875654697, 0.009923316538333893, 0.062327418476343155, 0.0, 0.01694757118821144, 0.09990138560533524, 0.0, 0.06174149364233017, 0.010769135318696499, 0.03331402689218521, 0.0, 0.0, 0.0, 0.0, 0.013220873661339283, 0.0, 0.022967487573623657, 0.05724649503827095, 0.016818422824144363, 0.0, 0.0073243179358541965, 0.0387323759496212, 0.0, 0.0, 0.0, 0.04361826926469803, 0.008176790550351143, 0.03287527710199356, 0.030268419533967972, 0.011197661980986595, 0.013719875365495682, 0.04628840461373329, 0.11877001821994781, 0.006676092278212309, 0.006855408661067486, 0.018448615446686745, 0.030461300164461136, 0.0, 0.010475924238562584, 0.013076662085950375, 0.03568072244524956, 0.015756765380501747, 0.0, 0.0, 0.0, 0.0889098197221756, 0.02012772299349308, 0.0, 0.07237964123487473, 0.02772441878914833, 0.12726585566997528, 0.0, 0.024094849824905396, 0.059007782489061356, 0.046570226550102234, 0.0, 0.0, 0.0, 0.0, 0.005249816924333572, 0.03608665242791176, 0.083749920129776, 0.0, 0.02815372869372368, 0.0, 0.0, 0.05399007350206375, 0.07201454043388367, 0.0, 0.048156045377254486, 0.0, 0.0, 0.10070101916790009, 0.0, 0.11437010765075684, 0.0, 0.09155678749084473, 0.05005383864045143, 0.0, 0.12438458204269409, 0.0, 0.060569457709789276, 0.0, 0.092941053211689, 0.0, 0.05057093873620033, 0.0, 0.0, 0.04313907027244568, 0.06069880351424217, 0.04441182687878609, 0.027279475703835487, 0.054095301777124405, 0.03800927847623825, 0.03521990403532982, 0.06243739277124405, 0.0, 0.02959703467786312, 0.0, 0.03727645054459572, 0.04952580854296684, 0.008555205538868904, 0.0, 0.04452388733625412, 0.08071929216384888, 0.06274095922708511, 0.0, 0.030292287468910217, 0.11367630213499069, 0.11912692338228226, 0.009572434239089489, 0.06559039652347565, 0.03299733251333237, 0.057392723858356476, 0.0, 0.0, 0.026839587837457657, 0.047365330159664154, 0.05924321711063385, 0.010713363066315651, 0.0, 0.044305220246315, 0.0, 0.026559215039014816, 0.0, 0.0744042843580246, 0.0, 0.03378073871135712, 0.0, 0.033992648124694824, 0.03152303025126457, 0.0, 0.0, 0.07024101912975311, 0.0, 0.0, 0.0, 0.037622708827257156, 0.0, 0.01784011721611023, 0.0, 0.04818618297576904, 0.0, 0.09145329892635345, 0.10048288851976395, 0.04247622936964035, 0.0, 0.0, 0.08544918149709702, 0.0, 0.0, 0.0, 0.07455632835626602, 0.0, 0.00994107872247696, 0.0, 0.05218471214175224, 0.0, 0.0019345091423019767, 0.05380168929696083, 0.0, 0.0, 0.13668955862522125, 0.04924769699573517, 0.0, 0.021245202049613, 0.0, 0.0, 0.05630602687597275, 0.07480110973119736, 0.0328199528157711, 0.0672653466463089, 0.03838269039988518, 0.033879801630973816, 0.0, 0.0, 0.04054815322160721, 0.0, 0.06544040888547897, 0.05615415796637535, 0.048112399876117706, 0.0, 0.056351739913225174, 0.03999374806880951, 0.024415230378508568, 0.0017712407279759645, 0.0, 0.044400736689567566, 0.04733794182538986, 0.0785994604229927, 0.10096468031406403, 0.13314808905124664, 0.0, 0.04086991772055626, 0.03572415933012962, 0.0, 0.01049860380589962, 0.0, 0.00034703471465036273, 0.0, 0.0, 0.01673789508640766, 0.0, 0.0, 0.0, 0.018347835168242455, 0.0, 0.07329846918582916, 0.0, 0.0, 0.03337222710251808, 0.0, 0.13681979477405548, 0.0, 0.0, 0.028105271980166435, 0.03933370113372803, 0.0, 0.0, 0.035451240837574005, 0.0, 0.022708283737301826, 0.041216205805540085, 0.0, 0.0, 0.03721163794398308, 0.017808562144637108, 0.05265277624130249, 0.13494709134101868, 0.0, 0.11362170428037643, 0.004714918322861195, 0.08123084902763367, 0.0019162270473316312, 0.012465830892324448, 0.0, 0.030126439407467842, 0.0, 0.0, 0.012267716228961945, 0.0, 0.023639900609850883, 0.0, 0.05841570720076561, 0.0006933341501280665, 0.02091485634446144, 0.022066565230488777, 0.09993155300617218, 0.0, 0.0, 0.0, 0.053533196449279785, 0.11285264790058136, 0.034360721707344055, 0.0, 0.0, 0.05215580016374588, 0.0, 0.008157026022672653, 0.10146156698465347, 0.0, 0.006020206492394209, 0.0, 0.0, 0.07578644901514053, 0.04826609417796135, 0.10105247050523758, 0.05049704760313034, 0.0, 0.07745730131864548, 0.0, 0.05565506964921951, 0.10622499138116837, 0.0, 0.048865124583244324, 0.045797575265169144, 0.025380892679095268, 0.0012576183071359992, 0.0, 0.0004131115274503827, 0.0, 0.0598464161157608, 0.0, 0.06916404515504837, 0.0, 0.0, 0.0, 0.07151130586862564, 0.04227958619594574, 0.0, 0.10035272687673569, 0.0, 0.0, 0.010949426330626011, 0.03812709078192711, 0.0, 0.0, 0.029637359082698822, 0.00509963184595108, 0.07364810258150101, 0.04346117749810219, 0.09621229767799377, 0.0, 0.0, 0.08711151033639908, 0.012971431948244572, 0.0, 0.02151731587946415, 0.02068432606756687, 0.0, 0.01575508899986744, 0.0, 0.0, 0.0, 0.09821707010269165, 0.0, 0.0, 0.0, 0.018922515213489532, 0.08149182051420212, 0.0, 0.04335851967334747, 0.0, 0.0, 0.0, 0.0, 0.037420324981212616, 0.08812244981527328, 0.09543497860431671, 0.03134153038263321, 0.019697893410921097, 0.0, 0.0, 0.11047311127185822, 0.07468008249998093, 0.0, 0.03477568179368973, 0.06684442609548569, 0.0, 0.05879810452461243, 0.042789239436388016, 0.02527700364589691, 0.06801774352788925, 0.05156499147415161, 0.0, 0.0, 0.0, 0.0, 0.09004642069339752, 0.0, 0.000521330104675144, 0.09398793429136276, 0.014966586604714394, 0.031070692464709282, 0.023660462349653244, 0.07007139176130295, 0.0, 0.0, 0.0, 0.06829819083213806, 0.0], [0.09624335169792175, 0.07752721756696701, 0.0, 0.0, 0.035214848816394806, 0.044096484780311584, 0.0, 0.09057245403528214, 0.10810151696205139, 0.0, 0.0, 0.046638160943984985, 0.0, 0.012674330733716488, 0.0, 0.012669921852648258, 0.01976463384926319, 0.0, 0.05899769812822342, 0.0, 0.13344405591487885, 0.11377677321434021, 0.01828574202954769, 0.0, 0.057915788143873215, 0.0646957978606224, 0.03659713640809059, 0.11116261780261993, 0.0, 0.0, 0.0753936618566513, 0.0, 0.0, 0.019339775666594505, 0.0008688414236530662, 0.0, 0.016929402947425842, 0.06832345575094223, 0.04209471866488457, 0.030069122090935707, 0.0, 0.05951334536075592, 0.1034277155995369, 0.0, 0.03989734500646591, 0.0, 0.021451102569699287, 0.12372945249080658, 0.006901431363075972, 0.0019657521042972803, 0.0, 0.04786674305796623, 0.04529386758804321, 0.019722145050764084, 0.137382373213768, 0.11203469336032867, 0.032551418989896774, 0.058746326714754105, 0.05897234380245209, 0.0, 0.0, 0.0, 0.0423472560942173, 0.0, 0.0, 0.0, 0.0, 0.0, 0.03631681948900223, 0.11449339240789413, 0.05193508788943291, 0.0, 0.04279319569468498, 0.0721987783908844, 0.05519154667854309, 0.0, 0.0, 0.0, 0.011420829221606255, 0.0, 0.0, 0.04919896274805069, 0.0, 0.018259543925523758, 0.0, 0.0, 0.0, 0.09242633730173111, 0.04354950785636902, 0.0, 0.0510205402970314, 0.0, 0.007978744804859161, 0.02794218249619007, 0.0, 0.04953666776418686, 0.0, 0.0, 0.0, 0.050288889557123184, 0.0, 0.0, 0.026628652587532997, 0.0, 0.009556098841130733, 0.03339400514960289, 0.0, 0.06293398886919022, 0.008860652334988117, 0.018971571698784828, 0.08952675759792328, 0.0, 0.05618039146065712, 0.0, 0.031070474535226822, 0.07058808207511902, 0.0071168201975524426, 0.0, 0.03248728811740875, 0.03472699597477913, 0.019829656928777695, 0.0, 0.018287047743797302, 0.03597049042582512, 0.0, 0.0, 0.0, 0.058046579360961914, 0.03763121739029884, 0.08871375769376755, 0.09098221361637115, 0.0, 0.0, 0.017421016469597816, 0.0, 0.0, 0.03947598114609718, 0.0561000257730484, 0.05291222780942917, 0.0036968281492590904, 0.0, 0.0054831006564199924, 0.05156334117054939, 0.007968704216182232, 0.0, 0.002476033288985491, 0.11482564359903336, 0.0, 0.05502888187766075, 0.015448045916855335, 0.0063612200319767, 0.0, 0.0, 0.0, 0.14518751204013824, 0.0010223155841231346, 0.017916835844516754, 0.0, 0.0654388815164566, 0.0023499540984630585, 0.046164147555828094, 0.0, 0.0, 0.02206435054540634, 0.0, 0.1337626874446869, 0.0, 0.06910862773656845, 0.0, 0.10861428081989288, 0.03995981067419052, 0.0, 0.0, 0.05532491207122803, 0.05653597414493561, 0.0, 0.03551480174064636, 0.04111021012067795, 0.0, 0.1018960252404213, 0.07069353759288788, 0.05062974989414215, 0.08164963871240616, 0.09977990388870239, 0.018083175644278526, 0.03779464215040207, 0.0939909964799881, 0.03214322775602341, 0.0, 0.0014320886693894863, 0.07950647920370102, 0.0, 0.005207928828895092, 0.0, 0.0, 0.018587280064821243, 0.07634686678647995, 0.0, 0.037233807146549225, 0.01964448392391205, 0.04220058023929596, 0.058265045285224915, 0.025011518970131874, 0.04915453866124153, 0.0, 0.043102335184812546, 0.0808536633849144, 0.0, 0.03916074335575104, 0.0, 0.0, 0.05952556058764458, 0.0722699984908104, 0.0, 0.05891319364309311, 0.018868573009967804, 0.0019325428875163198, 0.0, 0.07667523622512817, 0.0, 0.0, 0.0, 0.0, 0.036728985607624054, 0.0, 0.0518658347427845, 0.0, 0.03390361741185188, 0.016085652634501457, 0.0, 0.07212691009044647, 0.03178784251213074, 0.052622076123952866, 0.0, 0.06844914704561234, 0.04869980365037918, 0.0, 0.0, 0.0, 0.0, 0.003131774952635169, 0.07368964701890945, 0.01678270287811756, 0.08044349402189255, 0.03948609158396721, 9.343265992356464e-05, 0.05920938029885292, 0.006536537781357765, 0.01867850124835968, 0.0022852537222206593, 0.035228583961725235, 0.013926932588219643, 0.05002288892865181, 0.0, 0.10449428111314774, 0.0625523254275322, 0.0, 0.014565370976924896, 0.026854200288653374, 0.027597924694418907, 0.08701537549495697, 0.0, 0.021162021905183792, 0.006888329982757568, 0.007514254190027714, 0.07727989554405212, 0.0, 0.057641468942165375, 0.02869355119764805, 0.0, 0.0, 0.0, 0.04746021330356598, 0.0, 0.08983460813760757, 0.017983518540859222, 0.04932957887649536, 0.0, 0.01100978534668684, 0.0, 0.11433852463960648, 0.06549558788537979, 0.0012831258354708552, 0.0, 0.09403978288173676, 0.0, 0.0014625659678131342, 0.03719783574342728, 0.005976118613034487, 0.0, 0.014238324016332626, 0.02762761153280735, 0.04375198483467102, 0.04466153681278229, 0.08421042561531067, 0.0, 0.12236222624778748, 0.0, 0.0, 0.0, 0.02109551802277565, 0.09022770822048187, 0.0, 0.0, 0.015515713021159172, 0.045682959258556366, 0.0, 0.0, 0.0, 0.00905993115156889, 0.06250561773777008, 0.014072242192924023, 0.10661471635103226, 0.054471228271722794, 0.01758653111755848, 0.09653563797473907, 0.017693372443318367, 0.0, 0.0, 0.0, 0.016959816217422485, 0.03604342043399811, 0.0, 0.0, 0.0, 0.034792009741067886, 0.0, 0.04455088824033737, 0.0, 0.044874660670757294, 0.07124429941177368, 0.0, 0.0, 0.05203060433268547, 0.10818159580230713, 0.015539330430328846, 0.0, 0.03028270974755287, 0.0, 0.06858322769403458, 0.08215753734111786, 0.0, 0.026961976662278175, 0.0, 0.061655398458242416, 0.03192935883998871, 0.0, 0.0, 0.0, 0.0, 0.005321887321770191, 0.0, 0.0, 0.0, 0.03339992091059685, 0.0, 0.06015694886445999, 0.08287331461906433, 0.036466941237449646, 0.0, 0.0, 0.0, 0.0, 0.0, 0.057576604187488556, 0.0, 0.07040803134441376, 0.0, 0.0, 0.0, 0.007497869897633791, 0.003536554519087076, 0.05612162500619888, 0.1512339860200882, 0.0, 0.0, 0.0, 0.023637427017092705, 0.0, 0.08459550142288208, 0.0, 0.011106621474027634, 0.05142606422305107, 0.0, 0.053689174354076385, 0.0, 0.0, 0.08751492947340012, 0.043482497334480286, 0.0, 0.08504344522953033, 0.042931947857141495, 0.08768638223409653, 0.0, 0.0, 0.06041562557220459, 0.0, 0.0, 0.0012281539384275675, 0.0, 0.0, 0.024684017524123192, 0.0, 0.03648944944143295, 0.014607995748519897, 0.0, 0.0, 0.04686540737748146, 0.0, 0.0, 0.0676095187664032, 0.0, 0.051398348063230515, 0.0, 0.0, 0.08590067923069, 0.0, 0.12008824944496155, 0.0762196034193039, 0.0, 0.0, 0.0, 0.05913432314991951, 0.023472124710679054, 0.0, 0.0986558124423027, 0.009508570656180382, 0.034457191824913025, 0.0, 0.0, 0.0, 0.0284971222281456, 0.005201656837016344, 0.013788586482405663, 0.0465986393392086, 0.08928444236516953, 0.0, 0.0, 0.09265829622745514, 0.014587623067200184, 0.0, 0.013587696477770805, 0.0, 0.025322172790765762, 7.493270095437765e-05, 0.0, 0.018534092232584953, 0.0, 0.014302440918982029, 0.0, 0.047487176954746246, 0.04618266969919205, 0.09386277198791504, 0.0383017398416996, 0.0, 0.024678263813257217, 0.055006399750709534, 0.0, 0.05249766260385513, 0.0006251135491766036, 0.0, 0.0, 0.02497950941324234, 0.03850531950592995, 0.043190594762563705, 0.015002214349806309, 0.010849724523723125, 0.0, 0.016812639310956, 0.0, 0.03374066203832626, 0.0, 0.06086857616901398, 0.019976967945694923, 0.0, 0.0725395604968071, 0.04125041514635086, 0.0, 0.07503723353147507, 0.024585630744695663, 0.04065565764904022, 0.08231740444898605, 0.0, 0.011403970420360565, 0.005732182879000902, 0.08642018586397171, 0.0, 0.0, 0.06803757697343826, 0.004760563839226961, 0.034141454845666885, 0.023955009877681732, 0.0, 0.050961852073669434, 0.03808015584945679, 0.0, 0.0, 0.08526477217674255, 0.0, 0.013495302759110928, 0.07322808355093002, 0.0, 0.0872790664434433, 0.05407869443297386, 0.0584028884768486, 0.016591405496001244, 0.09371792525053024, 0.0, 0.07069236785173416, 0.0, 0.09576816111803055, 0.0]], "last_cam": "5", "nombre": "Desconocido", "ts": 1782406698.2781408, "actualizaciones_globales": 18}, "108": {"galeria_deep": [[0.0030240106862038374, 0.07639876753091812, 0.0, 0.0, 0.06978218257427216, 0.0864880383014679, 0.005286186467856169, 0.09987813234329224, 0.008134820498526096, 0.0, 0.0, 0.05774376541376114, 0.0024847767781466246, 0.0, 0.0016639410750940442, 0.0, 0.0004467200196813792, 0.010122141800820827, 0.009503480978310108, 0.0, 0.03733011707663536, 0.08135505020618439, 0.0, 0.0, 0.0029798701871186495, 0.090042844414711, 0.04352589696645737, 0.03235939145088196, 0.0004228981561027467, 0.039788395166397095, 0.006153280846774578, 0.0, 0.0, 0.02675798162817955, 0.0, 0.0, 0.022785963490605354, 0.09141600131988525, 0.07792166620492935, 0.06673594564199448, 0.07347872853279114, 0.0965486541390419, 0.03332224488258362, 0.0, 0.07313989102840424, 0.0028883276972919703, 0.012352787889540195, 0.02684071473777294, 0.01016908697783947, 0.0, 0.0, 0.1533154547214508, 0.09653015434741974, 0.040688998997211456, 0.1445903331041336, 0.12457121163606644, 0.03113829717040062, 0.05004003271460533, 0.07317151129245758, 0.02097085863351822, 0.0, 0.00724249379709363, 0.0, 0.0, 0.010997132398188114, 0.022391662001609802, 0.06821171939373016, 0.01710011251270771, 0.010259389877319336, 0.0013451819540932775, 0.061674147844314575, 0.005395712796598673, 0.020210202783346176, 0.00012080716987838969, 0.0022737805265933275, 0.0009054119582287967, 0.0, 0.0, 0.0, 0.0, 0.0035863060038536787, 0.04475342854857445, 0.0, 0.0, 0.0008735591545701027, 0.0, 0.0, 0.0703325942158699, 0.002577240811660886, 0.0, 0.03219464048743248, 0.012662790715694427, 0.00016322030569426715, 0.012840017676353455, 0.030029810965061188, 0.019304905086755753, 0.0, 0.0, 0.0009209233685396612, 0.015898672863841057, 0.0, 0.04943646490573883, 0.13951632380485535, 0.006281707435846329, 0.04628150910139084, 0.06525952368974686, 0.005268089938908815, 0.039453744888305664, 0.03146172687411308, 0.06189844012260437, 0.1104046031832695, 0.027619244530797005, 0.024756770581007004, 0.0, 0.037873316556215286, 0.022128300741314888, 0.0, 0.012510322965681553, 0.045531339943408966, 0.0006739716627635062, 0.026023292914032936, 0.0, 0.002194480039179325, 0.014140835963189602, 0.0, 0.006301868706941605, 0.008707036264240742, 0.011060851626098156, 3.662484959932044e-05, 0.04623233526945114, 0.020886464044451714, 0.0, 0.011838656850159168, 0.0, 0.0, 0.0, 0.0, 0.06689731031656265, 0.02165539190173149, 0.0, 0.0, 0.0004455037706065923, 0.06497527658939362, 0.0034367418847978115, 0.0011171902297064662, 0.007309674751013517, 0.10882426798343658, 0.046838682144880295, 0.0, 0.0, 0.0009018807904794812, 0.0, 0.028606843203306198, 0.011740489862859249, 0.007205207832157612, 0.0011363090015947819, 0.030424775555729866, 0.0, 0.09194625914096832, 0.01663975790143013, 0.07646864652633667, 0.007076646201312542, 0.002172167180106044, 0.08218660205602646, 0.025174684822559357, 0.03613832965493202, 0.0, 0.0968858078122139, 0.018625156953930855, 0.058418016880750656, 0.004059048369526863, 0.0, 0.0240850318223238, 0.0050851134583354, 0.06734909862279892, 0.004156860057264566, 0.0010455077281221747, 0.05757095664739609, 0.006644655484706163, 0.017327306792140007, 0.0005248249508440495, 0.0, 0.08474897593259811, 0.04123267903923988, 0.0, 0.012654370628297329, 0.04094023257493973, 0.05710141733288765, 0.0, 0.0, 0.11954134702682495, 0.03122645802795887, 0.013127140700817108, 0.0016254234360530972, 0.007414954248815775, 0.01034141518175602, 0.13961534202098846, 0.08754322677850723, 0.0, 0.02138463221490383, 0.08629820495843887, 0.1180143728852272, 0.0034984759986400604, 0.09458515793085098, 0.01478405948728323, 0.06281818449497223, 0.02927008830010891, 0.006864715833216906, 0.0, 0.0, 0.0, 0.07675281167030334, 0.047645870596170425, 0.0, 0.020780708640813828, 0.09785915166139603, 0.0554986298084259, 0.07389504462480545, 0.14027225971221924, 0.002730530919507146, 0.0024839090183377266, 0.004006744362413883, 0.06290076673030853, 0.08372333645820618, 0.008354630321264267, 0.05977103114128113, 0.0, 0.047285135835409164, 0.044512901455163956, 0.0, 0.07668964564800262, 0.07195699214935303, 0.09693446010351181, 0.06304726004600525, 0.04664495587348938, 0.018088750541210175, 0.026147427037358284, 0.012878766283392906, 0.00033376761712133884, 0.0, 0.026538962498307228, 0.009817277081310749, 0.0550234392285347, 0.0018014538800343871, 0.03796423599123955, 0.0, 0.005866319872438908, 0.006245995871722698, 0.01774389110505581, 8.475104550598189e-05, 0.0014584754826501012, 0.06333371251821518, 0.0, 0.029618162661790848, 0.12796549499034882, 0.040627770125865936, 0.06746689230203629, 0.0, 0.0, 0.05545154586434364, 0.08345518261194229, 0.0, 0.01999799720942974, 0.0, 0.007824834436178207, 0.10082478076219559, 0.008847860619425774, 0.0, 0.0, 0.025286037474870682, 0.0, 0.0, 0.054637663066387177, 0.018018079921603203, 0.10189267247915268, 0.06852135062217712, 0.0004758879076689482, 0.0, 0.03142604976892471, 0.005445743910968304, 0.09287575632333755, 0.013392549939453602, 0.014659828506410122, 0.0, 0.08366172760725021, 0.07573090493679047, 0.0, 0.0022315052337944508, 0.04197872430086136, 0.0, 0.003236403688788414, 0.0018836164381355047, 0.044588446617126465, 9.571273403707892e-05, 0.05543829873204231, 0.07991746813058853, 0.16663575172424316, 0.0, 0.0, 0.05740600824356079, 0.008031394332647324, 0.04015958309173584, 0.004385318607091904, 0.02794315665960312, 0.00021293683676049113, 0.0031771394424140453, 0.0, 0.03653616085648537, 0.0, 0.004137475974857807, 0.026858927682042122, 0.0004260922723915428, 0.07430652529001236, 0.12414513528347015, 0.004771411884576082, 0.023120583966374397, 0.0, 0.09771791845560074, 0.017957434058189392, 0.008604376576840878, 0.04852611944079399, 0.0, 0.0007961055962368846, 0.025309665128588676, 0.0013983199605718255, 0.009194358251988888, 0.024955591186881065, 0.1527479737997055, 0.0, 0.05561305582523346, 0.06806929409503937, 0.07560297846794128, 0.0, 0.09162432700395584, 0.07527977973222733, 0.0, 0.0, 0.0, 0.07326110452413559, 0.03765374794602394, 0.027453524991869926, 0.0630793422460556, 0.0696500912308693, 0.0, 0.029310481622815132, 0.018444016575813293, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.025295095518231392, 0.0, 0.011885087937116623, 0.015459155663847923, 0.05193837359547615, 0.06009161099791527, 0.014473640359938145, 0.0, 0.0, 0.0, 0.0, 0.053118329495191574, 0.018291987478733063, 0.020539337769150734, 0.011802693828940392, 0.0, 0.0, 0.0, 0.023399392142891884, 0.01044249814003706, 0.05123070627450943, 0.07319620996713638, 0.036378517746925354, 0.0014555027009919286, 0.0, 0.0, 0.009770805947482586, 0.10068768262863159, 0.0, 0.09813500195741653, 0.028627200052142143, 0.001959207933396101, 0.0, 0.01042263489216566, 0.01141376607120037, 0.10879111289978027, 0.0, 0.0, 0.0754615068435669, 0.0, 0.0194700937718153, 0.0, 0.0, 0.07821482419967651, 0.01411714032292366, 0.020352421328425407, 0.023102644830942154, 0.0, 0.026457855477929115, 0.0, 0.030365606769919395, 0.14040863513946533, 0.037195801734924316, 0.0, 0.0, 0.07552077621221542, 0.0, 0.0, 0.029843930155038834, 0.0, 0.040020689368247986, 0.0, 0.0, 0.07076200842857361, 0.0035699945874512196, 0.16498978435993195, 0.06491552293300629, 0.003187774680554867, 0.04225340113043785, 0.0, 0.0671096220612526, 0.11063376814126968, 0.0, 0.0685613751411438, 0.06489083170890808, 0.01621231436729431, 0.0, 0.014041080139577389, 0.004092903342097998, 0.0, 0.029495850205421448, 0.01379142515361309, 0.005012460518628359, 0.0030510774813592434, 0.0, 0.0, 0.08472806215286255, 0.02988373301923275, 0.0, 0.01962667889893055, 0.023822082206606865, 0.0016699291300028563, 0.0011643778998404741, 0.019595254212617874, 0.0, 0.0, 0.07044754177331924, 0.0, 0.009792599827051163, 0.009308123961091042, 0.05212275683879852, 0.0, 0.0010650387266650796, 0.022874481976032257, 0.05805904418230057, 0.00029370453557930887, 0.03556574508547783, 0.009813024662435055, 0.0, 0.001197619130834937, 0.0, 0.048606086522340775, 0.05888616293668747, 0.0, 0.004839886445552111, 0.0, 6.432610098272562e-05, 0.011270960792899132, 0.05753978341817856, 0.0, 0.05804109573364258, 0.041218411177396774, 0.0, 0.04025537893176079, 0.01005479134619236, 0.0, 0.040342509746551514, 0.020357927307486534, 0.0, 0.046783220022916794, 0.0, 0.0, 0.01216031238436699, 0.009408580139279366, 0.0, 0.004100591875612736, 0.10158541798591614, 0.0004906168906018138, 0.004220179747790098, 0.0, 0.0, 0.03985675051808357, 0.03053123876452446, 0.0017095074290409684, 0.002153751440346241, 0.053648997098207474, 0.005795185454189777, 0.06547991186380386, 0.029575569555163383, 0.0, 0.10210903733968735, 0.028280174359679222, 0.010477229952812195, 0.0, 0.04183029383420944, 0.0, 0.06120220944285393, 0.0, 0.000979395117610693, 0.01095943059772253], [0.020944636315107346, 0.11357184499502182, 0.0013235557125881314, 0.0, 0.07848449796438217, 0.07680811733007431, 0.015034054405987263, 0.05841949209570885, 0.01831723563373089, 0.02035577967762947, 0.01424927357584238, 0.007465200498700142, 0.0030276949983090162, 0.0, 0.03873293846845627, 0.0003701231617014855, 0.07291039079427719, 0.06315720081329346, 0.005048343446105719, 0.0, 0.05048062652349472, 0.05438023433089256, 0.048921044915914536, 0.0009803847642615438, 0.06697481125593185, 0.03411759436130524, 0.07483197003602982, 0.08833488076925278, 0.005473338533192873, 0.0, 0.09370491653680801, 0.05261511355638504, 3.903108154190704e-05, 0.10577613860368729, 0.0, 0.0, 0.0, 0.0014367637922987342, 0.0368708036839962, 0.12093061208724976, 0.04975569248199463, 0.05923685431480408, 0.0008811558946035802, 0.0, 0.09134199470281601, 0.008126424625515938, 0.03717198967933655, 0.06130656972527504, 0.05219303444027901, 0.0, 0.0, 0.12959854304790497, 0.0, 0.0, 0.17413359880447388, 0.15963491797447205, 0.0019106834661215544, 0.08729681372642517, 0.04744061827659607, 0.0, 0.04669106379151344, 0.05781661346554756, 0.0025141166988760233, 0.0, 0.0, 0.03741737827658653, 0.08037173748016357, 0.032380763441324234, 8.695263386471197e-05, 0.033766813576221466, 0.020956600084900856, 0.0, 0.017752476036548615, 0.0, 0.051682621240615845, 0.0006167151732370257, 0.0006455665570683777, 0.0, 0.000441064708866179, 0.0, 0.05207084119319916, 0.002021264284849167, 0.0, 0.0, 0.0, 7.139361696317792e-05, 0.006399164441972971, 0.07855349779129028, 0.08417081832885742, 0.0, 0.020925791934132576, 0.0, 0.02797720581293106, 0.009216636419296265, 0.008961730636656284, 0.031694818288087845, 0.0, 0.0, 0.007321793120354414, 0.0003091442631557584, 0.0006482303724624217, 0.047006282955408096, 0.006641386542469263, 0.07423465698957443, 0.01066002156585455, 0.004305942915380001, 0.0, 0.02114137075841427, 0.012369729578495026, 0.006799294147640467, 0.1382271647453308, 0.002986231818795204, 0.060160014778375626, 0.0, 0.0, 0.005680351052433252, 0.0, 0.005846092477440834, 0.03426257520914078, 0.017945021390914917, 0.0, 0.07742886990308762, 0.0, 0.11415993422269821, 0.0, 0.0037809768691658974, 0.0033561496529728174, 0.0894753634929657, 0.00737850833684206, 0.013645305298268795, 0.01964368298649788, 0.0, 0.044712722301483154, 0.00038802303606644273, 0.0041451044380664825, 0.005134806968271732, 0.027139147743582726, 0.054409921169281006, 0.04220592603087425, 0.0, 0.009888660162687302, 0.011418337933719158, 0.0004471619613468647, 0.056752003729343414, 0.012849010527133942, 0.0, 0.11618013679981232, 0.08577065914869308, 0.0, 0.0, 0.04027815908193588, 0.0, 0.07795171439647675, 0.05346368998289108, 0.029565606266260147, 0.004951433278620243, 0.013725174590945244, 0.04448600485920906, 0.11669737100601196, 0.00026976436492986977, 0.049791119992733, 0.0010317470878362656, 0.0, 0.0563666857779026, 0.02007390931248665, 0.01997235044836998, 0.0, 0.05024811252951622, 0.0, 0.10346858948469162, 0.020222080871462822, 0.0, 0.0033511060755699873, 0.0, 0.0038471140433102846, 0.0, 0.002531351987272501, 0.016731468960642815, 0.0, 0.01861606352031231, 0.0, 0.003338718554005027, 0.026385841891169548, 0.09831978380680084, 0.009836524724960327, 0.0015009439084678888, 0.0460861511528492, 0.042501069605350494, 0.0, 0.014158587902784348, 0.05427834764122963, 0.005891171749681234, 0.0, 0.0, 0.0007805615314282477, 0.0023171729408204556, 0.04010139778256416, 0.013222948648035526, 0.0, 0.09682760387659073, 0.05615057423710823, 0.058936309069395065, 0.0, 0.06868837028741837, 0.0, 0.08781494200229645, 0.0007592143956571817, 0.0, 0.03484819456934929, 0.0, 0.026838768273591995, 0.04203503951430321, 0.02958456054329872, 0.0, 0.052157923579216, 0.09203354269266129, 0.020377613604068756, 0.07339038699865341, 0.1522970050573349, 0.010221904143691063, 0.008167862892150879, 0.0751122385263443, 0.10724557191133499, 0.1162702739238739, 0.016475172713398933, 0.007691230624914169, 0.0032486647833138704, 0.06566998362541199, 0.08880897611379623, 0.008584531024098396, 0.0916508361697197, 0.031100837513804436, 0.10558607429265976, 0.0, 0.0369773730635643, 0.0, 0.042246874421834946, 0.08056977391242981, 0.0, 0.02287825383245945, 0.026874007657170296, 0.002214265987277031, 0.04409484192728996, 0.10184573382139206, 0.025556528940796852, 0.0, 0.044777851551771164, 0.004986651241779327, 0.08788689225912094, 0.0, 0.0, 0.01020798273384571, 0.0011828816495835781, 0.008335084654390812, 0.0070802136324346066, 0.03253048658370972, 0.01050837617367506, 0.004517445806413889, 0.03097696229815483, 0.015231580473482609, 0.010677700862288475, 0.0004872731224168092, 0.055048275738954544, 0.0, 0.06515772640705109, 0.015064854174852371, 0.0015744344564154744, 0.03319753706455231, 0.05417098477482796, 0.049680083990097046, 0.002310874406248331, 0.0, 0.08684360980987549, 0.0, 0.033823996782302856, 0.047382619231939316, 0.0, 0.0, 0.007494816090911627, 0.024766145274043083, 0.016649628058075905, 0.02476068213582039, 0.004567458759993315, 4.2891133489320055e-05, 0.016659816727042198, 0.019819557666778564, 0.0, 0.00790977943688631, 0.0, 0.011168933473527431, 0.003948640078306198, 0.0005797865451313555, 0.011380992829799652, 0.003186768852174282, 0.12993043661117554, 0.01131818164139986, 0.16292332112789154, 0.005113876424729824, 0.028101924806833267, 0.04267437756061554, 0.040570151060819626, 0.00297472788952291, 0.006478027440607548, 0.04015330970287323, 0.0, 0.008361018262803555, 0.0, 0.007363799959421158, 0.00019613078620750457, 0.019056955352425575, 0.05016285926103592, 0.0016043198993429542, 0.06269830465316772, 0.06673767417669296, 0.0, 0.0, 0.022522039711475372, 0.019730892032384872, 0.010866465978324413, 0.0004760963493026793, 0.0, 0.00049289979506284, 0.0, 8.08148761279881e-05, 0.00010826602374436334, 0.014365865848958492, 0.03206856548786163, 0.122576043009758, 0.005092050414532423, 0.12184074521064758, 0.04053301736712456, 0.053321968764066696, 0.0, 0.12046726793050766, 0.09157082438468933, 0.00013694912195205688, 0.023008577525615692, 0.010675660334527493, 0.021765321493148804, 0.025931894779205322, 0.0020336073357611895, 0.03465845063328743, 0.07038669288158417, 0.013005184009671211, 0.006305390503257513, 0.0021494030952453613, 0.01342097856104374, 0.007665668148547411, 0.0, 0.01883506402373314, 0.0, 0.0028179024811834097, 0.0016603389522060752, 0.003228377318009734, 0.0, 0.0009741918765939772, 0.09582571685314178, 0.03253750130534172, 0.07975111156702042, 0.0, 0.0, 0.0, 0.027250142768025398, 0.10221301019191742, 0.025107797235250473, 0.04517865553498268, 0.005048285238444805, 0.002196230925619602, 0.0, 0.0131688816472888, 0.0437946617603302, 0.001521313562989235, 0.03894424811005592, 0.0713638961315155, 0.0, 0.00341643369756639, 0.0, 0.0047454871237277985, 0.0, 0.059861328452825546, 0.0233613234013319, 0.0902744010090828, 0.0008087530150078237, 0.05077872425317764, 0.0312904492020607, 0.0, 0.037153325974941254, 0.06090177968144417, 0.00888803880661726, 0.0, 0.07981056720018387, 0.0011029787128791213, 0.03978876397013664, 0.0, 0.0, 0.019958607852458954, 0.0, 0.0, 0.015856774523854256, 0.0, 0.017191996797919273, 0.030419452115893364, 0.001203496241942048, 0.08635339140892029, 0.0, 0.03336702659726143, 0.0003263175021857023, 0.05678582936525345, 0.0, 0.0009615116287022829, 0.0006063494947738945, 0.0, 0.05486377328634262, 0.0, 0.01125272735953331, 0.06181521341204643, 0.020853886380791664, 0.14313699305057526, 0.014031094498932362, 0.006504695862531662, 0.002963315462693572, 7.036422175588086e-05, 0.015860864892601967, 0.075156569480896, 0.0, 0.017294352874159813, 0.07423021644353867, 0.032176561653614044, 0.0005151237710379064, 0.0014554982772096992, 0.014703981578350067, 0.0, 0.08495330810546875, 0.004020344000309706, 0.03613511100411415, 0.0, 0.0, 0.0, 0.13742175698280334, 0.0, 0.0, 0.001429745345376432, 0.028764886781573296, 0.001467705937102437, 0.00017554336227476597, 0.028548171743750572, 0.0021604036446660757, 0.0, 0.08681520819664001, 0.0, 0.0021546443458646536, 0.0, 0.07904709130525589, 0.002707026433199644, 0.015105877071619034, 0.0, 0.010970920324325562, 0.0, 0.132237046957016, 0.02913208305835724, 0.039376743137836456, 0.0473981648683548, 0.013722831383347511, 0.0, 0.06800983846187592, 0.03672675043344498, 0.0, 0.0, 0.006275685038417578, 0.03269883617758751, 0.045962508767843246, 0.008253264240920544, 0.07768193632364273, 0.004587155766785145, 0.0, 0.0071517894975841045, 0.012423624284565449, 0.0, 0.026456249877810478, 0.0428033322095871, 0.032881781458854675, 0.10828177630901337, 0.0475396066904068, 0.0, 0.0025679394602775574, 0.04961615428328514, 0.0, 0.007477168925106525, 0.06113420054316521, 0.008475193753838539, 0.0, 0.0020636480767279863, 0.033873748034238815, 0.043906498700380325, 0.0027978119906038046, 0.0007549760630354285, 0.006749055813997984, 0.0, 0.031587183475494385, 0.019266294315457344, 0.0, 0.0033048554323613644, 0.13442014157772064, 0.019954964518547058, 0.026452679187059402, 0.011420834809541702, 0.0358630008995533, 0.0008224996272474527, 0.07054121792316437, 0.0, 0.008737845346331596, 0.0546758808195591], [0.00647343834862113, 0.1553179770708084, 0.0, 0.0, 0.0607423298060894, 0.016424300149083138, 0.0031571858562529087, 0.08037400245666504, 0.003622631309553981, 0.0, 0.0, 0.02252116985619068, 0.0056726569309830666, 0.0, 0.0, 0.017101265490055084, 0.0, 0.0031630764715373516, 0.0425688810646534, 0.0, 0.03754923492670059, 0.014959966763854027, 0.020552417263388634, 0.052058134227991104, 0.0063271005637943745, 0.12271567434072495, 0.07795887440443039, 0.1666879653930664, 0.035062845796346664, 0.07422853261232376, 0.03942083939909935, 0.022039717063307762, 0.0, 0.0, 0.05820804834365845, 0.0, 0.05359623208642006, 0.04344157874584198, 0.07580355554819107, 0.07061535865068436, 0.055877622216939926, 0.09838991612195969, 0.0654749721288681, 0.0024877225514501333, 0.07015343010425568, 0.0, 0.050260353833436966, 0.1043890044093132, 0.013528697192668915, 0.0, 0.0, 0.08711875975131989, 0.012040398083627224, 0.0, 0.06935620307922363, 0.10832475870847702, 0.0195409394800663, 0.08942906558513641, 0.0428418330848217, 0.014355472289025784, 0.0011075433576479554, 0.044756997376680374, 0.011858215555548668, 0.006314835976809263, 0.010396621190011501, 0.01331411488354206, 0.04951832816004753, 0.02372760884463787, 0.0, 0.050810180604457855, 0.08794806152582169, 0.043535299599170685, 0.023748651146888733, 0.002509589772671461, 0.0, 0.001722420915029943, 0.0, 0.0, 0.0, 0.0, 0.01871328242123127, 0.001590006286278367, 0.0, 0.0, 0.0, 0.0, 0.044102758169174194, 0.07104603201150894, 0.02406390942633152, 0.00125834159553051, 0.0008576915715821087, 0.033776164054870605, 0.030282437801361084, 0.08966279774904251, 5.478855382534675e-05, 0.027406973764300346, 0.0, 0.0, 0.001370141515508294, 0.0, 0.0, 0.008420496247708797, 0.03019491583108902, 0.001548862550407648, 0.058971941471099854, 0.033468715846538544, 0.0, 0.0, 0.05851246416568756, 0.014470036141574383, 0.05213479325175285, 0.054307721555233, 0.01474851742386818, 0.0, 0.004351993557065725, 0.0646216943860054, 0.0, 0.07862070947885513, 0.056539054960012436, 0.0, 0.0056793880648911, 0.0, 0.0, 0.0039660451002418995, 0.0, 0.007367705460637808, 0.0351705476641655, 0.08073723316192627, 0.0, 0.01971646398305893, 0.04385761171579361, 0.0, 0.05524946749210358, 0.0, 0.004325560759752989, 0.002510181860998273, 0.0008225353085435927, 0.002139671240001917, 0.08118095993995667, 0.0009379872353747487, 0.0, 0.016249343752861023, 0.0068131485022604465, 0.0012124970089644194, 0.030952872708439827, 0.010552062653005123, 0.06961960345506668, 0.03065432235598564, 0.0, 0.001687883399426937, 0.07465721666812897, 0.007373745087534189, 0.030118493363261223, 0.054386500269174576, 0.03210031986236572, 0.0060521201230585575, 0.05868178978562355, 0.013689803890883923, 0.07994017004966736, 0.0026150636840611696, 0.03590472787618637, 0.0, 0.008421688340604305, 0.010155193507671356, 0.013363027013838291, 0.04048306122422218, 0.0, 0.1001954898238182, 0.007066397462040186, 0.03638720139861107, 0.00024333654437214136, 0.0011641676537692547, 0.0, 0.03284912556409836, 0.05438018962740898, 0.0, 0.013372144661843777, 0.010918785817921162, 0.02097407355904579, 0.0014871718594804406, 0.02044408768415451, 0.0198530163615942, 0.021827861666679382, 0.08163311332464218, 0.024131789803504944, 0.0326322577893734, 0.07757759839296341, 0.023357093334197998, 0.0016334938118234277, 0.00042750066495500505, 0.09886277467012405, 0.006805394776165485, 0.02794921211898327, 0.0, 0.0, 0.007628272287547588, 0.11966764181852341, 0.07703670114278793, 0.0, 0.06606439501047134, 0.10744809359312057, 0.06531355530023575, 0.004677901975810528, 0.1273217499256134, 0.007607178762555122, 0.03711944445967674, 0.023057052865624428, 0.0, 0.0073016672395169735, 0.0, 0.00011479300883365795, 0.05420815944671631, 0.0972960963845253, 0.0, 0.006262898910790682, 0.05497438833117485, 0.0005926832091063261, 0.0654544085264206, 0.08037351816892624, 0.0, 0.05205926671624184, 0.00394802400842309, 0.015463829971849918, 0.09683800488710403, 0.0, 0.050277598202228546, 0.04230095446109772, 0.053404029458761215, 0.07930629700422287, 0.001151803880929947, 0.06273150444030762, 0.016686657443642616, 0.03503529727458954, 0.004724725615233183, 0.0022545168176293373, 0.0, 0.0980767011642456, 0.0016786879859864712, 0.0, 0.0010672413045540452, 0.014999129809439182, 0.1073320135474205, 0.10694839805364609, 0.03705139085650444, 0.010468246415257454, 4.552986501948908e-05, 0.0599786601960659, 0.004171690437942743, 0.0585753358900547, 0.025432225316762924, 0.0, 0.1388290375471115, 0.0, 0.00024937765556387603, 0.09962768852710724, 0.1074114441871643, 0.01707148179411888, 0.0, 0.009509170427918434, 0.010332245379686356, 0.05468979477882385, 0.0, 0.0273850429803133, 0.0231524258852005, 0.018337205052375793, 0.06069774180650711, 0.0, 0.012887360528111458, 0.016622688621282578, 0.01396394893527031, 0.0, 0.0, 0.07533463090658188, 0.0, 0.0032794808503240347, 0.026886172592639923, 0.06865376234054565, 0.0, 0.0769088938832283, 0.0, 0.04728279262781143, 0.05032661557197571, 0.06624625623226166, 0.0, 0.08777571469545364, 0.028667578473687172, 0.0006404767045751214, 0.01350022666156292, 0.013072250410914421, 0.0, 0.0, 0.0, 0.11909683793783188, 0.06611546128988266, 0.06616097688674927, 0.00041604743455536664, 0.13712100684642792, 0.0, 0.0013454644940793514, 0.07307474315166473, 0.015057126060128212, 0.0388190858066082, 0.01359572820365429, 0.06791173666715622, 0.001329506398178637, 0.004252796061336994, 0.0, 0.007589539047330618, 0.0, 0.03451312333345413, 0.04499354213476181, 0.01658056303858757, 0.05988794565200806, 0.07560393959283829, 0.012029966339468956, 0.0186605304479599, 0.06116226688027382, 0.0017776214517652988, 0.0, 0.04964330419898033, 0.06965676695108414, 0.03203437477350235, 0.0, 0.0041883341036736965, 0.005475098732858896, 0.0, 0.040174681693315506, 0.08815912902355194, 0.00223254831507802, 0.06616111099720001, 0.06400667130947113, 0.09780099242925644, 0.0, 0.09660325199365616, 0.03958214074373245, 0.0008590574143454432, 0.08869267255067825, 0.0, 0.015558251179754734, 0.0808131992816925, 0.008641661144793034, 0.06879650056362152, 0.08859371393918991, 0.019198304042220116, 0.010602491907775402, 0.05253346636891365, 0.0, 0.0037215049378573895, 0.0, 0.0, 0.0, 0.00015455065295100212, 0.0, 0.0, 0.0, 0.0003044086042791605, 0.004169506952166557, 0.0, 0.13176032900810242, 0.0, 0.0011097793467342854, 0.005906589794903994, 0.0, 0.08507133275270462, 0.0277208611369133, 0.009404705837368965, 0.005097922403365374, 0.0, 0.01004925649613142, 0.0, 0.023939575999975204, 0.0005710193072445691, 0.04476896673440933, 0.08761429786682129, 0.0, 0.002279234118759632, 0.005368005484342575, 0.01190964225679636, 0.0, 0.07891575247049332, 0.0, 0.029817761853337288, 0.0406891293823719, 0.011123809963464737, 0.0726543441414833, 0.042240798473358154, 0.0, 0.06315714120864868, 0.0, 0.0, 0.012385522946715355, 0.0, 0.016090210527181625, 0.0, 0.0007415535510517657, 0.012103934772312641, 0.04037012159824371, 0.0009387860773131251, 0.005705705378204584, 0.0, 0.0, 0.0004299675056245178, 0.03910710662603378, 0.08968012034893036, 0.0327141098678112, 0.014693989418447018, 0.0, 0.00925037544220686, 0.0, 0.0, 0.03881360962986946, 0.0, 0.02393098920583725, 0.0, 0.0, 0.029287196695804596, 0.0, 0.07365098595619202, 0.008756514638662338, 0.008474973030388355, 0.04264743626117706, 0.01867104321718216, 0.03799822926521301, 0.07473575323820114, 0.0002022735570790246, 0.03736082836985588, 0.06042449176311493, 0.037362124770879745, 0.0, 0.0, 0.010278810746967793, 0.0, 0.06472218781709671, 0.00513828918337822, 0.05382664129137993, 0.0, 0.0, 0.0, 0.09974239021539688, 0.07115150988101959, 0.0, 0.06727807968854904, 0.0, 0.0026847596745938063, 0.045181576162576675, 0.12159502506256104, 0.0, 0.0, 0.13591083884239197, 0.042907726019620895, 0.03701663017272949, 0.03251578286290169, 0.11442079395055771, 0.0, 0.0064844065345823765, 0.06056564301252365, 0.024701489135622978, 0.0, 0.014511914923787117, 0.08840645104646683, 0.0, 0.04270312190055847, 0.040310513228178024, 0.0, 0.002056403085589409, 0.05291243642568588, 0.0, 0.0, 0.0, 0.009200445376336575, 0.13566406071186066, 0.0, 0.05427156388759613, 0.0011546309106051922, 0.0, 0.011095778085291386, 0.0, 0.00883424747735262, 0.013211633078753948, 0.055878423154354095, 0.013495305553078651, 0.11160184442996979, 0.0, 0.0039839968085289, 0.0496964231133461, 0.0038927169516682625, 0.007857597433030605, 0.020059769973158836, 0.00030859120306558907, 0.012852467596530914, 0.024422351270914078, 0.030663536861538887, 0.0, 0.06405419111251831, 0.04974665865302086, 0.0014983783476054668, 0.0, 0.00038952098111622036, 0.0003927687357645482, 0.032703548669815063, 0.0009211951983161271, 0.0, 0.061451759189367294, 0.06235424056649208, 0.01980547234416008, 0.006686518434435129, 0.05075077712535858, 0.004839728120714426, 0.038043729960918427, 0.0, 0.08153244853019714, 0.002128564054146409]], "last_cam": "8", "nombre": "Desconocido", "ts": 1782406605.932829, "actualizaciones_globales": 12}, "109": {"galeria_deep": [[0.01087245438247919, 0.05437138304114342, 0.0, 0.0, 0.07342949509620667, 0.04187188670039177, 0.002946866676211357, 0.055416010320186615, 0.0001975755876628682, 0.004041287116706371, 0.027557913213968277, 0.017816567793488503, 0.00676485151052475, 0.0, 0.0, 0.0, 0.037660107016563416, 0.02893896959722042, 0.002780427224934101, 0.006457403767853975, 0.08128175139427185, 0.11083966493606567, 0.0019089963752776384, 0.0, 0.017963266000151634, 0.0756724551320076, 0.01307617500424385, 0.05164097622036934, 0.0003226700355298817, 0.005737880244851112, 0.0, 0.014668754301965237, 0.0, 0.07017557322978973, 0.03884800896048546, 0.0, 0.02313360944390297, 0.04019252210855484, 0.06131790950894356, 0.050413504242897034, 0.0010912910802289844, 0.02553662285208702, 0.037833817303180695, 0.0, 0.03252677619457245, 0.0, 0.0, 0.06139058247208595, 0.00601339852437377, 0.0, 0.0, 0.1000666618347168, 0.0, 0.023586513474583626, 0.14516843855381012, 0.11193469166755676, 0.011174749583005905, 0.05794542282819748, 0.07193132489919662, 0.01062395516782999, 0.012633453123271465, 0.04598784074187279, 0.00043856975389644504, 0.018521977588534355, 0.0, 0.04128924012184143, 0.07147863507270813, 0.008014908991754055, 0.0, 0.01875649392604828, 0.07289630174636841, 0.005398849491029978, 0.039263203740119934, 0.019267583265900612, 0.038910944014787674, 0.0, 0.005584234371781349, 0.0, 0.037637535482645035, 0.0, 0.040576666593551636, 0.0017899784725159407, 0.0, 0.015629064291715622, 0.0, 0.012720152735710144, 0.0041916510090231895, 0.1018957868218422, 0.00833947490900755, 0.001945427618920803, 0.0, 0.0, 0.07886773347854614, 0.12488125264644623, 0.0, 0.008371885865926743, 0.024293625727295876, 0.0, 0.0, 0.015002794563770294, 0.0, 0.04602610319852829, 0.04439852014183998, 0.0, 0.009046194143593311, 0.03044922463595867, 0.0, 0.031393032521009445, 0.039812371134757996, 0.0, 0.08221645653247833, 0.033474911004304886, 0.05642247945070267, 0.0, 0.013565141707658768, 0.040352657437324524, 0.0, 0.0141392070800066, 0.0080353282392025, 0.0, 0.035487979650497437, 0.0006042556487955153, 0.0008177892304956913, 0.00031049331300891936, 0.005327226128429174, 0.0, 0.0017137241084128618, 0.08681304007768631, 0.008708616718649864, 0.059012215584516525, 0.041312482208013535, 0.0027768390718847513, 6.112080882303417e-05, 0.0, 0.04574307054281235, 0.005121846217662096, 0.011014701798558235, 0.04609541967511177, 0.01955226995050907, 0.032534852623939514, 0.003762866137549281, 0.000285754184005782, 0.024177828803658485, 0.008928412571549416, 0.0174142736941576, 0.006436638534069061, 0.17134828865528107, 0.05827466771006584, 0.0, 0.0, 0.02680259570479393, 0.0, 0.007585783954709768, 0.0, 0.04528665915131569, 0.0, 0.003355756402015686, 0.02739495411515236, 0.1223665177822113, 0.019190220162272453, 0.07973416894674301, 0.0, 0.0, 0.028117110952734947, 0.02786037139594555, 0.08305453509092331, 0.0, 0.07505061477422714, 0.0, 0.11800797283649445, 0.0, 0.006609432399272919, 0.018403200432658195, 0.003250507405027747, 0.05007059499621391, 0.015014315955340862, 0.0, 0.040858082473278046, 0.038176823407411575, 0.038488417863845825, 0.0, 0.004568496253341436, 0.07247310876846313, 0.05531175434589386, 0.02919444814324379, 0.013541816733777523, 0.09967997670173645, 0.014082266017794609, 0.0, 0.017831357195973396, 0.1635575294494629, 0.0009513677796348929, 0.0, 0.0, 0.0216553695499897, 0.0449671633541584, 0.11660518497228622, 0.000668217078782618, 0.0, 0.04520284757018089, 0.1250789314508438, 0.043828777968883514, 0.02286229096353054, 0.11126915365457535, 0.015087123960256577, 0.0570463091135025, 0.0006299364031292498, 0.019142810255289078, 0.008957501500844955, 0.0, 0.002109037945047021, 0.0971967950463295, 0.0, 0.0, 0.03047351725399494, 0.019527779892086983, 0.07149078696966171, 0.07496704161167145, 0.0817110687494278, 0.0, 0.004826414864510298, 0.0, 0.04688243195414543, 0.06932947784662247, 0.0, 0.0266006700694561, 0.029051918536424637, 0.06870697438716888, 0.07805386185646057, 0.0, 0.05820267274975777, 0.0016752256779000163, 0.08015001565217972, 0.025933241471648216, 0.0, 0.0, 0.034626323729753494, 0.007838721387088299, 0.043908677995204926, 0.05535941198468208, 0.023853957653045654, 0.0335189551115036, 0.050909195095300674, 0.01594824157655239, 0.0, 0.0, 0.013101627118885517, 0.0, 0.044168006628751755, 0.026529327034950256, 0.0, 0.02385704778134823, 0.025081608444452286, 0.0, 0.06500069797039032, 0.0965103805065155, 0.010949413292109966, 0.0, 0.07414832711219788, 0.02679181843996048, 0.08771158754825592, 0.008842304348945618, 0.002159928437322378, 0.007877239026129246, 0.06966421008110046, 0.015661127865314484, 0.008844944648444653, 0.006703813094645739, 0.0707351565361023, 0.0, 0.0, 0.0, 0.02628990449011326, 0.0, 0.006019544787704945, 0.009756318293511868, 0.0, 0.025503193959593773, 0.0047936649061739445, 0.03695822134613991, 0.06875678896903992, 0.041876085102558136, 0.060775477439165115, 0.005188035778701305, 0.13160863518714905, 0.1316191703081131, 0.00033184720086865127, 0.0317101925611496, 0.0, 0.07927854359149933, 0.049152713268995285, 0.0028582378290593624, 0.03128478676080704, 0.056026093661785126, 0.04375288635492325, 0.0, 0.16148103773593903, 0.0, 0.0, 0.013083781115710735, 0.0, 0.06324496120214462, 0.03959343582391739, 0.010849901475012302, 0.023673702031373978, 0.045859385281801224, 0.0014647667994722724, 0.009522119536995888, 0.026409165933728218, 0.05145987868309021, 0.026160260662436485, 0.0, 0.12729185819625854, 0.06450051069259644, 0.027268581092357635, 0.002672998933121562, 0.035798899829387665, 0.0027457685209810734, 0.0, 0.045970022678375244, 0.013282355852425098, 0.03457178547978401, 0.00873479526489973, 0.04823470115661621, 0.002421439392492175, 0.04123152419924736, 0.09437087178230286, 0.14701136946678162, 0.019078852608799934, 0.04669712483882904, 0.029965989291667938, 0.05338622257113457, 0.00143089285120368, 0.03089245967566967, 0.15742181241512299, 0.013851718045771122, 0.0, 0.012787967920303345, 0.010756398551166058, 0.049121733754873276, 8.652390533825383e-05, 0.013282767497003078, 0.11785675585269928, 0.002574828453361988, 0.08552838116884232, 0.051729585975408554, 0.0, 0.0007950237486511469, 0.0, 0.004068946931511164, 0.015860363841056824, 0.02706555835902691, 0.05946407467126846, 0.026180988177657127, 0.0, 0.037706270813941956, 0.03568051755428314, 0.06419378519058228, 0.04710771143436432, 0.0, 0.0, 0.0, 0.0, 0.06705290824174881, 0.0011241792235523462, 0.0025033047422766685, 0.00945163145661354, 0.011613723821938038, 0.0, 0.0, 0.005391934420913458, 0.0, 0.09054543077945709, 0.09431963413953781, 0.0030315001495182514, 0.0, 0.0, 0.00683251116424799, 0.0, 0.021762538701295853, 0.00010404556087451056, 0.06951681524515152, 0.030365413054823875, 0.0, 0.058827757835388184, 0.0027976406272500753, 0.041909120976924896, 0.05972035601735115, 0.0, 0.0, 0.01660262420773506, 0.02777295932173729, 0.08260929584503174, 0.0018661890644580126, 0.0, 0.04995207116007805, 0.0013414622517302632, 0.04236621409654617, 0.00610499968752265, 0.0, 0.0077743493020534515, 0.0, 0.0014982790453359485, 0.07667869329452515, 0.02042124792933464, 0.0, 0.005819984246045351, 0.0, 0.036804575473070145, 0.0, 0.07410522550344467, 0.000502750976011157, 0.00040687996079213917, 0.0, 0.0, 0.0433664508163929, 0.0, 0.08966880291700363, 0.0013261704007163644, 0.007138322107493877, 0.04351323843002319, 0.0, 0.056778013706207275, 0.12581674754619598, 0.0, 0.09856195002794266, 0.06215973570942879, 0.08977945894002914, 0.010736379772424698, 0.0, 0.001748524373397231, 0.0, 0.07122495770454407, 0.00446921493858099, 6.968326488276944e-05, 0.07092694938182831, 0.0, 0.004309098236262798, 0.03931725025177002, 0.03331255912780762, 0.0, 0.05214327946305275, 0.006232530809938908, 0.02677866816520691, 0.06719092279672623, 0.006260300520807505, 0.0, 0.0, 0.01976761221885681, 0.0, 0.03468245640397072, 0.029376458376646042, 0.11711578071117401, 0.055689487606287, 0.0, 0.001923729432746768, 0.0, 0.0, 0.01700715720653534, 0.03569011390209198, 0.0008198327268473804, 0.01577380858361721, 0.0032665422186255455, 0.000311217678245157, 0.07727551460266113, 0.00759723549708724, 0.0, 0.0, 0.0026782082859426737, 0.04164011776447296, 0.07525072991847992, 0.07122606784105301, 0.05506916344165802, 0.026014233008027077, 0.0, 0.016144726425409317, 0.0007659452967345715, 0.0, 0.04237370565533638, 0.06803713738918304, 0.07600920647382736, 0.030686162412166595, 0.0, 0.0, 0.07320349663496017, 0.014598687179386616, 0.012460444122552872, 0.0, 0.08768805861473083, 0.006896571256220341, 0.012479392811655998, 0.0, 0.0004411762347444892, 0.09542505443096161, 0.022655442357063293, 0.0015054901596158743, 0.0, 0.030000774189829826, 0.0, 0.0, 0.0, 0.004330887459218502, 0.08387910574674606, 0.02800910361111164, 0.017893530428409576, 0.00031788129126653075, 0.09851494431495667, 0.00023297607549466193, 0.08329061418771744, 0.0, 0.002552284160628915, 0.04729200154542923]], "last_cam": "3", "nombre": null, "ts": 1782406682.3562586, "actualizaciones_globales": 3}, "110": {"galeria_deep": [[0.048550091683864594, 0.09752142429351807, 0.0, 0.0, 0.09169607609510422, 0.07818538695573807, 0.008313885889947414, 0.025293173268437386, 0.0416838601231575, 0.0, 0.0040392582304775715, 0.03801374137401581, 0.0, 0.0, 0.0011596829863265157, 0.0007282434962689877, 0.0, 0.0, 0.0048220157623291016, 0.02308395318686962, 0.09711187332868576, 0.10762622952461243, 0.007996538653969765, 0.0, 0.007737577892839909, 0.07606611400842667, 0.06853748112916946, 0.069479800760746, 0.0005056334775872529, 0.0, 0.02466130629181862, 0.038645606487989426, 0.0, 0.0513504259288311, 0.027694888412952423, 0.004308757372200489, 0.0350910909473896, 0.017734892666339874, 0.0063854544423520565, 0.03271924704313278, 0.0, 0.03024958446621895, 0.06824563443660736, 0.000801759772002697, 0.004625340923666954, 0.0, 0.014276846311986446, 0.09105641394853592, 0.0, 0.0, 7.262348844960798e-06, 0.008602401241660118, 0.009932751767337322, 0.035627540200948715, 0.08494417369365692, 0.09106292575597763, 0.034768763929605484, 0.027441352605819702, 0.04221240431070328, 0.0072278776206076145, 0.003883324097841978, 0.0, 0.01188826747238636, 0.00044737537973560393, 0.0003887574130203575, 0.0, 0.08990256488323212, 0.0, 0.0, 0.07954256981611252, 0.061473581939935684, 0.01091733481734991, 0.0010860522743314505, 0.041463106870651245, 0.039252880960702896, 0.0, 0.0, 0.0, 0.00049583800137043, 0.0, 0.04352391138672829, 0.0, 0.0, 0.0126123558729887, 0.0, 0.0, 0.0015014734817668796, 0.049639247357845306, 0.0028861502651125193, 0.0003157519386149943, 0.0019006410147994757, 0.005727238953113556, 0.09894167631864548, 0.09603672474622726, 0.0, 0.05037512630224228, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06963355839252472, 0.05550272762775421, 0.010010889731347561, 0.015286359935998917, 0.01558325532823801, 0.0, 0.0007013717549853027, 0.031109774485230446, 0.01731831580400467, 0.010897497646510601, 0.007339670322835445, 0.07870801538228989, 0.017631012946367264, 0.0013019437901675701, 0.03905348479747772, 0.0, 0.046559348702430725, 0.05101973935961723, 0.006527043879032135, 0.00044190994231030345, 0.0, 0.00010403579653939232, 0.06312347203493118, 0.0, 0.0, 0.00564483692869544, 0.06973448395729065, 0.0012979164021089673, 0.11179615557193756, 0.03797219321131706, 7.664076110813767e-05, 0.0004173224442638457, 0.0, 0.0, 0.014992984011769295, 0.008120239712297916, 0.05885150656104088, 0.006154255475848913, 0.026021867990493774, 0.0, 0.0, 0.0554417259991169, 0.0011477008229121566, 0.006191154010593891, 0.008514522574841976, 0.15150898694992065, 0.0382695347070694, 0.0, 0.00023407096159644425, 0.0018542344914749265, 0.002763911848887801, 0.05238284915685654, 0.0, 0.08850694447755814, 0.00023849103308748454, 0.035618189722299576, 0.0, 0.10272754728794098, 0.01852332055568695, 0.052005600184202194, 0.0, 0.0, 0.0054828268475830555, 0.00977563951164484, 0.059157587587833405, 0.0, 0.06294224411249161, 0.004630669020116329, 0.14491362869739532, 0.005399064626544714, 0.000732339161913842, 0.0, 0.014154898934066296, 0.0464860200881958, 0.008481905795633793, 0.0007237225654534996, 0.028405286371707916, 0.026812050491571426, 0.03371425345540047, 0.006099306978285313, 0.006893308367580175, 0.007926532067358494, 0.06648831069469452, 0.019001036882400513, 0.003110748017206788, 0.056730352342128754, 0.005929865874350071, 0.03906463831663132, 0.025152908638119698, 0.13091926276683807, 0.05367609113454819, 0.0007395917200483382, 0.0003990870318375528, 0.01655495911836624, 0.0, 0.16196390986442566, 0.008572826161980629, 0.001195424934849143, 0.02877155877649784, 0.08436287194490433, 0.08545491099357605, 0.013071768917143345, 0.04116906225681305, 0.03214006498456001, 0.10912992060184479, 0.020046468824148178, 0.000527858326677233, 0.03189385309815407, 0.0, 0.009645269252359867, 0.04824865609407425, 0.06013888865709305, 0.0, 0.0686924159526825, 0.008944488130509853, 0.044742364436388016, 0.04405170679092407, 0.08900086581707001, 0.0, 0.007542875129729509, 0.004673759452998638, 0.005578057374805212, 0.10440307855606079, 0.003110921708866954, 0.06333959102630615, 0.01071224082261324, 0.06162428483366966, 0.012545092962682247, 0.001510348403826356, 0.06474190950393677, 0.042640961706638336, 0.11921101063489914, 0.0, 0.02933722920715809, 0.0, 0.0055011878721416, 0.0, 0.008707601577043533, 0.007922780700027943, 0.014104540459811687, 0.10029105097055435, 0.05809398740530014, 0.04451943561434746, 0.0019498808542266488, 0.0, 0.038968026638031006, 0.01454975362867117, 0.07902064174413681, 0.06270746141672134, 0.0, 0.05933167785406113, 0.026823250576853752, 0.00030507537303492427, 0.1363184005022049, 0.1037805825471878, 0.0, 0.0, 0.05060633271932602, 0.0769377276301384, 0.0799710601568222, 0.0006330858450382948, 0.08805028349161148, 0.00028472125995904207, 0.045057907700538635, 0.05170469358563423, 0.0, 0.05320227891206741, 0.042465679347515106, 0.0011626217747107148, 0.01788231171667576, 0.0, 0.07478996366262436, 0.0036333196330815554, 0.015544293448328972, 0.008635209873318672, 0.005467639770358801, 0.0, 0.046811990439891815, 0.015938784927129745, 0.03307070583105087, 0.07138311117887497, 0.017437754198908806, 0.00022400471789296716, 0.06996552646160126, 0.029660681262612343, 0.0, 0.08222407847642899, 0.01760697551071644, 0.00025428907247260213, 0.017803579568862915, 0.006028720643371344, 0.05230790376663208, 0.03160599619150162, 0.04594215378165245, 0.028557604178786278, 0.19750826060771942, 0.006999085191637278, 0.0023864046670496464, 0.05171724408864975, 0.0, 0.15161138772964478, 0.014943510293960571, 0.03923901170492172, 0.0402870774269104, 0.07360292226076126, 0.0, 0.0008731627021916211, 0.0014199628494679928, 0.08156424015760422, 0.057152245193719864, 0.0010861387709155679, 0.11416991800069809, 0.13411425054073334, 0.026056112721562386, 0.03848597779870033, 0.004681083839386702, 0.002505915705114603, 0.02221972681581974, 0.0008676839061081409, 0.09191717207431793, 0.015726542100310326, 0.0, 0.04665033891797066, 0.0031843476463109255, 0.0011005130363628268, 0.023519262671470642, 0.12177469581365585, 0.022885311394929886, 0.12243571877479553, 0.03653409704566002, 0.050911180675029755, 0.0014007134595885873, 0.028662540018558502, 0.057251155376434326, 0.06480024009943008, 0.0, 0.00024909860803745687, 0.0, 0.08480716496706009, 0.0637664720416069, 0.014111658558249474, 0.07581526786088943, 0.0, 0.09429710358381271, 0.07523538917303085, 0.0, 0.003278326475992799, 0.0, 0.002134759444743395, 0.026604201644659042, 0.0, 0.0, 0.0, 0.0, 0.05283249169588089, 0.01223092619329691, 0.01806161180138588, 0.05583595484495163, 0.0, 0.001947287586517632, 0.0, 0.0007551673916168511, 0.013084379956126213, 0.04474356025457382, 0.0071050687693059444, 0.006035295315086842, 0.0, 0.0027477687690407038, 0.0, 0.013510645367205143, 0.003444112604483962, 0.08478577435016632, 0.08718597143888474, 0.008827916346490383, 0.0, 0.0, 0.0, 0.0, 0.08359570056200027, 0.00199686037376523, 0.0014341011410579085, 0.05998794361948967, 0.0, 0.019611645489931107, 0.0, 0.0, 0.030205657705664635, 0.0, 0.0008477691444568336, 0.05849524959921837, 0.0, 0.06702738255262375, 0.0, 0.0, 0.054471809417009354, 0.0020473813638091087, 0.08253508806228638, 0.01039577554911375, 0.0, 0.012643962167203426, 0.007573435548692942, 0.009312616661190987, 0.08841769397258759, 0.03522568941116333, 0.0, 0.0, 0.0, 0.0005664132768288255, 0.0022519559133797884, 0.06820419430732727, 0.0, 0.0038751233369112015, 0.0, 0.0012325241696089506, 0.07386434823274612, 0.0013536225305870175, 0.107340008020401, 0.052797164767980576, 0.0005166457849554718, 0.007959327660501003, 0.0, 0.021209100261330605, 0.05795787647366524, 0.0, 0.10108029097318649, 0.012479136697947979, 0.04309057071805, 0.015595853328704834, 0.0, 0.012023307383060455, 0.0, 0.06811594218015671, 0.0009862544247880578, 0.010632304474711418, 0.02333930879831314, 0.0, 0.0, 0.0675428956747055, 0.02469242922961712, 0.0, 0.031209703534841537, 0.032993242144584656, 0.014405332505702972, 0.04115414619445801, 0.00026350750704295933, 0.0, 0.0, 0.058289870619773865, 0.002492708619683981, 0.013905405066907406, 0.03797273337841034, 0.08049523830413818, 0.00019193583284504712, 0.005763822700828314, 0.08453942835330963, 0.00861547701060772, 0.0, 0.0378720723092556, 0.06355438381433487, 0.00038967913133092225, 0.00039602353353984654, 0.012182803824543953, 0.002429546322673559, 0.0, 0.004642503801733255, 0.0, 0.0, 0.002292667981237173, 0.00012071462697349489, 0.091094009578228, 0.03177682310342789, 0.030003808438777924, 0.0, 0.0, 0.047420937567949295, 0.00216897064819932, 0.0, 0.008033056743443012, 0.15185269713401794, 0.008046013303101063, 0.07111330330371857, 0.0011047620791941881, 0.012807025574147701, 0.000582497741561383, 0.012403232045471668, 0.01091451570391655, 0.005320372991263866, 0.08916379511356354, 0.004618551582098007, 0.06114364042878151, 0.010564693249762058, 0.0030716070905327797, 0.053111277520656586, 0.00587867945432663, 0.0033204921055585146, 0.0, 0.00013611758186016232, 0.051592353731393814, 0.014958750456571579, 0.001245623454451561, 0.0, 0.08277631551027298, 0.014036593027412891, 0.06050572171807289, 0.0008108632173389196, 0.0855841264128685, 0.0016338377026841044, 0.0454108901321888, 0.0, 0.015618509612977505, 0.0]], "last_cam": "7", "nombre": null, "ts": 1782406700.626288, "actualizaciones_globales": 3}} \ No newline at end of file diff --git a/cache_nombres/nombre_Alfredo Martinez.mp3 b/cache_nombres/nombre_Alfredo Martinez.mp3 new file mode 100644 index 0000000..1ced2fd Binary files /dev/null and b/cache_nombres/nombre_Alfredo Martinez.mp3 differ diff --git a/cache_nombres/nombre_Ana Lidia Gaspariano.mp3 b/cache_nombres/nombre_Ana Lidia Gaspariano.mp3 new file mode 100644 index 0000000..d1444ae Binary files /dev/null and b/cache_nombres/nombre_Ana Lidia Gaspariano.mp3 differ diff --git a/cache_nombres/nombre_Aridai montiel.mp3 b/cache_nombres/nombre_Aridai montiel.mp3 new file mode 100644 index 0000000..0c87aa1 Binary files /dev/null and b/cache_nombres/nombre_Aridai montiel.mp3 differ diff --git a/cache_nombres/nombre_Bertha.mp3 b/cache_nombres/nombre_Bertha.mp3 new file mode 100644 index 0000000..0b51021 Binary files /dev/null and b/cache_nombres/nombre_Bertha.mp3 differ diff --git a/cache_nombres/nombre_Brayan Mendoza.mp3 b/cache_nombres/nombre_Brayan Mendoza.mp3 new file mode 100644 index 0000000..95675d2 Binary files /dev/null and b/cache_nombres/nombre_Brayan Mendoza.mp3 differ diff --git a/cache_nombres/nombre_Carlos Eduardo Cuamatzi.mp3 b/cache_nombres/nombre_Carlos Eduardo Cuamatzi.mp3 new file mode 100644 index 0000000..02d79b5 Binary files /dev/null and b/cache_nombres/nombre_Carlos Eduardo Cuamatzi.mp3 differ diff --git a/cache_nombres/nombre_Cristian Hernandez.mp3 b/cache_nombres/nombre_Cristian Hernandez.mp3 new file mode 100644 index 0000000..e7f89b3 Binary files /dev/null and b/cache_nombres/nombre_Cristian Hernandez.mp3 differ diff --git a/cache_nombres/nombre_Cristian Lopez Garcia.mp3 b/cache_nombres/nombre_Cristian Lopez Garcia.mp3 new file mode 100644 index 0000000..c9ce9dc Binary files /dev/null and b/cache_nombres/nombre_Cristian Lopez Garcia.mp3 differ diff --git a/cache_nombres/nombre_Daniel Vasquez Ramirez.mp3 b/cache_nombres/nombre_Daniel Vasquez Ramirez.mp3 new file mode 100644 index 0000000..d551d68 Binary files /dev/null and b/cache_nombres/nombre_Daniel Vasquez Ramirez.mp3 differ diff --git a/cache_nombres/nombre_Eedwin Santa ana.mp3 b/cache_nombres/nombre_Eedwin Santa ana.mp3 new file mode 100644 index 0000000..a20b842 Binary files /dev/null and b/cache_nombres/nombre_Eedwin Santa ana.mp3 differ diff --git a/cache_nombres/nombre_Fernando.mp3 b/cache_nombres/nombre_Fernando.mp3 new file mode 100644 index 0000000..424f7ae Binary files /dev/null and b/cache_nombres/nombre_Fernando.mp3 differ diff --git a/cache_nombres/nombre_Jair Gregorio.mp3 b/cache_nombres/nombre_Jair Gregorio.mp3 new file mode 100644 index 0000000..664a972 Binary files /dev/null and b/cache_nombres/nombre_Jair Gregorio.mp3 differ diff --git a/cache_nombres/nombre_Jesus Eduardo.mp3 b/cache_nombres/nombre_Jesus Eduardo.mp3 new file mode 100644 index 0000000..4557298 Binary files /dev/null and b/cache_nombres/nombre_Jesus Eduardo.mp3 differ diff --git a/cache_nombres/nombre_Jose Luis Tenchil.mp3 b/cache_nombres/nombre_Jose Luis Tenchil.mp3 new file mode 100644 index 0000000..cf1ea94 Binary files /dev/null and b/cache_nombres/nombre_Jose Luis Tenchil.mp3 differ diff --git a/cache_nombres/nombre_Jose Luis.mp3 b/cache_nombres/nombre_Jose Luis.mp3 new file mode 100644 index 0000000..cb2c32e Binary files /dev/null and b/cache_nombres/nombre_Jose Luis.mp3 differ diff --git a/cache_nombres/nombre_Jose Miguel Albarado.mp3 b/cache_nombres/nombre_Jose Miguel Albarado.mp3 new file mode 100644 index 0000000..e20e91e Binary files /dev/null and b/cache_nombres/nombre_Jose Miguel Albarado.mp3 differ diff --git a/cache_nombres/nombre_Jose.mp3 b/cache_nombres/nombre_Jose.mp3 new file mode 100644 index 0000000..94381d6 Binary files /dev/null and b/cache_nombres/nombre_Jose.mp3 differ diff --git a/cache_nombres/nombre_Josue Muñoz.mp3 b/cache_nombres/nombre_Josue Muñoz.mp3 new file mode 100644 index 0000000..b3af11f Binary files /dev/null and b/cache_nombres/nombre_Josue Muñoz.mp3 differ diff --git a/cache_nombres/nombre_Mauricio Aguilar.mp3 b/cache_nombres/nombre_Mauricio Aguilar.mp3 new file mode 100644 index 0000000..71cc443 Binary files /dev/null and b/cache_nombres/nombre_Mauricio Aguilar.mp3 differ diff --git a/cache_nombres/nombre_Miguel Angel Sanchez Papalotzi.mp3 b/cache_nombres/nombre_Miguel Angel Sanchez Papalotzi.mp3 new file mode 100644 index 0000000..e2a5171 Binary files /dev/null and b/cache_nombres/nombre_Miguel Angel Sanchez Papalotzi.mp3 differ diff --git a/cache_nombres/nombre_Mileidy Perez.mp3 b/cache_nombres/nombre_Mileidy Perez.mp3 new file mode 100644 index 0000000..5ef1da1 Binary files /dev/null and b/cache_nombres/nombre_Mileidy Perez.mp3 differ diff --git a/cache_nombres/nombre_Oscar.mp3 b/cache_nombres/nombre_Oscar.mp3 new file mode 100644 index 0000000..41ff050 Binary files /dev/null and b/cache_nombres/nombre_Oscar.mp3 differ diff --git a/cache_nombres/nombre_Rafael Lopez Preza.mp3 b/cache_nombres/nombre_Rafael Lopez Preza.mp3 new file mode 100644 index 0000000..5aff18f Binary files /dev/null and b/cache_nombres/nombre_Rafael Lopez Preza.mp3 differ diff --git a/cache_nombres/nombre_Rafael.mp3 b/cache_nombres/nombre_Rafael.mp3 new file mode 100644 index 0000000..4e24bff Binary files /dev/null and b/cache_nombres/nombre_Rafael.mp3 differ diff --git a/cache_nombres/nombre_Raul Sanchez.mp3 b/cache_nombres/nombre_Raul Sanchez.mp3 new file mode 100644 index 0000000..8f01215 Binary files /dev/null and b/cache_nombres/nombre_Raul Sanchez.mp3 differ diff --git a/cache_nombres/nombre_Sergio.mp3 b/cache_nombres/nombre_Sergio.mp3 new file mode 100644 index 0000000..814011b Binary files /dev/null and b/cache_nombres/nombre_Sergio.mp3 differ diff --git a/cache_nombres/nombre_Victor Manuel Ocampo.mp3 b/cache_nombres/nombre_Victor Manuel Ocampo.mp3 new file mode 100644 index 0000000..5f528db Binary files /dev/null and b/cache_nombres/nombre_Victor Manuel Ocampo.mp3 differ diff --git a/cache_nombres/nombre_Victor.mp3 b/cache_nombres/nombre_Victor.mp3 new file mode 100644 index 0000000..98650bc Binary files /dev/null and b/cache_nombres/nombre_Victor.mp3 differ diff --git a/cache_nombres/nombre_Vikicar Aldana.mp3 b/cache_nombres/nombre_Vikicar Aldana.mp3 new file mode 100644 index 0000000..6e5cc32 Binary files /dev/null and b/cache_nombres/nombre_Vikicar Aldana.mp3 differ diff --git a/cache_nombres/nombre_Vikicar.mp3 b/cache_nombres/nombre_Vikicar.mp3 new file mode 100644 index 0000000..7153127 Binary files /dev/null and b/cache_nombres/nombre_Vikicar.mp3 differ diff --git a/cache_nombres/nombre_Wendoly.mp3 b/cache_nombres/nombre_Wendoly.mp3 new file mode 100644 index 0000000..e9d3abd Binary files /dev/null and b/cache_nombres/nombre_Wendoly.mp3 differ diff --git a/cache_nombres/nombre_Xaily Ximena Garcia.mp3 b/cache_nombres/nombre_Xaily Ximena Garcia.mp3 new file mode 100644 index 0000000..7935b39 Binary files /dev/null and b/cache_nombres/nombre_Xaily Ximena Garcia.mp3 differ diff --git a/cache_nombres/nombre_aridai montiel zistecatl.mp3 b/cache_nombres/nombre_aridai montiel zistecatl.mp3 new file mode 100644 index 0000000..5990424 Binary files /dev/null and b/cache_nombres/nombre_aridai montiel zistecatl.mp3 differ diff --git a/cache_nombres/nombre_lore.mp3 b/cache_nombres/nombre_lore.mp3 new file mode 100644 index 0000000..8c1095a Binary files /dev/null and b/cache_nombres/nombre_lore.mp3 differ diff --git a/cache_nombres/registro_movimientos.csv b/cache_nombres/registro_movimientos.csv index 4f1b7ed..8b70ccf 100644 --- a/cache_nombres/registro_movimientos.csv +++ b/cache_nombres/registro_movimientos.csv @@ -1,4 +1,25 @@ -Fecha_Hora,Nombre,Origen,Destino,Segundos_en_Origen -2026-04-13 10:40:23,Emanuel Flores,8,5,0.0 -Fecha_Hora,Nombre,Camara_Origen,Camara_Destino -2026-04-13 10:40:23,Emanuel Flores,8,5 +fecha,nombre,cam_origen,cam_destino,duracion_seg,certeza_reid,certeza_facial,imagen_rostro +2026-06-25 10:24:44,ID 100,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/100_cam6_2026-06-25.jpg +2026-06-25 10:31:05,ID 101,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/101_cam7_2026-06-25.jpg +2026-06-25 10:31:10,Rodrigo Cahuantzi C,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam7_2026-06-25.jpg +2026-06-25 10:31:12,ID 102,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/102_cam5_2026-06-25.jpg +2026-06-25 10:31:14,ID 103,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/103_cam6_2026-06-25.jpg +2026-06-25 10:31:14,Rodrigo Cahuantzi C,7,8,5.27,75.4,0.0,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam7_2026-06-25.jpg +2026-06-25 10:31:22,Rodrigo Cahuantzi C,8,3,6.86,78.5,0.0,cache_nombres/auditoria_caras/101_cam3_2026-06-25.jpg +2026-06-25 10:31:28,ID 103,6,3,10.76,75.3,0.0,cache_nombres/auditoria_caras/103_cam3_2026-06-25.jpg +2026-06-25 10:32:37,ID 104,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/104_cam6_2026-06-25.jpg +2026-06-25 10:32:43,Jesus Eduardo,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/Jesus Eduardo_cam6_2026-06-25.jpg +2026-06-25 10:49:57,ID 105,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/105_cam7_2026-06-25.jpg +2026-06-25 10:50:00,ID 106,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/106_cam7_2026-06-25.jpg +2026-06-25 10:50:02,Jose Luis Tenchil,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/Jose Luis Tenchil_cam7_2026-06-25.jpg +2026-06-25 10:50:04,ID 107,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/107_cam5_2026-06-25.jpg +2026-06-25 10:50:07,ID 108,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/108_cam8_2026-06-25.jpg +2026-06-25 10:50:10,Jose Luis Tenchil,7,8,9.96,75.9,0.0,cache_nombres/auditoria_caras/Jose Luis Tenchil_cam7_2026-06-25.jpg +2026-06-25 10:50:17,ID 108,8,3,8.61,72.3,0.0,cache_nombres/auditoria_caras/108_cam3_2026-06-25.jpg +2026-06-25 10:50:32,ID 108,3,6,8.93,87.1,0.0,cache_nombres/auditoria_caras/108_cam6_2026-06-25.jpg +2026-06-25 10:56:34,ID 107,5,7,388.94,77.6,0.0,cache_nombres/auditoria_caras/107_cam5_2026-06-25.jpg +2026-06-25 10:56:37,Emanuel Flores,7,7,0.0,77.6,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg +2026-06-25 10:56:43,ID 108,6,8,368.6,84.8,0.0,cache_nombres/auditoria_caras/108_cam6_2026-06-25.jpg +2026-06-25 10:56:52,Emanuel Flores,5,3,13.57,79.7,0.0,cache_nombres/auditoria_caras/107_cam3_2026-06-25.jpg +2026-06-25 10:58:01,ID 109,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/109_cam3_2026-06-25.jpg +2026-06-25 10:58:19,ID 110,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/110_cam7_2026-06-25.jpg diff --git a/cache_nombres/registro_movimientos1.csv b/cache_nombres/registro_movimientos1.csv new file mode 100644 index 0000000..f63f744 --- /dev/null +++ b/cache_nombres/registro_movimientos1.csv @@ -0,0 +1,350 @@ +Fecha_Hora,Nombre,Origen,Destino,Segundos_en_Origen +2026-04-13 10:40:23,Emanuel Flores,8,5,0.0 +Fecha_Hora,Nombre,Camara_Origen,Camara_Destino +2026-04-13 10:40:23,Emanuel Flores,8,5 +2026-04-15 08:43:02,Rodrigo Cahuantzi C,8,5,0.0 +2026-04-15 08:43:02,Rodrigo Cahuantzi C,8,5 +2026-04-15 08:43:44,Rodrigo Cahuantzi C,3,7,41.88 +2026-04-15 08:43:44,Rodrigo Cahuantzi C,3,7 +2026-04-16 10:45:06,Rodrigo Cahuantzi C,5,7,0.0 +2026-04-16 10:45:06,Rodrigo Cahuantzi C,5,7 +2026-04-17 08:52:33,Rodrigo Cahuantzi C,8,5,0.0 +2026-04-17 08:52:33,Rodrigo Cahuantzi C,8,5 +2026-04-17 08:52:47,Rodrigo Cahuantzi C,8,7,13.88 +2026-04-17 08:52:47,Rodrigo Cahuantzi C,8,7 +2026-04-17 08:58:17,Rodrigo Cahuantzi C,5,7,0.0 +2026-04-17 08:58:17,Rodrigo Cahuantzi C,5,7 +2026-04-17 08:59:06,Rodrigo Cahuantzi C,8,5,48.53 +2026-04-17 08:59:06,Rodrigo Cahuantzi C,8,5 +2026-04-17 08:59:14,Rodrigo Cahuantzi C,8,7,7.86 +2026-04-17 08:59:14,Rodrigo Cahuantzi C,8,7 +2026-04-17 09:37:42,Rodrigo Cahuantzi C,8,5,0.0 +2026-04-17 09:37:42,Rodrigo Cahuantzi C,8,5 +2026-04-17 09:38:24,Rodrigo Cahuantzi C,3,8,41.67 +2026-04-17 09:38:24,Rodrigo Cahuantzi C,3,8 +2026-04-17 09:38:33,Rodrigo Cahuantzi C,8,7,9.61 +2026-04-17 09:38:33,Rodrigo Cahuantzi C,8,7 +2026-04-23 09:40:29,ian axel,8,5,0.0 +2026-04-23 09:40:29,ian axel,8,5 +2026-04-27 09:18:38,Vikicar,5,8,0.0 +2026-04-27 09:18:38,Vikicar,5,8 +2026-04-27 09:18:42,Vikicar,5,8,4.18 +2026-04-27 09:18:42,Vikicar,5,8 +2026-04-27 09:46:18,Rubisela Barrientos,7,8,0.0 +2026-04-27 09:46:18,Rubisela Barrientos,7,8 +2026-04-27 09:49:23,Ana Karen Guerrero,5,8,0.0 +2026-04-27 09:49:23,Ana Karen Guerrero,5,8 +2026-04-27 09:53:10,Rubisela Barrientos,5,8,411.97 +2026-04-27 09:53:10,Rubisela Barrientos,5,8 +2026-04-27 09:53:34,Wendoly,7,8,0.0 +2026-04-27 09:53:34,Wendoly,7,8 +2026-04-27 09:54:07,Wendoly,8,3,32.86 +2026-04-27 09:54:07,Wendoly,8,3 +2026-04-27 09:54:38,Wendoly,3,6,31.74 +2026-04-27 09:54:38,Wendoly,3,6 +2026-04-27 09:55:11,Emanuel Flores,3,8,0.0 +2026-04-27 09:55:11,Emanuel Flores,3,8 +2026-04-27 09:55:52,Ana Karen Guerrero,8,5,388.75 +2026-04-27 09:55:52,Ana Karen Guerrero,8,5 +2026-04-27 09:55:56,Ana Karen Guerrero,5,8,4.19 +2026-04-27 09:55:56,Ana Karen Guerrero,5,8 +2026-04-27 09:56:25,Jose Luis,8,3,29.51 +2026-04-27 09:56:25,Jose Luis,8,3 +2026-04-27 09:56:26,Jose Luis,8,3,1.02 +2026-04-27 09:56:26,Jose Luis,8,3 +2026-04-27 09:57:01,Jose Luis,3,6,34.53 +2026-04-27 09:57:01,Jose Luis,3,6 +2026-04-27 09:59:30,Bertha,7,5,0.0 +2026-04-27 09:59:30,Bertha,7,5 +2026-04-27 09:59:34,Bertha,8,5,4.12 +2026-04-27 09:59:34,Bertha,8,5 +2026-04-27 10:00:18,Bertha,6,3,44.11 +2026-04-27 10:00:18,Bertha,6,3 +2026-04-27 10:00:19,Bertha,6,3,1.08 +2026-04-27 10:00:19,Bertha,6,3 +2026-04-27 10:00:26,Bertha,8,6,7.03 +2026-04-27 10:00:26,Bertha,8,6 +2026-04-27 10:00:30,Bertha,6,5,4.25 +2026-04-27 10:00:30,Bertha,6,5 +2026-04-27 10:03:35,Jesus Eduardo,7,5,0.0 +2026-04-27 10:03:35,Jesus Eduardo,7,5 +2026-04-27 10:03:41,Jesus Eduardo,8,5,6.63 +2026-04-27 10:03:41,Jesus Eduardo,8,5 +2026-04-27 10:03:58,Jesus Eduardo,8,3,16.77 +2026-04-27 10:03:58,Jesus Eduardo,8,3 +2026-04-27 10:04:25,Jesus Eduardo,3,6,26.73 +2026-04-27 10:04:25,Jesus Eduardo,3,6 +2026-04-27 10:06:21,Jose Luis,1,7,0.0 +2026-04-27 10:06:21,Jose Luis,1,7 +2026-04-27 10:06:22,Jose Luis,1,7,1.06 +2026-04-27 10:06:22,Jose Luis,1,7 +2026-04-27 10:06:23,Jose Luis,7,1,1.61 +2026-04-27 10:06:23,Jose Luis,7,1 +2026-04-27 10:06:25,Jose Luis,1,7,1.56 +2026-04-27 10:06:25,Jose Luis,1,7 +2026-04-27 10:07:11,Miguel Angel Sanchez Papalotzi,7,5,45.74 +2026-04-27 10:07:11,Miguel Angel Sanchez Papalotzi,7,5 +2026-04-27 10:07:38,Miguel Angel Sanchez Papalotzi,8,3,27.07 +2026-04-27 10:07:38,Miguel Angel Sanchez Papalotzi,8,3 +2026-04-27 10:10:03,Victor Manuel Ocampo,7,5,0.0 +2026-04-27 10:10:03,Victor Manuel Ocampo,7,5 +2026-04-27 10:10:28,Victor Manuel Ocampo,8,3,25.14 +2026-04-27 10:10:28,Victor Manuel Ocampo,8,3 +2026-04-27 10:10:54,Victor Manuel Ocampo,3,6,25.94 +2026-04-27 10:10:54,Victor Manuel Ocampo,3,6 +2026-04-27 10:13:05,Xaily Ximena Garcia,8,5,0.0 +2026-04-27 10:13:05,Xaily Ximena Garcia,8,5 +2026-04-27 10:13:29,Xaily Ximena Garcia,8,3,24.59 +2026-04-27 10:13:29,Xaily Ximena Garcia,8,3 +2026-04-27 10:13:58,Oscar Atriano Ponce,7,6,380.53 +2026-04-27 10:13:58,Oscar Atriano Ponce,7,6 +2026-04-27 10:34:50,Raul Sanchez,7,5,0.0 +2026-04-27 10:34:50,Raul Sanchez,7,5 +2026-04-27 10:34:55,Raul Sanchez,5,8,4.59 +2026-04-27 10:34:55,Raul Sanchez,5,8 +2026-04-27 10:35:22,Raul Sanchez,8,3,27.22 +2026-04-27 10:35:22,Raul Sanchez,8,3 +2026-04-27 10:35:23,Raul Sanchez,3,8,1.21 +2026-04-27 10:35:23,Raul Sanchez,3,8 +2026-04-27 10:35:25,Raul Sanchez,8,3,1.29 +2026-04-27 10:35:25,Raul Sanchez,8,3 +2026-04-27 10:36:31,Eedwin Santa ana,7,5,0.0 +2026-04-27 10:36:31,Eedwin Santa ana,7,5 +2026-04-27 10:36:50,Eedwin Santa ana,8,3,19.58 +2026-04-27 10:36:50,Eedwin Santa ana,8,3 +2026-04-27 10:36:51,Eedwin Santa ana,8,3,1.16 +2026-04-27 10:36:51,Eedwin Santa ana,8,3 +2026-04-27 10:36:52,Eedwin Santa ana,3,8,1.05 +2026-04-27 10:36:52,Eedwin Santa ana,3,8 +2026-04-27 10:36:54,Eedwin Santa ana,3,8,1.26 +2026-04-27 10:36:54,Eedwin Santa ana,3,8 +2026-04-27 10:36:55,Eedwin Santa ana,3,8,1.29 +2026-04-27 10:36:55,Eedwin Santa ana,3,8 +2026-04-27 10:38:17,Jair Gregorio,7,5,0.0 +2026-04-27 10:38:17,Jair Gregorio,7,5 +2026-04-27 10:38:21,Jair Gregorio,5,8,4.05 +2026-04-27 10:38:21,Jair Gregorio,5,8 +2026-04-27 10:40:40,Mileidy Perez,8,5,0.0 +2026-04-27 10:40:40,Mileidy Perez,8,5 +2026-04-27 10:40:44,Mileidy Perez,8,5,4.05 +2026-04-27 10:40:44,Mileidy Perez,8,5 +2026-04-27 10:42:42,Cristian Lopez Garcia,7,5,0.0 +2026-04-27 10:42:42,Cristian Lopez Garcia,7,5 +2026-04-27 10:42:53,Cristian Lopez Garcia,5,8,11.38 +2026-04-27 10:42:53,Cristian Lopez Garcia,5,8 +2026-04-27 10:42:58,Cristian Lopez Garcia,8,5,4.25 +2026-04-27 10:42:58,Cristian Lopez Garcia,8,5 +2026-04-27 10:43:53,Josue Muñoz,7,5,0.0 +2026-04-27 10:43:53,Josue Muñoz,7,5 +2026-04-27 10:43:58,Josue Muñoz,8,5,4.49 +2026-04-27 10:43:58,Josue Muñoz,8,5 +2026-04-27 10:44:02,Josue Muñoz,8,5,4.01 +2026-04-27 10:44:02,Josue Muñoz,8,5 +2026-04-27 10:44:11,Cristian Lopez Garcia,5,3,72.91 +2026-04-27 10:44:11,Cristian Lopez Garcia,5,3 +2026-04-27 10:44:53,Josue Muñoz,3,6,50.9 +2026-04-27 10:44:53,Josue Muñoz,3,6 +2026-04-27 10:47:03,Cristian Hernandez,5,8,0.0 +2026-04-27 10:47:03,Cristian Hernandez,5,8 +2026-04-27 10:50:18,Alfredo Martinez,7,5,0.0 +2026-04-27 10:50:18,Alfredo Martinez,7,5 +2026-04-27 10:50:22,Alfredo Martinez,5,8,4.03 +2026-04-27 10:50:22,Alfredo Martinez,5,8 +2026-04-27 10:50:46,Alfredo Martinez,8,3,23.25 +2026-04-27 10:50:46,Alfredo Martinez,8,3 +2026-04-27 10:52:29,Cristian Lopez Garcia,3,7,498.62 +2026-04-27 10:52:29,Cristian Lopez Garcia,3,7 +2026-04-27 10:53:29,Jose Miguel Albarado,7,5,59.73 +2026-04-27 10:53:29,Jose Miguel Albarado,7,5 +2026-04-27 10:54:40,Josue Muñoz,3,6,587.04 +2026-04-27 10:54:40,Josue Muñoz,3,6 +2026-04-27 10:54:41,Josue Muñoz,3,6,1.13 +2026-04-27 10:54:41,Josue Muñoz,3,6 +2026-04-27 10:56:21,Daniel Vasquez Ramirez,7,8,0.0 +2026-04-27 10:56:21,Daniel Vasquez Ramirez,7,8 +2026-04-27 10:58:58,Daniel Vasquez Ramirez,5,8,156.67 +2026-04-27 10:58:58,Daniel Vasquez Ramirez,5,8 +2026-04-27 10:59:43,Daniel Vasquez Ramirez,5,8,44.4 +2026-04-27 10:59:43,Daniel Vasquez Ramirez,5,8 +2026-04-27 11:01:48,Mauricio Aguilar,8,5,0.0 +2026-04-27 11:01:48,Mauricio Aguilar,8,5 +2026-04-27 11:01:55,Mauricio Aguilar,8,5,7.0 +2026-04-27 11:01:55,Mauricio Aguilar,8,5 +2026-04-27 11:04:41,Rafael Lopez Preza,7,5,0.0 +2026-04-27 11:04:41,Rafael Lopez Preza,7,5 +2026-04-27 11:04:45,Rafael Lopez Preza,8,5,4.12 +2026-04-27 11:04:45,Rafael Lopez Preza,8,5 +2026-04-27 11:04:54,Rafael Lopez Preza,5,8,8.86 +2026-04-27 11:04:54,Rafael Lopez Preza,5,8 +2026-04-27 11:05:21,Daniel Vasquez Ramirez,5,8,338.26 +2026-04-27 11:05:21,Daniel Vasquez Ramirez,5,8 +2026-04-27 11:07:17,Mauricio Aguilar,5,8,321.38 +2026-04-27 11:07:17,Mauricio Aguilar,5,8 +2026-04-27 11:08:21,Jose Luis Tenchil,7,5,64.57 +2026-04-27 11:08:21,Jose Luis Tenchil,7,5 +2026-04-27 11:23:34,Oscar Atriano Ponce,8,5,0.0 +2026-04-27 11:23:34,Oscar Atriano Ponce,8,5 +2026-04-27 11:23:59,Oscar Atriano Ponce,8,5,24.82 +2026-04-27 11:23:59,Oscar Atriano Ponce,8,5 +2026-04-27 11:26:30,Oscar Atriano Ponce,7,1,150.48 +2026-04-27 11:26:30,Oscar Atriano Ponce,7,1 +2026-04-27 11:26:31,Oscar Atriano Ponce,6,7,1.14 +2026-04-27 11:26:31,Oscar Atriano Ponce,6,7 +2026-04-27 11:26:34,Oscar Atriano Ponce,5,6,3.55 +2026-04-27 11:26:34,Oscar Atriano Ponce,5,6 +2026-04-27 11:26:35,Oscar Atriano Ponce,5,6,1.01 +2026-04-27 11:26:35,Oscar Atriano Ponce,5,6 +2026-04-27 11:28:16,Oscar Atriano Ponce,8,5,101.21 +2026-04-27 11:28:16,Oscar Atriano Ponce,8,5 +2026-04-27 11:35:26,Ana Lidia Gaspariano,5,8,0.0 +2026-04-27 11:35:26,Ana Lidia Gaspariano,5,8 +2026-04-27 11:36:19,Ana Lidia Gaspariano,5,8,53.37 +2026-04-27 11:36:19,Ana Lidia Gaspariano,5,8 +2026-04-28 09:06:45,Josue Muñoz,1,7,0.0 +2026-04-28 09:06:45,Josue Muñoz,1,7 +2026-04-28 09:06:47,Josue Muñoz,7,1,1.86 +2026-04-28 09:06:47,Josue Muñoz,7,1 +2026-04-28 09:06:54,Alfredo Martinez,7,8,0.0 +2026-04-28 09:06:54,Alfredo Martinez,7,8 +2026-04-28 09:06:55,Alfredo Martinez,7,8,1.39 +2026-04-28 09:06:55,Alfredo Martinez,7,8 +2026-04-28 09:06:56,Alfredo Martinez,8,7,1.17 +2026-04-28 09:06:56,Alfredo Martinez,8,7 +2026-04-28 09:07:52,Miguel Angel Sanchez Papalotzi,8,5,0.0 +2026-04-28 09:07:52,Miguel Angel Sanchez Papalotzi,8,5 +2026-04-28 09:08:35,Jose Miguel Albarado,7,3,0.0 +2026-04-28 09:08:35,Jose Miguel Albarado,7,3 +2026-04-28 09:16:53,Raul Sanchez,8,5,0.0 +2026-04-28 09:16:53,Raul Sanchez,8,5 +2026-04-28 09:17:35,Rafael Lopez Preza,3,7,0.0 +2026-04-28 09:17:35,Rafael Lopez Preza,3,7 +2026-04-29 08:59:23,Alfredo Martinez,7,5,0.0 +2026-04-29 08:59:23,Alfredo Martinez,7,5 +2026-04-29 09:09:19,Josue Muñoz,5,3,0.0 +2026-04-29 09:09:19,Josue Muñoz,5,3 +2026-04-29 09:12:16,Jose Luis Tenchil,7,5,0.0 +2026-04-29 09:12:16,Jose Luis Tenchil,7,5 +2026-04-29 12:11:42,Rodrigo Cahuantzi C,3,6,0.0 +2026-04-30 09:04:58,Jose Miguel Albarado,7,5,0.0 +2026-04-30 12:29:15,Brayan Mendoza,6,3,0.0 +2026-05-04 08:58:31,Jose Miguel Albarado,7,5,0.0 +2026-05-05 09:25:24,Brayan Mendoza,7,5,0.0 +2026-05-05 09:25:47,Brayan Mendoza,5,3,22.98 +2026-05-05 11:04:10,Emanuel Flores,8,7,0.0 +2026-05-06 09:02:22,Jose Miguel Albarado,7,5,0.0 +2026-05-06 09:05:01,Jose Miguel Albarado,7,3,158.37 +2026-05-08 10:18:57,Emanuel Flores,6,3,470.3 +2026-05-08 10:20:25,Emanuel Flores,3,5,87.63 +2026-05-08 10:23:29,Emanuel Flores,5,8,184.68 +2026-05-08 10:23:32,Emanuel Flores,7,5,2.31 +2026-05-11 11:25:49,Yuriel,3,5,0.0 +2026-05-11 11:25:51,Yuriel,3,5,2.12 +2026-05-11 11:25:53,Yuriel,5,3,1.38 +2026-05-11 11:25:54,Yuriel,5,3,1.2 +2026-05-11 11:26:07,Yuriel,3,6,12.81 +2026-05-12 09:09:46,Josue Muñoz,5,7,0.0 +2026-05-12 09:10:14,Josue Muñoz,8,6,27.75 +2026-05-12 09:10:15,Josue Muñoz,8,6,1.4 +2026-05-12 09:10:17,Josue Muñoz,6,8,1.39 +2026-05-12 09:10:18,Josue Muñoz,8,6,1.03 +2026-05-12 09:16:30,Cristian Lopez Garcia,7,5,0.0 +2026-05-12 09:24:36,Emanuel Flores,7,5,0.0 +2026-05-12 09:24:50,Emanuel Flores,5,3,14.1 +2026-05-12 09:25:18,Brayan Mendoza,8,5,0.0 +2026-05-12 09:25:20,Emanuel Flores,3,1,29.82 +2026-05-12 09:25:54,Emanuel Flores,1,5,34.02 +2026-05-12 09:25:56,Brayan Mendoza,8,3,38.23 +2026-05-12 09:25:58,Brayan Mendoza,8,3,1.4 +2026-05-12 09:26:58,Emanuel Flores,5,6,63.86 +2026-05-12 09:28:32,Emanuel Flores,3,5,94.45 +2026-05-12 11:48:44,Rodrigo Cahuantzi C,8,5,0.0 +2026-05-13 09:14:08,Rodrigo Cahuantzi C,5,7,0.0 +2026-05-13 09:25:38,Mauricio Aguilar,5,8,0.0 +2026-05-13 09:28:09,Brayan Mendoza,7,3,0.0 +2026-05-13 09:30:33,Brayan Mendoza,3,6,144.01 +2026-05-13 09:38:14,Brayan Mendoza,6,3,460.16 +2026-05-13 09:41:49,Brayan Mendoza,3,6,214.89 +2026-05-13 09:43:13,Brayan Mendoza,6,3,84.42 +2026-05-13 09:43:51,Brayan Mendoza,3,5,38.48 +2026-05-13 09:44:04,Brayan Mendoza,8,5,13.14 +2026-05-13 10:13:12,Rodrigo Cahuantzi C,5,3,0.0 +2026-05-13 10:14:40,Rodrigo Cahuantzi C,8,3,88.27 +2026-05-13 11:10:15,Oscar Atriano Ponce,5,8,0.0 +2026-05-13 11:14:36,Oscar Atriano Ponce,5,3,261.06 +2026-05-13 12:22:23,Mauricio Aguilar,8,5,0.0 +2026-05-15 09:05:48,Josue Muñoz,7,5,0.0 +2026-05-15 09:06:19,Josue Muñoz,5,6,31.34 +2026-05-15 09:07:03,Josue Muñoz,6,5,44.32 +2026-05-15 09:07:05,Josue Muñoz,5,6,1.53 +2026-05-15 09:07:06,Josue Muñoz,6,5,1.0 +2026-05-15 09:07:07,Josue Muñoz,5,6,1.51 +2026-05-15 10:02:55,Emanuel Flores,5,8,0.0 +2026-05-15 10:38:27,Emanuel Flores,3,7,0.0 +2026-05-15 10:45:31,Jose Luis,5,8,0.0 +2026-05-18 09:21:25,Jose Luis Tenchil,5,7,0.0 +2026-05-18 09:21:28,Jose Luis Tenchil,7,5,3.14 +2026-05-18 11:53:50,Oscar Atriano Ponce,5,8,0.0 +2026-05-18 12:08:52,Brayan Mendoza,8,5,0.0 +2026-05-18 12:08:56,Brayan Mendoza,5,8,4.12 +2026-05-18 12:09:00,Brayan Mendoza,8,5,4.3 +2026-05-18 12:09:04,Brayan Mendoza,5,8,4.03 +2026-05-18 12:09:08,Brayan Mendoza,8,5,4.2 +2026-05-18 12:09:13,Brayan Mendoza,5,8,4.32 +2026-05-18 12:09:17,Brayan Mendoza,8,5,4.16 +2026-05-19 09:07:55,Josue Muñoz,1,7,0.0 +2026-05-19 09:07:56,Josue Muñoz,7,1,1.5 +2026-05-19 09:08:04,Josue Muñoz,1,3,7.19 +2026-05-19 09:09:11,Josue Muñoz,3,6,67.44 +2026-05-19 09:09:38,Josue Muñoz,6,3,27.56 +2026-05-19 09:11:33,Emanuel Flores,3,7,0.0 +2026-05-19 09:11:44,Mauricio Aguilar,7,8,0.0 +2026-05-19 09:11:51,Mauricio Aguilar,8,3,7.46 +2026-05-19 09:12:02,Mauricio Aguilar,3,7,10.92 +2026-05-19 09:12:12,Mauricio Aguilar,7,8,9.24 +2026-05-19 09:12:18,Josue Muñoz,3,6,159.64 +2026-05-19 09:12:21,Josue Muñoz,3,6,2.58 +2026-05-19 09:12:22,Josue Muñoz,3,6,1.24 +2026-05-19 09:12:23,Josue Muñoz,6,3,1.06 +2026-05-19 09:12:25,Josue Muñoz,3,6,1.52 +2026-05-19 09:12:27,Josue Muñoz,6,8,2.08 +2026-05-19 09:12:33,Josue Muñoz,8,3,5.93 +2026-05-19 09:15:27,Eedwin Santa ana,7,1,0.0 +2026-05-19 09:18:54,Mauricio Aguilar,8,6,402.63 +2026-05-19 09:21:08,Mauricio Aguilar,6,3,134.09 +2026-05-19 09:28:11,Emanuel Flores,5,8,0.0 +2026-05-19 09:30:12,Emanuel Flores,8,7,121.26 +2026-05-19 10:33:43,Josue Muñoz,5,8,0.0 +2026-05-19 10:35:21,Josue Muñoz,7,3,98.65 +2026-05-20 12:01:27,Mileidy Perez,8,7,0.0 +2026-05-20 12:01:44,Mileidy Perez,7,5,17.94 +2026-06-03 10:50:28,Rodrigo Cahuantzi C,6,3,7.43 +2026-06-03 10:50:44,Rodrigo Cahuantzi C,3,8,4.68 +2026-06-03 10:51:33,Rodrigo Cahuantzi C,8,1,47.64 +2026-06-03 10:51:56,Rodrigo Cahuantzi C,1,7,20.62 +2026-06-03 10:52:04,Rodrigo Cahuantzi C,7,5,2.57 +2026-06-03 10:52:23,Rodrigo Cahuantzi C,5,3,12.11 +2026-06-03 12:13:07,Brayan Mendoza,3,8,11.05 +2026-06-03 12:13:17,Brayan Mendoza,8,5,2.88 +2026-06-03 12:13:23,Brayan Mendoza,5,7,5.27 +2026-06-03 12:14:40,Rodrigo Cahuantzi C,3,8,5.63 +2026-06-03 12:14:43,Rodrigo Cahuantzi C,8,5,0.96 +2026-06-03 12:14:53,Rodrigo Cahuantzi C,5,3,6.22 +2026-06-03 12:15:06,Rodrigo Cahuantzi C,3,6,7.47 +2026-06-03 12:19:16,Brayan Mendoza,7,5,4.26 +2026-06-04 09:00:38,Alfredo Martinez,7,3,22.82 +2026-06-04 09:06:17,Alfredo Martinez,3,1,135.32 +2026-06-04 09:07:13,Alfredo Martinez,1,7,54.56 +2026-06-04 09:10:03,Jose Miguel Albarado,7,1,115.3 +2026-06-04 09:10:48,Jose Miguel Albarado,1,7,40.69 +2026-06-04 09:11:19,Emanuel Flores,7,5,2.75 +2026-06-04 09:11:22,Emanuel Flores,5,8,2.33 +2026-06-04 09:11:31,Emanuel Flores,8,3,8.72 +2026-06-04 09:12:51,Emanuel Flores,3,8,7.27 +2026-06-04 09:12:59,Emanuel Flores,8,7,4.79 +2026-06-04 09:14:00,Josue Muñoz,7,5,1.94 +2026-06-04 09:14:10,Rafael,7,3,15.65 +2026-06-04 09:15:03,Emanuel Flores,7,5,4.88 +2026-06-04 09:15:19,Rafael,3,6,57.27 +2026-06-04 09:15:45,Emanuel Flores,5,8,40.4 +2026-06-04 09:15:47,Emanuel Flores,8,5,2.15 +2026-06-04 09:15:58,Emanuel Flores,5,7,3.72 diff --git a/cache_nombres/registro_movimientos2.csv b/cache_nombres/registro_movimientos2.csv new file mode 100644 index 0000000..02bec9a --- /dev/null +++ b/cache_nombres/registro_movimientos2.csv @@ -0,0 +1,165 @@ +fecha,nombre,cam_origen,cam_destino,duracion_seg,certeza_reid,certeza_facial,intentos +2026-06-04 11:16:28,Rodrigo Cahuantzi C,3,8,7.42,98.6,34.4,2 +2026-06-04 11:16:33,Rodrigo Cahuantzi C,8,5,0.97,91.6,34.4,2 +2026-06-04 11:16:38,Rodrigo Cahuantzi C,5,7,3.82,89.7,34.4,2 +2026-06-04 11:17:03,Rodrigo Cahuantzi C,7,1,20.09,96.4,34.4,2 +2026-06-04 11:17:48,Rodrigo Cahuantzi C,1,7,29.54,100.0,34.4,2 +2026-06-04 11:17:56,Rodrigo Cahuantzi C,7,5,3.22,89.2,34.4,2 +2026-06-04 11:17:59,Rodrigo Cahuantzi C,5,8,2.19,94.4,34.4,2 +2026-06-04 11:18:17,Rodrigo Cahuantzi C,8,3,7.44,99.1,34.4,2 +2026-06-04 11:18:36,Rodrigo Cahuantzi C,3,6,11.36,97.8,34.4,2 +2026-06-04 11:22:27,Rodrigo Cahuantzi C,6,3,7.37,90.3,34.4,2 +2026-06-05 09:00:48,Alfredo Martinez,7,5,2.32,77.8,31.6,3 +2026-06-05 09:00:51,Alfredo Martinez,5,8,1.87,94.4,31.6,3 +2026-06-05 09:01:02,Alfredo Martinez,8,3,10.26,81.2,31.6,3 +2026-06-05 09:06:58,Alfredo Martinez,3,7,352.49,82.9,31.6,3 +2026-06-05 09:07:31,Alfredo Martinez,7,5,33.13,82.9,31.6,3 +2026-06-05 10:03:00,Emanuel Flores,7,5,4.23,94.8,57.9,3 +2026-06-05 10:03:03,Emanuel Flores,5,8,2.13,92.8,57.9,3 +2026-06-05 10:42:55,Oscar Atriano Ponce,8,6,9.68,100.0,0.0,0 +2026-06-05 11:31:06,lore,7,5,9.79,97.7,49.0,3 +2026-06-05 11:31:38,lore,5,6,32.14,94.8,49.0,3 +2026-06-05 11:33:10,Rodrigo Cahuantzi C,7,5,3.08,82.9,58.8,3 +2026-06-05 11:33:17,Rodrigo Cahuantzi C,5,3,6.33,94.4,58.8,3 +2026-06-05 11:35:05,Rodrigo Cahuantzi C,3,8,5.13,95.0,58.8,3 +2026-06-08 08:55:32,Alfredo Martinez,7,3,17.19,82.7,48.5,3 +2026-06-08 08:55:56,Alfredo Martinez,3,1,18.33,79.3,48.5,3 +2026-06-08 08:56:25,Alfredo Martinez,1,7,24.11,81.0,48.5,3 +2026-06-08 08:56:45,Miguel Angel Sanchez Papalotzi,7,3,14.2,81.1,37.2,2 +2026-06-08 09:49:36,Emanuel Flores,7,5,2.84,78.4,31.5,2 +2026-06-08 11:14:36,Jose,7,8,6.92,80.0,0.0,0 +2026-06-08 12:06:08,Jose,7,1,18.81,81.8,0.0,0 +2026-06-08 12:07:24,Jose,1,7,73.27,78.6,0.0,0 +2026-06-09 09:00:44,Miguel Angel Sanchez Papalotzi,3,1,138.1,81.7,30.0,3 +2026-06-16 11:31:42,Emanuel Flores,7,6,194.69,79.9,0.0,0 +2026-06-17 11:59:32,Brayan Mendoza,7,5,2.16,75.8,44.9,0 +2026-06-17 11:59:34,Brayan Mendoza,5,8,0.91,77.6,44.9,0 +2026-06-17 12:02:08,Brayan Mendoza,8,6,51.67,87.1,44.9,0 +2026-06-17 12:04:26,Brayan Mendoza,6,7,137.68,79.7,44.9,0 +2026-06-17 12:27:07,Rodrigo Cahuantzi C,3,8,5.84,67.5 +2026-06-17 12:27:12,Rodrigo Cahuantzi C,8,5,1.3,72.0 +2026-06-17 12:27:16,Rodrigo Cahuantzi C,5,7,3.25,77.6 +2026-06-17 12:27:47,Rodrigo Cahuantzi C,7,8,8.33,74.8 +2026-06-17 12:27:58,Rodrigo Cahuantzi C,8,3,4.83,75.3 +2026-06-17 12:28:18,Rodrigo Cahuantzi C,3,6,8.36,73.1 +2026-06-17 12:30:15,Rodrigo Cahuantzi C,6,1,99.33,76.5 +2026-06-17 12:30:41,Rodrigo Cahuantzi C,1,7,17.91,72.0 +2026-06-18 10:10:24,Desconocido,6,3,7.94,74.4 +2026-06-18 10:10:47,Desconocido,3,6,9.19,60.7 +2026-06-18 10:23:31,Emanuel Flores,7,5,2.9,77.0 +2026-06-18 10:23:35,Emanuel Flores,5,8,2.83,73.2 +2026-06-18 10:23:47,Emanuel Flores,8,3,7.25,73.0 +2026-06-18 10:25:12,Emanuel Flores,3,8,9.52,76.3 +2026-06-18 10:25:17,Emanuel Flores,8,5,2.09,75.1 +2026-06-18 10:25:24,Emanuel Flores,5,7,5.16,76.0 +2026-06-18 10:29:29,Emanuel Flores,7,1,1.59,66.6 +2026-06-18 10:29:31,Emanuel Flores,1,5,1.19,63.5 +2026-06-18 10:29:33,Emanuel Flores,5,8,0.99,67.9 +2026-06-18 10:29:43,Emanuel Flores,8,3,7.62,69.8 +2026-06-18 10:30:08,Emanuel Flores,3,7,15.88,77.8 +2026-06-18 10:30:16,Emanuel Flores,7,5,4.75,67.8 +2026-06-18 10:30:18,Emanuel Flores,5,8,1.55,65.0 +2026-06-18 11:41:28,Desconocido,6,6,0.0,53.0,15.1 +2026-06-18 11:43:44,Desconocido,6,3,126.17,68.3,15.1 +2026-06-18 11:43:57,Desconocido,3,6,10.54,71.3,15.1 +2026-06-18 11:44:48,Desconocido,6,3,5.51,71.0,15.1 +2026-06-18 11:46:00,Desconocido,3,8,71.26,61.8,15.1 +2026-06-18 11:46:02,Desconocido,5,5,0.0,61.8,15.1 +2026-06-18 11:46:44,Desconocido,8,3,42.77,79.2,15.1 +2026-06-18 11:46:56,Desconocido,3,6,10.99,77.3,15.1 +2026-06-18 12:02:36,Desconocido,3,3,0.0,0.0,0.0 +2026-06-18 12:02:50,Desconocido,3,6,15.34,0.0,0.0 +2026-06-18 12:02:57,Desconocido,6,3,4.24,0.0,0.0 +2026-06-18 12:03:13,Desconocido,3,8,7.41,0.0,0.0 +2026-06-18 12:03:15,Desconocido,8,5,1.52,0.0,0.0 +2026-06-18 12:03:59,Desconocido,5,3,43.61,0.0,0.0 +2026-06-18 12:04:10,Desconocido,3,8,5.57,0.0,0.0 +2026-06-18 12:04:13,Desconocido,8,5,2.45,0.0,0.0 +2026-06-18 12:11:42,Desconocido,5,5,0.0,0.0,0.0 +2026-06-18 12:11:43,Desconocido,3,8,459.12,0.0,0.0 +2026-06-18 12:12:11,Desconocido,8,3,27.33,0.0,0.0 +2026-06-18 12:12:22,Desconocido,3,6,7.16,0.0,0.0 +2026-06-18 12:12:33,Desconocido,6,5,5.55,0.0,0.0 +2026-06-18 12:12:36,Desconocido,5,8,1.62,0.0,0.0 +2026-06-18 12:12:45,Desconocido,8,3,6.41,0.0,0.0 +2026-06-18 12:25:04,Desconocido,6,6,0.0,0.0,0.0 +2026-06-18 12:25:06,Rodrigo Cahuantzi C,6,6,0.0,0.0,55.5 +2026-06-18 12:25:20,Desconocido,3,3,0.0,0.0,0.0 +2026-06-18 12:27:11,Desconocido,8,8,0.0,0.0,0.0 +2026-06-18 12:27:16,Rodrigo Cahuantzi C,8,8,0.0,0.0,55.5 +2026-06-18 12:27:18,Rodrigo Cahuantzi C,5,5,0.0,0.0,55.5 +2026-06-18 12:27:49,Desconocido,6,6,0.0,57.3,0.0 +2026-06-18 12:27:57,Rodrigo Cahuantzi C,6,6,0.0,0.0,55.5 +2026-06-19 08:53:13,Desconocido,8,8,0.0,72.3,0.0 +2026-06-19 08:53:56,Desconocido,3,3,0.0,72.3,0.0 +2026-06-19 08:57:38,Desconocido,8,1,210.9,69.9,0.0 +2026-06-19 09:12:07,Desconocido,1,8,867.76,66.8,0.0 +2026-06-19 09:12:11,Desconocido,8,5,0.69,63.8,0.0 +2026-06-19 09:14:32,Desconocido,7,7,0.0,0.0,0.0 +2026-06-19 09:14:35,Oscar Atriano Ponce,7,7,0.0,0.0,54.6 +2026-06-19 09:14:38,Oscar Atriano Ponce,7,5,5.68,66.3,54.6 +2026-06-19 09:14:39,Oscar Atriano Ponce,5,8,0.44,77.8,54.6 +2026-06-19 09:16:08,Oscar Atriano Ponce,8,1,86.84,66.0,54.6 +2026-06-19 09:16:10,Desconocido,1,1,0.0,0.0,0.0 +2026-06-19 09:23:09,Desconocido,7,7,0.0,0.0,0.0 +2026-06-19 09:24:13,Desconocido,1,1,0.0,0.0,0.0 +2026-06-19 09:24:44,Desconocido,7,7,0.0,0.0,0.0 +2026-06-19 09:25:11,Desconocido,1,1,0.0,67.0,0.0 +2026-06-19 09:25:27,Desconocido,3,3,0.0,67.0,0.0 +2026-06-19 09:33:39,Desconocido,7,8,477.38,66.1,0.0 +2026-06-19 09:41:36,Desconocido,7,7,0.0,66.2,0.0 +2026-06-19 09:41:39,Desconocido,7,5,4.72,66.5,0.0 +2026-06-19 09:41:52,Desconocido,5,1,12.0,66.5,0.0 +2026-06-19 09:42:24,Desconocido,1,7,28.57,79.0,0.0 +2026-06-19 09:42:35,Desconocido,7,8,5.11,64.8,0.0 +2026-06-19 09:42:45,Desconocido,8,3,7.35,68.7,0.0 +2026-06-19 12:09:16,Desconocido,8,8,0.0,78.6,0.0 +2026-06-19 12:09:20,Desconocido,5,5,0.0,64.6,0.0 +2026-06-19 12:16:06,Desconocido,3,3,0.0,64.6,0.0 +2026-06-19 12:25:36,Desconocido,8,5,125.68,64.6,0.0 +2026-06-19 12:25:48,Oscar Atriano Ponce,8,8,0.0,0.0,36.9 +2026-06-19 12:26:37,Desconocido,5,8,28.28,64.6,0.0 +2026-06-19 12:26:58,Desconocido,5,5,0.0,64.6,0.0 +2026-06-19 12:27:27,Cristian Hernandez Suarez,8,8,0.0,76.6,25.2 +2026-06-19 12:27:36,Abigail,8,8,0.0,76.6,71.3 +2026-06-19 12:27:55,Desconocido,5,5,0.0,64.6,0.0 +2026-06-19 12:28:06,lore,5,5,0.0,0.0,25.9 +2026-06-19 12:28:12,Judith,5,5,0.0,0.0,46.6 +2026-06-19 12:29:05,Rubisela Barrientos,8,8,0.0,74.4,25.7 +2026-06-19 12:29:58,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:30:57,Rubisela Barrientos,8,5,84.83,76.5,0.0 +2026-06-19 12:30:59,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:31:31,Martha,8,8,0.0,73.8,75.8 +2026-06-19 12:33:05,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:33:35,Raul Sanchez,8,8,0.0,0.0,26.4 +2026-06-19 12:33:37,Yoseimy,8,8,0.0,0.0,74.0 +2026-06-19 12:34:20,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:34:24,Desconocido,5,5,0.0,0.0,0.0 +2026-06-19 12:34:30,Oscar,5,5,0.0,0.0,63.4 +2026-06-19 12:34:32,Mileidy Perez,8,8,0.0,0.0,26.2 +2026-06-19 12:35:02,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:35:04,Desconocido,5,5,0.0,0.0,0.0 +2026-06-19 12:35:05,Jesus Eduardo,8,8,0.0,0.0,25.2 +2026-06-19 12:35:06,lore,5,5,0.0,0.0,29.1 +2026-06-19 12:35:19,Leticia,5,5,0.0,0.0,64.9 +2026-06-19 12:39:51,Desconocido,5,5,0.0,0.0,0.0 +2026-06-19 12:40:19,Fernando,8,8,0.0,84.7,79.5 +2026-06-19 12:40:53,Desconocido,8,8,0.0,0.0,0.0 +2026-06-19 12:41:11,Sergio,8,8,0.0,0.0,72.9 +2026-06-22 09:07:35,Desconocido,1,1,0.0,0.0,0.0 +2026-06-22 09:07:45,Desconocido,1,8,13.27,63.6,0.0 +2026-06-22 09:07:52,Desconocido,8,7,5.27,76.8,0.0 +2026-06-22 09:08:19,Desconocido,7,8,27.99,81.9,0.0 +2026-06-22 09:08:34,Desconocido,8,5,13.3,76.4,0.0 +2026-06-22 09:21:16,Desconocido,5,3,762.65,71.5,0.0 +2026-06-22 09:21:32,Desconocido,3,6,9.64,71.5,0.0 +2026-06-22 09:21:38,Desconocido,5,7,784.51,79.0,0.0 +2026-06-22 09:38:56,Rodrigo Cahuantzi C,7,7,0.0,76.8,63.1,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C.jpg +2026-06-22 09:40:23,Alfredo Martinez,7,7,0.0,71.7,29.2,cache_nombres/auditoria_caras/102.jpg +2026-06-22 09:40:24,Rodrigo Cahuantzi C,7,7,0.0,71.7,61.8,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C.jpg +2026-06-22 09:40:30,Alfredo Martinez,7,7,0.0,71.7,25.2,cache_nombres/auditoria_caras/102.jpg +2026-06-22 09:40:31,Rodrigo Cahuantzi C,7,7,0.0,71.7,41.7,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C.jpg +2026-06-22 09:42:18,Oscar,7,7,0.0,71.7,28.9,cache_nombres/auditoria_caras/102.jpg +2026-06-22 09:42:19,Rodrigo Cahuantzi C,7,7,0.0,71.7,46.6,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C.jpg +2026-06-22 09:42:41,Yuriel,7,7,0.0,71.7,25.3,cache_nombres/auditoria_caras/102.jpg +2026-06-22 09:42:42,Rodrigo Cahuantzi C,7,7,0.0,71.7,58.5,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C.jpg diff --git a/cache_nombres/registro_movimientos3.csv b/cache_nombres/registro_movimientos3.csv new file mode 100644 index 0000000..95135f7 --- /dev/null +++ b/cache_nombres/registro_movimientos3.csv @@ -0,0 +1,80 @@ +fecha,nombre,cam_origen,cam_destino,duracion_seg,certeza_reid,certeza_facial,imagen_rostro +2026-06-22 11:08:53,ID 101,6,3,615.83,68.4,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:09:13,ID 101,3,8,5.97,76.3,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:09:16,ID 102,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/102_cam5.jpg +2026-06-22 11:10:29,ID 101,8,7,4.4,69.4,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-22 11:10:55,ID 100,6,3,130.57,69.2,0.0,cache_nombres/auditoria_caras/100_cam6.jpg +2026-06-22 11:10:55,Rodrigo Cahuantzi C,3,3,0.0,69.2,48.0,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam3.jpg +2026-06-22 11:11:09,ID 101,7,8,39.72,77.9,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-22 11:11:19,ID 102,5,7,3.58,76.2,0.0,cache_nombres/auditoria_caras/102_cam5.jpg +2026-06-22 11:11:22,ID 103,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/103_cam7.jpg +2026-06-22 11:11:26,ID 101,8,5,13.47,61.0,0.0,cache_nombres/auditoria_caras/101_cam8.jpg +2026-06-22 11:11:28,Rodrigo Cahuantzi C,3,8,6.25,71.0,48.0,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam3.jpg +2026-06-22 11:11:29,ID 103,7,8,6.85,59.9,0.0,cache_nombres/auditoria_caras/103_cam8.jpg +2026-06-22 11:11:39,ID 101,5,3,12.37,75.5,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:12:03,ID 101,3,6,10.84,76.8,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:13:24,ID 103,8,6,112.03,68.7,0.0,cache_nombres/auditoria_caras/103_cam6.jpg +2026-06-22 11:13:50,ID 103,6,3,11.16,66.8,0.0,cache_nombres/auditoria_caras/103_cam3.jpg +2026-06-22 11:14:51,Rodrigo Cahuantzi C,8,6,162.21,75.5,48.0,cache_nombres/auditoria_caras/Rodrigo Cahuantzi C_cam6.jpg +2026-06-22 11:15:38,ID 104,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/104_cam3.jpg +2026-06-22 11:15:48,ID 103,3,6,8.25,61.7,0.0,cache_nombres/auditoria_caras/103_cam3.jpg +2026-06-22 11:15:58,ID 104,3,6,16.63,60.2,0.0,cache_nombres/auditoria_caras/104_cam6.jpg +2026-06-22 11:23:55,ID 101,6,3,8.07,77.7,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:24:09,ID 101,3,8,7.25,71.8,0.0,cache_nombres/auditoria_caras/101_cam8.jpg +2026-06-22 11:24:13,ID 102,7,5,773.66,71.5,0.0,cache_nombres/auditoria_caras/102_cam5.jpg +2026-06-22 11:24:17,ID 103,6,7,499.65,68.6,0.0,cache_nombres/auditoria_caras/103_cam6.jpg +2026-06-22 11:24:40,ID 101,8,1,31.09,73.9,0.0,cache_nombres/auditoria_caras/101_cam8.jpg +2026-06-22 11:25:42,ID 103,7,5,4.89,67.9,0.0,cache_nombres/auditoria_caras/103_cam5.jpg +2026-06-22 11:25:45,ID 101,1,8,30.11,77.5,0.0,cache_nombres/auditoria_caras/101_cam8.jpg +2026-06-22 11:25:55,ID 101,8,3,9.2,78.9,0.0,cache_nombres/auditoria_caras/101_cam3.jpg +2026-06-22 11:53:48,ID 105,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/105_cam6.jpg +2026-06-22 11:54:01,ID 105,6,3,6.68,63.8,0.0,cache_nombres/auditoria_caras/105_cam3.jpg +2026-06-22 11:56:13,ID 105,3,6,130.58,78.4,0.0,cache_nombres/auditoria_caras/105_cam3.jpg +2026-06-22 11:57:02,ID 105,6,3,45.21,66.5,0.0,cache_nombres/auditoria_caras/105_cam3.jpg +2026-06-22 12:00:30,ID 106,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/106_cam6.jpg +2026-06-22 12:02:17,ID 107,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/107_cam5.jpg +2026-06-22 12:03:06,ID 105,8,3,50.26,67.3,0.0,cache_nombres/auditoria_caras/105_cam3.jpg +2026-06-22 12:05:52,ID 107,8,3,8.96,64.3,0.0,cache_nombres/auditoria_caras/107_cam3.jpg +2026-06-22 12:09:22,ID 106,3,6,141.48,90.7,0.0,cache_nombres/auditoria_caras/106_cam6.jpg +2026-06-22 12:10:17,ID 105,6,3,152.98,70.1,0.0,cache_nombres/auditoria_caras/105_cam3.jpg +2026-06-22 12:30:12,ID 108,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/108_cam3.jpg +2026-06-22 12:31:27,ID 108,3,6,55.18,70.5,0.0,cache_nombres/auditoria_caras/108_cam6.jpg +2026-06-24 08:42:18,ID 100,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/100_cam6.jpg +2026-06-24 08:42:56,ID 100,6,3,27.94,64.4,0.0,cache_nombres/auditoria_caras/100_cam6.jpg +2026-06-24 09:08:32,ID 101,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/101_cam1.jpg +2026-06-24 09:09:04,ID 101,1,7,23.35,73.9,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-24 09:09:08,Jose Miguel Albarado,7,7,0.0,73.9,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-24 09:09:13,ID 102,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/102_cam8.jpg +2026-06-24 09:10:02,ID 101,7,1,49.93,73.9,0.0,cache_nombres/auditoria_caras/101_cam1.jpg +2026-06-24 09:10:30,ID 101,1,7,21.86,72.4,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-24 09:10:34,Cristian Lopez Garcia,7,7,0.0,72.4,0.0,cache_nombres/auditoria_caras/101_cam7.jpg +2026-06-24 09:56:41,ID 103,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/103_cam3_2026-06-24.jpg +2026-06-24 09:58:06,ID 104,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/104_cam3_2026-06-24.jpg +2026-06-24 09:58:29,ID 104,3,6,16.11,70.6,0.0,cache_nombres/auditoria_caras/104_cam6_2026-06-24.jpg +2026-06-24 10:08:59,ID 105,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/105_cam3_2026-06-24.jpg +2026-06-24 10:09:35,ID 106,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/106_cam8_2026-06-24.jpg +2026-06-24 10:09:45,ID 107,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/107_cam3_2026-06-24.jpg +2026-06-24 10:10:08,ID 108,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/108_cam6_2026-06-24.jpg +2026-06-24 10:10:52,ID 109,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/109_cam3_2026-06-24.jpg +2026-06-24 10:11:11,ID 110,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/110_cam5_2026-06-24.jpg +2026-06-24 10:11:14,ID 111,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/111_cam7_2026-06-24.jpg +2026-06-24 10:15:31,ID 106,8,6,262.93,75.4,0.0,cache_nombres/auditoria_caras/106_cam8_2026-06-24.jpg +2026-06-24 10:15:58,ID 105,3,6,333.35,72.8,0.0,cache_nombres/auditoria_caras/105_cam3_2026-06-24.jpg +2026-06-24 10:16:10,ID 104,6,5,353.14,78.6,0.0,cache_nombres/auditoria_caras/104_cam6_2026-06-24.jpg +2026-06-24 10:16:10,ID 110,5,8,299.77,76.0,0.0,cache_nombres/auditoria_caras/110_cam8_2026-06-24.jpg +2026-06-24 10:17:02,ID 112,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/112_cam1_2026-06-24.jpg +2026-06-24 10:17:02,ID 113,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/113_cam1_2026-06-24.jpg +2026-06-24 10:17:22,ID 114,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/114_cam7_2026-06-24.jpg +2026-06-24 10:17:24,ID 115,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/115_cam6_2026-06-24.jpg +2026-06-24 10:17:39,Oscar Atriano Ponce,7,7,0.0,66.5,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam7_2026-06-24.jpg +2026-06-24 10:17:48,ID 116,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/116_cam8_2026-06-24.jpg +2026-06-24 10:17:58,ID 117,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/117_cam3_2026-06-24.jpg +2026-06-24 10:18:00,ID 118,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/118_cam3_2026-06-24.jpg +2026-06-24 10:18:13,ID 104,5,8,20.37,71.7,0.0,cache_nombres/auditoria_caras/104_cam5_2026-06-24.jpg +2026-06-24 10:18:23,ID 107,8,3,31.92,72.8,0.0,cache_nombres/auditoria_caras/107_cam3_2026-06-24.jpg +2026-06-24 10:19:27,ID 119,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/119_cam5_2026-06-24.jpg +2026-06-24 10:20:01,ID 120,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/120_cam6_2026-06-24.jpg +2026-06-24 10:20:09,ID 116,8,3,138.9,67.2,0.0,cache_nombres/auditoria_caras/116_cam3_2026-06-24.jpg +2026-06-24 10:20:24,ID 121,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/121_cam5_2026-06-24.jpg +2026-06-24 10:20:27,ID 116,3,7,10.67,71.3,0.0,cache_nombres/auditoria_caras/116_cam7_2026-06-24.jpg +2026-06-24 10:21:53,ID 116,7,8,84.66,70.1,0.0,cache_nombres/auditoria_caras/116_cam8_2026-06-24.jpg diff --git a/cache_nombres/registro_movimientos4.csv b/cache_nombres/registro_movimientos4.csv new file mode 100644 index 0000000..047191f --- /dev/null +++ b/cache_nombres/registro_movimientos4.csv @@ -0,0 +1,211 @@ +fecha,nombre,cam_origen,cam_destino,duracion_seg,certeza_reid,certeza_facial,imagen_rostro +2026-06-24 10:29:31,ID 100,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/100_cam1_2026-06-24.jpg +2026-06-24 10:30:35,ID 101,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/101_cam7_2026-06-24.jpg +2026-06-24 11:04:51,ID 102,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/102_cam1_2026-06-24.jpg +2026-06-24 11:11:09,ID 103,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/103_cam7_2026-06-24.jpg +2026-06-24 11:12:12,ID 103,7,1,31.93,69.1,0.0,cache_nombres/auditoria_caras/103_cam1_2026-06-24.jpg +2026-06-24 11:12:36,ID 104,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/104_cam3_2026-06-24.jpg +2026-06-24 11:12:50,ID 105,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/105_cam6_2026-06-24.jpg +2026-06-24 11:12:53,ID 106,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/106_cam8_2026-06-24.jpg +2026-06-24 11:12:56,ID 104,3,5,12.35,66.5,0.0,cache_nombres/auditoria_caras/104_cam5_2026-06-24.jpg +2026-06-24 11:13:00,ID 107,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/107_cam7_2026-06-24.jpg +2026-06-24 11:13:03,ID 106,8,3,8.73,61.1,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-24.jpg +2026-06-24 11:13:13,ID 104,5,8,17.24,60.4,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:13:27,ID 104,8,7,10.71,77.0,0.0,cache_nombres/auditoria_caras/104_cam7_2026-06-24.jpg +2026-06-24 11:13:37,ID 108,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/108_cam3_2026-06-24.jpg +2026-06-24 11:13:47,ID 104,7,3,19.5,70.6,0.0,cache_nombres/auditoria_caras/104_cam3_2026-06-24.jpg +2026-06-24 11:13:47,ID 108,3,6,6.47,67.0,0.0,cache_nombres/auditoria_caras/108_cam6_2026-06-24.jpg +2026-06-24 11:13:54,ID 109,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/109_cam6_2026-06-24.jpg +2026-06-24 11:13:58,ID 110,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/110_cam1_2026-06-24.jpg +2026-06-24 11:14:19,ID 104,3,6,31.16,69.5,0.0,cache_nombres/auditoria_caras/104_cam6_2026-06-24.jpg +2026-06-24 11:14:39,ID 104,3,8,7.71,79.3,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:15:07,ID 107,7,1,21.94,72.7,0.0,cache_nombres/auditoria_caras/107_cam7_2026-06-24.jpg +2026-06-24 11:15:15,ID 104,8,6,35.37,71.0,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:15:36,ID 104,3,8,6.04,79.2,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:15:40,ID 103,1,5,107.69,63.6,0.0,cache_nombres/auditoria_caras/103_cam5_2026-06-24.jpg +2026-06-24 11:16:01,ID 104,8,1,21.55,73.5,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:16:19,ID 104,1,5,17.88,65.5,0.0,cache_nombres/auditoria_caras/104_cam1_2026-06-24.jpg +2026-06-24 11:16:21,ID 103,5,8,40.09,74.9,0.0,cache_nombres/auditoria_caras/103_cam5_2026-06-24.jpg +2026-06-24 11:16:24,ID 105,6,5,206.1,65.9,0.0,cache_nombres/auditoria_caras/105_cam5_2026-06-24.jpg +2026-06-24 11:16:27,ID 104,7,8,5.57,78.2,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-24.jpg +2026-06-24 11:16:31,ID 108,6,3,156.03,79.8,0.0,cache_nombres/auditoria_caras/108_cam3_2026-06-24.jpg +2026-06-24 11:16:32,ID 107,7,1,17.9,61.2,0.0,cache_nombres/auditoria_caras/107_cam7_2026-06-24.jpg +2026-06-24 11:16:37,ID 110,1,3,83.65,70.9,0.0,cache_nombres/auditoria_caras/110_cam3_2026-06-24.jpg +2026-06-24 11:16:51,ID 104,3,6,15.63,81.4,0.0,cache_nombres/auditoria_caras/104_cam6_2026-06-24.jpg +2026-06-24 11:16:53,ID 105,5,6,28.81,66.9,0.0,cache_nombres/auditoria_caras/105_cam5_2026-06-24.jpg +2026-06-24 11:17:29,ID 111,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/111_cam6_2026-06-24.jpg +2026-06-24 11:17:43,ID 104,6,3,48.48,78.7,0.0,cache_nombres/auditoria_caras/104_cam3_2026-06-24.jpg +2026-06-24 11:17:47,Josue Muñoz,3,3,0.0,78.7,0.0,cache_nombres/auditoria_caras/Josue Muñoz_cam3_2026-06-24.jpg +2026-06-24 11:20:29,ID 107,1,3,181.2,69.6,0.0,cache_nombres/auditoria_caras/107_cam3_2026-06-24.jpg +2026-06-24 11:21:20,ID 107,3,7,47.78,80.0,0.0,cache_nombres/auditoria_caras/107_cam7_2026-06-24.jpg +2026-06-24 11:21:23,Omar,7,7,0.0,80.0,0.0,cache_nombres/auditoria_caras/Omar_cam7_2026-06-24.jpg +2026-06-24 11:21:24,ID 108,3,5,285.34,74.9,0.0,cache_nombres/auditoria_caras/108_cam5_2026-06-24.jpg +2026-06-24 11:21:26,ID 106,3,8,497.67,64.4,0.0,cache_nombres/auditoria_caras/106_cam8_2026-06-24.jpg +2026-06-24 11:21:56,Josue Muñoz,3,5,173.95,63.1,0.0,cache_nombres/auditoria_caras/Josue Muñoz_cam3_2026-06-24.jpg +2026-06-24 11:21:57,ID 111,6,8,259.65,68.9,0.0,cache_nombres/auditoria_caras/111_cam6_2026-06-24.jpg +2026-06-24 11:40:20,Josue Muñoz,5,1,790.15,71.9,0.0,cache_nombres/auditoria_caras/104_cam1_2026-06-24.jpg +2026-06-24 11:41:54,ID 104,1,6,71.15,73.3,0.0,cache_nombres/auditoria_caras/104_cam1_2026-06-24.jpg +2026-06-24 11:42:14,ID 112,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/112_cam1_2026-06-24.jpg +2026-06-24 11:44:29,ID 113,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/113_cam6_2026-06-24.jpg +2026-06-24 11:46:50,ID 114,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/114_cam5_2026-06-24.jpg +2026-06-24 11:46:53,ID 113,8,7,5.19,77.7,0.0,cache_nombres/auditoria_caras/113_cam8_2026-06-24.jpg +2026-06-24 11:47:18,ID 115,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/115_cam8_2026-06-24.jpg +2026-06-24 11:47:25,ID 115,8,3,6.6,63.8,0.0,cache_nombres/auditoria_caras/115_cam3_2026-06-24.jpg +2026-06-24 11:47:36,ID 113,7,6,21.71,78.3,0.0,cache_nombres/auditoria_caras/113_cam7_2026-06-24.jpg +2026-06-24 11:48:45,ID 116,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/116_cam3_2026-06-24.jpg +2026-06-24 11:48:55,ID 117,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/117_cam8_2026-06-24.jpg +2026-06-24 11:48:58,ID 116,3,5,9.22,72.1,0.0,cache_nombres/auditoria_caras/116_cam5_2026-06-24.jpg +2026-06-24 11:49:43,ID 118,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/118_cam7_2026-06-24.jpg +2026-06-24 11:49:50,ID 116,5,8,51.93,72.2,0.0,cache_nombres/auditoria_caras/116_cam8_2026-06-24.jpg +2026-06-24 11:50:15,ID 114,5,6,10.54,70.7,0.0,cache_nombres/auditoria_caras/114_cam6_2026-06-24.jpg +2026-06-24 11:50:30,ID 116,8,3,40.17,67.1,0.0,cache_nombres/auditoria_caras/116_cam3_2026-06-24.jpg +2026-06-24 11:54:14,ID 119,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/119_cam3_2026-06-24.jpg +2026-06-24 11:54:33,ID 119,3,6,5.46,73.2,0.0,cache_nombres/auditoria_caras/119_cam6_2026-06-24.jpg +2026-06-24 11:56:17,ID 120,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/120_cam6_2026-06-24.jpg +2026-06-24 11:56:46,ID 119,6,3,128.2,88.5,0.0,cache_nombres/auditoria_caras/119_cam6_2026-06-24.jpg +2026-06-24 11:57:24,ID 121,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/121_cam3_2026-06-24.jpg +2026-06-24 11:57:36,ID 122,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/122_cam8_2026-06-24.jpg +2026-06-24 11:57:40,ID 121,3,5,11.82,64.4,0.0,cache_nombres/auditoria_caras/121_cam5_2026-06-24.jpg +2026-06-24 11:57:45,ID 121,5,7,4.27,69.5,0.0,cache_nombres/auditoria_caras/121_cam7_2026-06-24.jpg +2026-06-24 11:58:17,Fernando,7,7,0.0,73.2,0.0,cache_nombres/auditoria_caras/Fernando_cam7_2026-06-24.jpg +2026-06-24 11:58:49,ID 104,6,1,232.03,80.2,0.0,cache_nombres/auditoria_caras/104_cam1_2026-06-24.jpg +2026-06-24 11:59:00,ID 116,3,6,275.66,65.2,0.0,cache_nombres/auditoria_caras/116_cam3_2026-06-24.jpg +2026-06-24 12:21:29,ID 123,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/123_cam1_2026-06-24.jpg +2026-06-24 12:21:53,ID 123,1,7,20.74,70.9,0.0,cache_nombres/auditoria_caras/123_cam1_2026-06-24.jpg +2026-06-24 12:21:58,ID 124,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/124_cam5_2026-06-24.jpg +2026-06-24 12:22:01,ID 125,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/125_cam8_2026-06-24.jpg +2026-06-24 12:22:11,ID 123,7,3,16.49,77.3,0.0,cache_nombres/auditoria_caras/123_cam3_2026-06-24.jpg +2026-06-24 12:22:28,ID 126,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/126_cam6_2026-06-24.jpg +2026-06-24 12:23:45,ID 127,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/127_cam7_2026-06-24.jpg +2026-06-24 12:24:09,ID 128,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/128_cam1_2026-06-24.jpg +2026-06-24 12:24:48,ID 125,8,1,67.55,72.1,0.0,cache_nombres/auditoria_caras/125_cam8_2026-06-24.jpg +2026-06-24 12:25:14,ID 123,3,7,103.27,82.5,0.0,cache_nombres/auditoria_caras/123_cam7_2026-06-24.jpg +2026-06-24 12:25:16,aridai montiel zistecatl,7,7,0.0,82.5,0.0,cache_nombres/auditoria_caras/aridai montiel zistecatl_cam7_2026-06-24.jpg +2026-06-24 12:25:21,ID 125,1,8,28.61,84.1,0.0,cache_nombres/auditoria_caras/125_cam8_2026-06-24.jpg +2026-06-24 12:25:31,aridai montiel zistecatl,7,3,16.22,84.7,0.0,cache_nombres/auditoria_caras/aridai montiel zistecatl_cam7_2026-06-24.jpg +2026-06-24 12:26:13,ID 125,8,3,48.27,67.7,0.0,cache_nombres/auditoria_caras/125_cam8_2026-06-24.jpg +2026-06-24 12:26:25,ID 129,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/129_cam6_2026-06-24.jpg +2026-06-24 12:26:40,ID 127,7,3,174.04,78.1,0.0,cache_nombres/auditoria_caras/127_cam3_2026-06-24.jpg +2026-06-24 12:33:45,aridai montiel zistecatl,6,3,472.58,70.3,0.0,cache_nombres/auditoria_caras/123_cam3_2026-06-24.jpg +2026-06-25 08:48:25,ID 100,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/100_cam6_2026-06-25.jpg +2026-06-25 08:55:34,ID 101,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/101_cam1_2026-06-25.jpg +2026-06-25 08:56:09,ID 102,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 08:56:15,ID 103,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/103_cam5_2026-06-25.jpg +2026-06-25 08:56:17,ID 104,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/104_cam8_2026-06-25.jpg +2026-06-25 08:59:25,Alfredo Martinez,7,7,0.0,74.0,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 08:59:31,ID 105,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/105_cam8_2026-06-25.jpg +2026-06-25 08:59:42,ID 101,1,5,31.2,71.5,0.0,cache_nombres/auditoria_caras/101_cam1_2026-06-25.jpg +2026-06-25 08:59:42,ID 106,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-25.jpg +2026-06-25 09:00:09,ID 107,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/107_cam6_2026-06-25.jpg +2026-06-25 09:03:42,ID 101,5,1,239.48,79.2,0.0,cache_nombres/auditoria_caras/101_cam1_2026-06-25.jpg +2026-06-25 09:03:47,ID 108,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/108_cam1_2026-06-25.jpg +2026-06-25 09:04:24,Josue Muñoz,7,7,0.0,78.8,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 09:04:27,Josue Muñoz,7,5,4.82,69.6,0.0,cache_nombres/auditoria_caras/Josue Muñoz_cam7_2026-06-25.jpg +2026-06-25 09:04:30,ID 106,3,8,271.8,79.8,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-25.jpg +2026-06-25 09:04:31,ID 107,6,3,257.54,73.7,0.0,cache_nombres/auditoria_caras/107_cam6_2026-06-25.jpg +2026-06-25 09:04:51,ID 104,3,6,495.18,70.7,0.0,cache_nombres/auditoria_caras/104_cam3_2026-06-25.jpg +2026-06-25 09:04:52,ID 109,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/109_cam6_2026-06-25.jpg +2026-06-25 09:05:03,ID 106,8,3,32.89,78.9,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-25.jpg +2026-06-25 09:05:16,ID 107,3,8,32.49,71.6,0.0,cache_nombres/auditoria_caras/107_cam8_2026-06-25.jpg +2026-06-25 09:05:26,ID 110,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/110_cam7_2026-06-25.jpg +2026-06-25 09:05:35,ID 106,3,8,25.82,85.8,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-25.jpg +2026-06-25 09:05:45,Josue Muñoz,5,1,12.97,81.4,0.0,cache_nombres/auditoria_caras/Josue Muñoz_cam5_2026-06-25.jpg +2026-06-25 09:06:15,ID 111,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/111_cam6_2026-06-25.jpg +2026-06-25 09:06:48,Josue Muñoz,1,7,59.26,81.5,0.0,cache_nombres/auditoria_caras/Josue Muñoz_cam7_2026-06-25.jpg +2026-06-25 09:06:56,ID 112,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/112_cam8_2026-06-25.jpg +2026-06-25 09:07:05,ID 112,8,3,5.79,73.7,0.0,cache_nombres/auditoria_caras/112_cam3_2026-06-25.jpg +2026-06-25 09:07:39,ID 105,8,3,198.23,72.7,0.0,cache_nombres/auditoria_caras/105_cam3_2026-06-25.jpg +2026-06-25 09:07:47,Fernando,8,8,0.0,78.2,0.0,cache_nombres/auditoria_caras/Fernando_cam8_2026-06-25.jpg +2026-06-25 09:08:46,ID 113,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/113_cam7_2026-06-25.jpg +2026-06-25 09:09:23,ID 114,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/114_cam6_2026-06-25.jpg +2026-06-25 09:09:36,ID 115,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/115_cam7_2026-06-25.jpg +2026-06-25 09:09:45,ID 114,6,8,16.43,79.4,0.0,cache_nombres/auditoria_caras/114_cam8_2026-06-25.jpg +2026-06-25 09:09:52,ID 114,8,7,6.43,77.7,0.0,cache_nombres/auditoria_caras/114_cam7_2026-06-25.jpg +2026-06-25 09:09:59,Oscar,7,7,0.0,77.7,0.0,cache_nombres/auditoria_caras/Oscar_cam7_2026-06-25.jpg +2026-06-25 09:10:26,Oscar Atriano Ponce,7,7,0.0,77.7,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam7_2026-06-25.jpg +2026-06-25 09:10:47,ID 116,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/116_cam1_2026-06-25.jpg +2026-06-25 09:10:49,Oscar Atriano Ponce,7,7,0.0,77.7,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam7_2026-06-25.jpg +2026-06-25 09:12:14,Fernando,8,3,8.6,76.4,0.0,cache_nombres/auditoria_caras/Fernando_cam3_2026-06-25.jpg +2026-06-25 09:12:25,ID 117,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/117_cam7_2026-06-25.jpg +2026-06-25 09:12:51,ID 118,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/118_cam7_2026-06-25.jpg +2026-06-25 09:13:23,ID 119,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/119_cam7_2026-06-25.jpg +2026-06-25 09:13:38,Emanuel Flores,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg +2026-06-25 09:13:42,ID 108,1,7,120.92,73.7,0.0,cache_nombres/auditoria_caras/108_cam1_2026-06-25.jpg +2026-06-25 09:14:20,ID 120,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/120_cam1_2026-06-25.jpg +2026-06-25 09:14:23,ID 121,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/121_cam3_2026-06-25.jpg +2026-06-25 09:14:45,ID 122,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/122_cam1_2026-06-25.jpg +2026-06-25 09:14:52,ID 123,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/123_cam1_2026-06-25.jpg +2026-06-25 09:15:10,Emanuel Flores,7,1,70.43,71.1,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam7_2026-06-25.jpg +2026-06-25 09:15:29,ID 124,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/124_cam7_2026-06-25.jpg +2026-06-25 09:15:36,ID 125,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/125_cam5_2026-06-25.jpg +2026-06-25 09:15:39,Oscar Atriano Ponce,7,8,51.87,81.0,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam7_2026-06-25.jpg +2026-06-25 09:15:43,ID 102,5,7,105.86,70.9,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 09:15:44,ID 121,3,7,79.92,72.2,0.0,cache_nombres/auditoria_caras/121_cam7_2026-06-25.jpg +2026-06-25 09:15:50,ID 126,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/126_cam3_2026-06-25.jpg +2026-06-25 09:15:58,ID 127,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/127_cam1_2026-06-25.jpg +2026-06-25 09:16:31,Raul Sanchez,7,7,0.0,61.2,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 09:16:34,Oscar Atriano Ponce,8,5,52.44,68.3,0.0,cache_nombres/auditoria_caras/114_cam5_2026-06-25.jpg +2026-06-25 09:16:36,ID 128,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/128_cam1_2026-06-25.jpg +2026-06-25 09:16:37,ID 106,3,8,612.14,77.6,0.0,cache_nombres/auditoria_caras/106_cam3_2026-06-25.jpg +2026-06-25 09:16:51,ID 129,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/129_cam3_2026-06-25.jpg +2026-06-25 09:17:41,ID 130,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/130_cam1_2026-06-25.jpg +2026-06-25 09:18:22,ID 131,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/131_cam1_2026-06-25.jpg +2026-06-25 09:18:26,lore,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/lore_cam1_2026-06-25.jpg +2026-06-25 09:19:02,ID 132,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/132_cam5_2026-06-25.jpg +2026-06-25 09:19:03,Oscar Atriano Ponce,5,8,149.18,72.5,0.0,cache_nombres/auditoria_caras/114_cam8_2026-06-25.jpg +2026-06-25 09:19:13,ID 121,7,3,16.74,81.0,0.0,cache_nombres/auditoria_caras/121_cam3_2026-06-25.jpg +2026-06-25 09:19:16,ID 133,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/133_cam8_2026-06-25.jpg +2026-06-25 09:20:07,Oscar Atriano Ponce,8,6,63.23,81.3,0.0,cache_nombres/auditoria_caras/114_cam6_2026-06-25.jpg +2026-06-25 09:20:55,ID 134,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/134_cam3_2026-06-25.jpg +2026-06-25 09:21:01,Fernando,6,8,93.36,74.2,0.0,cache_nombres/auditoria_caras/Fernando_cam8_2026-06-25.jpg +2026-06-25 09:21:07,ID 135,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/135_cam5_2026-06-25.jpg +2026-06-25 09:21:11,ID 136,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/136_cam7_2026-06-25.jpg +2026-06-25 09:21:37,ID 116,1,5,650.14,73.9,0.0,cache_nombres/auditoria_caras/116_cam5_2026-06-25.jpg +2026-06-25 09:21:41,ID 137,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/137_cam7_2026-06-25.jpg +2026-06-25 09:21:48,ID 138,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/138_cam5_2026-06-25.jpg +2026-06-25 09:21:51,ID 139,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/139_cam8_2026-06-25.jpg +2026-06-25 09:21:55,Oscar,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/Oscar_cam8_2026-06-25.jpg +2026-06-25 09:22:01,ID 140,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/140_cam8_2026-06-25.jpg +2026-06-25 09:22:44,ID 141,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/141_cam7_2026-06-25.jpg +2026-06-25 09:22:51,Oscar Atriano Ponce,8,8,0.0,91.5,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam8_2026-06-25.jpg +2026-06-25 09:22:52,Emanuel Flores,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam8_2026-06-25.jpg +2026-06-25 09:23:28,ID 114,6,7,139.47,73.3,0.0,cache_nombres/auditoria_caras/114_cam7_2026-06-25.jpg +2026-06-25 09:23:32,Oscar Atriano Ponce,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam8_2026-06-25.jpg +2026-06-25 09:23:38,Emanuel Flores,5,5,0.0,65.0,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam5_2026-06-25.jpg +2026-06-25 09:23:43,ID 105,3,8,86.32,70.1,0.0,cache_nombres/auditoria_caras/105_cam8_2026-06-25.jpg +2026-06-25 09:23:52,ID 142,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/142_cam5_2026-06-25.jpg +2026-06-25 09:24:02,ID 143,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/143_cam3_2026-06-25.jpg +2026-06-25 09:24:11,ID 144,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/144_cam8_2026-06-25.jpg +2026-06-25 09:24:45,ID 145,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/145_cam5_2026-06-25.jpg +2026-06-25 09:24:50,Jair Gregorio,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/145_cam5_2026-06-25.jpg +2026-06-25 09:25:08,ID 146,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/146_cam7_2026-06-25.jpg +2026-06-25 09:25:09,Oscar,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/Oscar_cam7_2026-06-25.jpg +2026-06-25 09:25:10,Emanuel Flores,8,8,0.0,65.0,0.0,cache_nombres/auditoria_caras/Emanuel Flores_cam8_2026-06-25.jpg +2026-06-25 09:25:18,Oscar,7,8,9.51,73.5,0.0,cache_nombres/auditoria_caras/Oscar_cam8_2026-06-25.jpg +2026-06-25 09:25:23,ID 147,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/147_cam5_2026-06-25.jpg +2026-06-25 09:25:25,Oscar Atriano Ponce,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/Oscar Atriano Ponce_cam5_2026-06-25.jpg +2026-06-25 09:27:04,ID 148,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/148_cam5_2026-06-25.jpg +2026-06-25 09:27:11,ID 149,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/149_cam7_2026-06-25.jpg +2026-06-25 09:27:12,ID 102,7,5,327.56,72.2,0.0,cache_nombres/auditoria_caras/102_cam7_2026-06-25.jpg +2026-06-25 09:27:32,ID 150,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/150_cam3_2026-06-25.jpg +2026-06-25 09:27:33,ID 151,3,3,0.0,0.0,0.0,cache_nombres/auditoria_caras/151_cam3_2026-06-25.jpg +2026-06-25 09:27:48,ID 152,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/152_cam6_2026-06-25.jpg +2026-06-25 09:28:40,ID 153,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/153_cam6_2026-06-25.jpg +2026-06-25 09:30:01,ID 154,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/154_cam6_2026-06-25.jpg +2026-06-25 09:30:18,ID 129,3,6,803.56,71.1,0.0,cache_nombres/auditoria_caras/129_cam6_2026-06-25.jpg +2026-06-25 09:30:59,ID 155,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/155_cam6_2026-06-25.jpg +2026-06-25 09:31:09,Oscar,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/Oscar_cam6_2026-06-25.jpg +2026-06-25 09:31:50,ID 156,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/156_cam6_2026-06-25.jpg +2026-06-25 09:34:37,ID 157,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/157_cam1_2026-06-25.jpg +2026-06-25 09:34:51,ID 158,1,1,0.0,0.0,0.0,cache_nombres/auditoria_caras/158_cam1_2026-06-25.jpg +2026-06-25 09:35:05,ID 159,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/159_cam7_2026-06-25.jpg +2026-06-25 09:35:18,ID 151,3,7,460.2,77.5,0.0,cache_nombres/auditoria_caras/151_cam7_2026-06-25.jpg +2026-06-25 09:35:49,ID 152,6,3,480.85,75.3,0.0,cache_nombres/auditoria_caras/152_cam3_2026-06-25.jpg +2026-06-25 09:36:06,ID 160,6,6,0.0,0.0,0.0,cache_nombres/auditoria_caras/160_cam6_2026-06-25.jpg +2026-06-25 09:36:42,ID 161,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/161_cam5_2026-06-25.jpg +2026-06-25 10:21:47,ID 162,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/162_cam7_2026-06-25.jpg +2026-06-25 10:21:48,Sergio,7,7,0.0,0.0,0.0,cache_nombres/auditoria_caras/Sergio_cam7_2026-06-25.jpg +2026-06-25 10:21:50,ID 163,5,5,0.0,0.0,0.0,cache_nombres/auditoria_caras/163_cam5_2026-06-25.jpg +2026-06-25 10:21:54,ID 164,8,8,0.0,0.0,0.0,cache_nombres/auditoria_caras/164_cam8_2026-06-25.jpg +2026-06-25 10:22:02,ID 164,8,3,5.33,93.7,0.0,cache_nombres/auditoria_caras/164_cam3_2026-06-25.jpg +2026-06-25 10:22:18,ID 164,3,6,6.93,76.2,0.0,cache_nombres/auditoria_caras/164_cam3_2026-06-25.jpg diff --git a/cache_nombres/registro_saludos.json b/cache_nombres/registro_saludos.json index 813641b..aa0e020 100644 --- a/cache_nombres/registro_saludos.json +++ b/cache_nombres/registro_saludos.json @@ -1 +1,14 @@ -{"Emanuel Flores": {"fecha": "2026-04-13", "timestamp": 1776101179.9010606}, "Rodrigo Cahuantzi C": {"fecha": "2026-04-13", "timestamp": 1776105668.9753058}} \ No newline at end of file +{ + "Rodrigo Cahuantzi C": { + "fecha": "2026-06-25", + "timestamp": 1782405070.2212636 + }, + "Jose Luis Tenchil": { + "fecha": "2026-06-25", + "timestamp": 1782406202.8602962 + }, + "Emanuel Flores": { + "fecha": "2026-06-25", + "timestamp": 1782406597.9973903 + } +} \ No newline at end of file diff --git a/comentado.txt b/comentado.txt new file mode 100644 index 0000000..8f98b49 --- /dev/null +++ b/comentado.txt @@ -0,0 +1,1623 @@ +"""---------------------------------------------version + +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 +from datetime import datetime +import json +import csv +import math +import queue +from scipy.spatial.distance import cosine + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" +SECUENCIA = [1, 7, 5, 8, 3, 6] +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_5_msmt17_batch1.onnx" + +VECINOS = { + "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], + "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] +} + +TIEMPO_MAX_AUSENCIA = 900.0 +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 + +# ────────────────────────────────────────────────────────────────────────────── +# OSNET +# ────────────────────────────────────────────────────────────────────────────── +print("Cargando OSNet...") +try: + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + input_name = ort_session.get_inputs()[0].name + print("OSNet listo para CPU.") +except Exception as e: + print(f"ERROR FATAL: {e}"); exit() + +MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1) +STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1) + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRACCIÓN DE FIRMAS MULTI-DESCRIPTOR +# ────────────────────────────────────────────────────────────────────────────── +def extraer_features_osnet_seguro(rois): + + #Procesamiento secuencial 1-a-1 de grado comercial. Garantiza 0% de probabilidad de crasheo C++ en ONNX. + + if not rois: return [] + + features_norm = [] + for roi in rois: + # 1. Preprocesamiento matemático exacto para OSNet + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + + # Expandimos la dimensión para crear un Batch estricto de tamaño 1: (1, 3, 256, 128) + blob = np.expand_dims((img - MEAN) / STD, 0) + + # 2. INFERENCIA AISLADA (El motor ONNX no sufrirá estrés de memoria) + # El [0][0] extrae el vector ignorando la dimensión del batch + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + # 3. Normalización L2 (Vital para que funcione la distancia Coseno) + n = np.linalg.norm(df) + if n > 0: df /= n + + features_norm.append(df) + + return features_norm + +def calcular_nitidez(img): + if img is None or img.size == 0: return 0 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + return cv2.Laplacian(gray, cv2.CV_64F).var() + +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 + return np.expand_dims((img - MEAN) / STD, 0) + +SOPORTA_BATCH = None # Variable global para detectar compatibilidad ONNX + +def procesar_batch_osnet(rois): +#Procesa los recortes de forma segura y secuencial para evitar abortos C++ en ONNX + if not rois: return [] + + features_norm = [] + for roi in rois: + # Preprocesamiento individual + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + blob = np.expand_dims((img - MEAN) / STD, 0) + + # ⚡ Ejecución segura 1 a 1 (Garantiza que ONNX no colapse) + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + n = np.linalg.norm(df) + if n > 0: df /= n + features_norm.append(df) + + return features_norm + +def extraer_color_zonas(roi): + if roi is None or roi.size == 0 or roi.shape[0] < 30 or roi.shape[1] < 10: + return np.zeros(512 * 3, dtype=np.float32) + + h, w = roi.shape[:2] + + # ⚡ ALUCÍN OPTIMIZADO: LA MÁSCARA ELÍPTICA + # Creamos un lienzo negro y dibujamos un óvalo blanco en el centro. + # El histograma IGNORARÁ todo lo negro (la pared/fondo) y solo leerá lo blanco (el cuerpo). + mask = np.zeros((h, w), dtype=np.uint8) + centro_x, centro_y = int(w/2), int(h/2) + eje_x, eje_y = int(w * 0.35), int(h * 0.48) # Óvalo ajustado a proporciones humanas + cv2.ellipse(mask, (centro_x, centro_y), (eje_x, eje_y), 0, 0, 360, 255, -1) + + # ⚡ REGRESO AL HSV PURO (Sin CLAHE que adultere la luz) + hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) + + t1 = int(h * 0.30) + t2 = int(h * 0.65) + + # Cortamos la imagen Y la máscara en las 3 zonas (Cabeza, Torso, Piernas) + z1_img, z1_mask = hsv[:t1, :], mask[:t1, :] + z2_img, z2_mask = hsv[t1:t2, :], mask[t1:t2, :] + z3_img, z3_mask = hsv[t2:, :], mask[t2:, :] + + def calc_hist(img_part, mask_part): + if img_part.size == 0: return np.zeros(512, dtype=np.float32) + # Calculamos el color 3D (H,S,V) PERO pasándole la máscara para ignorar el fondo + hist = cv2.calcHist([img_part], [0, 1, 2], mask_part, [8, 8, 8], [0, 180, 0, 256, 0, 256]) + cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1) + return hist.flatten() + + h1 = calc_hist(z1_img, z1_mask) + h2 = calc_hist(z2_img, z2_mask) + h3 = calc_hist(z3_img, z3_mask) + + # Retorna 1536 dimensiones de color PURO y libre de paredes + return np.concatenate([h1, h2, h3]).astype(np.float32) + + +def extraer_proporciones_anatomicas(kpts): + if kpts is None or len(kpts) < 17: return None + kpts = np.array(kpts) + if kpts.shape[0] < 17: return None + + puntos = {i: kpts[i] for i in range(17)} + + if puntos[0][2] < 0.30: return None + if puntos[5][2] < 0.30 and puntos[6][2] < 0.30: return None + if puntos[11][2] < 0.30 and puntos[12][2] < 0.30: return None + + def dist(i, j): + if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0 + return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2])) + + hombros = dist(5, 6) + caderas = dist(11, 12) + torso = max(dist(5, 11), dist(6, 12)) + pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0 + pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0 + piernas = max(pierna_izq, pierna_der) + brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0 + brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0 + brazos = max(brazo_izq, brazo_der) + + altura = torso + piernas + if altura < 15.0: return None + + feats = np.array([ + hombros / max(altura, 1e-6), + caderas / max(altura, 1e-6), + torso / max(altura, 1e-6), + piernas / max(altura, 1e-6), + brazos / max(altura, 1e-6), + hombros / max(caderas, 1e-6), + piernas / max(torso, 1e-6), + ], dtype=np.float32) + + feats = np.clip(feats, 0.0, 3.0) + n = np.linalg.norm(feats) + if n > 0: feats /= n + return feats + +def es_humano_valido(kpts, box, conf): + if conf < 0.45: return False + if kpts is None or len(kpts) < 17: return False + + x1, y1, x2, y2 = box + w, h = x2 - x1, y2 - y1 + if h / max(w, 1) < 0.50 or h < 50: return False + + cabeza_segura = any(p[2] > 0.55 for p in kpts[:5]) + if not cabeza_segura: return False + + torso_seguro = any(p[2] > 0.40 for p in kpts[5:13]) + if not torso_seguro: return False + + return True + +def extraer_firma_hibrida(frame_hd, box_480, kpts=None, deep_feat=None): + try: + h_hd, w_hd = frame_hd.shape[:2] + x1, y1, x2, y2 = box_480 + + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: + return None + + score_nitidez = calcular_nitidez(roi) + + # ⚡ SOPORTE BATCH: Si no se entregó deep_feat masivo, lo calculamos individualmente (Fallback) + if deep_feat is None: + blob = preprocess_onnx(roi) + deep_feat = ort_session.run(None, {input_name: blob})[0][0].flatten() + n = np.linalg.norm(deep_feat) + if n > 0: deep_feat /= n + + color_f = extraer_color_zonas(roi) + anat_f = extraer_proporciones_anatomicas(kpts) + + ratio_hw = roi.shape[0] / max(roi.shape[1], 1) + calidad = (x2_c - x1_c) * (y2_c - y1_c) + + return { + 'deep': deep_feat, + 'color': color_f, # ⚡ ELIMINADO: Textura LBP + 'anatomia': anat_f, + 'ratio_hw': ratio_hw, + 'calidad': calidad, + 'nitidez': score_nitidez + } + except Exception as e: + return None + +def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=1.0): + if f1 is None or f2 is None: return 0.0 + + from scipy.spatial.distance import cosine + import numpy as np + + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is None or deep2 is None: return 0.0 + + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ───────────────────────────────────────────────────────────── + # ⚡ ECUALIZADOR CROSS-CAM (Domain Gap Fix) + # Compensamos la pérdida de puntaje causada por el cambio de iluminación. + # Evita que IDs viejos de la misma cámara ganen injustamente. + # ───────────────────────────────────────────────────────────── + if cross_cam and sim_deep > 0.50: + sim_deep += 0.05 + + # 1. ZONA DE CERTEZA ABSOLUTA + if sim_deep >= 0.70: + return min(1.0, sim_deep + 0.15) + + # 2. VÍA DE RECHAZO (No hay forma de que sea él) + if sim_deep < 0.45: + return sim_deep + + # 3. TRIBUNAL DE TEXTURAS Y ANATOMÍA + resultado_final = sim_deep + 0.05 + + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + if a1 is not None and a2 is not None and hasattr(a1, "shape") and a1.shape == a2.shape: + sim_anat = max(0.0, 1.0 - np.linalg.norm(a1 - a2)) + peso_anat = 0.12 * confianza_fisica + + if sim_anat > 0.88: + resultado_final += peso_anat + if sim_anat >= 0.95: resultado_final += 0.04 + elif sim_anat < 0.35: + resultado_final -= peso_anat + + else: + # PUNTOS CIEGOS (Solapamiento / Espaldas) + if sim_deep >= 0.50: + resultado_final += 0.12 + + return float(max(0.0, min(1.0, resultado_final))) + + +def desglose_similitud(f1, f2, cross_cam=False): + # ------------------------------------------------- + # 1. DEEP (OSNET) + # ------------------------------------------------- + sim_deep = 0.0 + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is not None and deep2 is not None and np.sum(deep1) > 0 and np.sum(deep2) > 0: + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ------------------------------------------------- + # 2. COLOR (2D HS INMUNE A SOMBRAS) + # ------------------------------------------------- + sim_color = -1.0 + c1, c2 = f1.get("color"), f2.get("color") + + # ⚡ CÓDIGO RESTAURADO A 1536 PARA LEER EL HSV PURO Y ENMASCARADO + if c1 is not None and c2 is not None and c1.shape == c2.shape and len(c1) == 1536: + if np.sum(c1) > 0.1 and np.sum(c2) > 0.1: + dist1 = cv2.compareHist(c1[:512].astype(np.float32), c2[:512].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist2 = cv2.compareHist(c1[512:1024].astype(np.float32), c2[512:1024].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist3 = cv2.compareHist(c1[1024:].astype(np.float32), c2[1024:].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + + s1 = max(0.0, 1.0 - float(dist1)) + s2 = max(0.0, 1.0 - float(dist2)) + s3 = max(0.0, 1.0 - float(dist3)) + + # Escudo chamarra abierta + if cross_cam and s3 > 0.60 and s2 < 0.35: + s2 = max(s2, s3 * 0.80) + + sim_color = (s1 * 0.20) + (s2 * 0.50) + (s3 * 0.30) + + # ------------------------------------------------- + # 3. ANATOMIA (7 Puntos) + # ------------------------------------------------- + sim_anat = -1.0 + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + + if a1 is not None and a2 is not None and hasattr(a1, "shape") and hasattr(a2, "shape") and a1.shape == a2.shape: + dist_a = np.linalg.norm(a1 - a2) + sim_anat = max(0.0, 1.0 - dist_a) + + return sim_deep, sim_color, sim_anat + +# ────────────────────────────────────────────────────────────────────────────── +# KALMAN TRACKER +# ────────────────────────────────────────────────────────────────────────────── +class KalmanTrack: + _count = 0 + + def __init__(self, box, now): + kf = cv2.KalmanFilter(7, 4) + 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) + kf.transitionMatrix = np.eye(7, dtype=np.float32) + kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1 + kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.01, 1.0, 2.0, 1.0]).astype(np.float32) + kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32) + kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32) + kf.statePost = np.zeros((7,1), np.float32) + kf.statePost[:4] = self._z(box) + self.kf = kf + + self.local_id = KalmanTrack._count; KalmanTrack._count += 1 + self.gid, self.origen_global, self.aprendiendo = None, False, False + self.box = list(box) + self.ts_creacion = 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.firma_pre_grupo = None + self.ts_salio_grupo = 0.0 + self.fallos_post_grupo = 0 + self.ultimo_aprendizaje = 0.0 + self.frames_continuos = 0 + self.frames_observados = 0 + self.firma_ema = None + self.muestras_capturadas = 0 + self.ultimo_cambio_id = 0.0 + + def _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/max(h,1e-6)]]).astype(np.float32) + + def _bbox(self, x): + cx, cy = float(x[0].item()), float(x[1].item()) + s, r = float(x[2].item()), float(x[3].item()) + w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6) + return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] + + @property + def confianza_fisica(self): + return min(1.0, self.frames_continuos / 20.0) + + def calcular_score_calidad(self, firma): + if firma is None: return 0.0 + score_nitidez = min(firma.get('nitidez', 0) / 100.0, 1.0) * 50.0 + score_area = min(firma.get('calidad', 0) / 12000.0, 1.0) * 50.0 + return score_nitidez + score_area + + def actualizar_ema(self, firma): + if firma is None: return + + self.muestras_capturadas = getattr(self, 'muestras_capturadas', 0) + 1 + score_nuevo = self.calcular_score_calidad(firma) + score_viejo = getattr(self, 'score_ema', 0.0) + + if self.firma_ema is None: + # Primera impresión (puede ser mala, pero es lo único que tenemos) + self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()} + self.score_ema = score_nuevo + return + + # ⚡ PROTECCIÓN DE MEMORIA (Anti-polución) + if self.muestras_capturadas > 4 and score_nuevo < (score_viejo * 0.60): + return + + # ───────────────────────────────────────────────────────────── + # ⚡ ASIMILACIÓN DINÁMICA (La cura a la mala primera impresión) + # ───────────────────────────────────────────────────────────── + # Si estamos en los primeros 5 frames, O la nueva foto es notoriamente mejor + # que nuestro mejor recuerdo (>20% mejor), somos una "esponja". + if self.muestras_capturadas <= 5 or score_nuevo > (score_viejo * 1.20): + # Alpha bajo = Sobrescribe agresivamente el pasado + alpha_deep = 0.20 # 20% vieja, 80% NUEVA + alpha_color = 0.20 + alpha_geo = 0.50 + self.score_ema = score_nuevo # Actualizamos nuestro estándar de calidad + else: + # Estado de madurez: La firma ya es excelente y estable. + # Los cambios deben ser muy lentos para no arruinarla. + alpha_deep = 0.85 # 85% vieja, 15% nueva + alpha_color = 0.50 + alpha_geo = 0.80 + self.score_ema = max(score_nuevo, score_viejo) + + ema = self.firma_ema + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + # Ya sin la textura LBP que eliminamos por rendimiento + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = alpha_geo * np.asarray(ema['anatomia']) + (1-alpha_geo) * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = max(ema.get('calidad', 0), firma['calidad']) + + def predict(self, turno_activo=True): + if self.time_since_update > 30: return None + + if getattr(self, 'en_grupo', False): + 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._bbox(self.kf.statePre) + return self.box + + if self.time_since_update > 0: + self.kf.statePost[4] *= 0.5 + self.kf.statePost[5] *= 0.5 + self.kf.statePost[6] = 0.0 + self.frames_continuos = max(0, self.frames_continuos - 1) + + 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._bbox(self.kf.statePre) + return self.box + + def update(self, box, en_grupo, now, kpts=None, firma_actual=None): + self.ts_ultima_deteccion = now + self.time_since_update, self.en_grupo = 0, en_grupo + self.box = list(box) + self.kf.correct(self._z(box)) + + if not en_grupo: + self.frames_observados += 1 + self.frames_continuos += 1 + self.listo_para_id = True + + +# ────────────────────────────────────────────────────────────────────────────── +# MEMORIA GLOBAL CON PERSISTENCIA +# ────────────────────────────────────────────────────────────────────────────── +class GlobalMemory: + def __init__(self): + self.db = {} + self.next_gid = 100 + self.lock = threading.RLock() + self.ruta_memoria = os.path.join("cache_nombres", "memoria_ids.json") + self.ruta_movimientos = os.path.join("cache_nombres", "registro_movimientos.csv") + os.makedirs("cache_nombres", exist_ok=True) + self.nombres_activos = {} + self.ultimos_saludos = {} + self._votos_nombre = {} + self.fecha_actual = datetime.now().date() + self._cargar_memoria() + self.reserva_temporal = {} + self.id_locks = {} + self.ultimas_detecciones = {} + + + def consolidar_clones(self): + + #Rutina de Auto-Merge comercial con Fusión Forzada por Biometría Facial. + from scipy.spatial.distance import cosine + + with self.lock: + ids_activos = list(self.db.keys()) + + for i in range(len(ids_activos)): + for j in range(i + 1, len(ids_activos)): + id1, id2 = ids_activos[i], ids_activos[j] + + if id1 not in self.db or id2 not in self.db: + continue + + ema1 = self.db[id1].get('ema') + ema2 = self.db[id2].get('ema') + if not ema1 or not ema2: continue + + deep1, deep2 = ema1.get('deep'), ema2.get('deep') + if deep1 is None or deep2 is None: continue + + nom1 = self.db[id1].get('nombre') + nom2 = self.db[id2].get('nombre') + + fusion_forzada = False + + # ⚡ AUTORIDAD FACIAL + if nom1 and nom2 and nom1 == nom2: + fusion_forzada = True # InsightFace los bautizó igual. Fusionamos sin importar la ropa. + elif nom1 and nom2 and nom1 != nom2: + continue # Nombres distintos = Personas distintas. Bloquear fusión. + + similitud = 1.0 - cosine(deep1, deep2) + + # Se fusionan si OSNet está seguro O si InsightFace forzó la orden + if similitud > 0.76 or fusion_forzada: + id_ganador = min(id1, id2) + id_clon = max(id1, id2) + + if self.db[id_clon].get('nombre'): + self.db[id_ganador]['nombre'] = self.db[id_clon]['nombre'] + + # Promediamos las firmas para que OSNet aprenda de ambos ángulos + self.db[id_ganador]['ema']['deep'] = (self.db[id_ganador]['ema']['deep'] * 0.7) + (self.db[id_clon]['ema']['deep'] * 0.3) + + self.db[id_clon] = {'fusionado_con': id_ganador, 'ts': time.time()} + + print(f" [AUTO-MERGE] Fragmentación corregida. Clon ID {id_clon} absorbido por Original ID {id_ganador} (Sim: {similitud:.2f})") + razon = "FUSIÓN FACIAL FORZADA" if fusion_forzada else "Fragmentación corregida" + print(f" [AUTO-MERGE] {razon}. Clon ID {id_clon} absorbido por Original ID {id_ganador} (Sim: {similitud:.2f})") + + # Método 1: Registrar detección global + def registrar_deteccion_global(self, gid, cam_id, firma, now): + #Registra que un ID fue detectado en una cámara para coordinación solapada. + if firma is None or 'deep' not in firma: + return + + # Hash simple de la firma deep para comparación rápida + firma_hash = hash(firma['deep'].tobytes()) % 10000 + + self.ultimas_detecciones[gid] = { + 'cam': str(cam_id), + 'ts': now, + 'firma_hash': firma_hash + } + + # Limpiar detecciones antiguas (>10 segundos) + antiguas = [g for g, d in self.ultimas_detecciones.items() if now - d['ts'] > 10.0] + for g in antiguas: + del self.ultimas_detecciones[g] + + # Método 2: Buscar coincidencia solapada + def buscar_coincidencia_solapada(self, firma, cam_id, now, umbral_sim=0.85): + #Busca si esta detección podría ser un ID ya detectado en cámara vecina recientemente. + if firma is None or 'deep' not in firma: + return None + + firma_hash = hash(firma['deep'].tobytes()) % 10000 + vecinos = VECINOS.get(str(cam_id), []) + + for gid, info in self.ultimas_detecciones.items(): + # Solo considerar si fue detectado en cámara vecina hace <5s + if info['cam'] not in vecinos: + continue + if now - info['ts'] > 5.0: + continue + + # Si el hash coincide exactamente, es muy probable que sea el mismo + if info['firma_hash'] == firma_hash: + return gid + + # Si no, hacer comparación completa (más costosa) + ema = self.db.get(gid, {}).get('ema') + if ema is not None: + sim = similitud_hibrida(firma, ema, cross_cam=True) + if sim >= umbral_sim: + return gid + + return None + + def lock_id_for_camera(self, gid, cam_id, now, duration=15.0): + self.id_locks[gid] = { + 'cam': str(cam_id), + 'unlock_ts': now + duration, + 'nombre': self.db.get(gid, {}).get('nombre') + } + print(f" [ID LOCK] ID {gid} bloqueado en Cam{cam_id} por {duration}s") + + def is_id_locked(self, gid, cam_id, now): + if gid not in self.id_locks: + return False + lock = self.id_locks[gid] + if lock['cam'] != str(cam_id): + return False + if now < lock['unlock_ts']: + return True + else: + del self.id_locks[gid] + return False + + def limpiar_locks_vencidos(self, now): + vencidos = [gid for gid, lock in self.id_locks.items() if now >= lock['unlock_ts']] + for gid in vencidos: + del self.id_locks[gid] + + def registrar_salida(self, gid, cam_salida, now): + if gid not in self.db: return + + vecinas = VECINOS.get(str(cam_salida), []) + self.reserva_temporal[gid] = { + 'cam_salida': str(cam_salida), + 'ts_salida': now, + 'cam_esperadas': vecinas, + 'nombre': self.db[gid].get('nombre') + } + + reservas_a_limpiar = [] + for gid_reserva, info in self.reserva_temporal.items(): + if now - info['ts_salida'] > 30.0: + reservas_a_limpiar.append(gid_reserva) + + for gid_limpiar in reservas_a_limpiar: + del self.reserva_temporal[gid_limpiar] + + def _bonus_reserva(self, gid_evaluado, cam_actual, now): + return 0.0 + + + def _firma_a_dict(self, firma): + if not firma: return None + res = {} + for k, v in firma.items(): + if isinstance(v, np.ndarray): + res[k] = v.tolist() + elif k == 'anatomia' and v is not None: + res[k] = v.tolist() if isinstance(v, np.ndarray) else list(v) + else: + res[k] = v + return res + + def _dict_a_firma(self, d): + if not d: return None + res = {} + for k, v in d.items(): + if isinstance(v, list) and k in ['deep', 'color', 'textura', 'anatomia']: + res[k] = np.array(v, dtype=np.float32) + else: + res[k] = v + return res + + def _cargar_memoria(self): + self.fecha_actual = datetime.now().date() + if os.path.exists(self.ruta_memoria): + # ⚡ RESET DIARIO EN INICIO: Si el archivo es de un día anterior, iniciar limpio. + fecha_archivo = datetime.fromtimestamp(os.path.getmtime(self.ruta_memoria)).date() + if fecha_archivo != self.fecha_actual: + print(f"[Memoria] Archivo de ayer ({fecha_archivo}). Iniciando memoria limpia desde ID 100.") + return + + try: + with open(self.ruta_memoria, 'r') as f: + datos = json.load(f) + + ahora = time.time() + cargados_validos = 0 + + for gid_str, info in datos.items(): + ts_guardado = info.get('ts', 0) + + # ⚡ PERSISTENCIA COMERCIAL + es_vip = info.get('nombre') is not None + # Los VIP duran 3 horas (10800s), los desconocidos 1 hora (3600s) para no saturar RAM + if es_vip and (ahora - ts_guardado > 10800.0): continue + if not es_vip and (ahora - ts_guardado > 3600.0): continue + + gid = int(gid_str) + self.db[gid] = { + 'ema': self._dict_a_firma(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'ts': ts_guardado, + 'nombre': info.get('nombre'), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._dict_a_firma(f) for f in info.get('firmas_altas', []) if f], + } + cargados_validos += 1 + + if cargados_validos > 0: + self.next_gid = max([int(k) for k in datos.keys()] + [99]) + 1 + print(f"[Memoria] {cargados_validos} IDs persistentes válidos cargados.") + except Exception as e: + print(f"[Memoria] Error cargando: {e}") + + # ⚡ FIX: Cargar los saludos previos para evitar spam auditivo + ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") + if os.path.exists(ruta_saludos): + try: + with open(ruta_saludos, 'r') as f: + self.ultimos_saludos = json.load(f) + except Exception: pass + + def guardar_memoria(self): + try: + datos = {} + for gid, info in self.db.items(): + datos[str(gid)] = { + 'ema': self._firma_a_dict(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'nombre': info.get('nombre'), + 'ts': info.get('ts', time.time()), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._firma_a_dict(f) for f in info.get('firmas_altas', [])], + } + with open(self.ruta_memoria, 'w') as f: + json.dump(datos, f) + except Exception as e: + print(f"[Memoria] Error guardando: {e}") + + def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion_seg): + nuevo = not os.path.exists(self.ruta_movimientos) + try: + with open(self.ruta_movimientos, 'a', newline='', encoding='utf-8') as f: + w = csv.writer(f) + if nuevo: w.writerow(["Fecha_Hora", "Nombre", "Origen", "Destino", "Seg_en_Origen"]) + w.writerow([datetime.now().strftime("%Y-%m-%d %H:%M:%S"), nombre, cam_origen, cam_destino, round(duracion_seg, 2)]) + except: pass + + def asignar_nombre(self, gid, nombre, confianza, es_fusion=False): + VOTOS_NOMBRE_MIN = 3 + UMBRAL_VOTO = 0.52 + UMBRAL_CONFIRMACION = 0.62 + + if confianza < (0.50 if es_fusion else UMBRAL_VOTO): + return False + + with self.lock: + rec = self.db.get(gid) + if not rec: return False + + nombre_viejo = rec.get('nombre') + + if not hasattr(self, 'nombres_activos'): + self.nombres_activos = {} + + gid_ocupante = self.nombres_activos.get(nombre) + if gid_ocupante is not None and gid_ocupante != gid: + if gid_ocupante in self.db: + ts_ultimo = self.db[gid_ocupante].get('ts', 0) + if (time.time() - ts_ultimo) < 10.0: + return False + + conf_actual = rec.get('confianza_nombre', 0.0) + if nombre_viejo == nombre and conf_actual > confianza: + return False + + if not hasattr(self, '_votos_nombre'): + self._votos_nombre = {} + + if gid not in self._votos_nombre: + self._votos_nombre[gid] = {} + + votos_gid = self._votos_nombre[gid] + + if any(n != nombre for n in votos_gid.keys()): + votos_gid.clear() + + if nombre not in votos_gid: + votos_gid[nombre] = [] + + votos_gid[nombre].append(confianza) + votos_gid[nombre] = votos_gid[nombre][-6:] + + n_votos = len(votos_gid[nombre]) + conf_media = sum(votos_gid[nombre]) / n_votos + + if n_votos < VOTOS_NOMBRE_MIN or conf_media < UMBRAL_CONFIRMACION: + return False + + if nombre_viejo is not None and nombre_viejo != nombre: + if confianza < 0.70: + print(f" [PROTECCIÓN] Rechazando cambio de {nombre_viejo} a {nombre} (conf={confianza:.2f} < 0.70)") + return False # Rechazar cambio automático + # Si confianza >= 0.70, permitir cambio pero con advertencia + print(f" [CAMBIO VIP] ID {gid} cambió de {nombre_viejo} a {nombre} (conf={confianza:.2f} >= 0.70)") + + rec['nombre'] = nombre + rec['confianza_nombre'] = conf_media + self.nombres_activos[nombre] = gid + + votos_gid.clear() + + tipo = "FUSIÓN" if es_fusion else "BAUTIZO" + print(f" [{tipo}] ID {gid} confirmado como {nombre} | Votos: {n_votos} | Conf. Media: {conf_media:.2f}") + return True + + def confirmar_firma_vip(self, gid, ts=None): + with self.lock: + rec = self.db.get(gid) + if rec and rec.get('ema') is not None: + # ⚡ FIX P2: Respetar la cristalización + if rec.get('cristalizado', False): + print(f" [MEMORIA] ID {gid} está cristalizado. No se añaden más firmas VIP.") + return + + firma_actual = rec['ema'] + if 'firmas_altas' not in rec: + rec['firmas_altas'] = [] + rec['firmas_altas'].append({ + k: v.copy() if isinstance(v, np.ndarray) else v + for k, v in firma_actual.items() + }) + if ts is not None: + rec['ts'] = ts + print(f" [MEMORIA] Firma de ID {gid} bloqueada y protegida como VIP.") + + def guardar_saludo(self, nombre): + #Guarda el saludo en memoria y lo escribe en el disco duro. + ahora = time.time() + fecha_hoy = datetime.fromtimestamp(ahora).strftime("%Y-%m-%d") + + self.ultimos_saludos[nombre] = { + 'fecha': fecha_hoy, + 'timestamp': ahora + } + + # Guardar físicamente en el disco + ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") + try: + with open(ruta_saludos, 'w') as f: + json.dump(self.ultimos_saludos, f, indent=4) + except Exception as e: + print(f" [MEMORIA] Error guardando registro de saludos: {e}") + + def _actualizar_sin_lock(self, gid, firma, cam_id, now): + if gid not in self.db: + self.db[gid] = { + 'ema': firma, + 'last_cam': cam_id, + 'ts': now, + 'nombre': None, + 'actualizaciones_globales': 1, + 'cristalizado': False + } + return + + rec = self.db[gid] + + # ⚡ CRISTALIZACIÓN: Después de 15 actualizaciones, CONGELAR EMA + if not rec.get('cristalizado', False) and rec.get('actualizaciones_globales', 0) >= 15: + rec['cristalizado'] = True + print(f" [CRISTALIZADO] ID {gid} ha alcanzado madurez. EMA congelado permanentemente.") + + # ⚡ BLINDAJE DE CRISTALIZACIÓN + # Si el ID ya es maduro, actualizamos dónde fue visto por última vez, + # pero NUNCA permitimos que una cámara modifique su firma maestra perfecta. + if rec.get('cristalizado', False): + rec['last_cam'] = cam_id + rec['ts'] = now + rec['actualizaciones_globales'] += 1 + return + + ema = rec['ema'] + rec['actualizaciones_globales'] += 1 + + if ema is None: + rec['ema'] = firma + else: + alpha_deep = 0.85 + alpha_color = 0.50 + alpha_geo = 0.80 + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = 0.80 * np.asarray(ema['anatomia']) + 0.20 * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = firma['calidad'] + + rec['last_cam'] = cam_id + rec['ts'] = now + + def _sim_contra_firma(self, firma_nueva, firma_guardada, cross_cam, confianza_fisica): + if firma_guardada is None: return 0.0 + return similitud_hibrida(firma_nueva, firma_guardada, cross_cam, confianza_fisica) + + def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0): + ema = gid_data.get('ema') + if not ema: return 0.0 + + sim_ema = self._sim_contra_firma(firma_nueva, ema, cross_cam, confianza_fisica) + + if sim_ema > 0.75: + return sim_ema + + sim_anclas = [sim_ema] + for ancla in gid_data.get('firmas_altas', []): + sim_anclas.append(self._sim_contra_firma(firma_nueva, ancla, cross_cam, confianza_fisica)) + + mejor_ancla = max(sim_anclas[1:]) if len(sim_anclas) > 1 else sim_ema + return sim_ema * 0.60 + mejor_ancla * 0.40 + + def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0): + self.limpiar_fantasmas() + + with self.lock: + candidatos = [] + + for gid, data in self.db.items(): + dt = now - data.get('ts', now) + distancia = self._distancia_topologica(str(data['last_cam']), str(cam_id)) + + misma_cam = (distancia == 0) + es_vecino = (distancia == 1) + es_salto = (distancia == 2) + es_cross = not misma_cam + + if gid in active_gids: + if misma_cam or distancia >= 3: continue + if dt > TIEMPO_MAX_AUSENCIA or data.get('ema') is None: continue + if self.is_id_locked(gid, cam_id, now): continue + + if not misma_cam: + if (es_vecino and dt < 0.3) or (es_salto and dt < 2.0) or (distancia >= 3 and dt < 8.0): + continue + + # ⚡ CÁLCULO OBLIGATORIO DE SIMILITUD ANTES DE CUALQUIER FILTRO + sim = self._sim_robusta(firma, data, es_cross, confianza_fisica) + + sim_deep, sim_color, sim_anat = -1.0, -1.0, -1.0 + ema = data.get("ema") + if ema is not None: + try: + sim_deep, sim_color, sim_anat = desglose_similitud(firma, ema, cross_cam=es_cross) + except: + pass + + # Penalizaciones y ajustes + if not misma_cam: + # NUNCA bloqueamos cámaras vecinas (distancia 1) por tiempo, + # porque al estar solapadas, pueden ver a la persona en el mismo milisegundo (dt=0.0). + if es_salto and dt < 1.5: + continue # Mínimo 1.5s para saltar una cámara entera + if distancia >= 3 and dt < 5.0: + continue # Mínimo 5.0s para cruzar a la otra punta del edificio + + if firma_ema_local is not None: + sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica) + sim = 0.60 * sim + 0.40 * sim_local + + # ⚡ VALIDACIONES VIP + es_vip = data.get('nombre') is not None + tiene_firmas_altas = len(data.get('firmas_altas', [])) > 0 + + if es_vip and tiene_firmas_altas and sim < 0.78: + continue + elif es_vip and not misma_cam and sim < 0.75: + continue + + # ───────────────────────────────────────────────────────────── + # ⚡ UMBRALES CORREGIDOS (Ni parálisis, ni robo de identidad) + # ───────────────────────────────────────────────────────────── + penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002) + + if misma_cam: umbral = 0.68 + elif es_vecino: umbral = 0.72 + elif es_salto: umbral = 0.75 + else: umbral = 0.78 + + # AHORA SÍ: sim_anat y sim_deep existen + if sim_anat > 0.90 and sim_deep > 0.65: + umbral -= 0.04 + + if en_borde and not misma_cam: + umbral -= 0.04 + + umbral = max(0.65, umbral + penal_multitud) + + bonus_aplicado = min(self._bonus_reserva(gid, cam_id, now), 0.03) + sim += bonus_aplicado + + # ⚡ RESTAURAR LOGS DE EVAL PARA DIAGNÓSTICO + if sim >= umbral - 0.10 or len(candidatos) < 3: + estado = "ACEPTADO" if sim >= umbral else "RECHAZADO" + faltante = umbral - sim if sim < umbral else 0.0 + info = f"(Faltó {faltante:.2f})" if faltante > 0 else "" + bonus_str = f" [+{bonus_aplicado:.2f}]" if bonus_aplicado > 0 else "" + + # ⚡ FIX P0: Eliminada la llamada doble a desglose_similitud. + # Usamos sim_deep, sim_color y sim_anat pre-calculados. + color_str = f"{sim_color:.2f}" if sim_color >= 0 else "N/A" + anat_str = f"{sim_anat:.2f}" if sim_anat >= 0 else "N/A" + + print( + f"[EVAL] Cam{cam_id} evalúa ID{gid} " + f"(Cam{data['last_cam']}, dt={dt:.1f}s) | " + f"Deep={sim_deep:.2f} | Color={color_str} | Anat={anat_str} | " + f"Final={sim:.2f}{bonus_str} | Umb={umbral:.2f} {info} -> {estado}" + ) + + if sim >= umbral: + candidatos.append((sim, sim, gid)) + + firma_guardar = firma_ema_local if firma_ema_local is not None else firma + + if not candidatos: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + candidatos.sort(reverse=True) + best_sim, _, best_gid = candidatos[0] + + # ⚡ ANTI-GEMELOS RECALIBRADO: Solo duda si son casi idénticos (<0.02) + if len(candidatos) >= 2: + _, segunda_sim, segundo_gid = candidatos[1] + if abs(best_sim - segunda_sim) < 0.02 and best_sim < 0.75: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + if best_gid in self.reserva_temporal: + del self.reserva_temporal[best_gid] + + # ───────────────────────────────────────────────────────────── + # ⚡ FIX: REGISTRO DE MOVIMIENTOS EN CSV + # Antes de actualizar, revisamos si viene de OTRA cámara y si tiene nombre + # ───────────────────────────────────────────────────────────── + cam_anterior = self.db[best_gid].get('last_cam') + nombre_registrado = self.db[best_gid].get('nombre') + + if cam_anterior and str(cam_anterior) != str(cam_id) and nombre_registrado: + tiempo_origen = now - self.db[best_gid].get('ts', now) + + # Lanzamos el registro en un hilo para no frenar el motor de tracking + threading.Thread( + target=self.registrar_movimiento, + args=(nombre_registrado, cam_anterior, cam_id, tiempo_origen), + daemon=True + ).start() + + # Ahora sí, actualizamos la memoria con la nueva cámara + self._actualizar_sin_lock(best_gid, firma_guardar, cam_id, now) + return best_gid, True + + + def limpiar_fantasmas(self): + ahora = time.time() + fecha_hoy = datetime.fromtimestamp(ahora).date() + + with self.lock: + # ⚡ RESET DIARIO EN CALIENTE (Si el programa corre 24/7) + if fecha_hoy != self.fecha_actual: + print(f"\n[SISTEMA] ¡Cambio de día! Purgando memoria y reiniciando contador a 100.\n") + self.db.clear() + self.nombres_activos.clear() + self.next_gid = 100 + self.fecha_actual = fecha_hoy + self.guardar_memoria() + return + + # ⚡ PURGA NORMAL (3 hrs VIP, 1 hr Desconocidos) + muertos = [] + for g, d in self.db.items(): + if d.get('nombre') is None and (ahora - d.get('ts', ahora)) > 3600: + muertos.append(g) + elif d.get('nombre') is not None and (ahora - d.get('ts', ahora)) > 10800: + muertos.append(g) + + for g in muertos: + nombre_perdido = self.db[g].get('nombre') + if nombre_perdido and nombre_perdido in self.nombres_activos: + del self.nombres_activos[nombre_perdido] + del self.db[g] + + if muertos: + self.guardar_memoria() + + def _distancia_topologica(self, cam_o, cam_d): + if cam_o == cam_d: return 0 + vecinos_directos = VECINOS.get(cam_o, []) + if cam_d in vecinos_directos: return 1 + for v in vecinos_directos: + if cam_d in VECINOS.get(v, []): return 2 + return 3 + +# ────────────────────────────────────────────────────────────────────────────── +# GESTOR LOCAL +# ────────────────────────────────────────────────────────────────────────────── +def iou_overlap(A, B): + xA,yA = max(A[0],B[0]),max(A[1],B[1]) + xB,yB = min(A[2],B[2]),min(A[3],B[3]) + inter = max(0,xB-xA)*max(0,yB-yA) + return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-inter+1e-6) + +class CamManager: + def __init__(self, cam_id, global_mem): + self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] + self.ultimas_detecciones_globales = {} + self.tracks_post_cruce = {} + + def _limpiar_tracks_post_cruce(self, now): + #Limpia registros de post-cruce antiguos. + antiguos = [idx for idx, info in self.tracks_post_cruce.items() if now - info['ts_cruce'] > 10.0] + for idx in antiguos: + del self.tracks_post_cruce[idx] + + def _detectar_grupo(self, trk, box, todos): + x1,y1,x2,y2 = box + w,h = x2-x1, y2-y1 + cx,cy = (x1+x2)/2, (y1+y2)/2 + fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35) + for o in todos: + if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue + if iou_overlap(box, o.box) > 0.05: return True + ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2 + if abs(cx-ocx)= 0.48: + trk.fallos_post_grupo = 0; trk.firma_pre_grupo = None + with self.global_mem.lock: + if trk.gid in self.global_mem.db: self.global_mem.db[trk.gid]['ema'] = fa + else: + trk.fallos_post_grupo += 1 + if trk.fallos_post_grupo >= 3: + trk.gid = trk.firma_pre_grupo = None; trk.fallos_post_grupo = 0 + + def _asignar(self, boxes, confidences, frame_hd, now, keypoints_list): + n_trk, n_det = len(self.trackers), len(boxes) + firmas = [None] * n_det + + # ───────────────────────────────────────────────────────────── + # ⚡ OPTIMIZACIÓN BATCH: Extraemos OSNet para todas las detecciones de un golpe + # ───────────────────────────────────────────────────────────── + if n_det > 0: + rois_validos = [] + mapa_rois = {} + h_hd, w_hd = frame_hd.shape[:2] + + # 1. Recolectar todos los recortes (ROIs) de la imagen + for d_idx, box in enumerate(boxes): + x1, y1, x2, y2 = box + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + # Descartamos basura óptica de inmediato + if roi.size > 0 and roi.shape[0] >= 40 and roi.shape[1] >= 20: + mapa_rois[d_idx] = len(rois_validos) + rois_validos.append(roi) + + # 2. Ejecutar la red neuronal para todos SIMULTÁNEAMENTE + if rois_validos: + # ⚡ Llamamos a la función segura que procesa 1 a 1 rápidamente + deep_feats_seguros = extraer_features_osnet_seguro(rois_validos) + + # Ensamblar las firmas + for d_idx, box in enumerate(boxes): + if d_idx in mapa_rois: + df_persona = deep_feats_seguros[mapa_rois[d_idx]] + kpts = keypoints_list[d_idx] if keypoints_list and d_idx < len(keypoints_list) else None + + # Extraemos el resto de la firma (Color enmascarado, Anatomía, etc.) + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts, deep_feat=df_persona) + + def obtener_firma(d_idx): + return firmas[d_idx] + + if n_trk == 0: return [], list(range(n_det)), [], firmas + if n_det == 0: return [], [], list(range(n_trk)), firmas + + # ... (A partir de aquí, el código de "alta" y "baja" confidence se queda EXACTAMENTE como ya lo tienes) ... + alta = [(d,boxes[d]) for d,c in enumerate(confidences) if c >= 0.45] + baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45] + matched, sin_match_trk = [], list(range(n_trk)) + + if alta: + C = np.full((n_trk, len(alta)), 100.0, np.float32) + for t, trk in enumerate(self.trackers): + dt_oculto = now - trk.ts_ultima_deteccion + radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto)))) + ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None) + + for idx, (d, det) in enumerate(alta): + iou = iou_overlap(trk.box, det) + dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2) + if dist > radio: continue + + a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1]) + a_det = (det[2]-det[0])*(det[3]-det[1]) + costo = (1.0*(1-iou)) + (0.3*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.1) + costo += (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.1) + costo += min(0.3, dt_oculto*0.1) + + if ref_firma is not None and obtener_firma(d) is not None: + sim_ap = similitud_hibrida(ref_firma, firmas[d]) + + if sim_ap < 0.25: + costo += 50.0 + else: + costo += (1.0 - sim_ap) * 2.0 + + nombre_vip = self.global_mem.db.get(trk.gid, {}).get('nombre') + umbral_ap = 0.55 if nombre_vip else 0.40 + if sim_ap < umbral_ap: + costo += (umbral_ap - sim_ap) * 3.0 + + C[t,idx] = costo + + for r,c in zip(*linear_sum_assignment(C)): + if C[r,c] <= 6.5: + matched.append((r, alta[c][0])); sin_match_trk.remove(r) + + if sin_match_trk and baja: + C2 = np.ones((len(sin_match_trk), len(baja)), np.float32) + for ti, trk_i in enumerate(sin_match_trk): + for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det) + nuevos_sin = [] + for r,c in zip(*linear_sum_assignment(C2)): + if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0])) + else: nuevos_sin.append(sin_match_trk[r]) + sin_match_trk = nuevos_sin + + return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas + + def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo): + # ───────────────────────────────────────────────────────────── + # 1. Resolver fusiones globales + # ───────────────────────────────────────────────────────────── + for trk in self.trackers: + if trk.gid: + with self.global_mem.lock: + d = self.global_mem.db.get(trk.gid) + if d and 'fusionado_con' in d: + trk.gid = d['fusionado_con'] + + # ───────────────────────────────────────────────────────────── + # 2. Predict y filtrar tracks muertos por Kalman + # ───────────────────────────────────────────────────────────── + self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None] + if not turno_activo: + return self.trackers + + # ───────────────────────────────────────────────────────────── + # 3. Asignar detecciones a tracks existentes + # ───────────────────────────────────────────────────────────── + matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints) + active_gids = {t.gid for t in self.trackers if t.gid is not None} + fh, fw = frame_hd.shape[:2] + + # ───────────────────────────────────────────────────────────── + # 4. DETECCIÓN DE CRUCES + Registro para recuperación post-cruce + # ───────────────────────────────────────────────────────────── + tracks_en_cruce_locals = set() + for i in range(len(self.trackers)): + for j in range(i + 1, len(self.trackers)): + trk_i, trk_j = self.trackers[i], self.trackers[j] + if trk_i.gid is None or trk_j.gid is None or trk_i.box is None or trk_j.box is None: continue + + iou = iou_overlap(trk_i.box, trk_j.box) + if iou > 0.30: + tracks_en_cruce_locals.add(trk_i.local_id) + tracks_en_cruce_locals.add(trk_j.local_id) + if not hasattr(self, 'tracks_post_cruce'): self.tracks_post_cruce = {} + self.tracks_post_cruce[trk_i.local_id] = {'gid_antes': trk_i.gid, 'ts_cruce': now} + self.tracks_post_cruce[trk_j.local_id] = {'gid_antes': trk_j.gid, 'ts_cruce': now} + + + # ───────────────────────────────────────────────────────────── + # 5. PROCESAR MATCHES + # ───────────────────────────────────────────────────────────── + for t_idx, d_idx in matched: + trk, box = self.trackers[t_idx], boxes[d_idx] + + # Asegurar firma visual REAL (mitigación del bug de contaminación en _asignar) + if firmas[d_idx] is None: + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts) + + firma_det = firmas[d_idx] + + # Debug + if trk.gid is None: + print(f" [DEBUG] LOCAL Trk {trk.local_id}: listo={trk.listo_para_id}, cap_locales={getattr(trk, 'muestras_capturadas', 0)}") + else: + with self.global_mem.lock: + db_act = self.global_mem.db.get(trk.gid, {}).get('actualizaciones_globales', 0) + print(f" [MEMORIA] ID {trk.gid} ha sido actualizado {db_act} veces en la red global.") + + # Detección de grupo + es_grupo = self._detectar_grupo(trk, box, self.trackers) + if not trk.en_grupo and es_grupo: + with self.global_mem.lock: + trk.firma_pre_grupo = self.global_mem.db.get(trk.gid, {}).get('ema') + trk.ts_salio_grupo = 0.0 + elif trk.en_grupo and not es_grupo: + trk.ts_salio_grupo = now + trk.en_grupo = es_grupo + + # Update Kalman + trk.update(box, es_grupo, now, kpts=(keypoints[d_idx] if keypoints else None), firma_actual=firma_det) + + # Geometría de frame + margen_x = fw * 0.05 + esta_en_centro = (box[0] > margen_x and box[2] < (fw - margen_x)) + + # ───────────────────────────────────────────────────────── + # 5a. APRENDIZAJE EMA (zona segura, centro del frame) + # ───────────────────────────────────────────────────────── + if firma_det is not None and not es_grupo and esta_en_centro: + trk.actualizar_ema(firma_det) + + # Nutrir memoria global cada 2 segundos + if trk.gid is not None: + if not hasattr(trk, 'ultimo_update_global'): + trk.ultimo_update_global = 0 + if (now - trk.ultimo_update_global) > 2.0: + with self.global_mem.lock: + if trk.gid in self.global_mem.db: + self.global_mem.db[trk.gid]['ema'] = trk.firma_ema.copy() + self.global_mem.db[trk.gid]['ts'] = now + self.global_mem.db[trk.gid]['actualizaciones_globales'] = \ + self.global_mem.db[trk.gid].get('actualizaciones_globales', 0) + 1 + trk.ultimo_update_global = now + + # ⚡ INYECCIÓN EXACTA AQUÍ: Disparar el Auto-Merge + self.global_mem.consolidar_clones() + + # ───────────────────────────────────────────────────────── + # 5b. RESCATE DE BORDE (Acelerado) + # ───────────────────────────────────────────────────────── + elif firma_det is not None and not es_grupo and not esta_en_centro: + muestras_act = getattr(trk, 'muestras_capturadas', 0) + if muestras_act < 3: + trk.actualizar_ema(firma_det) + elif muestras_act < 5 and trk.frames_observados % 3 == 0: + trk.actualizar_ema(firma_det) + + # ───────────────────────────────────────────────────────── + # 5c. IDENTIFICACIÓN CON PROTECCIÓN + # ───────────────────────────────────────────────────────── + if trk.gid is None and trk.listo_para_id and firma_det is not None: + if getattr(trk, 'muestras_capturadas', 0) >= 4: + if trk.local_id in tracks_en_cruce_locals: + print(f" [CRUCE] Track {trk.local_id} en cruce físico. Bloqueando identificación.") + else: + tiempo_enfriamiento = now - getattr(trk, 'ultimo_cambio_id', 0) + if tiempo_enfriamiento < 0.6: + print(f" [HYSTERESIS] Track {trk.local_id} en enfriamiento ({tiempo_enfriamiento:.2f}s)") + else: + gid_c, es_reid = self.global_mem.identificar_candidato( + firma_det, self.cam_id, now, active_gids, + en_borde=not esta_en_centro, + firma_ema_local=trk.firma_ema, + confianza_fisica=trk.confianza_fisica + ) + if gid_c: + active_gids.add(gid_c) + trk.gid = gid_c + trk.origen_global = es_reid + trk.ultimo_cambio_id = now + + # ───────────────────────────────────────────────────────────── + # 6. VERIFICACIÓN POST-CRUCE (recuperar ID original tras separarse) + # ───────────────────────────────────────────────────────────── + if hasattr(self, 'tracks_post_cruce') and self.tracks_post_cruce: + tracks_post_cruce_nuevos = {} + + # ⚡ CAMBIO CRÍTICO: Iterar por local_id, no por índice + for local_id, info in list(self.tracks_post_cruce.items()): + # Buscar el track con este local_id (único e inmutable) + trk = next((t for t in self.trackers if t.local_id == local_id), None) + if trk is None: + continue # El track ya murió, ignorar + + # Solo actuar si ya pasaron >2 segundos desde el cruce + if now - info['ts_cruce'] > 2.0: + # Si el ID cambió durante el cruce (o se asignó uno nuevo incorrecto) + if trk.gid is not None and trk.gid != info['gid_antes']: + if trk.firma_ema is not None and info['gid_antes'] in self.global_mem.db: + ema_original = self.global_mem.db[info['gid_antes']].get('ema') + if ema_original is not None: + sim_recuperacion = similitud_hibrida( + trk.firma_ema, ema_original, cross_cam=False + ) + if sim_recuperacion > 0.85: + # Verificar que el ID original no esté activo en otro track + if info['gid_antes'] not in active_gids: + print(f" [POST-CRUCE] Track {trk.local_id} recuperó ID {info['gid_antes']} (sim={sim_recuperacion:.2f})") + active_gids.discard(trk.gid) + trk.gid = info['gid_antes'] + active_gids.add(trk.gid) + # No guardamos este registro más (ya cumplió su ciclo) + continue + + # Aún no pasan 2 segundos, mantener registro (usando local_id) + tracks_post_cruce_nuevos[local_id] = info + + self.tracks_post_cruce = tracks_post_cruce_nuevos + + # ───────────────────────────────────────────────────────────── + # 7. CREAR NUEVOS TRACKERS para detecciones sin match + # ───────────────────────────────────────────────────────────── + for d_idx in new_dets: + nt = KalmanTrack(boxes[d_idx], now) + f = firmas[d_idx] + if f: + nt.actualizar_ema(f) + self.trackers.append(nt) + + # ───────────────────────────────────────────────────────────── + # 8. LIMPIEZA: matar tracks sin detección por >3 segundos + # ───────────────────────────────────────────────────────────── + tracks_vivos = [] + for t in self.trackers: + if (now - t.ts_ultima_deteccion) < 3.0: + tracks_vivos.append(t) + self.trackers = tracks_vivos + + # ⚡ FIX P0: Purga de memory leak en tracks_post_cruce + if hasattr(self, 'tracks_post_cruce'): + local_ids_vivos = {t.local_id for t in self.trackers} + self.tracks_post_cruce = {lid: info for lid, info in self.tracks_post_cruce.items() if lid in local_ids_vivos} + + # ───────────────────────────────────────────────────────────── + # 9. Gestión post-grupo (recuperación de firma tras salir de multitud) + # ───────────────────────────────────────────────────────────── + self._gestionar_post_grupo(now, frame_hd) + + return self.trackers + +class CamStream: + def __init__(self, url): + self.url = url; self.cap = cv2.VideoCapture(url) + self.q = queue.Queue(maxsize=1); self.stopped = False + threading.Thread(target=self._run, daemon=True).start() + def _run(self): + while not self.stopped: + ret, f = self.cap.read() + if not ret: time.sleep(2); self.cap.open(self.url); continue + if self.q.full(): + try: self.q.get_nowait() + except queue.Empty: pass + self.q.put(f) + @property + def frame(self): + try: return self.q.get(timeout=0.5) + except queue.Empty: return None + def stop(self): self.stopped = True; self.cap.release() + +def dibujar_track(frame_show, trk): + try: x1,y1,x2,y2 = map(int,trk.box) + except: return + if trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}" + elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]" + elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]" + elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]" + 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) + +# ────────────────────────────────────────────────────────────────────────────── +# MAIN +# ────────────────────────────────────────────────────────────────────────────── +def main(): + print("\n=== Tracker Autónomo Definitivo ===") + print("Persistencia activa. Los IDs se recuerdan entre reinicios.") + print("Tecla [q] para salir (la memoria se guarda automáticamente).\n") + + model = YOLO("yolov8n-pose.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 + ultimo_guardado = time.time() + + try: + while True: + now, tiles = time.time(), [] + 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, confs, kpts = [], [], [] + 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: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + + if es_humano_valido(kpt_persona, box, conf): + boxes.append(box) + confs.append(conf) + kpts.append(kpt_persona) + + ESQUELETO = [(0,1),(0,2),(1,3),(2,4),(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)] + for pk in kpts: + if pk is None: continue + for a,b in ESQUELETO: + if pk[a][2]>0.40 and pk[b][2]>0.40: + cv2.line(frame_show, (int(pk[a][0]),int(pk[a][1])), (int(pk[b][0]),int(pk[b][1])), (255,0,255), 2) + for kx, ky, kc in pk: + if kc > 0.40: cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1) + + tracks = managers[cid].update(boxes, confs, kpts, frame_show, frame, 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 getattr(t,'gid',None) 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[:3]),np.hstack(tiles[3:])])) + + idx += 1 + + if now - ultimo_guardado > 30: + global_mem.guardar_memoria() + ultimo_guardado = now + + if cv2.waitKey(1) == ord('q'): break + + finally: + global_mem.guardar_memoria() + print("[Memoria] Guardada antes de salir.") + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() +""" \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..cfa57e8 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "dvr_ip": "192.168.1.244", + "dvr_user": "admin", + "dvr_pass": "TCA200503", + "secuencia_total": "2, 1, 5, 7, 8, 4, 3, 6", + "ignoradas": "2, 4", + "vecinos": "{\"1\":[\"7\"], \"7\":[\"1\",\"5\"], \"5\":[\"7\",\"8\"], \"8\":[\"5\",\"3\"], \"3\":[\"8\",\"6\"], \"6\":[\"3\"]}", + "cams_por_pantalla": "8" +} \ No newline at end of file diff --git a/db_aprendida/Adriana Lopez_auto_1782399933.jpg b/db_aprendida/Adriana Lopez_auto_1782399933.jpg new file mode 100644 index 0000000..ba1f4f0 Binary files /dev/null and b/db_aprendida/Adriana Lopez_auto_1782399933.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400429.jpg b/db_aprendida/Emanuel Flores_auto_1782400429.jpg new file mode 100644 index 0000000..39c04ce Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400429.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400534.jpg b/db_aprendida/Emanuel Flores_auto_1782400534.jpg new file mode 100644 index 0000000..557c69c Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400534.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400668.jpg b/db_aprendida/Emanuel Flores_auto_1782400668.jpg new file mode 100644 index 0000000..d092b22 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400668.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400754.jpg b/db_aprendida/Emanuel Flores_auto_1782400754.jpg new file mode 100644 index 0000000..d8c5c9e Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400754.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400911.jpg b/db_aprendida/Emanuel Flores_auto_1782400911.jpg new file mode 100644 index 0000000..101a263 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400911.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400934.jpg b/db_aprendida/Emanuel Flores_auto_1782400934.jpg new file mode 100644 index 0000000..f5c25b8 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400934.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400950.jpg b/db_aprendida/Emanuel Flores_auto_1782400950.jpg new file mode 100644 index 0000000..651a9c4 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400950.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400980.jpg b/db_aprendida/Emanuel Flores_auto_1782400980.jpg new file mode 100644 index 0000000..f01b6fb Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400980.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782400997.jpg b/db_aprendida/Emanuel Flores_auto_1782400997.jpg new file mode 100644 index 0000000..c9e59f6 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782400997.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401034.jpg b/db_aprendida/Emanuel Flores_auto_1782401034.jpg new file mode 100644 index 0000000..6e7d8db Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401034.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401044.jpg b/db_aprendida/Emanuel Flores_auto_1782401044.jpg new file mode 100644 index 0000000..6f14ced Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401044.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401077.jpg b/db_aprendida/Emanuel Flores_auto_1782401077.jpg new file mode 100644 index 0000000..26561e4 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401077.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401111.jpg b/db_aprendida/Emanuel Flores_auto_1782401111.jpg new file mode 100644 index 0000000..3e567bf Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401111.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401147.jpg b/db_aprendida/Emanuel Flores_auto_1782401147.jpg new file mode 100644 index 0000000..4604f0e Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401147.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401160.jpg b/db_aprendida/Emanuel Flores_auto_1782401160.jpg new file mode 100644 index 0000000..35d852f Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401160.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401171.jpg b/db_aprendida/Emanuel Flores_auto_1782401171.jpg new file mode 100644 index 0000000..416c32e Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401171.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401187.jpg b/db_aprendida/Emanuel Flores_auto_1782401187.jpg new file mode 100644 index 0000000..87ed741 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401187.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401204.jpg b/db_aprendida/Emanuel Flores_auto_1782401204.jpg new file mode 100644 index 0000000..2553385 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401204.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401238.jpg b/db_aprendida/Emanuel Flores_auto_1782401238.jpg new file mode 100644 index 0000000..cff4c8d Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401238.jpg differ diff --git a/db_aprendida/Emanuel Flores_auto_1782401426.jpg b/db_aprendida/Emanuel Flores_auto_1782401426.jpg new file mode 100644 index 0000000..afe0919 Binary files /dev/null and b/db_aprendida/Emanuel Flores_auto_1782401426.jpg differ diff --git a/db_aprendida/Fernando_auto_1782400130.jpg b/db_aprendida/Fernando_auto_1782400130.jpg new file mode 100644 index 0000000..8b3a9cd Binary files /dev/null and b/db_aprendida/Fernando_auto_1782400130.jpg differ diff --git a/db_aprendida/Fernando_auto_1782400188.jpg b/db_aprendida/Fernando_auto_1782400188.jpg new file mode 100644 index 0000000..1895b25 Binary files /dev/null and b/db_aprendida/Fernando_auto_1782400188.jpg differ diff --git a/db_aprendida/Fernando_auto_1782400338.jpg b/db_aprendida/Fernando_auto_1782400338.jpg new file mode 100644 index 0000000..ba5690b Binary files /dev/null and b/db_aprendida/Fernando_auto_1782400338.jpg differ diff --git a/db_aprendida/Fernando_auto_1782400865.jpg b/db_aprendida/Fernando_auto_1782400865.jpg new file mode 100644 index 0000000..569a6c2 Binary files /dev/null and b/db_aprendida/Fernando_auto_1782400865.jpg differ diff --git a/db_aprendida/Jose Luis Tenchil_auto_1782406221.jpg b/db_aprendida/Jose Luis Tenchil_auto_1782406221.jpg new file mode 100644 index 0000000..e459127 Binary files /dev/null and b/db_aprendida/Jose Luis Tenchil_auto_1782406221.jpg differ diff --git a/db_aprendida/Jose Luis_auto_1782400014.jpg b/db_aprendida/Jose Luis_auto_1782400014.jpg new file mode 100644 index 0000000..bc0d381 Binary files /dev/null and b/db_aprendida/Jose Luis_auto_1782400014.jpg differ diff --git a/db_aprendida/Josue Muñoz_auto_1782399868.jpg b/db_aprendida/Josue Muñoz_auto_1782399868.jpg new file mode 100644 index 0000000..2521511 Binary files /dev/null and b/db_aprendida/Josue Muñoz_auto_1782399868.jpg differ diff --git a/db_aprendida/Josue Muñoz_auto_1782399899.jpg b/db_aprendida/Josue Muñoz_auto_1782399899.jpg new file mode 100644 index 0000000..59de774 Binary files /dev/null and b/db_aprendida/Josue Muñoz_auto_1782399899.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782400230.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782400230.jpg new file mode 100644 index 0000000..0e63134 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782400230.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782400302.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782400302.jpg new file mode 100644 index 0000000..8d1fbaa Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782400302.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782400922.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782400922.jpg new file mode 100644 index 0000000..f6155a2 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782400922.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782400948.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782400948.jpg new file mode 100644 index 0000000..fe6599e Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782400948.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782400973.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782400973.jpg new file mode 100644 index 0000000..9c9cba4 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782400973.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782401014.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782401014.jpg new file mode 100644 index 0000000..b851a7a Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782401014.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782401478.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782401478.jpg new file mode 100644 index 0000000..3646838 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782401478.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782401490.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782401490.jpg new file mode 100644 index 0000000..3e97a31 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782401490.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782401515.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782401515.jpg new file mode 100644 index 0000000..160e396 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782401515.jpg differ diff --git a/db_aprendida/Oscar Atriano Ponce_auto_1782401526.jpg b/db_aprendida/Oscar Atriano Ponce_auto_1782401526.jpg new file mode 100644 index 0000000..6763911 Binary files /dev/null and b/db_aprendida/Oscar Atriano Ponce_auto_1782401526.jpg differ diff --git a/db_aprendida/Rubisela Barrientos_auto_1782401735.jpg b/db_aprendida/Rubisela Barrientos_auto_1782401735.jpg new file mode 100644 index 0000000..287e99a Binary files /dev/null and b/db_aprendida/Rubisela Barrientos_auto_1782401735.jpg differ diff --git a/db_aprendida/lore_auto_1782400709.jpg b/db_aprendida/lore_auto_1782400709.jpg new file mode 100644 index 0000000..887721a Binary files /dev/null and b/db_aprendida/lore_auto_1782400709.jpg differ diff --git a/db_institucion/Abigail.jpg b/db_institucion/Abigail.jpg new file mode 100644 index 0000000..76ddfe3 Binary files /dev/null and b/db_institucion/Abigail.jpg differ diff --git a/db_institucion/Alfredo Martinez.jpg b/db_institucion/Alfredo Martinez.jpg new file mode 100644 index 0000000..705ff83 Binary files /dev/null and b/db_institucion/Alfredo Martinez.jpg differ diff --git a/db_institucion/Ana Lidia Gaspariano.jpg b/db_institucion/Ana Lidia Gaspariano.jpg new file mode 100644 index 0000000..d86c2a8 Binary files /dev/null and b/db_institucion/Ana Lidia Gaspariano.jpg differ diff --git a/db_institucion/aridai montiel zistecatl.jpg b/db_institucion/Aridai Montiel _1.jpg similarity index 100% rename from db_institucion/aridai montiel zistecatl.jpg rename to db_institucion/Aridai Montiel _1.jpg diff --git a/db_institucion/Aridai montiel.jpg b/db_institucion/Aridai Montiel.jpg similarity index 100% rename from db_institucion/Aridai montiel.jpg rename to db_institucion/Aridai Montiel.jpg diff --git a/db_institucion/Bertha.jpg b/db_institucion/Bertha.jpg new file mode 100644 index 0000000..c00bbcd Binary files /dev/null and b/db_institucion/Bertha.jpg differ diff --git a/db_institucion/Brayan Mendoza.jpg b/db_institucion/Brayan Mendoza.jpg new file mode 100644 index 0000000..fb5b9a1 Binary files /dev/null and b/db_institucion/Brayan Mendoza.jpg differ diff --git a/db_institucion/Cintia.jpg b/db_institucion/Cintia.jpg new file mode 100644 index 0000000..a84cc67 Binary files /dev/null and b/db_institucion/Cintia.jpg differ diff --git a/db_institucion/Cristian Hernandez.jpg b/db_institucion/Cristian Hernandez.jpg new file mode 100644 index 0000000..aa22b80 Binary files /dev/null and b/db_institucion/Cristian Hernandez.jpg differ diff --git a/db_institucion/Cristian Hernandez Suarez.jpg b/db_institucion/Cristian Hernandez_1.jpg similarity index 100% rename from db_institucion/Cristian Hernandez Suarez.jpg rename to db_institucion/Cristian Hernandez_1.jpg diff --git a/db_institucion/Cristian Lopez Garcia.jpg b/db_institucion/Cristian Lopez Garcia.jpg new file mode 100644 index 0000000..47a6aeb Binary files /dev/null and b/db_institucion/Cristian Lopez Garcia.jpg differ diff --git a/db_institucion/Daniel Vasquez Ramirez.jpg b/db_institucion/Daniel Vasquez Ramirez.jpg new file mode 100644 index 0000000..457f6a4 Binary files /dev/null and b/db_institucion/Daniel Vasquez Ramirez.jpg differ diff --git a/db_institucion/Diana Laura Tecpa.jpg b/db_institucion/Diana Laura Tecpa.jpg deleted file mode 100644 index 35079d1..0000000 Binary files a/db_institucion/Diana Laura Tecpa.jpg and /dev/null differ diff --git a/db_institucion/Diana Laura.jpg b/db_institucion/Diana Laura.jpg deleted file mode 100644 index 479b9fd..0000000 Binary files a/db_institucion/Diana Laura.jpg and /dev/null differ diff --git a/db_institucion/Eedwin Santa ana.jpg b/db_institucion/Eedwin Santa ana.jpg new file mode 100644 index 0000000..76dbf73 Binary files /dev/null and b/db_institucion/Eedwin Santa ana.jpg differ diff --git a/db_institucion/Fernando.jpg b/db_institucion/Fernando.jpg new file mode 100644 index 0000000..a3cc9c0 Binary files /dev/null and b/db_institucion/Fernando.jpg differ diff --git a/db_institucion/ian axel.jpg b/db_institucion/Ian Axel_1.jpg similarity index 100% rename from db_institucion/ian axel.jpg rename to db_institucion/Ian Axel_1.jpg diff --git a/db_institucion/Jair Gregorio.jpg b/db_institucion/Jair Gregorio.jpg new file mode 100644 index 0000000..70e34f2 Binary files /dev/null and b/db_institucion/Jair Gregorio.jpg differ diff --git a/db_institucion/Jesus Eduardo.jpg b/db_institucion/Jesus Eduardo.jpg new file mode 100644 index 0000000..fd8ae23 Binary files /dev/null and b/db_institucion/Jesus Eduardo.jpg differ diff --git a/db_institucion/Jose Luis Tenchil.jpg b/db_institucion/Jose Luis Tenchil.jpg new file mode 100644 index 0000000..a5499a3 Binary files /dev/null and b/db_institucion/Jose Luis Tenchil.jpg differ diff --git a/db_institucion/Jose Luis.jpg b/db_institucion/Jose Luis.jpg new file mode 100644 index 0000000..955efd9 Binary files /dev/null and b/db_institucion/Jose Luis.jpg differ diff --git a/db_institucion/Jose Miguel Albarado.jpg b/db_institucion/Jose Miguel Albarado.jpg new file mode 100644 index 0000000..4f37708 Binary files /dev/null and b/db_institucion/Jose Miguel Albarado.jpg differ diff --git a/db_institucion/Josue Muñoz.jpg b/db_institucion/Josue Muñoz.jpg new file mode 100644 index 0000000..6341fa8 Binary files /dev/null and b/db_institucion/Josue Muñoz.jpg differ diff --git a/db_institucion/Judith.jpg b/db_institucion/Judith.jpg new file mode 100644 index 0000000..f25ebb8 Binary files /dev/null and b/db_institucion/Judith.jpg differ diff --git a/db_institucion/Leticia.jpg b/db_institucion/Leticia.jpg new file mode 100644 index 0000000..f7e5e1d Binary files /dev/null and b/db_institucion/Leticia.jpg differ diff --git a/db_institucion/Martha.jpg b/db_institucion/Martha.jpg new file mode 100644 index 0000000..df06433 Binary files /dev/null and b/db_institucion/Martha.jpg differ diff --git a/db_institucion/Martha_1.jpg b/db_institucion/Martha_1.jpg new file mode 100644 index 0000000..f357e94 Binary files /dev/null and b/db_institucion/Martha_1.jpg differ diff --git a/db_institucion/Mauricio Aguilar.jpg b/db_institucion/Mauricio Aguilar.jpg new file mode 100644 index 0000000..1438a36 Binary files /dev/null and b/db_institucion/Mauricio Aguilar.jpg differ diff --git a/db_institucion/Miguel Angel Sanchez Papalotzi.jpg b/db_institucion/Miguel Angel Sanchez Papalotzi.jpg new file mode 100644 index 0000000..156fdc1 Binary files /dev/null and b/db_institucion/Miguel Angel Sanchez Papalotzi.jpg differ diff --git a/db_institucion/Miguel Angel.jpg b/db_institucion/Miguel Angel.jpg deleted file mode 100644 index 91f16e2..0000000 Binary files a/db_institucion/Miguel Angel.jpg and /dev/null differ diff --git a/db_institucion/Mileidy Perez.jpg b/db_institucion/Mileidy Perez.jpg new file mode 100644 index 0000000..07f9c10 Binary files /dev/null and b/db_institucion/Mileidy Perez.jpg differ diff --git a/db_institucion/Rafael Lopez Preza.jpg b/db_institucion/Rafael Lopez Preza.jpg new file mode 100644 index 0000000..4a5d4fc Binary files /dev/null and b/db_institucion/Rafael Lopez Preza.jpg differ diff --git a/db_institucion/Rafael.jpg b/db_institucion/Rafael Lopez Preza_1.jpg similarity index 100% rename from db_institucion/Rafael.jpg rename to db_institucion/Rafael Lopez Preza_1.jpg diff --git a/db_institucion/Raul Sanchez.jpg b/db_institucion/Raul Sanchez.jpg new file mode 100644 index 0000000..2bbf015 Binary files /dev/null and b/db_institucion/Raul Sanchez.jpg differ diff --git a/db_institucion/Sergio.jpg b/db_institucion/Sergio.jpg new file mode 100644 index 0000000..4e2bb59 Binary files /dev/null and b/db_institucion/Sergio.jpg differ diff --git a/db_institucion/Victor Manuel Ocampo.jpg b/db_institucion/Victor Manuel Ocampo.jpg new file mode 100644 index 0000000..25d6c7b Binary files /dev/null and b/db_institucion/Victor Manuel Ocampo.jpg differ diff --git a/db_institucion/Victor Manuel Ocampo Mendez.jpg b/db_institucion/Victor Manuel Ocampo_1.jpg similarity index 100% rename from db_institucion/Victor Manuel Ocampo Mendez.jpg rename to db_institucion/Victor Manuel Ocampo_1.jpg diff --git a/db_institucion/Vikicar.jpg b/db_institucion/Vikicar Aldana_1.jpg similarity index 100% rename from db_institucion/Vikicar.jpg rename to db_institucion/Vikicar Aldana_1.jpg diff --git a/db_institucion/Wendoly.jpg b/db_institucion/Wendoly.jpg new file mode 100644 index 0000000..d4cf2ed Binary files /dev/null and b/db_institucion/Wendoly.jpg differ diff --git a/db_institucion/Xayli Ximena_1.jpg b/db_institucion/Xayli Ximena_1.jpg new file mode 100644 index 0000000..a1a430a Binary files /dev/null and b/db_institucion/Xayli Ximena_1.jpg differ diff --git a/db_institucion/Ximena.jpg b/db_institucion/Ximena.jpg deleted file mode 100644 index ee7ba9f..0000000 Binary files a/db_institucion/Ximena.jpg and /dev/null differ diff --git a/db_institucion/Yoseimy.jpg b/db_institucion/Yoseimy.jpg new file mode 100644 index 0000000..8ccf450 Binary files /dev/null and b/db_institucion/Yoseimy.jpg differ diff --git a/db_institucion/lore.jpg b/db_institucion/lore.jpg new file mode 100644 index 0000000..33bc8e5 Binary files /dev/null and b/db_institucion/lore.jpg differ diff --git a/fision1.py b/fision1.py index fa97beb..278c3b7 100644 --- a/fision1.py +++ b/fision1.py @@ -1,9 +1,483 @@ import os +import cv2 +import numpy as np +import time +import threading +from queue import Queue +from ultralytics import YOLO +import warnings +import json +import math + +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +os.environ['CUDA_VISIBLE_DEVICES'] = '-1' +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|flags;low_delay" +os.environ['QT_LOGGING_RULES'] = '*=false' +os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' +warnings.filterwarnings("ignore") + +# 1. IMPORTAMOS LIBRERÍAS DE SMARTSOFT +import seguimiento2 +from seguimiento2 import GlobalMemory, CamManager, FUENTE, es_humano_valido +from reconocimiento import app, gestionar_vectores, buscar_mejor_match, hilo_bienvenida + +# 2. VARIABLES GLOBALES DE COMUNICACIÓN CON LA INTERFAZ +COLA_ROSTROS = Queue(maxsize=4) +IA_LOCK = threading.Lock() + +MOTOR_ACTIVO = False +PAGINA_ACTUAL = 0 +CAMS_POR_PANTALLA = 4 +CAMARA_MAXIMIZADA = None +TELEMETRIA = {'fps': 0.0, 'ids_activos': 0} +FRAME_MOSAICO = None +NUEVAS_ALERTAS = [] + +def obtener_ultimo_frame(): + global FRAME_MOSAICO + return FRAME_MOSAICO + +print("\nIniciando carga de base de datos biométrica...") +BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) + +def crear_mosaico(tiles): + if not tiles: return np.zeros((270, 480, 3), dtype=np.uint8) + n = len(tiles) + if n == 1: return tiles[0] + + cols = math.ceil(math.sqrt(n)) + rows = math.ceil(n / cols) + + h, w, c = tiles[0].shape + mosaico = np.zeros((rows * h, cols * w, c), dtype=np.uint8) + + for i, tile in enumerate(tiles): + r = i // cols + c_idx = i % cols + mosaico[r*h:(r+1)*h, c_idx*w:(c_idx+1)*w] = tile + + return mosaico + +def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): + try: + if roi_cabeza is None or roi_cabeza.size == 0: return + with IA_LOCK: faces = app.get(roi_cabeza) + if len(faces) == 0: return + + face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) + box = face.bbox.astype(int) + w_f, h_f = box[2] - box[0], box[3] - box[1] + + if w_f < 20 or h_f < 20: return + + emb = np.array(face.normed_embedding, dtype=np.float32) + genero_detectado = "Man" if face.sex == "M" else "Woman" + + h_roi, w_roi = roi_cabeza.shape[:2] + m_x, m_y_top, m_y_bot = int(w_f * 0.50), int(h_f * 0.50), int(h_f * 0.70) + y1, y2 = max(0, box[1] - m_y_top), min(h_roi, box[3] + m_y_bot) + x1, x2 = max(0, box[0] - m_x), min(w_roi, box[2] + m_x) + rostro_puro = roi_cabeza[y1:y2, x1:x2] + + area_rostro = w_f * h_f + es_mejor_rostro_camara = False + llave_camara = f"{gid}_cam{cam_id}" + + with global_mem.lock: + if not hasattr(global_mem, 'mejores_rostros'): global_mem.mejores_rostros = {} + mejor_area_actual = global_mem.mejores_rostros.get(llave_camara, {}).get('area', 0) + if area_rostro > mejor_area_actual: + global_mem.mejores_rostros[llave_camara] = {'area': area_rostro, 'ts': time.time()} + es_mejor_rostro_camara = True + + ruta_dir = os.path.join("cache_nombres", "auditoria_caras") + os.makedirs(ruta_dir, exist_ok=True) + fecha_hoy = time.strftime('%Y-%m-%d') + + if BASE_DATOS_ROSTROS: mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) + else: mejor_match, max_sim = None, 0.0 + + with global_mem.lock: + datos_id = global_mem.db.get(gid) + if not datos_id: return + nombre_actual = datos_id.get('nombre') + + # ───────────────────────────────────────────────────────────── + # ⚡ SISTEMA DE VOTACIÓN DUAL (Estricto para nuevos, Tolerante para VIPs) + # ───────────────────────────────────────────────────────────── + puntos_voto = 0 + es_vip_confirmado = (nombre_actual and nombre_actual != "Desconocido") + + if es_vip_confirmado: + # Si OSNet ya lo rastrea, somos muy tolerantes con los ángulos + if max_sim >= 0.40: puntos_voto = 3 + elif max_sim >= 0.25: puntos_voto = 2 + elif max_sim >= 0.18: puntos_voto = 1 + else: + # Si es un Desconocido, exigimos muchísima claridad para bautizarlo + if max_sim >= 0.50: puntos_voto = 3 + elif max_sim >= 0.42: puntos_voto = 2 + elif max_sim >= 0.38: puntos_voto = 1 + # Si saca 0.31 (como tu falso Jesus Eduardo), saca 0 puntos y se va a Alertas ROJAS. + + # 🧠 SISTEMA DE AUTO-APRENDIZAJE (Doble Llave) + if es_vip_confirmado and mejor_match: + nombre_limpio = mejor_match.split('_')[0] + if nombre_actual == nombre_limpio and 0.18 <= max_sim < 0.45: + ahora = time.time() + if (ahora - datos_id.get('ultimo_aprendizaje', 0)) > 10.0 and rostro_puro.size > 0: + ruta_ap = "db_aprendida" + os.makedirs(ruta_ap, exist_ok=True) + nombre_archivo = f"{nombre_limpio}_auto_{int(ahora)}.jpg" + cv2.imwrite(os.path.join(ruta_ap, nombre_archivo), rostro_puro) + BASE_DATOS_ROSTROS[nombre_archivo] = emb + datos_id['ultimo_aprendizaje'] = ahora + print(f" 🧠 [APRENDIZAJE] Cam {cam_id}: Nuevo ángulo de {nombre_limpio} aprendido! (Sim: {max_sim:.2f})") + puntos_voto = 3 + + # --- CASO A: LA IA DUDA --- + if puntos_voto == 0 or not mejor_match: + if nombre_actual and nombre_actual != "Desconocido": + datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) - 1 + if datos_id['votos_nombre'] <= 0: + datos_id['nombre'] = "Desconocido" + datos_id['votos_nombre'] = 0 + print(f" [CORRECCIÓN] IA duda del ID {gid}. Pierde el nombre '{nombre_actual}'.") + else: + datos_id['nombre'] = "Desconocido" + if es_mejor_rostro_camara and rostro_puro.size > 0: + cv2.imwrite(os.path.join(ruta_dir, f"{gid}_cam{cam_id}_{fecha_hoy}.jpg"), rostro_puro) + import fision1 + fision1.NUEVAS_ALERTAS.append((gid, cam_id, rostro_puro)) + return + + nombre_limpio = mejor_match.split('_')[0] + + # --- CASO B: CONFLICTO (Evita que alguien más robe tu nombre) --- + if nombre_actual and nombre_actual != "Desconocido" and nombre_actual != nombre_limpio: + datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) - puntos_voto + if datos_id['votos_nombre'] <= 0: + datos_id['nombre'] = "Desconocido" + datos_id['votos_nombre'] = 0 + print(f" [CORRECCIÓN] Conflicto facial en ID {gid}. Revertido a Desconocido.") + return + + # --- CASO C: CONFIRMACIÓN Y BAUTIZO --- + if es_mejor_rostro_camara and rostro_puro.size > 0: + cv2.imwrite(os.path.join(ruta_dir, f"{nombre_limpio}_cam{cam_id}_{fecha_hoy}.jpg"), rostro_puro) + + if nombre_actual == nombre_limpio: + datos_id['votos_nombre'] = min(5, datos_id.get('votos_nombre', 0) + puntos_voto) + return + + votos = datos_id.get('votos_nombre', 0) + puntos_voto + datos_id['votos_nombre'] = votos + + if votos >= 3: + id_veterano = None + for gid_mem, data_mem in global_mem.db.items(): + if data_mem.get('nombre') == nombre_limpio and gid_mem != gid: + id_veterano = gid_mem + break + + if id_veterano is not None: + print(f" [FUSIÓN] ArcFace une clon {gid} al original {nombre_limpio} (ID {id_veterano}).") + datos_id['fusionado_con'] = id_veterano + trk.gid = id_veterano + global_mem.db[id_veterano]['ts'] = time.time() + else: + datos_id['nombre'] = nombre_limpio + print(f" [BAUTIZO VIP] Cam {cam_id}: ID {gid} es {nombre_limpio} ({max_sim*100:.1f}%)") + + threading.Thread( + target=global_mem.registrar_movimiento, + args=(nombre_limpio, cam_id, cam_id, 0.0, gid), + daemon=True + ).start() + + if str(cam_id) == "7": + ahora = time.time() + info_saludo = global_mem.ultimos_saludos.get(nombre_limpio, {}) + if (ahora - info_saludo.get('timestamp', 0)) > 30: + global_mem.guardar_saludo(nombre_limpio) + threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_detectado), daemon=True).start() + + except Exception as e: + print(f" [ERROR ARCFACE] ID {gid}: {str(e)}") + finally: + trk.procesando_rostro = False + +def worker_rostros(global_mem): + while True: + roi_cabeza, gid, cam_id, trk = COLA_ROSTROS.get() + procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk) + COLA_ROSTROS.task_done() + +class CamStream: + def __init__(self, url, cam_id): + self.url = url + self.cam_id = str(cam_id) + self.cap = cv2.VideoCapture(url) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + self.q = Queue(maxsize=1) + self.stopped = False + self.is_alive = True + self.fallos = 0 + + # Pantalla de muerte con el número de cámara + self.frame_error = np.zeros((270, 480, 3), dtype=np.uint8) + cv2.putText(self.frame_error, f"CAM {self.cam_id} OFFLINE", (110, 135), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) + + threading.Thread(target=self._run, daemon=True).start() + + def _run(self): + while not self.stopped: + ret, f = self.cap.read() + if not ret: + if self.stopped: break + self.is_alive = False + self.fallos += 1 + + # Liberamos el socket inmediatamente para que el DVR respire + if self.cap: self.cap.release() + + wait_time = min(30, 2 ** self.fallos) + print(f" [ALERTA] Cam {self.cam_id} caída. Reintentando en {wait_time}s...") + + # Espera fraccionada para no bloquear la orden de cierre del usuario + for _ in range(wait_time * 10): + if self.stopped: break + time.sleep(0.1) + + if self.stopped: break + + self.cap = cv2.VideoCapture(self.url) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + continue + + if self.fallos > 0: + print(f" [INFO] Cam {self.cam_id} RECUPERADA exitosamente.") + + self.fallos = 0 + self.is_alive = True + + if self.q.full(): + try: self.q.get_nowait() + except Exception: pass + self.q.put(f) + + # La memoria C++ se libera DENTRO de su propio hilo. + if self.cap: + self.cap.release() + + @property + def frame(self): + if not self.is_alive: return self.frame_error.copy() + try: return self.q.get(timeout=0.5) + except Exception: return self.frame_error.copy() + + def stop(self): + self.stopped = True + +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}" + # ⚡ AQUÍ ESTÁ EL NARANJA DE REGRESO + elif getattr(trk, 'origen_global', False): color, label = (0, 165, 255), f"ID:{trk.gid} [re-id]" + elif getattr(trk, 'en_grupo', False): color, label = (0, 0, 255), f"ID:{trk.gid} [grp]" + 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(): + # ⚡ FIX: RESTAURADOS LOS NOMBRES ORIGINALES (FRAME_MOSAICO) PARA QUE LA INTERFAZ RECIBA EL VIDEO + global MOTOR_ACTIVO, PAGINA_ACTUAL, CAMS_POR_PANTALLA, FRAME_MOSAICO, CAMARA_MAXIMIZADA, TELEMETRIA + print("\n=== Motor IA Iniciado (Control fision1) ===") + + config_data = {} + if os.path.exists("config.json"): + try: + with open("config.json", 'r') as f: config_data = json.load(f) + except Exception: pass + + ip_dvr = config_data.get("dvr_ip", "192.168.1.244") + usr = config_data.get("dvr_user", "admin") + pwd = config_data.get("dvr_pass", "TCA200503") + sec_str = config_data.get("secuencia_total", "2, 1, 7, 5, 8, 4, 3, 6") + ign_str = config_data.get("ignoradas", "2, 4") + + SECUENCIA_TOTAL = [x.strip() for x in sec_str.split(",") if x.strip()] + IGNORADAS = [x.strip() for x in ign_str.split(",") if x.strip()] + URLS = [f"rtsp://{usr}:{pwd}@{ip_dvr}:554/Streaming/Channels/{c}02" for c in SECUENCIA_TOTAL] + + camaras_activas = [c for c in SECUENCIA_TOTAL if c not in IGNORADAS] + + model = YOLO("yolov8n-pose.pt") + global_mem = GlobalMemory() + managers = {str(c): CamManager(c, global_mem) for c in camaras_activas} + + # ⚡ FIX: Enviamos el URL y el cam_id al CamStream modificado + cams = [CamStream(u, c) for u, c in zip(URLS, SECUENCIA_TOTAL)] + + threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start() + + idx = 0 + ultimo_guardado = time.time() + ultimo_auto_merge = time.time() + MOTOR_ACTIVO = True + + try: + while MOTOR_ACTIVO: + now = time.time() + tiles_pagina_actual = [] + cam_ia_actual = camaras_activas[idx % len(camaras_activas)] if camaras_activas else None + + try: cams_pantalla = int(CAMS_POR_PANTALLA) + except: cams_pantalla = 4 + inicio_slice = PAGINA_ACTUAL * cams_pantalla + fin_slice = inicio_slice + cams_pantalla + + for i, cam_obj in enumerate(cams): + cid = str(SECUENCIA_TOTAL[i]) + frame = cam_obj.frame + + if frame is None or not getattr(cam_obj, 'is_alive', True): + frame_err = np.zeros((270, 480, 3), np.uint8) + cv2.putText(frame_err, f"CAM {cid} OFFLINE", (120, 135), FUENTE, 1.0, (0,0,255), 2) + if inicio_slice <= i < fin_slice: tiles_pagina_actual.append(frame_err) + continue + + frame_show = cv2.resize(frame.copy(), (480, 270)) + + if cid in IGNORADAS: + cv2.putText(frame_show, f"CAM {cid} [MODO VISOR - Sin IA]", (10, 28), FUENTE, 0.7, (150, 150, 150), 2) + if inicio_slice <= i < fin_slice: tiles_pagina_actual.append(frame_show) + continue + + boxes, confs, keypoints = [], [], [] + turno_activo = (cid == cam_ia_actual) + + if turno_activo: + res = model.predict(frame_show, conf=0.45, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu') + if res[0].boxes: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + if es_humano_valido(kpt_persona, box, conf): + boxes.append(box) + confs.append(conf) + keypoints.append(kpt_persona) + + tracks = managers[cid].update(boxes, confs, keypoints, frame_show, frame, now, turno_activo) + + for trk in tracks: + if getattr(trk, 'time_since_update', 1) <= 2: + # ⚡ SOLUCIÓN PARTE 2B: El cuadro naranja durará 5 segundos reales + if trk.gid is not None: + with global_mem.lock: + ts_cruce = global_mem.db.get(trk.gid, {}).get('ts_cruce', 0) + if (now - ts_cruce) < 5.0: + trk.origen_global = True + else: + trk.origen_global = False + + dibujar_track_fusion(frame_show, trk, global_mem) + + # ⚡ SOLUCIÓN PARTE 1: GARANTIZAR EL ENCUADRE DEL ROSTRO + if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False): + ultimo_envio = getattr(trk, 'ultimo_rostro_enviado', 0) + if (now - ultimo_envio) > 0.7: + try: + x1, y1, x2, y2 = trk.box + h_r, w_r = frame.shape[:2] + e_x, e_y = w_r / 480.0, h_r / 270.0 + h_b, w_b = y2 - y1, x2 - x1 + + # Bajamos al 50% de la caja de YOLO para asegurar que entre la barbilla + # y ampliamos el margen horizontal a 25% por si la persona va girando. + y_exp = max(0, int(y1*e_y - h_b*e_y*0.20)) + y_cab = min(h_r, int(y1*e_y + h_b*e_y*0.50)) + x_izq = max(0, int(x1*e_x - w_b*e_x*0.25)) + x_der = min(w_r, int(x2*e_x + w_b*e_x*0.25)) + + roi = frame[y_exp:y_cab, x_izq:x_der].copy() + + # Bajamos la exigencia de 60x60 a 40x40 para atrapar rostros lejanos + if roi.size > 0 and roi.shape[0] >= 40 and roi.shape[1] >= 40: + if not COLA_ROSTROS.full(): + trk.ultimo_rostro_enviado = now + trk.procesando_rostro = True + COLA_ROSTROS.put((roi, trk.gid, cid, trk), block=False) + # DEBUG OBLIGATORIO: Te avisa visualmente que sí capturó la foto y la mandó a la fila + print(f" [CÁMARA] Cam {cid} envió foto de ID {trk.gid} a la IA Facial.") + except Exception as e: + print(f" [ERROR DE RECORTE] {e}") + + if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) + con_id = sum(1 for t in tracks if getattr(t, 'gid', None) and getattr(t, 'time_since_update', 1) == 0) + cv2.putText(frame_show, f"CAM {cid} [{con_id} ID]", (10, 28), FUENTE, 0.7, (255, 255, 255), 2) + + if inicio_slice <= i < fin_slice: + tiles_pagina_actual.append(frame_show) + + # ⚡ RENDERIZADO CORREGIDO: RESTAURADO "FRAME_MOSAICO" + if CAMARA_MAXIMIZADA is not None: + idx_cam = SECUENCIA_TOTAL.index(str(CAMARA_MAXIMIZADA)) if str(CAMARA_MAXIMIZADA) in SECUENCIA_TOTAL else 0 + frame_max = cams[idx_cam].frame + if frame_max is not None: + mosaico = cv2.resize(frame_max, (1280, 720)) + cv2.putText(mosaico, f"MODO ZOOM - CAM {CAMARA_MAXIMIZADA}", (20, 40), FUENTE, 1, (0,255,0), 3) + FRAME_MOSAICO = mosaico + else: + FRAME_MOSAICO = crear_mosaico(tiles_pagina_actual) + else: + FRAME_MOSAICO = crear_mosaico(tiles_pagina_actual) + + tiempo_ciclo = time.time() - now + fps_actual = 1.0 / (tiempo_ciclo + 1e-6) + TELEMETRIA['fps'] = (TELEMETRIA['fps'] * 0.9) + (fps_actual * 0.1) + TELEMETRIA['ids_activos'] = len(global_mem.db) + + if now - ultimo_auto_merge > 2.0: + global_mem.consolidar_clones() + ultimo_auto_merge = now + + if now - ultimo_guardado > 30: + global_mem.guardar_memoria() + ultimo_guardado = now + + idx += 1 + time.sleep(0.01) + + finally: + print("Apagando conexiones de cámara de forma segura...") + for cam in cams: + cam.stop() + global_mem.guardar_memoria() + print("Motor IA Detenido y Memoria Guardada.") + + +"""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" -# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de OpenCV y Qt +# Silenciamos a OpenCV y Qt os.environ['QT_LOGGING_RULES'] = '*=false' os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' import cv2 @@ -14,23 +488,21 @@ from queue import Queue from ultralytics import YOLO import warnings import json +import math warnings.filterwarnings("ignore") # ────────────────────────────────────────────────────────────────────────────── # 1. IMPORTAMOS NUESTROS MÓDULOS # ────────────────────────────────────────────────────────────────────────────── -# Del motor matemático y tracking -from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE, similitud_hibrida +import seguimiento2 +from seguimiento2 import GlobalMemory, CamManager, FUENTE, es_humano_valido -# ⚡ Del motor de reconocimiento facial from reconocimiento import ( app, gestionar_vectores, buscar_mejor_match, - hilo_bienvenida, - UMBRAL_SIM, - COOLDOWN_TIME + hilo_bienvenida ) # ────────────────────────────────────────────────────────────────────────────── @@ -39,9 +511,45 @@ from reconocimiento import ( COLA_ROSTROS = Queue(maxsize=4) IA_LOCK = threading.Lock() +PAGINA_ACTUAL = 0 +CAMS_POR_PANTALLA = 4 +CAMARA_MAXIMIZADA = None +TELEMETRIA = {'fps': 0.0, 'ids_activos': 0} +FRAME_MOSAICO = None + +def obtener_ultimo_frame(): + global FRAME_MOSAICO + return FRAME_MOSAICO + print("\nIniciando carga de base de datos...") BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) +# ────────────────────────────────────────────────────────────────────────────── +# FUNCIÓN: MOSAICO DINÁMICO +# ────────────────────────────────────────────────────────────────────────────── +def crear_mosaico(tiles): + if not tiles: return None + n = len(tiles) + if n == 1: return tiles[0] + + if n <= 4: cols, rows = 2, 2 + elif n <= 6: cols, rows = 3, 2 + elif n <= 8: cols, rows = 4, 2 + elif n <= 9: cols, rows = 3, 3 + else: + cols = math.ceil(math.sqrt(n)) + rows = math.ceil(n / cols) + + h, w, c = tiles[0].shape + mosaico = np.zeros((rows * h, cols * w, c), dtype=np.uint8) + + for i, tile in enumerate(tiles): + r = i // cols + c_idx = i % cols + mosaico[r*h:(r+1)*h, c_idx*w:(c_idx+1)*w] = tile + + return mosaico + # ────────────────────────────────────────────────────────────────────────────── # 3. MOTOR ASÍNCRONO CON INSIGHTFACE # ────────────────────────────────────────────────────────────────────────────── @@ -52,30 +560,25 @@ def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): with IA_LOCK: faces = app.get(roi_cabeza) - if len(faces) == 0: - return + if len(faces) == 0: return face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) + w_rostro, h_rostro = face.bbox[2] - face.bbox[0], face.bbox[3] - face.bbox[1] - w_rostro = face.bbox[2] - face.bbox[0] - h_rostro = face.bbox[3] - face.bbox[1] - - # ⚡ Límite bajo (20px) para reconocer desde más lejos - if w_rostro < 20 or h_rostro < 20 or (w_rostro / h_rostro) < 0.35: + if w_rostro < 25 or h_rostro < 25 or (w_rostro / h_rostro) < 0.30: return emb = np.array(face.normed_embedding, dtype=np.float32) genero_detectado = "Man" if face.sex == "M" else "Woman" mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) + + if max_sim < 0.23: + return + print(f"[DEBUG CAM {cam_id}] InsightFace: {mejor_match} al {max_sim:.2f}") - votos_finales = 0 - - # ────────────────────────────────────────────────────────────────── - # ⚡ LÓGICA DE VOTACIÓN PONDERADA (Con tus umbrales) - # ────────────────────────────────────────────────────────────────── - if max_sim >= 0.18 and mejor_match: + if max_sim >= 0.23 and mejor_match: nombre_limpio = mejor_match.split('_')[0] id_definitivo = gid @@ -86,81 +589,95 @@ def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): dueno_actual = datos_id.get('nombre') candidato_actual = datos_id.get('candidato_nombre') - # ⚡ REBELIÓN DE IDENTIDAD: - if max_sim >= 0.56 and dueno_actual is not None and dueno_actual != nombre_limpio: - print(f"[REBELIÓN] Cara de {nombre_limpio} clarísima ({max_sim:.2f}). Destronando a {dueno_actual}.") - datos_id['candidato_nombre'] = nombre_limpio - datos_id['votos_nombre'] = 3 - else: - if candidato_actual == nombre_limpio: - datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1 + if max_sim >= 0.42: puntos = 3 + elif max_sim >= 0.30: puntos = 2 + elif max_sim >= 0.23: puntos = 1 + else: puntos = 0 + + if puntos == 0: return + + if dueno_actual is not None and dueno_actual != nombre_limpio: + datos_id['dudas_identidad'] = datos_id.get('dudas_identidad', 0) + puntos + strikes = datos_id['dudas_identidad'] + + if strikes >= 2: + print(f"[REBELIÓN] 2 Strikes superados en Cam {cam_id}. Destronando a {dueno_actual} a favor de {nombre_limpio}.") + datos_id['nombre'] = None + datos_id['firma_ema'] = None + dueno_actual = None else: + print(f"[DUDA] Strike 1 en Cam {cam_id}: Se parece a {nombre_limpio}. Vigilando de cerca a {dueno_actual}.") datos_id['candidato_nombre'] = nombre_limpio datos_id['votos_nombre'] = 1 + return - if max_sim >= 0.50: - datos_id['votos_nombre'] += 3 - elif max_sim >= 0.40: - datos_id['votos_nombre'] += 2 + datos_id['dudas_identidad'] = 0 + if candidato_actual == nombre_limpio: + datos_id['votos_nombre'] += puntos + else: + datos_id['candidato_nombre'] = nombre_limpio + datos_id['votos_nombre'] = puntos + votos_finales = datos_id['votos_nombre'] - # ⚡ Exigimos 3 votos - if votos_finales >= 3: - # 1. Buscamos al veterano + if votos_finales >= 2: + + clon_activo = False + for gid_activo, data_activo in global_mem.db.items(): + if gid_activo != gid and data_activo.get('nombre') == nombre_limpio: + if (time.time() - data_activo.get('ts', 0)) < 2.0: + print(f"[BLOQUEO BAUTIZO] Cam {cam_id}: {nombre_limpio} ya está activo en el ID {gid_activo}.") + clon_activo = True + break + if clon_activo: + return + id_veterano = None + sim_cuerpo = 0.0 + from scipy.spatial.distance import cosine + for gid_mem, data_mem in global_mem.db.items(): if data_mem.get('nombre') == nombre_limpio and gid_mem != gid: id_veterano = gid_mem + ema_vet = data_mem.get('ema') + ema_act = datos_id.get('ema') + if ema_vet and ema_act and 'deep' in ema_vet and 'deep' in ema_act: + sim_cuerpo = 1.0 - cosine(ema_act['deep'], ema_vet['deep']) break - # 2. FUSIÓN MÁGICA if id_veterano is not None: - print(f" [FUSIÓN FACIAL] ID {gid} es clon. Fusionando con veterano ID {id_veterano} ({nombre_limpio}).") - global_mem.db[id_veterano]['firmas'].extend(datos_id.get('firmas', [])) - if len(global_mem.db[id_veterano]['firmas']) > 15: - global_mem.db[id_veterano]['firmas'] = global_mem.db[id_veterano]['firmas'][-15:] + if sim_cuerpo < 0.60: + print(f" [FALSO POSITIVO] Cam {cam_id}: InsightFace detectó a {nombre_limpio}, pero su cuerpo NO coincide (Sim: {sim_cuerpo:.2f}). Rechazado.") + datos_id['votos_nombre'] = 0 + datos_id['candidato_nombre'] = None + return + else: + print(f" [FUSIÓN FACIAL] Cam {cam_id}: Asignando la identidad de {nombre_limpio} (ID {id_veterano}) al tracker actual (Sim: {sim_cuerpo:.2f}).") + datos_id['fusionado_con'] = id_veterano + trk.gid = id_veterano + id_definitivo = id_veterano - datos_id['firmas'] = [] - datos_id['fusionado_con'] = id_veterano - trk.gid = id_veterano - id_definitivo = id_veterano - - # 3. BAUTIZO NORMAL O CORRECCIÓN else: - if dueno_actual is not None and dueno_actual != nombre_limpio: - if max_sim < 0.56: - print(f"[RECHAZO VIP] {dueno_actual} protegido de {nombre_limpio} ({max_sim:.2f})") - return - else: - print(f" [CORRECCIÓN VIP] Renombrando a {nombre_limpio} ({max_sim:.2f})") - - if dueno_actual != nombre_limpio: + if dueno_actual is None: datos_id['nombre'] = nombre_limpio - print(f"[BAUTIZO] ID {gid} confirmado como {nombre_limpio}") + datos_id['confianza_bautizo'] = max_sim * 100 + datos_id['intentos_bautizo'] = votos_finales + print(f"[BAUTIZO] Cam {cam_id}: ID {gid} confirmado como {nombre_limpio} ({max_sim*100:.1f}%)") global_mem.db[id_definitivo]['ts'] = time.time() - # ────────────────────────────────────────────────────────────────── - # 🔊 AUDIO DE BIENVENIDA (DISPARADOR CORREGIDO) - # ────────────────────────────────────────────────────────────────── - # Verificamos si es la cámara 7 y si la persona actual ya tiene nombre confirmado if str(cam_id) == "7" and nombre_limpio: ahora = time.time() - TIEMPO_COOLDOWN = 60 # 1 minuto para pruebas + TIEMPO_COOLDOWN = 30 - # Revisamos el historial de saludos en la memoria global (que ya carga el JSON) info_saludo = global_mem.ultimos_saludos.get(nombre_limpio, {}) ultimo_ts = info_saludo.get('timestamp', 0) if isinstance(info_saludo, dict) else 0 if (ahora - ultimo_ts) > TIEMPO_COOLDOWN: - # 1. Bloqueamos inmediatamente para evitar repeticiones global_mem.guardar_saludo(nombre_limpio) - - # 2. Determinar género genero_oficial = genero_detectado try: - import json, os ruta_gen = os.path.join("cache_nombres", "generos.json") if os.path.exists(ruta_gen): with open(ruta_gen, 'r') as f: @@ -169,15 +686,10 @@ def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): genero_oficial = dic_gen[nombre_limpio] except Exception: pass - print(f"📢 [AUDIO] {nombre_limpio} reconocido en Cam 7. Saludando...") - import threading + print(f" [AUDIO] Cam {cam_id}: {nombre_limpio} reconocido. Saludando...") threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_oficial), daemon=True).start() - else: - # Opcional: print de debug para saber que el cooldown bloqueó el audio - pass - # Blindaje OSNet - if max_sim > 0.50 and votos_finales >= 3: + if max_sim > 0.50 and votos_finales >= 2: global_mem.confirmar_firma_vip(id_definitivo, time.time()) except Exception as e: @@ -192,24 +704,24 @@ def worker_rostros(global_mem): COLA_ROSTROS.task_done() # ────────────────────────────────────────────────────────────────────────────── -# 4. LOOP PRINCIPAL DE FUSIÓN +# 4. LECTURA DE VIDEO CONTINUA Y PURA # ────────────────────────────────────────────────────────────────────────────── class CamStream: def __init__(self, url): self.url = url self.cap = cv2.VideoCapture(url) - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + # Buffer ultra-pequeño para que siempre se lea el cuadro más fresco, evitando retrasos + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) self.frame = None threading.Thread(target=self._run, daemon=True).start() def _run(self): while True: ret, f = self.cap.read() - if ret: + if ret: self.frame = f - time.sleep(0.01) else: - time.sleep(2) + time.sleep(1) self.cap.open(self.url) def dibujar_track_fusion(frame_show, trk, global_mem): @@ -234,68 +746,127 @@ def dibujar_track_fusion(frame_show, trk, global_mem): 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") +def main(modo_interfaz=True): + print("\nIniciando Motor de IA...") + + config_data = {} + if os.path.exists("config.json"): + try: + with open("config.json", 'r') as f: config_data = json.load(f) + except Exception as e: print(f"Error cargando config: {e}") + + ip_dvr = config_data.get("dvr_ip", "192.168.1.244") + usr = config_data.get("dvr_user", "admin") + pwd = config_data.get("dvr_pass", "TCA200503") + sec_str = config_data.get("secuencia_total", "2, 1, 7, 5, 8, 4, 3, 6") + ign_str = config_data.get("ignoradas", "2, 4") + vecinos_str = config_data.get("vecinos", "{}") + + SECUENCIA_TOTAL = [x.strip() for x in sec_str.split(",") if x.strip()] + IGNORADAS = [x.strip() for x in ign_str.split(",") if x.strip()] + URLS = [f"rtsp://{usr}:{pwd}@{ip_dvr}:554/Streaming/Channels/{c}02" for c in SECUENCIA_TOTAL] + + try: seguimiento2.VECINOS = json.loads(vecinos_str) + except: pass + + camaras_activas = [c for c in SECUENCIA_TOTAL if c not in IGNORADAS] + + model = YOLO("yolov8n-pose.pt") global_mem = GlobalMemory() - managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} + + managers = {str(c): CamManager(c, global_mem) for c in camaras_activas} cams = [CamStream(u) for u in URLS] threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start() - cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) idx = 0 + ultimo_guardado = time.time() while True: now = time.time() tiles = [] - cam_ia = idx % len(cams) + cam_ia_actual = camaras_activas[idx % len(camaras_activas)] if camaras_activas else None + + # ⚡ 1. CALCULAMOS QUÉ CÁMARAS VAN EN ESTA PÁGINA + try: cams_pantalla = int(CAMS_POR_PANTALLA) + except: cams_pantalla = 4 + inicio_slice = PAGINA_ACTUAL * cams_pantalla + fin_slice = inicio_slice + cams_pantalla + + if now - ultimo_guardado > 15: + try: + global_mem.guardar_memoria() + ultimo_guardado = now + except: pass 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)) + cid = str(SECUENCIA_TOTAL[i]) + frame = cam_obj.frame + + if frame is None: + # Solo rellenamos de negro si la cámara apagada pertenece a la página actual + if inicio_slice <= i < fin_slice: + 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 cid in IGNORADAS: + cv2.putText(frame_show, f"CAM {cid} [MODO VISOR - Sin IA]", (10, 28), FUENTE, 0.7, (150, 150, 150), 2) + # ⚡ FILTRO: Solo agregar al mosaico si es de esta página + if inicio_slice <= i < fin_slice: + tiles.append(frame_show) + continue + + boxes, confs, keypoints = [], [], [] + turno_activo = (cid == cam_ia_actual) if turno_activo: - # ⚡ 1. Subimos la confianza de YOLO de 0.50 a 0.60 para matar siluetas dudosas - res = model.predict(frame_show, conf=0.60, iou=0.50, classes=[0], verbose=False, imgsz=480) - - if res[0].boxes: - cajas_crudas = res[0].boxes.xyxy.cpu().numpy().tolist() + res = model.predict(frame_show, conf=0.45, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu') + if res[0].boxes: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + if es_humano_valido(kpt_persona, box, conf): + boxes.append(box) + confs.append(conf) + keypoints.append(kpt_persona) + + ESQUELETO = [(0,1), (0,2), (1,3), (2,4), (5,6), (5,7), (7,9), (6,8), (8,10), + (5,11), (6,12), (11,12), (11,13), (13,15), (12,14), (14,16)] - # ⚡ 2. FILTRO BIOMECÁNICO (Anti-Sillas) - for b in cajas_crudas: - w_caja = b[2] - b[0] - h_caja = b[3] - b[1] - - # Un humano real es al menos 10% más alto que ancho (ratio > 1.1) - # Si la caja es muy cuadrada o ancha, la ignoramos y no se la pasamos a Kalman - if h_caja > (w_caja * 1.1): - boxes.append(b) + for persona_kpts in keypoints: + for union in ESQUELETO: + p1, p2 = persona_kpts[union[0]], persona_kpts[union[1]] + if p1[2] > 0.40 and p2[2] > 0.40: + pt1 = (int(p1[0]), int(p1[1])) + pt2 = (int(p2[0]), int(p2[1])) + cv2.line(frame_show, pt1, pt2, (255, 0, 255), 2) + for kx, ky, kconf in persona_kpts: + if kconf > 0.40: + cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1) - # ⚡ UPDATE LIMPIO (Sin IDs Globales) - tracks = managers[cid].update(boxes, frame_show, frame, now, turno_activo) + tracks = managers[cid].update(boxes, confs, keypoints, frame_show, frame, now, turno_activo) for trk in tracks: if getattr(trk, 'time_since_update', 1) <= 2: dibujar_track_fusion(frame_show, trk, global_mem) if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False): - with global_mem.lock: votos_actuales = global_mem.db.get(trk.gid, {}).get('votos_nombre', 0) - if votos_actuales >= 2: - continue + ultimo_envio = getattr(trk, 'ultimo_rostro_enviado', 0) + tiempo_espera = 1.3 if votos_actuales >= 3 else 0.5 + + if (now - ultimo_envio) < tiempo_espera: continue + x1, y1, x2, y2 = trk.box h_real, w_real = frame.shape[:2] - escala_x = w_real / 480.0 - escala_y = h_real / 270.0 + escala_x = w_real / 480.0; escala_y = h_real / 270.0 h_box = y2 - y1 y_exp = max(0, y1 - h_box * 0.15) @@ -306,134 +877,49 @@ def main(): int(max(0, x1) * escala_x) : int(min(480, x2) * escala_x) ].copy() - # ⚡ COMPUERTA ÓPTICA (Filtro Anti-Tartamudeo 25x25) - if roi_recortado.size > 0 and roi_recortado.shape[0] >= 25 and roi_recortado.shape[1] >= 25: + if roi_recortado.size > 0 and roi_recortado.shape[0] >= 30 and roi_recortado.shape[1] >= 30: gray = cv2.cvtColor(roi_recortado, cv2.COLOR_BGR2GRAY) nitidez = cv2.Laplacian(gray, cv2.CV_64F).var() - # ⚡ BAJAMOS DE 12.0 a 5.0. - # Deja pasar rostros un poco borrosos por la velocidad de caminar, - # pero sigue bloqueando espaldas lisas. - if nitidez > 8.0: - if not COLA_ROSTROS.full(): - ultimo_envio = getattr(trk, 'ultimo_rostro_enviado', 0) - - # Solo mandamos la cara si ha pasado medio segundo desde la última vez que lo intentamos - if (now - ultimo_envio) > 0.5: - - # Tu escudo anti-lag previo - if COLA_ROSTROS.qsize() < 2: - trk.ultimo_rostro_enviado = now # Registramos la hora del envío - trk.procesando_rostro = True # Bloqueamos el tracker - - # Aquí pones TU línea original de la cola: - COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False) + if nitidez > 12.0: + if not COLA_ROSTROS.full() and COLA_ROSTROS.qsize() < 2: + trk.ultimo_rostro_enviado = now + trk.procesando_rostro = True + COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False) else: trk.ultimo_intento_cara = time.time() - 0.5 - """if nitidez > 8.0: - - if not COLA_ROSTROS.full(): - - try: - - if COLA_ROSTROS.qsize() < 2: - - COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False) - - trk.procesando_rostro = True - - trk.ultimo_intento_cara = time.time() - - except queue.Full: - - pass - - else: - - trk.ultimo_intento_cara = time.time() - 0.5""" - 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 getattr(t, 'time_since_update', 1) == 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 - key = cv2.waitKey(1) & 0xFF - - if key == ord('q'): - break + # ⚡ FILTRO CRÍTICO: Solo añadimos el frame al mosaico si pertenece a la página activa + if inicio_slice <= i < fin_slice: + tiles.append(frame_show) - elif key == ord('r'): - print("\n[MODO REGISTRO] Escaneando mosaico para registrar...") - mejor_roi = None - max_area = 0 - face_metadata = None - - for i, cam_obj in enumerate(cams): - if cam_obj.frame is None: continue - - faces = app.get(cam_obj.frame) - for face in faces: - fw = face.bbox[2] - face.bbox[0] - fh = face.bbox[3] - face.bbox[1] - area = fw * fh - if area > max_area: - max_area = area - h_frame, w_frame = cam_obj.frame.shape[:2] - - m_x, m_y = int(fw * 0.30), int(fh * 0.30) - y1 = max(0, int(face.bbox[1]) - m_y) - y2 = min(h_frame, int(face.bbox[3]) + m_y) - x1 = max(0, int(face.bbox[0]) - m_x) - x2 = min(w_frame, int(face.bbox[2]) + m_x) - - mejor_roi = cam_obj.frame[y1:y2, x1:x2] - face_metadata = face - - if mejor_roi is not None and mejor_roi.size > 0: - cv2.imshow("Nueva Persona", mejor_roi) - cv2.waitKey(1) - - nom = input("Escribe el nombre de la persona: ").strip() - cv2.destroyWindow("Nueva Persona") - - if nom: - import json - genero_guardado = "Man" if face_metadata.sex == "M" else "Woman" - - ruta_generos = os.path.join("cache_nombres", "generos.json") - os.makedirs("cache_nombres", exist_ok=True) - 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) + # ⚡ 2. RENDERIZADO ASÍNCRONO DEL MOSAICO (Soporta Zoom y Paginación) + global FRAME_MOSAICO + if CAMARA_MAXIMIZADA is not None: + idx_cam = SECUENCIA_TOTAL.index(str(CAMARA_MAXIMIZADA)) if str(CAMARA_MAXIMIZADA) in SECUENCIA_TOTAL else 0 + frame_max = cams[idx_cam].frame + if frame_max is not None: + mosaico = cv2.resize(frame_max, (1280, 720)) + cv2.putText(mosaico, f"MODO ZOOM - CAM {CAMARA_MAXIMIZADA}", (20, 40), FUENTE, 1, (0,255,0), 3) + FRAME_MOSAICO = mosaico + else: + FRAME_MOSAICO = crear_mosaico(tiles) + else: + FRAME_MOSAICO = crear_mosaico(tiles) - ruta_db = "db_institucion" - os.makedirs(ruta_db, exist_ok=True) - cv2.imwrite(os.path.join(ruta_db, f"{nom}.jpg"), mejor_roi) - - print(f"[OK] Rostro guardado (Género autodetectado: {genero_guardado})") - print(" Sincronizando base de datos en caliente...") - - nuevos_vectores = gestionar_vectores(actualizar=True) - with IA_LOCK: - BASE_DATOS_ROSTROS.clear() - BASE_DATOS_ROSTROS.update(nuevos_vectores) - print(" Sistema listo.") - else: - print("[!] Registro cancelado.") + # ⚡ 3. CÁLCULO DE TELEMETRÍA PARA EL HUD DE LA INTERFAZ + tiempo_ciclo = time.time() - now + fps_actual = 1.0 / (tiempo_ciclo + 1e-6) + TELEMETRIA['fps'] = (TELEMETRIA['fps'] * 0.9) + (fps_actual * 0.1) + TELEMETRIA['ids_activos'] = len(global_mem.db) + + idx += 1 + time.sleep(0.01) if __name__ == "__main__": - main() \ No newline at end of file + main(modo_interfaz=False)""" \ No newline at end of file diff --git a/fusion.py b/fusion.py deleted file mode 100644 index 9878f7c..0000000 --- a/fusion.py +++ /dev/null @@ -1,432 +0,0 @@ -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 HD, aplica filtros físicos y votación biométrica """ - try: - if not BASE_DATOS_ROSTROS: return - - # ────────────────────────────────────────────────────────── - # 1. ESCALADO HD Y EXTRACCIÓN DE CABEZA (Solución Xayli) - # ────────────────────────────────────────────────────────── - h_real, w_real = frame_hd.shape[:2] - 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)) - # ⚡ 50% del cuerpo para no cortar cabezas de personas de menor estatura - y_max_cabeza = min(270, y_min + (h_box * 0.50)) - - 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] - - # ⚡ Filtro físico relajado a 40x40 - if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 20 or roi_cabeza.shape[1] < 20: - return - - h_roi, w_roi = roi_cabeza.shape[:2] - - # ────────────────────────────────────────────────────────── - # 2. DETECCIÓN YUNET Y FILTROS ANTI-BASURA - # ────────────────────────────────────────────────────────── - 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_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] < 20 or roi_rostro.shape[1] < 20: - continue - - # 🛡️ FILTRO ANTI-PERFIL: Evita falsos positivos de personas viendo de lado - ratio_aspecto = roi_rostro.shape[1] / float(roi_rostro.shape[0]) - if ratio_aspecto < 0.50: - continue - - # 🛡️ FILTRO ÓPTICO (Movimiento) - gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY) - nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() - if nitidez < 15.0: - continue - - # VISIÓN NOCTURNA (Simetría con Base de Datos) - try: - lab = cv2.cvtColor(roi_rostro, 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_rostro - - # ────────────────────────────────────────────────────────── - # 3. MOTOR MTCNN Y SISTEMA DE VOTACIÓN - # ────────────────────────────────────────────────────────── - with IA_LOCK: - try: - res = DeepFace.represent( - img_path=roi_mejorado, - model_name="ArcFace", - detector_backend="mtcnn", - align=True, - enforce_detection=True - ) - emb = np.array(res[0]["embedding"], dtype=np.float32) - mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) - except Exception: - continue # Cara inválida para MTCNN - - print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f}") - - if max_sim >= UMBRAL_SIM and mejor_match: - nombre_limpio = mejor_match.split('_')[0] - - with global_mem.lock: - datos_id = global_mem.db.get(gid) - if not datos_id: continue - - # SISTEMA DE VOTACIÓN (Anti-Falsos Positivos) - if datos_id.get('candidato_nombre') == nombre_limpio: - datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1 - else: - datos_id['candidato_nombre'] = nombre_limpio - datos_id['votos_nombre'] = 1 - - # ⚡ EL PASE VIP: Si la certeza es aplastante (>0.55), salta la burocracia - if max_sim >= 0.50: - datos_id['votos_nombre'] = max(2, datos_id['votos_nombre']) - - # Solo actuamos si tiene 2 votos consistentes... - if datos_id['votos_nombre'] >= 2: - nombre_actual = datos_id.get('nombre') - - # CANDADO DE BAUTIZO: Protege a los VIPs de alucinaciones borrosas - if nombre_actual is not None and nombre_actual != nombre_limpio: - if max_sim < 0.59: - # Si es un puntaje bajo, es una confusión de ArcFace. Lo ignoramos. - print(f" [RECHAZO] ArcFace intentó renombrar a {nombre_actual} como {nombre_limpio} con solo {max_sim:.2f}") - continue - else: - # Si el puntaje es masivo, significa que OSNet se equivocó y pegó 2 personas - print(f"[CORRECCIÓN VIP] OSNet se confundió. Renombrando a {nombre_limpio} ({max_sim:.2f})") - - # ⚡ BAUTIZO Y LIMPIEZA - if nombre_actual != nombre_limpio: - datos_id['nombre'] = nombre_limpio - print(f" [BAUTIZO] ID {gid} confirmado como {nombre_limpio}") - - ids_a_borrar = [] - firma_actual = datos_id['firmas'][0] if datos_id['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] - - # Actualizamos referencias - datos_id['area_rostro_ref'] = area_rostro_actual - datos_id['ts'] = time.time() - - # BLINDAJE VIP: Si la certeza es absoluta, amarrar la ropa a este ID - if max_sim > 0.65: - # Usamos la función externa para evitar bloqueos dobles (Deadlocks) - pass - - # 🔊 SALUDO DE BIENVENIDA - if str(cam_id) == "7" and necesita_saludo: - global_mem.ultimos_saludos[nombre_limpio] = time.time() - - import json - genero = "Man" # Valor por defecto seguro - ruta_generos = os.path.join("cache_nombres", "generos.json") - - if os.path.exists(ruta_generos): - try: - with open(ruta_generos, 'r') as f: - dic_generos = json.load(f) - genero = dic_generos.get(nombre_limpio, "Man") - except Exception: - pass # Si el archivo está ocupado, usamos el defecto - - # Lanzamos el audio instantáneamente sin IA pesada - threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero), daemon=True).start() - - # Ejecutamos el blindaje fuera del lock principal - if max_sim > 0.65 and datos_id.get('votos_nombre', 0) >= 2: - global_mem.confirmar_firma_vip(gid, time.time()) - - break # Salimos del loop de rostros si ya identificamos - 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, frame, 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 - - # ⚡ CAPTURAMOS LA TECLA EN UNA VARIABLE - # ⚡ CAPTURAMOS LA TECLA EN UNA VARIABLE - key = cv2.waitKey(1) & 0xFF - - if key == ord('q'): - break - - elif key == ord('r'): - print("\n[MODO REGISTRO] Escaneando mosaico para registrar...") - mejor_roi = None - max_area = 0 - - # ⚡ CORRECCIÓN: 'cams' es una lista, usamos enumerate - for i, cam_obj in enumerate(cams): - if cam_obj.frame is None: continue - - faces = detectar_rostros_yunet(cam_obj.frame) - for (fx, fy, fw, fh, score) in faces: - area = fw * fh - if area > max_area: - max_area = area - h_frame, w_frame = cam_obj.frame.shape[:2] - - # Margen amplio (30%) para MTCNN - 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_roi = cam_obj.frame[y1:y2, x1:x2] - - if mejor_roi is not None and mejor_roi.size > 0: - cv2.imshow("Nueva Persona", mejor_roi) - cv2.waitKey(1) - - nom = input("Escribe el nombre de la persona: ").strip() - cv2.destroyWindow("Nueva Persona") - - if nom: - import json - # 1. Pedimos el género para no usar IA en el futuro - gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower() - genero_guardado = "Woman" if gen_input == 'm' else "Man" - - # 2. Actualizamos el caché de géneros al instante - ruta_generos = os.path.join("cache_nombres", "generos.json") - os.makedirs("cache_nombres", exist_ok=True) - 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 - try: - with open(ruta_generos, 'w') as f: - json.dump(dic_generos, f) - except Exception as e: - print(f"[!] Error al guardar el género: {e}") - - # 3. Guardado tradicional de la foto - ruta_db = "db_institucion" - os.makedirs(ruta_db, exist_ok=True) - cv2.imwrite(os.path.join(ruta_db, f"{nom}.jpg"), mejor_roi) - - print(f"[OK] Rostro de '{nom}' guardado como {genero_guardado}.") - print(" Sincronizando base de datos en caliente...") - - # Al llamar a esta función, el sistema alineará la foto sin pisar nuestro JSON - nuevos_vectores = gestionar_vectores(actualizar=True) - BASE_DATOS_ROSTROS.clear() - BASE_DATOS_ROSTROS.update(nuevos_vectores) - print(" Sistema listo.") - else: - print("[!] Registro cancelado.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/generar_db_rostros.py b/generar_db_rostros.py deleted file mode 100644 index b0412a5..0000000 --- a/generar_db_rostros.py +++ /dev/null @@ -1,110 +0,0 @@ -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/in.py b/in.py new file mode 100644 index 0000000..358dd1e --- /dev/null +++ b/in.py @@ -0,0 +1,3054 @@ +#---------------------------------------------version + +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 +from datetime import datetime +import json +import csv +import math +import queue +from scipy.spatial.distance import cosine + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" +SECUENCIA = [1, 7, 5, 8, 3, 6] +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_5_msmt17_batch1.onnx" + +VECINOS = { + "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], + "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] +} + +TIEMPO_MAX_AUSENCIA = 900.0 +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 + +# ────────────────────────────────────────────────────────────────────────────── +# OSNET +# ────────────────────────────────────────────────────────────────────────────── +print("Cargando OSNet...") +try: + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + input_name = ort_session.get_inputs()[0].name + print("OSNet listo para CPU.") +except Exception as e: + print(f"ERROR FATAL: {e}"); exit() + +MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1) +STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1) + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRACCIÓN DE FIRMAS MULTI-DESCRIPTOR +# ────────────────────────────────────────────────────────────────────────────── +def extraer_features_osnet_seguro(rois): + """ + Procesamiento secuencial 1-a-1 de grado comercial. + Garantiza 0% de probabilidad de crasheo C++ en ONNX. + """ + if not rois: return [] + + features_norm = [] + for roi in rois: + # 1. Preprocesamiento matemático exacto para OSNet + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + + # Expandimos la dimensión para crear un Batch estricto de tamaño 1: (1, 3, 256, 128) + blob = np.expand_dims((img - MEAN) / STD, 0) + + # 2. INFERENCIA AISLADA (El motor ONNX no sufrirá estrés de memoria) + # El [0][0] extrae el vector ignorando la dimensión del batch + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + # 3. Normalización L2 (Vital para que funcione la distancia Coseno) + n = np.linalg.norm(df) + if n > 0: df /= n + + features_norm.append(df) + + return features_norm + +def calcular_nitidez(img): + if img is None or img.size == 0: return 0 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + return cv2.Laplacian(gray, cv2.CV_64F).var() + +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 + return np.expand_dims((img - MEAN) / STD, 0) + +SOPORTA_BATCH = None # Variable global para detectar compatibilidad ONNX + +def procesar_batch_osnet(rois): + """Procesa los recortes de forma segura y secuencial para evitar abortos C++ en ONNX""" + if not rois: return [] + + features_norm = [] + for roi in rois: + # Preprocesamiento individual + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + blob = np.expand_dims((img - MEAN) / STD, 0) + + # ⚡ Ejecución segura 1 a 1 (Garantiza que ONNX no colapse) + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + n = np.linalg.norm(df) + if n > 0: df /= n + features_norm.append(df) + + return features_norm + +def extraer_color_zonas(roi): + if roi is None or roi.size == 0 or roi.shape[0] < 30 or roi.shape[1] < 10: + return np.zeros(512 * 3, dtype=np.float32) + + h, w = roi.shape[:2] + + # ⚡ ALUCÍN OPTIMIZADO: LA MÁSCARA ELÍPTICA + # Creamos un lienzo negro y dibujamos un óvalo blanco en el centro. + # El histograma IGNORARÁ todo lo negro (la pared/fondo) y solo leerá lo blanco (el cuerpo). + mask = np.zeros((h, w), dtype=np.uint8) + centro_x, centro_y = int(w/2), int(h/2) + eje_x, eje_y = int(w * 0.35), int(h * 0.48) # Óvalo ajustado a proporciones humanas + cv2.ellipse(mask, (centro_x, centro_y), (eje_x, eje_y), 0, 0, 360, 255, -1) + + # ⚡ REGRESO AL HSV PURO (Sin CLAHE que adultere la luz) + hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) + + t1 = int(h * 0.30) + t2 = int(h * 0.65) + + # Cortamos la imagen Y la máscara en las 3 zonas (Cabeza, Torso, Piernas) + z1_img, z1_mask = hsv[:t1, :], mask[:t1, :] + z2_img, z2_mask = hsv[t1:t2, :], mask[t1:t2, :] + z3_img, z3_mask = hsv[t2:, :], mask[t2:, :] + + def calc_hist(img_part, mask_part): + if img_part.size == 0: return np.zeros(512, dtype=np.float32) + # Calculamos el color 3D (H,S,V) PERO pasándole la máscara para ignorar el fondo + hist = cv2.calcHist([img_part], [0, 1, 2], mask_part, [8, 8, 8], [0, 180, 0, 256, 0, 256]) + cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1) + return hist.flatten() + + h1 = calc_hist(z1_img, z1_mask) + h2 = calc_hist(z2_img, z2_mask) + h3 = calc_hist(z3_img, z3_mask) + + # Retorna 1536 dimensiones de color PURO y libre de paredes + return np.concatenate([h1, h2, h3]).astype(np.float32) + + +def extraer_proporciones_anatomicas(kpts): + if kpts is None or len(kpts) < 17: return None + kpts = np.array(kpts) + if kpts.shape[0] < 17: return None + + puntos = {i: kpts[i] for i in range(17)} + + if puntos[0][2] < 0.30: return None + if puntos[5][2] < 0.30 and puntos[6][2] < 0.30: return None + if puntos[11][2] < 0.30 and puntos[12][2] < 0.30: return None + + def dist(i, j): + if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0 + return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2])) + + hombros = dist(5, 6) + caderas = dist(11, 12) + torso = max(dist(5, 11), dist(6, 12)) + pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0 + pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0 + piernas = max(pierna_izq, pierna_der) + brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0 + brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0 + brazos = max(brazo_izq, brazo_der) + + altura = torso + piernas + if altura < 15.0: return None + + feats = np.array([ + hombros / max(altura, 1e-6), + caderas / max(altura, 1e-6), + torso / max(altura, 1e-6), + piernas / max(altura, 1e-6), + brazos / max(altura, 1e-6), + hombros / max(caderas, 1e-6), + piernas / max(torso, 1e-6), + ], dtype=np.float32) + + feats = np.clip(feats, 0.0, 3.0) + n = np.linalg.norm(feats) + if n > 0: feats /= n + return feats + +def es_humano_valido(kpts, box, conf): + if conf < 0.45: return False + if kpts is None or len(kpts) < 17: return False + + x1, y1, x2, y2 = box + w, h = x2 - x1, y2 - y1 + if h / max(w, 1) < 0.50 or h < 50: return False + + cabeza_segura = any(p[2] > 0.55 for p in kpts[:5]) + if not cabeza_segura: return False + + torso_seguro = any(p[2] > 0.40 for p in kpts[5:13]) + if not torso_seguro: return False + + return True + +def extraer_firma_hibrida(frame_hd, box_480, kpts=None, deep_feat=None): + try: + h_hd, w_hd = frame_hd.shape[:2] + x1, y1, x2, y2 = box_480 + + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: + return None + + score_nitidez = calcular_nitidez(roi) + + # ⚡ SOPORTE BATCH: Si no se entregó deep_feat masivo, lo calculamos individualmente (Fallback) + if deep_feat is None: + blob = preprocess_onnx(roi) + deep_feat = ort_session.run(None, {input_name: blob})[0][0].flatten() + n = np.linalg.norm(deep_feat) + if n > 0: deep_feat /= n + + color_f = extraer_color_zonas(roi) + anat_f = extraer_proporciones_anatomicas(kpts) + + ratio_hw = roi.shape[0] / max(roi.shape[1], 1) + calidad = (x2_c - x1_c) * (y2_c - y1_c) + + return { + 'deep': deep_feat, + 'color': color_f, # ⚡ ELIMINADO: Textura LBP + 'anatomia': anat_f, + 'ratio_hw': ratio_hw, + 'calidad': calidad, + 'nitidez': score_nitidez + } + except Exception as e: + return None + +def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=1.0): + if f1 is None or f2 is None: return 0.0 + + from scipy.spatial.distance import cosine + import numpy as np + + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is None or deep2 is None: return 0.0 + + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ───────────────────────────────────────────────────────────── + # ⚡ ECUALIZADOR CROSS-CAM (Domain Gap Fix) + # Compensamos la pérdida de puntaje causada por el cambio de iluminación. + # Evita que IDs viejos de la misma cámara ganen injustamente. + # ───────────────────────────────────────────────────────────── + if cross_cam and sim_deep > 0.50: + sim_deep += 0.05 + + # 1. ZONA DE CERTEZA ABSOLUTA + if sim_deep >= 0.70: + return min(1.0, sim_deep + 0.15) + + # 2. VÍA DE RECHAZO (No hay forma de que sea él) + if sim_deep < 0.45: + return sim_deep + + # 3. TRIBUNAL DE TEXTURAS Y ANATOMÍA + resultado_final = sim_deep + 0.05 + + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + if a1 is not None and a2 is not None and hasattr(a1, "shape") and a1.shape == a2.shape: + sim_anat = max(0.0, 1.0 - np.linalg.norm(a1 - a2)) + peso_anat = 0.12 * confianza_fisica + + if sim_anat > 0.88: + resultado_final += peso_anat + if sim_anat >= 0.95: resultado_final += 0.04 + elif sim_anat < 0.35: + resultado_final -= peso_anat + + else: + # PUNTOS CIEGOS (Solapamiento / Espaldas) + if sim_deep >= 0.50: + resultado_final += 0.12 + + return float(max(0.0, min(1.0, resultado_final))) + + +def desglose_similitud(f1, f2, cross_cam=False): + # ------------------------------------------------- + # 1. DEEP (OSNET) + # ------------------------------------------------- + sim_deep = 0.0 + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is not None and deep2 is not None and np.sum(deep1) > 0 and np.sum(deep2) > 0: + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ------------------------------------------------- + # 2. COLOR (2D HS INMUNE A SOMBRAS) + # ------------------------------------------------- + sim_color = -1.0 + c1, c2 = f1.get("color"), f2.get("color") + + # ⚡ CÓDIGO RESTAURADO A 1536 PARA LEER EL HSV PURO Y ENMASCARADO + if c1 is not None and c2 is not None and c1.shape == c2.shape and len(c1) == 1536: + if np.sum(c1) > 0.1 and np.sum(c2) > 0.1: + dist1 = cv2.compareHist(c1[:512].astype(np.float32), c2[:512].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist2 = cv2.compareHist(c1[512:1024].astype(np.float32), c2[512:1024].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist3 = cv2.compareHist(c1[1024:].astype(np.float32), c2[1024:].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + + s1 = max(0.0, 1.0 - float(dist1)) + s2 = max(0.0, 1.0 - float(dist2)) + s3 = max(0.0, 1.0 - float(dist3)) + + # Escudo chamarra abierta + if cross_cam and s3 > 0.60 and s2 < 0.35: + s2 = max(s2, s3 * 0.80) + + sim_color = (s1 * 0.20) + (s2 * 0.50) + (s3 * 0.30) + + # ------------------------------------------------- + # 3. ANATOMIA (7 Puntos) + # ------------------------------------------------- + sim_anat = -1.0 + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + + if a1 is not None and a2 is not None and hasattr(a1, "shape") and hasattr(a2, "shape") and a1.shape == a2.shape: + dist_a = np.linalg.norm(a1 - a2) + sim_anat = max(0.0, 1.0 - dist_a) + + return sim_deep, sim_color, sim_anat + +# ────────────────────────────────────────────────────────────────────────────── +# KALMAN TRACKER +# ────────────────────────────────────────────────────────────────────────────── +class KalmanTrack: + _count = 0 + + def __init__(self, box, now): + kf = cv2.KalmanFilter(7, 4) + 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) + kf.transitionMatrix = np.eye(7, dtype=np.float32) + kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1 + kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.01, 1.0, 2.0, 1.0]).astype(np.float32) + kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32) + kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32) + kf.statePost = np.zeros((7,1), np.float32) + kf.statePost[:4] = self._z(box) + self.kf = kf + + self.local_id = KalmanTrack._count; KalmanTrack._count += 1 + self.gid, self.origen_global, self.aprendiendo = None, False, False + self.box = list(box) + self.ts_creacion = 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.firma_pre_grupo = None + self.ts_salio_grupo = 0.0 + self.fallos_post_grupo = 0 + self.ultimo_aprendizaje = 0.0 + self.frames_continuos = 0 + self.frames_observados = 0 + self.firma_ema = None + self.muestras_capturadas = 0 + self.ultimo_cambio_id = 0.0 + + def _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/max(h,1e-6)]]).astype(np.float32) + + def _bbox(self, x): + cx, cy = float(x[0].item()), float(x[1].item()) + s, r = float(x[2].item()), float(x[3].item()) + w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6) + return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] + + @property + def confianza_fisica(self): + return min(1.0, self.frames_continuos / 20.0) + + def calcular_score_calidad(self, firma): + if firma is None: return 0.0 + score_nitidez = min(firma.get('nitidez', 0) / 100.0, 1.0) * 50.0 + score_area = min(firma.get('calidad', 0) / 12000.0, 1.0) * 50.0 + return score_nitidez + score_area + + def actualizar_ema(self, firma): + if firma is None: return + + self.muestras_capturadas = getattr(self, 'muestras_capturadas', 0) + 1 + score_nuevo = self.calcular_score_calidad(firma) + score_viejo = getattr(self, 'score_ema', 0.0) + + if self.firma_ema is None: + # Primera impresión (puede ser mala, pero es lo único que tenemos) + self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()} + self.score_ema = score_nuevo + return + + # ⚡ PROTECCIÓN DE MEMORIA (Anti-polución) + if self.muestras_capturadas > 4 and score_nuevo < (score_viejo * 0.60): + return + + # ───────────────────────────────────────────────────────────── + # ⚡ ASIMILACIÓN DINÁMICA (La cura a la mala primera impresión) + # ───────────────────────────────────────────────────────────── + # Si estamos en los primeros 5 frames, O la nueva foto es notoriamente mejor + # que nuestro mejor recuerdo (>20% mejor), somos una "esponja". + if self.muestras_capturadas <= 5 or score_nuevo > (score_viejo * 1.20): + # Alpha bajo = Sobrescribe agresivamente el pasado + alpha_deep = 0.20 # 20% vieja, 80% NUEVA + alpha_color = 0.20 + alpha_geo = 0.50 + self.score_ema = score_nuevo # Actualizamos nuestro estándar de calidad + else: + # Estado de madurez: La firma ya es excelente y estable. + # Los cambios deben ser muy lentos para no arruinarla. + alpha_deep = 0.85 # 85% vieja, 15% nueva + alpha_color = 0.50 + alpha_geo = 0.80 + self.score_ema = max(score_nuevo, score_viejo) + + ema = self.firma_ema + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + # Ya sin la textura LBP que eliminamos por rendimiento + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = alpha_geo * np.asarray(ema['anatomia']) + (1-alpha_geo) * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = max(ema.get('calidad', 0), firma['calidad']) + + def predict(self, turno_activo=True): + if self.time_since_update > 30: return None + + if getattr(self, 'en_grupo', False): + 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._bbox(self.kf.statePre) + return self.box + + if self.time_since_update > 0: + self.kf.statePost[4] *= 0.5 + self.kf.statePost[5] *= 0.5 + self.kf.statePost[6] = 0.0 + self.frames_continuos = max(0, self.frames_continuos - 1) + + 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._bbox(self.kf.statePre) + return self.box + + def update(self, box, en_grupo, now, kpts=None, firma_actual=None): + self.ts_ultima_deteccion = now + self.time_since_update, self.en_grupo = 0, en_grupo + self.box = list(box) + self.kf.correct(self._z(box)) + + if not en_grupo: + self.frames_observados += 1 + self.frames_continuos += 1 + self.listo_para_id = True + + +# ────────────────────────────────────────────────────────────────────────────── +# MEMORIA GLOBAL CON PERSISTENCIA +# ────────────────────────────────────────────────────────────────────────────── +class GlobalMemory: + def __init__(self): + self.db = {} + self.next_gid = 100 + self.lock = threading.RLock() + self.ruta_memoria = os.path.join("cache_nombres", "memoria_ids.json") + self.ruta_movimientos = os.path.join("cache_nombres", "registro_movimientos.csv") + os.makedirs("cache_nombres", exist_ok=True) + self.nombres_activos = {} + self.ultimos_saludos = {} + self._votos_nombre = {} + self._cargar_memoria() + self.reserva_temporal = {} + self.id_locks = {} + self.ultimas_detecciones = {} + + + def consolidar_clones(self): + """ + Rutina de Auto-Merge con Blindaje Biométrico y compensación de Domain Gap. + """ + from scipy.spatial.distance import cosine + + with self.lock: + ids_activos = list(self.db.keys()) + + for i in range(len(ids_activos)): + for j in range(i + 1, len(ids_activos)): + id1, id2 = ids_activos[i], ids_activos[j] + + if id1 not in self.db or id2 not in self.db: + continue + + ema1 = self.db[id1].get('ema') + ema2 = self.db[id2].get('ema') + if not ema1 or not ema2: continue + + deep1, deep2 = ema1.get('deep'), ema2.get('deep') + if deep1 is None or deep2 is None: continue + + # ⚡ BLINDAJE FACIAL: Respetar la autoridad de InsightFace + nom1 = self.db[id1].get('nombre') + nom2 = self.db[id2].get('nombre') + if nom1 and nom2 and nom1 != nom2: + continue # Nombres distintos = Personas distintas. Bloquear fusión. + + similitud = 1.0 - cosine(deep1, deep2) + + # ⚡ UMBRAL RELAJADO (0.76): Permite fusiones entre diferentes cámaras + if similitud > 0.76: + id_ganador = min(id1, id2) + id_clon = max(id1, id2) + + if self.db[id_clon].get('nombre'): + self.db[id_ganador]['nombre'] = self.db[id_clon]['nombre'] + + self.db[id_ganador]['ema']['deep'] = (self.db[id_ganador]['ema']['deep'] * 0.7) + (self.db[id_clon]['ema']['deep'] * 0.3) + + del self.db[id_clon] + print(f" [AUTO-MERGE] Fragmentación corregida. Clon ID {id_clon} absorbido por Original ID {id_ganador} (Sim: {similitud:.2f})") + + # Método 1: Registrar detección global + def registrar_deteccion_global(self, gid, cam_id, firma, now): + """Registra que un ID fue detectado en una cámara para coordinación solapada.""" + if firma is None or 'deep' not in firma: + return + + # Hash simple de la firma deep para comparación rápida + firma_hash = hash(firma['deep'].tobytes()) % 10000 + + self.ultimas_detecciones[gid] = { + 'cam': str(cam_id), + 'ts': now, + 'firma_hash': firma_hash + } + + # Limpiar detecciones antiguas (>10 segundos) + antiguas = [g for g, d in self.ultimas_detecciones.items() if now - d['ts'] > 10.0] + for g in antiguas: + del self.ultimas_detecciones[g] + + # Método 2: Buscar coincidencia solapada + def buscar_coincidencia_solapada(self, firma, cam_id, now, umbral_sim=0.85): + """Busca si esta detección podría ser un ID ya detectado en cámara vecina recientemente.""" + if firma is None or 'deep' not in firma: + return None + + firma_hash = hash(firma['deep'].tobytes()) % 10000 + vecinos = VECINOS.get(str(cam_id), []) + + for gid, info in self.ultimas_detecciones.items(): + # Solo considerar si fue detectado en cámara vecina hace <5s + if info['cam'] not in vecinos: + continue + if now - info['ts'] > 5.0: + continue + + # Si el hash coincide exactamente, es muy probable que sea el mismo + if info['firma_hash'] == firma_hash: + return gid + + # Si no, hacer comparación completa (más costosa) + ema = self.db.get(gid, {}).get('ema') + if ema is not None: + sim = similitud_hibrida(firma, ema, cross_cam=True) + if sim >= umbral_sim: + return gid + + return None + + def lock_id_for_camera(self, gid, cam_id, now, duration=15.0): + self.id_locks[gid] = { + 'cam': str(cam_id), + 'unlock_ts': now + duration, + 'nombre': self.db.get(gid, {}).get('nombre') + } + print(f" [ID LOCK] ID {gid} bloqueado en Cam{cam_id} por {duration}s") + + def is_id_locked(self, gid, cam_id, now): + if gid not in self.id_locks: + return False + lock = self.id_locks[gid] + if lock['cam'] != str(cam_id): + return False + if now < lock['unlock_ts']: + return True + else: + del self.id_locks[gid] + return False + + def limpiar_locks_vencidos(self, now): + vencidos = [gid for gid, lock in self.id_locks.items() if now >= lock['unlock_ts']] + for gid in vencidos: + del self.id_locks[gid] + + def registrar_salida(self, gid, cam_salida, now): + if gid not in self.db: return + + vecinas = VECINOS.get(str(cam_salida), []) + self.reserva_temporal[gid] = { + 'cam_salida': str(cam_salida), + 'ts_salida': now, + 'cam_esperadas': vecinas, + 'nombre': self.db[gid].get('nombre') + } + + reservas_a_limpiar = [] + for gid_reserva, info in self.reserva_temporal.items(): + if now - info['ts_salida'] > 30.0: + reservas_a_limpiar.append(gid_reserva) + + for gid_limpiar in reservas_a_limpiar: + del self.reserva_temporal[gid_limpiar] + + def _bonus_reserva(self, gid_evaluado, cam_actual, now): + return 0.0 + + + def _firma_a_dict(self, firma): + if not firma: return None + res = {} + for k, v in firma.items(): + if isinstance(v, np.ndarray): + res[k] = v.tolist() + elif k == 'anatomia' and v is not None: + res[k] = v.tolist() if isinstance(v, np.ndarray) else list(v) + else: + res[k] = v + return res + + def _dict_a_firma(self, d): + if not d: return None + res = {} + for k, v in d.items(): + if isinstance(v, list) and k in ['deep', 'color', 'textura', 'anatomia']: + res[k] = np.array(v, dtype=np.float32) + else: + res[k] = v + return res + + def _cargar_memoria(self): + if os.path.exists(self.ruta_memoria): + try: + with open(self.ruta_memoria, 'r') as f: + datos = json.load(f) + + ahora = time.time() + cargados_validos = 0 + + for gid_str, info in datos.items(): + ts_guardado = info.get('ts', 0) + + if ahora - ts_guardado > 900.0: + continue + + gid = int(gid_str) + self.db[gid] = { + 'ema': self._dict_a_firma(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'ts': ts_guardado, + 'nombre': info.get('nombre'), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._dict_a_firma(f) for f in info.get('firmas_altas', []) if f], + } + cargados_validos += 1 + + self.next_gid = max([int(k) for k in datos.keys()] + [99]) + 1 + print(f"[Memoria] {cargados_validos} IDs persistentes válidos cargados (Se purgaron los expirados).") + except Exception as e: + print(f"[Memoria] Error cargando: {e}") + + def guardar_memoria(self): + try: + datos = {} + for gid, info in self.db.items(): + datos[str(gid)] = { + 'ema': self._firma_a_dict(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'nombre': info.get('nombre'), + 'ts': info.get('ts', time.time()), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._firma_a_dict(f) for f in info.get('firmas_altas', [])], + } + with open(self.ruta_memoria, 'w') as f: + json.dump(datos, f) + except Exception as e: + print(f"[Memoria] Error guardando: {e}") + + def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion_seg): + nuevo = not os.path.exists(self.ruta_movimientos) + try: + with open(self.ruta_movimientos, 'a', newline='', encoding='utf-8') as f: + w = csv.writer(f) + if nuevo: w.writerow(["Fecha_Hora", "Nombre", "Origen", "Destino", "Seg_en_Origen"]) + w.writerow([datetime.now().strftime("%Y-%m-%d %H:%M:%S"), nombre, cam_origen, cam_destino, round(duracion_seg, 2)]) + except: pass + + def asignar_nombre(self, gid, nombre, confianza, es_fusion=False): + VOTOS_NOMBRE_MIN = 3 + UMBRAL_VOTO = 0.52 + UMBRAL_CONFIRMACION = 0.62 + + if confianza < (0.50 if es_fusion else UMBRAL_VOTO): + return False + + with self.lock: + rec = self.db.get(gid) + if not rec: return False + + nombre_viejo = rec.get('nombre') + + if not hasattr(self, 'nombres_activos'): + self.nombres_activos = {} + + gid_ocupante = self.nombres_activos.get(nombre) + if gid_ocupante is not None and gid_ocupante != gid: + if gid_ocupante in self.db: + ts_ultimo = self.db[gid_ocupante].get('ts', 0) + if (time.time() - ts_ultimo) < 10.0: + return False + + conf_actual = rec.get('confianza_nombre', 0.0) + if nombre_viejo == nombre and conf_actual > confianza: + return False + + if not hasattr(self, '_votos_nombre'): + self._votos_nombre = {} + + if gid not in self._votos_nombre: + self._votos_nombre[gid] = {} + + votos_gid = self._votos_nombre[gid] + + if any(n != nombre for n in votos_gid.keys()): + votos_gid.clear() + + if nombre not in votos_gid: + votos_gid[nombre] = [] + + votos_gid[nombre].append(confianza) + votos_gid[nombre] = votos_gid[nombre][-6:] + + n_votos = len(votos_gid[nombre]) + conf_media = sum(votos_gid[nombre]) / n_votos + + if n_votos < VOTOS_NOMBRE_MIN or conf_media < UMBRAL_CONFIRMACION: + return False + + if nombre_viejo is not None and nombre_viejo != nombre: + if confianza < 0.70: + print(f" [PROTECCIÓN] Rechazando cambio de {nombre_viejo} a {nombre} (conf={confianza:.2f} < 0.70)") + return False # Rechazar cambio automático + # Si confianza >= 0.70, permitir cambio pero con advertencia + print(f" [CAMBIO VIP] ID {gid} cambió de {nombre_viejo} a {nombre} (conf={confianza:.2f} >= 0.70)") + + rec['nombre'] = nombre + rec['confianza_nombre'] = conf_media + self.nombres_activos[nombre] = gid + + votos_gid.clear() + + tipo = "FUSIÓN" if es_fusion else "BAUTIZO" + print(f" [{tipo}] ID {gid} confirmado como {nombre} | Votos: {n_votos} | Conf. Media: {conf_media:.2f}") + return True + + def confirmar_firma_vip(self, gid, ts=None): + with self.lock: + rec = self.db.get(gid) + if rec and rec.get('ema') is not None: + # ⚡ FIX P2: Respetar la cristalización + if rec.get('cristalizado', False): + print(f" [MEMORIA] ID {gid} está cristalizado. No se añaden más firmas VIP.") + return + + firma_actual = rec['ema'] + if 'firmas_altas' not in rec: + rec['firmas_altas'] = [] + rec['firmas_altas'].append({ + k: v.copy() if isinstance(v, np.ndarray) else v + for k, v in firma_actual.items() + }) + if ts is not None: + rec['ts'] = ts + print(f" [MEMORIA] Firma de ID {gid} bloqueada y protegida como VIP.") + + def guardar_saludo(self, nombre): + self.ultimos_saludos[nombre] = {'timestamp': time.time()} + + def _actualizar_sin_lock(self, gid, firma, cam_id, now): + if gid not in self.db: + self.db[gid] = { + 'ema': firma, + 'last_cam': cam_id, + 'ts': now, + 'nombre': None, + 'actualizaciones_globales': 1, + 'cristalizado': False + } + return + + rec = self.db[gid] + + # ⚡ CRISTALIZACIÓN: Después de 15 actualizaciones, CONGELAR EMA + if not rec.get('cristalizado', False) and rec.get('actualizaciones_globales', 0) >= 15: + rec['cristalizado'] = True + print(f" [CRISTALIZADO] ID {gid} ha alcanzado madurez. EMA congelado permanentemente.") + + # ⚡ BLINDAJE DE CRISTALIZACIÓN + # Si el ID ya es maduro, actualizamos dónde fue visto por última vez, + # pero NUNCA permitimos que una cámara modifique su firma maestra perfecta. + if rec.get('cristalizado', False): + rec['last_cam'] = cam_id + rec['ts'] = now + rec['actualizaciones_globales'] += 1 + return + + ema = rec['ema'] + rec['actualizaciones_globales'] += 1 + + if ema is None: + rec['ema'] = firma + else: + alpha_deep = 0.85 + alpha_color = 0.50 + alpha_geo = 0.80 + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = 0.80 * np.asarray(ema['anatomia']) + 0.20 * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = firma['calidad'] + + rec['last_cam'] = cam_id + rec['ts'] = now + + def _sim_contra_firma(self, firma_nueva, firma_guardada, cross_cam, confianza_fisica): + if firma_guardada is None: return 0.0 + return similitud_hibrida(firma_nueva, firma_guardada, cross_cam, confianza_fisica) + + def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0): + ema = gid_data.get('ema') + if not ema: return 0.0 + + sim_ema = self._sim_contra_firma(firma_nueva, ema, cross_cam, confianza_fisica) + + if sim_ema > 0.75: + return sim_ema + + sim_anclas = [sim_ema] + for ancla in gid_data.get('firmas_altas', []): + sim_anclas.append(self._sim_contra_firma(firma_nueva, ancla, cross_cam, confianza_fisica)) + + mejor_ancla = max(sim_anclas[1:]) if len(sim_anclas) > 1 else sim_ema + return sim_ema * 0.60 + mejor_ancla * 0.40 + + def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0): + self.limpiar_fantasmas() + + with self.lock: + candidatos = [] + + for gid, data in self.db.items(): + dt = now - data.get('ts', now) + distancia = self._distancia_topologica(str(data['last_cam']), str(cam_id)) + + misma_cam = (distancia == 0) + es_vecino = (distancia == 1) + es_salto = (distancia == 2) + es_cross = not misma_cam + + if gid in active_gids: + if misma_cam or distancia >= 3: continue + if dt > TIEMPO_MAX_AUSENCIA or data.get('ema') is None: continue + if self.is_id_locked(gid, cam_id, now): continue + + if not misma_cam: + if (es_vecino and dt < 0.3) or (es_salto and dt < 2.0) or (distancia >= 3 and dt < 8.0): + continue + + # ⚡ CÁLCULO OBLIGATORIO DE SIMILITUD ANTES DE CUALQUIER FILTRO + sim = self._sim_robusta(firma, data, es_cross, confianza_fisica) + + sim_deep, sim_color, sim_anat = -1.0, -1.0, -1.0 + ema = data.get("ema") + if ema is not None: + try: + sim_deep, sim_color, sim_anat = desglose_similitud(firma, ema, cross_cam=es_cross) + except: + pass + + # Penalizaciones y ajustes + if not misma_cam: + # NUNCA bloqueamos cámaras vecinas (distancia 1) por tiempo, + # porque al estar solapadas, pueden ver a la persona en el mismo milisegundo (dt=0.0). + if es_salto and dt < 1.5: + continue # Mínimo 1.5s para saltar una cámara entera + if distancia >= 3 and dt < 5.0: + continue # Mínimo 5.0s para cruzar a la otra punta del edificio + + if firma_ema_local is not None: + sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica) + sim = 0.60 * sim + 0.40 * sim_local + + # ⚡ VALIDACIONES VIP + es_vip = data.get('nombre') is not None + tiene_firmas_altas = len(data.get('firmas_altas', [])) > 0 + + if es_vip and tiene_firmas_altas and sim < 0.78: + continue + elif es_vip and not misma_cam and sim < 0.75: + continue + + # ───────────────────────────────────────────────────────────── + # ⚡ UMBRALES CORREGIDOS (Ni parálisis, ni robo de identidad) + # ───────────────────────────────────────────────────────────── + penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002) + + if misma_cam: umbral = 0.68 + elif es_vecino: umbral = 0.72 + elif es_salto: umbral = 0.75 + else: umbral = 0.78 + + # AHORA SÍ: sim_anat y sim_deep existen + if sim_anat > 0.90 and sim_deep > 0.65: + umbral -= 0.04 + + if en_borde and not misma_cam: + umbral -= 0.04 + + umbral = max(0.65, umbral + penal_multitud) + + bonus_aplicado = min(self._bonus_reserva(gid, cam_id, now), 0.03) + sim += bonus_aplicado + + # ⚡ RESTAURAR LOGS DE EVAL PARA DIAGNÓSTICO + if sim >= umbral - 0.10 or len(candidatos) < 3: + estado = "ACEPTADO" if sim >= umbral else "RECHAZADO" + faltante = umbral - sim if sim < umbral else 0.0 + info = f"(Faltó {faltante:.2f})" if faltante > 0 else "" + bonus_str = f" [+{bonus_aplicado:.2f}]" if bonus_aplicado > 0 else "" + + # ⚡ FIX P0: Eliminada la llamada doble a desglose_similitud. + # Usamos sim_deep, sim_color y sim_anat pre-calculados. + color_str = f"{sim_color:.2f}" if sim_color >= 0 else "N/A" + anat_str = f"{sim_anat:.2f}" if sim_anat >= 0 else "N/A" + + print( + f"[EVAL] Cam{cam_id} evalúa ID{gid} " + f"(Cam{data['last_cam']}, dt={dt:.1f}s) | " + f"Deep={sim_deep:.2f} | Color={color_str} | Anat={anat_str} | " + f"Final={sim:.2f}{bonus_str} | Umb={umbral:.2f} {info} -> {estado}" + ) + + if sim >= umbral: + candidatos.append((sim, sim, gid)) + + firma_guardar = firma_ema_local if firma_ema_local is not None else firma + + if not candidatos: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + candidatos.sort(reverse=True) + best_sim, _, best_gid = candidatos[0] + + # ⚡ ANTI-GEMELOS RECALIBRADO: Solo duda si son casi idénticos (<0.02) + if len(candidatos) >= 2: + _, segunda_sim, segundo_gid = candidatos[1] + if abs(best_sim - segunda_sim) < 0.02 and best_sim < 0.75: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + if best_gid in self.reserva_temporal: del self.reserva_temporal[best_gid] + + self._actualizar_sin_lock(best_gid, firma_guardar, cam_id, now) + return best_gid, True + + def limpiar_fantasmas(self): + with self.lock: + ahora = time.time() + muertos = [] + for g, d in self.db.items(): + if d.get('nombre') is None and (ahora - d.get('ts', ahora)) > 600: + muertos.append(g) + elif d.get('nombre') is not None and (ahora - d.get('ts', ahora)) > 900: + muertos.append(g) + for g in muertos: + del self.db[g] + if muertos: + self.guardar_memoria() + + def _distancia_topologica(self, cam_o, cam_d): + if cam_o == cam_d: return 0 + vecinos_directos = VECINOS.get(cam_o, []) + if cam_d in vecinos_directos: return 1 + for v in vecinos_directos: + if cam_d in VECINOS.get(v, []): return 2 + return 3 + +# ────────────────────────────────────────────────────────────────────────────── +# GESTOR LOCAL +# ────────────────────────────────────────────────────────────────────────────── +def iou_overlap(A, B): + xA,yA = max(A[0],B[0]),max(A[1],B[1]) + xB,yB = min(A[2],B[2]),min(A[3],B[3]) + inter = max(0,xB-xA)*max(0,yB-yA) + return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-inter+1e-6) + +class CamManager: + def __init__(self, cam_id, global_mem): + self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] + self.ultimas_detecciones_globales = {} + self.tracks_post_cruce = {} + + def _limpiar_tracks_post_cruce(self, now): + """Limpia registros de post-cruce antiguos.""" + antiguos = [idx for idx, info in self.tracks_post_cruce.items() if now - info['ts_cruce'] > 10.0] + for idx in antiguos: + del self.tracks_post_cruce[idx] + + def _detectar_grupo(self, trk, box, todos): + x1,y1,x2,y2 = box + w,h = x2-x1, y2-y1 + cx,cy = (x1+x2)/2, (y1+y2)/2 + fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35) + for o in todos: + if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue + if iou_overlap(box, o.box) > 0.05: return True + ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2 + if abs(cx-ocx)= 0.48: + trk.fallos_post_grupo = 0; trk.firma_pre_grupo = None + with self.global_mem.lock: + if trk.gid in self.global_mem.db: self.global_mem.db[trk.gid]['ema'] = fa + else: + trk.fallos_post_grupo += 1 + if trk.fallos_post_grupo >= 3: + trk.gid = trk.firma_pre_grupo = None; trk.fallos_post_grupo = 0 + + def _asignar(self, boxes, confidences, frame_hd, now, keypoints_list): + n_trk, n_det = len(self.trackers), len(boxes) + firmas = [None] * n_det + + # ───────────────────────────────────────────────────────────── + # ⚡ OPTIMIZACIÓN BATCH: Extraemos OSNet para todas las detecciones de un golpe + # ───────────────────────────────────────────────────────────── + if n_det > 0: + rois_validos = [] + mapa_rois = {} + h_hd, w_hd = frame_hd.shape[:2] + + # 1. Recolectar todos los recortes (ROIs) de la imagen + for d_idx, box in enumerate(boxes): + x1, y1, x2, y2 = box + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + # Descartamos basura óptica de inmediato + if roi.size > 0 and roi.shape[0] >= 40 and roi.shape[1] >= 20: + mapa_rois[d_idx] = len(rois_validos) + rois_validos.append(roi) + + # 2. Ejecutar la red neuronal para todos SIMULTÁNEAMENTE + if rois_validos: + # ⚡ Llamamos a la función segura que procesa 1 a 1 rápidamente + deep_feats_seguros = extraer_features_osnet_seguro(rois_validos) + + # Ensamblar las firmas + for d_idx, box in enumerate(boxes): + if d_idx in mapa_rois: + df_persona = deep_feats_seguros[mapa_rois[d_idx]] + kpts = keypoints_list[d_idx] if keypoints_list and d_idx < len(keypoints_list) else None + + # Extraemos el resto de la firma (Color enmascarado, Anatomía, etc.) + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts, deep_feat=df_persona) + + def obtener_firma(d_idx): + return firmas[d_idx] + + if n_trk == 0: return [], list(range(n_det)), [], firmas + if n_det == 0: return [], [], list(range(n_trk)), firmas + + # ... (A partir de aquí, el código de "alta" y "baja" confidence se queda EXACTAMENTE como ya lo tienes) ... + alta = [(d,boxes[d]) for d,c in enumerate(confidences) if c >= 0.45] + baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45] + matched, sin_match_trk = [], list(range(n_trk)) + + if alta: + C = np.full((n_trk, len(alta)), 100.0, np.float32) + for t, trk in enumerate(self.trackers): + dt_oculto = now - trk.ts_ultima_deteccion + radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto)))) + ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None) + + for idx, (d, det) in enumerate(alta): + iou = iou_overlap(trk.box, det) + dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2) + if dist > radio: continue + + a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1]) + a_det = (det[2]-det[0])*(det[3]-det[1]) + costo = (1.0*(1-iou)) + (0.3*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.1) + costo += (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.1) + costo += min(0.3, dt_oculto*0.1) + + if ref_firma is not None and obtener_firma(d) is not None: + sim_ap = similitud_hibrida(ref_firma, firmas[d]) + + if sim_ap < 0.25: + costo += 50.0 + else: + costo += (1.0 - sim_ap) * 2.0 + + nombre_vip = self.global_mem.db.get(trk.gid, {}).get('nombre') + umbral_ap = 0.55 if nombre_vip else 0.40 + if sim_ap < umbral_ap: + costo += (umbral_ap - sim_ap) * 3.0 + + C[t,idx] = costo + + for r,c in zip(*linear_sum_assignment(C)): + if C[r,c] <= 6.5: + matched.append((r, alta[c][0])); sin_match_trk.remove(r) + + if sin_match_trk and baja: + C2 = np.ones((len(sin_match_trk), len(baja)), np.float32) + for ti, trk_i in enumerate(sin_match_trk): + for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det) + nuevos_sin = [] + for r,c in zip(*linear_sum_assignment(C2)): + if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0])) + else: nuevos_sin.append(sin_match_trk[r]) + sin_match_trk = nuevos_sin + + return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas + + def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo): + # ───────────────────────────────────────────────────────────── + # 1. Resolver fusiones globales + # ───────────────────────────────────────────────────────────── + for trk in self.trackers: + if trk.gid: + with self.global_mem.lock: + d = self.global_mem.db.get(trk.gid) + if d and 'fusionado_con' in d: + trk.gid = d['fusionado_con'] + + # ───────────────────────────────────────────────────────────── + # 2. Predict y filtrar tracks muertos por Kalman + # ───────────────────────────────────────────────────────────── + self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None] + if not turno_activo: + return self.trackers + + # ───────────────────────────────────────────────────────────── + # 3. Asignar detecciones a tracks existentes + # ───────────────────────────────────────────────────────────── + matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints) + active_gids = {t.gid for t in self.trackers if t.gid is not None} + fh, fw = frame_hd.shape[:2] + + # ───────────────────────────────────────────────────────────── + # 4. DETECCIÓN DE CRUCES + Registro para recuperación post-cruce + # ───────────────────────────────────────────────────────────── + tracks_en_cruce_locals = set() + for i in range(len(self.trackers)): + for j in range(i + 1, len(self.trackers)): + trk_i, trk_j = self.trackers[i], self.trackers[j] + if trk_i.gid is None or trk_j.gid is None or trk_i.box is None or trk_j.box is None: continue + + iou = iou_overlap(trk_i.box, trk_j.box) + if iou > 0.30: + tracks_en_cruce_locals.add(trk_i.local_id) + tracks_en_cruce_locals.add(trk_j.local_id) + if not hasattr(self, 'tracks_post_cruce'): self.tracks_post_cruce = {} + self.tracks_post_cruce[trk_i.local_id] = {'gid_antes': trk_i.gid, 'ts_cruce': now} + self.tracks_post_cruce[trk_j.local_id] = {'gid_antes': trk_j.gid, 'ts_cruce': now} + + + # ───────────────────────────────────────────────────────────── + # 5. PROCESAR MATCHES + # ───────────────────────────────────────────────────────────── + for t_idx, d_idx in matched: + trk, box = self.trackers[t_idx], boxes[d_idx] + + # Asegurar firma visual REAL (mitigación del bug de contaminación en _asignar) + if firmas[d_idx] is None: + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts) + + firma_det = firmas[d_idx] + + # Debug + if trk.gid is None: + print(f" [DEBUG] LOCAL Trk {trk.local_id}: listo={trk.listo_para_id}, cap_locales={getattr(trk, 'muestras_capturadas', 0)}") + else: + with self.global_mem.lock: + db_act = self.global_mem.db.get(trk.gid, {}).get('actualizaciones_globales', 0) + print(f" [MEMORIA] ID {trk.gid} ha sido actualizado {db_act} veces en la red global.") + + # Detección de grupo + es_grupo = self._detectar_grupo(trk, box, self.trackers) + if not trk.en_grupo and es_grupo: + with self.global_mem.lock: + trk.firma_pre_grupo = self.global_mem.db.get(trk.gid, {}).get('ema') + trk.ts_salio_grupo = 0.0 + elif trk.en_grupo and not es_grupo: + trk.ts_salio_grupo = now + trk.en_grupo = es_grupo + + # Update Kalman + trk.update(box, es_grupo, now, kpts=(keypoints[d_idx] if keypoints else None), firma_actual=firma_det) + + # Geometría de frame + margen_x = fw * 0.05 + esta_en_centro = (box[0] > margen_x and box[2] < (fw - margen_x)) + + # ───────────────────────────────────────────────────────── + # 5a. APRENDIZAJE EMA (zona segura, centro del frame) + # ───────────────────────────────────────────────────────── + if firma_det is not None and not es_grupo and esta_en_centro: + trk.actualizar_ema(firma_det) + + # Nutrir memoria global cada 2 segundos + if trk.gid is not None: + if not hasattr(trk, 'ultimo_update_global'): + trk.ultimo_update_global = 0 + if (now - trk.ultimo_update_global) > 2.0: + with self.global_mem.lock: + if trk.gid in self.global_mem.db: + self.global_mem.db[trk.gid]['ema'] = trk.firma_ema.copy() + self.global_mem.db[trk.gid]['ts'] = now + self.global_mem.db[trk.gid]['actualizaciones_globales'] = \ + self.global_mem.db[trk.gid].get('actualizaciones_globales', 0) + 1 + trk.ultimo_update_global = now + + # ⚡ INYECCIÓN EXACTA AQUÍ: Disparar el Auto-Merge + self.global_mem.consolidar_clones() + + # ───────────────────────────────────────────────────────── + # 5b. RESCATE DE BORDE (Acelerado) + # ───────────────────────────────────────────────────────── + elif firma_det is not None and not es_grupo and not esta_en_centro: + muestras_act = getattr(trk, 'muestras_capturadas', 0) + if muestras_act < 3: + trk.actualizar_ema(firma_det) + elif muestras_act < 5 and trk.frames_observados % 3 == 0: + trk.actualizar_ema(firma_det) + + # ───────────────────────────────────────────────────────── + # 5c. IDENTIFICACIÓN CON PROTECCIÓN + # ───────────────────────────────────────────────────────── + if trk.gid is None and trk.listo_para_id and firma_det is not None: + if getattr(trk, 'muestras_capturadas', 0) >= 4: + if trk.local_id in tracks_en_cruce_locals: + print(f" [CRUCE] Track {trk.local_id} en cruce físico. Bloqueando identificación.") + else: + tiempo_enfriamiento = now - getattr(trk, 'ultimo_cambio_id', 0) + if tiempo_enfriamiento < 0.6: + print(f" [HYSTERESIS] Track {trk.local_id} en enfriamiento ({tiempo_enfriamiento:.2f}s)") + else: + gid_c, es_reid = self.global_mem.identificar_candidato( + firma_det, self.cam_id, now, active_gids, + en_borde=not esta_en_centro, + firma_ema_local=trk.firma_ema, + confianza_fisica=trk.confianza_fisica + ) + if gid_c: + active_gids.add(gid_c) + trk.gid = gid_c + trk.origen_global = es_reid + trk.ultimo_cambio_id = now + + # ───────────────────────────────────────────────────────────── + # 6. VERIFICACIÓN POST-CRUCE (recuperar ID original tras separarse) + # ───────────────────────────────────────────────────────────── + if hasattr(self, 'tracks_post_cruce') and self.tracks_post_cruce: + tracks_post_cruce_nuevos = {} + + # ⚡ CAMBIO CRÍTICO: Iterar por local_id, no por índice + for local_id, info in list(self.tracks_post_cruce.items()): + # Buscar el track con este local_id (único e inmutable) + trk = next((t for t in self.trackers if t.local_id == local_id), None) + if trk is None: + continue # El track ya murió, ignorar + + # Solo actuar si ya pasaron >2 segundos desde el cruce + if now - info['ts_cruce'] > 2.0: + # Si el ID cambió durante el cruce (o se asignó uno nuevo incorrecto) + if trk.gid is not None and trk.gid != info['gid_antes']: + if trk.firma_ema is not None and info['gid_antes'] in self.global_mem.db: + ema_original = self.global_mem.db[info['gid_antes']].get('ema') + if ema_original is not None: + sim_recuperacion = similitud_hibrida( + trk.firma_ema, ema_original, cross_cam=False + ) + if sim_recuperacion > 0.85: + # Verificar que el ID original no esté activo en otro track + if info['gid_antes'] not in active_gids: + print(f" [POST-CRUCE] Track {trk.local_id} recuperó ID {info['gid_antes']} (sim={sim_recuperacion:.2f})") + active_gids.discard(trk.gid) + trk.gid = info['gid_antes'] + active_gids.add(trk.gid) + # No guardamos este registro más (ya cumplió su ciclo) + continue + + # Aún no pasan 2 segundos, mantener registro (usando local_id) + tracks_post_cruce_nuevos[local_id] = info + + self.tracks_post_cruce = tracks_post_cruce_nuevos + + # ───────────────────────────────────────────────────────────── + # 7. CREAR NUEVOS TRACKERS para detecciones sin match + # ───────────────────────────────────────────────────────────── + for d_idx in new_dets: + nt = KalmanTrack(boxes[d_idx], now) + f = firmas[d_idx] + if f: + nt.actualizar_ema(f) + self.trackers.append(nt) + + # ───────────────────────────────────────────────────────────── + # 8. LIMPIEZA: matar tracks sin detección por >3 segundos + # ───────────────────────────────────────────────────────────── + tracks_vivos = [] + for t in self.trackers: + if (now - t.ts_ultima_deteccion) < 3.0: + tracks_vivos.append(t) + self.trackers = tracks_vivos + + # ⚡ FIX P0: Purga de memory leak en tracks_post_cruce + if hasattr(self, 'tracks_post_cruce'): + local_ids_vivos = {t.local_id for t in self.trackers} + self.tracks_post_cruce = {lid: info for lid, info in self.tracks_post_cruce.items() if lid in local_ids_vivos} + + # ───────────────────────────────────────────────────────────── + # 9. Gestión post-grupo (recuperación de firma tras salir de multitud) + # ───────────────────────────────────────────────────────────── + self._gestionar_post_grupo(now, frame_hd) + + return self.trackers + +class CamStream: + def __init__(self, url): + self.url = url; self.cap = cv2.VideoCapture(url) + self.q = queue.Queue(maxsize=1); self.stopped = False + threading.Thread(target=self._run, daemon=True).start() + def _run(self): + while not self.stopped: + ret, f = self.cap.read() + if not ret: time.sleep(2); self.cap.open(self.url); continue + if self.q.full(): + try: self.q.get_nowait() + except queue.Empty: pass + self.q.put(f) + @property + def frame(self): + try: return self.q.get(timeout=0.5) + except queue.Empty: return None + def stop(self): self.stopped = True; self.cap.release() + +def dibujar_track(frame_show, trk): + try: x1,y1,x2,y2 = map(int,trk.box) + except: return + if trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}" + elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]" + elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]" + elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]" + 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) + +# ────────────────────────────────────────────────────────────────────────────── +# MAIN +# ────────────────────────────────────────────────────────────────────────────── +def main(): + print("\n=== Tracker Autónomo Definitivo ===") + print("Persistencia activa. Los IDs se recuerdan entre reinicios.") + print("Tecla [q] para salir (la memoria se guarda automáticamente).\n") + + model = YOLO("yolov8n-pose.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 + ultimo_guardado = time.time() + + try: + while True: + now, tiles = time.time(), [] + 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, confs, kpts = [], [], [] + 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: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + + if es_humano_valido(kpt_persona, box, conf): + boxes.append(box) + confs.append(conf) + kpts.append(kpt_persona) + + ESQUELETO = [(0,1),(0,2),(1,3),(2,4),(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)] + for pk in kpts: + if pk is None: continue + for a,b in ESQUELETO: + if pk[a][2]>0.40 and pk[b][2]>0.40: + cv2.line(frame_show, (int(pk[a][0]),int(pk[a][1])), (int(pk[b][0]),int(pk[b][1])), (255,0,255), 2) + for kx, ky, kc in pk: + if kc > 0.40: cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1) + + tracks = managers[cid].update(boxes, confs, kpts, frame_show, frame, 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 getattr(t,'gid',None) 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[:3]),np.hstack(tiles[3:])])) + + idx += 1 + + if now - ultimo_guardado > 30: + global_mem.guardar_memoria() + ultimo_guardado = now + + if cv2.waitKey(1) == ord('q'): break + + finally: + global_mem.guardar_memoria() + print("[Memoria] Guardada antes de salir.") + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() + + +""" +#---------------------------------------------version + +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 +from datetime import datetime +import json +import csv +import math +import queue +from scipy.spatial.distance import cosine + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" +SECUENCIA = [1, 7, 5, 8, 3, 6] +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_5_msmt17_batch1.onnx" + +VECINOS = { + "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], + "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] +} + +TIEMPO_MAX_AUSENCIA = 900.0 +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 + +# ────────────────────────────────────────────────────────────────────────────── +# OSNET +# ────────────────────────────────────────────────────────────────────────────── +print("Cargando OSNet...") +try: + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + input_name = ort_session.get_inputs()[0].name + print("OSNet listo para CPU.") +except Exception as e: + print(f"ERROR FATAL: {e}"); exit() + +MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1) +STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1) + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRACCIÓN DE FIRMAS MULTI-DESCRIPTOR +# ────────────────────────────────────────────────────────────────────────────── +def extraer_features_osnet_seguro(rois): + + #Procesamiento secuencial 1-a-1 de grado comercial. Garantiza 0% de probabilidad de crasheo C++ en ONNX. + + if not rois: return [] + + features_norm = [] + for roi in rois: + # 1. Preprocesamiento matemático exacto para OSNet + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + + # Expandimos la dimensión para crear un Batch estricto de tamaño 1: (1, 3, 256, 128) + blob = np.expand_dims((img - MEAN) / STD, 0) + + # 2. INFERENCIA AISLADA (El motor ONNX no sufrirá estrés de memoria) + # El [0][0] extrae el vector ignorando la dimensión del batch + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + # 3. Normalización L2 (Vital para que funcione la distancia Coseno) + n = np.linalg.norm(df) + if n > 0: df /= n + + features_norm.append(df) + + return features_norm + +def calcular_nitidez(img): + if img is None or img.size == 0: return 0 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + return cv2.Laplacian(gray, cv2.CV_64F).var() + +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 + return np.expand_dims((img - MEAN) / STD, 0) + +SOPORTA_BATCH = None # Variable global para detectar compatibilidad ONNX + +def procesar_batch_osnet(rois): + #Procesa los recortes de forma segura y secuencial para evitar abortos C++ en ONNX + if not rois: return [] + + features_norm = [] + for roi in rois: + # Preprocesamiento individual + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + blob = np.expand_dims((img - MEAN) / STD, 0) + + # ⚡ Ejecución segura 1 a 1 (Garantiza que ONNX no colapse) + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + n = np.linalg.norm(df) + if n > 0: df /= n + features_norm.append(df) + + return features_norm + +def extraer_color_zonas(roi): + if roi is None or roi.size == 0 or roi.shape[0] < 30 or roi.shape[1] < 10: + return np.zeros(512 * 3, dtype=np.float32) + + h, w = roi.shape[:2] + + # ⚡ ALUCÍN OPTIMIZADO: LA MÁSCARA ELÍPTICA + # Creamos un lienzo negro y dibujamos un óvalo blanco en el centro. + # El histograma IGNORARÁ todo lo negro (la pared/fondo) y solo leerá lo blanco (el cuerpo). + mask = np.zeros((h, w), dtype=np.uint8) + centro_x, centro_y = int(w/2), int(h/2) + eje_x, eje_y = int(w * 0.35), int(h * 0.48) # Óvalo ajustado a proporciones humanas + cv2.ellipse(mask, (centro_x, centro_y), (eje_x, eje_y), 0, 0, 360, 255, -1) + + # ⚡ REGRESO AL HSV PURO (Sin CLAHE que adultere la luz) + hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) + + t1 = int(h * 0.30) + t2 = int(h * 0.65) + + # Cortamos la imagen Y la máscara en las 3 zonas (Cabeza, Torso, Piernas) + z1_img, z1_mask = hsv[:t1, :], mask[:t1, :] + z2_img, z2_mask = hsv[t1:t2, :], mask[t1:t2, :] + z3_img, z3_mask = hsv[t2:, :], mask[t2:, :] + + def calc_hist(img_part, mask_part): + if img_part.size == 0: return np.zeros(512, dtype=np.float32) + # Calculamos el color 3D (H,S,V) PERO pasándole la máscara para ignorar el fondo + hist = cv2.calcHist([img_part], [0, 1, 2], mask_part, [8, 8, 8], [0, 180, 0, 256, 0, 256]) + cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1) + return hist.flatten() + + h1 = calc_hist(z1_img, z1_mask) + h2 = calc_hist(z2_img, z2_mask) + h3 = calc_hist(z3_img, z3_mask) + + # Retorna 1536 dimensiones de color PURO y libre de paredes + return np.concatenate([h1, h2, h3]).astype(np.float32) + + +def extraer_proporciones_anatomicas(kpts): + if kpts is None or len(kpts) < 17: return None + kpts = np.array(kpts) + if kpts.shape[0] < 17: return None + + puntos = {i: kpts[i] for i in range(17)} + + if puntos[0][2] < 0.30: return None + if puntos[5][2] < 0.30 and puntos[6][2] < 0.30: return None + if puntos[11][2] < 0.30 and puntos[12][2] < 0.30: return None + + def dist(i, j): + if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0 + return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2])) + + hombros = dist(5, 6) + caderas = dist(11, 12) + torso = max(dist(5, 11), dist(6, 12)) + pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0 + pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0 + piernas = max(pierna_izq, pierna_der) + brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0 + brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0 + brazos = max(brazo_izq, brazo_der) + + altura = torso + piernas + if altura < 15.0: return None + + feats = np.array([ + hombros / max(altura, 1e-6), + caderas / max(altura, 1e-6), + torso / max(altura, 1e-6), + piernas / max(altura, 1e-6), + brazos / max(altura, 1e-6), + hombros / max(caderas, 1e-6), + piernas / max(torso, 1e-6), + ], dtype=np.float32) + + feats = np.clip(feats, 0.0, 3.0) + n = np.linalg.norm(feats) + if n > 0: feats /= n + return feats + +def es_humano_valido(kpts, box, conf): + if conf < 0.45: return False + if kpts is None or len(kpts) < 17: return False + + x1, y1, x2, y2 = box + w, h = x2 - x1, y2 - y1 + if h / max(w, 1) < 0.50 or h < 50: return False + + cabeza_segura = any(p[2] > 0.55 for p in kpts[:5]) + if not cabeza_segura: return False + + torso_seguro = any(p[2] > 0.40 for p in kpts[5:13]) + if not torso_seguro: return False + + return True + +def extraer_firma_hibrida(frame_hd, box_480, kpts=None, deep_feat=None): + try: + h_hd, w_hd = frame_hd.shape[:2] + x1, y1, x2, y2 = box_480 + + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: + return None + + score_nitidez = calcular_nitidez(roi) + + # ⚡ SOPORTE BATCH: Si no se entregó deep_feat masivo, lo calculamos individualmente (Fallback) + if deep_feat is None: + blob = preprocess_onnx(roi) + deep_feat = ort_session.run(None, {input_name: blob})[0][0].flatten() + n = np.linalg.norm(deep_feat) + if n > 0: deep_feat /= n + + color_f = extraer_color_zonas(roi) + anat_f = extraer_proporciones_anatomicas(kpts) + + ratio_hw = roi.shape[0] / max(roi.shape[1], 1) + calidad = (x2_c - x1_c) * (y2_c - y1_c) + + return { + 'deep': deep_feat, + 'color': color_f, # ⚡ ELIMINADO: Textura LBP + 'anatomia': anat_f, + 'ratio_hw': ratio_hw, + 'calidad': calidad, + 'nitidez': score_nitidez + } + except Exception as e: + return None + +def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=1.0): + #Similitud estable: sin discontinuidades, sin vetos acumulativos, predecible. + if f1 is None or f2 is None: + return 0.0 + + # 1. OSNet (siempre disponible y confiable) + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is None or deep2 is None or np.sum(deep1) == 0 or np.sum(deep2) == 0: + return 0.0 + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # 2. Anatomía (solo si hay coincidencia mínima) + sim_anat = 0.0 + usar_anat = False + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + if (a1 is not None and a2 is not None and + hasattr(a1, "shape") and hasattr(a2, "shape") and a1.shape == a2.shape): + dist_a = np.linalg.norm(a1 - a2) + sim_anat = max(0.0, 1.0 - dist_a) + usar_anat = sim_anat > 0.50 + + # 3. Color (HSV + Luminancia con CLAHE) + sim_color = 0.0 + usar_color = False + c1, c2 = f1.get("color"), f2.get("color") + if (c1 is not None and c2 is not None and + c1.shape == c2.shape and len(c1) == 1008 and + np.sum(c1) > 0.1 and np.sum(c2) > 0.1): + + dist1 = cv2.compareHist(c1[:336].astype(np.float32), c2[:336].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist2 = cv2.compareHist(c1[336:672].astype(np.float32), c2[336:672].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist3 = cv2.compareHist(c1[672:].astype(np.float32), c2[672:].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + + s1, s2, s3 = max(0.0, 1.0 - float(dist1)), max(0.0, 1.0 - float(dist2)), max(0.0, 1.0 - float(dist3)) + + if cross_cam and s3 > 0.60 and s2 < 0.35: + s2 = max(s2, s3 * 0.80) + + sim_color = (s1 * 0.20) + (s2 * 0.50) + (s3 * 0.30) + usar_color = sim_color > 0.25 + + # ───────────────────────────────────────────────────────────── + # FUSIÓN LINEAL (sin discontinuidades, sin restas, predecible) + # ───────────────────────────────────────────────────────────── + peso_deep = 0.70 + peso_anat = 0.20 if usar_anat else 0.0 + peso_color = 0.10 if usar_color else 0.0 + + # Renormalizar + total = peso_deep + peso_anat + peso_color + if total > 0: + peso_deep /= total + peso_anat /= total + peso_color /= total + + resultado = (sim_deep * peso_deep) + (sim_anat * peso_anat) + (sim_color * peso_color) + + # ───────────────────────────────────────────────────────────── + # ÚNICO VETO: Color extremadamente opuesto + OSNet inseguro + # ───────────────────────────────────────────────────────────── + if not cross_cam and usar_color and sim_color < 0.08 and sim_deep < 0.80: + return 0.0 + + return float(max(0.0, min(1.0, resultado))) + +def desglose_similitud(f1, f2, cross_cam=False): + # ------------------------------------------------- + # 1. DEEP (OSNET) + # ------------------------------------------------- + sim_deep = 0.0 + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is not None and deep2 is not None and np.sum(deep1) > 0 and np.sum(deep2) > 0: + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ------------------------------------------------- + # 2. COLOR (2D HS INMUNE A SOMBRAS) + # ------------------------------------------------- + sim_color = -1.0 + c1, c2 = f1.get("color"), f2.get("color") + + # ⚡ CÓDIGO RESTAURADO A 1536 PARA LEER EL HSV PURO Y ENMASCARADO + if c1 is not None and c2 is not None and c1.shape == c2.shape and len(c1) == 1536: + if np.sum(c1) > 0.1 and np.sum(c2) > 0.1: + dist1 = cv2.compareHist(c1[:512].astype(np.float32), c2[:512].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist2 = cv2.compareHist(c1[512:1024].astype(np.float32), c2[512:1024].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist3 = cv2.compareHist(c1[1024:].astype(np.float32), c2[1024:].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + + s1 = max(0.0, 1.0 - float(dist1)) + s2 = max(0.0, 1.0 - float(dist2)) + s3 = max(0.0, 1.0 - float(dist3)) + + # Escudo chamarra abierta + if cross_cam and s3 > 0.60 and s2 < 0.35: + s2 = max(s2, s3 * 0.80) + + sim_color = (s1 * 0.20) + (s2 * 0.50) + (s3 * 0.30) + + # ------------------------------------------------- + # 3. ANATOMIA (7 Puntos) + # ------------------------------------------------- + sim_anat = -1.0 + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + + if a1 is not None and a2 is not None and hasattr(a1, "shape") and hasattr(a2, "shape") and a1.shape == a2.shape: + dist_a = np.linalg.norm(a1 - a2) + sim_anat = max(0.0, 1.0 - dist_a) + + return sim_deep, sim_color, sim_anat + +# ────────────────────────────────────────────────────────────────────────────── +# KALMAN TRACKER +# ────────────────────────────────────────────────────────────────────────────── +class KalmanTrack: + _count = 0 + + def __init__(self, box, now): + kf = cv2.KalmanFilter(7, 4) + 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) + kf.transitionMatrix = np.eye(7, dtype=np.float32) + kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1 + kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.01, 1.0, 2.0, 1.0]).astype(np.float32) + kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32) + kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32) + kf.statePost = np.zeros((7,1), np.float32) + kf.statePost[:4] = self._z(box) + self.kf = kf + + self.local_id = KalmanTrack._count; KalmanTrack._count += 1 + self.gid, self.origen_global, self.aprendiendo = None, False, False + self.box = list(box) + self.ts_creacion = 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.firma_pre_grupo = None + self.ts_salio_grupo = 0.0 + self.fallos_post_grupo = 0 + self.ultimo_aprendizaje = 0.0 + self.frames_continuos = 0 + self.frames_observados = 0 + self.firma_ema = None + self.muestras_capturadas = 0 + self.ultimo_cambio_id = 0.0 + + def _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/max(h,1e-6)]]).astype(np.float32) + + def _bbox(self, x): + cx, cy = float(x[0].item()), float(x[1].item()) + s, r = float(x[2].item()), float(x[3].item()) + w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6) + return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] + + @property + def confianza_fisica(self): + return min(1.0, self.frames_continuos / 20.0) + + def calcular_score_calidad(self, firma): + if firma is None: return 0.0 + score_nitidez = min(firma.get('nitidez', 0) / 100.0, 1.0) * 50.0 + score_area = min(firma.get('calidad', 0) / 12000.0, 1.0) * 50.0 + return score_nitidez + score_area + + def actualizar_ema(self, firma): + if firma is None: return + + self.muestras_capturadas = getattr(self, 'muestras_capturadas', 0) + 1 + score_nuevo = self.calcular_score_calidad(firma) + score_viejo = getattr(self, 'score_ema', 0.0) + + if self.firma_ema is None: + # Primera impresión (puede ser mala, pero es lo único que tenemos) + self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()} + self.score_ema = score_nuevo + return + + # ⚡ PROTECCIÓN DE MEMORIA (Anti-polución) + if self.muestras_capturadas > 4 and score_nuevo < (score_viejo * 0.60): + return + + # ───────────────────────────────────────────────────────────── + # ⚡ ASIMILACIÓN DINÁMICA (La cura a la mala primera impresión) + # ───────────────────────────────────────────────────────────── + # Si estamos en los primeros 5 frames, O la nueva foto es notoriamente mejor + # que nuestro mejor recuerdo (>20% mejor), somos una "esponja". + if self.muestras_capturadas <= 5 or score_nuevo > (score_viejo * 1.20): + # Alpha bajo = Sobrescribe agresivamente el pasado + alpha_deep = 0.20 # 20% vieja, 80% NUEVA + alpha_color = 0.20 + alpha_geo = 0.50 + self.score_ema = score_nuevo # Actualizamos nuestro estándar de calidad + else: + # Estado de madurez: La firma ya es excelente y estable. + # Los cambios deben ser muy lentos para no arruinarla. + alpha_deep = 0.85 # 85% vieja, 15% nueva + alpha_color = 0.50 + alpha_geo = 0.80 + self.score_ema = max(score_nuevo, score_viejo) + + ema = self.firma_ema + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + # Ya sin la textura LBP que eliminamos por rendimiento + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = alpha_geo * np.asarray(ema['anatomia']) + (1-alpha_geo) * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = max(ema.get('calidad', 0), firma['calidad']) + + def predict(self, turno_activo=True): + if self.time_since_update > 30: return None + + if getattr(self, 'en_grupo', False): + 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._bbox(self.kf.statePre) + return self.box + + if self.time_since_update > 0: + self.kf.statePost[4] *= 0.5 + self.kf.statePost[5] *= 0.5 + self.kf.statePost[6] = 0.0 + self.frames_continuos = max(0, self.frames_continuos - 1) + + 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._bbox(self.kf.statePre) + return self.box + + def update(self, box, en_grupo, now, kpts=None, firma_actual=None): + self.ts_ultima_deteccion = now + self.time_since_update, self.en_grupo = 0, en_grupo + self.box = list(box) + self.kf.correct(self._z(box)) + + if not en_grupo: + self.frames_observados += 1 + self.frames_continuos += 1 + self.listo_para_id = True + + +# ────────────────────────────────────────────────────────────────────────────── +# MEMORIA GLOBAL CON PERSISTENCIA +# ────────────────────────────────────────────────────────────────────────────── +class GlobalMemory: + def __init__(self): + self.db = {} + self.next_gid = 100 + self.lock = threading.RLock() + self.ruta_memoria = os.path.join("cache_nombres", "memoria_ids.json") + self.ruta_movimientos = os.path.join("cache_nombres", "registro_movimientos.csv") + os.makedirs("cache_nombres", exist_ok=True) + self.nombres_activos = {} + self.ultimos_saludos = {} + self._votos_nombre = {} + self._cargar_memoria() + self.reserva_temporal = {} + self.id_locks = {} + self.ultimas_detecciones = {} + + # Método 1: Registrar detección global + def registrar_deteccion_global(self, gid, cam_id, firma, now): + #Registra que un ID fue detectado en una cámara para coordinación solapada. + if firma is None or 'deep' not in firma: + return + + # Hash simple de la firma deep para comparación rápida + firma_hash = hash(firma['deep'].tobytes()) % 10000 + + self.ultimas_detecciones[gid] = { + 'cam': str(cam_id), + 'ts': now, + 'firma_hash': firma_hash + } + + # Limpiar detecciones antiguas (>10 segundos) + antiguas = [g for g, d in self.ultimas_detecciones.items() if now - d['ts'] > 10.0] + for g in antiguas: + del self.ultimas_detecciones[g] + + # Método 2: Buscar coincidencia solapada + def buscar_coincidencia_solapada(self, firma, cam_id, now, umbral_sim=0.85): + #Busca si esta detección podría ser un ID ya detectado en cámara vecina recientemente. + if firma is None or 'deep' not in firma: + return None + + firma_hash = hash(firma['deep'].tobytes()) % 10000 + vecinos = VECINOS.get(str(cam_id), []) + + for gid, info in self.ultimas_detecciones.items(): + # Solo considerar si fue detectado en cámara vecina hace <5s + if info['cam'] not in vecinos: + continue + if now - info['ts'] > 5.0: + continue + + # Si el hash coincide exactamente, es muy probable que sea el mismo + if info['firma_hash'] == firma_hash: + return gid + + # Si no, hacer comparación completa (más costosa) + ema = self.db.get(gid, {}).get('ema') + if ema is not None: + sim = similitud_hibrida(firma, ema, cross_cam=True) + if sim >= umbral_sim: + return gid + + return None + + def lock_id_for_camera(self, gid, cam_id, now, duration=15.0): + self.id_locks[gid] = { + 'cam': str(cam_id), + 'unlock_ts': now + duration, + 'nombre': self.db.get(gid, {}).get('nombre') + } + print(f" [ID LOCK] ID {gid} bloqueado en Cam{cam_id} por {duration}s") + + def is_id_locked(self, gid, cam_id, now): + if gid not in self.id_locks: + return False + lock = self.id_locks[gid] + if lock['cam'] != str(cam_id): + return False + if now < lock['unlock_ts']: + return True + else: + del self.id_locks[gid] + return False + + def limpiar_locks_vencidos(self, now): + vencidos = [gid for gid, lock in self.id_locks.items() if now >= lock['unlock_ts']] + for gid in vencidos: + del self.id_locks[gid] + + def registrar_salida(self, gid, cam_salida, now): + if gid not in self.db: return + + vecinas = VECINOS.get(str(cam_salida), []) + self.reserva_temporal[gid] = { + 'cam_salida': str(cam_salida), + 'ts_salida': now, + 'cam_esperadas': vecinas, + 'nombre': self.db[gid].get('nombre') + } + + reservas_a_limpiar = [] + for gid_reserva, info in self.reserva_temporal.items(): + if now - info['ts_salida'] > 30.0: + reservas_a_limpiar.append(gid_reserva) + + for gid_limpiar in reservas_a_limpiar: + del self.reserva_temporal[gid_limpiar] + + def _bonus_reserva(self, gid_evaluado, cam_actual, now): + return 0.0 + + + def _firma_a_dict(self, firma): + if not firma: return None + res = {} + for k, v in firma.items(): + if isinstance(v, np.ndarray): + res[k] = v.tolist() + elif k == 'anatomia' and v is not None: + res[k] = v.tolist() if isinstance(v, np.ndarray) else list(v) + else: + res[k] = v + return res + + def _dict_a_firma(self, d): + if not d: return None + res = {} + for k, v in d.items(): + if isinstance(v, list) and k in ['deep', 'color', 'textura', 'anatomia']: + res[k] = np.array(v, dtype=np.float32) + else: + res[k] = v + return res + + def _cargar_memoria(self): + if os.path.exists(self.ruta_memoria): + try: + with open(self.ruta_memoria, 'r') as f: + datos = json.load(f) + + ahora = time.time() + cargados_validos = 0 + + for gid_str, info in datos.items(): + ts_guardado = info.get('ts', 0) + + if ahora - ts_guardado > 900.0: + continue + + gid = int(gid_str) + self.db[gid] = { + 'ema': self._dict_a_firma(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'ts': ts_guardado, + 'nombre': info.get('nombre'), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._dict_a_firma(f) for f in info.get('firmas_altas', []) if f], + } + cargados_validos += 1 + + self.next_gid = max([int(k) for k in datos.keys()] + [99]) + 1 + print(f"[Memoria] {cargados_validos} IDs persistentes válidos cargados (Se purgaron los expirados).") + except Exception as e: + print(f"[Memoria] Error cargando: {e}") + + def guardar_memoria(self): + try: + datos = {} + for gid, info in self.db.items(): + datos[str(gid)] = { + 'ema': self._firma_a_dict(info.get('ema')), + 'last_cam': info.get('last_cam', '1'), + 'nombre': info.get('nombre'), + 'ts': info.get('ts', time.time()), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0), + 'firmas_altas': [self._firma_a_dict(f) for f in info.get('firmas_altas', [])], + } + with open(self.ruta_memoria, 'w') as f: + json.dump(datos, f) + except Exception as e: + print(f"[Memoria] Error guardando: {e}") + + def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion_seg): + nuevo = not os.path.exists(self.ruta_movimientos) + try: + with open(self.ruta_movimientos, 'a', newline='', encoding='utf-8') as f: + w = csv.writer(f) + if nuevo: w.writerow(["Fecha_Hora", "Nombre", "Origen", "Destino", "Seg_en_Origen"]) + w.writerow([datetime.now().strftime("%Y-%m-%d %H:%M:%S"), nombre, cam_origen, cam_destino, round(duracion_seg, 2)]) + except: pass + + def asignar_nombre(self, gid, nombre, confianza, es_fusion=False): + VOTOS_NOMBRE_MIN = 3 + UMBRAL_VOTO = 0.52 + UMBRAL_CONFIRMACION = 0.62 + + if confianza < (0.50 if es_fusion else UMBRAL_VOTO): + return False + + with self.lock: + rec = self.db.get(gid) + if not rec: return False + + nombre_viejo = rec.get('nombre') + + if not hasattr(self, 'nombres_activos'): + self.nombres_activos = {} + + gid_ocupante = self.nombres_activos.get(nombre) + if gid_ocupante is not None and gid_ocupante != gid: + if gid_ocupante in self.db: + ts_ultimo = self.db[gid_ocupante].get('ts', 0) + if (time.time() - ts_ultimo) < 10.0: + return False + + conf_actual = rec.get('confianza_nombre', 0.0) + if nombre_viejo == nombre and conf_actual > confianza: + return False + + if not hasattr(self, '_votos_nombre'): + self._votos_nombre = {} + + if gid not in self._votos_nombre: + self._votos_nombre[gid] = {} + + votos_gid = self._votos_nombre[gid] + + if any(n != nombre for n in votos_gid.keys()): + votos_gid.clear() + + if nombre not in votos_gid: + votos_gid[nombre] = [] + + votos_gid[nombre].append(confianza) + votos_gid[nombre] = votos_gid[nombre][-6:] + + n_votos = len(votos_gid[nombre]) + conf_media = sum(votos_gid[nombre]) / n_votos + + if n_votos < VOTOS_NOMBRE_MIN or conf_media < UMBRAL_CONFIRMACION: + return False + + if nombre_viejo is not None and nombre_viejo != nombre: + if confianza < 0.70: + print(f" [PROTECCIÓN] Rechazando cambio de {nombre_viejo} a {nombre} (conf={confianza:.2f} < 0.70)") + return False # Rechazar cambio automático + # Si confianza >= 0.70, permitir cambio pero con advertencia + print(f" [CAMBIO VIP] ID {gid} cambió de {nombre_viejo} a {nombre} (conf={confianza:.2f} >= 0.70)") + + rec['nombre'] = nombre + rec['confianza_nombre'] = conf_media + self.nombres_activos[nombre] = gid + + votos_gid.clear() + + tipo = "FUSIÓN" if es_fusion else "BAUTIZO" + print(f" [{tipo}] ID {gid} confirmado como {nombre} | Votos: {n_votos} | Conf. Media: {conf_media:.2f}") + return True + + def confirmar_firma_vip(self, gid, ts=None): + with self.lock: + rec = self.db.get(gid) + if rec and rec.get('ema') is not None: + # ⚡ FIX P2: Respetar la cristalización + if rec.get('cristalizado', False): + print(f" [MEMORIA] ID {gid} está cristalizado. No se añaden más firmas VIP.") + return + + firma_actual = rec['ema'] + if 'firmas_altas' not in rec: + rec['firmas_altas'] = [] + rec['firmas_altas'].append({ + k: v.copy() if isinstance(v, np.ndarray) else v + for k, v in firma_actual.items() + }) + if ts is not None: + rec['ts'] = ts + print(f" [MEMORIA] Firma de ID {gid} bloqueada y protegida como VIP.") + + def guardar_saludo(self, nombre): + self.ultimos_saludos[nombre] = {'timestamp': time.time()} + + def _actualizar_sin_lock(self, gid, firma, cam_id, now): + if gid not in self.db: + self.db[gid] = { + 'ema': firma, + 'last_cam': cam_id, + 'ts': now, + 'nombre': None, + 'actualizaciones_globales': 1, + 'cristalizado': False + } + return + + rec = self.db[gid] + + # ⚡ CRISTALIZACIÓN: Después de 15 actualizaciones, CONGELAR EMA + if not rec.get('cristalizado', False) and rec.get('actualizaciones_globales', 0) >= 15: + rec['cristalizado'] = True + print(f" [CRISTALIZADO] ID {gid} ha alcanzado madurez. EMA congelado permanentemente.") + + # ⚡ BLINDAJE DE CRISTALIZACIÓN + # Si el ID ya es maduro, actualizamos dónde fue visto por última vez, + # pero NUNCA permitimos que una cámara modifique su firma maestra perfecta. + if rec.get('cristalizado', False): + rec['last_cam'] = cam_id + rec['ts'] = now + rec['actualizaciones_globales'] += 1 + return + + ema = rec['ema'] + rec['actualizaciones_globales'] += 1 + + if ema is None: + rec['ema'] = firma + else: + alpha_deep = 0.85 + alpha_color = 0.50 + alpha_geo = 0.80 + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'ratio_hw' in firma: + ema['ratio_hw'] = alpha_geo * ema['ratio_hw'] + (1-alpha_geo) * firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = 0.80 * np.asarray(ema['anatomia']) + 0.20 * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = firma['calidad'] + + rec['last_cam'] = cam_id + rec['ts'] = now + + def _sim_contra_firma(self, firma_nueva, firma_guardada, cross_cam, confianza_fisica): + if firma_guardada is None: return 0.0 + return similitud_hibrida(firma_nueva, firma_guardada, cross_cam, confianza_fisica) + + def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0): + ema = gid_data.get('ema') + if not ema: return 0.0 + + sim_ema = self._sim_contra_firma(firma_nueva, ema, cross_cam, confianza_fisica) + + if sim_ema > 0.75: + return sim_ema + + sim_anclas = [sim_ema] + for ancla in gid_data.get('firmas_altas', []): + sim_anclas.append(self._sim_contra_firma(firma_nueva, ancla, cross_cam, confianza_fisica)) + + mejor_ancla = max(sim_anclas[1:]) if len(sim_anclas) > 1 else sim_ema + return sim_ema * 0.60 + mejor_ancla * 0.40 + + def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0): + self.limpiar_fantasmas() + + with self.lock: + candidatos = [] + + for gid, data in self.db.items(): + dt = now - data.get('ts', now) + distancia = self._distancia_topologica(str(data['last_cam']), str(cam_id)) + + misma_cam = (distancia == 0) + es_vecino = (distancia == 1) + es_salto = (distancia == 2) + es_cross = not misma_cam + + if gid in active_gids: + if misma_cam or distancia >= 3: continue + if dt > TIEMPO_MAX_AUSENCIA or data.get('ema') is None: continue + if self.is_id_locked(gid, cam_id, now): continue + + if not misma_cam: + if (es_vecino and dt < 0.3) or (es_salto and dt < 2.0) or (distancia >= 3 and dt < 8.0): + continue + + # ⚡ CÁLCULO OBLIGATORIO DE SIMILITUD ANTES DE CUALQUIER FILTRO + sim = self._sim_robusta(firma, data, es_cross, confianza_fisica) + + sim_deep, sim_color, sim_anat = -1.0, -1.0, -1.0 + ema = data.get("ema") + if ema is not None: + try: + sim_deep, sim_color, sim_anat = desglose_similitud(firma, ema, cross_cam=es_cross) + except: + pass + + # Penalizaciones y ajustes + if not misma_cam: + # NUNCA bloqueamos cámaras vecinas (distancia 1) por tiempo, + # porque al estar solapadas, pueden ver a la persona en el mismo milisegundo (dt=0.0). + if es_salto and dt < 1.5: + continue # Mínimo 1.5s para saltar una cámara entera + if distancia >= 3 and dt < 5.0: + continue # Mínimo 5.0s para cruzar a la otra punta del edificio + + if firma_ema_local is not None: + sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica) + sim = 0.60 * sim + 0.40 * sim_local + + # ⚡ VALIDACIONES VIP + es_vip = data.get('nombre') is not None + tiene_firmas_altas = len(data.get('firmas_altas', [])) > 0 + + if es_vip and tiene_firmas_altas and sim < 0.78: + continue + elif es_vip and not misma_cam and sim < 0.75: + continue + + # ───────────────────────────────────────────────────────────── + # ⚡ UMBRALES CORREGIDOS (Ni parálisis, ni robo de identidad) + # ───────────────────────────────────────────────────────────── + penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002) + + if misma_cam: umbral = 0.68 + elif es_vecino: umbral = 0.72 + elif es_salto: umbral = 0.75 + else: umbral = 0.78 + + # AHORA SÍ: sim_anat y sim_deep existen + if sim_anat > 0.90 and sim_deep > 0.65: + umbral -= 0.04 + + if en_borde and not misma_cam: + umbral -= 0.04 + + umbral = max(0.65, umbral + penal_multitud) + + bonus_aplicado = min(self._bonus_reserva(gid, cam_id, now), 0.03) + sim += bonus_aplicado + + # ⚡ RESTAURAR LOGS DE EVAL PARA DIAGNÓSTICO + if sim >= umbral - 0.10 or len(candidatos) < 3: + estado = "ACEPTADO" if sim >= umbral else "RECHAZADO" + faltante = umbral - sim if sim < umbral else 0.0 + info = f"(Faltó {faltante:.2f})" if faltante > 0 else "" + bonus_str = f" [+{bonus_aplicado:.2f}]" if bonus_aplicado > 0 else "" + + # ⚡ FIX P0: Eliminada la llamada doble a desglose_similitud. + # Usamos sim_deep, sim_color y sim_anat pre-calculados. + color_str = f"{sim_color:.2f}" if sim_color >= 0 else "N/A" + anat_str = f"{sim_anat:.2f}" if sim_anat >= 0 else "N/A" + + print( + f"[EVAL] Cam{cam_id} evalúa ID{gid} " + f"(Cam{data['last_cam']}, dt={dt:.1f}s) | " + f"Deep={sim_deep:.2f} | Color={color_str} | Anat={anat_str} | " + f"Final={sim:.2f}{bonus_str} | Umb={umbral:.2f} {info} -> {estado}" + ) + + if sim >= umbral: + candidatos.append((sim, sim, gid)) + + firma_guardar = firma_ema_local if firma_ema_local is not None else firma + + if not candidatos: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + candidatos.sort(reverse=True) + best_sim, _, best_gid = candidatos[0] + + # ⚡ ANTI-GEMELOS RECALIBRADO: Solo duda si son casi idénticos (<0.02) + if len(candidatos) >= 2: + _, segunda_sim, segundo_gid = candidatos[1] + if abs(best_sim - segunda_sim) < 0.02 and best_sim < 0.75: + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + return nid, False + + if best_gid in self.reserva_temporal: del self.reserva_temporal[best_gid] + + self._actualizar_sin_lock(best_gid, firma_guardar, cam_id, now) + return best_gid, True + + def limpiar_fantasmas(self): + with self.lock: + ahora = time.time() + muertos = [] + for g, d in self.db.items(): + if d.get('nombre') is None and (ahora - d.get('ts', ahora)) > 600: + muertos.append(g) + elif d.get('nombre') is not None and (ahora - d.get('ts', ahora)) > 900: + muertos.append(g) + for g in muertos: + del self.db[g] + if muertos: + self.guardar_memoria() + + def _distancia_topologica(self, cam_o, cam_d): + if cam_o == cam_d: return 0 + vecinos_directos = VECINOS.get(cam_o, []) + if cam_d in vecinos_directos: return 1 + for v in vecinos_directos: + if cam_d in VECINOS.get(v, []): return 2 + return 3 + +# ────────────────────────────────────────────────────────────────────────────── +# GESTOR LOCAL +# ────────────────────────────────────────────────────────────────────────────── +def iou_overlap(A, B): + xA,yA = max(A[0],B[0]),max(A[1],B[1]) + xB,yB = min(A[2],B[2]),min(A[3],B[3]) + inter = max(0,xB-xA)*max(0,yB-yA) + return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-inter+1e-6) + +class CamManager: + def __init__(self, cam_id, global_mem): + self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] + self.ultimas_detecciones_globales = {} + self.tracks_post_cruce = {} + + def _limpiar_tracks_post_cruce(self, now): + #Limpia registros de post-cruce antiguos. + antiguos = [idx for idx, info in self.tracks_post_cruce.items() if now - info['ts_cruce'] > 10.0] + for idx in antiguos: + del self.tracks_post_cruce[idx] + + def _detectar_grupo(self, trk, box, todos): + x1,y1,x2,y2 = box + w,h = x2-x1, y2-y1 + cx,cy = (x1+x2)/2, (y1+y2)/2 + fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35) + for o in todos: + if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue + if iou_overlap(box, o.box) > 0.05: return True + ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2 + if abs(cx-ocx)= 0.48: + trk.fallos_post_grupo = 0; trk.firma_pre_grupo = None + with self.global_mem.lock: + if trk.gid in self.global_mem.db: self.global_mem.db[trk.gid]['ema'] = fa + else: + trk.fallos_post_grupo += 1 + if trk.fallos_post_grupo >= 3: + trk.gid = trk.firma_pre_grupo = None; trk.fallos_post_grupo = 0 + + def _asignar(self, boxes, confidences, frame_hd, now, keypoints_list): + n_trk, n_det = len(self.trackers), len(boxes) + firmas = [None] * n_det + + # ───────────────────────────────────────────────────────────── + # ⚡ OPTIMIZACIÓN BATCH: Extraemos OSNet para todas las detecciones de un golpe + # ───────────────────────────────────────────────────────────── + if n_det > 0: + rois_validos = [] + mapa_rois = {} + h_hd, w_hd = frame_hd.shape[:2] + + # 1. Recolectar todos los recortes (ROIs) de la imagen + for d_idx, box in enumerate(boxes): + x1, y1, x2, y2 = box + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + # Descartamos basura óptica de inmediato + if roi.size > 0 and roi.shape[0] >= 40 and roi.shape[1] >= 20: + mapa_rois[d_idx] = len(rois_validos) + rois_validos.append(roi) + + # 2. Ejecutar la red neuronal para todos SIMULTÁNEAMENTE + if rois_validos: + # ⚡ Llamamos a la función segura que procesa 1 a 1 rápidamente + deep_feats_seguros = extraer_features_osnet_seguro(rois_validos) + + # Ensamblar las firmas + for d_idx, box in enumerate(boxes): + if d_idx in mapa_rois: + df_persona = deep_feats_seguros[mapa_rois[d_idx]] + kpts = keypoints_list[d_idx] if keypoints_list and d_idx < len(keypoints_list) else None + + # Extraemos el resto de la firma (Color enmascarado, Anatomía, etc.) + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts, deep_feat=df_persona) + + def obtener_firma(d_idx): + return firmas[d_idx] + + if n_trk == 0: return [], list(range(n_det)), [], firmas + if n_det == 0: return [], [], list(range(n_trk)), firmas + + # ... (A partir de aquí, el código de "alta" y "baja" confidence se queda EXACTAMENTE como ya lo tienes) ... + alta = [(d,boxes[d]) for d,c in enumerate(confidences) if c >= 0.45] + baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45] + matched, sin_match_trk = [], list(range(n_trk)) + + if alta: + C = np.full((n_trk, len(alta)), 100.0, np.float32) + for t, trk in enumerate(self.trackers): + dt_oculto = now - trk.ts_ultima_deteccion + radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto)))) + ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None) + + for idx, (d, det) in enumerate(alta): + iou = iou_overlap(trk.box, det) + dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2) + if dist > radio: continue + + a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1]) + a_det = (det[2]-det[0])*(det[3]-det[1]) + costo = (1.0*(1-iou)) + (0.3*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.1) + costo += (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.1) + costo += min(0.3, dt_oculto*0.1) + + if ref_firma is not None and obtener_firma(d) is not None: + sim_ap = similitud_hibrida(ref_firma, firmas[d]) + + if sim_ap < 0.25: + costo += 50.0 + else: + costo += (1.0 - sim_ap) * 2.0 + + nombre_vip = self.global_mem.db.get(trk.gid, {}).get('nombre') + umbral_ap = 0.55 if nombre_vip else 0.40 + if sim_ap < umbral_ap: + costo += (umbral_ap - sim_ap) * 3.0 + + C[t,idx] = costo + + for r,c in zip(*linear_sum_assignment(C)): + if C[r,c] <= 6.5: + matched.append((r, alta[c][0])); sin_match_trk.remove(r) + + if sin_match_trk and baja: + C2 = np.ones((len(sin_match_trk), len(baja)), np.float32) + for ti, trk_i in enumerate(sin_match_trk): + for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det) + nuevos_sin = [] + for r,c in zip(*linear_sum_assignment(C2)): + if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0])) + else: nuevos_sin.append(sin_match_trk[r]) + sin_match_trk = nuevos_sin + + return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas + + def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo): + # ───────────────────────────────────────────────────────────── + # 1. Resolver fusiones globales + # ───────────────────────────────────────────────────────────── + for trk in self.trackers: + if trk.gid: + with self.global_mem.lock: + d = self.global_mem.db.get(trk.gid) + if d and 'fusionado_con' in d: + trk.gid = d['fusionado_con'] + + # ───────────────────────────────────────────────────────────── + # 2. Predict y filtrar tracks muertos por Kalman + # ───────────────────────────────────────────────────────────── + self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None] + if not turno_activo: + return self.trackers + + # ───────────────────────────────────────────────────────────── + # 3. Asignar detecciones a tracks existentes + # ───────────────────────────────────────────────────────────── + matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints) + active_gids = {t.gid for t in self.trackers if t.gid is not None} + fh, fw = frame_hd.shape[:2] + + # ───────────────────────────────────────────────────────────── + # 4. DETECCIÓN DE CRUCES + Registro para recuperación post-cruce + # ───────────────────────────────────────────────────────────── + tracks_en_cruce_locals = set() + for i in range(len(self.trackers)): + for j in range(i + 1, len(self.trackers)): + trk_i, trk_j = self.trackers[i], self.trackers[j] + if trk_i.gid is None or trk_j.gid is None or trk_i.box is None or trk_j.box is None: continue + + iou = iou_overlap(trk_i.box, trk_j.box) + if iou > 0.30: + tracks_en_cruce_locals.add(trk_i.local_id) + tracks_en_cruce_locals.add(trk_j.local_id) + if not hasattr(self, 'tracks_post_cruce'): self.tracks_post_cruce = {} + self.tracks_post_cruce[trk_i.local_id] = {'gid_antes': trk_i.gid, 'ts_cruce': now} + self.tracks_post_cruce[trk_j.local_id] = {'gid_antes': trk_j.gid, 'ts_cruce': now} + + + # ───────────────────────────────────────────────────────────── + # 5. PROCESAR MATCHES + # ───────────────────────────────────────────────────────────── + for t_idx, d_idx in matched: + trk, box = self.trackers[t_idx], boxes[d_idx] + + # Asegurar firma visual REAL (mitigación del bug de contaminación en _asignar) + if firmas[d_idx] is None: + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts) + + firma_det = firmas[d_idx] + + # Debug + if trk.gid is None: + print(f" [DEBUG] LOCAL Trk {trk.local_id}: listo={trk.listo_para_id}, cap_locales={getattr(trk, 'muestras_capturadas', 0)}") + else: + with self.global_mem.lock: + db_act = self.global_mem.db.get(trk.gid, {}).get('actualizaciones_globales', 0) + print(f" [MEMORIA] ID {trk.gid} ha sido actualizado {db_act} veces en la red global.") + + # Detección de grupo + es_grupo = self._detectar_grupo(trk, box, self.trackers) + if not trk.en_grupo and es_grupo: + with self.global_mem.lock: + trk.firma_pre_grupo = self.global_mem.db.get(trk.gid, {}).get('ema') + trk.ts_salio_grupo = 0.0 + elif trk.en_grupo and not es_grupo: + trk.ts_salio_grupo = now + trk.en_grupo = es_grupo + + # Update Kalman + trk.update(box, es_grupo, now, kpts=(keypoints[d_idx] if keypoints else None), firma_actual=firma_det) + + # Geometría de frame + margen_x = fw * 0.05 + esta_en_centro = (box[0] > margen_x and box[2] < (fw - margen_x)) + + # ───────────────────────────────────────────────────────── + # 5a. APRENDIZAJE EMA (zona segura, centro del frame) + # ───────────────────────────────────────────────────────── + if firma_det is not None and not es_grupo and esta_en_centro: + trk.actualizar_ema(firma_det) + + # Nutrir memoria global cada 2 segundos + if trk.gid is not None: + if not hasattr(trk, 'ultimo_update_global'): + trk.ultimo_update_global = 0 + if (now - trk.ultimo_update_global) > 2.0: + with self.global_mem.lock: + if trk.gid in self.global_mem.db: + self.global_mem.db[trk.gid]['ema'] = trk.firma_ema.copy() + self.global_mem.db[trk.gid]['ts'] = now + self.global_mem.db[trk.gid]['actualizaciones_globales'] = \ + self.global_mem.db[trk.gid].get('actualizaciones_globales', 0) + 1 + trk.ultimo_update_global = now + + # ───────────────────────────────────────────────────────── + # 5b. RESCATE DE BORDE (Acelerado) + # ───────────────────────────────────────────────────────── + elif firma_det is not None and not es_grupo and not esta_en_centro: + muestras_act = getattr(trk, 'muestras_capturadas', 0) + if muestras_act < 3: + trk.actualizar_ema(firma_det) + elif muestras_act < 5 and trk.frames_observados % 3 == 0: + trk.actualizar_ema(firma_det) + + # ───────────────────────────────────────────────────────── + # 5c. IDENTIFICACIÓN CON PROTECCIÓN + # ───────────────────────────────────────────────────────── + if trk.gid is None and trk.listo_para_id and firma_det is not None: + if getattr(trk, 'muestras_capturadas', 0) >= 4: + if trk.local_id in tracks_en_cruce_locals: + print(f" [CRUCE] Track {trk.local_id} en cruce físico. Bloqueando identificación.") + else: + tiempo_enfriamiento = now - getattr(trk, 'ultimo_cambio_id', 0) + if tiempo_enfriamiento < 0.6: + print(f" [HYSTERESIS] Track {trk.local_id} en enfriamiento ({tiempo_enfriamiento:.2f}s)") + else: + gid_c, es_reid = self.global_mem.identificar_candidato( + firma_det, self.cam_id, now, active_gids, + en_borde=not esta_en_centro, + firma_ema_local=trk.firma_ema, + confianza_fisica=trk.confianza_fisica + ) + if gid_c: + active_gids.add(gid_c) + trk.gid = gid_c + trk.origen_global = es_reid + trk.ultimo_cambio_id = now + + # ───────────────────────────────────────────────────────────── + # 6. VERIFICACIÓN POST-CRUCE (recuperar ID original tras separarse) + # ───────────────────────────────────────────────────────────── + if hasattr(self, 'tracks_post_cruce') and self.tracks_post_cruce: + tracks_post_cruce_nuevos = {} + + # ⚡ CAMBIO CRÍTICO: Iterar por local_id, no por índice + for local_id, info in list(self.tracks_post_cruce.items()): + # Buscar el track con este local_id (único e inmutable) + trk = next((t for t in self.trackers if t.local_id == local_id), None) + if trk is None: + continue # El track ya murió, ignorar + + # Solo actuar si ya pasaron >2 segundos desde el cruce + if now - info['ts_cruce'] > 2.0: + # Si el ID cambió durante el cruce (o se asignó uno nuevo incorrecto) + if trk.gid is not None and trk.gid != info['gid_antes']: + if trk.firma_ema is not None and info['gid_antes'] in self.global_mem.db: + ema_original = self.global_mem.db[info['gid_antes']].get('ema') + if ema_original is not None: + sim_recuperacion = similitud_hibrida( + trk.firma_ema, ema_original, cross_cam=False + ) + if sim_recuperacion > 0.85: + # Verificar que el ID original no esté activo en otro track + if info['gid_antes'] not in active_gids: + print(f" [POST-CRUCE] Track {trk.local_id} recuperó ID {info['gid_antes']} (sim={sim_recuperacion:.2f})") + active_gids.discard(trk.gid) + trk.gid = info['gid_antes'] + active_gids.add(trk.gid) + # No guardamos este registro más (ya cumplió su ciclo) + continue + + # Aún no pasan 2 segundos, mantener registro (usando local_id) + tracks_post_cruce_nuevos[local_id] = info + + self.tracks_post_cruce = tracks_post_cruce_nuevos + + # ───────────────────────────────────────────────────────────── + # 7. CREAR NUEVOS TRACKERS para detecciones sin match + # ───────────────────────────────────────────────────────────── + for d_idx in new_dets: + nt = KalmanTrack(boxes[d_idx], now) + f = firmas[d_idx] + if f: + nt.actualizar_ema(f) + self.trackers.append(nt) + + # ───────────────────────────────────────────────────────────── + # 8. LIMPIEZA: matar tracks sin detección por >3 segundos + # ───────────────────────────────────────────────────────────── + tracks_vivos = [] + for t in self.trackers: + if (now - t.ts_ultima_deteccion) < 3.0: + tracks_vivos.append(t) + self.trackers = tracks_vivos + + # ⚡ FIX P0: Purga de memory leak en tracks_post_cruce + if hasattr(self, 'tracks_post_cruce'): + local_ids_vivos = {t.local_id for t in self.trackers} + self.tracks_post_cruce = {lid: info for lid, info in self.tracks_post_cruce.items() if lid in local_ids_vivos} + + # ───────────────────────────────────────────────────────────── + # 9. Gestión post-grupo (recuperación de firma tras salir de multitud) + # ───────────────────────────────────────────────────────────── + self._gestionar_post_grupo(now, frame_hd) + + return self.trackers + +class CamStream: + def __init__(self, url): + self.url = url; self.cap = cv2.VideoCapture(url) + self.q = queue.Queue(maxsize=1); self.stopped = False + threading.Thread(target=self._run, daemon=True).start() + def _run(self): + while not self.stopped: + ret, f = self.cap.read() + if not ret: time.sleep(2); self.cap.open(self.url); continue + if self.q.full(): + try: self.q.get_nowait() + except queue.Empty: pass + self.q.put(f) + @property + def frame(self): + try: return self.q.get(timeout=0.5) + except queue.Empty: return None + def stop(self): self.stopped = True; self.cap.release() + +def dibujar_track(frame_show, trk): + try: x1,y1,x2,y2 = map(int,trk.box) + except: return + if trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}" + elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]" + elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]" + elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]" + 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) + +# ────────────────────────────────────────────────────────────────────────────── +# MAIN +# ────────────────────────────────────────────────────────────────────────────── +def main(): + print("\n=== Tracker Autónomo Definitivo ===") + print("Persistencia activa. Los IDs se recuerdan entre reinicios.") + print("Tecla [q] para salir (la memoria se guarda automáticamente).\n") + + model = YOLO("yolov8n-pose.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 + ultimo_guardado = time.time() + + try: + while True: + now, tiles = time.time(), [] + 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, confs, kpts = [], [], [] + 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: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + + if es_humano_valido(kpt_persona, box, conf): + boxes.append(box) + confs.append(conf) + kpts.append(kpt_persona) + + ESQUELETO = [(0,1),(0,2),(1,3),(2,4),(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)] + for pk in kpts: + if pk is None: continue + for a,b in ESQUELETO: + if pk[a][2]>0.40 and pk[b][2]>0.40: + cv2.line(frame_show, (int(pk[a][0]),int(pk[a][1])), (int(pk[b][0]),int(pk[b][1])), (255,0,255), 2) + for kx, ky, kc in pk: + if kc > 0.40: cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1) + + tracks = managers[cid].update(boxes, confs, kpts, frame_show, frame, 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 getattr(t,'gid',None) 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[:3]),np.hstack(tiles[3:])])) + + idx += 1 + + if now - ultimo_guardado > 30: + global_mem.guardar_memoria() + ultimo_guardado = now + + if cv2.waitKey(1) == ord('q'): break + + finally: + global_mem.guardar_memoria() + print("[Memoria] Guardada antes de salir.") + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() """ \ No newline at end of file diff --git a/interfaz.py b/interfaz.py new file mode 100644 index 0000000..bfddd57 --- /dev/null +++ b/interfaz.py @@ -0,0 +1,1335 @@ +import customtkinter as ctk +from tkinter import ttk +from PIL import Image +import cv2 +import json +import os +import threading +import csv +from datetime import datetime +from tkinter import filedialog +import time +# Importamos el motor de IA +import fision1 + +ctk.set_appearance_mode("Dark") +ctk.set_default_color_theme("blue") +CONFIG_FILE = "config.json" +DB_PATH = "db_institucion" + +class SmartSoftApp(ctk.CTk): + def __init__(self): + super().__init__() + self.title("SmartSoft IA - Centro de Control") + self.geometry("1250x800") + self.minsize(1100, 700) + + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + + self.motor_corriendo = False + self.construir_login() + + + def log_evento(self, mensaje, tipo="INFO"): + """Registra el evento en la terminal interna y lanza una notificación flotante (Toast)""" + # 1. Escribir en la Terminal Interna (Registro permanente) + if hasattr(self, 'terminal_texto'): + hora = datetime.now().strftime('%H:%M:%S') + self.terminal_texto.configure(state="normal") + self.terminal_texto.insert("end", f"[{hora}] [{tipo}] {mensaje}\n") + self.terminal_texto.see("end") # Auto-scroll hacia abajo + self.terminal_texto.configure(state="disabled") + + # 2. Notificación Flotante (Toast) + if hasattr(self, 'toast_frame') and self.toast_frame.winfo_exists(): + self.toast_frame.destroy() + + color_fondo = "#27ae60" if tipo == "ÉXITO" else ("#c0392b" if tipo == "ERROR" else "#2980b9") + + self.toast_frame = ctk.CTkFrame(self, fg_color=color_fondo, corner_radius=8) + self.toast_frame.place(relx=0.98, rely=0.92, anchor="se") # Esquina inferior derecha + + ctk.CTkLabel(self.toast_frame, text=mensaje, font=ctk.CTkFont(weight="bold", size=13), text_color="white").pack(padx=25, pady=12) + + # El toast se autodestruye suavemente tras 3.5 segundos + self.after(3500, self.destruir_toast) + + def destruir_toast(self): + if hasattr(self, 'toast_frame') and self.toast_frame.winfo_exists(): + self.toast_frame.destroy() + + + # ────────────────────────────────────────────────────────────────────────── + # PANTALLA 1: LOGIN + # ────────────────────────────────────────────────────────────────────────── + def construir_login(self): + self.frame_fondo = ctk.CTkFrame(self, fg_color="transparent") + self.frame_fondo.grid(row=0, column=0, columnspan=2, sticky="nsew") + + self.frame_login = ctk.CTkFrame(self.frame_fondo, width=400, height=400, corner_radius=15) + self.frame_login.place(relx=0.5, rely=0.5, anchor="center") + + ctk.CTkLabel(self.frame_login, text="SmartSoft IA", font=ctk.CTkFont(size=32, weight="bold")).pack(pady=(40, 5)) + ctk.CTkLabel(self.frame_login, text="Code Innovations", text_color="#2ecc71", font=ctk.CTkFont(size=14)).pack(pady=(0, 40)) + + self.entry_user = ctk.CTkEntry(self.frame_login, placeholder_text="Usuario Operador", width=280, height=40) + self.entry_user.pack(pady=10) + self.entry_user.insert(0, "admin") + + self.entry_pass = ctk.CTkEntry(self.frame_login, placeholder_text="Contraseña", width=280, height=40, show="•") + self.entry_pass.pack(pady=10) + self.entry_pass.insert(0, "1234") + + self.lbl_error = ctk.CTkLabel(self.frame_login, text="", text_color="#e74c3c") + self.lbl_error.pack(pady=5) + + btn_login = ctk.CTkButton(self.frame_login, text="Acceder al Sistema", command=self.validar_login, width=280, height=45, font=ctk.CTkFont(weight="bold")) + btn_login.pack(pady=20) + + def validar_login(self): + if self.entry_user.get() == "admin" and self.entry_pass.get() == "1234": + self.frame_fondo.destroy() + self.construir_app_principal() + else: + self.lbl_error.configure(text="Acceso denegado. Verifique sus credenciales.") + + # ────────────────────────────────────────────────────────────────────────── + # PANTALLA 2: APLICACIÓN PRINCIPAL + # ────────────────────────────────────────────────────────────────────────── + def construir_app_principal(self): + self.ip_var = ctk.StringVar() + self.user_var = ctk.StringVar() + self.pass_var = ctk.StringVar() + self.secuencia_var = ctk.StringVar() + self.ignoradas_var = ctk.StringVar() + self.vecinos_var = ctk.StringVar() + self.cams_por_pantalla_var = ctk.StringVar(value="4") # ⚡ VARIABLE RECUPERADA + + self.cargar_configuracion() + + # SIDEBAR + self.sidebar_frame = ctk.CTkFrame(self, width=250, corner_radius=0) + self.sidebar_frame.grid(row=0, column=0, sticky="nsew") + self.sidebar_frame.grid_rowconfigure(5, weight=1) + + ctk.CTkLabel(self.sidebar_frame, text="SmartSoft IA", font=ctk.CTkFont(size=24, weight="bold")).grid(row=0, column=0, padx=20, pady=(30, 5)) + ctk.CTkLabel(self.sidebar_frame, text="Centro de Control", text_color="#2ecc71", font=ctk.CTkFont(size=12)).grid(row=1, column=0, padx=20, pady=(0, 30)) + + self.btn_iniciar = ctk.CTkButton(self.sidebar_frame, text="▶ Iniciar Motor IA", fg_color="#27ae60", hover_color="#2ecc71", height=45, command=self.iniciar_sistema) + self.btn_iniciar.grid(row=2, column=0, padx=20, pady=10) + + self.lbl_estado = ctk.CTkLabel(self.sidebar_frame, text="Estado: EN ESPERA", text_color="gray") + self.lbl_estado.grid(row=3, column=0, padx=20, pady=5) + + # ⚡ CONTENEDOR DE BOTONES DESLIZANTES + self.frame_paginas_control = ctk.CTkFrame(self.sidebar_frame, fg_color="transparent") + self.frame_paginas_control.grid(row=4, column=0, padx=20, pady=15, sticky="ew") + + # TABS + self.tabview = ctk.CTkTabview(self, corner_radius=10) + self.tabview.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") + + self.tab_monitor = self.tabview.add("📷 Monitor en Vivo") + self.tab_reporte = self.tabview.add("📊 Auditoría de Accesos") + self.tab_registro = self.tabview.add("👤 Gestión de Personal") + self.tab_config = self.tabview.add("⚙️ Parámetros") + + self.crear_tab_monitor() + self.crear_tab_reporte() + self.crear_tab_registro() + self.crear_tab_configuracion() + + # ⚡ EVENTO: Doble Clic para Zoom + self.lbl_video.bind("", self.zoom_tactico_camara) + + + # TABS + self.tabview = ctk.CTkTabview(self, corner_radius=10) + self.tabview.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") + + + self.tab_monitor = self.tabview.add("📷 Monitor en Vivo") + self.tab_reporte = self.tabview.add("📊 Auditoría de Accesos") + self.tab_registro = self.tabview.add("👤 Gestión de Personal") + self.tab_config = self.tabview.add("⚙️ Parámetros") + + self.sidebar_visible = True + # ⚡ EVENTO: Capturar la tecla 'Enter' globalmente + self.bind('', self.atajo_teclado_enter) + + self.crear_tab_monitor() + self.crear_tab_reporte() + self.crear_tab_registro() + self.crear_tab_configuracion() + # ───────────────────────────────────────────────────────────── + # ⚡ BARRA DE TELEMETRÍA (HUD INFERIOR) + # ───────────────────────────────────────────────────────────── + self.hud_frame = ctk.CTkFrame(self, height=30, fg_color="#1a1a1a", corner_radius=0) + self.hud_frame.grid(row=1, column=1, sticky="ew") + + self.lbl_hud_fps = ctk.CTkLabel(self.hud_frame, text="⚡ Motor: OFF", font=ctk.CTkFont(size=11, weight="bold"), text_color="#7f8c8d") + self.lbl_hud_fps.pack(side="left", padx=20) + + self.lbl_hud_mem = ctk.CTkLabel(self.hud_frame, text="🧠 Memoria RAM: 0 IDs", font=ctk.CTkFont(size=11, weight="bold"), text_color="#7f8c8d") + self.lbl_hud_mem.pack(side="left", padx=20) + + self.lbl_hud_alertas = ctk.CTkLabel(self.hud_frame, text="🚨 Alertas: 0", font=ctk.CTkFont(size=11, weight="bold"), text_color="#7f8c8d") + self.lbl_hud_alertas.pack(side="right", padx=20) + + # ───────────────────────────────────────────────────────────── + # ⚡ LIMPIEZA AUTOMÁTICA DE CACHÉ (Mantenimiento Silencioso) + # ───────────────────────────────────────────────────────────── + self.limpiar_basura_temporal() + + def atajo_teclado_enter(self, event): + """Ejecuta la acción principal de la pestaña en la que esté el usuario""" + pestana_actual = self.tabview.get() + if pestana_actual == "📊 Auditoría de Accesos": + self.cargar_datos_csv() + self.log_evento("Búsqueda ejecutada por teclado", "INFO") + elif pestana_actual == "👤 Gestión de Personal": + if self.buscar_empleado_var.get().strip() != "": + self.buscar_registro_visual() + + def toggle_modo_enfoque(self): + """Colapsa el menú lateral para dar 100% de pantalla a la videovigilancia""" + if self.sidebar_visible: + self.sidebar_frame.grid_remove() # Oculta la columna + self.btn_focus.configure(text="▶ Mostrar Menú", fg_color="#34495e", hover_color="#2c3e50") + self.sidebar_visible = False + self.log_evento("Modo Enfoque Activado", "INFO") + else: + self.sidebar_frame.grid() # Restaura la columna + self.btn_focus.configure(text="⛶ Modo Enfoque", fg_color="#8e44ad", hover_color="#9b59b6") + self.sidebar_visible = True + + + def limpiar_basura_temporal(self): + """Elimina recortes de rostros temporales antiguos para no saturar el disco""" + try: + if not os.path.exists("cache_nombres"): os.makedirs("cache_nombres") + for archivo in os.listdir("cache_nombres"): + if archivo.startswith("alerta_") and archivo.endswith(".jpg"): + ruta = os.path.join("cache_nombres", archivo) + # Si la alerta tiene más de 24 horas, se elimina + if time.time() - os.path.getmtime(ruta) > 86400: + os.remove(ruta) + except Exception: pass + + def generar_botonera_deslizante(self): + for widget in self.frame_paginas_control.winfo_children(): widget.destroy() + camaras_disponibles = [c.strip() for c in self.secuencia_var.get().split(',') if c.strip()] + total_cams = len(camaras_disponibles) + try: cams_por_pagina = max(1, int(self.cams_por_pantalla_var.get())) + except ValueError: cams_por_pagina = 4 + + import math + total_paginas = math.ceil(total_cams / cams_por_pagina) + if total_paginas <= 1: return + + ctk.CTkLabel(self.frame_paginas_control, text="PANTALLAS DISPONIBLES", font=ctk.CTkFont(size=10, weight="bold"), text_color="gray").pack(pady=5) + self.botones_paginas = [] + for i in range(total_paginas): + btn = ctk.CTkButton(self.frame_paginas_control, text=f"📺 Sección {i+1}", height=32, + fg_color="#34495e" if i != 0 else "#2980b9", + command=lambda idx=i: self.cambiar_seccion_mosaico(idx)) + btn.pack(fill="x", pady=4) + self.botones_paginas.append(btn) + + def cambiar_seccion_mosaico(self, indice_pagina): + # ⚡ FIX: Obligamos al motor de IA a cambiar la página globalmente + fision1.PAGINA_ACTUAL = indice_pagina + + # Limpiamos temporalmente la pantalla para forzar el redibujado + try: + self.lbl_video.configure(image=self.img_ctk_vacia) + except Exception: pass + + # Actualizamos los colores de la botonera + for idx, btn in enumerate(self.botones_paginas): + if idx == indice_pagina: + btn.configure(fg_color="#2980b9", font=ctk.CTkFont(weight="bold")) + else: + btn.configure(fg_color="#34495e", font=ctk.CTkFont(weight="normal")) + + self.log_evento(f"Visualizando Sección {indice_pagina + 1}", "INFO") + + +# ────────────────────────────────────────────────────────────────────────── + # PESTAÑA 1: MONITOR EN VIVO Y ALERTAS DE INTRUSIÓN + # ────────────────────────────────────────────────────────────────────────── + def crear_tab_monitor(self): + self.tab_monitor.grid_columnconfigure(0, weight=1) + self.tab_monitor.grid_columnconfigure(1, weight=0) + self.tab_monitor.grid_rowconfigure(1, weight=1) + + # --- BARRA DE HERRAMIENTAS TÁCTICA --- + frame_toolbar = ctk.CTkFrame(self.tab_monitor, fg_color="transparent", height=40) + frame_toolbar.grid(row=0, column=0, columnspan=2, sticky="ew") + + self.btn_toggle_alertas = ctk.CTkButton(frame_toolbar, text="👁 Mostrar Alertas (0)", fg_color="#34495e", hover_color="#2c3e50", command=self.toggle_panel_alertas, width=150) + self.btn_toggle_alertas.pack(side="right", padx=10, pady=5) + + # (Dentro de crear_tab_monitor, donde está btn_toggle_alertas) + self.btn_focus = ctk.CTkButton(frame_toolbar, text="⛶ Modo Enfoque", fg_color="#8e44ad", hover_color="#9b59b6", command=self.toggle_modo_enfoque, width=130) + self.btn_focus.pack(side="left", padx=10, pady=5) + + + # --- PANEL IZQUIERDO (VIDEO) --- + self.frame_video_container = ctk.CTkFrame(self.tab_monitor, fg_color="black", corner_radius=10) + self.frame_video_container.grid(row=1, column=0, sticky="nsew", padx=(0, 5)) + self.frame_video_container.grid_rowconfigure(0, weight=1) + self.frame_video_container.grid_columnconfigure(0, weight=1) + + self.lbl_video = ctk.CTkLabel(self.frame_video_container, text="Inicie el Motor IA desde el panel lateral.") + self.lbl_video.grid(row=0, column=0, sticky="nsew") + + # ⚡ EVENTO: Doble Clic para Zoom Táctico + self.lbl_video.bind("", self.zoom_tactico_camara) + + # --- PANEL DERECHO (ALERTAS) --- + self.panel_alertas = ctk.CTkScrollableFrame(self.tab_monitor, fg_color="#1a1a1a", corner_radius=10, width=300) + self.alertas_visibles = False + + header_alertas = ctk.CTkFrame(self.panel_alertas, fg_color="#c0392b", corner_radius=5) + header_alertas.pack(fill="x", pady=(0, 10)) + ctk.CTkLabel(header_alertas, text="🚨 NO IDENTIFICADOS", font=ctk.CTkFont(weight="bold", size=13), text_color="white").pack(pady=8) + + self.lbl_no_alertas = ctk.CTkLabel(self.panel_alertas, text="Área segura.\nSin detecciones pendientes.", text_color="gray") + self.lbl_no_alertas.pack(pady=30) + + self.alertas_ui_activas = {} + + def toggle_panel_alertas(self): + if self.alertas_visibles: + self.panel_alertas.grid_forget() + self.btn_toggle_alertas.configure(text=f"👁 Mostrar Alertas ({len(self.alertas_ui_activas)})") + self.alertas_visibles = False + else: + self.panel_alertas.grid(row=1, column=1, sticky="nsew", padx=(5, 0)) + self.btn_toggle_alertas.configure(text="🙈 Ocultar Panel") + self.alertas_visibles = True + + def procesar_intruso(self, id_rastreo, camara, imagen_bgr): + """Filtro de calidad y generación de alerta""" + if id_rastreo in self.alertas_ui_activas: return + + # ⚡ FILTRO DE CALIDAD CORREGIDO (Solo validamos que la foto no esté corrupta) + h, w = imagen_bgr.shape[:2] + if h < 20 or w < 20: return + + # Guardado temporal en caché y envío al panel lateral + if not os.path.exists("cache_nombres"): os.makedirs("cache_nombres") + ruta_alerta = os.path.join("cache_nombres", f"alerta_{id_rastreo}.jpg") + cv2.imwrite(ruta_alerta, imagen_bgr) + self.inyectar_alerta_ui(id_rastreo, camara, ruta_alerta, imagen_bgr) + + def inyectar_alerta_ui(self, id_rastreo, camara, ruta_rostro, img_bgr): + self.lbl_no_alertas.pack_forget() + + tarjeta = ctk.CTkFrame(self.panel_alertas, fg_color="#2b2b2b", corner_radius=8) + tarjeta.pack(fill="x", pady=5, padx=5) + + try: + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + img_pil = Image.fromarray(img_rgb) + img_ctk = ctk.CTkImage(light_image=img_pil, size=(70, 70)) + lbl_foto = ctk.CTkLabel(tarjeta, image=img_ctk, text="") + lbl_foto.image_ref = img_ctk + lbl_foto.pack(side="left", padx=10, pady=10) + except Exception: pass + + info_frame = ctk.CTkFrame(tarjeta, fg_color="transparent") + info_frame.pack(side="left", fill="both", expand=True) + + ctk.CTkLabel(info_frame, text=f"Sujeto ID: {id_rastreo}", font=ctk.CTkFont(weight="bold", size=12), text_color="#e74c3c").pack(anchor="w", pady=(10, 0)) + ctk.CTkLabel(info_frame, text=f"Visto en: Cam {camara}", font=ctk.CTkFont(size=11), text_color="gray").pack(anchor="w") + + # ⚡ LOS DOS BOTONES TÁCTICOS + btn_frame = ctk.CTkFrame(info_frame, fg_color="transparent") + btn_frame.pack(fill="x", pady=(5, 10)) + + btn_registrar = ctk.CTkButton(btn_frame, text="✚ Registrar", width=70, height=24, fg_color="#27ae60", hover_color="#2ecc71", + command=lambda: self.quick_enroll(id_rastreo, img_bgr, tarjeta)) + btn_registrar.pack(side="left", padx=(0, 5)) + + btn_descartar = ctk.CTkButton(btn_frame, text="✖ Descartar", width=70, height=24, fg_color="#7f8c8d", hover_color="#95a5a6", + command=lambda: self.descartar_alerta(tarjeta, id_rastreo)) + btn_descartar.pack(side="left") + + self.alertas_ui_activas[id_rastreo] = tarjeta + + # Actualizar contador HUD + if self.alertas_visibles: + self.btn_toggle_alertas.configure(text="🙈 Ocultar Panel") + else: + self.btn_toggle_alertas.configure(text=f"👁 Mostrar Alertas ({len(self.alertas_ui_activas)})") + self.lbl_hud_alertas.configure(text=f"🚨 Alertas: {len(self.alertas_ui_activas)}", text_color="#e74c3c") + + def descartar_alerta(self, tarjeta_widget, id_rastreo): + tarjeta_widget.destroy() + if id_rastreo in self.alertas_ui_activas: + del self.alertas_ui_activas[id_rastreo] + + total = len(self.alertas_ui_activas) + if total == 0: + self.lbl_no_alertas.pack(pady=30) + self.lbl_hud_alertas.configure(text="🚨 Alertas: 0", text_color="#7f8c8d") + + if not self.alertas_visibles: + self.btn_toggle_alertas.configure(text=f"👁 Mostrar Alertas ({total})") + + def quick_enroll(self, id_rastreo, img_bgr, tarjeta_widget): + """Puente directo desde la Alerta hasta el Formulario de Registro""" + self.descartar_alerta(tarjeta_widget, id_rastreo) + + # Cambiamos a la pestaña de registro + self.tabview.set("👤 Gestión de Personal") + + # Inyectamos la foto y habilitamos el campo de nombre + self.face_temporal = img_bgr + self.genero_temporal = "Desconocido" # El guardia lo verificará visualmente + + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + img_pil = Image.fromarray(img_rgb) + img_ctk = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(200, 200)) + + self.lbl_cam_registro.image_ref = img_ctk + self.lbl_cam_registro.configure(image=img_ctk, text="") + + self.lbl_feedback_reg.configure(text="✅ Intruso importado. Ingrese el nombre para enrolar.", text_color="#f1c40f") + self.entry_nombre_reg.configure(state="normal") + self.btn_guardar_nuevo.configure(state="normal") + self.entry_nombre_reg.focus() + + def guardar_configuracion(self): + data = { + "dvr_ip": self.ip_var.get(), "dvr_user": self.user_var.get(), + "dvr_pass": self.pass_var.get(), "secuencia_total": self.secuencia_var.get(), + "ignoradas": self.ignoradas_var.get(), "vecinos": self.vecinos_var.get(), + "cams_por_pantalla": self.cams_por_pantalla_var.get() + } + with open(CONFIG_FILE, 'w') as f: json.dump(data, f, indent=4) + + # ⚡ FIX ABSOLUTO: Inyección (Hot-Reload) de la cuadrícula + if self.motor_corriendo: + try: + # 1. Actualizamos el número en el cerebro del motor + fision1.CAMS_POR_PANTALLA = max(1, int(self.cams_por_pantalla_var.get())) + # 2. Obligamos al motor a regresar a la página 0 para evitar errores de índice + fision1.PAGINA_ACTUAL = 0 + # 3. Redibujamos los botones azules de la izquierda + self.generar_botonera_deslizante() + # 4. Forzamos visualmente el clic en el botón 1 para que la pantalla "parpadee" y se ajuste + if hasattr(self, 'botones_paginas') and self.botones_paginas: + self.cambiar_seccion_mosaico(0) + + self.log_evento("Cuadrícula actualizada dinámicamente", "ÉXITO") + except Exception as e: pass + + self.btn_guardar.configure(text="¡Configuración Actualizada!", fg_color="green") + self.after(2000, lambda: self.btn_guardar.configure(text="Guardar Parámetros", fg_color="#1f538d")) + + def iniciar_sistema(self): + if not self.motor_corriendo: + self.guardar_configuracion() + + # ⚡ FIX CRÍTICO: El motor DEBE obedecer a la interfaz, no a su código duro interno + sec_str = self.secuencia_var.get() + fision1.SECUENCIA = [int(x.strip()) for x in sec_str.split(',') if x.strip()] + fision1.URLS = [f"rtsp://{self.user_var.get()}:{self.pass_var.get()}@{self.ip_var.get()}:554/Streaming/Channels/{i}02" for i in fision1.SECUENCIA] + + try: fision1.CAMS_POR_PANTALLA = max(1, int(self.cams_por_pantalla_var.get())) + except: fision1.CAMS_POR_PANTALLA = 4 + fision1.PAGINA_ACTUAL = 0 + + # Encendemos el botón de detener + self.btn_iniciar.configure(state="normal", text="⏹ DETENER MOTOR IA", fg_color="#c0392b", hover_color="#e74c3c") + self.lbl_estado.configure(text="Estado: EN LÍNEA", text_color="#2ecc71") + self.motor_corriendo = True + + self.generar_botonera_deslizante() + self.tabview.set("📷 Monitor en Vivo") + + threading.Thread(target=fision1.main, daemon=True).start() + self.after(45, self.actualizar_video) + self.log_evento("Sistema de inferencia iniciado", "ÉXITO") + else: + # --- RUTINA DE APAGADO SEGURO --- + fision1.MOTOR_ACTIVO = False + self.motor_corriendo = False + self.btn_iniciar.configure(state="normal", text="▶ Iniciar Motor IA", fg_color="#27ae60", hover_color="#2ecc71") + self.lbl_estado.configure(text="Estado: DETENIDO", text_color="#7f8c8d") + try: self.lbl_video.configure(image=self.img_ctk_vacia, text="Motor IA Detenido. Esperando conexión...") + except Exception: pass + for widget in self.frame_paginas_control.winfo_children(): widget.destroy() + self.log_evento("Motor IA detenido por el usuario", "INFO") + + def zoom_tactico_camara(self, event): + if not hasattr(fision1, 'CAMARA_MAXIMIZADA'): fision1.CAMARA_MAXIMIZADA = None + + # Si ya hay zoom, el doble clic lo desactiva + if fision1.CAMARA_MAXIMIZADA is not None: + fision1.CAMARA_MAXIMIZADA = None + self.log_evento("Zoom táctico desactivado", "INFO") + return + + ancho_lbl = self.lbl_video.winfo_width() + alto_lbl = self.lbl_video.winfo_height() + + try: cams_actuales = int(self.cams_por_pantalla_var.get()) + except: cams_actuales = 4 + if cams_actuales <= 1: return + + import math + cols = math.ceil(math.sqrt(cams_actuales)) + rows = math.ceil(cams_actuales / cols) + + col_click = int(event.x // (ancho_lbl / cols)) + row_click = int(event.y // (alto_lbl / rows)) + idx_relativo = (row_click * cols) + col_click + + try: + # ⚡ FIX: Calculamos la cámara real basada en la Secuencia Inyectada + secuencia = getattr(fision1, 'SECUENCIA', []) + idx_global = (fision1.PAGINA_ACTUAL * cams_actuales) + idx_relativo + if idx_global < len(secuencia): + fision1.CAMARA_MAXIMIZADA = secuencia[idx_global] + self.log_evento(f"Zoom activado en Cámara {fision1.CAMARA_MAXIMIZADA}", "INFO") + except Exception: pass + + def actualizar_video(self): + if not self.motor_corriendo: return + frame_bgr = fision1.obtener_ultimo_frame() + if frame_bgr is not None: + ancho = self.tab_monitor.winfo_width() - 10 + alto = self.tab_monitor.winfo_height() - 10 + if ancho > 200 and alto > 200: + frame_resized = cv2.resize(frame_bgr, (ancho, alto), interpolation=cv2.INTER_LINEAR) + frame_rgb = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB) + img_pil = Image.fromarray(frame_rgb) + + img_ctk = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(ancho, alto)) + self.lbl_video.image_ref = img_ctk + + try: + self.lbl_video.configure(image=img_ctk, text="") + except Exception: pass + + # ⚡ ACTUALIZAR LA BARRA DE TELEMETRÍA EN TIEMPO REAL + try: + if hasattr(fision1, 'TELEMETRIA'): + fps = fision1.TELEMETRIA.get('fps', 0) + ids_ram = fision1.TELEMETRIA.get('ids_activos', 0) + + color_fps = "#2ecc71" if fps > 15 else ("#f1c40f" if fps > 8 else "#e74c3c") + self.lbl_hud_fps.configure(text=f"⚡ Motor: {fps:.1f} FPS", text_color=color_fps) + self.lbl_hud_mem.configure(text=f"🧠 Memoria RAM: {ids_ram} IDs activos", text_color="#3498db") + except Exception: pass + + # ───────────────────────────────────────────────────────────── + # ⚡ PUENTE DE ALERTAS: Leer Desconocidos desde el Motor IA + # ───────────────────────────────────────────────────────────── + try: + if hasattr(fision1, 'NUEVAS_ALERTAS') and len(fision1.NUEVAS_ALERTAS) > 0: + alerta = fision1.NUEVAS_ALERTAS.pop(0) # Extrae: (gid, cam_id, roi_cabeza) + self.procesar_intruso(alerta[0], alerta[1], alerta[2]) + except Exception: pass + + self.after(45, self.actualizar_video) + + # ────────────────────────────────────────────────────────────────────────── + # PESTAÑA 2: AUDITORÍA (REPORTES) + # ────────────────────────────────────────────────────────────────────────── + def crear_tab_reporte(self): + # ⚡ DIVISIÓN DE PANTALLA: 70% Tabla, 30% Timeline + self.tab_reporte.grid_columnconfigure(0, weight=1) + # Columna 1 (Panel) es rígida, NUNCA se hará más pequeña de 320 píxeles + self.tab_reporte.grid_columnconfigure(1, weight=0, minsize=320) + self.tab_reporte.grid_rowconfigure(2, weight=1) + + # 1. FILA SUPERIOR: Buscador y Filtros + frame_ctrl = ctk.CTkFrame(self.tab_reporte, fg_color="transparent") + frame_ctrl.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0,5)) + + ctk.CTkLabel(frame_ctrl, text="Nombre:").pack(side="left", padx=(5,2)) + self.search_var = ctk.StringVar() + ctk.CTkEntry(frame_ctrl, textvariable=self.search_var, width=150).pack(side="left", padx=(0,15)) + + ctk.CTkLabel(frame_ctrl, text="Fecha:").pack(side="left", padx=(5,2)) + self.date_var = ctk.StringVar() + ctk.CTkEntry(frame_ctrl, textvariable=self.date_var, width=110, placeholder_text="YYYY-MM-DD").pack(side="left", padx=(0,15)) + + ctk.CTkButton(frame_ctrl, text="🔍 Buscar", command=self.cargar_datos_csv, width=100).pack(side="left", padx=5) + ctk.CTkButton(frame_ctrl, text="🔄 Limpiar", fg_color="#d35400", hover_color="#e67e22", command=self.recargar_completo, width=100).pack(side="left", padx=10) + + # 2. FILA INTERMEDIA: Controles de Vista y Exportación Ejecutiva + frame_modo = ctk.CTkFrame(self.tab_reporte, fg_color="#2b2b2b", corner_radius=8) + frame_modo.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5,15), ipadx=5, ipady=5) + + self.modo_vista_var = ctk.StringVar(value="asistencia") + + rb_asistencia = ctk.CTkRadioButton(frame_modo, text="Control de Asistencia", variable=self.modo_vista_var, value="asistencia", command=self.cargar_datos_csv, text_color="#2ecc71", font=ctk.CTkFont(weight="bold")) + rb_asistencia.pack(side="left", padx=15, pady=5) + + rb_crudo = ctk.CTkRadioButton(frame_modo, text="Auditoría Forense", variable=self.modo_vista_var, value="crudo", command=self.cargar_datos_csv) + rb_crudo.pack(side="left", padx=15, pady=5) + + btn_exportar = ctk.CTkButton(frame_modo, text="📄 Exportar a Excel", fg_color="#27ae60", hover_color="#2ecc71", command=self.exportar_reporte) + btn_exportar.pack(side="right", padx=15) + + # 3. ÁREA IZQUIERDA: LA TABLA + style = ttk.Style() + style.theme_use("default") + style.configure("Treeview", background="#2b2b2b", foreground="white", fieldbackground="#2b2b2b", rowheight=28, borderwidth=0, font=("Arial", 9)) + style.configure("Treeview.Heading", background="#1a1a1a", foreground="white", relief="flat", font=("Arial", 10, "bold")) + style.map("Treeview", background=[("selected", "#2980b9")]) + + # Frame contenedor para la tabla y su scroll + frame_tabla = ctk.CTkFrame(self.tab_reporte, fg_color="transparent") + frame_tabla.grid(row=2, column=0, sticky="nsew", padx=(0, 10)) + frame_tabla.grid_columnconfigure(0, weight=1) + frame_tabla.grid_rowconfigure(0, weight=1) + + self.tabla = ttk.Treeview(frame_tabla, show="headings", style="Treeview") + self.tabla.tag_configure('bautizo', background="#154360", foreground="#AED6F1") + self.tabla.tag_configure('normal', background="#2b2b2b", foreground="white") + self.tabla.tag_configure('presente', background="#1e8449", foreground="white") + + scroll = ttk.Scrollbar(frame_tabla, orient="vertical", command=self.tabla.yview) + self.tabla.configure(yscrollcommand=scroll.set) + + self.tabla.grid(row=0, column=0, sticky="nsew") + scroll.grid(row=0, column=1, sticky="ns") + + # ⚡ EVENTO: Detectar clics en la tabla para armar la ruta visual + self.tabla.bind("", self.dibujar_hilo_ariadna) + + # 4. ÁREA DERECHA: PANEL TIMELINE (HILO DE ARIADNA) + self.panel_timeline = ctk.CTkScrollableFrame(self.tab_reporte, fg_color="#1a1a1a", corner_radius=10) + self.panel_timeline.grid(row=2, column=1, sticky="nsew") + + self.header_timeline = ctk.CTkLabel(self.panel_timeline, text="TRAZABILIDAD", font=ctk.CTkFont(weight="bold", size=14), text_color="gray") + self.header_timeline.pack(pady=(10, 20)) + + self.lbl_vacio_timeline = ctk.CTkLabel(self.panel_timeline, text="Seleccione un registro\npara rastrear la ruta.", text_color="#555555") + self.lbl_vacio_timeline.pack(pady=50) + + # Carga inicial + self.cargar_datos_csv() + + def configurar_columnas_tabla(self, modo): + self.tabla.delete(*self.tabla.get_children()) + if modo == "crudo": + columnas = ("Fecha/Hora", "Nombre del Empleado", "Origen", "Destino", "Tiempo (s)", "Re-ID Corporal", "Certeza Facial") + anchos = [150, 200, 70, 70, 80, 110, 110] + else: + columnas = ("Fecha", "Nombre del Empleado", "Hora Llegada", "Punto Llegada", "Última Vista", "Punto Salida", "Permanencia Total", "Estado") + anchos = [100, 200, 100, 100, 100, 100, 130, 90] + + self.tabla["columns"] = columnas + for i, col in enumerate(columnas): + self.tabla.heading(col, text=col) + self.tabla.column(col, anchor="center", width=anchos[i]) + + def dibujar_hilo_ariadna(self, event): + # Limpiar el panel actual + for widget in self.panel_timeline.winfo_children(): + if widget != self.header_timeline: + widget.destroy() + + seleccion = self.tabla.selection() + if not seleccion: + self.lbl_vacio_timeline = ctk.CTkLabel(self.panel_timeline, text="Seleccione un registro\npara rastrear la ruta.", text_color="#555555") + self.lbl_vacio_timeline.pack(pady=50) + return + + item = self.tabla.item(seleccion[0]) + valores = item['values'] + + # Extraer Fecha y Nombre según el modo de vista actual + modo = self.modo_vista_var.get() + if modo == "asistencia": + fecha_buscada = valores[0] + nombre_buscado = valores[1] + else: # modo crudo + fecha_buscada = valores[0].split(" ")[0] # Extraer solo el YYYY-MM-DD + nombre_buscado = valores[1] + + self.header_timeline.configure(text=f"RUTA: {nombre_buscado.upper()}", text_color="white") + ctk.CTkLabel(self.panel_timeline, text=f"Fecha: {fecha_buscada}", font=ctk.CTkFont(size=11), text_color="#3498db").pack(pady=(0, 15)) + + # ⚡ 1. LEER CSV Y FILTRAR LA RUTA (AHORA EXTRALLENDO LA IMAGEN) + ruta_csv = os.path.join("cache_nombres", "registro_movimientos.csv") + pasos = [] + try: + with open(ruta_csv, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader, None) + for row in reader: + if len(row) >= 4: + if fecha_buscada in row[0] and str(nombre_buscado).lower() == str(row[1]).lower(): + hora = row[0].split(" ")[1] + origen = row[2] + destino = row[3] + duracion = row[4] + + # La columna 8 (índice 7) tiene la ruta de la foto que guardó seguimiento2.py + ruta_img = row[7] if len(row) >= 8 else "" + + # Es un bautizo (entrada inicial) + if origen == destino and float(duracion) == 0.0: + pasos.append({"hora": hora, "cam": origen, "tipo": "entrada", "img": ruta_img}) + else: + pasos.append({"hora": hora, "cam": destino, "tipo": "cruce", "img": ruta_img}) + except Exception as e: + print(f"Error trazando ruta: {e}") + return + + # ⚡ 2. DIBUJAR LOS NODOS Y SUS IMÁGENES + if not pasos: + ctk.CTkLabel(self.panel_timeline, text="Sin datos de ruta", text_color="#e74c3c").pack() + return + + for i, paso in enumerate(pasos): + frame_nodo = ctk.CTkFrame(self.panel_timeline, fg_color="transparent") + frame_nodo.pack(fill="x", padx=10, pady=5) # Padding extendido para dar espacio a la foto + + # Columna Izquierda: El Dibujo del Nodo + frame_dibujo = ctk.CTkFrame(frame_nodo, fg_color="transparent", width=30) + frame_dibujo.pack(side="left", fill="y") + + if i == 0: + color_nodo, texto_bold = "#2ecc71", True + elif i == len(pasos) - 1: + color_nodo, texto_bold = "#e67e22", True + else: + color_nodo, texto_bold = "#3498db", False + + dot = ctk.CTkFrame(frame_dibujo, width=14, height=14, corner_radius=7, fg_color=color_nodo) + dot.pack(pady=(15, 0)) # Centramos el nodo con la imagen + + if i < len(pasos) - 1: + linea = ctk.CTkFrame(frame_dibujo, width=2, height=45, fg_color="#333333") + linea.pack(pady=2) + + # Columna Central: Textos + frame_info = ctk.CTkFrame(frame_nodo, fg_color="transparent") + frame_info.pack(side="left", fill="both", expand=True, padx=10) + + font_principal = ctk.CTkFont(weight="bold", size=13) if texto_bold else ctk.CTkFont(size=13) + ctk.CTkLabel(frame_info, text=f"Cámara {paso['cam']}", font=font_principal, text_color="white" if texto_bold else "#cccccc").pack(anchor="w", pady=(10, 0)) + ctk.CTkLabel(frame_info, text=f"{paso['hora']}", font=ctk.CTkFont(size=11), text_color="gray").pack(anchor="w") + + # ⚡ SOLUCIÓN PARTE 1: Columna Derecha (RENDER DE LA IMAGEN) + ruta_imagen = paso.get("img", "") + if ruta_imagen and os.path.exists(ruta_imagen): + try: + img_pil = Image.open(ruta_imagen) + # Forzamos un tamaño cuadrado uniforme de 50x50 para la máquina de estados + img_ctk = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(50, 50)) + lbl_img = ctk.CTkLabel(frame_nodo, text="", image=img_ctk, corner_radius=5) + lbl_img.image_ref = img_ctk # Previene que Python borre la imagen + lbl_img.pack(side="right", padx=5) + except Exception as e: + pass # Si la foto se corrompió, simplemente no dibuja nada y no crashea + + def exportar_reporte(self): + filepath = filedialog.asksaveasfilename( + defaultextension=".csv", + filetypes=[("Archivo CSV (Excel)", "*.csv"), ("Todos los archivos", "*.*")], + title="Exportar Reporte de Asistencia" + ) + if not filepath: return + + try: + with open(filepath, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + # Extraer y escribir cabeceras + columnas = [self.tabla.heading(c, 'text') for c in self.tabla["columns"]] + writer.writerow(columnas) + # Escribir filas + for item in self.tabla.get_children(): + writer.writerow(self.tabla.item(item)['values']) + print(f"Reporte exportado a {filepath}") + except Exception as e: + print(f"Error exportando: {e}") + + def recargar_completo(self): + self.search_var.set("") + self.date_var.set("") + self.cargar_datos_csv() + + def cargar_datos_csv(self): + modo = self.modo_vista_var.get() + self.configurar_columnas_tabla(modo) + + ruta_csv = os.path.join("cache_nombres", "registro_movimientos.csv") + busqueda_n = self.search_var.get().lower().strip() + busqueda_f = self.date_var.get().strip() + + if not os.path.exists(ruta_csv): return + + try: + with open(ruta_csv, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader, None) # Saltar cabecera + filas = list(reader) + + if modo == "crudo": + # LÓGICA DE AUDITORÍA FORENSE (Cruce por Cruce) + for row in reversed(filas): + if len(row) < 8: row.extend(["0.0", "0.0", "0"] * (8 - len(row))) + f_csv, n_csv = row[0], row[1].lower() + origen, destino, tiempo = row[2], row[3], row[4] + + es_bautizo = (origen == destino and float(tiempo) == 0.0) + if es_bautizo: + row[2], row[3], row[4] = f"Cam {origen}", "Detección", "INICIAL" + tag = 'bautizo' + else: + row[2], row[3], row[4] = f"Cam {origen}", f"Cam {destino}", f"{tiempo}s" + tag = 'normal' + + row[5] = f"{row[5]}%" if float(row[5]) > 0 else "N/A" + row[6] = f"{row[6]}%" if float(row[6]) > 0 else "N/A" + + if (busqueda_n == "" or busqueda_n in n_csv) and (busqueda_f == "" or busqueda_f in f_csv): + self.tabla.insert("", "end", values=row[:7], tags=(tag,)) + + else: + # LÓGICA DE CONTROL DE ASISTENCIA (Agrupado por Día y Persona) + asistencia = {} + + for row in filas: + if len(row) < 4: continue + dt_str, n_original, origen, destino = row[0], row[1], row[2], row[3] + n_csv = n_original.lower() + + if (busqueda_n != "" and busqueda_n not in n_csv) or (busqueda_f != "" and busqueda_f not in dt_str): + continue + + try: + dt_obj = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') + fecha_str = dt_obj.strftime('%Y-%m-%d') + hora_str = dt_obj.strftime('%H:%M:%S') + except Exception: continue + + key = (fecha_str, n_original.title().replace('_', ' ')) + + if key not in asistencia: + asistencia[key] = { + 'llegada_dt': dt_obj, 'llegada_str': hora_str, 'cam_llegada': origen, + 'salida_dt': dt_obj, 'salida_str': hora_str, 'cam_salida': destino + } + else: + # Evaluar si es un registro más antiguo (Llegada) o más nuevo (Salida) + if dt_obj < asistencia[key]['llegada_dt']: + asistencia[key]['llegada_dt'] = dt_obj + asistencia[key]['llegada_str'] = hora_str + asistencia[key]['cam_llegada'] = origen + if dt_obj > asistencia[key]['salida_dt']: + asistencia[key]['salida_dt'] = dt_obj + asistencia[key]['salida_str'] = hora_str + asistencia[key]['cam_salida'] = destino + + fecha_hoy = datetime.now().strftime('%Y-%m-%d') + + # Ordenar por fecha más reciente primero y poblar tabla + for key, data in sorted(asistencia.items(), reverse=True): + fecha, nombre = key + t_perm = data['salida_dt'] - data['llegada_dt'] + segundos = t_perm.total_seconds() + + horas, resto = divmod(segundos, 3600) + minutos, _ = divmod(resto, 60) + + if segundos == 0: + # Solo se pinta de verde si el registro ocurrió el día de HOY + if fecha == fecha_hoy: + perm_str = "Recién Llegado" + estado = "En Instalaciones" + tag = "presente" + else: + # Si es de días pasados y solo hubo una vista, es un cruce único + perm_str = "Cruce Único" + estado = "Registrado" + tag = "normal" + else: + perm_str = f"{int(horas)}h {int(minutos)}m" + estado = "Registrado" + tag = "normal" + + valores = (fecha, nombre, data['llegada_str'], f"Cam {data['cam_llegada']}", + data['salida_str'], f"Cam {data['cam_salida']}", perm_str, estado) + self.tabla.insert("", "end", values=valores, tags=(tag,)) + + except Exception as e: + print(f"Error procesando reporte: {e}") + + # ────────────────────────────────────────────────────────────────────────── + # PESTAÑA 3: GESTIÓN DE PERSONAL (MÁQUINA DE ESTADOS + CRUD) + # ────────────────────────────────────────────────────────────────────────── + def crear_tab_registro(self): + self.tab_registro.grid_columnconfigure((0, 1), weight=1) + self.tab_registro.grid_rowconfigure(0, weight=1) + + self.face_temporal = None + self.genero_temporal = None + + # ====================================================================== + # --- PANEL IZQUIERDO: NUEVO ENROLAMIENTO --- + # ====================================================================== + frame_nuevo = ctk.CTkFrame(self.tab_registro) + frame_nuevo.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") + + ctk.CTkLabel(frame_nuevo, text="Paso 1: Captura Biométrica", font=ctk.CTkFont(weight="bold", size=16)).pack(pady=(15, 5)) + + self.lbl_cam_registro = ctk.CTkLabel(frame_nuevo, text="Monitor inactivo", bg_color="black", width=360, height=202) + self.lbl_cam_registro.pack(pady=10) + + self.cam_reg_activa = False + self.cap_registro = None + + frame_ctrl_cam = ctk.CTkFrame(frame_nuevo, fg_color="transparent") + frame_ctrl_cam.pack(fill="x", padx=20, pady=5) + + ctk.CTkLabel(frame_ctrl_cam, text="Seleccionar Cámara:").pack(side="left", padx=5) + + camaras_disponibles = [c.strip() for c in self.secuencia_var.get().split(',') if c.strip()] + if not camaras_disponibles: camaras_disponibles = ["7"] + + self.cam_id_reg_var = ctk.StringVar(value=camaras_disponibles[0]) + ctk.CTkOptionMenu(frame_ctrl_cam, variable=self.cam_id_reg_var, values=camaras_disponibles, width=80).pack(side="left", padx=5) + + self.btn_encender_cam = ctk.CTkButton(frame_ctrl_cam, text="Conectar", command=self.toggle_camara_registro, width=100) + self.btn_encender_cam.pack(side="left", padx=15) + + self.btn_capturar = ctk.CTkButton(frame_nuevo, text="📸 Extraer y Validar Rostro", fg_color="#27ae60", hover_color="#2ecc71", command=self.validar_rostro_ia, state="disabled", height=40) + self.btn_capturar.pack(pady=(15, 5)) + + self.lbl_feedback_reg = ctk.CTkLabel(frame_nuevo, text="Esperando captura...", text_color="gray") + self.lbl_feedback_reg.pack(pady=(0, 15)) + + ctk.CTkLabel(frame_nuevo, text="Paso 2: Asignación de Identidad", font=ctk.CTkFont(weight="bold", size=16)).pack(pady=(10, 5)) + ctk.CTkLabel(frame_nuevo, text="Nombre y Apellidos (Sin acentos):").pack(anchor="w", padx=20) + + self.nombre_reg_var = ctk.StringVar() + self.entry_nombre_reg = ctk.CTkEntry(frame_nuevo, textvariable=self.nombre_reg_var, width=300, state="disabled") + self.entry_nombre_reg.pack(pady=5, padx=20) + + self.btn_guardar_nuevo = ctk.CTkButton(frame_nuevo, text="💾 Guardar Registro en DB", fg_color="#2980b9", hover_color="#3498db", command=self.guardar_nuevo_registro, state="disabled", height=40) + self.btn_guardar_nuevo.pack(pady=20) + + # ====================================================================== + # --- PANEL DERECHO: AUDITORÍA VISUAL (CRUD) --- + # ====================================================================== + frame_gestion = ctk.CTkFrame(self.tab_registro) + frame_gestion.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") + + ctk.CTkLabel(frame_gestion, text="Auditoría y Edición de DB", font=ctk.CTkFont(weight="bold", size=16)).pack(pady=15) + + frame_busqueda = ctk.CTkFrame(frame_gestion, fg_color="transparent") + frame_busqueda.pack(fill="x", padx=20, pady=(10, 5)) + + self.buscar_empleado_var = ctk.StringVar() + ctk.CTkEntry(frame_busqueda, textvariable=self.buscar_empleado_var, placeholder_text="Nombre a buscar...").pack(side="left", fill="x", expand=True, padx=(0, 10)) + ctk.CTkButton(frame_busqueda, text="🔍 Buscar", command=self.buscar_registro_visual, width=80).pack(side="left") + + # MENÚ MULTI-BÚSQUEDA INTEGRAD0 + self.resultados_var = ctk.StringVar(value="Resultados...") + self.menu_resultados = ctk.CTkOptionMenu(frame_gestion, variable=self.resultados_var, values=["Resultados..."], command=self.cargar_empleado_seleccionado, state="disabled") + self.menu_resultados.pack(fill="x", padx=40, pady=(0, 15)) + + self.lbl_foto_empleado = ctk.CTkLabel(frame_gestion, text="Sin imagen seleccionada", bg_color="#1a1a1a", width=200, height=200) + self.lbl_foto_empleado.pack(pady=10) + + ctk.CTkLabel(frame_gestion, text="Modificar Nombre:").pack(anchor="w", padx=40) + self.nuevo_nombre_var = ctk.StringVar() + self.entry_nuevo_nom = ctk.CTkEntry(frame_gestion, textvariable=self.nuevo_nombre_var, width=250, state="disabled") + self.entry_nuevo_nom.pack(pady=5) + + self.ruta_imagen_actual = None + self.nombre_actual_seleccionado = None + + frame_acciones = ctk.CTkFrame(frame_gestion, fg_color="transparent") + frame_acciones.pack(fill="x", padx=40, pady=20) + + self.btn_actualizar_reg = ctk.CTkButton(frame_acciones, text="💾 Actualizar", fg_color="#f39c12", hover_color="#e67e22", command=self.actualizar_registro, state="disabled", width=120) + self.btn_actualizar_reg.pack(side="left", padx=5) + + self.btn_eliminar_reg = ctk.CTkButton(frame_acciones, text="🗑️ Eliminar", fg_color="#c0392b", hover_color="#e74c3c", command=self.eliminar_registro, state="disabled", width=120) + self.btn_eliminar_reg.pack(side="right", padx=5) + + # --- LÓGICA DE CÁMARA --- + def toggle_camara_registro(self): + if not self.cam_reg_activa: + cam_id = self.cam_id_reg_var.get().strip() + url = f"rtsp://{self.user_var.get()}:{self.pass_var.get()}@{self.ip_var.get()}:554/Streaming/Channels/{cam_id}02" + + # ⚡ FIX 1: Obligamos a usar TCP para evitar que el DVR colapse por doble petición UDP + os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" + + self.cap_registro = cv2.VideoCapture(url) + self.cap_registro.set(cv2.CAP_PROP_BUFFERSIZE, 1) + + if self.cap_registro.isOpened(): + self.cam_reg_activa = True + self.btn_encender_cam.configure(text="Desconectar", fg_color="#c0392b", hover_color="#e74c3c") + self.btn_capturar.configure(state="normal") + self.lbl_feedback_reg.configure(text="Enfoque el rostro y presione Validar", text_color="white") + self.actualizar_camara_registro() + else: + self.cam_reg_activa = False + if self.cap_registro: + self.cap_registro.release() + + self.lbl_cam_registro.image_ref = None + try: self.lbl_cam_registro.configure(image=None, text="Monitor inactivo") + except Exception: pass + + self.btn_encender_cam.configure(text="Conectar", fg_color="#1f538d", hover_color="#14375e") + self.btn_capturar.configure(state="disabled") + self.resetear_formulario_nuevo() + + def actualizar_camara_registro(self): + if self.cam_reg_activa and self.cap_registro and self.cap_registro.isOpened(): + ret, frame = self.cap_registro.read() + if ret: + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + img_pil = Image.fromarray(frame_rgb) + img_ctk = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(360, 202)) + + # ⚡ Protección estricta de memoria RAM + self.lbl_cam_registro.image_ref = img_ctk + try: + self.lbl_cam_registro.configure(image=img_ctk, text="") + except Exception: pass + + self.after(30, self.actualizar_camara_registro) + + # --- LÓGICA DE VALIDACIÓN Y GUARDADO --- + def validar_rostro_ia(self): + if self.cam_reg_activa and self.cap_registro: + ret, frame = self.cap_registro.read() + if ret: + faces = fision1.app.get(frame) + + if len(faces) == 0: + self.lbl_feedback_reg.configure(text="❌ Rostro no detectado o muy borroso. Intente de nuevo.", text_color="#e74c3c") + self.resetear_formulario_nuevo(solo_datos=True) + return + + face_registro = max(faces, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])) + box = face_registro.bbox.astype(int) + fx, fy, x2, y2 = box[0], box[1], box[2], box[3] + fw, fh = x2 - fx, y2 - fy + h, w = frame.shape[:2] + + # ⚡ FIX 2: Recorte tipo pasaporte perfecto (Solución a image_2d722c.png) + m_x = int(fw * 0.50) + m_y_top = int(fh * 0.50) + m_y_bot = int(fh * 0.85) # Margen gigante hacia abajo para incluir cuello y barbilla + + y1, y2 = max(0, fy - m_y_top), min(h, y2 + m_y_bot) + x1, x2 = max(0, fx - m_x), min(w, x2 + m_x) + + face_roi = frame[y1:y2, x1:x2] + + if face_roi.size > 0: + self.face_temporal = face_roi + self.genero_temporal = "Man" if face_registro.sex == 1 else "Woman" + + self.lbl_feedback_reg.configure(text=f"Verifique la foto capturada ({self.genero_temporal}) e ingrese el nombre.", text_color="#2ecc71") + self.entry_nombre_reg.configure(state="normal") + self.btn_guardar_nuevo.configure(state="normal") + self.entry_nombre_reg.focus() + + self.cam_reg_activa = False + self.btn_encender_cam.configure(text="Reintentar (Descartar)", fg_color="#f39c12", hover_color="#e67e22") + + face_rgb = cv2.cvtColor(face_roi, cv2.COLOR_BGR2RGB) + img_pil = Image.fromarray(face_rgb) + img_ctk = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(200, 200)) + + self.lbl_cam_registro.image_ref = img_ctk + self.lbl_cam_registro.configure(image=img_ctk, text="") + + def guardar_nuevo_registro(self): + nombre = self.nombre_reg_var.get().strip() + if not nombre or self.face_temporal is None: return + + if not os.path.exists("db_institucion"): os.makedirs("db_institucion") + + # ⚡ FIX: Todo lo que va a la IA debe llevar guiones bajos + nombre_str = nombre.replace(' ', '_') + nombre_archivo = f"{nombre_str}.jpg" + ruta = os.path.join("db_institucion", nombre_archivo) + cv2.imwrite(ruta, self.face_temporal) + + ruta_gen = os.path.join("cache_nombres", "generos.json") + dic_gen = {} + if os.path.exists(ruta_gen): + try: + with open(ruta_gen, 'r') as f: dic_gen = json.load(f) + except: pass + + # Guardamos usando la llave con guiones bajos para que la IA la encuentre + dic_gen[nombre_str] = self.genero_temporal + with open(ruta_gen, 'w') as f: json.dump(dic_gen, f) + + self.reconstruir_base_datos() + self.lbl_feedback_reg.configure(text=f"🚀 ¡{nombre} registrado exitosamente en el sistema!", text_color="#3498db") + self.resetear_formulario_nuevo(solo_datos=True) + + def resetear_formulario_nuevo(self, solo_datos=False): + self.face_temporal = None + self.genero_temporal = None + self.nombre_reg_var.set("") + self.entry_nombre_reg.configure(state="disabled") + self.btn_guardar_nuevo.configure(state="disabled") + if not solo_datos: + self.lbl_feedback_reg.configure(text="Esperando captura...", text_color="gray") + + def reconstruir_base_datos(self): + try: + # ⚡ FIX: fision1.gestionar_vectores se importa directo, sin el ".reconocimiento" + fision1.BASE_DATOS_ROSTROS = fision1.gestionar_vectores(actualizar=True) + print("[INFO] DB Sincronizada exitosamente.") + except Exception as e: + print(f"Error sincronizando base de datos: {e}") + +# ========================================================================= + # LÓGICA DE AUDITORÍA VISUAL (CRUD Y MULTI-BÚSQUEDA) + # ========================================================================= + # ========================================================================= + # LÓGICA DE AUDITORÍA VISUAL (CRUD Y MULTI-BÚSQUEDA) + # ========================================================================= + def limpiar_imagen_visor(self, texto="Sin imagen seleccionada"): + """⚡ FIX: Evita el bug pyimage de CustomTkinter usando una imagen transparente""" + img_vacia_pil = Image.new("RGBA", (200, 200), (0,0,0,0)) + self.img_ctk_vacia = ctk.CTkImage(light_image=img_vacia_pil, size=(200,200)) + try: + self.lbl_foto_empleado.configure(image=self.img_ctk_vacia, text=texto) + except Exception: pass + + def buscar_registro_visual(self): + nombre_buscado = self.buscar_empleado_var.get().strip().lower() + if not nombre_buscado: return + + coincidencias = [] + self.diccionario_archivos = getattr(self, 'diccionario_archivos', {}) + + if os.path.exists("db_institucion"): + for archivo in os.listdir("db_institucion"): + if nombre_buscado in archivo.lower() and archivo.endswith(('.jpg', '.png', '.jpeg')): + nombre_limpio = os.path.splitext(archivo)[0].replace('_', ' ') + coincidencias.append(nombre_limpio) + self.diccionario_archivos[nombre_limpio] = archivo + + if coincidencias: + self.menu_resultados.configure(state="normal", values=coincidencias) + self.resultados_var.set(coincidencias[0]) + self.cargar_empleado_seleccionado(coincidencias[0]) + else: + self.limpiar_panel_edicion() + self.limpiar_imagen_visor("Persona no encontrada") + + def cargar_empleado_seleccionado(self, nombre_seleccionado): + self.nombre_actual_seleccionado = nombre_seleccionado + + nombre_archivo = self.diccionario_archivos.get(nombre_seleccionado) + if not nombre_archivo: + nombre_archivo = f"{nombre_seleccionado.replace(' ', '_')}.jpg" + + self.ruta_imagen_actual = os.path.join("db_institucion", nombre_archivo) + + if os.path.exists(self.ruta_imagen_actual): + try: + # ⚡ FIX: Usamos copy() para no bloquear el archivo en el disco duro + with Image.open(self.ruta_imagen_actual) as img_temp: + img_pil = img_temp.copy() + + self.img_ctk_actual = ctk.CTkImage(light_image=img_pil, dark_image=img_pil, size=(200, 200)) + self.lbl_foto_empleado.configure(image=self.img_ctk_actual, text="") + + self.nuevo_nombre_var.set(self.nombre_actual_seleccionado) + self.entry_nuevo_nom.configure(state="normal") + self.btn_eliminar_reg.configure(state="normal") + self.btn_actualizar_reg.configure(state="normal") + except Exception as e: + self.limpiar_imagen_visor("Error cargando foto") + else: + self.limpiar_imagen_visor("Foto no encontrada en disco") + + def actualizar_registro(self): + if not self.ruta_imagen_actual: return + nuevo_nombre = self.nuevo_nombre_var.get().strip() + + if nuevo_nombre and nuevo_nombre != self.nombre_actual_seleccionado: + viejo_str = self.nombre_actual_seleccionado.replace(' ', '_') + nuevo_str = nuevo_nombre.replace(' ', '_') + + extension = os.path.splitext(self.ruta_imagen_actual)[1] + nuevo_archivo = f"{nuevo_str}{extension}" + nueva_ruta = os.path.join("db_institucion", nuevo_archivo) + + # 1. Renombrar archivo + try: + os.rename(self.ruta_imagen_actual, nueva_ruta) + except Exception as e: + print(f"Error renombrando archivo: {e}") + return + + # 2. ⚡ FIX: Traspaso de género blindado + ruta_gen = os.path.join("cache_nombres", "generos.json") + dic_gen = {} + if os.path.exists(ruta_gen): + try: + with open(ruta_gen, 'r') as f: dic_gen = json.load(f) + except Exception: pass + + genero_original = dic_gen.pop(viejo_str, "Man") # Por defecto asume hombre si no hay registro + dic_gen[nuevo_str] = genero_original + + os.makedirs(os.path.dirname(ruta_gen), exist_ok=True) + with open(ruta_gen, 'w') as f: json.dump(dic_gen, f) + + # 3. Actualizar diccionarios y UI dinámicamente + self.diccionario_archivos[nuevo_nombre] = nuevo_archivo + if self.nombre_actual_seleccionado in self.diccionario_archivos: + del self.diccionario_archivos[self.nombre_actual_seleccionado] + + opciones_actuales = self.menu_resultados.cget("values") + nuevas_opciones = [nuevo_nombre if opt == self.nombre_actual_seleccionado else opt for opt in opciones_actuales] + + self.menu_resultados.configure(values=nuevas_opciones) + self.resultados_var.set(nuevo_nombre) + + self.nombre_actual_seleccionado = nuevo_nombre + self.ruta_imagen_actual = nueva_ruta + + try: + self.lbl_foto_empleado.configure(text="¡Registro Actualizado!") + except Exception: pass + + self.reconstruir_base_datos() + + def eliminar_registro(self): + if self.ruta_imagen_actual and os.path.exists(self.ruta_imagen_actual): + try: + os.remove(self.ruta_imagen_actual) + except Exception: pass + + ruta_gen = os.path.join("cache_nombres", "generos.json") + viejo_str = self.nombre_actual_seleccionado.replace(' ', '_') + + if os.path.exists(ruta_gen): + try: + with open(ruta_gen, 'r') as f: dic_gen = json.load(f) + if viejo_str in dic_gen: + del dic_gen[viejo_str] + with open(ruta_gen, 'w') as f: json.dump(dic_gen, f) + except Exception: pass + + self.limpiar_panel_edicion() + self.limpiar_imagen_visor("¡Registro Eliminado!") + self.reconstruir_base_datos() + self.buscar_empleado_var.set("") + + def limpiar_panel_edicion(self): + self.nuevo_nombre_var.set("") + self.entry_nuevo_nom.configure(state="disabled") + self.btn_eliminar_reg.configure(state="disabled") + self.btn_actualizar_reg.configure(state="disabled") + + self.ruta_imagen_actual = None + self.nombre_actual_seleccionado = None + + self.menu_resultados.configure(state="disabled", values=["Resultados..."]) + self.resultados_var.set("Resultados...") + + # ────────────────────────────────────────────────────────────────────────── + # PESTAÑA 4: CONFIGURACIÓN Y TERMINAL + # ────────────────────────────────────────────────────────────────────────── + def crear_tab_configuracion(self): + self.tab_config.grid_columnconfigure((0, 1), weight=1) + + # 1. Panel de Conexión DVR + frame_dvr = ctk.CTkFrame(self.tab_config) + frame_dvr.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") + ctk.CTkLabel(frame_dvr, text="Conexión DVR", font=ctk.CTkFont(weight="bold")).pack(pady=15) + ctk.CTkEntry(frame_dvr, textvariable=self.ip_var, placeholder_text="IP DVR").pack(pady=10, padx=20, fill="x") + ctk.CTkEntry(frame_dvr, textvariable=self.user_var, placeholder_text="Usuario").pack(pady=10, padx=20, fill="x") + ctk.CTkEntry(frame_dvr, textvariable=self.pass_var, placeholder_text="Contraseña", show="*").pack(pady=10, padx=20, fill="x") + + # 2. Panel de Topología IA + frame_top = ctk.CTkFrame(self.tab_config) + frame_top.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") + ctk.CTkLabel(frame_top, text="Topología de Red Neural", font=ctk.CTkFont(weight="bold")).pack(pady=15) + + ctk.CTkLabel(frame_top, text="Secuencia Total:").pack(anchor="w", padx=20) + ctk.CTkEntry(frame_top, textvariable=self.secuencia_var).pack(pady=(0,10), padx=20, fill="x") + + ctk.CTkLabel(frame_top, text="Cámaras permitidas por pantalla:").pack(anchor="w", padx=20) + ctk.CTkEntry(frame_top, textvariable=self.cams_por_pantalla_var, placeholder_text="Ej. 1, 4, 6...").pack(pady=(0,10), padx=20, fill="x") + + ctk.CTkLabel(frame_top, text="Cámaras Bypass (Ignoradas por IA):").pack(anchor="w", padx=20) + ctk.CTkEntry(frame_top, textvariable=self.ignoradas_var).pack(pady=(0,10), padx=20, fill="x") + + ctk.CTkLabel(frame_top, text="Matriz de Adyacencia (JSON):").pack(anchor="w", padx=20) + ctk.CTkEntry(frame_top, textvariable=self.vecinos_var).pack(pady=(0,10), padx=20, fill="x") + + # ⚡ 3. CREACIÓN DEL BOTÓN GUARDAR (Debe existir antes que la Terminal) + self.btn_guardar = ctk.CTkButton(self.tab_config, text="Guardar Parámetros", width=200) + self.btn_guardar.grid(row=1, column=0, columnspan=2, pady=10) + + # ⚡ 4. TERMINAL DE OPERADOR (Historial de Sistema) + frame_terminal = ctk.CTkFrame(self.tab_config) + frame_terminal.grid(row=2, column=0, columnspan=2, padx=10, pady=(10, 10), sticky="nsew") + self.tab_config.grid_rowconfigure(2, weight=1) # Permite que la terminal crezca + + ctk.CTkLabel(frame_terminal, text="Terminal de Eventos y Auditoría del Sistema", font=ctk.CTkFont(weight="bold"), text_color="gray").pack(anchor="w", padx=15, pady=5) + + self.terminal_texto = ctk.CTkTextbox(frame_terminal, fg_color="#000000", text_color="#2ecc71", font=ctk.CTkFont(family="Consolas", size=12)) + self.terminal_texto.pack(fill="both", expand=True, padx=15, pady=(0, 15)) + self.terminal_texto.configure(state="disabled") + + # ⚡ 5. ASIGNACIÓN DEL COMANDO MÚLTIPLE (Guardado + Toast Flotante) + self.btn_guardar.configure(command=lambda: [self.guardar_configuracion(), self.log_evento("Parámetros actualizados en disco", "ÉXITO")]) + + def cargar_configuracion(self): + config = { + "dvr_ip": "192.168.1.244", "dvr_user": "admin", "dvr_pass": "TCA200503", + "secuencia_total": "2, 1, 7, 5, 8, 4, 3, 6", "ignoradas": "2, 4", + "vecinos": '{"1":["7"], "7":["1","5"], "5":["7","8"], "8":["5","3"], "3":["8","6"], "6":["3"]}', + "cams_por_pantalla": "4" # Por defecto 4 cuadrículas + } + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, 'r') as f: config.update(json.load(f)) + except: pass + + self.ip_var.set(config.get("dvr_ip", "")) + self.user_var.set(config.get("dvr_user", "")) + self.pass_var.set(config.get("dvr_pass", "")) + self.secuencia_var.set(config.get("secuencia_total", "")) + self.ignoradas_var.set(config.get("ignoradas", "")) + self.vecinos_var.set(config.get("vecinos", "")) + self.cams_por_pantalla_var.set(config.get("cams_por_pantalla", "4")) + + def guardar_configuracion(self): + data = { + "dvr_ip": self.ip_var.get(), "dvr_user": self.user_var.get(), + "dvr_pass": self.pass_var.get(), "secuencia_total": self.secuencia_var.get(), + "ignoradas": self.ignoradas_var.get(), "vecinos": self.vecinos_var.get(), + "cams_por_pantalla": self.cams_por_pantalla_var.get() + } + with open(CONFIG_FILE, 'w') as f: json.dump(data, f, indent=4) + self.btn_guardar.configure(text="¡Configuración Actualizada!", fg_color="green") + self.after(2000, lambda: self.btn_guardar.configure(text="Guardar Parámetros", fg_color="#1f538d")) + +if __name__ == "__main__": + app = SmartSoftApp() + app.mainloop() \ No newline at end of file diff --git a/mejor.txt b/mejor.txt deleted file mode 100644 index eb83808..0000000 --- a/mejor.txt +++ /dev/null @@ -1,1733 +0,0 @@ -############################################################################# seguimineto -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 -from datetime import datetime -import json -import csv - -# ────────────────────────────────────────────────────────────────────────────── -# CONFIGURACIÓN DEL SISTEMA -# ────────────────────────────────────────────────────────────────────────────── -USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" -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.6 -ASPECT_RATIO_MAX = 4.0 -AREA_MIN_CALIDAD = 1200 -FRAMES_CALIDAD = 3 -TIEMPO_MAX_AUSENCIA = 800.0 - -MAX_FIRMAS_MEMORIA = 10 - -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): - # ⚡ ESCUDO ANTI-SOMBRAS (CLAHE) - # Convertimos a LAB para ecualizar solo la luz (L) sin deformar los colores reales (A y B) - lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) - l, a, b = cv2.split(lab) - clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) - l_eq = clahe.apply(l) - lab_eq = cv2.merge((l_eq, a, b)) - roi_eq = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR) - - # Preprocesamiento original para OSNet - img = cv2.resize(roi_eq, (128, 256)) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - img = img.transpose(2,0,1).astype(np.float32) / 255.0 - img = (img - MEAN) / STD - - # Devolvemos el tensor correcto de 1 sola imagen - return np.expand_dims(img, axis=0) - -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_hd, box_480): - try: - h_hd, w_hd = frame_hd.shape[:2] - escala_x = w_hd / 480.0 - escala_y = h_hd / 270.0 - - x1, y1, x2, y2 = box_480 - x1_hd, y1_hd = int(x1 * escala_x), int(y1 * escala_y) - x2_hd, y2_hd = int(x2 * escala_x), int(y2 * escala_y) - - x1_c, y1_c = max(0, x1_hd), max(0, y1_hd) - x2_c, y2_c = min(w_hd, x2_hd), min(h_hd, y2_hd) - - roi = frame_hd[y1_c:y2_c, x1_c:x2_c] - if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: return None - - calidad_area = (x2_c - x1_c) * (y2_c - y1_c) - - # ⚡ VOLVEMOS AL BATCH DE 16 PARA EVITAR EL CRASH FATAL - blob = preprocess_onnx(roi) # Esto devuelve (1, 3, 256, 128) - - # Creamos el contenedor de 16 espacios que el modelo ONNX exige - blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32) - blob_16[0] = blob[0] # Metemos nuestra imagen en el primer espacio - - # Ejecutamos la inferencia - outputs = ort_session.run(None, {input_name: blob_16}) - deep_feat = outputs[0][0].flatten() # Extraemos solo el primer resultado - - 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 as e: - print(f"Error en extracción: {e}") - return None - -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 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))) - else: - sim_head, sim_torso, sim_legs = 0.0, 0.0, 0.0 - - if cross_cam: - if sim_legs < 0.25: - sim_deep -= 0.15 - return max(0.0, sim_deep) - - sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs) - - 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.05 - 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 - self.firma_pre_grupo = None - self.ts_salio_grupo = 0 - self.validado_post_grupo = True - - 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): - # ⚡ EL SUICIDIO DE CAJAS FANTASMAS - # Si lleva más de 5 frames de ceguera total (YOLO no lo ve), matamos la predicción - if self.time_since_update > 5: - return None - - 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 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.RLock() - self.ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") - self.ultimos_saludos = self._cargar_y_limpiar_saludos() - - def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion_seg): - """Guarda el movimiento y la duración en la cámara anterior en un CSV.""" - directorio = "cache_nombres" - if not os.path.exists(directorio): - os.makedirs(directorio) - - ruta_csv = os.path.join(directorio, "registro_movimientos.csv") - ahora = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - archivo_nuevo = not os.path.exists(ruta_csv) - - try: - with open(ruta_csv, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - if archivo_nuevo: - writer.writerow(["Fecha_Hora", "Nombre", "Origen", "Destino", "Segundos_en_Origen"]) - - # Redondeamos la duración a 2 decimales - writer.writerow([ahora, nombre, cam_origen, cam_destino, round(duracion_seg, 2)]) - f.flush() # Forzamos la escritura física en el disco - print(f" [LOG] {nombre} estuvo {duracion_seg:.1f}s en Cam {cam_origen} antes de pasar a {cam_destino}") - except Exception as e: - print(f"Error al guardar CSV: {e}") - - try: - with open(ruta_csv, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - if archivo_nuevo: - writer.writerow(["Fecha_Hora", "Nombre", "Camara_Origen", "Camara_Destino"]) - - writer.writerow([ahora, nombre, cam_origen, cam_destino]) - print(f" [LOG MOVIMIENTO] {nombre} pasó de Cam {cam_origen} a Cam {cam_destino}") - except Exception as e: - print(f"Error al guardar movimiento: {e}") - - def _cargar_y_limpiar_saludos(self): - """Carga el historial y elimina registros que no sean de hoy.""" - hoy = datetime.now().strftime("%Y-%m-%d") - if os.path.exists(self.ruta_saludos): - try: - with open(self.ruta_saludos, 'r') as f: - datos = json.load(f) - # Solo mantenemos los saludos cuya fecha sea 'hoy' - return {nombre: info for nombre, info in datos.items() - if info.get('fecha') == hoy} - except Exception as e: - print(f"Error cargando saludos: {e}") - return {} - - def guardar_saludo(self, nombre): - """Registra el saludo con la fecha actual en disco.""" - hoy = datetime.now().strftime("%Y-%m-%d") - self.ultimos_saludos[nombre] = { - 'fecha': hoy, - 'timestamp': time.time() - } - with open(self.ruta_saludos, 'w') as f: - json.dump(self.ultimos_saludos, f) - - def _es_transito_posible(self, data, cam_id, now): - cam_origen = str(data['last_cam']) - cam_destino = str(cam_id) - dt = now - data['ts'] - - # 1. Si es la misma cámara, aplicamos la regla normal de tiempo máximo de ausencia - if cam_origen == cam_destino: - return dt < TIEMPO_MAX_AUSENCIA - - # 2. ⚡ DEDUCCIÓN DE TOPOLOGÍA (El sistema aprende el mapa solo) - # Si salta a otra cámara en menos de 1.5 segundos, es físicamente imposible - # a menos que ambas cámaras apunten al mismo lugar (están solapadas). - if dt < 1.5: - # Le damos vía libre inmediata porque sabemos que está en una intersección - return True - - # 3. Si tardó un tiempo normal (ej. > 1.5s), es un tránsito de pasillo válido - return dt < TIEMPO_MAX_AUSENCIA - - 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Ó LÓGICA ESPACIO-TEMPORAL DINÁMICA - def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True): - self.limpiar_fantasmas() - - 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'] - - # Bloqueo inmediato si es físicamente imposible - 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 - - sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam) - - # UMBRALES DINÁMICOS INTELIGENTES - if misma_cam: - if dt < 2.0: umbral = 0.60 # Para parpadeos rápidos de YOLO - else: umbral = 0.63 # Si desapareció un rato en la misma cámara - elif es_vecino: - # ⚡ EL PUNTO DULCE: 0.62. - # Está por encima del ruido máximo (0.57) y por debajo de las - # caídas de Omar en el cluster 7-5-8 (0.64). - umbral = 0.62 - else: - # ⚡ LEJANOS: 0.66. - # Si salta de la Cam 1 a la Cam 8, exigimos seguridad alta - # para no robarle el ID a alguien que acaba de entrar por la otra puerta. - umbral = 0.70 - - - """ - if misma_cam: - if dt < 2.0: umbral = 0.55 - elif dt < 10.0: umbral = 0.60 - else: umbral = 0.60 - elif es_vecino: - # ⚡ Subimos a 0.66. Si un extraño saca 0.62, será rechazado y nacerá un ID nuevo. - umbral = 0.66 - else: - # ⚡ Cámaras lejanas: Exigencia casi perfecta. - umbral = 0.70""" - - # PROTECCIÓN VIP (Le exigimos un poquito más si ya tiene nombre para no mancharlo) - if data.get('nombre') is not None: - umbral += 0.05 if misma_cam else 0.02 - - # EL CHIVATO DE OSNET (Debug crucial) - # Esto imprimirá en tu consola qué similitud real de ropa detectó al cruzar de cámara - if sim > 0.35 and not misma_cam: - print(f" [OSNet] Cam {cam_id} evaluando ID {gid} (Viene de Cam {data['last_cam']}) -> Similitud Ropa: {sim:.2f} (Umbral exigido: {umbral:.2f})") - - if sim > umbral: - candidatos.append((sim, gid, misma_cam, es_vecino)) - - 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, best_misma, best_vecino = candidatos[0] - - if len(candidatos) >= 2: - segunda_sim, segundo_gid, seg_misma, seg_vecino = candidatos[1] - margen = best_sim - segunda_sim - - if margen <= 0.06 and best_sim < 0.75: - peso_1 = best_sim + (0.10 if (best_misma or best_vecino) else 0.0) - peso_2 = segunda_sim + (0.10 if (seg_misma or seg_vecino) else 0.0) - - if peso_1 > peso_2: - best_gid = best_gid - elif peso_2 > peso_1: - best_gid = segundo_gid - else: - print(f"\n[ ALERTA] Empate extremo entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna temporal.") - 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) - - def limpiar_fantasmas(self): - with self.lock: - ahora = time.time() - ids_a_borrar = [] - - for gid, data in self.db.items(): - tiempo_inactivo = ahora - data.get('ts', ahora) - - if data.get('nombre') is None and tiempo_inactivo > 600.0: - ids_a_borrar.append(gid) - - elif data.get('nombre') is not None and tiempo_inactivo > 900.0: - ids_a_borrar.append(gid) - - for gid in ids_a_borrar: - del self.db[gid] - - def confirmar_firma_vip(self, gid, ts): - with self.lock: - if gid in self.db and self.db[gid]['firmas']: - firmas_actuales = self.db[gid]['firmas'] - self.db[gid]['firmas'] = firmas_actuales[-3:] - self.db[gid]['ts'] = ts - - def fusionar_ids(self, id_mantiene, id_elimina): - with self.lock: - if id_mantiene in self.db and id_elimina in self.db: - # 1. Le pasamos toda la memoria de ropa al ID ganador - self.db[id_mantiene]['firmas'].extend(self.db[id_elimina]['firmas']) - - # Topamos a las 15 mejores firmas para no saturar la RAM - if len(self.db[id_mantiene]['firmas']) > 15: - self.db[id_mantiene]['firmas'] = self.db[id_mantiene]['firmas'][-15:] - - self.db[id_mantiene]['ts'] = max(self.db[id_mantiene]['ts'], self.db[id_elimina]['ts']) - - # 2. Vaciamos al perdedor y le ponemos un "Redireccionamiento" - self.db[id_elimina]['firmas'] = [] - self.db[id_elimina]['fusionado_con'] = id_mantiene - print(f"[FUSIÓN MÁGICA] Las firmas del ID {id_elimina} fueron absorbidas por el ID {id_mantiene}.") - return True - return False - -# ────────────────────────────────────────────────────────────────────────────── -# 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 _detectar_grupo(self, trk, box, todos_los_tracks): - x1, y1, x2, y2 = box - w_box = x2 - x1 - h_box = y2 - y1 - cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 - - estado_actual = getattr(trk, 'en_grupo', False) - # Si están a menos de medio cuerpo de distancia lateral, es un grupo. - factor_x = 0.9 if estado_actual else 0.7 - factor_y = 0.35 if estado_actual else 0.2 - - for other in todos_los_tracks: - if other is trk: continue - if not hasattr(other, 'box') or other.box is None: continue - - ox1, oy1, ox2, oy2 = other.box - ocx, ocy = (ox1 + ox2) / 2, (oy1 + oy2) / 2 - - dist_x = abs(cx - ocx) - dist_y = abs(cy - ocy) - - if dist_x < (w_box * factor_x) and dist_y < (h_box * factor_y): - return True - return False - - def _gestionar_aprendizaje_post_grupo(self, now, frame_hd): - CUARENTENA = 1.2 - for trk in self.trackers: - if trk.gid is None or getattr(trk, 'en_grupo', False) or getattr(trk, 'firma_pre_grupo', None) is None: - continue - - tiempo_fuera = now - getattr(trk, 'ts_salio_grupo', 0) - - if tiempo_fuera >= CUARENTENA: - firma_actual = extraer_firma_hibrida(frame_hd, trk.box) - - if firma_actual is not None: - sim = similitud_hibrida(firma_actual, trk.firma_pre_grupo) - - # ⚡ Bajamos a 0.50 como umbral de supervivencia base - if sim >= 0.50: - print(f"[GRUPO] ID {trk.gid} validado post-grupo ({sim:.2f}).") - trk.fallos_post_grupo = 0 # Reseteamos los strikes si tenía - - with self.global_mem.lock: - if trk.gid in self.global_mem.db: - self.global_mem.db[trk.gid]['firmas'] = [trk.firma_pre_grupo, firma_actual] - trk.firma_pre_grupo = None # Aprobado, limpiamos memoria - - else: - # ⚡ SISTEMA DE 3 STRIKES - trk.fallos_post_grupo = getattr(trk, 'fallos_post_grupo', 0) + 1 - - if trk.fallos_post_grupo >= 3: - print(f" [ALERTA GRUPO] ID {trk.gid} falló 3 veces validación ({sim:.2f}). Reseteando ID.") - trk.gid = None - trk.firma_pre_grupo = None # Eliminado, limpiamos memoria - trk.fallos_post_grupo = 0 # Reiniciamos contador - else: - print(f" [STRIKE {trk.fallos_post_grupo}/3] ID {trk.gid} sacó {sim:.2f}. Dando otra oportunidad...") - # OJO: NO ponemos firma_pre_grupo en None aquí, - # para que lo vuelva a intentar en el siguiente frame. - - def update(self, boxes, frame_show, frame_hd, now, turno_activo): - - for trk in self.trackers: - if trk.gid is not None: - with self.global_mem.lock: - if trk.gid in self.global_mem.db and 'fusionado_con' in self.global_mem.db[trk.gid]: - print(f"🔗 [HILO CAM {self.cam_id}] Tracker mutando de ID {trk.gid} a {self.global_mem.db[trk.gid]['fusionado_con']}") - trk.gid = self.global_mem.db[trk.gid]['fusionado_con'] - # ────────────────────────────────────────────────────────── - - # ⚡ FILTRO ANTI-FANTASMAS - vivos_predict = [] - for trk in self.trackers: - caja_predicha = trk.predict(turno_activo=turno_activo) - # Si el tracker lleva más de 5 frames ciego, devuelve None. Lo ignoramos. - if caja_predicha is not None: - vivos_predict.append(trk) - - self.trackers = vivos_predict # Actualizamos la lista solo con los vivos - - if not turno_activo: return self.trackers - - # ────────────────────────────────────────────────────────── - # Aquí sigue tu código normal: - matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now) - - active_gids = {t.gid for t in self.trackers if t.gid is not None} - - for t_idx, d_idx in matched: - trk = self.trackers[t_idx] - box = boxes[d_idx] - - es_grupo_ahora = self._detectar_grupo(trk, box, self.trackers) - - if not getattr(trk, 'en_grupo', False) and es_grupo_ahora: - if trk.gid is not None: - with self.global_mem.lock: - firmas = self.global_mem.db.get(trk.gid, {}).get('firmas', []) - if firmas: - trk.firma_pre_grupo = firmas[-1] - trk.validado_post_grupo = False - print(f" [GRUPO] ID {trk.gid} entró a grupo. Protegiendo firma.") - trk.ts_salio_grupo = 0.0 - - elif getattr(trk, 'en_grupo', False) and not es_grupo_ahora: - trk.ts_salio_grupo = now - print(f" [GRUPO] ID {trk.gid} salió de grupo. Cuarentena de 3s.") - - trk.en_grupo = es_grupo_ahora - trk.update(box, es_grupo_ahora, now) - - area_actual = (box[2] - box[0]) * (box[3] - box[1]) - tiempo_desde_separacion = now - getattr(trk, 'ts_salio_grupo', 0) - en_cuarentena = (tiempo_desde_separacion < 3.0) and (getattr(trk, 'ts_salio_grupo', 0) > 0) - - extracciones_hoy = 0 # ⚡ EL SALVAVIDAS: Contador para el frame actual - - if not trk.en_grupo and not en_cuarentena: - - # A) Bautizo de IDs Nuevos (Con Aduana Temporal) - if trk.gid is None and trk.listo_para_id: - firma = extraer_firma_hibrida(frame_hd, box) - if firma is not None: - fh, fw = frame_hd.shape[:2] - bx1, by1, bx2, by2 = map(int, box) - nace_en_borde = (bx1 < 25 or by1 < 25 or bx2 > fw - 25 or by2 > fh - 25) - - candidato_gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde) - - if candidato_gid is not None: - # ⚡ BLOQUEO INMEDIATO: Reservamos el ID en este milisegundo - # para que ninguna otra persona en esta cámara pueda evaluarlo. - active_gids.add(candidato_gid) - - # Si la memoria dice "Es un desconocido nuevo", lo bautizamos al instante - if not es_reid: - trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, False, area_actual - - # ⚡ ADUANA TEMPORAL - else: - if getattr(trk, 'candidato_temporal', None) == candidato_gid: - trk.votos_reid = getattr(trk, 'votos_reid', 0) + 1 - else: - # Si cambia de opinión, reiniciamos sin restar - trk.candidato_temporal = candidato_gid - trk.votos_reid = 1 - - if trk.votos_reid >= 2: - trk.gid, trk.origen_global = candidato_gid, True - - with self.global_mem.lock: - datos = self.global_mem.db.get(candidato_gid, {}) - nombre_oficial = datos.get('nombre') - cam_anterior = datos.get('last_cam') - entrada_ts = datos.get('last_cam_entry_ts', now) - - # ⚡ REGISTRO SOLO SI TIENE NOMBRE Y VIENE DE OTRA CÁMARA - if nombre_oficial and cam_anterior is not None and str(cam_anterior) != str(self.cam_id): - duracion = now - entrada_ts - self.global_mem.registrar_movimiento(nombre_oficial, cam_anterior, self.cam_id, duracion) - - # Actualizamos los datos para el siguiente salto - datos['last_cam'] = self.cam_id - datos['last_cam_entry_ts'] = now - - # B) Aprendizaje Continuo (Captura de Ángulos) - elif trk.gid is not None: - tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) - - # ⚡ APRENDIZAJE ESCALONADO: - # Mantenemos tus 0.5s perfectos, pero solo 1 persona por frame puede saturar la CPU. - if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box) and extracciones_hoy < 1: - fh, fw = frame_hd.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_hd, box) - if firma_nueva is not None: - extracciones_hoy += 1 # ⚡ Cerramos la compuerta para los demás en este frame - - with self.global_mem.lock: - if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: - - firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1] - firma_original = self.global_mem.db[trk.gid]['firmas'][0] - - sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente) - sim_raiz = similitud_hibrida(firma_nueva, firma_original) - - # ⚡ EL BOTÓN DE PÁNICO (Anti ID-Switch) - # Si la ropa de la caja actual no se parece en NADA a la original (< 0.35), - # significa que Kalman le pegó el ID a la persona equivocada en un cruce. - if sim_raiz < 0.35: - print(f"[ID SWITCH] Ropa de ID {trk.gid} cambió drásticamente. Revocando ID.") - trk.gid = None - trk.listo_para_id = False - trk.frames_buena_calidad = 0 - continue # Rompemos el ciclo para que nazca como alguien nuevo - - ya_bautizado = self.global_mem.db[trk.gid].get('nombre') is not None - umbral_raiz = 0.52 if ya_bautizado else 0.62 - - if sim_coherencia > 0.60 and sim_raiz > umbral_raiz: - 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_raiz: - 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_show.shape[:2] - for t in self.trackers: - x1, y1, x2, y2 = t.box - toca_borde = (x1 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20) - tiempo_oculto = now - t.ts_ultima_deteccion - - # ⚡ LIMPIEZA AGRESIVA PARA PROTEGER LA CPU DE MEMORY LEAKS - if t.gid is None: - # Si es un ID gris (no bautizado), lo matamos rápido si YOLO lo pierde - limite_vida = 1.0 if toca_borde else 2.5 - else: - # Si es VIP, le damos paciencia para que OSNet no se fragmente - limite_vida = 3.0 if toca_borde else 8.0 - - if tiempo_oculto < limite_vida: - vivos.append(t) - - self.trackers = vivos - - self._gestionar_aprendizaje_post_grupo(now, frame_hd) - - 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) - - for t, trk in enumerate(self.trackers): - tiempo_oculto = now - trk.ts_ultima_deteccion - - # ⚡ 2A. Aumentamos el radio dinámico mínimo para no perder gente rápida - radio_dinamico = min(350.0, max(150.0, 300.0 * tiempo_oculto)) - - # La incertidumbre crece con el tiempo, pero la topamos en 0.5 - incertidumbre = min(0.5, tiempo_oculto * 0.2) - - es_fantasma = getattr(trk, 'time_since_update', 0) > 1 - - 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_pixel = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) - - area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1]) - area_det = (det[2] - det[0]) * (det[3] - det[1]) - - if dist_pixel > radio_dinamico: - cost_mat[t, d] = 100.0 - continue - - # ⚡ 2B. REDUCIMOS CASTIGOS INJUSTOS - # Eliminamos el "castigo_secuestro" que penalizaba excesivamente a los trackers sin IOU. - # Reducimos la penalización por cambio de tamaño de 0.4 a 0.2. - - dist_norm = dist_pixel / radio_dinamico - ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) - - # Si la caja de YOLO es mucho MÁS PEQUEÑA que la predicción de Kalman, - # penalizamos fuerte (evita que la caja encoja mágicamente y pierda a la persona). - if area_det < area_trk: - castigo_tam = (ratio_area - 1.0) * 0.5 - else: - # Si la caja de YOLO es MÁS GRANDE (ej. persona acercándose/subiendo escaleras), - # somos muy permisivos para que Kalman acepte el nuevo tamaño sin soltarse. - castigo_tam = (ratio_area - 1.0) * 0.15 - - # Fórmula equilibrada - cost_mat[t, d] = (1.0 - iou) + (0.5 * dist_norm) + castigo_tam + incertidumbre - - from scipy.optimize import linear_sum_assignment - row_ind, col_ind = linear_sum_assignment(cost_mat) - matched, unmatched_dets, unmatched_trks = [], [], [] - - for r, c in zip(row_ind, col_ind): - # ⚡ UMBRAL RELAJADO: De 2.5 a 3.5. - # Esto evita que rechace la caja por la latencia del procesador. - if cost_mat[r, c] > 3.5: - 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, frame, 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() - - - -########################################## reconocimineto -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' -os.environ['CUDA_VISIBLE_DEVICES'] = '-1' - -# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de OpenCV y Qt -os.environ['QT_LOGGING_RULES'] = '*=false' -os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' - -import cv2 -import numpy as np -import pickle -import time -import threading -import asyncio -import edge_tts -import subprocess -from datetime import datetime -import warnings -import json - -# ⚡ IMPORTACIÓN DE INSIGHTFACE -from insightface.app import FaceAnalysis - -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 # ⚡ Ajustado a la exigencia de producción -COOLDOWN_TIME = 15 # Segundos entre saludos - -USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.200" -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) - -# ────────────────────────────────────────────────────────────────────────────── -# EL NUEVO CEREBRO: INSIGHTFACE (buffalo_l) -# ────────────────────────────────────────────────────────────────────────────── -print("Cargando motor InsightFace (buffalo_l)...") -# Carga detección (SCRFD), landmarks, reconocimiento (ArcFace) y atributos (género/edad) -app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) -# det_size a 320x320 es un balance perfecto entre velocidad y capacidad de detectar rostros lejanos -app.prepare(ctx_id=0, det_size=(320, 320)) -print("Motor cargado exitosamente.") - -# ────────────────────────────────────────────────────────────────────────────── -# 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 INSIGHTFACE) -# ────────────────────────────────────────────────────────────────────────────── -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 = {} - - 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 (modelo 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 - - 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) - 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) - - # ⚡ INSIGHTFACE: Extracción primaria - faces = app.get(img_db) - - # HACK DEL LIENZO NEGRO: Si la foto está muy recortada y no ve la cara, - # le agregamos un borde negro gigante para engañar al detector - if len(faces) == 0: - h_img, w_img = img_db.shape[:2] - img_pad = cv2.copyMakeBorder(img_db, h_img, h_img, w_img, w_img, cv2.BORDER_CONSTANT, value=(0,0,0)) - faces = app.get(img_pad) - - if len(faces) == 0: - print(f" [!] Rostro irrecuperable en '{archivo}'.") - continue - - face = max(faces, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])) - emb = np.array(face.normed_embedding, dtype=np.float32) - - # ⚡ CORRECCIÓN DE GÉNERO: InsightFace devuelve "M" o "F" - if falta_genero: - dic_generos[nombre_archivo] = "Man" if face.sex == "M" else "Woman" - cambio_generos = True - - vectores_actuales[nombre_archivo] = emb - timestamps[nombre_archivo] = ts_actual - hubo_cambios = True - print(f" Procesado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}") - - except Exception as e: - print(f" Error procesando '{archivo}': {e}") - - 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}") - - 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) - - 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 -# ────────────────────────────────────────────────────────────────────────────── -def buscar_mejor_match(emb_consulta, base_datos): - # InsightFace devuelve normed_embedding, por lo que la magnitud ya es 1 - mejor_match, max_sim = None, -1.0 - for nombre, vec in base_datos.items(): - 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 - INSIGHTFACE (buffalo_l)") - 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() - - # ⚡ INSIGHTFACE: Hace TODO aquí (Detección, Landmarks, Género, ArcFace) - faces_raw = app.get(frame) - faces_ultimo_frame = faces_raw - - for face in faces_raw: - # Las cajas de InsightFace vienen en formato [x1, y1, x2, y2] - box = face.bbox.astype(int) - fx, fy, x2, y2 = box[0], box[1], box[2], box[3] - fw, fh = x2 - fx, y2 - fy - - # Limitamos a los bordes de la pantalla - fx, fy = max(0, fx), max(0, fy) - fw, fh = min(w - fx, fw), 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"DET:{face.det_score:.2f}", - (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) - - if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: - continue - - # FILTRO DE TAMAÑO (Para evitar reconocer píxeles lejanos) - if fw < 20 or fh < 20: - cv2.putText(display_frame, "muy pequeno", - (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) - continue - - # SIMETRÍA MATEMÁTICA INMEDIATA - emb = np.array(face.normed_embedding, dtype=np.float32) - mejor_match, max_sim = buscar_mejor_match(emb, base_datos) - - 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) - - # ⚡ GÉNERO INMEDIATO SIN SOBRECARGA DE CPU - genero = "Man" if face.sex == 1 else "Woman" - - 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: - # Tomamos la cara más grande detectada por InsightFace - face_registro = max(faces_ultimo_frame, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])) - box = face_registro.bbox.astype(int) - fx, fy, fw, fh = box[0], box[1], box[2]-box[0], box[3]-box[1] - - # Margen estético para guardar la foto - m_x, m_y = int(fw * 0.30), int(fh * 0.30) - y1, y2 = max(0, fy-m_y), min(h, fy+fh+m_y) - x1, x2 = max(0, fx-m_x), min(w, fx+fw+m_x) - - face_roi = frame[y1:y2, x1:x2] - - if face_roi.size > 0: - nom = input("\nNombre de la persona: ").strip() - if nom: - # Extraemos el género automáticamente gracias a InsightFace - gen_str = "Man" if face_registro.sex == 1 else "Woman" - - # Actualizamos JSON directo sin preguntar - ruta_generos = os.path.join(CACHE_PATH, "generos.json") - dic_generos = {} - if os.path.exists(ruta_generos): - with open(ruta_generos, 'r') as f: - dic_generos = json.load(f) - dic_generos[nom] = gen_str - with open(ruta_generos, 'w') as f: - json.dump(dic_generos, f) - - # Guardado de imagen y sincronización - foto_path = os.path.join(DB_PATH, f"{nom}.jpg") - cv2.imwrite(foto_path, face_roi) - print(f"[OK] Rostro de '{nom}' guardado como {gen_str}. 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() - - -####################################################################### fusion - -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" - -# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de OpenCV y Qt -os.environ['QT_LOGGING_RULES'] = '*=false' -os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' -import cv2 -import numpy as np -import time -import threading -import queue -from queue import Queue -from ultralytics import YOLO -import warnings -import json - -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 -from reconocimiento import ( - app, - gestionar_vectores, - buscar_mejor_match, - hilo_bienvenida, - UMBRAL_SIM, - COOLDOWN_TIME -) - -# ────────────────────────────────────────────────────────────────────────────── -# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN -# ────────────────────────────────────────────────────────────────────────────── -COLA_ROSTROS = Queue(maxsize=4) -IA_LOCK = threading.Lock() - -print("\nIniciando carga de base de datos...") -BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) - -# ────────────────────────────────────────────────────────────────────────────── -# 3. MOTOR ASÍNCRONO CON INSIGHTFACE -# ────────────────────────────────────────────────────────────────────────────── -def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): - try: - if not BASE_DATOS_ROSTROS or roi_cabeza.size == 0: return - - with IA_LOCK: - faces = app.get(roi_cabeza) - - if len(faces) == 0: - return - - face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) - - w_rostro = face.bbox[2] - face.bbox[0] - h_rostro = face.bbox[3] - face.bbox[1] - - # ⚡ Límite bajo (20px) para reconocer desde más lejos - if w_rostro < 20 or h_rostro < 20 or (w_rostro / h_rostro) < 0.35: - return - - emb = np.array(face.normed_embedding, dtype=np.float32) - genero_detectado = "Man" if face.sex == "M" else "Woman" - - mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) - print(f"[DEBUG CAM {cam_id}] InsightFace: {mejor_match} al {max_sim:.2f}") - - votos_finales = 0 - - # ────────────────────────────────────────────────────────────────── - # ⚡ LÓGICA DE VOTACIÓN PONDERADA INYECTADA - # ────────────────────────────────────────────────────────────────── - # Aceptamos rostros de CCTV desde 0.38 (ajustado según tu código a 0.35) - if max_sim >= 0.35 and mejor_match: - nombre_limpio = mejor_match.split('_')[0] - - # Variable para rastrear quién es el ID definitivo al final del proceso - id_definitivo = gid - - with global_mem.lock: - datos_id = global_mem.db.get(gid) - if not datos_id: return - - if datos_id.get('candidato_nombre') == nombre_limpio: - datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1 - else: - datos_id['candidato_nombre'] = nombre_limpio - datos_id['votos_nombre'] = 1 - - # Sistema de Puntos - if max_sim >= 0.50: - datos_id['votos_nombre'] += 3 # Pase VIP por buena foto - elif max_sim >= 0.40: - datos_id['votos_nombre'] += 2 # Voto extra por foto decente - - votos_finales = datos_id['votos_nombre'] - - # ⚡ Exigimos 3 votos (según tu condicional) - if votos_finales >= 3: - nombre_actual = datos_id.get('nombre') - - # 1. Buscamos si ese nombre ya lo tiene un veterano en la memoria - id_veterano = None - for gid_mem, data_mem in global_mem.db.items(): - if data_mem.get('nombre') == nombre_limpio and gid_mem != gid: - id_veterano = gid_mem - break - - # 2. FUSIÓN MÁGICA: Si ya existe, lo fusionamos - if id_veterano is not None: - print(f"[FUSIÓN FACIAL] ID {gid} es el clon de la espalda. Fusionando con el veterano ID {id_veterano} ({nombre_limpio}).") - - # Pasamos la memoria de ropa al veterano - global_mem.db[id_veterano]['firmas'].extend(datos_id.get('firmas', [])) - if len(global_mem.db[id_veterano]['firmas']) > 15: - global_mem.db[id_veterano]['firmas'] = global_mem.db[id_veterano]['firmas'][-15:] - - # Vaciamos al perdedor y redireccionamos - datos_id['firmas'] = [] - datos_id['fusionado_con'] = id_veterano - - # Actualizamos el tracker y el ID definitivo - trk.gid = id_veterano - id_definitivo = id_veterano - - # 3. BAUTIZO NORMAL: Si no hay clones, procedemos con tu lógica de protección VIP - else: - if nombre_actual is not None and nombre_actual != nombre_limpio: - # Si ya tiene nombre y quieren robárselo, exigimos 0.56 mínimo - if max_sim < 0.56: - print(f"[RECHAZO VIP] {nombre_actual} protegido de {nombre_limpio} ({max_sim:.2f})") - return - else: - print(f" [CORRECCIÓN VIP] Renombrando a {nombre_limpio} ({max_sim:.2f})") - - if nombre_actual != nombre_limpio: - datos_id['nombre'] = nombre_limpio - print(f"[BAUTIZO] ID {gid} confirmado como {nombre_limpio}") - - # Actualizamos el tiempo de vida del ID ganador - global_mem.db[id_definitivo]['ts'] = time.time() - - # AUDIO DE BIENVENIDA (Funciona tanto para bautizos como para fusiones) - if str(cam_id) == "7": - ahora = time.time() - # Si estás probando, usa 30 segundos. Para producción usa 28800 (8h). - TIEMPO_COOLDOWN = 60 - - with global_mem.lock: - # Intentamos obtener info del registro de saludos persistente - info_saludo = global_mem.ultimos_saludos.get(nombre_limpio, {}) - # Si es un string (vieja versión), lo convertimos - ultimo_ts = info_saludo.get('timestamp', 0) if isinstance(info_saludo, dict) else 0 - - if (ahora - ultimo_ts) > TIEMPO_COOLDOWN: - print(f" Disparando saludo para {nombre_limpio}...") - # Actualizamos memoria y disco - global_mem.guardar_saludo(nombre_limpio) - - # Género desde el JSON - genero_oficial = genero_detectado - try: - ruta_gen = os.path.join("cache_nombres", "generos.json") - if os.path.exists(ruta_gen): - with open(ruta_gen, 'r') as f: - genero_oficial = json.load(f).get(nombre_limpio, genero_detectado) - except Exception: pass - - threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_oficial), daemon=True).start() - else: - print(f"Saludo a {nombre_limpio} omitido (Cooldown activo: {int(ahora - ultimo_ts)}s)") - # Blindaje OSNet fuera del lock (Usando el id_definitivo por si hubo fusión) - if max_sim > 0.50 and votos_finales >= 3: - global_mem.confirmar_firma_vip(id_definitivo, time.time()) - - except Exception as e: - print(f"Error en InsightFace asíncrono: {e}") - finally: - # ⚡ EL LIBERADOR (Vital para que el tracker no se quede bloqueado) - trk.procesando_rostro = False - -def worker_rostros(global_mem): - while True: - roi_cabeza, gid, cam_id, trk = COLA_ROSTROS.get() - procesar_rostro_async(roi_cabeza, 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 getattr(trk, 'en_grupo', False): color, label = (0, 0, 255), f"ID:{trk.gid} [grp]" - elif getattr(trk, 'aprendiendo', False): color, label = (255, 255, 0), f"ID:{trk.gid} [++]" - elif getattr(trk, 'origen_global', False): 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] - - 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: - # ⚡ 1. Subimos la confianza de YOLO de 0.50 a 0.60 para matar siluetas dudosas - res = model.predict(frame_show, conf=0.60, iou=0.50, classes=[0], verbose=False, imgsz=480) - - if res[0].boxes: - cajas_crudas = res[0].boxes.xyxy.cpu().numpy().tolist() - - # ⚡ 2. FILTRO BIOMECÁNICO (Anti-Sillas) - for b in cajas_crudas: - w_caja = b[2] - b[0] - h_caja = b[3] - b[1] - - # Un humano real es al menos 10% más alto que ancho (ratio > 1.1) - # Si la caja es muy cuadrada o ancha, la ignoramos y no se la pasamos a Kalman - if h_caja > (w_caja * 1.1): - boxes.append(b) - - # ⚡ UPDATE LIMPIO (Sin IDs Globales) - tracks = managers[cid].update(boxes, frame_show, frame, now, turno_activo) - - for trk in tracks: - if getattr(trk, 'time_since_update', 1) <= 2: - dibujar_track_fusion(frame_show, trk, global_mem) - - if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False): - - with global_mem.lock: - votos_actuales = global_mem.db.get(trk.gid, {}).get('votos_nombre', 0) - if votos_actuales >= 2: - continue - - x1, y1, x2, y2 = trk.box - h_real, w_real = frame.shape[:2] - escala_x = w_real / 480.0 - escala_y = h_real / 270.0 - h_box = y2 - y1 - - y_exp = max(0, y1 - h_box * 0.15) - y_cab = min(270, y1 + h_box * 0.50) - - roi_recortado = frame[ - int(y_exp * escala_y) : int(y_cab * escala_y), - int(max(0, x1) * escala_x) : int(min(480, x2) * escala_x) - ].copy() - - # ⚡ COMPUERTA ÓPTICA (Filtro Anti-Tartamudeo 25x25) - if roi_recortado.size > 0 and roi_recortado.shape[0] >= 25 and roi_recortado.shape[1] >= 25: - gray = cv2.cvtColor(roi_recortado, cv2.COLOR_BGR2GRAY) - nitidez = cv2.Laplacian(gray, cv2.CV_64F).var() - - # ⚡ BAJAMOS DE 12.0 a 5.0. - # Deja pasar rostros un poco borrosos por la velocidad de caminar, - # pero sigue bloqueando espaldas lisas. - if nitidez > 8.0: - if not COLA_ROSTROS.full(): - ultimo_envio = getattr(trk, 'ultimo_rostro_enviado', 0) - - # Solo mandamos la cara si ha pasado medio segundo desde la última vez que lo intentamos - if (now - ultimo_envio) > 0.5: - - # Tu escudo anti-lag previo - if COLA_ROSTROS.qsize() < 2: - trk.ultimo_rostro_enviado = now # Registramos la hora del envío - trk.procesando_rostro = True # Bloqueamos el tracker - - # Aquí pones TU línea original de la cola: - COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False) - else: - trk.ultimo_intento_cara = time.time() - 0.5 - - """if nitidez > 8.0: - - if not COLA_ROSTROS.full(): - - try: - - if COLA_ROSTROS.qsize() < 2: - - COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False) - - trk.procesando_rostro = True - - trk.ultimo_intento_cara = time.time() - - except queue.Full: - - pass - - else: - - trk.ultimo_intento_cara = time.time() - 0.5""" - - 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 getattr(t, 'time_since_update', 1) == 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 - key = cv2.waitKey(1) & 0xFF - - if key == ord('q'): - break - - elif key == ord('r'): - print("\n[MODO REGISTRO] Escaneando mosaico para registrar...") - mejor_roi = None - max_area = 0 - face_metadata = None - - for i, cam_obj in enumerate(cams): - if cam_obj.frame is None: continue - - faces = app.get(cam_obj.frame) - for face in faces: - fw = face.bbox[2] - face.bbox[0] - fh = face.bbox[3] - face.bbox[1] - area = fw * fh - if area > max_area: - max_area = area - h_frame, w_frame = cam_obj.frame.shape[:2] - - m_x, m_y = int(fw * 0.30), int(fh * 0.30) - y1 = max(0, int(face.bbox[1]) - m_y) - y2 = min(h_frame, int(face.bbox[3]) + m_y) - x1 = max(0, int(face.bbox[0]) - m_x) - x2 = min(w_frame, int(face.bbox[2]) + m_x) - - mejor_roi = cam_obj.frame[y1:y2, x1:x2] - face_metadata = face - - if mejor_roi is not None and mejor_roi.size > 0: - cv2.imshow("Nueva Persona", mejor_roi) - cv2.waitKey(1) - - nom = input("Escribe el nombre de la persona: ").strip() - cv2.destroyWindow("Nueva Persona") - - if nom: - import json - genero_guardado = "Man" if face_metadata.sex == "M" else "Woman" - - ruta_generos = os.path.join("cache_nombres", "generos.json") - os.makedirs("cache_nombres", exist_ok=True) - 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) - - ruta_db = "db_institucion" - os.makedirs(ruta_db, exist_ok=True) - cv2.imwrite(os.path.join(ruta_db, f"{nom}.jpg"), mejor_roi) - - print(f"[OK] Rostro guardado (Género autodetectado: {genero_guardado})") - print(" Sincronizando base de datos en caliente...") - - nuevos_vectores = gestionar_vectores(actualizar=True) - with IA_LOCK: - BASE_DATOS_ROSTROS.clear() - BASE_DATOS_ROSTROS.update(nuevos_vectores) - print(" Sistema listo.") - else: - print("[!] Registro cancelado.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/osnet_dinamico.onnx b/osnet_dinamico.onnx deleted file mode 100644 index 2fa21e8..0000000 Binary files a/osnet_dinamico.onnx and /dev/null differ diff --git a/osnet_dinamico.py b/osnet_dinamico.py index 64b30f2..5747d80 100644 --- a/osnet_dinamico.py +++ b/osnet_dinamico.py @@ -1,28 +1,31 @@ -import onnx +import torch +import torchreid -# 1. Rutas de archivos -modelo_estatico = "osnet_x0_25_msmt17.onnx" -modelo_dinamico = "osnet_dinamico.onnx" +archivo_salida = "osnet_x1_0_msmt17_batch1.onnx" -print(f"Abriendo {modelo_estatico}...") -model = onnx.load(modelo_estatico) +print("1. Descargando los pesos originales de la Bestia (OSNet x1_0)...") +# ⚡ Al poner pretrained=True, Python usa el enlace directo interno de la librería +# saltándose los bloqueos del navegador web. +model = torchreid.models.build_model( + name='osnet_x1_0', + num_classes=1000, + loss='softmax', + pretrained=True +) +model.eval() -# 2. Modificar todas las entradas (Inputs) -for input_proto in model.graph.input: - # Accedemos a la primera dimensión (índice 0), que es el Batch - dim = input_proto.type.tensor_type.shape.dim[0] - - # IMPORTANTE: Debemos borrar el valor fijo (ej. 16) antes de asignar el nombre dinámico - # Esto evita conflictos en ciertas versiones de la librería ONNX - dim.ClearField('dim_value') - dim.dim_param = 'batch_size' +print("2. Compilando el modelo ONNX blindado a Batch=1...") +dummy_input = torch.randn(1, 3, 256, 128) -# 3. Modificar todas las salidas (Outputs) -for output_proto in model.graph.output: - dim = output_proto.type.tensor_type.shape.dim[0] - dim.ClearField('dim_value') - dim.dim_param = 'batch_size' +torch.onnx.export( + model, + dummy_input, + archivo_salida, + export_params=True, + opset_version=18, # Mantenemos la versión 18 que ya nos funcionó perfecto + do_constant_folding=True, + input_names=['images'], + output_names=['features'] +) -# 4. Guardar el nuevo modelo -onnx.save(model, modelo_dinamico) -print(f"¡Éxito! El nuevo modelo se ha guardado como: {modelo_dinamico}") \ No newline at end of file +print(f"¡Éxito total! Tu modelo de grado comercial máximo está listo: {archivo_salida}") \ No newline at end of file diff --git a/osnet_x0_25_msmt17.onnx b/osnet_x0_25_msmt17.onnx deleted file mode 100644 index 917d3e8..0000000 Binary files a/osnet_x0_25_msmt17.onnx and /dev/null differ diff --git a/osnet_x0_5_msmt17.pth b/osnet_x0_5_msmt17.pth new file mode 100644 index 0000000..75b7b8d Binary files /dev/null and b/osnet_x0_5_msmt17.pth differ diff --git a/osnet_x0_5_msmt17_batch1.onnx b/osnet_x0_5_msmt17_batch1.onnx new file mode 100644 index 0000000..5cfc94f Binary files /dev/null and b/osnet_x0_5_msmt17_batch1.onnx differ diff --git a/osnet_x0_5_msmt17_batch1.onnx.data b/osnet_x0_5_msmt17_batch1.onnx.data new file mode 100644 index 0000000..4b24fbf Binary files /dev/null and b/osnet_x0_5_msmt17_batch1.onnx.data differ diff --git a/osnet_x1_0_msmt17_batch1.onnx b/osnet_x1_0_msmt17_batch1.onnx new file mode 100644 index 0000000..57a6f02 Binary files /dev/null and b/osnet_x1_0_msmt17_batch1.onnx differ diff --git a/osnet_x1_0_msmt17_batch1.onnx.data b/osnet_x1_0_msmt17_batch1.onnx.data new file mode 100644 index 0000000..ce37103 Binary files /dev/null and b/osnet_x1_0_msmt17_batch1.onnx.data differ diff --git a/p.py b/p.py new file mode 100644 index 0000000..ea2f9e1 --- /dev/null +++ b/p.py @@ -0,0 +1,933 @@ +""" +Tracker Multi-Cámara Definitivo (SmartSoft IA) +Características: + 1. OSNet dominante con EMA fluida. + 2. Color LAB con Veto Cruzado Letal (anti-robos de identidad). + 3. Huella ANATÓMICA por keypoints (anti-uniformes). + 4. Sistema Inmune (Hard Negatives): Aprende de tus correcciones (tecla 'n'). + 5. Memoria Persistente: No olvida las correcciones al reiniciar. +""" + +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 +from datetime import datetime +import json +import csv +import math +import queue + +# ────────────────────────────────────────────────────────────────────────────── +# CONFIGURACIÓN +# ────────────────────────────────────────────────────────────────────────────── +USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" +SECUENCIA = [1, 7, 5, 8, 3, 6] +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_dinamico_int8.onnx" + +VECINOS = { + "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], + "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] +} + +# ⚡ TIEMPOS REALES: Bloqueo estricto de teletransportaciones +TIEMPO_MIN_POR_DISTANCIA = { + 0: 0.0, + 1: 1.0, # Vecino directo (casi instantáneo) + 2: 15.0, # Un salto intermedio -> Mínimo 15 seg. + 3: 40.0, # Dos o más saltos -> Mínimo 40 seg. +} + +TIEMPO_MAX_AUSENCIA = 800.0 +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) +C_FEEDBACK = (255, 0, 255) +FUENTE = cv2.FONT_HERSHEY_SIMPLEX + +# ────────────────────────────────────────────────────────────────────────────── +# OSNET +# ────────────────────────────────────────────────────────────────────────────── +print("Cargando OSNet...") +try: + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + input_name = ort_session.get_inputs()[0].name + print("OSNet listo para CPU.") +except Exception as e: + print(f"ERROR FATAL: {e}"); exit() + +MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1) +STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1) + +# ────────────────────────────────────────────────────────────────────────────── +# HUELLA ANATÓMICA (Anti-uniformes) +# ────────────────────────────────────────────────────────────────────────────── +def extraer_proporciones_anatomicas(kpts): + if kpts is None or len(kpts) < 17: return None + kpts = np.array(kpts) + if kpts.shape[0] < 17: return None + + puntos = {i: kpts[i] for i in range(17)} + nariz_ok = puntos[0][2] > 0.30 + hombro_izq = puntos[5][2] > 0.30 + hombro_der = puntos[6][2] > 0.30 + cadera_izq = puntos[11][2] > 0.30 + cadera_der = puntos[12][2] > 0.30 + + if not nariz_ok or (not hombro_izq and not hombro_der) or (not cadera_izq and not cadera_der): + return None + + puntos_validos = sum(1 for p in puntos.values() if p[2] > 0.30) + if puntos_validos < 7: return None + + def dist(i, j): + if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0 + return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2])) + + hombros = dist(5, 6) + caderas = dist(11, 12) + torso_izq = dist(5, 11) + torso_der = dist(6, 12) + pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0 + pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0 + brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0 + brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0 + + altura_approx = max(torso_izq, torso_der) + max(pierna_izq, pierna_der) + if altura_approx < 10.0: return None + + feats = np.array([ + hombros / altura_approx, + caderas / altura_approx, + max(torso_izq, torso_der) / altura_approx, + max(pierna_izq, pierna_der) / altura_approx, + max(brazo_izq, brazo_der) / altura_approx, + hombros / max(caderas, 1e-6), + max(pierna_izq, pierna_der) / max(max(torso_izq, torso_der), 1e-6), + (brazo_izq + brazo_der) / max(max(torso_izq, torso_der) * 2, 1e-6), + ], dtype=np.float32) + + feats = np.clip(feats, 0.0, 3.0) + n = np.linalg.norm(feats) + if n > 0: feats /= n + return feats + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRACCIÓN DE FIRMAS +# ────────────────────────────────────────────────────────────────────────────── +def analizar_calidad(box, kpts=None, estricto=False): + x1, y1, x2, y2 = box + w, h = x2-x1, y2-y1 + if w <= 0 or h <= 0: return False + + ratio = h / float(w) + puntos_buenos = sum(1 for kx, ky, conf in kpts if conf > 0.40) if kpts is not None else 0 + + if puntos_buenos >= 4: + ratio_min = 0.60 + else: + ratio_min = 1.25 if estricto else 1.20 + + ratio_max = 4.0 if estricto else 5.0 + area_min = 800 if estricto else 400 + + return (ratio_min < ratio < ratio_max) and (w*h > area_min) + +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 + return np.expand_dims((img - MEAN) / STD, 0) + +def extraer_color_zonas(img): + if img is None or img.size == 0 or img.shape[0] < 5 or img.shape[1] < 5: + return np.zeros(512 * 3, dtype=np.float32) + + h, w = img.shape[:2] + t1, t2 = int(h * 0.15), int(h * 0.55) + m_w = int(w * 0.20) + core_img = img[:, m_w:(w - m_w)] + if core_img.size == 0 or core_img.shape[1] < 4: + core_img = img + + lab = cv2.cvtColor(core_img, cv2.COLOR_BGR2LAB) + l, a, b = cv2.split(lab) + if l.shape[0] >= 8 and l.shape[1] >= 8: + l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l) + lab_eq = cv2.merge((l, a, b)) + + def hzone(z): + if z is None or z.size == 0 or z.shape[0] < 2 or z.shape[1] < 2: + return np.zeros(512, dtype=np.float32) + hist = cv2.calcHist([z], [0,1,2], None, [8,8,8], [0,256, 0,256, 0,256]) + cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1) + return hist.flatten() + + return np.concatenate([hzone(lab_eq[:t1,:]), hzone(lab_eq[t1:t2,:]), hzone(lab_eq[t2:,:])]) + +def calidad_real(roi_shape, x1_c, y1_c, x2_c, y2_c, w_hd, h_hd): + area = (x2_c - x1_c) * (y2_c - y1_c) + ratio = roi_shape[0] / max(roi_shape[1], 1) + factor_ratio = 1.0 if 1.8 < ratio < 3.2 else 0.5 + en_borde = (x1_c < 20 or y1_c < 20 or x2_c > w_hd-20 or y2_c > h_hd-20) + factor_borde = 0.3 if en_borde else 1.0 + return area * factor_ratio * factor_borde + +def extraer_firma_hibrida(frame_hd, box_480, kpts=None): + try: + h_hd, w_hd = frame_hd.shape[:2] + x1, y1, x2, y2 = box_480 + w, h = x2-x1, y2-y1 + px, py = w*0.08, h*0.05 + + x1_c = max(0, int((x1-px)*(w_hd/480.0))) + y1_c = max(0, int((y1-py)*(h_hd/270.0))) + x2_c = min(w_hd, int((x2+px)*(w_hd/480.0))) + y2_c = min(h_hd, int((y2+py)*(h_hd/270.0))) + + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: return None + + cal = calidad_real(roi.shape, x1_c, y1_c, x2_c, y2_c, w_hd, h_hd) + + blob = preprocess_onnx(roi) + deep_f = ort_session.run(None, {input_name: blob})[0][0].flatten() + n = np.linalg.norm(deep_f) + if n > 0: deep_f /= n + + color_f = extraer_color_zonas(roi) + anat_f = extraer_proporciones_anatomicas(kpts) + + return { + 'deep': deep_f, + 'color': color_f, + 'anatomia': anat_f, + 'ratio_hw': roi.shape[0] / max(roi.shape[1], 1), + 'calidad': cal, + } + except Exception as e: + print(f"[Firma Error] {e}") + return None + +def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=0.0): + if f1 is None or f2 is None: return 0.0 + + sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep'])) + + sim_head = sim_torso = sim_legs = 1.0 + if f1['color'].shape == f2['color'].shape and f1['color'].size > 1: + L = len(f1['color'])//3 + def hcmp(a,b): + if np.sum(a) == 0 or np.sum(b) == 0: return 1.0 + dist = cv2.compareHist(a.astype(np.float32), b.astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + return max(0.0, 1.0 - float(dist)) + + sim_head = hcmp(f1['color'][:L], f2['color'][:L]) + sim_torso = hcmp(f1['color'][L:2*L], f2['color'][L:2*L]) + sim_legs = hcmp(f1['color'][2*L:], f2['color'][2*L:]) + + sim_color = (sim_head * 0.15) + (sim_torso * 0.50) + (sim_legs * 0.35) + + sim_anat = 1.0 + if 'anatomia' in f1 and 'anatomia' in f2: + a1, a2 = f1['anatomia'], f2['anatomia'] + if a1 is not None and a2 is not None: + sim_anat = max(0.0, float(np.dot(a1, a2))) + + penal_sil = 0.0 + if 'ratio_hw' in f1 and 'ratio_hw' in f2: + diff = abs(f1['ratio_hw'] - f2['ratio_hw']) + tol = 0.70 if cross_cam else (0.40 + confianza_fisica * 0.35) + if diff > tol: + penal_sil = min(0.20, (diff-tol) * 0.25) * (1.0 - confianza_fisica) + + if cross_cam: + # ⚡ VETO CRUZADO LETAL: Evita que alguien de claro le robe el ID a alguien oscuro + veto_cruzado = 0.0 + msg_veto = "" + if sim_torso < 0.20: + veto_cruzado = 0.60 + msg_veto = "[VETO LETAL COLOR -0.60]" + elif sim_torso < 0.38: # ⚡ Subimos el límite para atrapar el 0.34 de tu log + veto_cruzado = 0.35 + msg_veto = "[VETO FUERTE COLOR -0.35]" + elif sim_torso < 0.48: # ⚡ Nuevo nivel de advertencia + veto_cruzado = 0.15 + msg_veto = "[VETO LEVE COLOR -0.15]" + + # Veto Biométrico Anti-Uniformes + if sim_anat < 0.70: + veto_cruzado += 0.20 + msg_veto += " [VETO BIOMÉTRICO]" + + resultado = max(0.0, sim_deep - veto_cruzado - penal_sil) + + if sim_deep > 0.40: + print(f" ↳ [CROSS-CAM] Deep:{sim_deep:.2f} Anat:{sim_anat:.2f} Col_Torso:{sim_torso:.2f} {msg_veto} Sil:{-penal_sil:.2f} => FINAL:{resultado:.2f}") + else: + resultado = max(0.0, (sim_deep * 0.80) + (sim_color * 0.10) + (sim_anat * 0.10) - penal_sil) + + return resultado + +# ────────────────────────────────────────────────────────────────────────────── +# KALMAN TRACKER +# ────────────────────────────────────────────────────────────────────────────── +class KalmanTrack: + _count = 0 + + def __init__(self, box, now): + kf = cv2.KalmanFilter(7, 4) + 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) + kf.transitionMatrix = np.eye(7, dtype=np.float32) + kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1 + kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.01, 1.0, 2.0, 1.0]).astype(np.float32) + kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32) + kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32) + kf.statePost = np.zeros((7,1), np.float32) + kf.statePost[:4] = self._z(box) + self.kf = kf + + self.local_id = KalmanTrack._count; KalmanTrack._count += 1 + self.gid, self.origen_global, self.aprendiendo = None, False, False + self.box = list(box) + self.ts_creacion = self.ts_ultima_deteccion = now + self.time_since_update, self.en_grupo, self.frames_buena_calidad = 0, False, 0 + self.listo_para_id = False + self.firma_pre_grupo, self.ts_salio_grupo, self.fallos_post_grupo = None, 0.0, 0 + self.ultimo_aprendizaje, self.frames_continuos, self.frames_observados = 0.0, 0, 0 + self.firma_ema = None + + def _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/max(h,1e-6)]]).astype(np.float32) + + def _bbox(self, x): + cx, cy = float(x[0].item()), float(x[1].item()) + s, r = float(x[2].item()), float(x[3].item()) + w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6) + return [cx - w / 2., cy - h / 2., cx + w / 2., cy + h / 2.] + + @property + def confianza_fisica(self): + return min(1.0, self.frames_continuos / 20.0) + + def actualizar_ema(self, firma, alpha=0.82): + if firma is None: return + if self.firma_ema is None: + self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()} + if 'anatomia' in self.firma_ema and self.firma_ema['anatomia'] is not None: + self.firma_ema['anatomia'] = self.firma_ema['anatomia'].copy() + return + for key in ['deep','color']: + if key in firma: + self.firma_ema[key] = alpha*self.firma_ema[key] + (1-alpha)*firma[key] + n = np.linalg.norm(self.firma_ema[key]) + if n > 0: self.firma_ema[key] /= n + self.firma_ema['ratio_hw'] = alpha*self.firma_ema['ratio_hw'] + (1-alpha)*firma['ratio_hw'] + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in self.firma_ema or self.firma_ema['anatomia'] is None: + self.firma_ema['anatomia'] = firma['anatomia'].copy() + else: + self.firma_ema['anatomia'] = alpha*self.firma_ema['anatomia'] + (1-alpha)*firma['anatomia'] + n = np.linalg.norm(self.firma_ema['anatomia']) + if n > 0: self.firma_ema['anatomia'] /= n + + def predict(self, turno_activo=True): + if self.time_since_update > 4: return None + if getattr(self, 'en_grupo', False): + 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._bbox(self.kf.statePre) + return self.box + + if self.time_since_update > 0: + self.kf.statePost[4] *= 0.5 + self.kf.statePost[5] *= 0.5 + self.kf.statePost[6] = 0.0 + self.frames_continuos = 0 + + 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._bbox(self.kf.statePre) + return self.box + + def update(self, box, en_grupo, now, kpts=None): + self.ts_ultima_deteccion = now + self.time_since_update, self.en_grupo = 0, en_grupo + self.box = list(box) + self.kf.correct(self._z(box)) + + if not en_grupo: + self.frames_observados += 1 + self.frames_continuos += 1 + + if analizar_calidad(box, kpts=kpts, estricto=False) and not en_grupo: + self.frames_buena_calidad += 1 + if self.frames_buena_calidad >= 3: + self.listo_para_id = True + elif self.gid is None: + self.frames_buena_calidad = max(0, self.frames_buena_calidad-1) + +# ────────────────────────────────────────────────────────────────────────────── +# MEMORIA GLOBAL +# ────────────────────────────────────────────────────────────────────────────── +class GlobalMemory: + def __init__(self): + self.db = {} + self.next_gid = 100 + self.lock = threading.RLock() + self.ruta_memoria = os.path.join("cache_nombres", "memoria_ia.json") + os.makedirs("cache_nombres", exist_ok=True) + self._cargar_memoria_inmune() + + def _guardar_memoria_inmune(self): + """Guarda los anticuerpos (Hard Negatives) en el disco""" + datos = {} + for gid, data in self.db.items(): + if data.get('hard_negatives'): + datos[str(gid)] = [hn.tolist() for hn in data['hard_negatives']] + try: + with open(self.ruta_memoria, 'w') as f: json.dump(datos, f) + except Exception as e: print(f"[Persistencia] Error guardando: {e}") + + def _cargar_memoria_inmune(self): + """Carga los anticuerpos al iniciar el sistema""" + if os.path.exists(self.ruta_memoria): + try: + with open(self.ruta_memoria, 'r') as f: + datos = json.load(f) + for gid_str, hn_list in datos.items(): + gid = int(gid_str) + if gid not in self.db: + self.db[gid] = {'ema': None, 'last_cam': '1', 'ts': time.time(), 'hard_negatives': []} + self.db[gid]['hard_negatives'] = [np.array(hn, dtype=np.float32) for hn in hn_list] + print(f"[Persistencia] Memoria Inmune cargada para {len(datos)} IDs.") + except Exception as e: print(f"[Persistencia] Error cargando: {e}") + + def _actualizar_sin_lock(self, gid, firma, cam_id, now): + ALPHA = 0.75 + if gid not in self.db: + self.db[gid] = { + 'ema': firma, + 'last_cam': cam_id, + 'ts': now, + 'nombre': None, + 'hard_negatives': [] # ⚡ Lista negra de firmas usurpadoras + } + return + + rec = self.db[gid] + ema = rec.get('ema') + if ema is None: + rec['ema'] = firma + else: + for key in ['deep','color']: + if key in firma: + ema[key] = ALPHA*ema[key] + (1-ALPHA)*firma[key] + n = np.linalg.norm(ema[key]) + if n > 0: ema[key] /= n + + if 'ratio_hw' in firma and 'ratio_hw' in ema: + ema['ratio_hw'] = ALPHA*ema['ratio_hw'] + (1-ALPHA)*firma['ratio_hw'] + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = firma['anatomia'].copy() + else: + ema['anatomia'] = ALPHA*ema['anatomia'] + (1-ALPHA)*firma['anatomia'] + n = np.linalg.norm(ema['anatomia']) + if n > 0: ema['anatomia'] /= n + ema['calidad'] = firma['calidad'] + + rec['last_cam'] = cam_id + rec['ts'] = now + + def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0): + ema = gid_data.get('ema') + if not ema: return 0.0 + return similitud_hibrida(firma_nueva, ema, cross_cam, confianza_fisica) + + def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0): + self.limpiar_fantasmas() + with self.lock: + candidatos = [] + for gid, data in self.db.items(): + distancia = self._distancia_topologica(str(data['last_cam']), str(cam_id)) + misma_cam = (distancia == 0) + es_vecino = (distancia == 1) + es_salto = (distancia == 2) + es_cross = not misma_cam + + if gid in active_gids: + if misma_cam or distancia >= 3: continue + + dt = now - data.get('ts', now) + + # ⚡ Validación Estricta de Tiempo Mínimo (Evita Teletransportes) + tiempo_min = TIEMPO_MIN_POR_DISTANCIA.get(distancia, 40.0) + if not misma_cam and dt < tiempo_min: + continue + + if dt > TIEMPO_MAX_AUSENCIA: continue + if data.get('ema') is None: continue + + sim = self._sim_robusta(firma, data, es_cross, confianza_fisica) + + # ⚡ SISTEMA INMUNE: Comparamos contra la Lista Negra del ID + anticuerpos = data.get('hard_negatives', []) + for anticuerpo_deep in anticuerpos: + sim_contra_error = max(0.0, 1.0 - cosine(firma['deep'], anticuerpo_deep)) + if sim_contra_error > 0.85: + sim -= 0.40 # Veto letal por parecerse al usurpador conocido + print(f" [INMUNE] ID {gid} protegido. Usurpador detectado (Sim: {sim_contra_error:.2f})") + break + + if firma_ema_local is not None: + sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica) + sim = 0.60*sim + 0.40*sim_local + + penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002) + + if misma_cam: + tipo_distancia = "MISMA_CAM" + umbral = 0.60 if dt < 5.0 else (0.65 if dt < 30.0 else 0.68) + elif es_vecino: + tipo_distancia = "VECINO" + umbral = 0.60 if dt < 10.0 else 0.75 + umbral -= confianza_fisica * 0.05 + elif es_salto: + tipo_distancia = "SALTO(2)" + umbral = 0.82 + else: + tipo_distancia = "MURO_LEJANO(3+)" # ⚡ LÍNEA FALTANTE + umbral = 0.94 + + piso_seguridad = 0.58 if (es_vecino and dt < 10.0) else 0.65 + umbral = max(piso_seguridad, umbral + penal_multitud - (0.03 if data.get('nombre') and not misma_cam else 0.0)) + + if sim > 0.40 and not misma_cam: + estado = "ACEPTADO" if sim > umbral else "RECHAZADO" + faltante = umbral - sim if sim <= umbral else 0.0 + info_faltante = f"(Faltó {faltante:.2f})" if faltante > 0 else "" + print(f" [EVAL] Cam{cam_id} evalúa ID{gid}(Viene de Cam{data['last_cam']} hace {dt:.1f}s) | Tipo: {tipo_distancia}") + print(f" -> Sim_Final={sim:.2f} vs Umbral={umbral:.2f} {info_faltante} -> {estado}") + + if sim > umbral: candidatos.append((sim, gid, misma_cam, es_vecino)) + + if not candidatos: + nid = self.next_gid; self.next_gid += 1 + self._actualizar_sin_lock(nid, firma, cam_id, now) + return nid, False + + candidatos.sort(reverse=True) + best_sim, best_gid, best_misma, best_vecino = candidatos[0] + + self._actualizar_sin_lock(best_gid, firma, cam_id, now) + return best_gid, True + + def limpiar_fantasmas(self): + with self.lock: + ahora = time.time() + muertos = [g for g,d in self.db.items() if (d.get('nombre') is None and ahora-d.get('ts',ahora)>600) or (d.get('nombre') is not None and ahora-d.get('ts',ahora)>900)] + for g in muertos: + # Solo borrar si no tiene anticuerpos importantes + if not self.db[g].get('hard_negatives'): + del self.db[g] + + def _distancia_topologica(self, cam_o, cam_d): + if cam_o == cam_d: return 0 + vecinos_directos = VECINOS.get(cam_o, []) + if cam_d in vecinos_directos: return 1 + for v in vecinos_directos: + if cam_d in VECINOS.get(v, []): return 2 + return 3 + +# ────────────────────────────────────────────────────────────────────────────── +# FEEDBACK EN VIVO (Sistema de Aprendizaje) +# ────────────────────────────────────────────────────────────────────────────── +class FeedbackCorrector: + def __init__(self, global_mem, managers): + self.global_mem = global_mem + self.managers = managers + self.modo_fusion = False + self.gid_fusion_origen = None + + def _tracker_bajo_mouse(self, tile_idx, mx, my): + if tile_idx < 0 or tile_idx >= len(SECUENCIA): return None, None + cid = str(SECUENCIA[tile_idx]) + mgr = self.managers.get(cid) + if not mgr: return None, None + for trk in mgr.trackers: + x1, y1, x2, y2 = trk.box + if x1 <= mx <= x2 and y1 <= my <= y2: return trk, cid + return None, None + + def procesar_tecla(self, key, mouse_state): + tile_idx = mouse_state.get('tile_idx', -1) + mx = mouse_state.get('tile_x', 0) + my = mouse_state.get('tile_y', 0) + + if key == ord('n'): + # ⚡ CREAR NUEVO ID + INYECTAR ANTICUERPO AL ORIGINAL + trk, cid = self._tracker_bajo_mouse(tile_idx, mx, my) + if trk and trk.gid is not None and trk.firma_ema is not None: + viejo_gid = trk.gid + with self.global_mem.lock: + # Inyectar el error en la lista negra del ID legítimo + if 'hard_negatives' not in self.global_mem.db[viejo_gid]: + self.global_mem.db[viejo_gid]['hard_negatives'] = [] + self.global_mem.db[viejo_gid]['hard_negatives'].append(trk.firma_ema['deep'].copy()) + self.global_mem._guardar_memoria_inmune() # Guardar en disco + + # Nace el nuevo ID limpio + nid = self.global_mem.next_gid + self.global_mem.next_gid += 1 + self.global_mem._actualizar_sin_lock(nid, trk.firma_ema, cid, time.time()) + + trk.gid = nid + trk.origen_global = False + print(f"\n[APRENDIZAJE ACTIVO] ¡Error corregido!") + print(f" -> La firma intrusa se inyectó como Anticuerpo en el ID {viejo_gid}.") + print(f" -> Nace el ID {nid} de forma independiente.") + return True + + elif key == ord('m'): + # FUSIONAR IDs + trk, cid = self._tracker_bajo_mouse(tile_idx, mx, my) + if trk and trk.gid is not None: + if not self.modo_fusion: + self.modo_fusion = True + self.gid_fusion_origen = trk.gid + print(f"\n[FUSIÓN] Seleccionaste ID {trk.gid} como ORIGEN. Ahora clickea el DESTINO y presiona 'm'.") + else: + gid_destino = trk.gid + if gid_destino != self.gid_fusion_origen: + with self.global_mem.lock: + if gid_destino in self.global_mem.db and self.gid_fusion_origen in self.global_mem.db: + origen_firmas = self.global_mem.db[self.gid_fusion_origen].get('ema') + if origen_firmas: + self.global_mem._actualizar_sin_lock(gid_destino, origen_firmas, cid, time.time()) + self.global_mem.db[self.gid_fusion_origen]['fusionado_con'] = gid_destino + for m in self.managers.values(): + for t in m.trackers: + if t.gid == self.gid_fusion_origen: t.gid = gid_destino + print(f"\n[FUSIÓN] Éxito: ID {self.gid_fusion_origen} absorbido por ID {gid_destino}") + self.modo_fusion = False + self.gid_fusion_origen = None + return True + + elif key == ord('c'): + if self.modo_fusion: + print("\n[FUSIÓN] Modo fusión cancelado.") + self.modo_fusion = False + self.gid_fusion_origen = None + return True + return False + +# ────────────────────────────────────────────────────────────────────────────── +# GESTOR LOCAL Y RESTO DEL CÓDIGO +# ────────────────────────────────────────────────────────────────────────────── +def iou_overlap(A, B): + xA,yA = max(A[0],B[0]),max(A[1],B[1]) + xB,yB = min(A[2],B[2]),min(A[3],B[3]) + inter = max(0,xB-xA)*max(0,yB-yA) + return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-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 _detectar_grupo(self, trk, box, todos): + x1,y1,x2,y2 = box + w,h = x2-x1, y2-y1 + cx,cy = (x1+x2)/2, (y1+y2)/2 + fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35) + for o in todos: + if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue + if iou_overlap(box, o.box) > 0.05: return True + ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2 + if abs(cx-ocx)= 0.45] + baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45] + matched, sin_match_trk = [], list(range(n_trk)) + + if alta: + C = np.full((n_trk, len(alta)), 100.0, np.float32) + for t, trk in enumerate(self.trackers): + dt_oculto = now - trk.ts_ultima_deteccion + radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto)))) + ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None) + + for idx, (d, det) in enumerate(alta): + iou = iou_overlap(trk.box, det) + dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2) + if dist > radio: continue + + a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1]) + a_det = (det[2]-det[0])*(det[3]-det[1]) + costo = (1.5*(1-iou)) + (0.5*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.2) + (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.2) + min(0.5, dt_oculto*0.2) + + if ref_firma is not None and obtener_firma(d) is not None: + sim_ap = similitud_hibrida(ref_firma, firmas[d]) + if sim_ap < 0.25: costo = 100.0 if trk.frames_observados >= 5 else costo + 1.2 + elif sim_ap < 0.45: costo += (0.45-sim_ap)*(3.5 if trk.frames_observados >= 5 else 1.5) + + C[t,idx] = costo + + for r,c in zip(*linear_sum_assignment(C)): + if C[r,c] <= 6.5: + matched.append((r, alta[c][0])); sin_match_trk.remove(r) + + if sin_match_trk and baja: + C2 = np.ones((len(sin_match_trk), len(baja)), np.float32) + for ti, trk_i in enumerate(sin_match_trk): + for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det) + nuevos_sin = [] + for r,c in zip(*linear_sum_assignment(C2)): + if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0])) + else: nuevos_sin.append(sin_match_trk[r]) + sin_match_trk = nuevos_sin + + return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas + + def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo): + for trk in self.trackers: + if trk.gid: + with self.global_mem.lock: + d = self.global_mem.db.get(trk.gid) + if d and 'fusionado_con' in d: trk.gid = d['fusionado_con'] + + self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None] + if not turno_activo: return self.trackers + + matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints) + active_gids = {t.gid for t in self.trackers if t.gid is not None} + fh, fw = frame_hd.shape[:2] + + for t_idx, d_idx in matched: + trk, box = self.trackers[t_idx], boxes[d_idx] + if firmas[d_idx] is None: + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts) + + firma_det = firmas[d_idx] + es_grupo = self._detectar_grupo(trk, box, self.trackers) + + if not trk.en_grupo and es_grupo: + if trk.gid: + with self.global_mem.lock: trk.firma_pre_grupo = self.global_mem.db.get(trk.gid,{}).get('ema') + trk.ts_salio_grupo = 0.0 + elif trk.en_grupo and not es_grupo: trk.ts_salio_grupo = now + + trk.en_grupo = es_grupo + kpts_persona = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + trk.update(box, es_grupo, now, kpts=kpts_persona) + + if firma_det is not None and not es_grupo and (trk.gid is None or trk.frames_observados < 30 or (box[0]fw*.85 or box[3]>fh*.85)): + trk.actualizar_ema(firma_det) + + if not trk.en_grupo and not ((now - getattr(trk,'ts_salio_grupo',0) < 2.0) and getattr(trk,'ts_salio_grupo',0) > 0): + if trk.gid is None and trk.listo_para_id and firma_det is not None and not (box[0]<5 or box[1]<5 or box[2]>fw-5 or box[3]>fh-5): + gid_c, es_reid = self.global_mem.identificar_candidato( + firma_det, self.cam_id, now, active_gids, + en_borde=(box[0]<35 or box[1]<35 or box[2]>fw-35 or box[3]>fh-35), + firma_ema_local=trk.firma_ema, + confianza_fisica=trk.confianza_fisica + ) + if gid_c: + active_gids.add(gid_c); trk.gid = gid_c; trk.origen_global = es_reid + + elif trk.gid is not None and now-trk.ultimo_aprendizaje > 1.5 and (box[0]fw*.85 or box[3]>fh*.85) and analizar_calidad(box, kpts=kpts_persona, estricto=True) and firma_det is not None and not (box[0]<15 or box[1]<15 or box[2]>fw-15 or box[3]>fh-15): + with self.global_mem.lock: + rec = self.global_mem.db.get(trk.gid) + if rec and rec.get('ema'): + sim_ema = similitud_hibrida(firma_det, rec['ema'], confianza_fisica=trk.confianza_fisica) + if sim_ema < 0.28 and trk.confianza_fisica < 0.30: + trk.gid = None; trk.listo_para_id = False; trk.frames_buena_calidad = 0; continue + if sim_ema > max(0.30, 0.48 - trk.confianza_fisica*0.15): + self.global_mem._actualizar_sin_lock(trk.gid, firma_det, self.cam_id, now) + trk.ultimo_aprendizaje = now; trk.aprendiendo = True + + for d_idx in new_dets: + nt = KalmanTrack(boxes[d_idx], now) + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + if firmas[d_idx] is None: firmas[d_idx] = extraer_firma_hibrida(frame_hd, boxes[d_idx], kpts) + if firmas[d_idx]: nt.actualizar_ema(firmas[d_idx]) + self.trackers.append(nt) + + self.trackers = [t for t in self.trackers if not (t.gid is None and (now - t.ts_creacion) > 4.0) and (now - t.ts_ultima_deteccion) < ((1.0 if (t.box[0]<20 or t.box[1]<20 or t.box[2]>fw-20 or t.box[3]>fh-20) else 2.5) if t.gid is None else (3.0 if (t.box[0]<20 or t.box[1]<20 or t.box[2]>fw-20 or t.box[3]>fh-20) else 8.0))] + return self.trackers + +class CamStream: + def __init__(self, url): + self.url = url; self.cap = cv2.VideoCapture(url) + self.q = queue.Queue(maxsize=1); self.stopped = False + threading.Thread(target=self._run, daemon=True).start() + def _run(self): + while not self.stopped: + ret, f = self.cap.read() + if not ret: time.sleep(2); self.cap.open(self.url); continue + if self.q.full(): + try: self.q.get_nowait() + except queue.Empty: pass + self.q.put(f) + @property + def frame(self): + try: return self.q.get(timeout=0.5) + except queue.Empty: return None + def stop(self): self.stopped = True; self.cap.release() + +def dibujar_track(frame_show, trk, seleccionado=False): + try: x1,y1,x2,y2 = map(int,trk.box) + except: return + if seleccionado: + color, label = C_FEEDBACK, f"SEL:{trk.gid if trk.gid is not None else trk.local_id}" + elif trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}" + elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]" + elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]" + elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]" + 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) + +# ────────────────────────────────────────────────────────────────────────────── +# MAIN +# ────────────────────────────────────────────────────────────────────────────── +def main(): + print("\n--- Iniciando Sistema SmartSoft (Versión Anti-Uniformes e Inmunológica) ---") + print("Controles de Aprendizaje en Vivo (Apunta con el Mouse):") + print(" [n] - ¡Te equivocaste! Separar este ID y marcarlo como usurpador (Anticuerpo).") + print(" [m] - ¡Son el mismo! Fusionar IDs (click origen -> click destino -> 'm').") + print(" [c] - Cancelar fusión.") + print(" [q] - Salir.\n") + + model = YOLO("yolov8n-pose.pt") + global_mem = GlobalMemory() + managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} + cams = [CamStream(u) for u in URLS] + feedback = FeedbackCorrector(global_mem, managers) + + cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE) + + mouse_state = {'x': 0, 'y': 0, 'tile_idx': -1, 'tile_x': 0, 'tile_y': 0} + def on_mouse(event, x, y, flags, param): + mouse_state['x'] = x; mouse_state['y'] = y + col, row = x // 480, y // 270 + if 0 <= col < 3 and 0 <= row < 2: + mouse_state['tile_idx'] = row * 3 + col + mouse_state['tile_x'] = x % 480 + mouse_state['tile_y'] = y % 270 + else: mouse_state['tile_idx'] = -1 + cv2.setMouseCallback("SmartSoft", on_mouse) + + idx = 0 + while True: + now, tiles = time.time(), [] + 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,confs,kpts = [],[],[] + 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: + raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist() + raw_confs = res[0].boxes.conf.cpu().numpy().tolist() + raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else [] + + for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)): + kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None + es_humano = False + + if kpt_persona is not None: + p_validos = sum(1 for p in kpt_persona if p[2] > 0.30) + nariz_ok = kpt_persona[0][2] > 0.30 + hombro_ok = kpt_persona[5][2] > 0.30 or kpt_persona[6][2] > 0.30 + cadera_ok = kpt_persona[11][2] > 0.30 or kpt_persona[12][2] > 0.30 + if p_validos >= 7 and nariz_ok and hombro_ok and cadera_ok: es_humano = True + else: + if analizar_calidad(box, kpts=None, estricto=True): es_humano = True + + if es_humano: + boxes.append(box); confs.append(conf); kpts.append(kpt_persona) + + ESQUELETO = [(0,1),(0,2),(1,3),(2,4),(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)] + for pk in kpts: + if pk is None: continue + for a,b in ESQUELETO: + if pk[a][2]>0.40 and pk[b][2]>0.40: + cv2.line(frame_show, (int(pk[a][0]),int(pk[a][1])), (int(pk[b][0]),int(pk[b][1])), (255,0,255), 2) + for kx, ky, kc in pk: + if kc > 0.40: cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1) + + tracks = managers[cid].update(boxes, confs, kpts, frame_show, frame, now, turno_activo) + + sel_trk = None + if mouse_state['tile_idx'] == i: + for trk in tracks: + x1,y1,x2,y2 = trk.box + if x1<=mouse_state['tile_x']<=x2 and y1<=mouse_state['tile_y']<=y2: + sel_trk = trk; break + + for trk in tracks: + if trk.time_since_update <= 1: dibujar_track(frame_show, trk, seleccionado=(trk is sel_trk)) + if turno_activo: cv2.circle(frame_show,(460,20),6,(0,0,255),-1) + + con_id = sum(1 for t in tracks if getattr(t,'gid',None) 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) + if feedback.modo_fusion and mouse_state['tile_idx'] == i: + cv2.putText(frame_show, "MODO FUSION", (10, 55), FUENTE, 0.6, (0, 0, 255), 2) + + tiles.append(frame_show) + + if len(tiles)==6: + cv2.imshow("SmartSoft", np.vstack([np.hstack(tiles[:3]),np.hstack(tiles[3:])])) + + idx += 1 + key = cv2.waitKey(1) & 0xFF + if key in [ord('n'), ord('m'), ord('c')]: feedback.procesar_tecla(key, mouse_state) + elif key == ord('q'): break + + cv2.destroyAllWindows() + +if __name__ == "__main__": + main() + diff --git a/prueba.png b/prueba.png new file mode 100644 index 0000000..a75d740 Binary files /dev/null and b/prueba.png differ diff --git a/prueba_video.py b/prueba_video.py deleted file mode 100644 index d72ea2f..0000000 --- a/prueba_video.py +++ /dev/null @@ -1,43 +0,0 @@ -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/public_audio/saludo_1782406598.mp3 b/public_audio/saludo_1782406598.mp3 new file mode 100644 index 0000000..df2eb56 Binary files /dev/null and b/public_audio/saludo_1782406598.mp3 differ diff --git a/reconocimiento.py b/reconocimiento.py index 95d80b1..17e6174 100644 --- a/reconocimiento.py +++ b/reconocimiento.py @@ -2,7 +2,7 @@ import os os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ['CUDA_VISIBLE_DEVICES'] = '-1' -# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de OpenCV y Qt +# Silenciamos la burocracia de OpenCV y Qt os.environ['QT_LOGGING_RULES'] = '*=false' os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' @@ -18,7 +18,7 @@ from datetime import datetime import warnings import json -# ⚡ IMPORTACIÓN DE INSIGHTFACE +# IMPORTACIÓN DE INSIGHTFACE from insightface.app import FaceAnalysis warnings.filterwarnings("ignore") @@ -30,7 +30,7 @@ DB_PATH = "db_institucion" CACHE_PATH = "cache_nombres" VECTORS_FILE = "base_datos_rostros.pkl" TIMESTAMPS_FILE = "representaciones_timestamps.pkl" -UMBRAL_SIM = 0.45 # ⚡ Ajustado a la exigencia de producción +UMBRAL_SIM = 0.45 # Ajustado a la exigencia COOLDOWN_TIME = 15 # Segundos entre saludos USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.200" @@ -101,7 +101,7 @@ def hilo_bienvenida(nombre, genero): # 3. Filtra solo los archivos que realmente existen en el disco archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] - # 4. Transmite a la ESP32 (¡Adiós a los parlantes locales!) + # 4. Transmite a la ESP32 if archivos: try: enviar_archivos_bocina(archivos) @@ -161,7 +161,7 @@ def gestionar_vectores(actualizar=False): try: img_db = cv2.imread(ruta_img) - # ⚡ INSIGHTFACE: Extracción primaria + # INSIGHTFACE: Extracción primaria faces = app.get(img_db) # HACK DEL LIENZO NEGRO: Si la foto está muy recortada y no ve la cara, @@ -178,9 +178,9 @@ def gestionar_vectores(actualizar=False): face = max(faces, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])) emb = np.array(face.normed_embedding, dtype=np.float32) - # ⚡ CORRECCIÓN DE GÉNERO: InsightFace devuelve "M" o "F" + # CORRECCIÓN DE GÉNERO: InsightFace devuelve "M" o "F" if falta_genero: - dic_generos[nombre_archivo] = "Man" if face.sex == "M" else "Woman" + dic_generos[nombre_archivo] = "Man" if face.sex == 1 else "Woman" cambio_generos = True vectores_actuales[nombre_archivo] = emb @@ -238,12 +238,13 @@ def buscar_mejor_match(emb_consulta, base_datos): def sistema_interactivo(): base_datos = gestionar_vectores(actualizar=False) cap = cv2.VideoCapture(RTSP_URL) - ultimo_saludo = 0 - persona_actual = None - confirmaciones = 0 + + # Diccionarios para rastrear independientemente a cada persona + cooldown_personas = {} + confirmaciones_personas = {} print("\n" + "=" * 50) - print(" MÓDULO DE REGISTRO - INSIGHTFACE (buffalo_l)") + print(" MÓDULO DE REGISTRO") print(" [R] Registrar nuevo rostro | [Q] Salir") print("=" * 50 + "\n") @@ -260,73 +261,71 @@ def sistema_interactivo(): display_frame = frame.copy() tiempo_actual = time.time() - # ⚡ INSIGHTFACE: Hace TODO aquí (Detección, Landmarks, Género, ArcFace) + # INSIGHTFACE: Extracción de todos los rostros en el frame faces_raw = app.get(frame) faces_ultimo_frame = faces_raw for face in faces_raw: - # Las cajas de InsightFace vienen en formato [x1, y1, x2, y2] box = face.bbox.astype(int) fx, fy, x2, y2 = box[0], box[1], box[2], box[3] fw, fh = x2 - fx, y2 - fy - # Limitamos a los bordes de la pantalla fx, fy = max(0, fx), max(0, fy) fw, fh = min(w - fx, fw), min(h - fy, fh) if fw <= 0 or fh <= 0: continue + # Dibujamos siempre la caja base de detección cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) cv2.putText(display_frame, f"DET:{face.det_score:.2f}", (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) - if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: - continue - - # FILTRO DE TAMAÑO (Para evitar reconocer píxeles lejanos) + # FILTRO DE TAMAÑO if fw < 20 or fh < 20: cv2.putText(display_frame, "muy pequeno", (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) continue - # SIMETRÍA MATEMÁTICA INMEDIATA + # EVALUACIÓN CONTINUA: Siempre extraemos y comparamos el vector, sin importar el cooldown. + # Esto permite que el nombre se actualice en pantalla si la IA corrige su predicción. emb = np.array(face.normed_embedding, dtype=np.float32) mejor_match, max_sim = buscar_mejor_match(emb, base_datos) - 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}%)") + # Evaluación de la similitud if max_sim > UMBRAL_SIM and mejor_match: color = (0, 255, 0) - texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" + texto = f"{nombre_d} ({max_sim:.2f})" - if mejor_match == persona_actual: - confirmaciones += 1 - else: - persona_actual, confirmaciones = mejor_match, 1 + # 1. Sumamos confirmación a esta persona específica + confirmaciones_personas[mejor_match] = confirmaciones_personas.get(mejor_match, 0) + 1 - if confirmaciones >= 2: + # 2. Leemos hace cuánto saludamos a ESTA persona en particular + ultimo_saludo_persona = cooldown_personas.get(mejor_match, 0) + + # Si ya tiene suficientes confirmaciones, marcamos en verde + if confirmaciones_personas[mejor_match] >= 2: cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) - # ⚡ GÉNERO INMEDIATO SIN SOBRECARGA DE CPU - genero = "Man" if face.sex == 1 else "Woman" + # 3. Solo lanzamos el audio si su reloj individual ya se enfrió + if (tiempo_actual - ultimo_saludo_persona) > COOLDOWN_TIME: + genero = "Man" if face.sex == 1 else "Woman" - threading.Thread( - target=hilo_bienvenida, - args=(mejor_match, genero), - daemon=True - ).start() - ultimo_saludo = tiempo_actual - confirmaciones = 0 + threading.Thread( + target=hilo_bienvenida, + args=(mejor_match, genero), + daemon=True + ).start() + + # Actualizamos su cooldown y reseteamos sus confirmaciones + cooldown_personas[mejor_match] = tiempo_actual + confirmaciones_personas[mejor_match] = 0 else: + # El rostro es desconocido o la certeza bajó 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) @@ -339,12 +338,10 @@ def sistema_interactivo(): elif key == ord('r'): if faces_ultimo_frame: - # Tomamos la cara más grande detectada por InsightFace face_registro = max(faces_ultimo_frame, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])) box = face_registro.bbox.astype(int) fx, fy, fw, fh = box[0], box[1], box[2]-box[0], box[3]-box[1] - # Margen estético para guardar la foto m_x, m_y = int(fw * 0.30), int(fh * 0.30) y1, y2 = max(0, fy-m_y), min(h, fy+fh+m_y) x1, x2 = max(0, fx-m_x), min(w, fx+fw+m_x) @@ -354,10 +351,8 @@ def sistema_interactivo(): if face_roi.size > 0: nom = input("\nNombre de la persona: ").strip() if nom: - # Extraemos el género automáticamente gracias a InsightFace gen_str = "Man" if face_registro.sex == 1 else "Woman" - # Actualizamos JSON directo sin preguntar ruta_generos = os.path.join(CACHE_PATH, "generos.json") dic_generos = {} if os.path.exists(ruta_generos): @@ -367,7 +362,6 @@ def sistema_interactivo(): with open(ruta_generos, 'w') as f: json.dump(dic_generos, f) - # Guardado de imagen y sincronización foto_path = os.path.join(DB_PATH, f"{nom}.jpg") cv2.imwrite(foto_path, face_roi) print(f"[OK] Rostro de '{nom}' guardado como {gen_str}. Sincronizando...") diff --git a/reconocimiento2.py b/reconocimiento2.py deleted file mode 100644 index b17378c..0000000 --- a/reconocimiento2.py +++ /dev/null @@ -1,465 +0,0 @@ -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() diff --git a/representaciones_timestamps.pkl b/representaciones_timestamps.pkl index fbcccae..56d9490 100644 Binary files a/representaciones_timestamps.pkl and b/representaciones_timestamps.pkl differ diff --git a/seguimiento2.py b/seguimiento2.py index b5ee7d6..4777a5e 100644 --- a/seguimiento2.py +++ b/seguimiento2.py @@ -9,940 +9,1683 @@ import onnxruntime as ort import os from datetime import datetime import json -import csv +import csv import math import queue +from scipy.spatial.distance import cosine + + +from queue import Queue +from reconocimiento import app, gestionar_vectores, buscar_mejor_match, hilo_bienvenida + +COLA_ROSTROS = Queue(maxsize=4) +IA_LOCK = threading.Lock() +try: + BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True) +except: + BASE_DATOS_ROSTROS = {} # ────────────────────────────────────────────────────────────────────────────── -# CONFIGURACIÓN DEL SISTEMA +# CONFIGURACIÓN # ────────────────────────────────────────────────────────────────────────────── USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" 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" + +# ⚡ FIX: Optimización extrema de red sin romper la interfaz gráfica de Linux +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|fflags;nobuffer|flags;low_delay" +os.environ["OPENCV_LOG_LEVEL"] = "ERROR" + URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA] -ONNX_MODEL_PATH = "osnet_dinamico.onnx" +ONNX_MODEL_PATH = "osnet_x1_0_msmt17_batch1.onnx" VECINOS = { "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], "8": ["5", "3"], "3": ["8", "6"], "6": ["3"] } -ASPECT_RATIO_MIN = 0.6 -ASPECT_RATIO_MAX = 4.0 -AREA_MIN_CALIDAD = 1200 -FRAMES_CALIDAD = 3 -TIEMPO_MAX_AUSENCIA = 800.0 - -MAX_FIRMAS_MEMORIA = 10 - -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) +TIEMPO_MAX_AUSENCIA = 900.0 +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 +# OSNET # ────────────────────────────────────────────────────────────────────────────── -print("Cargando cerebro de Re-Identificación (OSNet)...") +print("Cargando OSNet...") try: - ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider']) + sess_options = ort.SessionOptions() + # Desbloquea los núcleos del procesador para evitar cuellos de botella con 3+ personas + sess_options.intra_op_num_threads = 4 + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + ort_session = ort.InferenceSession(ONNX_MODEL_PATH, sess_options, providers=['CPUExecutionProvider']) input_name = ort_session.get_inputs()[0].name - print("Modelo OSNet cargado exitosamente.") + print("OSNet listo para CPU (Multi-Thread).") 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) + print(f"ERROR FATAL: {e}"); exit() MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1) STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1) -def preprocess_onnx(roi): - # ⚡ ESCUDO ANTI-SOMBRAS (CLAHE) - lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) - l, a, b = cv2.split(lab) - clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) - l_eq = clahe.apply(l) - lab_eq = cv2.merge((l_eq, a, b)) - roi_eq = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR) - - # Preprocesamiento original para OSNet - img = cv2.resize(roi_eq, (128, 256)) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - img = img.transpose(2,0,1).astype(np.float32) / 255.0 - img = (img - MEAN) / STD - - # ⚡ Ahora sí devolverá exactamente (1, 3, 256, 128) - return np.expand_dims(img, axis=0) - -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_hd, box_480): - try: - h_hd, w_hd = frame_hd.shape[:2] - escala_x = w_hd / 480.0 - escala_y = h_hd / 270.0 - - x1, y1, x2, y2 = box_480 - x1_hd, y1_hd = int(x1 * escala_x), int(y1 * escala_y) - x2_hd, y2_hd = int(x2 * escala_x), int(y2 * escala_y) - - x1_c, y1_c = max(0, x1_hd), max(0, y1_hd) - x2_c, y2_c = min(w_hd, x2_hd), min(h_hd, y2_hd) - - roi = frame_hd[y1_c:y2_c, x1_c:x2_c] - if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: return None - - calidad_area = (x2_c - x1_c) * (y2_c - y1_c) - - # ⚡ OPTIMIZACIÓN OSNET DINÁMICO - # Pasamos el tensor directo (1, 3, 256, 128) sin rellenar con ceros - blob = preprocess_onnx(roi) - outputs = ort_session.run(None, {input_name: blob}) - deep_feat = outputs[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 as e: - print(f"Error en extracción: {e}") - return None - -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 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))) - else: - sim_head, sim_torso, sim_legs = 0.0, 0.0, 0.0 - - if cross_cam: - if sim_legs < 0.25: - sim_deep -= 0.15 - return max(0.0, sim_deep) - - sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs) - - 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 +# CONTROL DE PAGINACIÓN DINÁMICA (Para Comunicación con Interfaz) +# ────────────────────────────────────────────────────────────────────────────── +# Variables globales para interfaz +PAGINA_ACTUAL = 0 +CAMS_POR_PANTALLA = 4 +ULTIMO_FRAME_COMPUESTO = None +CAMARA_MAXIMIZADA = None +TELEMETRIA = {'fps': 0.0, 'ids_activos': 0} +MOTOR_ACTIVO = False + +def obtener_ultimo_frame(): + global ULTIMO_FRAME_COMPUESTO + return ULTIMO_FRAME_COMPUESTO + +def construir_mosaico(tiles): + """Construye una matriz dinámica según la cantidad de cámaras visibles en la página""" + n = len(tiles) + if n == 0: return np.zeros((270, 480, 3), dtype=np.uint8) + if n == 1: return tiles[0] + + # Calcular distribución óptima de filas y columnas de forma cuadrada + import math + cols = math.ceil(math.sqrt(n)) + rows = math.ceil(n / cols) + + h, w, c = tiles[0].shape + mosaico = np.zeros((rows * h, cols * w, c), dtype=np.uint8) + + for i, tile in enumerate(tiles): + r = i // cols + c_idx = i % cols + mosaico[r*h:(r+1)*h, c_idx*w:(c_idx+1)*w] = tile + + return mosaico + +# ────────────────────────────────────────────────────────────────────────────── +# EXTRACCIÓN DE FIRMAS MULTI-DESCRIPTOR +# ────────────────────────────────────────────────────────────────────────────── +def extraer_features_osnet_seguro(rois): + + #Procesamiento secuencial 1-a-1 de grado comercial. Garantiza 0% de probabilidad de crasheo C++ en ONNX. + + if not rois: return [] + + features_norm = [] + for roi in rois: + # 1. Preprocesamiento matemático exacto para OSNet + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + + # Expandimos la dimensión para crear un Batch estricto de tamaño 1: (1, 3, 256, 128) + blob = np.expand_dims((img - MEAN) / STD, 0) + + # 2. INFERENCIA AISLADA (El motor ONNX no sufrirá estrés de memoria) + # El [0][0] extrae el vector ignorando la dimensión del batch + df = ort_session.run(None, {input_name: blob})[0][0].flatten() + + # 3. Normalización L2 (Vital para que funcione la distancia Coseno) + n = np.linalg.norm(df) + if n > 0: df /= n + + features_norm.append(df) + + return features_norm + +def calcular_nitidez(img): + if img is None or img.size == 0: return 0 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + return cv2.Laplacian(gray, cv2.CV_64F).var() + +def extraer_color_zonas(roi): + if roi is None or roi.size == 0 or roi.shape[0] < 30 or roi.shape[1] < 10: + return np.zeros(512 * 3, dtype=np.float32) + + h, w = roi.shape[:2] + + # ⚡ LA MÁSCARA ELÍPTICA: Ignora el fondo/paredes + mask = np.zeros((h, w), dtype=np.uint8) + centro_x, centro_y = int(w/2), int(h/2) + eje_x, eje_y = int(w * 0.35), int(h * 0.48) + cv2.ellipse(mask, (centro_x, centro_y), (eje_x, eje_y), 0, 0, 360, 255, -1) + + hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) + t1, t2 = int(h * 0.30), int(h * 0.65) + + z1_img, z1_mask = hsv[:t1, :], mask[:t1, :] + z2_img, z2_mask = hsv[t1:t2, :], mask[t1:t2, :] + z3_img, z3_mask = hsv[t2:, :], mask[t2:, :] + + def calc_hist(img_part, mask_part): + if img_part.size == 0: return np.zeros(512, dtype=np.float32) + hist = cv2.calcHist([img_part], [0, 1, 2], mask_part, [8, 8, 8], [0, 180, 0, 256, 0, 256]) + cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1) + return hist.flatten() + + h1 = calc_hist(z1_img, z1_mask) + h2 = calc_hist(z2_img, z2_mask) + h3 = calc_hist(z3_img, z3_mask) + + return np.concatenate([h1, h2, h3]).astype(np.float32) + + +def extraer_proporciones_anatomicas(kpts): + if kpts is None or len(kpts) < 17: return None + kpts = np.array(kpts) + if kpts.shape[0] < 17: return None + + puntos = {i: kpts[i] for i in range(17)} + + if puntos[0][2] < 0.30: return None + if puntos[5][2] < 0.30 and puntos[6][2] < 0.30: return None + if puntos[11][2] < 0.30 and puntos[12][2] < 0.30: return None + + def dist(i, j): + if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0 + return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2])) + + hombros = dist(5, 6) + caderas = dist(11, 12) + torso = max(dist(5, 11), dist(6, 12)) + pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0 + pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0 + piernas = max(pierna_izq, pierna_der) + brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0 + brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0 + brazos = max(brazo_izq, brazo_der) + + altura = torso + piernas + if altura < 15.0: return None + + feats = np.array([ + hombros / max(altura, 1e-6), + caderas / max(altura, 1e-6), + torso / max(altura, 1e-6), + piernas / max(altura, 1e-6), + brazos / max(altura, 1e-6), + hombros / max(caderas, 1e-6), + piernas / max(torso, 1e-6), + ], dtype=np.float32) + + feats = np.clip(feats, 0.0, 3.0) + n = np.linalg.norm(feats) + if n > 0: feats /= n + return feats + +def es_humano_valido(kpts, box, conf): + if conf < 0.45: return False + if kpts is None or len(kpts) < 17: return False + + x1, y1, x2, y2 = box + w, h = x2 - x1, y2 - y1 + + # Tolerancia extrema a la deformación del cuerpo (agacharse) + if h < 40 or w < 20: return False + if h / max(w, 1) < 0.20: return False # Soporta que estén doblados + + # Si no le vemos la cabeza (ej. inclinado hacia adelante), + # pero el torso está firme, ES humano. + cabeza_segura = sum(1 for p in kpts[:5] if p[2] > 0.40) >= 1 + torso_seguro = sum(1 for p in kpts[5:13] if p[2] > 0.40) >= 2 + + if not (cabeza_segura or torso_seguro): return False + + return True + +def extraer_firma_hibrida(frame_hd, box_480, kpts=None, deep_feat=None): + try: + h_hd, w_hd = frame_hd.shape[:2] + x1, y1, x2, y2 = box_480 + + # Conversión de coordenadas de 480p a resolucion HD + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + + # ROI COMPLETO (Se usará para OSNet/Deep) + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] + + if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: + return None + + score_nitidez = calcular_nitidez(roi) + + # ⚡ PREPROCESAMIENTO OSNET (Usa el roi completo: pies a cabeza) + if deep_feat is None: + img = cv2.resize(roi, (128, 256)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = img.transpose(2, 0, 1).astype(np.float32) / 255.0 + blob = np.expand_dims((img - MEAN) / STD, 0) + + # Inferencia de rescate individual + deep_feat = ort_session.run(None, {input_name: blob})[0][0].flatten() + n = np.linalg.norm(deep_feat) + if n > 0: deep_feat /= n + + # ───────────────────────────────────────────────────────────── + # ⚡ FIX: RECORTE EXCLUSIVO PARA EL COLOR (Ignora fondo) + # ───────────────────────────────────────────────────────────── + h_r, w_r = roi.shape[:2] + + # Cortamos cabeza (15%), piso (20%) y paredes (25% lateral) + y_col1, y_col2 = int(h_r * 0.15), int(h_r * 0.80) + x_col1, x_col2 = int(w_r * 0.25), int(w_r * 0.75) + + roi_color = roi[y_col1:y_col2, x_col1:x_col2] + + # Si el recorte es válido, se lo mandamos a tu extractor de color + if roi_color.size > 0 and roi_color.shape[0] > 5 and roi_color.shape[1] > 5: + color_f = extraer_color_zonas(roi_color) + else: + color_f = extraer_color_zonas(roi) # Fallback por si la caja es muy pequeña + + anat_f = extraer_proporciones_anatomicas(kpts) + calidad = (x2_c - x1_c) * (y2_c - y1_c) + + # Retorna el diccionario original intacto + return { + 'deep': deep_feat, + 'color': color_f, + 'anatomia': anat_f, + 'calidad': calidad, + 'nitidez': score_nitidez + } + except Exception as e: + return None + +def desglose_similitud(f1, f2, cross_cam=False): + if f1 is None or f2 is None: return -1.0, -1.0, -1.0 + deep1, deep2 = f1.get('deep'), f2.get('deep') + c1, c2 = f1.get('color'), f2.get('color') + a1, a2 = f1.get('anatomia'), f2.get('anatomia') + + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) if (deep1 is not None and deep2 is not None) else -1.0 + + # --- ⚡ FIX RADICAL: COMPARACIÓN DE COLOR POR ZONAS (ANTI-INVERSIÓN) --- + sim_color = -1.0 + if c1 is not None and c2 is not None and c1.shape == c2.shape: + s1 = 1.0 - cv2.compareHist(c1[:512], c2[:512], cv2.HISTCMP_BHATTACHARYYA) # Cabeza + s2 = 1.0 - cv2.compareHist(c1[512:1024], c2[512:1024], cv2.HISTCMP_BHATTACHARYYA) # Torso + s3 = 1.0 - cv2.compareHist(c1[1024:], c2[1024:], cv2.HISTCMP_BHATTACHARYYA) # Piernas + + # Penalización severa si el torso o las piernas están invertidas + if s2 < 0.25 or s3 < 0.25: + sim_color = ((s1 * 0.1) + (s2 * 0.6) + (s3 * 0.3)) * 0.3 # Destruimos el score de color + else: + sim_color = (s1 * 0.1) + (s2 * 0.6) + (s3 * 0.3) + + sim_anat = -1.0 + if a1 is not None and a2 is not None and a1.shape == a2.shape: + sim_anat = max(0.0, 1.0 - np.linalg.norm(a1 - a2)) + + return float(sim_deep), float(sim_color), float(sim_anat) + + +def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=1.0): + if f1 is None or f2 is None: return 0.0 + from scipy.spatial.distance import cosine + import numpy as np + + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is None or deep2 is None: return 0.0 + + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + # ⚡ ECUALIZADOR CROSS-CAM + if cross_cam and sim_deep > 0.50: + sim_deep += 0.05 + + # ZONA DE CERTEZA + if sim_deep >= 0.70: return min(1.0, sim_deep + 0.15) + # VÍA DE RECHAZO + if sim_deep < 0.45: return sim_deep + + # TRIBUNAL DE ANATOMÍA + resultado_final = sim_deep + 0.05 + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + + if a1 is not None and a2 is not None and hasattr(a1, "shape") and a1.shape == a2.shape: + sim_anat = max(0.0, 1.0 - np.linalg.norm(a1 - a2)) + peso_anat = 0.12 * confianza_fisica + + if sim_anat > 0.88: resultado_final += peso_anat + if sim_anat >= 0.95: resultado_final += 0.04 + elif sim_anat < 0.35: resultado_final -= peso_anat + else: + if sim_deep >= 0.50: resultado_final += 0.12 + + return float(max(0.0, min(1.0, resultado_final))) + + +# ────────────────────────────────────────────────────────────────────────────── +# 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.05 - 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 + kf = cv2.KalmanFilter(7, 4) + 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) + kf.transitionMatrix = np.eye(7, dtype=np.float32) + kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1 + + # El índice 3 pasa de 0.01 a 0.08 para permitir deformación elástica de la caja + kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.08, 1.0, 2.0, 1.0]).astype(np.float32) + kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32) + kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32) + kf.statePost = np.zeros((7,1), np.float32) + kf.statePost[:4] = self._z(box) + self.kf = kf + + self.local_id = KalmanTrack._count; KalmanTrack._count += 1 + self.gid, self.origen_global, self.aprendiendo = None, False, False self.box = list(box) - self.ts_creacion = now - self.ts_ultima_deteccion = now - self.time_since_update = 0 + self.ts_creacion = self.ts_ultima_deteccion = now + self.time_since_update = 0 self.en_grupo = False - self.frames_buena_calidad = 0 + self.frames_buena_calidad = 0 self.listo_para_id = False - self.area_referencia = 0.0 self.firma_pre_grupo = None - self.ts_salio_grupo = 0 - self.validado_post_grupo = True + self.ts_salio_grupo = 0.0 + self.fallos_post_grupo = 0 + self.ultimo_aprendizaje = 0.0 + self.frames_continuos = 0 + self.frames_observados = 0 + self.firma_ema = None + self.muestras_capturadas = 0 + self.ultimo_cambio_id = 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 _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/max(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) + def _bbox(self, x): + cx, cy = float(x[0].item()), float(x[1].item()) + s, r = float(x[2].item()), float(x[3].item()) + w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6) return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.] - def predict(self, turno_activo=True): - # ⚡ EL SUICIDIO DE CAJAS FANTASMAS - # Si lleva más de 5 frames de ceguera total (YOLO no lo ve), matamos la predicción - if self.time_since_update > 5: - return None - - if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: - self.kf.statePost[6] *= 0.0 - - self.kf.predict() + @property + def confianza_fisica(self): + return min(1.0, self.frames_continuos / 20.0) + + def calcular_score_calidad(self, firma): + if firma is None: return 0.0 + score_nitidez = min(firma.get('nitidez', 0) / 100.0, 1.0) * 50.0 + score_area = min(firma.get('calidad', 0) / 12000.0, 1.0) * 50.0 + return score_nitidez + score_area + + def actualizar_ema(self, firma): + if firma is None: return - if turno_activo: - self.time_since_update += 1 + self.muestras_capturadas = getattr(self, 'muestras_capturadas', 0) + 1 + score_nuevo = self.calcular_score_calidad(firma) + score_viejo = getattr(self, 'score_ema', 0.0) + + if self.firma_ema is None: + # Primera impresión (puede ser mala, pero es lo único que tenemos) + self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()} + self.score_ema = score_nuevo + return - self.aprendiendo = False - self.box = self._convert_x_to_bbox(self.kf.statePre) - return self.box - - - """def predict(self, turno_activo=True): - if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: self.kf.statePost[6] *= 0.0 + # ⚡ PROTECCIÓN DE MEMORIA (Anti-polución) + if self.muestras_capturadas > 4 and score_nuevo < (score_viejo * 0.60): + return + + # ───────────────────────────────────────────────────────────── + # ⚡ ASIMILACIÓN DINÁMICA (La cura a la mala primera impresión) + # ───────────────────────────────────────────────────────────── + # Si estamos en los primeros 5 frames, O la nueva foto es notoriamente mejor + # que nuestro mejor recuerdo (>20% mejor), somos una "esponja". + if self.muestras_capturadas <= 5 or score_nuevo > (score_viejo * 1.20): + # Alpha bajo = Sobrescribe agresivamente el pasado + alpha_deep = 0.20 # 20% vieja, 80% NUEVA + alpha_color = 0.20 + alpha_geo = 0.50 + self.score_ema = score_nuevo # Actualizamos nuestro estándar de calidad + else: + # Estado de madurez: La firma ya es excelente y estable. + # Los cambios deben ser muy lentos para no arruinarla. + alpha_deep = 0.85 # 85% vieja, 15% nueva + alpha_color = 0.50 + alpha_geo = 0.80 + self.score_ema = max(score_nuevo, score_viejo) + + ema = self.firma_ema + + if 'deep' in firma: + ema['deep'] = alpha_deep * ema['deep'] + (1-alpha_deep) * firma['deep'] + n = np.linalg.norm(ema['deep']) + if n > 0: ema['deep'] /= n + + # Ya sin la textura LBP que eliminamos por rendimiento + for key in ['color']: + if key in firma and firma[key] is not None: + ema[key] = alpha_color * ema[key] + (1-alpha_color) * firma[key] + normed = cv2.normalize(ema[key], None, alpha=1.0, norm_type=cv2.NORM_L1) + ema[key] = normed.flatten() + + if 'anatomia' in firma and firma['anatomia'] is not None: + if 'anatomia' not in ema or ema['anatomia'] is None: + ema['anatomia'] = np.array(firma['anatomia'], dtype=np.float32) + else: + ema['anatomia'] = alpha_geo * np.asarray(ema['anatomia']) + (1-alpha_geo) * np.asarray(firma['anatomia']) + + if 'calidad' in firma: + ema['calidad'] = max(ema.get('calidad', 0), firma['calidad']) + + def predict(self, turno_activo=True): + if self.time_since_update > 30: return None + + if getattr(self, 'en_grupo', False): + 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._bbox(self.kf.statePre) + return self.box + + if self.time_since_update > 0: + self.kf.statePost[4] *= 0.5 + self.kf.statePost[5] *= 0.5 + self.kf.statePost[6] = 0.0 + self.frames_continuos = max(0, self.frames_continuos - 1) + + 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""" + self.aprendiendo = False + self.box = self._bbox(self.kf.statePre) + return self.box - def update(self, box, en_grupo, now): + def update(self, box, en_grupo, now, kpts=None, firma_actual=None): self.ts_ultima_deteccion = now - self.time_since_update = 0 + self.time_since_update, self.en_grupo = 0, en_grupo self.box = list(box) - self.en_grupo = en_grupo - self.kf.correct(self._convert_bbox_to_z(box)) + self.kf.correct(self._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) + if not en_grupo: + self.frames_observados += 1 + self.frames_continuos += 1 + self.listo_para_id = True + + +def desglose_similitud(f1, f2, cross_cam=False): + from scipy.spatial.distance import cosine + import numpy as np + import cv2 + + sim_deep = 0.0 + deep1, deep2 = f1.get('deep'), f2.get('deep') + if deep1 is not None and deep2 is not None and np.sum(deep1) > 0 and np.sum(deep2) > 0: + sim_deep = max(0.0, 1.0 - cosine(deep1, deep2)) + + sim_color = -1.0 + c1, c2 = f1.get("color"), f2.get("color") + if c1 is not None and c2 is not None and c1.shape == c2.shape and len(c1) == 1536: + if np.sum(c1) > 0.1 and np.sum(c2) > 0.1: + dist1 = cv2.compareHist(c1[:512].astype(np.float32), c2[:512].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist2 = cv2.compareHist(c1[512:1024].astype(np.float32), c2[512:1024].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + dist3 = cv2.compareHist(c1[1024:].astype(np.float32), c2[1024:].astype(np.float32), cv2.HISTCMP_BHATTACHARYYA) + + s1 = max(0.0, 1.0 - float(dist1)) + s2 = max(0.0, 1.0 - float(dist2)) + s3 = max(0.0, 1.0 - float(dist3)) + + # ⚡ Escudo chamarra abierta original + if cross_cam and s3 > 0.60 and s2 < 0.35: + s2 = max(s2, s3 * 0.80) + + sim_color = (s1 * 0.20) + (s2 * 0.50) + (s3 * 0.30) + + sim_anat = -1.0 + a1, a2 = f1.get("anatomia"), f2.get("anatomia") + if a1 is not None and a2 is not None and hasattr(a1, "shape") and hasattr(a2, "shape") and a1.shape == a2.shape: + dist_a = np.linalg.norm(a1 - a2) + sim_anat = max(0.0, 1.0 - dist_a) + + return sim_deep, sim_color, sim_anat # ────────────────────────────────────────────────────────────────────────────── -# 3. MEMORIA GLOBAL (Anti-Robos y Físicas de Tiempo) +# MEMORIA GLOBAL CON PERSISTENCIA # ────────────────────────────────────────────────────────────────────────────── class GlobalMemory: def __init__(self): self.db = {} + self.mejores_rostros = {} self.next_gid = 100 self.lock = threading.RLock() - self.ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") - self.ultimos_saludos = self._cargar_y_limpiar_saludos() - - def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion_seg): - """Guarda el movimiento y la duración en la cámara anterior en un CSV.""" - directorio = "cache_nombres" - if not os.path.exists(directorio): - os.makedirs(directorio) - - ruta_csv = os.path.join(directorio, "registro_movimientos.csv") - ahora = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - archivo_nuevo = not os.path.exists(ruta_csv) + self.ruta_memoria = os.path.join("cache_nombres", "memoria_ids.json") + self.ruta_movimientos = os.path.join("cache_nombres", "registro_movimientos.csv") + os.makedirs("cache_nombres", exist_ok=True) + self.nombres_activos = {} + self.ultimos_saludos = {} + self.fecha_actual = datetime.now().date() + self.reserva_temporal = {} + self.id_locks = {} + self.ultimas_detecciones = {} - try: - with open(ruta_csv, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - if archivo_nuevo: - writer.writerow(["Fecha_Hora", "Nombre", "Origen", "Destino", "Segundos_en_Origen"]) - - # Redondeamos la duración a 2 decimales - writer.writerow([ahora, nombre, cam_origen, cam_destino, round(duracion_seg, 2)]) - f.flush() # Forzamos la escritura física en el disco - print(f" [LOG] {nombre} estuvo {duracion_seg:.1f}s en Cam {cam_origen} antes de pasar a {cam_destino}") - except Exception as e: - print(f"Error al guardar CSV: {e}") - - try: - with open(ruta_csv, 'a', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - if archivo_nuevo: - writer.writerow(["Fecha_Hora", "Nombre", "Camara_Origen", "Camara_Destino"]) - - writer.writerow([ahora, nombre, cam_origen, cam_destino]) - print(f" [LOG MOVIMIENTO] {nombre} pasó de Cam {cam_origen} a Cam {cam_destino}") - except Exception as e: - print(f"Error al guardar movimiento: {e}") + # 1. Cargamos la memoria base (que ya limpia si es otro día) + self._cargar_memoria() + + # 2. ⚡ REINICIO DIARIO INTELIGENTE: Buscamos el ID más alto de HOY en el CSV + # Esto evita que si apagaste el motor a las 3 PM, vuelva a empezar en 100 y cicle los IDs. + try: + import csv + fecha_hoy_str = datetime.now().strftime('%Y-%m-%d') + if os.path.exists(self.ruta_movimientos): + with open(self.ruta_movimientos, 'r', encoding='utf-8') as f: + for row in csv.reader(f): + # Buscamos filas que sean de HOY y cuyo nombre empiece con "ID " + if len(row) > 1 and fecha_hoy_str in row[0] and str(row[1]).startswith("ID "): + try: + val = int(str(row[1]).replace("ID ", "").strip()) + if val >= self.next_gid: + self.next_gid = val + 1 + except: pass + except Exception as e: + print(f"[MEMORIA] Error leyendo CSV histórico: {e}") + + def _distancia_topologica(self, cam_o, cam_d): + if str(cam_o) == str(cam_d): return 0 + try: + import seguimiento2 + vecinos_dict = getattr(seguimiento2, 'VECINOS', {}) + vecinos_directos = vecinos_dict.get(str(cam_o), []) + if str(cam_d) in vecinos_directos: return 1 + for v in vecinos_directos: + if str(cam_d) in vecinos_dict.get(str(v), []): return 2 + except Exception: pass + return 3 + + def _bonus_reserva(self, gid_evaluado, cam_actual, now): + return 0.0 + + def _cargar_memoria(self): + self.fecha_actual = datetime.now().date() + if os.path.exists(self.ruta_memoria): + fecha_archivo = datetime.fromtimestamp(os.path.getmtime(self.ruta_memoria)).date() + if fecha_archivo != self.fecha_actual: + print(f"[Memoria] Archivo de ayer ({fecha_archivo}). Iniciando memoria limpia desde ID 100.") + return - def _cargar_y_limpiar_saludos(self): - """Carga el historial y elimina registros que no sean de hoy.""" - hoy = datetime.now().strftime("%Y-%m-%d") - if os.path.exists(self.ruta_saludos): try: - with open(self.ruta_saludos, 'r') as f: + with open(self.ruta_memoria, 'r') as f: datos = json.load(f) - # Solo mantenemos los saludos cuya fecha sea 'hoy' - return {nombre: info for nombre, info in datos.items() - if info.get('fecha') == hoy} - except Exception as e: - print(f"Error cargando saludos: {e}") - return {} + + ahora = time.time() + cargados_validos = 0 + + for gid_str, info in datos.items(): + ts_guardado = info.get('ts', 0) + es_vip = info.get('nombre') is not None + + if es_vip and (ahora - ts_guardado > 10800.0): continue + if not es_vip and (ahora - ts_guardado > 3600.0): continue + + gid = int(gid_str) + galeria_deep = [np.array(g, dtype=np.float32) for g in info.get('galeria_deep', [])] + + self.db[gid] = { + 'galeria_deep': galeria_deep, + 'last_cam': info.get('last_cam', '1'), + 'ts': ts_guardado, + 'nombre': info.get('nombre'), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0) + } + cargados_validos += 1 + + if cargados_validos > 0: + self.next_gid = max([int(k) for k in datos.keys()] + [99]) + 1 + print(f"[Memoria] {cargados_validos} IDs persistentes cargados.") + except Exception as e: pass + + ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") + if os.path.exists(ruta_saludos): + try: + with open(ruta_saludos, 'r') as f: self.ultimos_saludos = json.load(f) + except Exception: pass + + def guardar_memoria(self): + try: + datos = {} + for gid, info in self.db.items(): + galeria_list = [g.tolist() for g in info.get('galeria_deep', [])] + datos[str(gid)] = { + 'galeria_deep': galeria_list, + 'last_cam': info.get('last_cam', '1'), + 'nombre': info.get('nombre'), + 'ts': info.get('ts', time.time()), + 'actualizaciones_globales': info.get('actualizaciones_globales', 0) + } + with open(self.ruta_memoria, 'w') as f: + json.dump(datos, f) + except Exception: pass + + # ───────────────────────────────────────────────────────────── + # ⚡ MOTOR DE SOLAPAMIENTO (CÁMARAS VECINAS SIMULTÁNEAS) + # ───────────────────────────────────────────────────────────── + # ⚡ FIX 4: RESTAURADAS LAS FUNCIONES CRÍTICAS DE SOLAPAMIENTO Y CLONES + # ───────────────────────────────────────────────────────────── + # ⚡ MOTOR DE SOLAPAMIENTO Y LIMPIEZA + # ───────────────────────────────────────────────────────────── + def registrar_deteccion_global(self, gid, cam_id, firma, now): + with self.lock: + if not hasattr(self, 'buffer_solapamiento'): self.buffer_solapamiento = [] + if 'deep' in firma: + self.buffer_solapamiento.append({'gid': gid, 'cam': str(cam_id), 'firma': firma['deep'], 'ts': now}) + self.buffer_solapamiento = [x for x in self.buffer_solapamiento if now - x['ts'] < 3.0] + + def buscar_coincidencia_solapada(self, firma, cam_id, now, umbral_sim=0.72): + if 'deep' not in firma: return None + with self.lock: + if not hasattr(self, 'buffer_solapamiento') or not self.buffer_solapamiento: return None + from scipy.spatial.distance import cosine + mejor_sim, mejor_gid = -1.0, None + for item in self.buffer_solapamiento: + if item['cam'] == str(cam_id): continue + sim = 1.0 - cosine(firma['deep'], item['firma']) + if sim > mejor_sim: mejor_sim, mejor_gid = sim, item['gid'] + if mejor_sim >= umbral_sim: + print(f" [ZONA SOLAPADA] Cam {cam_id} hereda ID {mejor_gid} en tiempo real (Sim: {mejor_sim:.2f})") + return mejor_gid + return None + + def consolidar_clones(self): + with self.lock: + gids = list(self.db.keys()) + for i in range(len(gids)): + id_a = gids[i] + if 'fusionado_con' in self.db[id_a]: continue + for j in range(i + 1, len(gids)): + id_b = gids[j] + if 'fusionado_con' in self.db[id_b]: continue + nombre_a, nombre_b = self.db[id_a].get('nombre'), self.db[id_b].get('nombre') + + # ⚡ Si dos cámaras le dieron IDs distintos pero ArcFace descubrió que son la misma persona: + if nombre_a and nombre_b and nombre_a == nombre_b and nombre_a != "Desconocido": + print(f" [LIMPIEZA] Uniendo clon {id_b} a {id_a} (Confirmado facialmente).") + self.db[id_b]['fusionado_con'] = id_a + self.db[id_a]['galeria_deep'].extend(self.db[id_b].get('galeria_deep', [])) + + def lock_id_for_camera(self, gid, cam_id, now, duration=15.0): + self.id_locks[gid] = {'cam': str(cam_id), 'unlock_ts': now + duration} + + def is_id_locked(self, gid, cam_id, now): + if gid not in self.id_locks: return False + lock = self.id_locks[gid] + if lock['cam'] != str(cam_id): return False + if now < lock['unlock_ts']: return True + else: + del self.id_locks[gid] + return False + + def limpiar_locks_vencidos(self, now): + vencidos = [gid for gid, lock in self.id_locks.items() if now >= lock['unlock_ts']] + for gid in vencidos: del self.id_locks[gid] def guardar_saludo(self, nombre): - """Registra el saludo con la fecha actual en disco.""" - hoy = datetime.now().strftime("%Y-%m-%d") - self.ultimos_saludos[nombre] = { - 'fecha': hoy, - 'timestamp': time.time() - } - with open(self.ruta_saludos, 'w') as f: - json.dump(self.ultimos_saludos, f) + ahora = time.time() + self.ultimos_saludos[nombre] = {'fecha': datetime.fromtimestamp(ahora).strftime("%Y-%m-%d"), 'timestamp': ahora} + ruta_saludos = os.path.join("cache_nombres", "registro_saludos.json") + try: + with open(ruta_saludos, 'w') as f: json.dump(self.ultimos_saludos, f, indent=4) + except Exception: pass - def _es_transito_posible(self, data, cam_id, now): - cam_origen = str(data['last_cam']) - cam_destino = str(cam_id) - dt = now - data['ts'] + def registrar_movimiento(self, nombre, cam_origen, cam_destino, duracion, gid=None): + ahora = time.time() + fecha_str = datetime.fromtimestamp(ahora).strftime('%Y-%m-%d %H:%M:%S') + # ⚡ SOLUCIÓN: Sacamos la fecha actual para vincularla a la foto + fecha_hoy = datetime.fromtimestamp(ahora).strftime('%Y-%m-%d') + conf_reid = 0.0 + conf_facial = 0.0 - # 1. Si es la misma cámara, aplicamos la regla normal de tiempo máximo de ausencia - if cam_origen == cam_destino: - return dt < TIEMPO_MAX_AUSENCIA - - # 2. ⚡ DEDUCCIÓN DE TOPOLOGÍA (El sistema aprende el mapa solo) - # Si salta a otra cámara en menos de 1.5 segundos, es físicamente imposible - # a menos que ambas cámaras apunten al mismo lugar (están solapadas). - if dt < 1.5: - # Le damos vía libre inmediata porque sabemos que está en una intersección - return True - - # 3. Si tardó un tiempo normal (ej. > 1.5s), es un tránsito de pasillo válido - return dt < TIEMPO_MAX_AUSENCIA - - 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Ó LÓGICA ESPACIO-TEMPORAL DINÁMICA - def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True): - self.limpiar_fantasmas() + # 1. Aseguramos tener el ID correcto + gid_str = str(gid) if gid else "0" with self.lock: + if gid and gid in self.db: + conf_reid = self.db[gid].get('ultimo_score_reid', 0.0) + conf_facial = self.db[gid].get('confianza_bautizo', 0.0) + # Convertimos "Desconocido" en "ID 105" + if not nombre or nombre == "Desconocido": + nombre = f"ID {gid}" + + # 2. ⚡ ESTAMPADO DE FECHA: La ruta ahora exige que la foto tenga la fecha del día + # Ej: 100_cam6_2026-06-24.jpg + ruta_imagen = os.path.join("cache_nombres", "auditoria_caras", f"{gid_str}_cam{cam_destino}_{fecha_hoy}.jpg") + + if nombre and not nombre.startswith("ID "): + ruta_nombre = os.path.join("cache_nombres", "auditoria_caras", f"{nombre}_cam{cam_destino}_{fecha_hoy}.jpg") + if os.path.exists(ruta_nombre): + ruta_imagen = ruta_nombre + elif os.path.exists(os.path.join("cache_nombres", "auditoria_caras", f"{nombre}_cam{cam_origen}_{fecha_hoy}.jpg")): + ruta_imagen = os.path.join("cache_nombres", "auditoria_caras", f"{nombre}_cam{cam_origen}_{fecha_hoy}.jpg") + else: + if not os.path.exists(ruta_imagen) and os.path.exists(os.path.join("cache_nombres", "auditoria_caras", f"{gid_str}_cam{cam_origen}_{fecha_hoy}.jpg")): + ruta_imagen = os.path.join("cache_nombres", "auditoria_caras", f"{gid_str}_cam{cam_origen}_{fecha_hoy}.jpg") + + # 3. Escribimos en el CSV sin censurar a nadie + try: + import csv + file_exists = os.path.isfile(self.ruta_movimientos) + with open(self.ruta_movimientos, mode='a', newline='') as f: + writer = csv.writer(f) + if not file_exists: + writer.writerow(['fecha', 'nombre', 'cam_origen', 'cam_destino', 'duracion_seg', 'certeza_reid', 'certeza_facial', 'imagen_rostro']) + writer.writerow([fecha_str, nombre, str(cam_origen), str(cam_destino), round(duracion, 2), round(conf_reid, 1), round(conf_facial, 1), ruta_imagen]) + except Exception: pass + + def _actualizar_sin_lock(self, gid, firma, cam_id, now): + if gid not in self.db: + self.db[gid] = { + 'galeria_deep': [], + 'color_promedio': None, + 'last_cam': cam_id, + 'ts': now, + 'nombre': None, + 'actualizaciones_globales': 1, + 'votos_nombre': 0 + } + + rec = self.db[gid] + rec['actualizaciones_globales'] += 1 + + # ───────────────────────────────────────────────────────────── + # ⚡ SOLUCIÓN PARTE 1: EL ANCLA DE HIERRO (Anti-Mutación) + # ───────────────────────────────────────────────────────────── + galeria = rec.get('galeria_deep', []) + if 'deep' in firma: + if not galeria: + # La primera firma es el Ancla (Índice 0). JAMÁS SE BORRA. + galeria.append(firma['deep']) + else: + from scipy.spatial.distance import cosine + sim_con_ancla = 1.0 - cosine(firma['deep'], galeria[0]) + sim_max = max([1.0 - cosine(firma['deep'], g) for g in galeria]) + + # Exigimos 70% de similitud con el Ancla original. + # Si es un extraño (ej. 0.58), será ignorado y forzado a tener un ID nuevo. + if sim_con_ancla >= 0.68 and sim_max <= 0.88: + galeria.append(firma['deep']) + # Siempre borramos el índice 1, NUNCA el 0 (para no borrar el Ancla) + if len(galeria) > 5: galeria.pop(1) + rec['galeria_deep'] = galeria + + if 'color' in firma and firma['color'] is not None: + if rec.get('color_promedio') is None: + rec['color_promedio'] = firma['color'] + else: + import cv2 + rec['color_promedio'] = 0.6 * rec['color_promedio'] + 0.4 * firma['color'] + rec['color_promedio'] = cv2.normalize(rec['color_promedio'], None, 1.0, cv2.NORM_L1).flatten() + + rec['last_cam'] = cam_id + rec['ts'] = now + + def _sim_contra_firma(self, firma_nueva, firma_guardada, cross_cam, confianza_fisica): + if firma_guardada is None: return 0.0 + return similitud_hibrida(firma_nueva, firma_guardada, cross_cam, confianza_fisica) + + def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0): + ema = gid_data.get('ema') + if not ema: return 0.0 + + sim_ema = self._sim_contra_firma(firma_nueva, ema, cross_cam, confianza_fisica) + if sim_ema > 0.75: return sim_ema # Si es muy alta, atajo rápido + + # ⚡ MAGIA RESTAURADA: Evaluación contra las anclas VIP cristalizadas + sim_anclas = [sim_ema] + for ancla in gid_data.get('firmas_altas', []): + sim_anclas.append(self._sim_contra_firma(firma_nueva, ancla, cross_cam, confianza_fisica)) + + mejor_ancla = max(sim_anclas[1:]) if len(sim_anclas) > 1 else sim_ema + return sim_ema * 0.60 + mejor_ancla * 0.40 + + def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0): + self.limpiar_fantasmas() + from scipy.spatial.distance import cosine + import cv2 + import numpy as np + + with self.lock: + # ⚡ 0. INICIALIZAMOS LA LISTA PRINCIPAL (Evita el UnboundLocalError) candidatos = [] - vecinos = VECINOS.get(str(cam_id), []) - - for gid, data in self.db.items(): - if gid in active_gids: continue - dt = now - data['ts'] - - # Bloqueo inmediato si es físicamente imposible - if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): - continue + + # ───────────────────────────────────────────────────────────── + # ⚡ 1. RESCATE INSTANTÁNEO Y SOLAPAMIENTO (Inercia Táctica) + # ───────────────────────────────────────────────────────────── + candidatos_rescate = [] + if 'deep' in firma: + for gid, data in self.db.items(): + if 'fusionado_con' in data: 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 - - sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam) - - penalizacion_multitud = 0.0 - if len(active_gids) > 15: penalizacion_multitud = 0.01 - if len(active_gids) > 30: penalizacion_multitud = 0.02 - if len(active_gids) > 50: penalizacion_multitud = 0.03 + last_cam = str(data.get('last_cam')) + misma_cam = (last_cam == str(cam_id)) + + # No hereda el ID si la persona original ya está en esta misma cámara + if gid in active_gids and misma_cam: continue + + dt = now - data.get('ts', now) + + if dt < 15.0: + distancia = self._distancia_topologica(last_cam, str(cam_id)) + galeria = data.get('galeria_deep', []) + + if galeria: + sim = max([1.0 - cosine(firma['deep'], g) for g in galeria]) + + # Filtro express: aborta si la ropa es opuesta (Ej. blanco vs negro) + if 'color' in firma and firma['color'] is not None and data.get('color_promedio') is not None: + try: + sim_color_rapida = cv2.compareHist(firma['color'].astype(np.float32), data['color_promedio'].astype(np.float32), cv2.HISTCMP_CORREL) + if sim_color_rapida < 0.15: continue + except: pass + + umbral_rescate = 0.72 if (distancia <= 1) else 0.85 + + if sim > umbral_rescate: + candidatos_rescate.append((sim, gid)) + + if candidatos_rescate: + candidatos_rescate.sort(reverse=True) + best_sim, best_gid = candidatos_rescate[0] + self._actualizar_sin_lock(best_gid, firma, cam_id, now) + print(f" [INERCIA] ID {best_gid} rescatado instantáneamente en Cam {cam_id} (Sim: {best_sim:.2f})") + return best_gid, True + + # ───────────────────────────────────────────────────────────── + # ⚡ 2. RE-ID HÍBRIDO NORMAL (Bucle principal) + # ───────────────────────────────────────────────────────────── + try: TIEMPO_MAX = TIEMPO_MAX_AUSENCIA + except: TIEMPO_MAX = 3600 + + for gid, data in self.db.items(): + if 'fusionado_con' in data: continue - if misma_cam: - if dt < 2.0: umbral = 0.58 + penalizacion_multitud - else: umbral = 0.60 + penalizacion_multitud - elif es_vecino: - umbral = 0.60 + penalizacion_multitud - else: - umbral = 0.66 + penalizacion_multitud - """# UMBRALES DINÁMICOS INTELIGENTES - if misma_cam: - if dt < 2.0: umbral = 0.60 # Para parpadeos rápidos de YOLO - else: umbral = 0.63 # Si desapareció un rato en la misma cámara - elif es_vecino: - # ⚡ EL PUNTO DULCE: 0.62. - # Está por encima del ruido máximo (0.57) y por debajo de las - # caídas de Omar en el cluster 7-5-8 (0.64). - umbral = 0.62 - else: - # ⚡ LEJANOS: 0.66. - # Si salta de la Cam 1 a la Cam 8, exigimos seguridad alta - # para no robarle el ID a alguien que acaba de entrar por la otra puerta. - umbral = 0.70""" + dt = now - data.get('ts', now) + last_cam = str(data.get('last_cam')) + cam_actual = str(cam_id) + + distancia = self._distancia_topologica(last_cam, cam_actual) + misma_cam = (distancia == 0) + es_vecino = (distancia == 1) + es_salto = (distancia == 2) + es_cross = not misma_cam + if gid in active_gids: + if misma_cam or distancia >= 3: continue + if dt > TIEMPO_MAX or data.get('ema') is None: continue + if self.is_id_locked(gid, cam_id, now): continue - """ - if misma_cam: - if dt < 2.0: umbral = 0.55 - elif dt < 10.0: umbral = 0.60 - else: umbral = 0.60 - elif es_vecino: - # ⚡ Subimos a 0.66. Si un extraño saca 0.62, será rechazado y nacerá un ID nuevo. - umbral = 0.66 - else: - # ⚡ Cámaras lejanas: Exigencia casi perfecta. - umbral = 0.70""" + # ⚡ Barrera Física Espacial Restaurada + if not misma_cam: + if (es_vecino and dt < 0.3) or (es_salto and dt < 2.0) or (distancia >= 3 and dt < 8.0): + continue - # PROTECCIÓN VIP (Le exigimos un poquito más si ya tiene nombre para no mancharlo) - if data.get('nombre') is not None: - umbral += 0.05 if misma_cam else 0.02 + # ───────────────────────────────────────────────────────────── + # ⚡ 3. TRIBUNAL DE SIMILITUD ROBUSTA (Tu arquitectura original) + # ───────────────────────────────────────────────────────────── + # Calculamos el peso contra TODA la historia de la persona + sim = self._sim_robusta(firma, data, es_cross, confianza_fisica) + + # Desglose para logs y veto + sim_deep, sim_color, sim_anat = -1.0, -1.0, -1.0 + ema = data.get("ema") + if ema is not None: + try: sim_deep, sim_color, sim_anat = desglose_similitud(firma, ema, cross_cam=es_cross) + except: pass - # EL CHIVATO DE OSNET (Debug crucial) - # Esto imprimirá en tu consola qué similitud real de ropa detectó al cruzar de cámara - if sim > 0.35 and not misma_cam: - print(f" [OSNet] Cam {cam_id} evaluando ID {gid} (Viene de Cam {data['last_cam']}) -> Similitud Ropa: {sim:.2f} (Umbral exigido: {umbral:.2f})") + # El Veto de Color ahora usa el cálculo Bhattacharyya hiper-preciso + color_alert = "" + sim = sim_deep - if sim > umbral: - candidatos.append((sim, gid, misma_cam, es_vecino)) + if sim_color >= 0: + if sim_color < 0.15: + # ⚡ SALVAVIDAS: Si el cuerpo y la anatomía juran que eres tú, perdonamos la sombra del pasillo. + if sim_deep > 0.65 and sim_anat > 0.65: + sim -= 0.08 # Castigo muy leve + color_alert = " [VETO MITIGADO!]" + else: + sim -= 0.30 # Castigo brutal a los verdaderos impostores + color_alert = " [VETO COLOR!]" + elif sim_color >= 0.50: + sim += 0.05 + color_alert = " [BONO COLOR]" + + if not misma_cam: + if es_salto and dt < 1.5: continue + if distancia >= 3 and dt < 5.0: continue + + if firma_ema_local is not None: + sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica) + sim = 0.60 * sim + 0.40 * sim_local + + # ───────────────────────────────────────────────────────────── + # ⚡ VALIDACIONES VIP (Escudo Anti-Suplantación) + # ───────────────────────────────────────────────────────────── + es_vip = data.get('nombre') is not None + tiene_firmas_altas = len(data.get('firmas_altas', [])) > 0 + + if es_vip and tiene_firmas_altas and sim < 0.78: continue + elif es_vip and not misma_cam and sim < 0.75: continue + + # ───────────────────────────────────────────────────────────── + # ⚡ 4. UMBRALES DE CRUCE (Los originales, altos y seguros) + # ───────────────────────────────────────────────────────────── + penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002) + + if misma_cam: umbral = 0.68 + elif es_vecino: umbral = 0.72 + elif es_salto: umbral = 0.75 + else: umbral = 0.78 + + if sim_anat > 0.90 and sim_deep > 0.65: + umbral -= 0.04 + color_alert += " [+ANAT]" + + if en_borde and not misma_cam: umbral -= 0.04 + + umbral = max(0.65, umbral + penal_multitud) + + # Bonus si el sistema previno su salida + bonus_aplicado = min(self._bonus_reserva(gid, cam_id, now) if hasattr(self, '_bonus_reserva') else 0.0, 0.03) + sim += bonus_aplicado + + if sim >= umbral - 0.08 or len(candidatos) < 3: + estado = "ACEPTADO" if sim >= umbral else "RECHAZADO" + faltante = f"(Faltó {umbral - sim:.2f})" if sim < umbral else "" + color_str = f"{sim_color:.2f}" if sim_color >= 0 else "N/A" + anat_str = f"{sim_anat:.2f}" if sim_anat >= 0 else "N/A" + print(f"[EVAL] Cam{cam_id} evalúa ID{gid} | Deep={sim_deep:.2f} | Color={color_str} | Anat={anat_str}{color_alert} | Final={sim:.2f} | Umb={umbral:.2f} {faltante} -> {estado}") + + if sim >= umbral: + candidatos.append((sim, sim, gid)) + + # ───────────────────────────────────────────────────────────── + # ⚡ 5. RESOLUCIÓN FINAL + # ───────────────────────────────────────────────────────────── + firma_guardar = firma_ema_local if firma_ema_local is not None else firma if not candidatos: - nid = self.next_gid; self.next_gid += 1 - self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) + nid = self.next_gid + self.next_gid += 1 + self._actualizar_sin_lock(nid, firma_guardar, cam_id, now) + + threading.Thread( + target=self.registrar_movimiento, + args=(f"ID {nid}", cam_id, cam_id, 0.0, nid), + daemon=True + ).start() return nid, False candidatos.sort(reverse=True) - best_sim, best_gid, best_misma, best_vecino = candidatos[0] - - if len(candidatos) >= 2: - segunda_sim, segundo_gid, seg_misma, seg_vecino = candidatos[1] - margen = best_sim - segunda_sim + best_sim, _, best_gid = candidatos[0] + + cam_anterior = self.db[best_gid].get('last_cam') + nombre_registrado = self.db[best_gid].get('nombre') + + if cam_anterior and str(cam_anterior) != str(cam_id): + tiempo_origen = now - self.db[best_gid].get('ts', now) + nombre_usar = nombre_registrado if nombre_registrado else f"ID {best_gid}" - if margen <= 0.06 and best_sim < 0.75: - peso_1 = best_sim + (0.10 if (best_misma or best_vecino) else 0.0) - peso_2 = segunda_sim + (0.10 if (seg_misma or seg_vecino) else 0.0) - - if peso_1 > peso_2: - best_gid = best_gid - elif peso_2 > peso_1: - best_gid = segundo_gid - else: - print(f"\n[ ALERTA] Empate extremo entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna temporal.") - 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) + threading.Thread( + target=self.registrar_movimiento, + args=(nombre_usar, cam_anterior, cam_id, tiempo_origen, best_gid), + daemon=True + ).start() + + self._actualizar_sin_lock(best_gid, firma_guardar, cam_id, now) + self.db[best_gid]['ultimo_score_reid'] = best_sim * 100 + 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) def limpiar_fantasmas(self): + ahora = time.time() + fecha_hoy = datetime.fromtimestamp(ahora).date() + with self.lock: - ahora = time.time() - ids_a_borrar = [] + if fecha_hoy != self.fecha_actual: + self.db.clear() + self.nombres_activos.clear() + self.next_gid = 100 + self.fecha_actual = fecha_hoy + self.guardar_memoria() + return + + muertos = [] + for g, d in self.db.items(): + if d.get('nombre') is None and (ahora - d.get('ts', ahora)) > 3600: + muertos.append(g) + elif d.get('nombre') is not None and (ahora - d.get('ts', ahora)) > 10800: + muertos.append(g) - for gid, data in self.db.items(): - tiempo_inactivo = ahora - data.get('ts', ahora) + for g in muertos: + nombre_perdido = self.db[g].get('nombre') + if nombre_perdido and nombre_perdido in self.nombres_activos: + del self.nombres_activos[nombre_perdido] + del self.db[g] - if data.get('nombre') is None and tiempo_inactivo > 600.0: - ids_a_borrar.append(gid) - - elif data.get('nombre') is not None and tiempo_inactivo > 900.0: - ids_a_borrar.append(gid) - - for gid in ids_a_borrar: - del self.db[gid] - - def confirmar_firma_vip(self, gid, ts): - with self.lock: - if gid in self.db and self.db[gid]['firmas']: - firmas_actuales = self.db[gid]['firmas'] - self.db[gid]['firmas'] = firmas_actuales[-3:] - self.db[gid]['ts'] = ts - - def fusionar_ids(self, id_mantiene, id_elimina): - with self.lock: - if id_mantiene in self.db and id_elimina in self.db: - # 1. Le pasamos toda la memoria de ropa al ID ganador - self.db[id_mantiene]['firmas'].extend(self.db[id_elimina]['firmas']) - - # Topamos a las 15 mejores firmas para no saturar la RAM - if len(self.db[id_mantiene]['firmas']) > 15: - self.db[id_mantiene]['firmas'] = self.db[id_mantiene]['firmas'][-15:] - - self.db[id_mantiene]['ts'] = max(self.db[id_mantiene]['ts'], self.db[id_elimina]['ts']) - - # 2. Vaciamos al perdedor y le ponemos un "Redireccionamiento" - self.db[id_elimina]['firmas'] = [] - self.db[id_elimina]['fusionado_con'] = id_mantiene - print(f"[FUSIÓN MÁGICA] Las firmas del ID {id_elimina} fueron absorbidas por el ID {id_mantiene}.") - return True - return False + if muertos: + self.guardar_memoria() # ────────────────────────────────────────────────────────────────────────────── -# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer) +# GESTOR LOCAL # ────────────────────────────────────────────────────────────────────────────── -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) +def iou_overlap(A, B): + xA,yA = max(A[0],B[0]),max(A[1],B[1]) + xB,yB = min(A[2],B[2]),min(A[3],B[3]) + inter = max(0,xB-xA)*max(0,yB-yA) + return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-inter+1e-6) class CamManager: def __init__(self, cam_id, global_mem): self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] + self.ultimas_detecciones_globales = {} + self.tracks_post_cruce = {} - def _detectar_grupo(self, trk, box, todos_los_tracks): - x1, y1, x2, y2 = box - w_box = x2 - x1 - h_box = y2 - y1 - cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + def _limpiar_tracks_post_cruce(self, now): + #Limpia registros de post-cruce antiguos. + antiguos = [idx for idx, info in self.tracks_post_cruce.items() if now - info['ts_cruce'] > 10.0] + for idx in antiguos: + del self.tracks_post_cruce[idx] - estado_actual = getattr(trk, 'en_grupo', False) - # Si están a menos de medio cuerpo de distancia lateral, es un grupo. - factor_x = 0.9 if estado_actual else 0.7 - factor_y = 0.35 if estado_actual else 0.2 - - for other in todos_los_tracks: - if other is trk: continue - if not hasattr(other, 'box') or other.box is None: continue - - ox1, oy1, ox2, oy2 = other.box - ocx, ocy = (ox1 + ox2) / 2, (oy1 + oy2) / 2 - - dist_x = abs(cx - ocx) - dist_y = abs(cy - ocy) - - if dist_x < (w_box * factor_x) and dist_y < (h_box * factor_y): - return True + def _detectar_grupo(self, trk, box, todos): + x1,y1,x2,y2 = box + w,h = x2-x1, y2-y1 + cx,cy = (x1+x2)/2, (y1+y2)/2 + fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35) + for o in todos: + if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue + if iou_overlap(box, o.box) > 0.05: return True + ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2 + if abs(cx-ocx)= 0.48: + trk.fallos_post_grupo = 0; trk.firma_pre_grupo = None + with self.global_mem.lock: + if trk.gid in self.global_mem.db: self.global_mem.db[trk.gid]['ema'] = fa + else: + trk.fallos_post_grupo += 1 + if trk.fallos_post_grupo >= 3: + trk.gid = trk.firma_pre_grupo = None; trk.fallos_post_grupo = 0 + + def _asignar(self, boxes, confidences, frame_hd, now, keypoints_list): + n_trk, n_det = len(self.trackers), len(boxes) + firmas = [None] * n_det + + if n_det > 0: + rois_validos = [] + mapa_rois = {} + h_hd, w_hd = frame_hd.shape[:2] - tiempo_fuera = now - getattr(trk, 'ts_salio_grupo', 0) - - if tiempo_fuera >= CUARENTENA: - firma_actual = extraer_firma_hibrida(frame_hd, trk.box) + for d_idx, box in enumerate(boxes): + x1, y1, x2, y2 = box + x1_c = max(0, int(x1 * (w_hd/480.0))) + y1_c = max(0, int(y1 * (h_hd/270.0))) + x2_c = min(w_hd, int(x2 * (w_hd/480.0))) + y2_c = min(h_hd, int(y2 * (h_hd/270.0))) + roi = frame_hd[y1_c:y2_c, x1_c:x2_c] - if firma_actual is not None: - sim = similitud_hibrida(firma_actual, trk.firma_pre_grupo) - - # ⚡ Bajamos a 0.50 como umbral de supervivencia base - if sim >= 0.50: - print(f"[GRUPO] ID {trk.gid} validado post-grupo ({sim:.2f}).") - trk.fallos_post_grupo = 0 # Reseteamos los strikes si tenía + if roi.size > 0 and roi.shape[0] >= 40 and roi.shape[1] >= 20: + mapa_rois[d_idx] = len(rois_validos) + rois_validos.append(roi) + + if rois_validos: + deep_feats_seguros = extraer_features_osnet_seguro(rois_validos) + + for d_idx, box in enumerate(boxes): + if d_idx in mapa_rois: + df_persona = deep_feats_seguros[mapa_rois[d_idx]] + kpts = keypoints_list[d_idx] if keypoints_list and d_idx < len(keypoints_list) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts, deep_feat=df_persona) + + def obtener_firma(d_idx): + return firmas[d_idx] + + if n_trk == 0: return [], list(range(n_det)), [], firmas + if n_det == 0: return [], [], list(range(n_trk)), firmas + + alta = [(d,boxes[d]) for d,c in enumerate(confidences) if c >= 0.45] + baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45] + matched, sin_match_trk = [], list(range(n_trk)) + + if alta: + C = np.full((n_trk, len(alta)), 100.0, np.float32) + for t, trk in enumerate(self.trackers): + dt_oculto = now - trk.ts_ultima_deteccion + radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto)))) + ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None) + + for idx, (d, det) in enumerate(alta): + iou = iou_overlap(trk.box, det) + dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2) + if dist > radio: continue + + a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1]) + a_det = (det[2]-det[0])*(det[3]-det[1]) + costo = (1.0*(1-iou)) + (0.3*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.1) + costo += (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.1) + costo += min(0.3, dt_oculto*0.1) + + if ref_firma is not None and obtener_firma(d) is not None: + sim_ap = similitud_hibrida(ref_firma, firmas[d], cross_cam=False, confianza_fisica=trk.confianza_fisica) + if getattr(trk, 'en_grupo', False): + if sim_ap < 0.40: + costo += 5.0 # Castigo suave en oclusiones, nos aferramos a la física de Kalman + else: + costo += (1.0 - sim_ap) * 0.5 + else: + if sim_ap < 0.45: + costo += 50.0 + else: + costo += (1.0 - sim_ap) * 1.5 + + nombre_vip = self.global_mem.db.get(trk.gid, {}).get('nombre') + if nombre_vip and not getattr(trk, 'en_grupo', False): + umbral_proteccion = 0.65 + if sim_ap < umbral_proteccion: + costo += (umbral_proteccion - sim_ap) * 5.0 + + C[t,idx] = costo + + for r,c in zip(*linear_sum_assignment(C)): + if C[r,c] <= 6.5: + matched.append((r, alta[c][0])); sin_match_trk.remove(r) + + if sin_match_trk and baja: + C2 = np.ones((len(sin_match_trk), len(baja)), np.float32) + for ti, trk_i in enumerate(sin_match_trk): + for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det) + nuevos_sin = [] + for r,c in zip(*linear_sum_assignment(C2)): + if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0])) + else: nuevos_sin.append(sin_match_trk[r]) + sin_match_trk = nuevos_sin + + return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas + + def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo): + # ───────────────────────────────────────────────────────────── + # 1. Resolver fusiones globales + # ───────────────────────────────────────────────────────────── + for trk in self.trackers: + if trk.gid: + with self.global_mem.lock: + d = self.global_mem.db.get(trk.gid) + if d and 'fusionado_con' in d: + trk.gid = d['fusionado_con'] + + # ───────────────────────────────────────────────────────────── + # 2. Predict y filtrar tracks muertos por Kalman + # ───────────────────────────────────────────────────────────── + self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None] + if not turno_activo: + return self.trackers + + # ───────────────────────────────────────────────────────────── + # 3. Asignar detecciones a tracks existentes + # ───────────────────────────────────────────────────────────── + matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints) + active_gids = {t.gid for t in self.trackers if t.gid is not None} + fh, fw = frame_hd.shape[:2] + + # ───────────────────────────────────────────────────────────── + # 4. DETECCIÓN DE CRUCES + Registro para recuperación post-cruce + # ───────────────────────────────────────────────────────────── + tracks_en_cruce_locals = set() + for i in range(len(self.trackers)): + for j in range(i + 1, len(self.trackers)): + trk_i, trk_j = self.trackers[i], self.trackers[j] + if trk_i.gid is None or trk_j.gid is None or trk_i.box is None or trk_j.box is None: continue + + iou = iou_overlap(trk_i.box, trk_j.box) + if iou > 0.30: + tracks_en_cruce_locals.add(trk_i.local_id) + tracks_en_cruce_locals.add(trk_j.local_id) + if not hasattr(self, 'tracks_post_cruce'): self.tracks_post_cruce = {} + self.tracks_post_cruce[trk_i.local_id] = {'gid_antes': trk_i.gid, 'ts_cruce': now} + self.tracks_post_cruce[trk_j.local_id] = {'gid_antes': trk_j.gid, 'ts_cruce': now} + + # ───────────────────────────────────────────────────────────── + # ⚡ 4.5. EXTRACCIÓN GARANTIZADA DE FIRMAS (Fix de Ceguera) + # Asegura que tanto los "matched" como los "nuevos" tengan firma. + # ───────────────────────────────────────────────────────────── + for d_idx in range(len(boxes)): + if firmas[d_idx] is None: + kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None + firmas[d_idx] = extraer_firma_hibrida(frame_hd, boxes[d_idx], kpts) + + # ───────────────────────────────────────────────────────────── + # 5. PROCESAR MATCHES + # ───────────────────────────────────────────────────────────── + for t_idx, d_idx in matched: + trk, box = self.trackers[t_idx], boxes[d_idx] + firma_det = firmas[d_idx] + + # Detección de grupo + es_grupo = self._detectar_grupo(trk, box, self.trackers) + if not trk.en_grupo and es_grupo: + with self.global_mem.lock: + trk.firma_pre_grupo = self.global_mem.db.get(trk.gid, {}).get('ema') + trk.ts_salio_grupo = 0.0 + elif trk.en_grupo and not es_grupo: + trk.ts_salio_grupo = now + trk.en_grupo = es_grupo + + # Update Kalman + trk.update(box, es_grupo, now, kpts=(keypoints[d_idx] if keypoints else None), firma_actual=firma_det) + + # Geometría de frame + margen_x = fw * 0.05 + esta_en_centro = (box[0] > margen_x and box[2] < (fw - margen_x)) + + # ───────────────────────────────────────────────────────── + # 5a. APRENDIZAJE EMA (zona segura, centro del frame) + # ───────────────────────────────────────────────────────── + if firma_det is not None and not es_grupo and esta_en_centro: + trk.actualizar_ema(firma_det) + + # Nutrir memoria global cada 2 segundos + if trk.gid is not None: + if not hasattr(trk, 'ultimo_update_global'): + trk.ultimo_update_global = 0 + if (now - trk.ultimo_update_global) > 2.0: with self.global_mem.lock: if trk.gid in self.global_mem.db: - self.global_mem.db[trk.gid]['firmas'] = [trk.firma_pre_grupo, firma_actual] - trk.firma_pre_grupo = None # Aprobado, limpiamos memoria - - else: - # ⚡ SISTEMA DE 3 STRIKES - trk.fallos_post_grupo = getattr(trk, 'fallos_post_grupo', 0) + 1 - - if trk.fallos_post_grupo >= 3: - print(f" [ALERTA GRUPO] ID {trk.gid} falló 3 veces validación ({sim:.2f}). Reseteando ID.") - trk.gid = None - trk.firma_pre_grupo = None # Eliminado, limpiamos memoria - trk.fallos_post_grupo = 0 # Reiniciamos contador - else: - print(f" [STRIKE {trk.fallos_post_grupo}/3] ID {trk.gid} sacó {sim:.2f}. Dando otra oportunidad...") - # OJO: NO ponemos firma_pre_grupo en None aquí, - # para que lo vuelva a intentar en el siguiente frame. + self.global_mem.db[trk.gid]['ema'] = trk.firma_ema.copy() + self.global_mem.db[trk.gid]['ts'] = now + self.global_mem.db[trk.gid]['actualizaciones_globales'] = \ + self.global_mem.db[trk.gid].get('actualizaciones_globales', 0) + 1 + trk.ultimo_update_global = now - def update(self, boxes, frame_show, frame_hd, now, turno_activo): + # ───────────────────────────────────────────────────────── + # 5b. RESCATE DE BORDE (Acelerado) + # ───────────────────────────────────────────────────────── + elif firma_det is not None and not es_grupo and not esta_en_centro: + muestras_act = getattr(trk, 'muestras_capturadas', 0) + if muestras_act < 3: + trk.actualizar_ema(firma_det) + elif muestras_act < 5 and trk.frames_observados % 3 == 0: + trk.actualizar_ema(firma_det) - # 1. MUTACIÓN FUSIÓN - for trk in self.trackers: - if trk.gid is not None: - with self.global_mem.lock: - if trk.gid in self.global_mem.db and 'fusionado_con' in self.global_mem.db[trk.gid]: - print(f" Tracker mutando de ID {trk.gid} a {self.global_mem.db[trk.gid]['fusionado_con']}") - trk.gid = self.global_mem.db[trk.gid]['fusionado_con'] - - # ────────────────────────────────────────────────────────── - # ⚡ FILTRO ANTI-FANTASMAS - vivos_predict = [] - for trk in self.trackers: - caja_predicha = trk.predict(turno_activo=turno_activo) - if caja_predicha is not None: - vivos_predict.append(trk) + # ───────────────────────────────────────────────────────── + # 5c. IDENTIFICACIÓN CON PROTECCIÓN + # ───────────────────────────────────────────────────────── + if trk.gid is None and trk.listo_para_id and firma_det is not None: + # ⚡ FIX RADICAL: Identificación rápida pero segura. + frames_requeridos = 3 if esta_en_centro else 6 - self.trackers = vivos_predict - - if not turno_activo: return self.trackers + if getattr(trk, 'muestras_capturadas', 0) >= frames_requeridos: + if trk.local_id not in tracks_en_cruce_locals: + tiempo_enfriamiento = now - getattr(trk, 'ultimo_cambio_id', 0) + if tiempo_enfriamiento >= 1.0: + gid_c, es_reid = self.global_mem.identificar_candidato( + firma_det, self.cam_id, now, active_gids, + en_borde=not esta_en_centro, + firma_ema_local=trk.firma_ema, + confianza_fisica=trk.confianza_fisica + ) + if gid_c: + active_gids.add(gid_c) + trk.gid = gid_c + trk.origen_global = es_reid + trk.ultimo_cambio_id = now - # ────────────────────────────────────────────────────────── - matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now) - - active_gids = {t.gid for t in self.trackers if t.gid is not None} - - # El Salvavidas OSNet - extracciones_hoy = 0 - - for t_idx, d_idx in matched: - trk = self.trackers[t_idx] - box = boxes[d_idx] + # ───────────────────────────────────────────────────────────── + # 6. VERIFICACIÓN POST-CRUCE (recuperar ID original tras separarse) + # ───────────────────────────────────────────────────────────── + if hasattr(self, 'tracks_post_cruce') and self.tracks_post_cruce: + tracks_post_cruce_nuevos = {} - es_grupo_ahora = self._detectar_grupo(trk, box, self.trackers) - - if not getattr(trk, 'en_grupo', False) and es_grupo_ahora: - if trk.gid is not None: - with self.global_mem.lock: - firmas = self.global_mem.db.get(trk.gid, {}).get('firmas', []) - if firmas: - trk.firma_pre_grupo = firmas[-1] - trk.validado_post_grupo = False - print(f" ID {trk.gid} entró a grupo. Protegiendo firma.") - trk.ts_salio_grupo = 0.0 - - elif getattr(trk, 'en_grupo', False) and not es_grupo_ahora: - trk.ts_salio_grupo = now - print(f" ID {trk.gid} salió de grupo. Cuarentena de 3s.") - - trk.en_grupo = es_grupo_ahora - trk.update(box, es_grupo_ahora, now) - - # ────────────────────────────────────────────────────────── - # 📝 REGISTRO DE MOVIMIENTO (Anti Ping-Pong Dinámico) - # ────────────────────────────────────────────────────────── - if trk.gid is not None: - registro_pendiente = None + for local_id, info in list(self.tracks_post_cruce.items()): + trk = next((t for t in self.trackers if t.local_id == local_id), None) + if trk is None: continue - with self.global_mem.lock: - datos = self.global_mem.db.get(trk.gid, {}) - nombre_oficial = datos.get('nombre') - cam_anterior = datos.get('last_cam') - ultimo_salto_ts = datos.get('last_jump_ts', 0.0) - - # Inicialización - if cam_anterior is None: - datos['last_cam'] = self.cam_id - datos['last_cam_entry_ts'] = now - datos['last_jump_ts'] = now - - # Detección de salto - elif nombre_oficial is not None and str(cam_anterior) != str(self.cam_id): - - cam_origen = str(cam_anterior) - cam_destino = str(self.cam_id) - cluster_interseccion = {'5', '7', '8'} - - # ⚡ COOLDOWN INTELIGENTE POR TOPOLOGÍA - if cam_origen in cluster_interseccion and cam_destino in cluster_interseccion: - cooldown_exigido = 4.0 - else: - cooldown_exigido = 1.0 - - if (now - ultimo_salto_ts) > cooldown_exigido: - entrada_ts = datos.get('last_cam_entry_ts', now) - duracion = now - entrada_ts - - registro_pendiente = (nombre_oficial, cam_anterior, self.cam_id, duracion) - - datos['last_cam'] = self.cam_id - datos['last_cam_entry_ts'] = now - datos['last_jump_ts'] = now - - # Escribimos el CSV fuera del lock de memoria - if registro_pendiente: - self.global_mem.registrar_movimiento(*registro_pendiente) - - # ────────────────────────────────────────────────────────── - # LÓGICA DE BAUTIZO Y APRENDIZAJE - # ────────────────────────────────────────────────────────── - area_actual = (box[2] - box[0]) * (box[3] - box[1]) - tiempo_desde_separacion = now - getattr(trk, 'ts_salio_grupo', 0) - en_cuarentena = (tiempo_desde_separacion < 3.0) and (getattr(trk, 'ts_salio_grupo', 0) > 0) - - if not trk.en_grupo and not en_cuarentena: - - # A) Bautizo de IDs Nuevos (Con Aduana Temporal) - if trk.gid is None and trk.listo_para_id: - firma = extraer_firma_hibrida(frame_hd, box) - if firma is not None: - fh, fw = frame_hd.shape[:2] # ⚡ FIX: Agregado [:2] - bx1, by1, bx2, by2 = map(int, box) - nace_en_borde = (bx1 < 25 or by1 < 25 or bx2 > fw - 25 or by2 > fh - 25) - - candidato_gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde) - - if candidato_gid is not None: - active_gids.add(candidato_gid) - - if not es_reid: - trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, False, area_actual - else: - if getattr(trk, 'candidato_temporal', None) == candidato_gid: - trk.votos_reid = getattr(trk, 'votos_reid', 0) + 1 - else: - trk.candidato_temporal = candidato_gid - trk.votos_reid = 1 - - if trk.votos_reid >= 2: - trk.gid, trk.origen_global = candidato_gid, True - - # B) Aprendizaje Continuo OSNet - elif trk.gid is not None: - tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) - - if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box) and extracciones_hoy < 1: - fh, fw = frame_hd.shape[:2] # ⚡ FIX: Agregado [: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_hd, box) - if firma_nueva is not None: - extracciones_hoy += 1 - - with self.global_mem.lock: - if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid].get('firmas'): - - firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1] - firma_original = self.global_mem.db[trk.gid]['firmas'][0] - - sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente) - sim_raiz = similitud_hibrida(firma_nueva, firma_original) - - # ⚡ BOTÓN DE PÁNICO (Anti ID-Switch) - if sim_raiz < 0.35: - print(f" Ropa de ID {trk.gid} cambió drásticamente. Revocando ID.") - trk.gid = None - trk.listo_para_id = False - trk.frames_buena_calidad = 0 - continue - - ya_bautizado = self.global_mem.db[trk.gid].get('nombre') is not None - umbral_raiz = 0.52 if ya_bautizado else 0.62 - - if sim_coherencia > 0.60 and sim_raiz > umbral_raiz: - es_coherente = True - for otro_gid, otro_data in self.global_mem.db.items(): - if otro_gid == trk.gid or not otro_data.get('firmas'): continue - sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0]) - if sim_intruso > sim_raiz: - 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)) - - # ────────────────────────────────────────────────────────── - # ⚡ LIMPIEZA AGRESIVA - # ────────────────────────────────────────────────────────── - vivos = [] - fh, fw = frame_show.shape[:2] # ⚡ FIX: Agregado [:2] - for t in self.trackers: - x1, y1, x2, y2 = t.box - toca_borde = (x1 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20) - tiempo_oculto = now - t.ts_ultima_deteccion - - if t.gid is None: - limite_vida = 1.0 if toca_borde else 2.5 - else: - limite_vida = 3.0 if toca_borde else 8.0 - - if tiempo_oculto < limite_vida: - vivos.append(t) - - self.trackers = vivos - self._gestionar_aprendizaje_post_grupo(now, frame_hd) - - 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) - - for t, trk in enumerate(self.trackers): - tiempo_oculto = now - trk.ts_ultima_deteccion - - # ⚡ INCERTIDUMBRE CUADRÁTICA DE KALMAN - radio_dinamico = min(400.0, max(150.0, 300.0 * math.sqrt(max(0.1, tiempo_oculto)))) - incertidumbre = min(0.5, tiempo_oculto * 0.2) - - 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_pixel = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) - - area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1]) - area_det = (det[2] - det[0]) * (det[3] - det[1]) - - if dist_pixel > radio_dinamico: - cost_mat[t, d] = 100.0 + if now - info['ts_cruce'] > 2.0: + if trk.gid is not None and trk.gid != info['gid_antes']: + if trk.firma_ema is not None and info['gid_antes'] in self.global_mem.db: + ema_original = self.global_mem.db[info['gid_antes']].get('ema') + if ema_original is not None: + sim_recuperacion = similitud_hibrida( + trk.firma_ema, ema_original, cross_cam=False + ) + if sim_recuperacion > 0.85: + if info['gid_antes'] not in active_gids: + print(f" [POST-CRUCE] Track {trk.local_id} recuperó ID {info['gid_antes']} (sim={sim_recuperacion:.2f})") + active_gids.discard(trk.gid) + trk.gid = info['gid_antes'] + active_gids.add(trk.gid) continue - dist_norm = dist_pixel / radio_dinamico - ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) - - if area_det < area_trk: castigo_tam = (ratio_area - 1.0) * 0.5 - else: castigo_tam = (ratio_area - 1.0) * 0.15 - - cost_mat[t, d] = (1.0 - iou) + (0.5 * dist_norm) + castigo_tam + incertidumbre - - row_ind, col_ind = linear_sum_assignment(cost_mat) - matched, unmatched_dets, unmatched_trks = [], [], [] - - for r, c in zip(row_ind, col_ind): - if cost_mat[r, c] > 3.5: - unmatched_trks.append(r); unmatched_dets.append(c) - else: - matched.append((r, c)) + tracks_post_cruce_nuevos[local_id] = info - 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 + self.tracks_post_cruce = tracks_post_cruce_nuevos -# ────────────────────────────────────────────────────────────────────────────── -# 5. STREAM Y MAIN LOOP (Standalone) -# ────────────────────────────────────────────────────────────────────────────── -class CamStream: - def __init__(self, url): - self.url = url - self.cap = cv2.VideoCapture(url) - # ⚡ COLA DE TAMAÑO 1 PARA PARALELISMO REAL (Sin frames del pasado) - self.q = queue.Queue(maxsize=1) - self.stopped = False - threading.Thread(target=self._run, daemon=True).start() + # ───────────────────────────────────────────────────────────── + # 7. CREAR NUEVOS TRACKERS para detecciones sin match + # ───────────────────────────────────────────────────────────── + for d_idx in new_dets: + nt = KalmanTrack(boxes[d_idx], now) + # ⚡ AHORA SÍ RECIBEN FIRMA AL NACER (Gracias al Paso 4.5) + f = firmas[d_idx] + if f: + nt.actualizar_ema(f) + self.trackers.append(nt) + + # ───────────────────────────────────────────────────────────── + # 8. LIMPIEZA Y ASESINO DE CLONES + # ───────────────────────────────────────────────────────────── + tracks_vivos = [t for t in self.trackers if (now - t.ts_ultima_deteccion) < 4.0] + + tracks_sin_clones = [] + for t in tracks_vivos: + es_clon = False + for otro in tracks_sin_clones: + if iou_overlap(t.box, otro.box) > 0.40: + es_clon = True + if getattr(t, 'muestras_capturadas', 0) > getattr(otro, 'muestras_capturadas', 0): + tracks_sin_clones.remove(otro) + tracks_sin_clones.append(t) + break + if not es_clon: + tracks_sin_clones.append(t) + + self.trackers = tracks_sin_clones + + if hasattr(self, 'tracks_post_cruce'): + local_ids_vivos = {t.local_id for t in self.trackers} + self.tracks_post_cruce = {lid: info for lid, info in self.tracks_post_cruce.items() if lid in local_ids_vivos} + + # ───────────────────────────────────────────────────────────── + # 9. Gestión post-grupo + # ───────────────────────────────────────────────────────────── + self._gestionar_post_grupo(now, frame_hd) + return self.trackers + +class CamStream: + def __init__(self, url, cam_id): + self.url = url + self.cam_id = str(cam_id) + self.cap = cv2.VideoCapture(url) + self.q = queue.Queue(maxsize=1) + self.stopped = False + self.is_alive = True + self.fallos = 0 + + # Pantalla de muerte con el número de cámara + self.frame_error = np.zeros((270, 480, 3), dtype=np.uint8) + cv2.putText(self.frame_error, f"CAM {self.cam_id} OFFLINE", (110, 135), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) + + threading.Thread(target=self._run, daemon=True).start() + def _run(self): while not self.stopped: ret, f = self.cap.read() - if not ret: - time.sleep(2) - self.cap.open(self.url) - continue + if not ret: + if self.stopped: break + self.is_alive = False + self.fallos += 1 - # Si la cola está llena, descartamos el frame viejo + # Liberamos el socket inmediatamente para que el DVR respire + if self.cap: self.cap.release() + + wait_time = min(30, 2 ** self.fallos) + print(f" [ALERTA] Cam {self.cam_id} caída. Reintentando en {wait_time}s...") + + # Espera fraccionada para no bloquear la orden de cierre del usuario + for _ in range(wait_time * 10): + if self.stopped: break + time.sleep(0.1) + + if self.stopped: break + + self.cap = cv2.VideoCapture(self.url) + continue + + if self.fallos > 0: + print(f" [INFO] Cam {self.cam_id} RECUPERADA exitosamente.") + + self.fallos = 0 + self.is_alive = True + if self.q.full(): - try: - self.q.get_nowait() - except queue.Empty: - pass - + try: self.q.get_nowait() + except queue.Empty: pass self.q.put(f) + # La memoria C++ se libera DENTRO de su propio hilo. + if self.cap: + self.cap.release() + @property def frame(self): - # ⚡ Extrae el frame fresco de la cola, si está vacía espera medio segundo - try: - return self.q.get(timeout=0.5) - except queue.Empty: - return None + if not self.is_alive: return self.frame_error.copy() + try: return self.q.get(timeout=0.5) + except queue.Empty: return self.frame_error.copy() - def stop(self): + def stop(self): self.stopped = True - self.cap.release() def dibujar_track(frame_show, trk): + try: x1,y1,x2,y2 = map(int,trk.box) + except: return + if trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}" + elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]" + elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]" + elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]" + 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 procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk): + try: + if roi_cabeza is None or roi_cabeza.size == 0: return + with IA_LOCK: faces = app.get(roi_cabeza) + if len(faces) == 0: return + + face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) + box = face.bbox.astype(int) + w_f, h_f = box[2] - box[0], box[3] - box[1] + + if w_f < 20 or h_f < 20: return + + emb = np.array(face.normed_embedding, dtype=np.float32) + genero_detectado = "Man" if face.sex == "M" else "Woman" + + h_roi, w_roi = roi_cabeza.shape[:2] + m_x, m_y_top, m_y_bot = int(w_f * 0.50), int(h_f * 0.50), int(h_f * 0.70) + y1, y2 = max(0, box[1] - m_y_top), min(h_roi, box[3] + m_y_bot) + x1, x2 = max(0, box[0] - m_x), min(w_roi, box[2] + m_x) + rostro_puro = roi_cabeza[y1:y2, x1:x2] + + area_rostro = w_f * h_f + es_mejor_rostro_camara = False + llave_camara = f"{gid}_cam{cam_id}" + + with global_mem.lock: + if not hasattr(global_mem, 'mejores_rostros'): global_mem.mejores_rostros = {} + mejor_area_actual = global_mem.mejores_rostros.get(llave_camara, {}).get('area', 0) + if area_rostro > mejor_area_actual: + global_mem.mejores_rostros[llave_camara] = {'area': area_rostro, 'ts': time.time()} + es_mejor_rostro_camara = True + + ruta_dir = os.path.join("cache_nombres", "auditoria_caras") + os.makedirs(ruta_dir, exist_ok=True) + fecha_hoy = time.strftime('%Y-%m-%d') + + if BASE_DATOS_ROSTROS: mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) + else: mejor_match, max_sim = None, 0.0 + + # ───────────────────────────────────────────────────────────── + # ⚡ SISTEMA DE VOTACIÓN COMPRENSIVO (Tolerancia a perfiles) + # ───────────────────────────────────────────────────────────── + puntos_voto = 0 + if max_sim >= 0.35: puntos_voto = 3 # Frontal / Claro + elif max_sim >= 0.25: puntos_voto = 2 # Ángulo Ligero + elif max_sim >= 0.18: puntos_voto = 1 # Perfil / Borroso / Luz difícil + + with global_mem.lock: + datos_id = global_mem.db.get(gid) + if not datos_id: return + nombre_actual = datos_id.get('nombre') + + # ───────────────────────────────────────────────────────────── + # 🧠 SISTEMA DE AUTO-APRENDIZAJE (VALIDACIÓN DE DOBLE LLAVE) + # ───────────────────────────────────────────────────────────── + if nombre_actual and nombre_actual != "Desconocido" and mejor_match: + nombre_limpio = mejor_match.split('_')[0] + + # LLAVE 1: OSNet dice que es nombre_actual. + # LLAVE 2: ArcFace dice que el match más cercano es nombre_limpio. + if nombre_actual == nombre_limpio and 0.18 <= max_sim < 0.40: + + ahora = time.time() + ultimo_ap = datos_id.get('ultimo_aprendizaje', 0) + + # Control de spam: Solo aprende 1 cara cada 10 segundos + if (ahora - ultimo_ap) > 10.0 and rostro_puro.size > 0: + ruta_ap = "db_aprendida" + os.makedirs(ruta_ap, exist_ok=True) + + # Guardamos la foto en el disco duro para respaldo + nombre_archivo = f"{nombre_limpio}_auto_{int(ahora)}.jpg" + ruta_guardado = os.path.join(ruta_ap, nombre_archivo) + cv2.imwrite(ruta_guardado, rostro_puro) + + # INYECCIÓN EN RAM: Actualizamos la base de datos viva al instante + BASE_DATOS_ROSTROS[nombre_archivo] = emb + datos_id['ultimo_aprendizaje'] = ahora + + print(f" 🧠 [APRENDIZAJE] Cam {cam_id}: Nuevo ángulo de {nombre_limpio} aprendido! (Sim base: {max_sim:.2f})") + + # Al validar que las llaves empataron, le damos los 3 puntos completos + # para que ArcFace deje de dudar y estabilice el Tracker. + puntos_voto = 3 + + # --- CASO A: LA IA DUDA --- + if puntos_voto == 0 or not mejor_match: + if nombre_actual and nombre_actual != "Desconocido": + datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) - 1 + if datos_id['votos_nombre'] <= 0: + datos_id['nombre'] = "Desconocido" + datos_id['votos_nombre'] = 0 + print(f" [CORRECCIÓN] IA duda del ID {gid}. Pierde el nombre '{nombre_actual}'.") + else: + datos_id['nombre'] = "Desconocido" + if es_mejor_rostro_camara and rostro_puro.size > 0: + cv2.imwrite(os.path.join(ruta_dir, f"{gid}_cam{cam_id}_{fecha_hoy}.jpg"), rostro_puro) + import fision1 + fision1.NUEVAS_ALERTAS.append((gid, cam_id, rostro_puro)) + return + + nombre_limpio = mejor_match.split('_')[0] + + # --- CASO B: CONFLICTO DE ROSTROS (El escudo anti-envenenamiento) --- + if nombre_actual and nombre_actual != "Desconocido" and nombre_actual != nombre_limpio: + datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) - puntos_voto + if datos_id['votos_nombre'] <= 0: + datos_id['nombre'] = "Desconocido" + datos_id['votos_nombre'] = 0 + print(f" [CORRECCIÓN] Conflicto facial en ID {gid}. Revertido a Desconocido.") + return + + # --- CASO C: CONFIRMACIÓN Y BAUTIZO --- + if es_mejor_rostro_camara and rostro_puro.size > 0: + cv2.imwrite(os.path.join(ruta_dir, f"{nombre_limpio}_cam{cam_id}_{fecha_hoy}.jpg"), rostro_puro) + + if nombre_actual == nombre_limpio: + datos_id['votos_nombre'] = min(5, datos_id.get('votos_nombre', 0) + puntos_voto) + return + + votos = datos_id.get('votos_nombre', 0) + puntos_voto + datos_id['votos_nombre'] = votos + + if votos >= 3: + id_veterano = None + for gid_mem, data_mem in global_mem.db.items(): + if data_mem.get('nombre') == nombre_limpio and gid_mem != gid: + id_veterano = gid_mem + break + + if id_veterano is not None: + print(f" [FUSIÓN] ArcFace une clon {gid} al original {nombre_limpio} (ID {id_veterano}).") + datos_id['fusionado_con'] = id_veterano + trk.gid = id_veterano + global_mem.db[id_veterano]['ts'] = time.time() + else: + datos_id['nombre'] = nombre_limpio + print(f" [BAUTIZO VIP] Cam {cam_id}: ID {gid} es {nombre_limpio} ({max_sim*100:.1f}%)") + + threading.Thread( + target=global_mem.registrar_movimiento, + args=(nombre_limpio, cam_id, cam_id, 0.0, gid), + daemon=True + ).start() + + if str(cam_id) == "7": + ahora = time.time() + info_saludo = global_mem.ultimos_saludos.get(nombre_limpio, {}) + if (ahora - info_saludo.get('timestamp', 0)) > 30: + global_mem.guardar_saludo(nombre_limpio) + threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_detectado), daemon=True).start() + + except Exception as e: + print(f" [ERROR ARCFACE] ID {gid}: {str(e)}") + finally: + trk.procesando_rostro = False + +def worker_rostros(global_mem): + while True: + roi, gid, cam, trk = COLA_ROSTROS.get() + procesar_rostro_async(roi, gid, cam, global_mem, trk) + COLA_ROSTROS.task_done() +# ────────────────────────────────────────────────────────────────────────────── +# FUNCIÓN DE DIBUJO VISUAL (CON NOMBRES VIP) +# ────────────────────────────────────────────────────────────────────────────── +def dibujar_track_fusion(frame_show, trk, global_mem): 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}" + nombre_str = "" + # Si tiene ID global, buscamos su nombre en la memoria + 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}]" + + # Asignación de colores + 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}" # VIP = Morado + elif getattr(trk, 'en_grupo', False): color, label = (0, 0, 255), f"ID:{trk.gid} [grp]" + elif getattr(trk, 'aprendiendo', False): color, label = (255, 255, 0), f"ID:{trk.gid} [++]" + elif getattr(trk, 'origen_global', False): 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("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, frame, 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() - - diff --git a/vesiones_seguras.txt b/vesiones_seguras.txt deleted file mode 100644 index 0653fd8..0000000 --- a/vesiones_seguras.txt +++ /dev/null @@ -1,1258 +0,0 @@ -############################################################ 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() diff --git a/video.mp4 b/video.mp4 deleted file mode 100644 index cc2ba25..0000000 Binary files a/video.mp4 and /dev/null differ diff --git a/yolov8n.pt b/yolov8n.pt deleted file mode 100644 index 0db4ca4..0000000 Binary files a/yolov8n.pt and /dev/null differ