import os os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ['CUDA_VISIBLE_DEVICES'] = '-1' import cv2 import numpy as np from deepface import DeepFace import pickle import time import threading import asyncio import edge_tts import subprocess from datetime import datetime import warnings import urllib.request import torch if torch.cuda.is_available(): device = "cuda" print("GPU detectada β†’ usando GPU πŸš€") else: device = "cpu" print("GPU no disponible β†’ usando CPU ⚠️") warnings.filterwarnings("ignore") # ────────────────────────────────────────────────────────────────────────────── # CONFIGURACIΓ“N # ────────────────────────────────────────────────────────────────────────────── DB_PATH = "db_institucion" CACHE_PATH = "cache_nombres" VECTORS_FILE = "base_datos_rostros.pkl" TIMESTAMPS_FILE = "representaciones_timestamps.pkl" UMBRAL_SIM = 0.42 # Por encima β†’ identificado. Por debajo β†’ desconocido. COOLDOWN_TIME = 15 # Segundos entre saludos USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244" RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" for path in [DB_PATH, CACHE_PATH]: os.makedirs(path, exist_ok=True) # ────────────────────────────────────────────────────────────────────────────── # YUNET β€” Detector facial rΓ‘pido en CPU # ────────────────────────────────────────────────────────────────────────────── YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx" if not os.path.exists(YUNET_MODEL_PATH): print(f"Descargando YuNet ({YUNET_MODEL_PATH})...") url = ("https://github.com/opencv/opencv_zoo/raw/main/models/" "face_detection_yunet/face_detection_yunet_2023mar.onnx") urllib.request.urlretrieve(url, YUNET_MODEL_PATH) print("YuNet descargado.") # Detector estricto para ROIs grandes (persona cerca) detector_yunet = cv2.FaceDetectorYN.create( model=YUNET_MODEL_PATH, config="", input_size=(320, 320), score_threshold=0.70, nms_threshold=0.3, top_k=5000 ) # Detector permisivo para ROIs pequeΓ±os (persona lejos) detector_yunet_lejano = cv2.FaceDetectorYN.create( model=YUNET_MODEL_PATH, config="", input_size=(320, 320), score_threshold=0.45, nms_threshold=0.3, top_k=5000 ) def detectar_rostros_yunet(roi, lock=None): """ Elige automΓ‘ticamente el detector segΓΊn el tamaΓ±o del ROI. """ h_roi, w_roi = roi.shape[:2] area = w_roi * h_roi det = detector_yunet if area > 8000 else detector_yunet_lejano try: if lock: with lock: det.setInputSize((w_roi, h_roi)) _, faces = det.detect(roi) else: det.setInputSize((w_roi, h_roi)) _, faces = det.detect(roi) except Exception: return [] if faces is None: return [] resultado = [] for face in faces: try: fx, fy, fw, fh = map(int, face[:4]) score = float(face[14]) if len(face) > 14 else 1.0 resultado.append((fx, fy, fw, fh, score)) except (ValueError, OverflowError, TypeError): continue return resultado # ────────────────────────────────────────────────────────────────────────────── # SISTEMA DE AUDIO # ────────────────────────────────────────────────────────────────────────────── def obtener_audios_humanos(genero): hora = datetime.now().hour es_mujer = genero.lower() == 'woman' suffix = "_m.mp3" if es_mujer else "_h.mp3" if 5 <= hora < 12: intro = "dias.mp3" elif 12 <= hora < 19: intro = "tarde.mp3" else: intro = "noches.mp3" cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix return intro, cierre async def sintetizar_nombre(nombre, ruta): nombre_limpio = nombre.replace('_', ' ') try: comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") await comunicador.save(ruta) except Exception: pass def reproducir(archivo): if os.path.exists(archivo): subprocess.Popen( ["mpv", "--no-video", "--volume=100", archivo], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) def hilo_bienvenida(nombre, genero): archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") if not os.path.exists(archivo_nombre): try: asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) except Exception: pass intro, cierre = obtener_audios_humanos(genero) archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] if archivos: subprocess.Popen( ["mpv", "--no-video", "--volume=100"] + archivos, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) # ────────────────────────────────────────────────────────────────────────────── # GESTIΓ“N DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIΓ“N) # ────────────────────────────────────────────────────────────────────────────── def gestionar_vectores(actualizar=False): import json # ⚑ AsegΓΊrate de tener importado json vectores_actuales = {} if os.path.exists(VECTORS_FILE): try: with open(VECTORS_FILE, 'rb') as f: vectores_actuales = pickle.load(f) except Exception: vectores_actuales = {} if not actualizar: return vectores_actuales timestamps = {} if os.path.exists(TIMESTAMPS_FILE): try: with open(TIMESTAMPS_FILE, 'rb') as f: timestamps = pickle.load(f) except Exception: timestamps = {} # ────────────────────────────────────────────────────────── # CARGA DEL CACHΓ‰ DE GΓ‰NEROS # ────────────────────────────────────────────────────────── ruta_generos = os.path.join(CACHE_PATH, "generos.json") dic_generos = {} if os.path.exists(ruta_generos): try: with open(ruta_generos, 'r') as f: dic_generos = json.load(f) except Exception: pass print("\nACTUALIZANDO BASE DE DATOS (AlineaciΓ³n y CachΓ© de GΓ©neros)...") imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))] nombres_en_disco = set() hubo_cambios = False cambio_generos = False # Bandera para saber si actualizamos el JSON for archivo in imagenes: nombre_archivo = os.path.splitext(archivo)[0] ruta_img = os.path.join(DB_PATH, archivo) nombres_en_disco.add(nombre_archivo) ts_actual = os.path.getmtime(ruta_img) ts_guardado = timestamps.get(nombre_archivo, 0) # Si ya tenemos el vector pero NO tenemos su gΓ©nero en el JSON, forzamos el procesamiento falta_genero = nombre_archivo not in dic_generos if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero: continue try: img_db = cv2.imread(ruta_img) lab = cv2.cvtColor(img_db, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) l = clahe.apply(l) img_mejorada = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) # IA DE GΓ‰NERO (Solo se ejecuta 1 vez por persona en toda la vida del sistema) if falta_genero: try: analisis = DeepFace.analyze(img_mejorada, actions=['gender'], enforce_detection=False)[0] dic_generos[nombre_archivo] = analisis.get('dominant_gender', 'Man') except Exception: dic_generos[nombre_archivo] = "Man" # Respaldo cambio_generos = True # Extraemos el vector res = DeepFace.represent( img_path=img_mejorada, model_name="ArcFace", detector_backend="opencv", align=False, enforce_detection=True ) emb = np.array(res[0]["embedding"], dtype=np.float32) norma = np.linalg.norm(emb) if norma > 0: emb = emb / norma vectores_actuales[nombre_archivo] = emb timestamps[nombre_archivo] = ts_actual hubo_cambios = True print(f" Procesado y alineado: {nombre_archivo} | GΓ©nero: {dic_generos.get(nombre_archivo)}") except Exception as e: print(f" Rostro no vΓ‘lido en '{archivo}', omitido. Error: {e}") # Limpieza de eliminados for nombre in list(vectores_actuales.keys()): if nombre not in nombres_en_disco: del vectores_actuales[nombre] timestamps.pop(nombre, None) if nombre in dic_generos: del dic_generos[nombre] cambio_generos = True hubo_cambios = True print(f" Eliminado (sin foto): {nombre}") # Guardado de la memoria if hubo_cambios: with open(VECTORS_FILE, 'wb') as f: pickle.dump(vectores_actuales, f) with open(TIMESTAMPS_FILE, 'wb') as f: pickle.dump(timestamps, f) # Guardado del JSON de gΓ©neros si hubo descubrimientos nuevos if cambio_generos: with open(ruta_generos, 'w') as f: json.dump(dic_generos, f) if hubo_cambios or cambio_generos: print(" SincronizaciΓ³n terminada.\n") else: print(" Sin cambios. Base de datos al dΓ­a.\n") return vectores_actuales # ────────────────────────────────────────────────────────────────────────────── # BÚSQUEDA BLINDADA (Similitud Coseno estricta) # ────────────────────────────────────────────────────────────────────────────── def buscar_mejor_match(emb_consulta, base_datos): # ⚑ MAGIA 3: NormalizaciΓ³n L2 del vector entrante norma = np.linalg.norm(emb_consulta) if norma > 0: emb_consulta = emb_consulta / norma mejor_match, max_sim = None, -1.0 for nombre, vec in base_datos.items(): # Como ambos estΓ‘n normalizados, esto es Similitud Coseno pura (-1.0 a 1.0) sim = float(np.dot(emb_consulta, vec)) if sim > max_sim: max_sim = sim mejor_match = nombre return mejor_match, max_sim # ────────────────────────────────────────────────────────────────────────────── # LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA) # ────────────────────────────────────────────────────────────────────────────── def sistema_interactivo(): base_datos = gestionar_vectores(actualizar=False) cap = cv2.VideoCapture(RTSP_URL) ultimo_saludo = 0 persona_actual = None confirmaciones = 0 print("\n" + "=" * 50) print(" MΓ“DULO DE REGISTRO Y DEPURACIΓ“N ESTRICTO") print(" [R] Registrar nuevo rostro | [Q] Salir") print("=" * 50 + "\n") faces_ultimo_frame = [] while True: ret, frame = cap.read() if not ret: time.sleep(2) cap.open(RTSP_URL) continue h, w = frame.shape[:2] display_frame = frame.copy() tiempo_actual = time.time() faces_raw = detectar_rostros_yunet(frame) faces_ultimo_frame = faces_raw for (fx, fy, fw, fh, score_yunet) in faces_raw: fx = max(0, fx); fy = max(0, fy) fw = min(w - fx, fw); fh = min(h - fy, fh) if fw <= 0 or fh <= 0: continue cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) cv2.putText(display_frame, f"YN:{score_yunet:.2f}", (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: continue m = int(fw * 0.15) roi = frame[max(0, fy-m): min(h, fy+fh+m), max(0, fx-m): min(w, fx+fw+m)] # πŸ›‘οΈ FILTRO DE TAMAΓ‘O FÍSICO if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40: cv2.putText(display_frame, "muy pequeno", (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) continue # πŸ›‘οΈ FILTRO DE NITIDEZ gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() if nitidez < 50.0: cv2.putText(display_frame, f"blur({nitidez:.0f})", (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) continue # πŸŒ™ SIMETRÍA 1: VISIΓ“N NOCTURNA (CLAHE) AL VIDEO EN VIVO try: lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) l = clahe.apply(l) roi_mejorado = cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) except Exception: roi_mejorado = roi # Respaldo de seguridad # 🧠 SIMETRÍA 2: MOTOR MTCNN Y ALINEACIΓ“N (Igual que la Base de Datos) try: res = DeepFace.represent( img_path=roi_mejorado, model_name="ArcFace", detector_backend="mtcnn", # El mismo que en gestionar_vectores align=True, # Enderezamos la cara enforce_detection=True # Si MTCNN no ve cara clara, aborta ) emb = np.array(res[0]["embedding"], dtype=np.float32) mejor_match, max_sim = buscar_mejor_match(emb, base_datos) except Exception: # MTCNN abortΓ³ porque la cara estaba de perfil, tapada o no era una cara cv2.putText(display_frame, "MTCNN Ignorado", (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) continue estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" n_bloques = int(max_sim * 20) barra = "β–ˆ" * n_bloques + "β–‘" * (20 - n_bloques) print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") if max_sim > UMBRAL_SIM and mejor_match: color = (0, 255, 0) texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" if mejor_match == persona_actual: confirmaciones += 1 else: persona_actual, confirmaciones = mejor_match, 1 if confirmaciones >= 1: cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) try: analisis = DeepFace.analyze( roi_mejorado, actions=['gender'], enforce_detection=False )[0] genero = analisis['dominant_gender'] except Exception: genero = "Man" threading.Thread( target=hilo_bienvenida, args=(mejor_match, genero), daemon=True ).start() ultimo_saludo = tiempo_actual confirmaciones = 0 else: color = (0, 0, 255) texto = f"? ({max_sim:.2f})" confirmaciones = max(0, confirmaciones - 1) cv2.putText(display_frame, texto, (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imshow("MΓ³dulo de Registro", display_frame) key = cv2.waitKey(1) & 0xFF if key == ord('q'): break elif key == ord('r'): if faces_ultimo_frame: areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame] fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)] m_x = int(fw * 0.30) m_y = int(fh * 0.30) face_roi = frame[max(0, fy-m_y): min(h, fy+fh+m_y), max(0, fx-m_x): min(w, fx+fw+m_x)] if face_roi.size > 0: nom = input("\nNombre de la persona: ").strip() if nom: foto_path = os.path.join(DB_PATH, f"{nom}.jpg") cv2.imwrite(foto_path, face_roi) print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...") base_datos = gestionar_vectores(actualizar=True) else: print("[!] Registro cancelado.") else: print("[!] Recorte vacΓ­o. Intenta de nuevo.") else: print("\n[!] No se detectΓ³ rostro. AcΓ©rcate mΓ‘s o mira a la lente.") cap.release() cv2.destroyAllWindows() if __name__ == "__main__": sistema_interactivo()