Registro de personas a traves de un archivo csv cuando ya fueron detectadas, coneccion con la ESP32 y modificacion del hilo de audio para que suene igual que como se tenia

This commit is contained in:
rodrigo 2026-04-13 12:46:22 -06:00
parent 6bc9a5cb44
commit 86d2e8fc9f
10 changed files with 2126 additions and 138 deletions

77
audio_iot.py Normal file
View File

@ -0,0 +1,77 @@
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
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
except Exception as e:
print(f" Error conectando a la bocina IP {IP_BOCINA}: {e}")
return False
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:
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))
except Exception as e:
print(f"Error procesando el audio para la bocina: {e}")

Binary file not shown.

View File

@ -0,0 +1,4 @@
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
1 Fecha_Hora,Nombre,Origen,Destino,Segundos_en_Origen
2 2026-04-13 10:40:23,Emanuel Flores,8,5,0.0
3 Fecha_Hora,Nombre,Camara_Origen,Camara_Destino
4 2026-04-13 10:40:23,Emanuel Flores,8,5

View File

@ -0,0 +1 @@
{"Emanuel Flores": {"fecha": "2026-04-13", "timestamp": 1776101179.9010606}, "Rodrigo Cahuantzi C": {"fecha": "2026-04-13", "timestamp": 1776105668.9753058}}

View File

