IdentificacionIA/fision1.py

925 lines
43 KiB
Python
Raw Normal View History

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
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
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
2026-06-25 17:06:15 +00:00
import math
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
from reconocimiento import (
app,
gestionar_vectores,
buscar_mejor_match,
2026-06-25 17:06:15 +00:00
hilo_bienvenida
)
# ──────────────────────────────────────────────────────────────────────────────
# 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
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
# ──────────────────────────────────────────────────────────────────────────────
# 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
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-06-25 17:06:15 +00:00
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)
2026-06-25 17:06:15 +00:00
if max_sim < 0.23:
return
2026-06-25 17:06:15 +00:00
print(f"[DEBUG CAM {cam_id}] InsightFace: {mejor_match} al {max_sim:.2f}")
2026-06-25 17:06:15 +00:00
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')
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
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}.")
datos_id['candidato_nombre'] = nombre_limpio
datos_id['votos_nombre'] = 1
2026-06-25 17:06:15 +00:00
return
2026-06-25 17:06:15 +00:00
datos_id['dudas_identidad'] = 0
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
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
id_veterano = None
2026-06-25 17:06:15 +00:00
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
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'])
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
else:
2026-06-25 17:06:15 +00:00
if dueno_actual is None:
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}%)")
global_mem.db[id_definitivo]['ts'] = time.time()
if str(cam_id) == "7" and nombre_limpio:
ahora = time.time()
2026-06-25 17:06:15 +00:00
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
2026-06-25 17:06:15 +00:00
print(f" [AUDIO] Cam {cam_id}: {nombre_limpio} reconocido. Saludando...")
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:
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()
# ──────────────────────────────────────────────────────────────────────────────
2026-06-25 17:06:15 +00:00
# 4. LECTURA DE VIDEO CONTINUA Y PURA
# ──────────────────────────────────────────────────────────────────────────────
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)
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:
self.frame = f
else:
2026-06-25 17:06:15 +00:00
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)
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")
global_mem = GlobalMemory()
2026-06-25 17:06:15 +00:00
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
2026-06-25 17:06:15 +00:00
ultimo_guardado = time.time()
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
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))
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)
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-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-06-25 17:06:15 +00:00
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)
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
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
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:
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)
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-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-06-25 17:06:15 +00:00
idx += 1
time.sleep(0.01)
if __name__ == "__main__":
2026-06-25 17:06:15 +00:00
main(modo_interfaz=False)"""