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)"""