@ -10,10 +10,10 @@ import cv2
import numpy as np import numpy as np
import time import time
import threading import threading
import queue
from queue import Queue from queue import Queue
from ultralytics import YOLO from ultralytics import YOLO
import warnings import warnings
import json
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
@ -73,97 +73,116 @@ def procesar_rostro_async(roi_cabeza, gid, cam_id, global_mem, trk):
votos_finales = 0 votos_finales = 0
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
# ⚡ LÓGICA DE VOTACIÓN PONDERADA INYECTADA # ⚡ LÓGICA DE VOTACIÓN PONDERADA (Con tus umbrales)
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
# Aceptamos rostros de CCTV desde 0.38 (ajustado según tu código a 0.35) if max_sim >= 0.18 and mejor_match:
if max_sim >= 0.35 and mejor_match:
nombre_limpio = mejor_match.split('_')[0] nombre_limpio = mejor_match.split('_')[0]
# Variable para rastrear quién es el ID definitivo al final del proceso
id_definitivo = gid id_definitivo = gid
with global_mem.lock: with global_mem.lock:
datos_id = global_mem.db.get(gid) datos_id = global_mem.db.get(gid)
if not datos_id: return if not datos_id: return
if datos_id.get('candidato_nombre') == nombre_limpio: 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 datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1
else: else:
datos_id['candidato_nombre'] = nombre_limpio datos_id['candidato_nombre'] = nombre_limpio
datos_id['votos_nombre'] = 1 datos_id['votos_nombre'] = 1
# Sistema de Puntos
if max_sim >= 0.50: if max_sim >= 0.50:
datos_id['votos_nombre'] += 3 # Pase VIP por buena foto datos_id['votos_nombre'] += 3
elif max_sim >= 0.40: elif max_sim >= 0.40:
datos_id['votos_nombre'] += 2 # Voto extra por foto decente datos_id['votos_nombre'] += 2
votos_finales = datos_id['votos_nombre'] votos_finales = datos_id['votos_nombre']
# ⚡ Exigimos 3 votos (según tu condicional) # ⚡ Exigimos 3 votos
if votos_finales >= 3: if votos_finales >= 3:
nombre_actual = datos_id.get('nombre') # 1. Buscamos al veterano
# 1. Buscamos si ese nombre ya lo tiene un veterano en la memoria
id_veterano = None id_veterano = None
for gid_mem, data_mem in global_mem.db.items(): for gid_mem, data_mem in global_mem.db.items():
if data_mem.get('nombre') == nombre_limpio and gid_mem != gid: if data_mem.get('nombre') == nombre_limpio and gid_mem != gid:
id_veterano = gid_mem id_veterano = gid_mem
break break
# 2. FUSIÓN MÁGICA: Si ya existe, lo fusionamos # 2. FUSIÓN MÁGICA
if id_veterano is not None: 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}).") print(f" [FUSIÓN FACIAL] ID {gid} es clon. Fusionando con veterano ID {id_veterano} ({nombre_limpio}).")
# Pasamos la memoria de ropa al veterano
global_mem.db[id_veterano]['firmas'].extend(datos_id.get('firmas', [])) global_mem.db[id_veterano]['firmas'].extend(datos_id.get('firmas', []))
if len(global_mem.db[id_veterano]['firmas']) > 15: if len(global_mem.db[id_veterano]['firmas']) > 15:
global_mem.db[id_veterano]['firmas'] = 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['firmas'] = []
datos_id['fusionado_con'] = id_veterano datos_id['fusionado_con'] = id_veterano
# Actualizamos el tracker y el ID definitivo
trk.gid = id_veterano trk.gid = id_veterano
id_definitivo = id_veterano id_definitivo = id_veterano
# 3. BAUTIZO NORMAL: Si no hay clones, procedemos con tu lógica de protección VIP # 3. BAUTIZO NORMAL O CORRECCIÓN
else: else:
if nombre_actual is not None and nombre_actual != nombre_limpio: if dueno_actual is not None and dueno_actual != nombre_limpio:
# Si ya tiene nombre y quieren robárselo, exigimos 0.56 mínimo
if max_sim < 0.56: if max_sim < 0.56:
print(f"[RECHAZO VIP] {nombre_actual} protegido de {nombre_limpio} ({max_sim:.2f})") print(f"[RECHAZO VIP] {dueno_actual} protegido de {nombre_limpio} ({max_sim:.2f})")
return return
else: else:
print(f" [CORRECCIÓN VIP] Renombrando a {nombre_limpio} ({max_sim:.2f})") print(f" [CORRECCIÓN VIP] Renombrando a {nombre_limpio} ({max_sim:.2f})")
if nombre_actual != nombre_limpio: if dueno_actual != nombre_limpio:
datos_id['nombre'] = nombre_limpio datos_id['nombre'] = nombre_limpio
print(f"[BAUTIZO] ID {gid} confirmado como {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() global_mem.db[id_definitivo]['ts'] = time.time()
# 🔊 AUDIO DE BIENVENIDA (Funciona tanto para bautizos como para fusiones) # ──────────────────────────────────────────────────────────────────
if str(cam_id) == "7": # 🔊 AUDIO DE BIENVENIDA (DISPARADOR CORREGIDO)
if not hasattr(global_mem, 'ultimos_saludos'): # ──────────────────────────────────────────────────────────────────
global_mem.ultimos_saludos = {} # Verificamos si es la cámara 7 y si la persona actual ya tiene nombre confirmado
ultimo = global_mem.ultimos_saludos.get(nombre_limpio, 0) if str(cam_id) == "7" and nombre_limpio:
ahora = time.time()
TIEMPO_COOLDOWN = 60 # 1 minuto para pruebas
# Aquí asumo que tienes COOLDOWN_TIME definido globalmente en tu archivo # Revisamos el historial de saludos en la memoria global (que ya carga el JSON)
if (time.time() - ultimo) > COOLDOWN_TIME: info_saludo = global_mem.ultimos_saludos.get(nombre_limpio, {})
global_mem.ultimos_saludos[nombre_limpio] = time.time() ultimo_ts = info_saludo.get('timestamp', 0) if isinstance(info_saludo, dict) else 0
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_detectado), daemon=True).start()
# Blindaje OSNet fuera del lock (Usando el id_definitivo por si hubo fusión) 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:
dic_gen = json.load(f)
if nombre_limpio in dic_gen:
genero_oficial = dic_gen[nombre_limpio]
except Exception: pass
print(f"📢 [AUDIO] {nombre_limpio} reconocido en Cam 7. Saludando...")
import threading
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 >= 3:
global_mem.confirmar_firma_vip(id_definitivo, time.time()) global_mem.confirmar_firma_vip(id_definitivo, time.time())
except Exception as e: except Exception as e:
print(f"Error en InsightFace asíncrono: {e}") print(f" Error en InsightFace asíncrono: {e}")
finally: finally:
# ⚡ EL LIBERADOR (Vital para que el tracker no se quede bloqueado)
trk.procesando_rostro = False trk.procesando_rostro = False
def worker_rostros(global_mem): def worker_rostros(global_mem):

