Codigos seguros y mejora en el reconocimiento

This commit is contained in:
rodrigo 2026-03-30 11:11:49 -06:00
parent e10bfb7bf4
commit d2e90d9c50
49 changed files with 1958 additions and 274 deletions

12
.gitignore vendored
View File

@ -160,3 +160,15 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# ────────────────────────────────────────────────────────
# ENTORNO VIRTUAL DEL PROYECTO
# ────────────────────────────────────────────────────────
ia_env/
# ────────────────────────────────────────────────────────
# MODELOS DE IA (Límite de GitHub: 100 MB)
# ────────────────────────────────────────────────────────
*.pt
*.onnx

BIN
Rodrigo Cahuantzi_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
base_datos_rostros.pkl Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
{"Emanuel Flores": "Man", "Vikicar Aldana": "Man", "Cristian Hernandez Suarez": "Man", "Oscar Atriano Ponce_1": "Man", "Carlos Eduardo Cuamatzi": "Man", "Rosa maria": "Woman", "Ximena": "Woman", "Ana Karen Guerrero": "Woman", "Diana laura": "Woman", "Diana Laura": "Woman", "Diana Laura Tecpa": "Woman", "aridai montiel zistecatl": "Woman", "Aridai montiel": "Woman", "Vikicar": "Man", "Ian Axel": "Man", "Rafael": "Man", "Rubisela Barrientos": "Woman", "ian axel": "Man", "Aridai Montiel": "Woman", "Adriana Lopez": "Man", "Oscar Atriano Ponce": "Man", "Xayli Ximena": "Man", "Victor Manuel Ocampo Mendez": "Man", "Rodrigo C": "Man", "Rodrigo Cahuantzi C": "Man", "Rodrigo c": "Man", "Yuriel": "Man", "Miguel Angel": "Man", "Omar": "Man"}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
db_institucion/Omar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
db_institucion/Yuriel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

269
fusion.py
View File

