IdentificacionIA/fision1.py
2026-06-25 11:06:15 -06:00

925 lines
43 KiB
Python

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"
# Silenciamos a 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
from queue import Queue
from ultralytics import YOLO
import warnings
import json
import math
warnings.filterwarnings("ignore")
# ──────────────────────────────────────────────────────────────────────────────
# 1. IMPORTAMOS NUESTROS MÓDULOS
# ──────────────────────────────────────────────────────────────────────────────
import seguimiento2
from seguimiento2 import GlobalMemory, CamManager, FUENTE, es_humano_valido
from reconocimiento import (
app,
gestionar_vectores,
buscar_mejor_match,
hilo_bienvenida
)
# ──────────────────────────────────────────────────────────────────────────────
# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN
# ──────────────────────────────────────────────────────────────────────────────
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
# ──────────────────────────────────────────────────────────────────────────────
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, h_rostro = face.bbox[2] - face.bbox[0], face.bbox[3] - face.bbox[1]
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}")
if max_sim >= 0.23 and mejor_match:
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
dueno_actual = datos_id.get('nombre')
candidato_actual = datos_id.get('candidato_nombre')
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
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']
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
if id_veterano is not None:
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
else:
if dueno_actual is None:
datos_id['nombre'] = 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()
if str(cam_id) == "7" and nombre_limpio:
ahora = time.time()
TIEMPO_COOLDOWN = 30
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
print(f" [AUDIO] Cam {cam_id}: {nombre_limpio} reconocido. Saludando...")
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_oficial), daemon=True).start()
if max_sim > 0.50 and votos_finales >= 2:
global_mem.confirmar_firma_vip(id_definitivo, time.time())
except Exception as e:
print(f" Error en InsightFace asíncrono: {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()
# ──────────────────────────────────────────────────────────────────────────────
# 4. LECTURA DE VIDEO CONTINUA Y PURA
# ──────────────────────────────────────────────────────────────────────────────
class CamStream:
def __init__(self, url):
self.url = url
self.cap = cv2.VideoCapture(url)
# 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:
self.frame = f
else:
time.sleep(1)
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(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 camaras_activas}
cams = [CamStream(u) for u in URLS]
threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start()
idx = 0
ultimo_guardado = time.time()
while True:
now = time.time()
tiles = []
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):
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))
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:
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)]
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)
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)
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
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()
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()
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 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)
# ⚡ 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)
# ⚡ 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)
idx += 1
time.sleep(0.01)
if __name__ == "__main__":
main(modo_interfaz=False)"""