1733
mejor.txt Normal file

File diff suppressed because it is too large Load Diff

BIN
osnet_dinamico.onnx Normal file

Binary file not shown.

28
osnet_dinamico.py Normal file
View File

@ -0,0 +1,28 @@
import onnx
# 1. Rutas de archivos
modelo_estatico = "osnet_x0_25_msmt17.onnx"
modelo_dinamico = "osnet_dinamico.onnx"
print(f"Abriendo {modelo_estatico}...")
model = onnx.load(modelo_estatico)
# 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'
# 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'
# 4. Guardar el nuevo modelo
onnx.save(model, modelo_dinamico)
print(f"¡Éxito! El nuevo modelo se ha guardado como: {modelo_dinamico}")

View File

@ -36,6 +36,8 @@ COOLDOWN_TIME = 15 # Segundos entre saludos
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.200" USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.200"
RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702"
from audio_iot import enviar_archivos_bocina
for path in [DB_PATH, CACHE_PATH]: for path in [DB_PATH, CACHE_PATH]:
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
@ -81,24 +83,30 @@ def reproducir(archivo):
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
def hilo_bienvenida(nombre, genero): def hilo_bienvenida(nombre, genero):
archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3")
# 1. Sintetiza el nombre si no existe en la caché
if not os.path.exists(archivo_nombre): if not os.path.exists(archivo_nombre):
try: try:
asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) asyncio.run(sintetizar_nombre(nombre, archivo_nombre))
except Exception: except Exception as e:
print(f"Error sintetizando voz: {e}")
pass pass
# 2. Busca los audios institucionales según el género y la hora
intro, cierre = obtener_audios_humanos(genero) intro, cierre = obtener_audios_humanos(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)] 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!)
if archivos: if archivos:
subprocess.Popen( try:
["mpv", "--no-video", "--volume=100"] + archivos, enviar_archivos_bocina(archivos)
stdout=subprocess.DEVNULL, except Exception as e:
stderr=subprocess.DEVNULL print(f"Error en streaming a ESP32: {e}")
)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# GESTIÓN DE BASE DE DATOS (AHORA CON INSIGHTFACE) # GESTIÓN DE BASE DE DATOS (AHORA CON INSIGHTFACE)

View File