@ -1,7 +1,7 @@
import os import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000"
import cv2 import cv2
import numpy as np import numpy as np
import time import time
@ -17,7 +17,7 @@ warnings.filterwarnings("ignore")
# 1. IMPORTAMOS NUESTROS MÓDULOS # 1. IMPORTAMOS NUESTROS MÓDULOS
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# Del motor matemático y tracking # Del motor matemático y tracking
from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE, similitud_hibrida
# Del motor de reconocimiento facial y audio # Del motor de reconocimiento facial y audio
from reconocimiento2 import ( from reconocimiento2 import (
@ -43,37 +43,46 @@ BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# 3. MOTOR ASÍNCRONO # 3. MOTOR ASÍNCRONO
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
def procesar_rostro_async(frame, box, gid, cam_id, global_mem, trk): def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
""" Toma el recorte del tracker, usa YuNet importado, y hace la Fusión Mágica """ """ Toma el recorte del tracker, escala a HD, aplica filtros físicos y votación biométrica """
try: try:
if not BASE_DATOS_ROSTROS: return if not BASE_DATOS_ROSTROS: return
h_real, w_real = frame.shape[:2] # ──────────────────────────────────────────────────────────
# 1. ESCALADO HD Y EXTRACCIÓN DE CABEZA (Solución Xayli)
# ──────────────────────────────────────────────────────────
h_real, w_real = frame_hd.shape[:2]
if w_real <= 480:
print(f"[ERROR CAM {cam_id}] Le estás pasando el frame_show (480x270) a ArcFace, no el HD.")
escala_x = w_real / 480.0 escala_x = w_real / 480.0
escala_y = h_real / 270.0 escala_y = h_real / 270.0
x_min, y_min, x_max, y_max = box x_min, y_min, x_max, y_max = box_480
h_box = y_max - y_min h_box = y_max - y_min
y_min_expandido = max(0, y_min - (h_box * 0.15)) y_min_expandido = max(0, y_min - (h_box * 0.15))
y_max_cabeza = min(y_max, y_min + (h_box * 0.40)) # ⚡ 50% del cuerpo para no cortar cabezas de personas de menor estatura
y_max_cabeza = min(270, y_min + (h_box * 0.50))
x1 = int(max(0, x_min) * escala_x) x1_hd = int(max(0, x_min) * escala_x)
y1 = int(y_min_expandido * escala_y) y1_hd = int(y_min_expandido * escala_y)
x2 = int(min(480, x_max) * escala_x) x2_hd = int(min(480, x_max) * escala_x)
y2 = int(y_max_cabeza * escala_y) y2_hd = int(y_max_cabeza * escala_y)
roi_cabeza = frame[y1:y2, x1:x2] roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd]
if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 20 or roi_cabeza.shape[1] < 20: # ⚡ Filtro físico relajado a 40x40
if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 40 or roi_cabeza.shape[1] < 40:
return return
h_roi, w_roi = roi_cabeza.shape[:2] h_roi, w_roi = roi_cabeza.shape[:2]
# Usamos la función de YuNet importada, pasándole nuestro candado # ──────────────────────────────────────────────────────────
# 2. DETECCIÓN YUNET Y FILTROS ANTI-BASURA
# ──────────────────────────────────────────────────────────
faces = detectar_rostros_yunet(roi_cabeza, lock=YUNET_LOCK) faces = detectar_rostros_yunet(roi_cabeza, lock=YUNET_LOCK)
# OJO: Tu detectar_rostros_yunet devuelve 5 valores (x, y, w, h, score)
for (rx, ry, rw, rh, score) in faces: for (rx, ry, rw, rh, score) in faces:
rx, ry = max(0, rx), max(0, ry) rx, ry = max(0, rx), max(0, ry)
rw, rh = min(w_roi - rx, rw), min(h_roi - ry, rh) rw, rh = min(w_roi - rx, rw), min(h_roi - ry, rh)
@ -94,8 +103,9 @@ def procesar_rostro_async(frame, box, gid, cam_id, global_mem, trk):
necesita_saludo = True necesita_saludo = True
if nombre_actual is None or area_rostro_actual >= (area_ref * 1.5) or necesita_saludo: if nombre_actual is None or area_rostro_actual >= (area_ref * 1.5) or necesita_saludo:
m_x = int(rw * 0.15)
m_y = int(rh * 0.15) m_x = int(rw * 0.25)
m_y = int(rh * 0.25)
roi_rostro = roi_cabeza[max(0, ry-m_y):min(h_roi, ry+rh+m_y), roi_rostro = roi_cabeza[max(0, ry-m_y):min(h_roi, ry+rh+m_y),
max(0, rx-m_x):min(w_roi, rx+rw+m_x)] max(0, rx-m_x):min(w_roi, rx+rw+m_x)]
@ -103,64 +113,131 @@ def procesar_rostro_async(frame, box, gid, cam_id, global_mem, trk):
if roi_rostro.size == 0 or roi_rostro.shape[0] < 40 or roi_rostro.shape[1] < 40: if roi_rostro.size == 0 or roi_rostro.shape[0] < 40 or roi_rostro.shape[1] < 40:
continue continue
# ── Filtro de nitidez ── # 🛡️ FILTRO ANTI-PERFIL: Evita falsos positivos de personas viendo de lado
ratio_aspecto = roi_rostro.shape[1] / float(roi_rostro.shape[0])
if ratio_aspecto < 0.60:
continue
# 🛡️ FILTRO ÓPTICO (Movimiento)
gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY) gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY)
nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var()
if nitidez < 50.0: if nitidez < 15.0:
continue continue
# ── ArcFace (Protegido con IA_LOCK) ── # VISIÓN NOCTURNA (Simetría con Base de Datos)
try:
lab = cv2.cvtColor(roi_rostro, 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_rostro
# ──────────────────────────────────────────────────────────
# 3. MOTOR MTCNN Y SISTEMA DE VOTACIÓN
# ──────────────────────────────────────────────────────────
with IA_LOCK: with IA_LOCK:
try: try:
res = DeepFace.represent(img_path=roi_rostro, model_name="ArcFace", enforce_detection=False) res = DeepFace.represent(
img_path=roi_mejorado,
model_name="ArcFace",
detector_backend="mtcnn",
align=True,
enforce_detection=True
)
emb = np.array(res[0]["embedding"], dtype=np.float32) emb = np.array(res[0]["embedding"], dtype=np.float32)
# Usamos tu función importada (ya con producto punto)
mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS) mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS)
except Exception: except Exception:
continue continue # Cara inválida para MTCNN
print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f} (Umbral: {UMBRAL_SIM})") print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f}")
if max_sim > UMBRAL_SIM and mejor_match: if max_sim >= UMBRAL_SIM and mejor_match:
nombre_limpio = mejor_match.split('_')[0] nombre_limpio = mejor_match.split('_')[0]
with global_mem.lock: with global_mem.lock:
gid_original = None datos_id = global_mem.db.get(gid)
for otro_gid, datos_otro in global_mem.db.items(): if not datos_id: continue
if datos_otro.get('nombre') == nombre_limpio and otro_gid != gid:
gid_original = otro_gid
break
if gid_original is not None: # SISTEMA DE VOTACIÓN (Anti-Falsos Positivos)
print(f"\n[FUSIÓN MÁGICA] Uniendo el ID {gid} al original {gid_original} ({nombre_limpio})") if datos_id.get('candidato_nombre') == nombre_limpio:
if gid in global_mem.db: datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1
del global_mem.db[gid]
global_mem.db[gid_original]['ts'] = time.time()
global_mem.db[gid_original]['last_cam'] = cam_id
trk.gid = gid_original
else: else:
global_mem.db[gid]['nombre'] = nombre_limpio datos_id['candidato_nombre'] = nombre_limpio
global_mem.db[gid]['area_rostro_ref'] = area_rostro_actual datos_id['votos_nombre'] = 1
# ⚡ EL PASE VIP: Si la certeza es aplastante (>0.55), salta la burocracia
if max_sim >= 0.45:
datos_id['votos_nombre'] = max(2, datos_id['votos_nombre'])
# Solo actuamos si tiene 2 votos consistentes...
if datos_id['votos_nombre'] >= 2:
nombre_actual = datos_id.get('nombre')
# CANDADO DE BAUTIZO: Protege a los VIPs de alucinaciones borrosas
if nombre_actual is not None and nombre_actual != nombre_limpio:
if max_sim < 0.55:
# Si es un puntaje bajo, es una confusión de ArcFace. Lo ignoramos.
print(f" [RECHAZO] ArcFace intentó renombrar a {nombre_actual} como {nombre_limpio} con solo {max_sim:.2f}")
continue
else:
# Si el puntaje es masivo, significa que OSNet se equivocó y pegó 2 personas
print(f"[CORRECCIÓN VIP] OSNet se confundió. Renombrando a {nombre_limpio} ({max_sim:.2f})")
# ⚡ BAUTIZO Y LIMPIEZA
if nombre_actual != nombre_limpio:
datos_id['nombre'] = nombre_limpio
print(f" [BAUTIZO] ID {gid} confirmado como {nombre_limpio}")
ids_a_borrar = []
firma_actual = datos_id['firmas'][0] if datos_id['firmas'] else None
for otro_gid, datos_otro in list(global_mem.db.items()):
if otro_gid == gid: continue
if datos_otro.get('nombre') == nombre_limpio:
ids_a_borrar.append(otro_gid)
elif datos_otro.get('nombre') is None and firma_actual and datos_otro['firmas']:
sim_huerfano = similitud_hibrida(firma_actual, datos_otro['firmas'][0])
if sim_huerfano > 0.75:
ids_a_borrar.append(otro_gid)
for id_basura in ids_a_borrar:
del global_mem.db[id_basura]
# Actualizamos referencias
datos_id['area_rostro_ref'] = area_rostro_actual
datos_id['ts'] = time.time()
# BLINDAJE VIP: Si la certeza es absoluta, amarrar la ropa a este ID
if max_sim > 0.65:
# Usamos la función externa para evitar bloqueos dobles (Deadlocks)
pass
# 🔊 SALUDO DE BIENVENIDA
if str(cam_id) == "7" and necesita_saludo: if str(cam_id) == "7" and necesita_saludo:
global_mem.ultimos_saludos[nombre_limpio] = time.time() global_mem.ultimos_saludos[nombre_limpio] = time.time()
try: import json
with IA_LOCK: genero = "Man" # Valor por defecto seguro
analisis = DeepFace.analyze(roi_rostro, actions=['gender'], enforce_detection=False)[0] ruta_generos = os.path.join("cache_nombres", "generos.json")
genero = analisis.get('dominant_gender', 'Man')
except Exception:
genero = "Man"
# Usamos la función importada para el audio if os.path.exists(ruta_generos):
threading.Thread( try:
target=hilo_bienvenida, with open(ruta_generos, 'r') as f:
args=(nombre_limpio, genero), dic_generos = json.load(f)
daemon=True genero = dic_generos.get(nombre_limpio, "Man")
).start() except Exception:
break pass # Si el archivo está ocupado, usamos el defecto
# Lanzamos el audio instantáneamente sin IA pesada
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero), daemon=True).start()
# Ejecutamos el blindaje fuera del lock principal
if max_sim > 0.65 and datos_id.get('votos_nombre', 0) >= 2:
global_mem.confirmar_firma_vip(gid, time.time())
break # Salimos del loop de rostros si ya identificamos
except Exception as e: except Exception as e:
pass pass
finally: finally:
@ -245,11 +322,11 @@ def main():
turno_activo = (i == cam_ia) turno_activo = (i == cam_ia)
if turno_activo: if turno_activo:
res = model.predict(frame_show, conf=0.40, iou=0.50, classes=[0], verbose=False, imgsz=480) res = model.predict(frame_show, conf=0.50, iou=0.50, classes=[0], verbose=False, imgsz=480)
if res[0].boxes: if res[0].boxes:
boxes = res[0].boxes.xyxy.cpu().numpy().tolist() boxes = res[0].boxes.xyxy.cpu().numpy().tolist()
tracks = managers[cid].update(boxes, frame_show, now, turno_activo) tracks = managers[cid].update(boxes, frame_show, frame, now, turno_activo)
for trk in tracks: for trk in tracks:
if trk.time_since_update <= 1: if trk.time_since_update <= 1:
@ -270,10 +347,86 @@ def main():
cv2.imshow("SmartSoft Fusion", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])])) cv2.imshow("SmartSoft Fusion", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])]))
idx += 1 idx += 1
if cv2.waitKey(1) == ord('q'):
# ⚡ CAPTURAMOS LA TECLA EN UNA VARIABLE
# ⚡ CAPTURAMOS LA TECLA EN UNA VARIABLE
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break break
cv2.destroyAllWindows() elif key == ord('r'):
print("\n[MODO REGISTRO] Escaneando mosaico para registrar...")
mejor_roi = None
max_area = 0
# ⚡ CORRECCIÓN: 'cams' es una lista, usamos enumerate
for i, cam_obj in enumerate(cams):
if cam_obj.frame is None: continue
faces = detectar_rostros_yunet(cam_obj.frame)
for (fx, fy, fw, fh, score) in faces:
area = fw * fh
if area > max_area:
max_area = area
h_frame, w_frame = cam_obj.frame.shape[:2]
# Margen amplio (30%) para MTCNN
m_x, m_y = int(fw * 0.30), int(fh * 0.30)
y1 = max(0, fy - m_y)
y2 = min(h_frame, fy + fh + m_y)
x1 = max(0, fx - m_x)
x2 = min(w_frame, fx + fw + m_x)
mejor_roi = cam_obj.frame[y1:y2, x1:x2]
if mejor_roi is not None and mejor_roi.size > 0:
cv2.imshow("Nueva Persona", mejor_roi)
cv2.waitKey(1)
nom = input("Escribe el nombre de la persona: ").strip()
cv2.destroyWindow("Nueva Persona")
if nom:
import json
# 1. Pedimos el género para no usar IA en el futuro
gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower()
genero_guardado = "Woman" if gen_input == 'm' else "Man"
# 2. Actualizamos el caché de géneros al instante
ruta_generos = os.path.join("cache_nombres", "generos.json")
os.makedirs("cache_nombres", exist_ok=True)
dic_generos = {}
if os.path.exists(ruta_generos):
try:
with open(ruta_generos, 'r') as f:
dic_generos = json.load(f)
except Exception:
pass
dic_generos[nom] = genero_guardado
try:
with open(ruta_generos, 'w') as f:
json.dump(dic_generos, f)
except Exception as e:
print(f"[!] Error al guardar el género: {e}")
# 3. Guardado tradicional de la foto
ruta_db = "db_institucion"
os.makedirs(ruta_db, exist_ok=True)
cv2.imwrite(os.path.join(ruta_db, f"{nom}.jpg"), mejor_roi)
print(f"[OK] Rostro de '{nom}' guardado como {genero_guardado}.")
print(" Sincronizando base de datos en caliente...")
# Al llamar a esta función, el sistema alineará la foto sin pisar nuestro JSON
nuevos_vectores = gestionar_vectores(actualizar=True)
BASE_DATOS_ROSTROS.clear()
BASE_DATOS_ROSTROS.update(nuevos_vectores)
print(" Sistema listo.")
else:
print("[!] Registro cancelado.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

110
generar_db_rostros.py Normal file
View File

@ -0,0 +1,110 @@
import cv2
import os
import json
import numpy as np
# ⚡ Importamos tus motores exactos para no romper la simetría
from reconocimiento2 import detectar_rostros_yunet, gestionar_vectores
def registrar_desde_webcam():
print("\n" + "="*50)
print("📸 MÓDULO DE REGISTRO LIMPIO (WEBCAM LOCAL)")
print("Alinea tu rostro, mira a la cámara con buena luz.")
print("Presiona [R] para capturar | [Q] para salir")
print("="*50 + "\n")
DB_PATH = "db_institucion"
CACHE_PATH = "cache_nombres"
os.makedirs(DB_PATH, exist_ok=True)
os.makedirs(CACHE_PATH, exist_ok=True)
# 0 es la cámara por defecto de tu laptop
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("[!] Error: No se pudo abrir la webcam local.")
return
while True:
ret, frame = cap.read()
if not ret: continue
# Espejamos la imagen para que actúe como un espejo natural
frame = cv2.flip(frame, 1)
display_frame = frame.copy()
# Usamos YuNet para garantizar que estamos capturando una cara válida
faces = detectar_rostros_yunet(frame)
mejor_rostro = None
max_area = 0
for (fx, fy, fw, fh, score) in faces:
area = fw * fh
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 2)
if area > max_area:
max_area = area
h_frame, w_frame = frame.shape[:2]
# Mismo margen del 30% que requiere MTCNN para alinear correctamente
m_x, m_y = int(fw * 0.30), int(fh * 0.30)
y1 = max(0, fy - m_y)
y2 = min(h_frame, fy + fh + m_y)
x1 = max(0, fx - m_x)
x2 = min(w_frame, fx + fw + m_x)
mejor_rostro = frame[y1:y2, x1:x2]
cv2.putText(display_frame, "Alineate y presiona [R]", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
cv2.imshow("Registro Webcam Local", display_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('r'):
if mejor_rostro is not None and mejor_rostro.size > 0:
cv2.imshow("Captura Congelada", mejor_rostro)
cv2.waitKey(1)
print("\n--- NUEVO REGISTRO ---")
nom = input("Escribe el nombre exacto de la persona: ").strip()
if nom:
gen_input = input("¿Es Hombre (h) o Mujer (m)?: ").strip().lower()
genero_guardado = "Woman" if gen_input == 'm' else "Man"
# 1. Guardamos la foto pura
foto_path = os.path.join(DB_PATH, f"{nom}.jpg")
cv2.imwrite(foto_path, mejor_rostro)
# 2. Actualizamos el caché de géneros sin usar IA
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
dic_generos[nom] = genero_guardado
with open(ruta_generos, 'w') as f:
json.dump(dic_generos, f)
print(f"\n[OK] Foto guardada. Generando punto de gravedad matemático...")
# 3. Forzamos la creación del vector en la base de datos
gestionar_vectores(actualizar=True)
print(" Registro inyectado exitosamente en el sistema principal.")
else:
print("[!] Registro cancelado por nombre vacío.")
cv2.destroyWindow("Captura Congelada")
else:
print("[!] No se detectó ningún rostro claro. Acércate más a la luz.")
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
registrar_desde_webcam()

43
prueba_video.py Normal file
View File

@ -0,0 +1,43 @@
import cv2
import time
from ultralytics import YOLO
from seguimiento2 import GlobalMemory, CamManager, dibujar_track
def test_video(video_path):
print(f"Iniciando Benchmark de Video: {video_path}")
model = YOLO("yolov8n.pt")
global_mem = GlobalMemory()
manager = CamManager("TEST_CAM", global_mem)
cap = cv2.VideoCapture(video_path)
cv2.namedWindow("Benchmark TT", cv2.WINDOW_AUTOSIZE)
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
now = time.time()
frame_show = cv2.resize(frame, (480, 270))
# Inferencia frame por frame sin hilos (sincrónico)
res = model.predict(frame_show, conf=0.40, iou=0.50, classes=[0], verbose=False, imgsz=480, device='cpu')
boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes else []
tracks = manager.update(boxes, frame_show, now, turno_activo=True)
for trk in tracks:
if trk.time_since_update == 0:
dibujar_track(frame_show, trk)
cv2.imshow("Benchmark TT", frame_show)
# Si presionas espacio se pausa, con 'q' sales
key = cv2.waitKey(30) & 0xFF
if key == ord('q'): break
elif key == ord(' '): cv2.waitKey(-1)
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
test_video("video.mp4") # Pon aquí el nombre de tu video

View File

@ -22,12 +22,12 @@ warnings.filterwarnings("ignore")
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
DB_PATH = "db_institucion" DB_PATH = "db_institucion"
CACHE_PATH = "cache_nombres" CACHE_PATH = "cache_nombres"
VECTORS_FILE = "representaciones.pkl" VECTORS_FILE = "base_datos_rostros.pkl"
TIMESTAMPS_FILE = "representaciones_timestamps.pkl" TIMESTAMPS_FILE = "representaciones_timestamps.pkl"
UMBRAL_SIM = 0.50 # Por encima → identificado. Por debajo → desconocido. UMBRAL_SIM = 0.39 # Por encima → identificado. Por debajo → desconocido.
COOLDOWN_TIME = 15 # Segundos entre saludos COOLDOWN_TIME = 15 # Segundos entre saludos
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702" RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702"
for path in [DB_PATH, CACHE_PATH]: for path in [DB_PATH, CACHE_PATH]:
@ -46,7 +46,6 @@ if not os.path.exists(YUNET_MODEL_PATH):
print("YuNet descargado.") print("YuNet descargado.")
# Detector estricto para ROIs grandes (persona cerca) # Detector estricto para ROIs grandes (persona cerca)
# score_threshold alto → menos falsos positivos en fondos
detector_yunet = cv2.FaceDetectorYN.create( detector_yunet = cv2.FaceDetectorYN.create(
model=YUNET_MODEL_PATH, config="", model=YUNET_MODEL_PATH, config="",
input_size=(320, 320), input_size=(320, 320),
@ -56,7 +55,6 @@ detector_yunet = cv2.FaceDetectorYN.create(
) )
# Detector permisivo para ROIs pequeños (persona lejos) # Detector permisivo para ROIs pequeños (persona lejos)
# score_threshold bajo → no perdemos caras pequeñas o a contraluz
detector_yunet_lejano = cv2.FaceDetectorYN.create( detector_yunet_lejano = cv2.FaceDetectorYN.create(
model=YUNET_MODEL_PATH, config="", model=YUNET_MODEL_PATH, config="",
input_size=(320, 320), input_size=(320, 320),
@ -68,9 +66,6 @@ detector_yunet_lejano = cv2.FaceDetectorYN.create(
def detectar_rostros_yunet(roi, lock=None): def detectar_rostros_yunet(roi, lock=None):
""" """
Elige automáticamente el detector según el tamaño del ROI. Elige automáticamente el detector según el tamaño del ROI.
ROI grande detector estricto (evita falsos positivos).
ROI pequeño detector permisivo (no pierde caras lejanas).
Devuelve lista de (x, y, w, h) o lista vacía.
""" """
h_roi, w_roi = roi.shape[:2] h_roi, w_roi = roi.shape[:2]
area = w_roi * h_roi area = w_roi * h_roi
@ -119,7 +114,6 @@ def obtener_audios_humanos(genero):
async def sintetizar_nombre(nombre, ruta): async def sintetizar_nombre(nombre, ruta):
"""Genera el audio del nombre con edge-tts (solo si no existe en caché)."""
nombre_limpio = nombre.replace('_', ' ') nombre_limpio = nombre.replace('_', ' ')
try: try:
comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%") comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%")
@ -129,7 +123,6 @@ async def sintetizar_nombre(nombre, ruta):
def reproducir(archivo): def reproducir(archivo):
"""Lanza mpv en segundo plano sin bloquear."""
if os.path.exists(archivo): if os.path.exists(archivo):
subprocess.Popen( subprocess.Popen(
["mpv", "--no-video", "--volume=100", archivo], ["mpv", "--no-video", "--volume=100", archivo],
@ -138,15 +131,9 @@ def reproducir(archivo):
) )
# mpv encadena los 3 archivos en una sola llamada
def hilo_bienvenida(nombre, genero): def hilo_bienvenida(nombre, genero):
"""
Reproduce intro + nombre + cierre sin time.sleep().
mpv los reproduce en orden nativamente el hilo queda libre de inmediato.
"""
archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3") archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3")
# Sintetizar solo si no estaba en caché
if not os.path.exists(archivo_nombre): if not os.path.exists(archivo_nombre):
try: try:
asyncio.run(sintetizar_nombre(nombre, archivo_nombre)) asyncio.run(sintetizar_nombre(nombre, archivo_nombre))
@ -155,7 +142,6 @@ def hilo_bienvenida(nombre, genero):
intro, cierre = obtener_audios_humanos(genero) intro, cierre = obtener_audios_humanos(genero)
# Una sola llamada a mpv con los 3 archivos en secuencia
archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)] archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)]
if archivos: if archivos:
subprocess.Popen( subprocess.Popen(
@ -166,21 +152,12 @@ def hilo_bienvenida(nombre, genero):
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# GESTIÓN DE BASE DE DATOS # GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN)
# Incremental: solo procesa fotos nuevas o modificadas
# Vectores normalizados al guardar → comparación 3x más rápida
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
def gestionar_vectores(actualizar=False): def gestionar_vectores(actualizar=False):
""" import json # ⚡ Asegúrate de tener importado json
Carga o actualiza el diccionario {nombre: vector_normalizado}.
- Si actualizar=False y el .pkl existe carga directamente (instantáneo).
- Si actualizar=True solo reprocesa fotos nuevas o modificadas (incremental).
- Los vectores se guardan NORMALIZADOS la comparación es un simple np.dot().
"""
vectores_actuales = {} vectores_actuales = {}
# Intentar cargar lo que ya teníamos
if os.path.exists(VECTORS_FILE): if os.path.exists(VECTORS_FILE):
try: try:
with open(VECTORS_FILE, 'rb') as f: with open(VECTORS_FILE, 'rb') as f:
@ -191,7 +168,6 @@ def gestionar_vectores(actualizar=False):
if not actualizar: if not actualizar:
return vectores_actuales return vectores_actuales
# ── Cargar timestamps para detectar cambios ───────────────────────────────
timestamps = {} timestamps = {}
if os.path.exists(TIMESTAMPS_FILE): if os.path.exists(TIMESTAMPS_FILE):
try: try:
@ -200,10 +176,23 @@ def gestionar_vectores(actualizar=False):
except Exception: except Exception:
timestamps = {} timestamps = {}
print("\nACTUALIZANDO BASE DE DATOS (solo fotos nuevas o modificadas)...") # ──────────────────────────────────────────────────────────
# 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'))] imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))]
nombres_en_disco = set() nombres_en_disco = set()
hubo_cambios = False hubo_cambios = False
cambio_generos = False # Bandera para saber si actualizamos el JSON
for archivo in imagenes: for archivo in imagenes:
nombre_archivo = os.path.splitext(archivo)[0] nombre_archivo = os.path.splitext(archivo)[0]
@ -213,17 +202,39 @@ def gestionar_vectores(actualizar=False):
ts_actual = os.path.getmtime(ruta_img) ts_actual = os.path.getmtime(ruta_img)
ts_guardado = timestamps.get(nombre_archivo, 0) ts_guardado = timestamps.get(nombre_archivo, 0)
# Si ya la teníamos y no cambió → saltar (no llamar a ArcFace) # Si ya tenemos el vector pero NO tenemos su género en el JSON, forzamos el procesamiento
if nombre_archivo in vectores_actuales and ts_actual == ts_guardado: falta_genero = nombre_archivo not in dic_generos
if nombre_archivo in vectores_actuales and ts_actual == ts_guardado and not falta_genero:
continue continue
try: 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( res = DeepFace.represent(
img_path=ruta_img, model_name="ArcFace", enforce_detection=True img_path=img_mejorada,
model_name="ArcFace",
detector_backend="mtcnn",
align=True,
enforce_detection=True
) )
emb = np.array(res[0]["embedding"], dtype=np.float32) emb = np.array(res[0]["embedding"], dtype=np.float32)
# MEJORA 2: Normalizar al guardar (una sola vez para siempre)
norma = np.linalg.norm(emb) norma = np.linalg.norm(emb)
if norma > 0: if norma > 0:
emb = emb / norma emb = emb / norma
@ -231,62 +242,64 @@ def gestionar_vectores(actualizar=False):
vectores_actuales[nombre_archivo] = emb vectores_actuales[nombre_archivo] = emb
timestamps[nombre_archivo] = ts_actual timestamps[nombre_archivo] = ts_actual
hubo_cambios = True hubo_cambios = True
print(f" Procesado: {nombre_archivo}") print(f" Procesado y alineado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}")
except Exception: except Exception as e:
print(f" Rostro no válido en '{archivo}', omitido.") print(f" Rostro no válido en '{archivo}', omitido. Error: {e}")
# Eliminar personas cuya foto fue borrada del disco # Limpieza de eliminados
for nombre in list(vectores_actuales.keys()): for nombre in list(vectores_actuales.keys()):
if nombre not in nombres_en_disco: if nombre not in nombres_en_disco:
del vectores_actuales[nombre] del vectores_actuales[nombre]
timestamps.pop(nombre, None) timestamps.pop(nombre, None)
if nombre in dic_generos:
del dic_generos[nombre]
cambio_generos = True
hubo_cambios = True hubo_cambios = True
print(f" Eliminado (sin foto): {nombre}") print(f" Eliminado (sin foto): {nombre}")
# Guardado de la memoria
if hubo_cambios: if hubo_cambios:
with open(VECTORS_FILE, 'wb') as f: with open(VECTORS_FILE, 'wb') as f:
pickle.dump(vectores_actuales, f) pickle.dump(vectores_actuales, f)
with open(TIMESTAMPS_FILE, 'wb') as f: with open(TIMESTAMPS_FILE, 'wb') as f:
pickle.dump(timestamps, 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") print(" Sincronización terminada.\n")
else: else:
print(" Sin cambios. Base de datos al día.\n") print(" Sin cambios. Base de datos al día.\n")
return vectores_actuales return vectores_actuales
# ──────────────────────────────────────────────────────────────────────────────
# BÚSQUEDA BLINDADA (Similitud Coseno estricta)
# ──────────────────────────────────────────────────────────────────────────────
def buscar_mejor_match(emb_consulta, base_datos): def buscar_mejor_match(emb_consulta, base_datos):
""" # ⚡ MAGIA 3: Normalización L2 del vector entrante
Compara un embedding (ya normalizado) contra la base de datos.
MEJORA 2: Solo producto punto no hay divisiones por norma en el loop.
Devuelve (mejor_nombre, similitud_maxima).
"""
# Normalizar el embedding de consulta
norma = np.linalg.norm(emb_consulta) norma = np.linalg.norm(emb_consulta)
if norma > 0: if norma > 0:
emb_consulta = emb_consulta / norma emb_consulta = emb_consulta / norma
mejor_match, max_sim = None, -1.0 mejor_match, max_sim = None, -1.0
for nombre, vec in base_datos.items(): for nombre, vec in base_datos.items():
# vec ya está normalizado → sim coseno = producto punto puro # Como ambos están normalizados, esto es Similitud Coseno pura (-1.0 a 1.0)
sim = float(np.dot(emb_consulta, vec)) sim = float(np.dot(emb_consulta, vec))
if sim > max_sim: if sim > max_sim:
max_sim, mejor_match = sim, nombre max_sim = sim
mejor_match = nombre
return mejor_match, max_sim return mejor_match, max_sim
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# LOOP DE PRUEBA Y REGISTRO # LOOP DE PRUEBA Y REGISTRO (CON SIMETRÍA ESTRICTA)
# MEJORA 5 — Reemplaza face_cascade por YuNet (consistente con principal2.py)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
def sistema_interactivo(): def sistema_interactivo():
"""
Ventana de depuración y registro de nuevas personas.
Muestra barra de similitud en consola y en pantalla.
Controles: [R] Registrar | [Q] Salir
"""
base_datos = gestionar_vectores(actualizar=False) base_datos = gestionar_vectores(actualizar=False)
cap = cv2.VideoCapture(RTSP_URL) cap = cv2.VideoCapture(RTSP_URL)
ultimo_saludo = 0 ultimo_saludo = 0
@ -294,11 +307,11 @@ def sistema_interactivo():
confirmaciones = 0 confirmaciones = 0
print("\n" + "=" * 50) print("\n" + "=" * 50)
print(" MÓDULO DE REGISTRO Y DEPURACIÓN") print(" MÓDULO DE REGISTRO Y DEPURACIÓN ESTRICTO")
print(" [R] Registrar nuevo rostro | [Q] Salir") print(" [R] Registrar nuevo rostro | [Q] Salir")
print("=" * 50 + "\n") print("=" * 50 + "\n")
faces_ultimo_frame = [] # Para que [R] pueda usarlas aunque no haya detección nueva faces_ultimo_frame = []
while True: while True:
ret, frame = cap.read() ret, frame = cap.read()
@ -311,36 +324,33 @@ def sistema_interactivo():
display_frame = frame.copy() display_frame = frame.copy()
tiempo_actual = time.time() tiempo_actual = time.time()
# ── Detección con YuNet sobre el frame completo ───────────────────────
faces_raw = detectar_rostros_yunet(frame) faces_raw = detectar_rostros_yunet(frame)
faces_ultimo_frame = faces_raw # Guardamos para usar con [R] faces_ultimo_frame = faces_raw
for (fx, fy, fw, fh, score_yunet) in faces_raw: for (fx, fy, fw, fh, score_yunet) in faces_raw:
# Clampear dentro de la imagen
fx = max(0, fx); fy = max(0, fy) fx = max(0, fx); fy = max(0, fy)
fw = min(w - fx, fw); fh = min(h - fy, fh) fw = min(w - fx, fw); fh = min(h - fy, fh)
if fw <= 0 or fh <= 0: if fw <= 0 or fh <= 0:
continue continue
# Rectángulo base (amarillo)
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2) cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2)
cv2.putText(display_frame, f"YN:{score_yunet:.2f}", cv2.putText(display_frame, f"YN:{score_yunet:.2f}",
(fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1) (fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1)
if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME: if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME:
continue # En cooldown, no procesamos continue
# ── Recorte del rostro con margen ─────────────────────────────────
m = int(fw * 0.15) m = int(fw * 0.15)
roi = frame[max(0, fy-m): min(h, fy+fh+m), roi = frame[max(0, fy-m): min(h, fy+fh+m),
max(0, fx-m): min(w, fx+fw+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: if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40:
cv2.putText(display_frame, "muy pequeño", cv2.putText(display_frame, "muy pequeno",
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1) (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1)
continue continue
# ── Filtro de nitidez (descarta caras movidas antes de ArcFace) ─── # 🛡️ FILTRO DE NITIDEZ
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var() nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var()
if nitidez < 50.0: if nitidez < 50.0:
@ -348,19 +358,34 @@ def sistema_interactivo():
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1) (fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
continue continue
# ── ArcFace ─────────────────────────────────────────────────────── # 🌙 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: try:
res = DeepFace.represent( res = DeepFace.represent(
img_path=roi, model_name="ArcFace", enforce_detection=False 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) emb = np.array(res[0]["embedding"], dtype=np.float32)
mejor_match, max_sim = buscar_mejor_match(emb, base_datos) mejor_match, max_sim = buscar_mejor_match(emb, base_datos)
except Exception as e: except Exception:
print(f"[ERROR ArcFace]: {e}") # 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 continue
# ── Barra de similitud en consola ─────────────────────────────────
estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO" estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO"
nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie" nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie"
n_bloques = int(max_sim * 20) n_bloques = int(max_sim * 20)
@ -368,7 +393,6 @@ def sistema_interactivo():
print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | " print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | "
f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)") f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)")
# ── Resultado en pantalla ─────────────────────────────────────────
if max_sim > UMBRAL_SIM and mejor_match: if max_sim > UMBRAL_SIM and mejor_match:
color = (0, 255, 0) color = (0, 255, 0)
texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})" texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})"
@ -382,7 +406,7 @@ def sistema_interactivo():
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3) cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3)
try: try:
analisis = DeepFace.analyze( analisis = DeepFace.analyze(
roi, actions=['gender'], enforce_detection=False roi_mejorado, actions=['gender'], enforce_detection=False
)[0] )[0]
genero = analisis['dominant_gender'] genero = analisis['dominant_gender']
except Exception: except Exception:
@ -404,22 +428,21 @@ def sistema_interactivo():
cv2.putText(display_frame, texto, cv2.putText(display_frame, texto,
(fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) (fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
# ── Mostrar frame ─────────────────────────────────────────────────────
cv2.imshow("Módulo de Registro", display_frame) cv2.imshow("Módulo de Registro", display_frame)
key = cv2.waitKey(1) & 0xFF key = cv2.waitKey(1) & 0xFF
if key == ord('q'): if key == ord('q'):
break break
#R: Registrar el rostro más grande del frame ─────────────────
elif key == ord('r'): elif key == ord('r'):
if faces_ultimo_frame: if faces_ultimo_frame:
# Elegimos la cara más grande (más cercana)
areas = [fw * fh for (fx, fy, fw, fh, _) in 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)] fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)]
m = 25
face_roi = frame[max(0, fy-m): min(h, fy+fh+m), m_x = int(fw * 0.30)
max(0, fx-m): min(w, fx+fw+m)] 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: if face_roi.size > 0:
nom = input("\nNombre de la persona: ").strip() nom = input("\nNombre de la persona: ").strip()
@ -438,7 +461,5 @@ def sistema_interactivo():
cap.release() cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
# ──────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__": if __name__ == "__main__":
sistema_interactivo() sistema_interactivo()

Binary file not shown.

Binary file not shown.

View File

@ -11,9 +11,10 @@ import os
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# CONFIGURACIÓN DEL SISTEMA # CONFIGURACIÓN DEL SISTEMA
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
SECUENCIA = [1, 7, 5, 8, 3, 6] SECUENCIA = [1, 7, 5, 8, 3, 6]
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" # 🛡️ RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg)
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000"
URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA] URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA]
ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx" ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx"
@ -22,19 +23,16 @@ VECINOS = {
"8": ["5", "3"], "3": ["8", "6"], "6": ["3"] "8": ["5", "3"], "3": ["8", "6"], "6": ["3"]
} }
# ─── PARÁMETROS TÉCNICOS
ASPECT_RATIO_MIN = 0.5 ASPECT_RATIO_MIN = 0.5
ASPECT_RATIO_MAX = 4.0 ASPECT_RATIO_MAX = 4.0
AREA_MIN_CALIDAD = 1200 AREA_MIN_CALIDAD = 1200
FRAMES_CALIDAD = 2 FRAMES_CALIDAD = 2
TIEMPO_MIN_TRANSITO_NO_VECINO = 10.0
TIEMPO_MAX_AUSENCIA = 800.0 TIEMPO_MAX_AUSENCIA = 800.0
# ─── UMBRALES DE RE-ID (VERSIÓN ESTABLE) UMBRAL_REID_MISMA_CAM = 0.53 # Antes 0.65
UMBRAL_REID_MISMA_CAM = 0.65 UMBRAL_REID_VECINO = 0.48 # Antes 0.53
UMBRAL_REID_VECINO = 0.55 UMBRAL_REID_NO_VECINO = 0.67 # Antes 0.72
UMBRAL_REID_NO_VECINO = 0.72 MAX_FIRMAS_MEMORIA = 10
MAX_FIRMAS_MEMORIA = 15
C_CANDIDATO = (150, 150, 150) C_CANDIDATO = (150, 150, 150)
C_LOCAL = (0, 255, 0) C_LOCAL = (0, 255, 0)
@ -44,7 +42,7 @@ C_APRENDIZAJE = (255, 255, 0)
FUENTE = cv2.FONT_HERSHEY_SIMPLEX FUENTE = cv2.FONT_HERSHEY_SIMPLEX
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# INICIALIZACIÓN DEL MOTOR DEEP LEARNING (ONNX) # INICIALIZACIÓN OSNET
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
print("Cargando cerebro de Re-Identificación (OSNet)...") print("Cargando cerebro de Re-Identificación (OSNet)...")
try: try:
@ -59,7 +57,7 @@ MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 3, 1, 1)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1) STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# 1. MOTOR HÍBRIDO ESTABLE (Sin filtros destructivos) # 1. EXTRACCIÓN DE FIRMAS (Deep + Color + Textura)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
def analizar_calidad(box): def analizar_calidad(box):
x1, y1, x2, y2 = box x1, y1, x2, y2 = box
@ -77,8 +75,7 @@ def preprocess_onnx(roi):
def extraer_color_zonas(img): def extraer_color_zonas(img):
h_roi = img.shape[0] h_roi = img.shape[0]
t1 = int(h_roi * 0.15) t1, t2 = int(h_roi * 0.15), int(h_roi * 0.55)
t2 = int(h_roi * 0.55)
zonas = [img[:t1, :], img[t1:t2, :], img[t2:, :]] zonas = [img[:t1, :], img[t1:t2, :], img[t2:, :]]
def hist_zona(z): def hist_zona(z):
@ -87,19 +84,34 @@ def extraer_color_zonas(img):
hist = cv2.calcHist([hsv], [0, 1], None, [16, 8], [0, 180, 0, 256]) hist = cv2.calcHist([hsv], [0, 1], None, [16, 8], [0, 180, 0, 256])
cv2.normalize(hist, hist) cv2.normalize(hist, hist)
return hist.flatten() return hist.flatten()
return np.concatenate([hist_zona(z) for z in zonas]) return np.concatenate([hist_zona(z) for z in zonas])
def extraer_firma_hibrida(frame, box): def extraer_textura_rapida(roi):
if roi.size == 0: return np.zeros(16)
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray_eq = cv2.equalizeHist(gray)
gx = cv2.Sobel(gray_eq, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(gray_eq, cv2.CV_32F, 0, 1, ksize=3)
mag, _ = cv2.cartToPolar(gx, gy)
hist = cv2.calcHist([mag], [0], None, [16], [0, 256])
cv2.normalize(hist, hist)
return hist.flatten()
def extraer_firma_hibrida(frame_hd, box_480):
try: try:
x1, y1, x2, y2 = map(int, box) h_hd, w_hd = frame_hd.shape[:2]
fh, fw = frame.shape[:2] escala_x = w_hd / 480.0
x1_c, y1_c = max(0, x1), max(0, y1) escala_y = h_hd / 270.0
x2_c, y2_c = min(fw, x2), min(fh, y2)
roi = frame[y1_c:y2_c, x1_c:x2_c] x1, y1, x2, y2 = box_480
x1_hd, y1_hd = int(x1 * escala_x), int(y1 * escala_y)
x2_hd, y2_hd = int(x2 * escala_x), int(y2 * escala_y)
if roi.size == 0 or roi.shape[0] < 20 or roi.shape[1] < 10: return None x1_c, y1_c = max(0, x1_hd), max(0, y1_hd)
x2_c, y2_c = min(w_hd, x2_hd), min(h_hd, y2_hd)
roi = frame_hd[y1_c:y2_c, x1_c:x2_c]
if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 20: return None
calidad_area = (x2_c - x1_c) * (y2_c - y1_c) calidad_area = (x2_c - x1_c) * (y2_c - y1_c)
@ -111,24 +123,44 @@ def extraer_firma_hibrida(frame, box):
if norma > 0: deep_feat = deep_feat / norma if norma > 0: deep_feat = deep_feat / norma
color_feat = extraer_color_zonas(roi) color_feat = extraer_color_zonas(roi)
textura_feat = extraer_textura_rapida(roi)
return {'deep': deep_feat, 'color': color_feat, 'calidad': calidad_area} return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area}
except Exception as e: except Exception:
return None return None
def similitud_hibrida(f1, f2): # ⚡ EL SECRETO: 100% IA entre cámaras. Textura solo en la misma cámara.
# ⚡ EL SECRETO: 100% IA entre cámaras. Textura solo en la misma cámara.
def similitud_hibrida(f1, f2, cross_cam=False):
if f1 is None or f2 is None: return 0.0 if f1 is None or f2 is None: return 0.0
sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep'])) sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep']))
# 1. Calculamos SIEMPRE los histogramas de color por zonas
if f1['color'].shape == f2['color'].shape and f1['color'].size > 1: if f1['color'].shape == f2['color'].shape and f1['color'].size > 1:
L = len(f1['color']) // 3 L = len(f1['color']) // 3
sim_head = max(0.0, float(cv2.compareHist(f1['color'][:L].astype(np.float32), f2['color'][:L].astype(np.float32), cv2.HISTCMP_CORREL))) sim_head = max(0.0, float(cv2.compareHist(f1['color'][:L].astype(np.float32), f2['color'][:L].astype(np.float32), cv2.HISTCMP_CORREL)))
sim_torso = max(0.0, float(cv2.compareHist(f1['color'][L:2*L].astype(np.float32), f2['color'][L:2*L].astype(np.float32), cv2.HISTCMP_CORREL))) sim_torso = max(0.0, float(cv2.compareHist(f1['color'][L:2*L].astype(np.float32), f2['color'][L:2*L].astype(np.float32), cv2.HISTCMP_CORREL)))
sim_legs = max(0.0, float(cv2.compareHist(f1['color'][2*L:].astype(np.float32), f2['color'][2*L:].astype(np.float32), cv2.HISTCMP_CORREL))) sim_legs = max(0.0, float(cv2.compareHist(f1['color'][2*L:].astype(np.float32), f2['color'][2*L:].astype(np.float32), cv2.HISTCMP_CORREL)))
sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs) else:
else: sim_color = 0.0 sim_head, sim_torso, sim_legs = 0.0, 0.0, 0.0
return (sim_deep * 0.90) + (sim_color * 0.10) if cross_cam:
# Si OSNet se confunde por la chamarra, revisamos las piernas.
# Una similitud < 0.25 en HISTCMP_CORREL significa colores radicalmente distintos (Ej. Negro vs Gris/Blanco).
if sim_legs < 0.25:
sim_deep -= 0.15 # Castigo letal: Le tumbamos 15 puntos porcentuales a OSNet
return max(0.0, sim_deep)
# 2. Si está en la misma cámara, usamos la fórmula híbrida completa
sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs)
if 'textura' in f1 and 'textura' in f2 and f1['textura'].size > 1:
sim_textura = max(0.0, float(cv2.compareHist(f1['textura'].astype(np.float32), f2['textura'].astype(np.float32), cv2.HISTCMP_CORREL)))
else: sim_textura = 0.0
return (sim_deep * 0.80) + (sim_color * 0.10) + (sim_textura * 0.10)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# 2. KALMAN TRACKER # 2. KALMAN TRACKER
@ -137,8 +169,7 @@ class KalmanTrack:
_count = 0 _count = 0
def __init__(self, box, now): def __init__(self, box, now):
self.kf = cv2.KalmanFilter(7, 4) self.kf = cv2.KalmanFilter(7, 4)
self.kf.measurementMatrix = np.array([ self.kf.measurementMatrix = np.array([[1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0]], np.float32)
[1,0,0,0,0,0,0], [0,1,0,0,0,0,0], [0,0,1,0,0,0,0], [0,0,0,1,0,0,0]], np.float32)
self.kf.transitionMatrix = np.eye(7, dtype=np.float32) self.kf.transitionMatrix = np.eye(7, dtype=np.float32)
self.kf.transitionMatrix[0,4] = 1; self.kf.transitionMatrix[1,5] = 1; self.kf.transitionMatrix[2,6] = 1 self.kf.transitionMatrix[0,4] = 1; self.kf.transitionMatrix[1,5] = 1; self.kf.transitionMatrix[2,6] = 1
self.kf.processNoiseCov *= 0.03 self.kf.processNoiseCov *= 0.03
@ -150,10 +181,8 @@ class KalmanTrack:
self.origen_global = False self.origen_global = False
self.aprendiendo = False self.aprendiendo = False
self.box = list(box) self.box = list(box)
self.ts_creacion = now self.ts_creacion = now
self.ts_ultima_deteccion = now self.ts_ultima_deteccion = now
self.time_since_update = 0 self.time_since_update = 0
self.en_grupo = False self.en_grupo = False
self.frames_buena_calidad = 0 self.frames_buena_calidad = 0
@ -192,7 +221,7 @@ class KalmanTrack:
self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1) self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# 3. MEMORIA GLOBAL (Top-3 Ponderado y Anti-Robo) # 3. MEMORIA GLOBAL (Anti-Robos y Físicas de Tiempo)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
class GlobalMemory: class GlobalMemory:
def __init__(self): def __init__(self):
@ -207,27 +236,25 @@ class GlobalMemory:
if ultima_cam == cam_destino: return True if ultima_cam == cam_destino: return True
vecinos = VECINOS.get(ultima_cam, []) vecinos = VECINOS.get(ultima_cam, [])
# Permite teletransportación mínima (-0.5s) para que no te fragmente en los pasillos conectados
if cam_destino in vecinos: return dt >= -0.5 if cam_destino in vecinos: return dt >= -0.5
return dt >= 4.0 return dt >= 4.0
def _sim_robusta(self, firma_nueva, firmas_guardadas): def _sim_robusta(self, firma_nueva, firmas_guardadas, cross_cam=False):
if not firmas_guardadas: return 0.0 if not firmas_guardadas: return 0.0
sims = sorted([similitud_hibrida(firma_nueva, f, cross_cam) for f in firmas_guardadas], reverse=True)
if len(sims) == 1: return sims[0]
elif len(sims) <= 4: return (sims[0] * 0.6) + (sims[1] * 0.4)
else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20)
sims = sorted( # ⚡ SE AGREGÓ 'en_borde' A LOS PARÁMETROS
[similitud_hibrida(firma_nueva, f) for f in firmas_guardadas], def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True):
reverse=True # 1. LIMPIEZA DE MEMORIA (El Recolector de Basura)
) # Esto asume que ya agregaste la función limpiar_fantasmas(self) a tu clase
self.limpiar_fantasmas()
if len(sims) == 1:
return sims[0]
elif len(sims) <= 4:
return (sims[0] * 0.6) + (sims[1] * 0.4)
else:
return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20)
def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids):
with self.lock: with self.lock:
best_gid, best_score = None, -1.0 candidatos = []
vecinos = VECINOS.get(str(cam_id), []) vecinos = VECINOS.get(str(cam_id), [])
for gid, data in self.db.items(): for gid, data in self.db.items():
@ -236,81 +263,138 @@ class GlobalMemory:
if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue
if not data['firmas']: continue if not data['firmas']: continue
sim = self._sim_robusta(firma_hibrida, data['firmas']) misma_cam = (str(data['last_cam']) == str(cam_id))
es_cross_cam = not misma_cam
misma_cam = str(data['last_cam']) == str(cam_id)
es_vecino = str(data['last_cam']) in vecinos es_vecino = str(data['last_cam']) in vecinos
# ⚡ FÍSICA DE PUERTAS
if es_vecino and not en_borde:
es_vecino = False
sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam)
if misma_cam: umbral = UMBRAL_REID_MISMA_CAM if misma_cam: umbral = UMBRAL_REID_MISMA_CAM
elif es_vecino: umbral = UMBRAL_REID_VECINO elif es_vecino: umbral = UMBRAL_REID_VECINO
else: umbral = UMBRAL_REID_NO_VECINO else: umbral = UMBRAL_REID_NO_VECINO
if sim > best_score and sim > umbral: # PROTECCIÓN VIP
best_score = sim if data.get('nombre') is not None:
best_gid = gid if misma_cam:
umbral += 0.04
if best_gid is not None:
self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now)
return best_gid, True
else: else:
umbral += 0.03
if sim > umbral:
# ⚡ MODIFICACIÓN: Guardamos también si es de la misma cam o vecino para el desempate final
candidatos.append((sim, gid, misma_cam, es_vecino))
if not candidatos:
nid = self.next_gid; self.next_gid += 1 nid = self.next_gid; self.next_gid += 1
self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now) self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now)
return nid, False return nid, False
# Ordena por la similitud (el primer elemento de la tupla) de mayor a menor
candidatos.sort(reverse=True)
best_sim, best_gid, best_misma, best_vecino = candidatos[0]
if len(candidatos) >= 2:
segunda_sim, segundo_gid, seg_misma, seg_vecino = candidatos[1]
margen = best_sim - segunda_sim
# Si las chamarras son casi idénticas...
if margen <= 0.02 and best_sim < 0.75:
# ⚖️ DESEMPATE GEOGRÁFICO INTELLIGENTE
# Le damos un "peso" matemático extra a la lógica espacial
peso_1 = best_sim + (0.10 if (best_misma or best_vecino) else 0.0)
peso_2 = segunda_sim + (0.10 if (seg_misma or seg_vecino) else 0.0)
if peso_1 > peso_2:
best_gid = best_gid # Gana el primero por lógica espacial
elif peso_2 > peso_1:
best_gid = segundo_gid # Gana el segundo por lógica espacial
else:
# Si empatan en TODO (ropa y espacio), entonces sí entramos en pánico seguro
print(f"\n[ ALERTA] Empate extremo entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna temporal.")
nid = self.next_gid; self.next_gid += 1
self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now)
return nid, False
self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now)
return best_gid, True
def _actualizar_sin_lock(self, gid, firma_dict, cam_id, now): def _actualizar_sin_lock(self, gid, firma_dict, cam_id, now):
if gid not in self.db: self.db[gid] = {'firmas': [], 'last_cam': cam_id, 'ts': now} if gid not in self.db: self.db[gid] = {'firmas': [], 'last_cam': cam_id, 'ts': now}
if firma_dict is not None: if firma_dict is not None:
firmas_list = self.db[gid]['firmas'] firmas_list = self.db[gid]['firmas']
if not firmas_list: if not firmas_list:
firmas_list.append(firma_dict) firmas_list.append(firma_dict)
else: else:
if firma_dict['calidad'] > (firmas_list[0]['calidad'] * 1.50): if firma_dict['calidad'] > (firmas_list[0]['calidad'] * 1.50):
vieja_ancla = firmas_list[0] vieja_ancla = firmas_list[0]; firmas_list[0] = firma_dict; firma_dict = vieja_ancla
firmas_list[0] = firma_dict
firma_dict = vieja_ancla
if len(firmas_list) >= MAX_FIRMAS_MEMORIA: if len(firmas_list) >= MAX_FIRMAS_MEMORIA:
max_sim_interna = -1.0 max_sim_interna = -1.0; idx_redundante = 1
idx_redundante = 1
for i in range(1, len(firmas_list)): for i in range(1, len(firmas_list)):
sims_con_otras = [ sims_con_otras = [similitud_hibrida(firmas_list[i], firmas_list[j]) for j in range(1, len(firmas_list)) if j != i]
similitud_hibrida(firmas_list[i], firmas_list[j])
for j in range(1, len(firmas_list)) if j != i
]
sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0 sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0
if sim_promedio > max_sim_interna: max_sim_interna = sim_promedio; idx_redundante = i
if sim_promedio > max_sim_interna:
max_sim_interna = sim_promedio
idx_redundante = i
firmas_list[idx_redundante] = firma_dict firmas_list[idx_redundante] = firma_dict
else: else:
firmas_list.append(firma_dict) firmas_list.append(firma_dict)
self.db[gid]['last_cam'] = cam_id self.db[gid]['last_cam'] = cam_id
self.db[gid]['ts'] = now self.db[gid]['ts'] = now
def actualizar(self, gid, firma, cam_id, now): def actualizar(self, gid, firma, cam_id, now):
with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now) with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now)
def limpiar_fantasmas(self):
""" Aniquila los IDs temporales que llevan más de 15 segundos sin verse """
with self.lock:
ahora = time.time()
ids_a_borrar = []
for gid, data in self.db.items():
tiempo_inactivo = ahora - data.get('ts', ahora)
# Si es un ID anónimo y lleva 15 segundos desaparecido, es basura
if data.get('nombre') is None and tiempo_inactivo > 600.0:
ids_a_borrar.append(gid)
# Si es un VIP (con nombre), le damos 5 minutos de memoria antes de borrarlo
elif data.get('nombre') is not None and tiempo_inactivo > 900.0:
ids_a_borrar.append(gid)
for gid in ids_a_borrar:
del self.db[gid]
def confirmar_firma_vip(self, gid, ts):
"""
ArcFace confirma la identidad con alta certeza (>0.55).
Aniquila las firmas viejas y deja solo la más reciente como la firma VIP definitiva.
"""
with self.lock:
if gid in self.db and self.db[gid]['firmas']:
# Tomamos la última firma que el hilo principal guardó (la ropa actual)
firma_actual_pura = self.db[gid]['firmas'][-1]
# Sobrescribimos el historial dejando solo esta firma confirmada
self.db[gid]['firmas'] = [firma_actual_pura]
self.db[gid]['ts'] = ts
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
# 4. GESTOR LOCAL # 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer)
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
def iou_overlap(boxA, boxB): def iou_overlap(boxA, boxB):
xA, yA, xB, yB = max(boxA[0], boxB[0]), max(boxA[1], boxB[1]), min(boxA[2], boxB[2]), min(boxA[3], boxB[3]) xA, yA, xB, yB = max(boxA[0], boxB[0]), max(boxA[1], boxB[1]), min(boxA[2], boxB[2]), min(boxA[3], boxB[3])
inter = max(0, xB-xA) * max(0, yB-yA) inter = max(0, xB-xA) * max(0, yB-yA)
areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1]) areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1]); areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1])
areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1])
return inter / (areaA + areaB - inter + 1e-6) return inter / (areaA + areaB - inter + 1e-6)
class CamManager: class CamManager:
def __init__(self, cam_id, global_mem): def __init__(self, cam_id, global_mem):
self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, [] self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, []
def update(self, boxes, frame, now, turno_activo): def update(self, boxes, frame_show, frame_hd, now, turno_activo):
for trk in self.trackers: trk.predict(turno_activo=turno_activo) for trk in self.trackers: trk.predict(turno_activo=turno_activo)
if not turno_activo: return self.trackers if not turno_activo: return self.trackers
@ -318,45 +402,56 @@ class CamManager:
for t_idx, d_idx in matched: for t_idx, d_idx in matched:
trk = self.trackers[t_idx]; box = boxes[d_idx] trk = self.trackers[t_idx]; box = boxes[d_idx]
en_grupo = any(other is not trk and iou_overlap(box, other.box) > 0.10 for other in self.trackers) en_grupo = any(other is not trk and iou_overlap(box, other.box) > 0.10 for other in self.trackers)
trk.update(box, en_grupo, now) trk.update(box, en_grupo, now)
active_gids = {t.gid for t in self.trackers if t.gid is not None} active_gids = {t.gid for t in self.trackers if t.gid is not None}
area_actual = (box[2] - box[0]) * (box[3] - box[1]) area_actual = (box[2] - box[0]) * (box[3] - box[1])
if trk.gid is None and trk.listo_para_id: # IGNORAMOS VECTORES MUTANTES DE GRUPOS
firma = extraer_firma_hibrida(frame, box) if trk.gid is None and trk.listo_para_id and not trk.en_grupo:
# ⚡ Usamos el frame_hd
firma = extraer_firma_hibrida(frame_hd, box)
if firma is not None: if firma is not None:
gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids) # ⚡ DETECCIÓN DE ZONA DE NACIMIENTO
fh, fw = frame_hd.shape[:2]
bx1, by1, bx2, by2 = map(int, box)
# Si nace a menos de 40 píxeles del margen, entró por el pasillo
nace_en_borde = (bx1 < 25 or by1 < 25 or bx2 > fw - 25 or by2 > fh - 25)
# Mandamos esa información al identificador
gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde)
trk.gid, trk.origen_global, trk.area_referencia = gid, es_reid, area_actual trk.gid, trk.origen_global, trk.area_referencia = gid, es_reid, area_actual
elif trk.gid is not None and not trk.en_grupo: elif trk.gid is not None and not trk.en_grupo:
tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0)
if (now - tiempo_ultima_firma) > 1.5 and analizar_calidad(box): # ⚡ APRENDIZAJE RÁPIDO: Bajamos de 1.5s a 0.5s para que llene la memoria volando
fh, fw = frame.shape[:2] if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box):
fh, fw = frame_hd.shape[:2]
x1, y1, x2, y2 = map(int, box) x1, y1, x2, y2 = map(int, box)
en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15) en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15)
if not en_borde: if not en_borde:
firma_nueva = extraer_firma_hibrida(frame, box) firma_nueva = extraer_firma_hibrida(frame_hd, box)
if firma_nueva is not None: if firma_nueva is not None:
with self.global_mem.lock: with self.global_mem.lock:
if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']:
firma_ancla = self.global_mem.db[trk.gid]['firmas'][0]
sim_coherencia = similitud_hibrida(firma_nueva, firma_ancla)
if sim_coherencia > 0.55: # ⚡ APRENDIZAJE EN CADENA: Comparamos contra la ÚLTIMA foto (-1), no contra la primera.
# Esto permite que el sistema "entienda" cuando te estás dando la vuelta o mostrando la mochila.
firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1]
sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente)
# Tolerancia relajada a 0.50 para permitir la transición de la espalda
if sim_coherencia > 0.50:
es_coherente = True es_coherente = True
for otro_gid, otro_data in self.global_mem.db.items(): for otro_gid, otro_data in self.global_mem.db.items():
if otro_gid == trk.gid or not otro_data['firmas']: continue if otro_gid == trk.gid or not otro_data['firmas']: continue
sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0]) sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0])
if sim_intruso > sim_coherencia: if sim_intruso > sim_coherencia:
es_coherente = False es_coherente = False
break break
if es_coherente: if es_coherente:
self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now) self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now)
trk.ultimo_aprendizaje = now trk.ultimo_aprendizaje = now
@ -365,16 +460,21 @@ class CamManager:
for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now)) for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now))
vivos = [] vivos = []
fh, fw = frame.shape[:2] fh, fw = frame_show.shape[:2] # Usamos frame_show para evaluar los bordes de la caja 480
for t in self.trackers: for t in self.trackers:
x1, y1, x2, y2 = t.box x1, y1, x2, y2 = t.box
toca_borde = (x1 < 5 or y1 < 5 or x2 > fw - 5 or y2 > fh - 5) toca_borde = (x1 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20)
tiempo_oculto = now - t.ts_ultima_deteccion tiempo_oculto = now - t.ts_ultima_deteccion
if toca_borde and tiempo_oculto > 1.0: # ⚡ MUERTE JUSTA: Si es anónimo en el borde, muere rápido.
continue # Si ya tiene un ID asignado, le damos al menos 3 segundos de gracia.
if t.gid is None and toca_borde:
limite_vida = 1.0
elif t.gid is not None and toca_borde:
limite_vida = 3.0
else:
limite_vida = 10.0
limite_vida = 5.0 if t.gid else 1.0
if tiempo_oculto < limite_vida: if tiempo_oculto < limite_vida:
vivos.append(t) vivos.append(t)
@ -382,47 +482,40 @@ class CamManager:
return self.trackers return self.trackers
def _asignar(self, boxes, now): def _asignar(self, boxes, now):
n_trk = len(self.trackers) n_trk = len(self.trackers); n_det = len(boxes)
n_det = len(boxes)
if n_trk == 0: return [], list(range(n_det)), [] if n_trk == 0: return [], list(range(n_det)), []
if n_det == 0: return [], [], list(range(n_trk)) if n_det == 0: return [], [], list(range(n_trk))
cost_mat = np.zeros((n_trk, n_det), dtype=np.float32) cost_mat = np.zeros((n_trk, n_det), dtype=np.float32)
TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035
for t, trk in enumerate(self.trackers): for t, trk in enumerate(self.trackers):
for d, det in enumerate(boxes): for d, det in enumerate(boxes):
iou = iou_overlap(trk.box, det) iou = iou_overlap(trk.box, det)
cx_t, cy_t = (trk.box[0]+trk.box[2])/2, (trk.box[1]+trk.box[3])/2 cx_t, cy_t = (trk.box[0]+trk.box[2])/2, (trk.box[1]+trk.box[3])/2
cx_d, cy_d = (det[0]+det[2])/2, (det[1]+det[3])/2 cx_d, cy_d = (det[0]+det[2])/2, (det[1]+det[3])/2
dist_norm = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) / 550.0 dist_norm = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2) / 550.0
area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1]) area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1])
area_det = (det[2] - det[0]) * (det[3] - det[1]) area_det = (det[2] - det[0]) * (det[3] - det[1])
ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6) ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6)
castigo_tam = (ratio_area - 1.0) * 0.7
castigo_tamano = (ratio_area - 1.0) * 0.4
tiempo_oculto = now - trk.ts_ultima_deteccion tiempo_oculto = now - trk.ts_ultima_deteccion
TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035
if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10: if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10:
fantasma_penalty = 5.0 fantasma_penalty = 5.0
else: else: fantasma_penalty = 0.0
fantasma_penalty = 0.0
if iou >= 0.05 or dist_norm < 0.50: if iou >= 0.05 or dist_norm < 0.80:
cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tamano cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tam
else: else: cost_mat[t, d] = 100.0
cost_mat[t, d] = 100.0
row_ind, col_ind = linear_sum_assignment(cost_mat) row_ind, col_ind = linear_sum_assignment(cost_mat)
matched, unmatched_dets, unmatched_trks = [], [], [] matched, unmatched_dets, unmatched_trks = [], [], []
for r, c in zip(row_ind, col_ind): for r, c in zip(row_ind, col_ind):
if cost_mat[r, c] > 4.0: # ⚡ CAJAS PEGAJOSAS: 6.0 evita que suelte el ID si te mueves rápido
if cost_mat[r, c] > 7.0:
unmatched_trks.append(r); unmatched_dets.append(c) unmatched_trks.append(r); unmatched_dets.append(c)
else: matched.append((r, c)) else: matched.append((r, c))
@ -446,11 +539,9 @@ class CamStream:
while True: while True:
ret, f = self.cap.read() ret, f = self.cap.read()
if ret: if ret:
self.frame = f self.frame = f; time.sleep(0.01)
time.sleep(0.01)
else: else:
time.sleep(2) time.sleep(2); self.cap.open(self.url)
self.cap.open(self.url)
def dibujar_track(frame_show, trk): def dibujar_track(frame_show, trk):
try: x1, y1, x2, y2 = map(int, trk.box) try: x1, y1, x2, y2 = map(int, trk.box)
@ -467,13 +558,8 @@ def dibujar_track(frame_show, trk):
cv2.rectangle(frame_show, (x1, y1-th-6), (x1+tw+2, y1), color, -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) cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1)
if trk.gid is None:
pct = min(trk.frames_buena_calidad / FRAMES_CALIDAD, 1.0)
bw = x2 - x1; cv2.rectangle(frame_show, (x1, y2+2), (x2, y2+7), (50,50,50), -1)
cv2.rectangle(frame_show, (x1, y2+2), (x1+int(bw*pct), y2+7), (0,220,220), -1)
def main(): def main():
print("Iniciando Sistema V-PRO — Tracker Resiliente (Rollback Estable)") print("Iniciando Sistema V-PRO — Tracker Resiliente (Código Unificado Maestro)")
model = YOLO("yolov8n.pt") model = YOLO("yolov8n.pt")
global_mem = GlobalMemory() global_mem = GlobalMemory()
managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA} managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA}
@ -490,9 +576,9 @@ def main():
if frame is None: tiles.append(np.zeros((270, 480, 3), np.uint8)); continue if frame is None: tiles.append(np.zeros((270, 480, 3), np.uint8)); continue
frame_show = cv2.resize(frame.copy(), (480, 270)); boxes = []; turno_activo = (i == cam_ia) frame_show = cv2.resize(frame.copy(), (480, 270)); boxes = []; turno_activo = (i == cam_ia)
if turno_activo: if turno_activo:
res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480) res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu')
if res[0].boxes: boxes = res[0].boxes.xyxy.cpu().numpy().tolist() if res[0].boxes: boxes = res[0].boxes.xyxy.cpu().numpy().tolist()
tracks = managers[cid].update(boxes, frame_show, now, turno_activo) tracks = managers[cid].update(boxes, frame_show, frame, now, turno_activo)
for trk in tracks: for trk in tracks:
if trk.time_since_update <= 1: dibujar_track(frame_show, trk) if trk.time_since_update <= 1: dibujar_track(frame_show, trk)
if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1) if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1)

1258
vesiones_seguras.txt Normal file

File diff suppressed because it is too large Load Diff

BIN
video.mp4 Normal file

Binary file not shown.