2026-04-08 16:05:57 +00:00
|
|
|
import os
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
2026-04-08 16:05:57 +00:00
|
|
|
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"
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
# Silenciamos a OpenCV y Qt
|
2026-04-08 16:05:57 +00:00
|
|
|
os.environ['QT_LOGGING_RULES'] = '*=false'
|
|
|
|
|
os.environ['OPENCV_LOG_LEVEL'] = 'ERROR'
|
|
|
|
|
import cv2
|
|
|
|
|
import numpy as np
|
|
|
|
|
import time
|
|
|
|
|
import threading
|
|
|
|
|
from queue import Queue
|
|
|
|
|
from ultralytics import YOLO
|
|
|
|
|
import warnings
|
2026-04-13 18:46:22 +00:00
|
|
|
import json
|
2026-06-25 17:06:15 +00:00
|
|
|
import math
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
warnings.filterwarnings("ignore")
|
|
|
|
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# 1. IMPORTAMOS NUESTROS MÓDULOS
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
2026-06-25 17:06:15 +00:00
|
|
|
import seguimiento2
|
|
|
|
|
from seguimiento2 import GlobalMemory, CamManager, FUENTE, es_humano_valido
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
from reconocimiento import (
|
|
|
|
|
app,
|
|
|
|
|
gestionar_vectores,
|
|
|
|
|
buscar_mejor_match,
|
2026-06-25 17:06:15 +00:00
|
|
|
hilo_bienvenida
|
2026-04-08 16:05:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
COLA_ROSTROS = Queue(maxsize=4)
|
|
|
|
|
IA_LOCK = threading.Lock()
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
print("\nIniciando carga de base de datos...")
|
|
|
|
|
BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True)
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# 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
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# 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)
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if len(faces) == 0: return
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
|
2026-06-25 17:06:15 +00:00
|
|
|
w_rostro, h_rostro = face.bbox[2] - face.bbox[0], face.bbox[3] - face.bbox[1]
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if w_rostro < 25 or h_rostro < 25 or (w_rostro / h_rostro) < 0.30:
|
2026-04-08 16:05:57 +00:00
|
|
|
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)
|
2026-06-25 17:06:15 +00:00
|
|
|
|
|
|
|
|
if max_sim < 0.23:
|
|
|
|
|
return
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
print(f"[DEBUG CAM {cam_id}] InsightFace: {mejor_match} al {max_sim:.2f}")
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if max_sim >= 0.23 and mejor_match:
|
2026-04-08 16:05:57 +00:00
|
|
|
nombre_limpio = mejor_match.split('_')[0]
|
|
|
|
|
id_definitivo = gid
|
|
|
|
|
|
|
|
|
|
with global_mem.lock:
|
|
|
|
|
datos_id = global_mem.db.get(gid)
|
|
|
|
|
if not datos_id: return
|
2026-04-13 18:46:22 +00:00
|
|
|
|
|
|
|
|
dueno_actual = datos_id.get('nombre')
|
|
|
|
|
candidato_actual = datos_id.get('candidato_nombre')
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
2026-04-13 18:46:22 +00:00
|
|
|
else:
|
2026-06-25 17:06:15 +00:00
|
|
|
print(f"[DUDA] Strike 1 en Cam {cam_id}: Se parece a {nombre_limpio}. Vigilando de cerca a {dueno_actual}.")
|
2026-04-13 18:46:22 +00:00
|
|
|
datos_id['candidato_nombre'] = nombre_limpio
|
|
|
|
|
datos_id['votos_nombre'] = 1
|
2026-06-25 17:06:15 +00:00
|
|
|
return
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
datos_id['dudas_identidad'] = 0
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if candidato_actual == nombre_limpio:
|
|
|
|
|
datos_id['votos_nombre'] += puntos
|
|
|
|
|
else:
|
|
|
|
|
datos_id['candidato_nombre'] = nombre_limpio
|
|
|
|
|
datos_id['votos_nombre'] = puntos
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
votos_finales = datos_id['votos_nombre']
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
id_veterano = None
|
2026-06-25 17:06:15 +00:00
|
|
|
sim_cuerpo = 0.0
|
|
|
|
|
from scipy.spatial.distance import cosine
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
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
|
2026-06-25 17:06:15 +00:00
|
|
|
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'])
|
2026-04-08 16:05:57 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if id_veterano is not None:
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
else:
|
2026-06-25 17:06:15 +00:00
|
|
|
if dueno_actual is None:
|
2026-04-08 16:05:57 +00:00
|
|
|
datos_id['nombre'] = nombre_limpio
|
2026-06-25 17:06:15 +00:00
|
|
|
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}%)")
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
global_mem.db[id_definitivo]['ts'] = time.time()
|
|
|
|
|
|
2026-04-13 18:46:22 +00:00
|
|
|
if str(cam_id) == "7" and nombre_limpio:
|
|
|
|
|
ahora = time.time()
|
2026-06-25 17:06:15 +00:00
|
|
|
TIEMPO_COOLDOWN = 30
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-04-13 18:46:22 +00:00
|
|
|
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:
|
|
|
|
|
global_mem.guardar_saludo(nombre_limpio)
|
|
|
|
|
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:
|
|
|
|
|
dic_gen = json.load(f)
|
|
|
|
|
if nombre_limpio in dic_gen:
|
|
|
|
|
genero_oficial = dic_gen[nombre_limpio]
|
|
|
|
|
except Exception: pass
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
print(f" [AUDIO] Cam {cam_id}: {nombre_limpio} reconocido. Saludando...")
|
2026-04-13 18:46:22 +00:00
|
|
|
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_oficial), daemon=True).start()
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if max_sim > 0.50 and votos_finales >= 2:
|
2026-04-08 16:05:57 +00:00
|
|
|
global_mem.confirmar_firma_vip(id_definitivo, time.time())
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-04-13 18:46:22 +00:00
|
|
|
print(f" Error en InsightFace asíncrono: {e}")
|
2026-04-08 16:05:57 +00:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
2026-06-25 17:06:15 +00:00
|
|
|
# 4. LECTURA DE VIDEO CONTINUA Y PURA
|
2026-04-08 16:05:57 +00:00
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
class CamStream:
|
|
|
|
|
def __init__(self, url):
|
|
|
|
|
self.url = url
|
|
|
|
|
self.cap = cv2.VideoCapture(url)
|
2026-06-25 17:06:15 +00:00
|
|
|
# Buffer ultra-pequeño para que siempre se lea el cuadro más fresco, evitando retrasos
|
|
|
|
|
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
|
2026-04-08 16:05:57 +00:00
|
|
|
self.frame = None
|
|
|
|
|
threading.Thread(target=self._run, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
def _run(self):
|
|
|
|
|
while True:
|
|
|
|
|
ret, f = self.cap.read()
|
2026-06-25 17:06:15 +00:00
|
|
|
if ret:
|
2026-04-08 16:05:57 +00:00
|
|
|
self.frame = f
|
|
|
|
|
else:
|
2026-06-25 17:06:15 +00:00
|
|
|
time.sleep(1)
|
2026-04-08 16:05:57 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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")
|
2026-04-08 16:05:57 +00:00
|
|
|
global_mem = GlobalMemory()
|
2026-06-25 17:06:15 +00:00
|
|
|
|
|
|
|
|
managers = {str(c): CamManager(c, global_mem) for c in camaras_activas}
|
2026-04-08 16:05:57 +00:00
|
|
|
cams = [CamStream(u) for u in URLS]
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start()
|
|
|
|
|
|
|
|
|
|
idx = 0
|
2026-06-25 17:06:15 +00:00
|
|
|
ultimo_guardado = time.time()
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
now = time.time()
|
|
|
|
|
tiles = []
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
for i, cam_obj in enumerate(cams):
|
2026-06-25 17:06:15 +00:00
|
|
|
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))
|
2026-04-08 16:05:57 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
frame_show = cv2.resize(frame.copy(), (480, 270))
|
2026-06-25 17:06:15 +00:00
|
|
|
|
|
|
|
|
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)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
if turno_activo:
|
2026-06-25 17:06:15 +00:00
|
|
|
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)]
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
tracks = managers[cid].update(boxes, confs, keypoints, frame_show, frame, now, turno_activo)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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
|
|
|
|
|
|
2026-04-08 16:05:57 +00:00
|
|
|
x1, y1, x2, y2 = trk.box
|
|
|
|
|
h_real, w_real = frame.shape[:2]
|
2026-06-25 17:06:15 +00:00
|
|
|
escala_x = w_real / 480.0; escala_y = h_real / 270.0
|
2026-04-08 16:05:57 +00:00
|
|
|
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()
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
if roi_recortado.size > 0 and roi_recortado.shape[0] >= 30 and roi_recortado.shape[1] >= 30:
|
2026-04-08 16:05:57 +00:00
|
|
|
gray = cv2.cvtColor(roi_recortado, cv2.COLOR_BGR2GRAY)
|
|
|
|
|
nitidez = cv2.Laplacian(gray, cv2.CV_64F).var()
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
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)
|
2026-04-08 16:05:57 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
# ⚡ 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)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
# ⚡ 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)
|
|
|
|
|
|
|
|
|
|
# ⚡ 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)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
2026-06-25 17:06:15 +00:00
|
|
|
idx += 1
|
|
|
|
|
time.sleep(0.01)
|
2026-04-08 16:05:57 +00:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-06-25 17:06:15 +00:00
|
|
|
main(modo_interfaz=False)"""
|