@ -7,6 +7,11 @@ from scipy.spatial.distance import cosine
from ultralytics import YOLO from ultralytics import YOLO
import onnxruntime as ort import onnxruntime as ort
import os import os
from datetime import datetime
import json
import csv
import math
import queue
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# CONFIGURACIÓN DEL SISTEMA # CONFIGURACIÓN DEL SISTEMA
@ -16,7 +21,7 @@ SECUENCIA = [1, 7, 5, 8, 3, 6]
# RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg) # RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg)
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000" 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] URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA]
ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx" ONNX_MODEL_PATH = "osnet_dinamico.onnx"
VECINOS = { VECINOS = {
"1": ["7"], "7": ["1", "5"], "5": ["7", "8"], "1": ["7"], "7": ["1", "5"], "5": ["7", "8"],
@ -62,9 +67,11 @@ def analizar_calidad(box):
if w <= 0 or h <= 0: return False if w <= 0 or h <= 0: return False
return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD) return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD)
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): def preprocess_onnx(roi):
# ⚡ ESCUDO ANTI-SOMBRAS (CLAHE) # ⚡ 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) lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab) l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
@ -78,7 +85,7 @@ def preprocess_onnx(roi):
img = img.transpose(2,0,1).astype(np.float32) / 255.0 img = img.transpose(2,0,1).astype(np.float32) / 255.0
img = (img - MEAN) / STD img = (img - MEAN) / STD
# Devolvemos el tensor correcto de 1 sola imagen # ⚡ Ahora sí devolverá exactamente (1, 3, 256, 128)
return np.expand_dims(img, axis=0) return np.expand_dims(img, axis=0)
def extraer_color_zonas(img): def extraer_color_zonas(img):
@ -123,16 +130,11 @@ def extraer_firma_hibrida(frame_hd, box_480):
calidad_area = (x2_c - x1_c) * (y2_c - y1_c) calidad_area = (x2_c - x1_c) * (y2_c - y1_c)
# ⚡ VOLVEMOS AL BATCH DE 16 PARA EVITAR EL CRASH FATAL # ⚡ OPTIMIZACIÓN OSNET DINÁMICO
blob = preprocess_onnx(roi) # Esto devuelve (1, 3, 256, 128) # Pasamos el tensor directo (1, 3, 256, 128) sin rellenar con ceros
blob = preprocess_onnx(roi)
# Creamos el contenedor de 16 espacios que el modelo ONNX exige outputs = ort_session.run(None, {input_name: blob})
blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32) deep_feat = outputs[0][0].flatten()
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) norma = np.linalg.norm(deep_feat)
if norma > 0: deep_feat = deep_feat / norma if norma > 0: deep_feat = deep_feat / norma
@ -259,6 +261,66 @@ class GlobalMemory:
self.db = {} self.db = {}
self.next_gid = 100 self.next_gid = 100
self.lock = threading.RLock() 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): def _es_transito_posible(self, data, cam_id, now):
cam_origen = str(data['last_cam']) cam_origen = str(data['last_cam'])
@ -310,7 +372,19 @@ class GlobalMemory:
sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam) sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam)
# UMBRALES DINÁMICOS INTELIGENTES 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
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 misma_cam:
if dt < 2.0: umbral = 0.60 # Para parpadeos rápidos de YOLO 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 else: umbral = 0.63 # Si desapareció un rato en la misma cámara
@ -318,12 +392,12 @@ class GlobalMemory:
# ⚡ EL PUNTO DULCE: 0.62. # ⚡ EL PUNTO DULCE: 0.62.
# Está por encima del ruido máximo (0.57) y por debajo de las # 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). # caídas de Omar en el cluster 7-5-8 (0.64).
umbral = 0.61 umbral = 0.62
else: else:
# ⚡ LEJANOS: 0.66. # ⚡ LEJANOS: 0.66.
# Si salta de la Cam 1 a la Cam 8, exigimos seguridad alta # 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. # para no robarle el ID a alguien que acaba de entrar por la otra puerta.
umbral = 0.66 umbral = 0.70"""
""" """
@ -342,7 +416,7 @@ class GlobalMemory:
if data.get('nombre') is not None: if data.get('nombre') is not None:
umbral += 0.05 if misma_cam else 0.02 umbral += 0.05 if misma_cam else 0.02
# 👁️ EL CHIVATO DE OSNET (Debug crucial) # EL CHIVATO DE OSNET (Debug crucial)
# Esto imprimirá en tu consola qué similitud real de ropa detectó al cruzar de cámara # Esto imprimirá en tu consola qué similitud real de ropa detectó al cruzar de cámara
if sim > 0.35 and not misma_cam: 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})") print(f" [OSNet] Cam {cam_id} evaluando ID {gid} (Viene de Cam {data['last_cam']}) -> Similitud Ropa: {sim:.2f} (Umbral exigido: {umbral:.2f})")
@ -442,7 +516,7 @@ class GlobalMemory:
# 2. Vaciamos al perdedor y le ponemos un "Redireccionamiento" # 2. Vaciamos al perdedor y le ponemos un "Redireccionamiento"
self.db[id_elimina]['firmas'] = [] self.db[id_elimina]['firmas'] = []
self.db[id_elimina]['fusionado_con'] = id_mantiene 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}.") print(f"[FUSIÓN MÁGICA] Las firmas del ID {id_elimina} fueron absorbidas por el ID {id_mantiene}.")
return True return True
return False return False
@ -467,8 +541,8 @@ class CamManager:
estado_actual = getattr(trk, 'en_grupo', False) estado_actual = getattr(trk, 'en_grupo', False)
# Si están a menos de medio cuerpo de distancia lateral, es un grupo. # Si están a menos de medio cuerpo de distancia lateral, es un grupo.
factor_x = 1.0 if estado_actual else 1.0 factor_x = 0.9 if estado_actual else 0.7
factor_y = 0.4 if estado_actual else 0.2 factor_y = 0.35 if estado_actual else 0.2
for other in todos_los_tracks: for other in todos_los_tracks:
if other is trk: continue if other is trk: continue
@ -524,32 +598,34 @@ class CamManager:
def update(self, boxes, frame_show, frame_hd, now, turno_activo): def update(self, boxes, frame_show, frame_hd, now, turno_activo):
# 1. MUTACIÓN FUSIÓN
for trk in self.trackers: for trk in self.trackers:
if trk.gid is not None: if trk.gid is not None:
with self.global_mem.lock: with self.global_mem.lock:
if trk.gid in self.global_mem.db and 'fusionado_con' in self.global_mem.db[trk.gid]: 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']}") 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'] trk.gid = self.global_mem.db[trk.gid]['fusionado_con']
# ──────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────
# ⚡ FILTRO ANTI-FANTASMAS # ⚡ FILTRO ANTI-FANTASMAS
vivos_predict = [] vivos_predict = []
for trk in self.trackers: for trk in self.trackers:
caja_predicha = trk.predict(turno_activo=turno_activo) 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: if caja_predicha is not None:
vivos_predict.append(trk) vivos_predict.append(trk)
self.trackers = vivos_predict # Actualizamos la lista solo con los vivos self.trackers = vivos_predict
if not turno_activo: return self.trackers if not turno_activo: return self.trackers
# ────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────
# Aquí sigue tu código normal:
matched, unmatched_dets, unmatched_trks = self._asignar(boxes, 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} 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: for t_idx, d_idx in matched:
trk = self.trackers[t_idx] trk = self.trackers[t_idx]
box = boxes[d_idx] box = boxes[d_idx]
@ -563,74 +639,111 @@ class CamManager:
if firmas: if firmas:
trk.firma_pre_grupo = firmas[-1] trk.firma_pre_grupo = firmas[-1]
trk.validado_post_grupo = False trk.validado_post_grupo = False
print(f" [GRUPO] ID {trk.gid} entró a grupo. Protegiendo firma.") print(f" ID {trk.gid} entró a grupo. Protegiendo firma.")
trk.ts_salio_grupo = 0.0 trk.ts_salio_grupo = 0.0
elif getattr(trk, 'en_grupo', False) and not es_grupo_ahora: elif getattr(trk, 'en_grupo', False) and not es_grupo_ahora:
trk.ts_salio_grupo = now trk.ts_salio_grupo = now
print(f" [GRUPO] ID {trk.gid} salió de grupo. Cuarentena de 3s.") print(f" ID {trk.gid} salió de grupo. Cuarentena de 3s.")
trk.en_grupo = es_grupo_ahora trk.en_grupo = es_grupo_ahora
trk.update(box, es_grupo_ahora, now) trk.update(box, es_grupo_ahora, now)
# ──────────────────────────────────────────────────────────
# 📝 REGISTRO DE MOVIMIENTO (Anti Ping-Pong Dinámico)
# ──────────────────────────────────────────────────────────
if trk.gid is not None:
registro_pendiente = None
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]) area_actual = (box[2] - box[0]) * (box[3] - box[1])
tiempo_desde_separacion = now - getattr(trk, 'ts_salio_grupo', 0) 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) 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: if not trk.en_grupo and not en_cuarentena:
# A) Bautizo de IDs Nuevos (Con Aduana Temporal) # A) Bautizo de IDs Nuevos (Con Aduana Temporal)
if trk.gid is None and trk.listo_para_id: if trk.gid is None and trk.listo_para_id:
firma = extraer_firma_hibrida(frame_hd, box) firma = extraer_firma_hibrida(frame_hd, box)
if firma is not None: if firma is not None:
fh, fw = frame_hd.shape[:2] fh, fw = frame_hd.shape[:2] # ⚡ FIX: Agregado [:2]
bx1, by1, bx2, by2 = map(int, box) bx1, by1, bx2, by2 = map(int, box)
nace_en_borde = (bx1 < 25 or by1 < 25 or bx2 > fw - 25 or by2 > fh - 25) 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) 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: 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) active_gids.add(candidato_gid)
# Si la memoria dice "Es un desconocido nuevo", lo bautizamos al instante
if not es_reid: if not es_reid:
trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, False, area_actual trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, False, area_actual
# ⚡ ADUANA TEMPORAL
else: else:
if getattr(trk, 'candidato_temporal', None) == candidato_gid: if getattr(trk, 'candidato_temporal', None) == candidato_gid:
trk.votos_reid = getattr(trk, 'votos_reid', 0) + 1 trk.votos_reid = getattr(trk, 'votos_reid', 0) + 1
else: else:
# Si cambia de opinión, reiniciamos sin restar
trk.candidato_temporal = candidato_gid trk.candidato_temporal = candidato_gid
trk.votos_reid = 1 trk.votos_reid = 1
if trk.votos_reid >= 2: if trk.votos_reid >= 2:
trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, True, area_actual trk.gid, trk.origen_global = candidato_gid, True
print(f" [ADUANA] ID {candidato_gid} validado.")
# B) Aprendizaje Continuo (Captura de Ángulos) # B) Aprendizaje Continuo OSNet
elif trk.gid is not None: elif trk.gid is not None:
tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) 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: if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box) and extracciones_hoy < 1:
fh, fw = frame_hd.shape[:2] fh, fw = frame_hd.shape[:2] # ⚡ FIX: Agregado [:2]
x1, y1, x2, y2 = map(int, box) x1, y1, x2, y2 = map(int, box)
en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15)
if not en_borde: if not en_borde:
firma_nueva = extraer_firma_hibrida(frame_hd, box) firma_nueva = extraer_firma_hibrida(frame_hd, box)
if firma_nueva is not None: if firma_nueva is not None:
extracciones_hoy += 1 # ⚡ Cerramos la compuerta para los demás en este frame extracciones_hoy += 1
with self.global_mem.lock: with self.global_mem.lock:
if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: 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_reciente = self.global_mem.db[trk.gid]['firmas'][-1]
firma_original = self.global_mem.db[trk.gid]['firmas'][0] firma_original = self.global_mem.db[trk.gid]['firmas'][0]
@ -638,15 +751,13 @@ class CamManager:
sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente) sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente)
sim_raiz = similitud_hibrida(firma_nueva, firma_original) sim_raiz = similitud_hibrida(firma_nueva, firma_original)
# ⚡ EL BOTÓN DE PÁNICO (Anti ID-Switch) # ⚡ 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: if sim_raiz < 0.35:
print(f"[ID SWITCH] Ropa de ID {trk.gid} cambió drásticamente. Revocando ID.") print(f" Ropa de ID {trk.gid} cambió drásticamente. Revocando ID.")
trk.gid = None trk.gid = None
trk.listo_para_id = False trk.listo_para_id = False
trk.frames_buena_calidad = 0 trk.frames_buena_calidad = 0
continue # Rompemos el ciclo para que nazca como alguien nuevo continue
ya_bautizado = self.global_mem.db[trk.gid].get('nombre') is not None ya_bautizado = self.global_mem.db[trk.gid].get('nombre') is not None
umbral_raiz = 0.52 if ya_bautizado else 0.62 umbral_raiz = 0.52 if ya_bautizado else 0.62
@ -654,7 +765,7 @@ class CamManager:
if sim_coherencia > 0.60 and sim_raiz > umbral_raiz: if sim_coherencia > 0.60 and sim_raiz > umbral_raiz:
es_coherente = True es_coherente = True
for otro_gid, otro_data in self.global_mem.db.items(): for otro_gid, otro_data in self.global_mem.db.items():
if otro_gid == trk.gid or not otro_data['firmas']: continue if otro_gid == trk.gid or not otro_data.get('firmas'): continue
sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0]) sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0])
if sim_intruso > sim_raiz: if sim_intruso > sim_raiz:
es_coherente = False es_coherente = False
@ -666,26 +777,25 @@ class CamManager:
for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now)) for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now))
# ──────────────────────────────────────────────────────────
# ⚡ LIMPIEZA AGRESIVA
# ──────────────────────────────────────────────────────────
vivos = [] vivos = []
fh, fw = frame_show.shape[:2] fh, fw = frame_show.shape[:2] # ⚡ FIX: Agregado [:2]
for t in self.trackers: for t in self.trackers:
x1, y1, x2, y2 = t.box x1, y1, x2, y2 = t.box
toca_borde = (x1 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20) toca_borde = (x1 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20)
tiempo_oculto = now - t.ts_ultima_deteccion tiempo_oculto = now - t.ts_ultima_deteccion
# ⚡ LIMPIEZA AGRESIVA PARA PROTEGER LA CPU DE MEMORY LEAKS
if t.gid is None: 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 limite_vida = 1.0 if toca_borde else 2.5
else: else:
# Si es VIP, le damos paciencia para que OSNet no se fragmente
limite_vida = 3.0 if toca_borde else 8.0 limite_vida = 3.0 if toca_borde else 8.0
if tiempo_oculto < limite_vida: if tiempo_oculto < limite_vida:
vivos.append(t) vivos.append(t)
self.trackers = vivos self.trackers = vivos
self._gestionar_aprendizaje_post_grupo(now, frame_hd) self._gestionar_aprendizaje_post_grupo(now, frame_hd)
return self.trackers return self.trackers
@ -701,14 +811,10 @@ class CamManager:
for t, trk in enumerate(self.trackers): for t, trk in enumerate(self.trackers):
tiempo_oculto = now - trk.ts_ultima_deteccion tiempo_oculto = now - trk.ts_ultima_deteccion
# ⚡ 2A. Aumentamos el radio dinámico mínimo para no perder gente rápida # ⚡ INCERTIDUMBRE CUADRÁTICA DE KALMAN
radio_dinamico = min(350.0, max(150.0, 300.0 * tiempo_oculto)) radio_dinamico = min(400.0, max(150.0, 300.0 * math.sqrt(max(0.1, tiempo_oculto))))
# La incertidumbre crece con el tiempo, pero la topamos en 0.5
incertidumbre = min(0.5, tiempo_oculto * 0.2) incertidumbre = min(0.5, tiempo_oculto * 0.2)
es_fantasma = getattr(trk, 'time_since_update', 0) > 1
for d, det in enumerate(boxes): for d, det in enumerate(boxes):
iou = iou_overlap(trk.box, det) 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_t, cy_t = (trk.box[0]+trk.box[2])/2, (trk.box[1]+trk.box[3])/2
@ -723,32 +829,18 @@ class CamManager:
cost_mat[t, d] = 100.0 cost_mat[t, d] = 100.0
continue 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 dist_norm = dist_pixel / radio_dinamico
ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) 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, if area_det < area_trk: castigo_tam = (ratio_area - 1.0) * 0.5
# penalizamos fuerte (evita que la caja encoja mágicamente y pierda a la persona). else: castigo_tam = (ratio_area - 1.0) * 0.15
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 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) row_ind, col_ind = linear_sum_assignment(cost_mat)
matched, unmatched_dets, unmatched_trks = [], [], [] matched, unmatched_dets, unmatched_trks = [], [], []
for r, c in zip(row_ind, col_ind): 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: if cost_mat[r, c] > 3.5:
unmatched_trks.append(r); unmatched_dets.append(c) unmatched_trks.append(r); unmatched_dets.append(c)
else: else:
@ -766,17 +858,41 @@ class CamManager:
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
class CamStream: class CamStream:
def __init__(self, url): def __init__(self, url):
self.url, self.cap = url, cv2.VideoCapture(url) self.url = url
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1); self.frame = None 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() threading.Thread(target=self._run, daemon=True).start()
def _run(self): def _run(self):
while True: while not self.stopped:
ret, f = self.cap.read() ret, f = self.cap.read()
if ret: if not ret:
self.frame = f; time.sleep(0.01) time.sleep(2)
else: self.cap.open(self.url)
time.sleep(2); self.cap.open(self.url) continue
# Si la cola está llena, descartamos el frame viejo
if self.q.full():
try:
self.q.get_nowait()
except queue.Empty:
pass
self.q.put(f)
@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
def stop(self):
self.stopped = True
self.cap.release()
def dibujar_track(frame_show, trk): def dibujar_track(frame_show, trk):
try: x1, y1, x2, y2 = map(int, trk.box) try: x1, y1, x2, y2 = map(int, trk.box)
@ -828,3 +944,5 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()