Actualizacion del modelo para la deteccion del rostro, modificacion en la identificacion del genero con ello el mensaje que de recibimiento, modificacion de umbrales para el seguimineto, aplicacion de fusion de IDs similares al detectar el rostro, guardar versiones seguras
This commit is contained in:
parent
d2e90d9c50
commit
6bc9a5cb44
Binary file not shown.
@ -1 +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"}
|
{"Emanuel Flores": "Man", "Vikicar Aldana": "Woman", "Rodrigo Cahuantzi C": "Man", "Cristian Hernandez Suarez": "Man", "Omar": "Man", "Oscar Atriano Ponce_1": "Man", "Miguel Angel": "Man", "Carlos Eduardo Cuamatzi": "Man", "Rosa maria": "Woman", "Ximena": "Woman", "Ana Karen Guerrero": "Woman", "Yuriel": "Man", "Diana Laura": "Woman", "Diana Laura Tecpa": "Woman", "aridai montiel zistecatl": "Woman", "Aridai montiel": "Woman", "Vikicar": "Woman", "Ian Axel": "Man", "Rafael": "Man", "Rubisela Barrientos": "Woman", "ian axel": "Man", "Adriana Lopez": "Woman", "Oscar Atriano Ponce": "Man", "Xayli Ximena": "Woman", "Victor Manuel Ocampo Mendez": "Man", "Victor": "Man"}
|
||||||
BIN
cache_nombres/nombre_Adriana Lopez.mp3
Normal file
BIN
cache_nombres/nombre_Adriana Lopez.mp3
Normal file
Binary file not shown.
BIN
cache_nombres/nombre_Ana Karen Guerrero.mp3
Normal file
BIN
cache_nombres/nombre_Ana Karen Guerrero.mp3
Normal file
Binary file not shown.
BIN
cache_nombres/nombre_Oscar Atriano Ponce.mp3
Normal file
BIN
cache_nombres/nombre_Oscar Atriano Ponce.mp3
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
BIN
db_institucion/Victor.jpg
Normal file
BIN
db_institucion/Victor.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
420
fision1.py
Normal file
420
fision1.py
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de 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
|
||||||
|
import queue
|
||||||
|
from queue import Queue
|
||||||
|
from ultralytics import YOLO
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 1. IMPORTAMOS NUESTROS MÓDULOS
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Del motor matemático y tracking
|
||||||
|
from seguimiento2 import GlobalMemory, CamManager, SECUENCIA, URLS, FUENTE, similitud_hibrida
|
||||||
|
|
||||||
|
# ⚡ Del motor de reconocimiento facial
|
||||||
|
from reconocimiento import (
|
||||||
|
app,
|
||||||
|
gestionar_vectores,
|
||||||
|
buscar_mejor_match,
|
||||||
|
hilo_bienvenida,
|
||||||
|
UMBRAL_SIM,
|
||||||
|
COOLDOWN_TIME
|
||||||
|
)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
COLA_ROSTROS = Queue(maxsize=4)
|
||||||
|
IA_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
print("\nIniciando carga de base de datos...")
|
||||||
|
BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 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 = face.bbox[2] - face.bbox[0]
|
||||||
|
h_rostro = face.bbox[3] - face.bbox[1]
|
||||||
|
|
||||||
|
# ⚡ Límite bajo (20px) para reconocer desde más lejos
|
||||||
|
if w_rostro < 20 or h_rostro < 20 or (w_rostro / h_rostro) < 0.35:
|
||||||
|
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)
|
||||||
|
print(f"[DEBUG CAM {cam_id}] InsightFace: {mejor_match} al {max_sim:.2f}")
|
||||||
|
|
||||||
|
votos_finales = 0
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
# ⚡ LÓGICA DE VOTACIÓN PONDERADA INYECTADA
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
# Aceptamos rostros de CCTV desde 0.38 (ajustado según tu código a 0.35)
|
||||||
|
if max_sim >= 0.35 and mejor_match:
|
||||||
|
nombre_limpio = mejor_match.split('_')[0]
|
||||||
|
|
||||||
|
# Variable para rastrear quién es el ID definitivo al final del proceso
|
||||||
|
id_definitivo = gid
|
||||||
|
|
||||||
|
with global_mem.lock:
|
||||||
|
datos_id = global_mem.db.get(gid)
|
||||||
|
if not datos_id: return
|
||||||
|
|
||||||
|
if datos_id.get('candidato_nombre') == nombre_limpio:
|
||||||
|
datos_id['votos_nombre'] = datos_id.get('votos_nombre', 0) + 1
|
||||||
|
else:
|
||||||
|
datos_id['candidato_nombre'] = nombre_limpio
|
||||||
|
datos_id['votos_nombre'] = 1
|
||||||
|
|
||||||
|
# Sistema de Puntos
|
||||||
|
if max_sim >= 0.50:
|
||||||
|
datos_id['votos_nombre'] += 3 # Pase VIP por buena foto
|
||||||
|
elif max_sim >= 0.40:
|
||||||
|
datos_id['votos_nombre'] += 2 # Voto extra por foto decente
|
||||||
|
|
||||||
|
votos_finales = datos_id['votos_nombre']
|
||||||
|
|
||||||
|
# ⚡ Exigimos 3 votos (según tu condicional)
|
||||||
|
if votos_finales >= 3:
|
||||||
|
nombre_actual = datos_id.get('nombre')
|
||||||
|
|
||||||
|
# 1. Buscamos si ese nombre ya lo tiene un veterano en la memoria
|
||||||
|
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
|
||||||
|
|
||||||
|
# 2. FUSIÓN MÁGICA: Si ya existe, lo fusionamos
|
||||||
|
if id_veterano is not None:
|
||||||
|
print(f"[FUSIÓN FACIAL] ID {gid} es el clon de la espalda. Fusionando con el veterano ID {id_veterano} ({nombre_limpio}).")
|
||||||
|
|
||||||
|
# Pasamos la memoria de ropa al veterano
|
||||||
|
global_mem.db[id_veterano]['firmas'].extend(datos_id.get('firmas', []))
|
||||||
|
if len(global_mem.db[id_veterano]['firmas']) > 15:
|
||||||
|
global_mem.db[id_veterano]['firmas'] = global_mem.db[id_veterano]['firmas'][-15:]
|
||||||
|
|
||||||
|
# Vaciamos al perdedor y redireccionamos
|
||||||
|
datos_id['firmas'] = []
|
||||||
|
datos_id['fusionado_con'] = id_veterano
|
||||||
|
|
||||||
|
# Actualizamos el tracker y el ID definitivo
|
||||||
|
trk.gid = id_veterano
|
||||||
|
id_definitivo = id_veterano
|
||||||
|
|
||||||
|
# 3. BAUTIZO NORMAL: Si no hay clones, procedemos con tu lógica de protección VIP
|
||||||
|
else:
|
||||||
|
if nombre_actual is not None and nombre_actual != nombre_limpio:
|
||||||
|
# Si ya tiene nombre y quieren robárselo, exigimos 0.56 mínimo
|
||||||
|
if max_sim < 0.56:
|
||||||
|
print(f"[RECHAZO VIP] {nombre_actual} protegido de {nombre_limpio} ({max_sim:.2f})")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(f" [CORRECCIÓN VIP] Renombrando a {nombre_limpio} ({max_sim:.2f})")
|
||||||
|
|
||||||
|
if nombre_actual != nombre_limpio:
|
||||||
|
datos_id['nombre'] = nombre_limpio
|
||||||
|
print(f"[BAUTIZO] ID {gid} confirmado como {nombre_limpio}")
|
||||||
|
|
||||||
|
# Actualizamos el tiempo de vida del ID ganador
|
||||||
|
global_mem.db[id_definitivo]['ts'] = time.time()
|
||||||
|
|
||||||
|
# 🔊 AUDIO DE BIENVENIDA (Funciona tanto para bautizos como para fusiones)
|
||||||
|
if str(cam_id) == "7":
|
||||||
|
if not hasattr(global_mem, 'ultimos_saludos'):
|
||||||
|
global_mem.ultimos_saludos = {}
|
||||||
|
ultimo = global_mem.ultimos_saludos.get(nombre_limpio, 0)
|
||||||
|
|
||||||
|
# Aquí asumo que tienes COOLDOWN_TIME definido globalmente en tu archivo
|
||||||
|
if (time.time() - ultimo) > COOLDOWN_TIME:
|
||||||
|
global_mem.ultimos_saludos[nombre_limpio] = time.time()
|
||||||
|
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero_detectado), daemon=True).start()
|
||||||
|
|
||||||
|
# Blindaje OSNet fuera del lock (Usando el id_definitivo por si hubo fusión)
|
||||||
|
if max_sim > 0.50 and votos_finales >= 3:
|
||||||
|
global_mem.confirmar_firma_vip(id_definitivo, time.time())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error en InsightFace asíncrono: {e}")
|
||||||
|
finally:
|
||||||
|
# ⚡ EL LIBERADOR (Vital para que el tracker no se quede bloqueado)
|
||||||
|
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. LOOP PRINCIPAL DE FUSIÓN
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
class CamStream:
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
self.cap = cv2.VideoCapture(url)
|
||||||
|
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||||
|
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
|
||||||
|
time.sleep(0.01)
|
||||||
|
else:
|
||||||
|
time.sleep(2)
|
||||||
|
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():
|
||||||
|
print("\nIniciando Sistema")
|
||||||
|
model = YOLO("yolov8n.pt")
|
||||||
|
global_mem = GlobalMemory()
|
||||||
|
managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA}
|
||||||
|
cams = [CamStream(u) for u in URLS]
|
||||||
|
|
||||||
|
threading.Thread(target=worker_rostros, args=(global_mem,), daemon=True).start()
|
||||||
|
|
||||||
|
cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE)
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.time()
|
||||||
|
tiles = []
|
||||||
|
cam_ia = idx % len(cams)
|
||||||
|
|
||||||
|
for i, cam_obj in enumerate(cams):
|
||||||
|
frame = cam_obj.frame; cid = str(SECUENCIA[i])
|
||||||
|
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)
|
||||||
|
|
||||||
|
if turno_activo:
|
||||||
|
# ⚡ 1. Subimos la confianza de YOLO de 0.50 a 0.60 para matar siluetas dudosas
|
||||||
|
res = model.predict(frame_show, conf=0.60, iou=0.50, classes=[0], verbose=False, imgsz=480)
|
||||||
|
|
||||||
|
if res[0].boxes:
|
||||||
|
cajas_crudas = res[0].boxes.xyxy.cpu().numpy().tolist()
|
||||||
|
|
||||||
|
# ⚡ 2. FILTRO BIOMECÁNICO (Anti-Sillas)
|
||||||
|
for b in cajas_crudas:
|
||||||
|
w_caja = b[2] - b[0]
|
||||||
|
h_caja = b[3] - b[1]
|
||||||
|
|
||||||
|
# Un humano real es al menos 10% más alto que ancho (ratio > 1.1)
|
||||||
|
# Si la caja es muy cuadrada o ancha, la ignoramos y no se la pasamos a Kalman
|
||||||
|
if h_caja > (w_caja * 1.1):
|
||||||
|
boxes.append(b)
|
||||||
|
|
||||||
|
# ⚡ UPDATE LIMPIO (Sin IDs Globales)
|
||||||
|
tracks = managers[cid].update(boxes, 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)
|
||||||
|
if votos_actuales >= 2:
|
||||||
|
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()
|
||||||
|
|
||||||
|
# ⚡ COMPUERTA ÓPTICA (Filtro Anti-Tartamudeo 25x25)
|
||||||
|
if roi_recortado.size > 0 and roi_recortado.shape[0] >= 25 and roi_recortado.shape[1] >= 25:
|
||||||
|
gray = cv2.cvtColor(roi_recortado, cv2.COLOR_BGR2GRAY)
|
||||||
|
nitidez = cv2.Laplacian(gray, cv2.CV_64F).var()
|
||||||
|
|
||||||
|
# ⚡ BAJAMOS DE 12.0 a 5.0.
|
||||||
|
# Deja pasar rostros un poco borrosos por la velocidad de caminar,
|
||||||
|
# pero sigue bloqueando espaldas lisas.
|
||||||
|
if nitidez > 8.0:
|
||||||
|
if not COLA_ROSTROS.full():
|
||||||
|
ultimo_envio = getattr(trk, 'ultimo_rostro_enviado', 0)
|
||||||
|
|
||||||
|
# Solo mandamos la cara si ha pasado medio segundo desde la última vez que lo intentamos
|
||||||
|
if (now - ultimo_envio) > 0.5:
|
||||||
|
|
||||||
|
# Tu escudo anti-lag previo
|
||||||
|
if COLA_ROSTROS.qsize() < 2:
|
||||||
|
trk.ultimo_rostro_enviado = now # Registramos la hora del envío
|
||||||
|
trk.procesando_rostro = True # Bloqueamos el tracker
|
||||||
|
|
||||||
|
# Aquí pones TU línea original de la cola:
|
||||||
|
COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False)
|
||||||
|
else:
|
||||||
|
trk.ultimo_intento_cara = time.time() - 0.5
|
||||||
|
|
||||||
|
"""if nitidez > 8.0:
|
||||||
|
|
||||||
|
if not COLA_ROSTROS.full():
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if COLA_ROSTROS.qsize() < 2:
|
||||||
|
|
||||||
|
COLA_ROSTROS.put((roi_recortado, trk.gid, cid, trk), block=False)
|
||||||
|
|
||||||
|
trk.procesando_rostro = True
|
||||||
|
|
||||||
|
trk.ultimo_intento_cara = time.time()
|
||||||
|
|
||||||
|
except queue.Full:
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
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)
|
||||||
|
tiles.append(frame_show)
|
||||||
|
|
||||||
|
if len(tiles) == 6:
|
||||||
|
cv2.imshow("SmartSoft Fusion", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])]))
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
key = cv2.waitKey(1) & 0xFF
|
||||||
|
|
||||||
|
if key == ord('q'):
|
||||||
|
break
|
||||||
|
|
||||||
|
elif key == ord('r'):
|
||||||
|
print("\n[MODO REGISTRO] Escaneando mosaico para registrar...")
|
||||||
|
mejor_roi = None
|
||||||
|
max_area = 0
|
||||||
|
face_metadata = None
|
||||||
|
|
||||||
|
for i, cam_obj in enumerate(cams):
|
||||||
|
if cam_obj.frame is None: continue
|
||||||
|
|
||||||
|
faces = app.get(cam_obj.frame)
|
||||||
|
for face in faces:
|
||||||
|
fw = face.bbox[2] - face.bbox[0]
|
||||||
|
fh = face.bbox[3] - face.bbox[1]
|
||||||
|
area = fw * fh
|
||||||
|
if area > max_area:
|
||||||
|
max_area = area
|
||||||
|
h_frame, w_frame = cam_obj.frame.shape[:2]
|
||||||
|
|
||||||
|
m_x, m_y = int(fw * 0.30), int(fh * 0.30)
|
||||||
|
y1 = max(0, int(face.bbox[1]) - m_y)
|
||||||
|
y2 = min(h_frame, int(face.bbox[3]) + m_y)
|
||||||
|
x1 = max(0, int(face.bbox[0]) - m_x)
|
||||||
|
x2 = min(w_frame, int(face.bbox[2]) + m_x)
|
||||||
|
|
||||||
|
mejor_roi = cam_obj.frame[y1:y2, x1:x2]
|
||||||
|
face_metadata = face
|
||||||
|
|
||||||
|
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
|
||||||
|
genero_guardado = "Man" if face_metadata.sex == "M" else "Woman"
|
||||||
|
|
||||||
|
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
|
||||||
|
with open(ruta_generos, 'w') as f:
|
||||||
|
json.dump(dic_generos, f)
|
||||||
|
|
||||||
|
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 guardado (Género autodetectado: {genero_guardado})")
|
||||||
|
print(" Sincronizando base de datos en caliente...")
|
||||||
|
|
||||||
|
nuevos_vectores = gestionar_vectores(actualizar=True)
|
||||||
|
with IA_LOCK:
|
||||||
|
BASE_DATOS_ROSTROS.clear()
|
||||||
|
BASE_DATOS_ROSTROS.update(nuevos_vectores)
|
||||||
|
print(" Sistema listo.")
|
||||||
|
else:
|
||||||
|
print("[!] Registro cancelado.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
fusion.py
10
fusion.py
@ -73,7 +73,7 @@ def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
|
|||||||
roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd]
|
roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd]
|
||||||
|
|
||||||
# ⚡ Filtro físico relajado a 40x40
|
# ⚡ Filtro físico relajado a 40x40
|
||||||
if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 40 or roi_cabeza.shape[1] < 40:
|
if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 20 or roi_cabeza.shape[1] < 20:
|
||||||
return
|
return
|
||||||
|
|
||||||
h_roi, w_roi = roi_cabeza.shape[:2]
|
h_roi, w_roi = roi_cabeza.shape[:2]
|
||||||
@ -110,12 +110,12 @@ def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
|
|||||||
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)]
|
||||||
|
|
||||||
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] < 20 or roi_rostro.shape[1] < 20:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 🛡️ FILTRO ANTI-PERFIL: Evita falsos positivos de personas viendo de lado
|
# 🛡️ FILTRO ANTI-PERFIL: Evita falsos positivos de personas viendo de lado
|
||||||
ratio_aspecto = roi_rostro.shape[1] / float(roi_rostro.shape[0])
|
ratio_aspecto = roi_rostro.shape[1] / float(roi_rostro.shape[0])
|
||||||
if ratio_aspecto < 0.60:
|
if ratio_aspecto < 0.50:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 🛡️ FILTRO ÓPTICO (Movimiento)
|
# 🛡️ FILTRO ÓPTICO (Movimiento)
|
||||||
@ -168,7 +168,7 @@ def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
|
|||||||
datos_id['votos_nombre'] = 1
|
datos_id['votos_nombre'] = 1
|
||||||
|
|
||||||
# ⚡ EL PASE VIP: Si la certeza es aplastante (>0.55), salta la burocracia
|
# ⚡ EL PASE VIP: Si la certeza es aplastante (>0.55), salta la burocracia
|
||||||
if max_sim >= 0.45:
|
if max_sim >= 0.50:
|
||||||
datos_id['votos_nombre'] = max(2, datos_id['votos_nombre'])
|
datos_id['votos_nombre'] = max(2, datos_id['votos_nombre'])
|
||||||
|
|
||||||
# Solo actuamos si tiene 2 votos consistentes...
|
# Solo actuamos si tiene 2 votos consistentes...
|
||||||
@ -177,7 +177,7 @@ def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
|
|||||||
|
|
||||||
# CANDADO DE BAUTIZO: Protege a los VIPs de alucinaciones borrosas
|
# CANDADO DE BAUTIZO: Protege a los VIPs de alucinaciones borrosas
|
||||||
if nombre_actual is not None and nombre_actual != nombre_limpio:
|
if nombre_actual is not None and nombre_actual != nombre_limpio:
|
||||||
if max_sim < 0.55:
|
if max_sim < 0.59:
|
||||||
# Si es un puntaje bajo, es una confusión de ArcFace. Lo ignoramos.
|
# 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}")
|
print(f" [RECHAZO] ArcFace intentó renombrar a {nombre_actual} como {nombre_limpio} con solo {max_sim:.2f}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
378
reconocimiento.py
Normal file
378
reconocimiento.py
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import os
|
||||||
|
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||||
|
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
||||||
|
|
||||||
|
# ⚡ NUEVOS CANDADOS: Silenciamos la burocracia de OpenCV y Qt
|
||||||
|
os.environ['QT_LOGGING_RULES'] = '*=false'
|
||||||
|
os.environ['OPENCV_LOG_LEVEL'] = 'ERROR'
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
import edge_tts
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
import warnings
|
||||||
|
import json
|
||||||
|
|
||||||
|
# ⚡ IMPORTACIÓN DE INSIGHTFACE
|
||||||
|
from insightface.app import FaceAnalysis
|
||||||
|
|
||||||
|
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.45 # ⚡ Ajustado a la exigencia de producción
|
||||||
|
COOLDOWN_TIME = 15 # Segundos entre saludos
|
||||||
|
|
||||||
|
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.200"
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# EL NUEVO CEREBRO: INSIGHTFACE (buffalo_l)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
print("Cargando motor InsightFace (buffalo_l)...")
|
||||||
|
# Carga detección (SCRFD), landmarks, reconocimiento (ArcFace) y atributos (género/edad)
|
||||||
|
app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider'])
|
||||||
|
# det_size a 320x320 es un balance perfecto entre velocidad y capacidad de detectar rostros lejanos
|
||||||
|
app.prepare(ctx_id=0, det_size=(320, 320))
|
||||||
|
print("Motor cargado exitosamente.")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 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 INSIGHTFACE)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
def gestionar_vectores(actualizar=False):
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
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 (modelo 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
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ⚡ INSIGHTFACE: Extracción primaria
|
||||||
|
faces = app.get(img_db)
|
||||||
|
|
||||||
|
# HACK DEL LIENZO NEGRO: Si la foto está muy recortada y no ve la cara,
|
||||||
|
# le agregamos un borde negro gigante para engañar al detector
|
||||||
|
if len(faces) == 0:
|
||||||
|
h_img, w_img = img_db.shape[:2]
|
||||||
|
img_pad = cv2.copyMakeBorder(img_db, h_img, h_img, w_img, w_img, cv2.BORDER_CONSTANT, value=(0,0,0))
|
||||||
|
faces = app.get(img_pad)
|
||||||
|
|
||||||
|
if len(faces) == 0:
|
||||||
|
print(f" [!] Rostro irrecuperable en '{archivo}'.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
face = max(faces, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1]))
|
||||||
|
emb = np.array(face.normed_embedding, dtype=np.float32)
|
||||||
|
|
||||||
|
# ⚡ CORRECCIÓN DE GÉNERO: InsightFace devuelve "M" o "F"
|
||||||
|
if falta_genero:
|
||||||
|
dic_generos[nombre_archivo] = "Man" if face.sex == "M" else "Woman"
|
||||||
|
cambio_generos = True
|
||||||
|
|
||||||
|
vectores_actuales[nombre_archivo] = emb
|
||||||
|
timestamps[nombre_archivo] = ts_actual
|
||||||
|
hubo_cambios = True
|
||||||
|
print(f" Procesado: {nombre_archivo} | Género: {dic_generos.get(nombre_archivo)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error procesando '{archivo}': {e}")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
def buscar_mejor_match(emb_consulta, base_datos):
|
||||||
|
# InsightFace devuelve normed_embedding, por lo que la magnitud ya es 1
|
||||||
|
mejor_match, max_sim = None, -1.0
|
||||||
|
for nombre, vec in base_datos.items():
|
||||||
|
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
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
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 - INSIGHTFACE (buffalo_l)")
|
||||||
|
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()
|
||||||
|
|
||||||
|
# ⚡ INSIGHTFACE: Hace TODO aquí (Detección, Landmarks, Género, ArcFace)
|
||||||
|
faces_raw = app.get(frame)
|
||||||
|
faces_ultimo_frame = faces_raw
|
||||||
|
|
||||||
|
for face in faces_raw:
|
||||||
|
# Las cajas de InsightFace vienen en formato [x1, y1, x2, y2]
|
||||||
|
box = face.bbox.astype(int)
|
||||||
|
fx, fy, x2, y2 = box[0], box[1], box[2], box[3]
|
||||||
|
fw, fh = x2 - fx, y2 - fy
|
||||||
|
|
||||||
|
# Limitamos a los bordes de la pantalla
|
||||||
|
fx, fy = max(0, fx), max(0, fy)
|
||||||
|
fw, fh = min(w - fx, fw), 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"DET:{face.det_score:.2f}",
|
||||||
|
(fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1)
|
||||||
|
|
||||||
|
if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# FILTRO DE TAMAÑO (Para evitar reconocer píxeles lejanos)
|
||||||
|
if fw < 20 or fh < 20:
|
||||||
|
cv2.putText(display_frame, "muy pequeno",
|
||||||
|
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SIMETRÍA MATEMÁTICA INMEDIATA
|
||||||
|
emb = np.array(face.normed_embedding, dtype=np.float32)
|
||||||
|
mejor_match, max_sim = buscar_mejor_match(emb, base_datos)
|
||||||
|
|
||||||
|
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 >= 2:
|
||||||
|
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3)
|
||||||
|
|
||||||
|
# ⚡ GÉNERO INMEDIATO SIN SOBRECARGA DE CPU
|
||||||
|
genero = "Man" if face.sex == 1 else "Woman"
|
||||||
|
|
||||||
|
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:
|
||||||
|
# Tomamos la cara más grande detectada por InsightFace
|
||||||
|
face_registro = max(faces_ultimo_frame, key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1]))
|
||||||
|
box = face_registro.bbox.astype(int)
|
||||||
|
fx, fy, fw, fh = box[0], box[1], box[2]-box[0], box[3]-box[1]
|
||||||
|
|
||||||
|
# Margen estético para guardar la foto
|
||||||
|
m_x, m_y = int(fw * 0.30), int(fh * 0.30)
|
||||||
|
y1, y2 = max(0, fy-m_y), min(h, fy+fh+m_y)
|
||||||
|
x1, x2 = max(0, fx-m_x), min(w, fx+fw+m_x)
|
||||||
|
|
||||||
|
face_roi = frame[y1:y2, x1:x2]
|
||||||
|
|
||||||
|
if face_roi.size > 0:
|
||||||
|
nom = input("\nNombre de la persona: ").strip()
|
||||||
|
if nom:
|
||||||
|
# Extraemos el género automáticamente gracias a InsightFace
|
||||||
|
gen_str = "Man" if face_registro.sex == 1 else "Woman"
|
||||||
|
|
||||||
|
# Actualizamos JSON directo sin preguntar
|
||||||
|
ruta_generos = os.path.join(CACHE_PATH, "generos.json")
|
||||||
|
dic_generos = {}
|
||||||
|
if os.path.exists(ruta_generos):
|
||||||
|
with open(ruta_generos, 'r') as f:
|
||||||
|
dic_generos = json.load(f)
|
||||||
|
dic_generos[nom] = gen_str
|
||||||
|
with open(ruta_generos, 'w') as f:
|
||||||
|
json.dump(dic_generos, f)
|
||||||
|
|
||||||
|
# Guardado de imagen y sincronización
|
||||||
|
foto_path = os.path.join(DB_PATH, f"{nom}.jpg")
|
||||||
|
cv2.imwrite(foto_path, face_roi)
|
||||||
|
print(f"[OK] Rostro de '{nom}' guardado como {gen_str}. 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()
|
||||||
@ -24,7 +24,7 @@ DB_PATH = "db_institucion"
|
|||||||
CACHE_PATH = "cache_nombres"
|
CACHE_PATH = "cache_nombres"
|
||||||
VECTORS_FILE = "base_datos_rostros.pkl"
|
VECTORS_FILE = "base_datos_rostros.pkl"
|
||||||
TIMESTAMPS_FILE = "representaciones_timestamps.pkl"
|
TIMESTAMPS_FILE = "representaciones_timestamps.pkl"
|
||||||
UMBRAL_SIM = 0.39 # Por encima → identificado. Por debajo → desconocido.
|
UMBRAL_SIM = 0.42 # 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.244"
|
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
|
||||||
@ -402,7 +402,7 @@ def sistema_interactivo():
|
|||||||
else:
|
else:
|
||||||
persona_actual, confirmaciones = mejor_match, 1
|
persona_actual, confirmaciones = mejor_match, 1
|
||||||
|
|
||||||
if confirmaciones >= 2:
|
if confirmaciones >= 1:
|
||||||
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(
|
||||||
|
|||||||
Binary file not shown.
467
seguimiento2.py
467
seguimiento2.py
@ -13,7 +13,7 @@ import os
|
|||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
|
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
|
||||||
SECUENCIA = [1, 7, 5, 8, 3, 6]
|
SECUENCIA = [1, 7, 5, 8, 3, 6]
|
||||||
# 🛡️ RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg)
|
# RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg)
|
||||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000"
|
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"
|
||||||
@ -23,15 +23,12 @@ VECINOS = {
|
|||||||
"8": ["5", "3"], "3": ["8", "6"], "6": ["3"]
|
"8": ["5", "3"], "3": ["8", "6"], "6": ["3"]
|
||||||
}
|
}
|
||||||
|
|
||||||
ASPECT_RATIO_MIN = 0.5
|
ASPECT_RATIO_MIN = 0.6
|
||||||
ASPECT_RATIO_MAX = 4.0
|
ASPECT_RATIO_MAX = 4.0
|
||||||
AREA_MIN_CALIDAD = 1200
|
AREA_MIN_CALIDAD = 1200
|
||||||
FRAMES_CALIDAD = 2
|
FRAMES_CALIDAD = 3
|
||||||
TIEMPO_MAX_AUSENCIA = 800.0
|
TIEMPO_MAX_AUSENCIA = 800.0
|
||||||
|
|
||||||
UMBRAL_REID_MISMA_CAM = 0.53 # Antes 0.65
|
|
||||||
UMBRAL_REID_VECINO = 0.48 # Antes 0.53
|
|
||||||
UMBRAL_REID_NO_VECINO = 0.67 # Antes 0.72
|
|
||||||
MAX_FIRMAS_MEMORIA = 10
|
MAX_FIRMAS_MEMORIA = 10
|
||||||
|
|
||||||
C_CANDIDATO = (150, 150, 150)
|
C_CANDIDATO = (150, 150, 150)
|
||||||
@ -66,12 +63,23 @@ def analizar_calidad(box):
|
|||||||
return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD)
|
return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD)
|
||||||
|
|
||||||
def preprocess_onnx(roi):
|
def preprocess_onnx(roi):
|
||||||
img = cv2.resize(roi, (128, 256))
|
# ⚡ ESCUDO ANTI-SOMBRAS (CLAHE)
|
||||||
|
# Convertimos a LAB para ecualizar solo la luz (L) sin deformar los colores reales (A y B)
|
||||||
|
lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB)
|
||||||
|
l, a, b = cv2.split(lab)
|
||||||
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
|
||||||
|
l_eq = clahe.apply(l)
|
||||||
|
lab_eq = cv2.merge((l_eq, a, b))
|
||||||
|
roi_eq = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR)
|
||||||
|
|
||||||
|
# Preprocesamiento original para OSNet
|
||||||
|
img = cv2.resize(roi_eq, (128, 256))
|
||||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||||
img = img.transpose(2, 0, 1).astype(np.float32) / 255.0
|
img = img.transpose(2,0,1).astype(np.float32) / 255.0
|
||||||
img = np.expand_dims(img, axis=0)
|
|
||||||
img = (img - MEAN) / STD
|
img = (img - MEAN) / STD
|
||||||
return img
|
|
||||||
|
# Devolvemos el tensor correcto de 1 sola imagen
|
||||||
|
return np.expand_dims(img, axis=0)
|
||||||
|
|
||||||
def extraer_color_zonas(img):
|
def extraer_color_zonas(img):
|
||||||
h_roi = img.shape[0]
|
h_roi = img.shape[0]
|
||||||
@ -115,10 +123,17 @@ def extraer_firma_hibrida(frame_hd, box_480):
|
|||||||
|
|
||||||
calidad_area = (x2_c - x1_c) * (y2_c - y1_c)
|
calidad_area = (x2_c - x1_c) * (y2_c - y1_c)
|
||||||
|
|
||||||
blob = preprocess_onnx(roi)
|
# ⚡ VOLVEMOS AL BATCH DE 16 PARA EVITAR EL CRASH FATAL
|
||||||
|
blob = preprocess_onnx(roi) # Esto devuelve (1, 3, 256, 128)
|
||||||
|
|
||||||
|
# Creamos el contenedor de 16 espacios que el modelo ONNX exige
|
||||||
blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32)
|
blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32)
|
||||||
blob_16[0] = blob[0]
|
blob_16[0] = blob[0] # Metemos nuestra imagen en el primer espacio
|
||||||
deep_feat = ort_session.run(None, {input_name: blob_16})[0][0].flatten()
|
|
||||||
|
# Ejecutamos la inferencia
|
||||||
|
outputs = ort_session.run(None, {input_name: blob_16})
|
||||||
|
deep_feat = outputs[0][0].flatten() # Extraemos solo el primer resultado
|
||||||
|
|
||||||
norma = np.linalg.norm(deep_feat)
|
norma = np.linalg.norm(deep_feat)
|
||||||
if norma > 0: deep_feat = deep_feat / norma
|
if norma > 0: deep_feat = deep_feat / norma
|
||||||
|
|
||||||
@ -126,17 +141,15 @@ def extraer_firma_hibrida(frame_hd, box_480):
|
|||||||
textura_feat = extraer_textura_rapida(roi)
|
textura_feat = extraer_textura_rapida(roi)
|
||||||
|
|
||||||
return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area}
|
return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
print(f"Error en extracción: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ⚡ 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):
|
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)))
|
||||||
@ -146,14 +159,10 @@ def similitud_hibrida(f1, f2, cross_cam=False):
|
|||||||
sim_head, sim_torso, sim_legs = 0.0, 0.0, 0.0
|
sim_head, sim_torso, sim_legs = 0.0, 0.0, 0.0
|
||||||
|
|
||||||
if cross_cam:
|
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:
|
if sim_legs < 0.25:
|
||||||
sim_deep -= 0.15 # Castigo letal: Le tumbamos 15 puntos porcentuales a OSNet
|
sim_deep -= 0.15
|
||||||
|
|
||||||
return max(0.0, sim_deep)
|
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)
|
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:
|
if 'textura' in f1 and 'textura' in f2 and f1['textura'].size > 1:
|
||||||
@ -172,7 +181,7 @@ class KalmanTrack:
|
|||||||
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)
|
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)
|
||||||
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.05
|
||||||
self.kf.statePost = np.zeros((7, 1), np.float32)
|
self.kf.statePost = np.zeros((7, 1), np.float32)
|
||||||
self.kf.statePost[:4] = self._convert_bbox_to_z(box)
|
self.kf.statePost[:4] = self._convert_bbox_to_z(box)
|
||||||
self.local_id = KalmanTrack._count
|
self.local_id = KalmanTrack._count
|
||||||
@ -188,6 +197,9 @@ class KalmanTrack:
|
|||||||
self.frames_buena_calidad = 0
|
self.frames_buena_calidad = 0
|
||||||
self.listo_para_id = False
|
self.listo_para_id = False
|
||||||
self.area_referencia = 0.0
|
self.area_referencia = 0.0
|
||||||
|
self.firma_pre_grupo = None
|
||||||
|
self.ts_salio_grupo = 0
|
||||||
|
self.validado_post_grupo = True
|
||||||
|
|
||||||
def _convert_bbox_to_z(self, bbox):
|
def _convert_bbox_to_z(self, bbox):
|
||||||
w = bbox[2] - bbox[0]; h = bbox[3] - bbox[1]; x = bbox[0] + w/2.; y = bbox[1] + h/2.
|
w = bbox[2] - bbox[0]; h = bbox[3] - bbox[1]; x = bbox[0] + w/2.; y = bbox[1] + h/2.
|
||||||
@ -199,12 +211,31 @@ class KalmanTrack:
|
|||||||
return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.]
|
return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.]
|
||||||
|
|
||||||
def predict(self, turno_activo=True):
|
def predict(self, turno_activo=True):
|
||||||
|
# ⚡ EL SUICIDIO DE CAJAS FANTASMAS
|
||||||
|
# Si lleva más de 5 frames de ceguera total (YOLO no lo ve), matamos la predicción
|
||||||
|
if self.time_since_update > 5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0:
|
||||||
|
self.kf.statePost[6] *= 0.0
|
||||||
|
|
||||||
|
self.kf.predict()
|
||||||
|
|
||||||
|
if turno_activo:
|
||||||
|
self.time_since_update += 1
|
||||||
|
|
||||||
|
self.aprendiendo = False
|
||||||
|
self.box = self._convert_x_to_bbox(self.kf.statePre)
|
||||||
|
return self.box
|
||||||
|
|
||||||
|
|
||||||
|
"""def predict(self, turno_activo=True):
|
||||||
if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: self.kf.statePost[6] *= 0.0
|
if (self.kf.statePost[6] + self.kf.statePost[2]) <= 0: self.kf.statePost[6] *= 0.0
|
||||||
self.kf.predict()
|
self.kf.predict()
|
||||||
if turno_activo: self.time_since_update += 1
|
if turno_activo: self.time_since_update += 1
|
||||||
self.aprendiendo = False
|
self.aprendiendo = False
|
||||||
self.box = self._convert_x_to_bbox(self.kf.statePre)
|
self.box = self._convert_x_to_bbox(self.kf.statePre)
|
||||||
return self.box
|
return self.box"""
|
||||||
|
|
||||||
def update(self, box, en_grupo, now):
|
def update(self, box, en_grupo, now):
|
||||||
self.ts_ultima_deteccion = now
|
self.ts_ultima_deteccion = now
|
||||||
@ -227,18 +258,26 @@ class GlobalMemory:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = {}
|
self.db = {}
|
||||||
self.next_gid = 100
|
self.next_gid = 100
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
def _es_transito_posible(self, data, cam_destino, now):
|
def _es_transito_posible(self, data, cam_id, now):
|
||||||
ultima_cam = str(data['last_cam'])
|
cam_origen = str(data['last_cam'])
|
||||||
cam_destino = str(cam_destino)
|
cam_destino = str(cam_id)
|
||||||
dt = now - data['ts']
|
dt = now - data['ts']
|
||||||
|
|
||||||
if ultima_cam == cam_destino: return True
|
# 1. Si es la misma cámara, aplicamos la regla normal de tiempo máximo de ausencia
|
||||||
vecinos = VECINOS.get(ultima_cam, [])
|
if cam_origen == cam_destino:
|
||||||
# Permite teletransportación mínima (-0.5s) para que no te fragmente en los pasillos conectados
|
return dt < TIEMPO_MAX_AUSENCIA
|
||||||
if cam_destino in vecinos: return dt >= -0.5
|
|
||||||
return dt >= 4.0
|
# 2. ⚡ DEDUCCIÓN DE TOPOLOGÍA (El sistema aprende el mapa solo)
|
||||||
|
# Si salta a otra cámara en menos de 1.5 segundos, es físicamente imposible
|
||||||
|
# a menos que ambas cámaras apunten al mismo lugar (están solapadas).
|
||||||
|
if dt < 1.5:
|
||||||
|
# Le damos vía libre inmediata porque sabemos que está en una intersección
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 3. Si tardó un tiempo normal (ej. > 1.5s), es un tránsito de pasillo válido
|
||||||
|
return dt < TIEMPO_MAX_AUSENCIA
|
||||||
|
|
||||||
def _sim_robusta(self, firma_nueva, firmas_guardadas, cross_cam=False):
|
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
|
||||||
@ -247,10 +286,8 @@ class GlobalMemory:
|
|||||||
elif len(sims) <= 4: return (sims[0] * 0.6) + (sims[1] * 0.4)
|
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)
|
else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20)
|
||||||
|
|
||||||
# ⚡ SE AGREGÓ 'en_borde' A LOS PARÁMETROS
|
# ⚡ SE AGREGÓ LÓGICA ESPACIO-TEMPORAL DINÁMICA
|
||||||
def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True):
|
def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=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()
|
self.limpiar_fantasmas()
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@ -260,32 +297,57 @@ class GlobalMemory:
|
|||||||
for gid, data in self.db.items():
|
for gid, data in self.db.items():
|
||||||
if gid in active_gids: continue
|
if gid in active_gids: continue
|
||||||
dt = now - data['ts']
|
dt = now - data['ts']
|
||||||
if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue
|
|
||||||
|
# Bloqueo inmediato si es físicamente imposible
|
||||||
|
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
|
||||||
|
|
||||||
misma_cam = (str(data['last_cam']) == str(cam_id))
|
misma_cam = (str(data['last_cam']) == str(cam_id))
|
||||||
es_cross_cam = not misma_cam
|
es_cross_cam = not misma_cam
|
||||||
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)
|
sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam)
|
||||||
|
|
||||||
if misma_cam: umbral = UMBRAL_REID_MISMA_CAM
|
# UMBRALES DINÁMICOS INTELIGENTES
|
||||||
elif es_vecino: umbral = UMBRAL_REID_VECINO
|
|
||||||
else: umbral = UMBRAL_REID_NO_VECINO
|
|
||||||
|
|
||||||
# PROTECCIÓN VIP
|
|
||||||
if data.get('nombre') is not None:
|
|
||||||
if misma_cam:
|
if misma_cam:
|
||||||
umbral += 0.04
|
if dt < 2.0: umbral = 0.60 # Para parpadeos rápidos de YOLO
|
||||||
|
else: umbral = 0.63 # Si desapareció un rato en la misma cámara
|
||||||
|
elif es_vecino:
|
||||||
|
# ⚡ EL PUNTO DULCE: 0.62.
|
||||||
|
# Está por encima del ruido máximo (0.57) y por debajo de las
|
||||||
|
# caídas de Omar en el cluster 7-5-8 (0.64).
|
||||||
|
umbral = 0.61
|
||||||
else:
|
else:
|
||||||
umbral += 0.03
|
# ⚡ LEJANOS: 0.66.
|
||||||
|
# Si salta de la Cam 1 a la Cam 8, exigimos seguridad alta
|
||||||
|
# para no robarle el ID a alguien que acaba de entrar por la otra puerta.
|
||||||
|
umbral = 0.66
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if misma_cam:
|
||||||
|
if dt < 2.0: umbral = 0.55
|
||||||
|
elif dt < 10.0: umbral = 0.60
|
||||||
|
else: umbral = 0.60
|
||||||
|
elif es_vecino:
|
||||||
|
# ⚡ Subimos a 0.66. Si un extraño saca 0.62, será rechazado y nacerá un ID nuevo.
|
||||||
|
umbral = 0.66
|
||||||
|
else:
|
||||||
|
# ⚡ Cámaras lejanas: Exigencia casi perfecta.
|
||||||
|
umbral = 0.70"""
|
||||||
|
|
||||||
|
# PROTECCIÓN VIP (Le exigimos un poquito más si ya tiene nombre para no mancharlo)
|
||||||
|
if data.get('nombre') is not None:
|
||||||
|
umbral += 0.05 if misma_cam else 0.02
|
||||||
|
|
||||||
|
# 👁️ EL CHIVATO DE OSNET (Debug crucial)
|
||||||
|
# Esto imprimirá en tu consola qué similitud real de ropa detectó al cruzar de cámara
|
||||||
|
if sim > 0.35 and not misma_cam:
|
||||||
|
print(f" [OSNet] Cam {cam_id} evaluando ID {gid} (Viene de Cam {data['last_cam']}) -> Similitud Ropa: {sim:.2f} (Umbral exigido: {umbral:.2f})")
|
||||||
|
|
||||||
if sim > umbral:
|
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))
|
candidatos.append((sim, gid, misma_cam, es_vecino))
|
||||||
|
|
||||||
if not candidatos:
|
if not candidatos:
|
||||||
@ -293,7 +355,6 @@ class GlobalMemory:
|
|||||||
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)
|
candidatos.sort(reverse=True)
|
||||||
best_sim, best_gid, best_misma, best_vecino = candidatos[0]
|
best_sim, best_gid, best_misma, best_vecino = candidatos[0]
|
||||||
|
|
||||||
@ -301,20 +362,15 @@ class GlobalMemory:
|
|||||||
segunda_sim, segundo_gid, seg_misma, seg_vecino = candidatos[1]
|
segunda_sim, segundo_gid, seg_misma, seg_vecino = candidatos[1]
|
||||||
margen = best_sim - segunda_sim
|
margen = best_sim - segunda_sim
|
||||||
|
|
||||||
# Si las chamarras son casi idénticas...
|
if margen <= 0.06 and best_sim < 0.75:
|
||||||
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_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)
|
peso_2 = segunda_sim + (0.10 if (seg_misma or seg_vecino) else 0.0)
|
||||||
|
|
||||||
if peso_1 > peso_2:
|
if peso_1 > peso_2:
|
||||||
best_gid = best_gid # Gana el primero por lógica espacial
|
best_gid = best_gid
|
||||||
elif peso_2 > peso_1:
|
elif peso_2 > peso_1:
|
||||||
best_gid = segundo_gid # Gana el segundo por lógica espacial
|
best_gid = segundo_gid
|
||||||
else:
|
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.")
|
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
|
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)
|
||||||
@ -348,7 +404,6 @@ class GlobalMemory:
|
|||||||
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):
|
def limpiar_fantasmas(self):
|
||||||
""" Aniquila los IDs temporales que llevan más de 15 segundos sin verse """
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
ahora = time.time()
|
ahora = time.time()
|
||||||
ids_a_borrar = []
|
ids_a_borrar = []
|
||||||
@ -356,11 +411,9 @@ class GlobalMemory:
|
|||||||
for gid, data in self.db.items():
|
for gid, data in self.db.items():
|
||||||
tiempo_inactivo = ahora - data.get('ts', ahora)
|
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:
|
if data.get('nombre') is None and tiempo_inactivo > 600.0:
|
||||||
ids_a_borrar.append(gid)
|
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:
|
elif data.get('nombre') is not None and tiempo_inactivo > 900.0:
|
||||||
ids_a_borrar.append(gid)
|
ids_a_borrar.append(gid)
|
||||||
|
|
||||||
@ -368,19 +421,31 @@ class GlobalMemory:
|
|||||||
del self.db[gid]
|
del self.db[gid]
|
||||||
|
|
||||||
def confirmar_firma_vip(self, gid, ts):
|
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:
|
with self.lock:
|
||||||
if gid in self.db and self.db[gid]['firmas']:
|
if gid in self.db and self.db[gid]['firmas']:
|
||||||
# Tomamos la última firma que el hilo principal guardó (la ropa actual)
|
firmas_actuales = self.db[gid]['firmas']
|
||||||
firma_actual_pura = self.db[gid]['firmas'][-1]
|
self.db[gid]['firmas'] = firmas_actuales[-3:]
|
||||||
|
|
||||||
# Sobrescribimos el historial dejando solo esta firma confirmada
|
|
||||||
self.db[gid]['firmas'] = [firma_actual_pura]
|
|
||||||
self.db[gid]['ts'] = ts
|
self.db[gid]['ts'] = ts
|
||||||
|
|
||||||
|
def fusionar_ids(self, id_mantiene, id_elimina):
|
||||||
|
with self.lock:
|
||||||
|
if id_mantiene in self.db and id_elimina in self.db:
|
||||||
|
# 1. Le pasamos toda la memoria de ropa al ID ganador
|
||||||
|
self.db[id_mantiene]['firmas'].extend(self.db[id_elimina]['firmas'])
|
||||||
|
|
||||||
|
# Topamos a las 15 mejores firmas para no saturar la RAM
|
||||||
|
if len(self.db[id_mantiene]['firmas']) > 15:
|
||||||
|
self.db[id_mantiene]['firmas'] = self.db[id_mantiene]['firmas'][-15:]
|
||||||
|
|
||||||
|
self.db[id_mantiene]['ts'] = max(self.db[id_mantiene]['ts'], self.db[id_elimina]['ts'])
|
||||||
|
|
||||||
|
# 2. Vaciamos al perdedor y le ponemos un "Redireccionamiento"
|
||||||
|
self.db[id_elimina]['firmas'] = []
|
||||||
|
self.db[id_elimina]['fusionado_con'] = id_mantiene
|
||||||
|
print(f"🧬 [FUSIÓN MÁGICA] Las firmas del ID {id_elimina} fueron absorbidas por el ID {id_mantiene}.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer)
|
# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer)
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
@ -394,40 +459,167 @@ 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 _detectar_grupo(self, trk, box, todos_los_tracks):
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
w_box = x2 - x1
|
||||||
|
h_box = y2 - y1
|
||||||
|
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
|
||||||
|
|
||||||
|
estado_actual = getattr(trk, 'en_grupo', False)
|
||||||
|
# Si están a menos de medio cuerpo de distancia lateral, es un grupo.
|
||||||
|
factor_x = 1.0 if estado_actual else 1.0
|
||||||
|
factor_y = 0.4 if estado_actual else 0.2
|
||||||
|
|
||||||
|
for other in todos_los_tracks:
|
||||||
|
if other is trk: continue
|
||||||
|
if not hasattr(other, 'box') or other.box is None: continue
|
||||||
|
|
||||||
|
ox1, oy1, ox2, oy2 = other.box
|
||||||
|
ocx, ocy = (ox1 + ox2) / 2, (oy1 + oy2) / 2
|
||||||
|
|
||||||
|
dist_x = abs(cx - ocx)
|
||||||
|
dist_y = abs(cy - ocy)
|
||||||
|
|
||||||
|
if dist_x < (w_box * factor_x) and dist_y < (h_box * factor_y):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _gestionar_aprendizaje_post_grupo(self, now, frame_hd):
|
||||||
|
CUARENTENA = 1.2
|
||||||
|
for trk in self.trackers:
|
||||||
|
if trk.gid is None or getattr(trk, 'en_grupo', False) or getattr(trk, 'firma_pre_grupo', None) is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tiempo_fuera = now - getattr(trk, 'ts_salio_grupo', 0)
|
||||||
|
|
||||||
|
if tiempo_fuera >= CUARENTENA:
|
||||||
|
firma_actual = extraer_firma_hibrida(frame_hd, trk.box)
|
||||||
|
|
||||||
|
if firma_actual is not None:
|
||||||
|
sim = similitud_hibrida(firma_actual, trk.firma_pre_grupo)
|
||||||
|
|
||||||
|
# ⚡ Bajamos a 0.50 como umbral de supervivencia base
|
||||||
|
if sim >= 0.50:
|
||||||
|
print(f"[GRUPO] ID {trk.gid} validado post-grupo ({sim:.2f}).")
|
||||||
|
trk.fallos_post_grupo = 0 # Reseteamos los strikes si tenía
|
||||||
|
|
||||||
|
with self.global_mem.lock:
|
||||||
|
if trk.gid in self.global_mem.db:
|
||||||
|
self.global_mem.db[trk.gid]['firmas'] = [trk.firma_pre_grupo, firma_actual]
|
||||||
|
trk.firma_pre_grupo = None # Aprobado, limpiamos memoria
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ⚡ SISTEMA DE 3 STRIKES
|
||||||
|
trk.fallos_post_grupo = getattr(trk, 'fallos_post_grupo', 0) + 1
|
||||||
|
|
||||||
|
if trk.fallos_post_grupo >= 3:
|
||||||
|
print(f" [ALERTA GRUPO] ID {trk.gid} falló 3 veces validación ({sim:.2f}). Reseteando ID.")
|
||||||
|
trk.gid = None
|
||||||
|
trk.firma_pre_grupo = None # Eliminado, limpiamos memoria
|
||||||
|
trk.fallos_post_grupo = 0 # Reiniciamos contador
|
||||||
|
else:
|
||||||
|
print(f" [STRIKE {trk.fallos_post_grupo}/3] ID {trk.gid} sacó {sim:.2f}. Dando otra oportunidad...")
|
||||||
|
# OJO: NO ponemos firma_pre_grupo en None aquí,
|
||||||
|
# para que lo vuelva a intentar en el siguiente frame.
|
||||||
|
|
||||||
def update(self, boxes, frame_show, frame_hd, 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:
|
||||||
|
if trk.gid is not None:
|
||||||
|
with self.global_mem.lock:
|
||||||
|
if trk.gid in self.global_mem.db and 'fusionado_con' in self.global_mem.db[trk.gid]:
|
||||||
|
print(f"🔗 [HILO CAM {self.cam_id}] Tracker mutando de ID {trk.gid} a {self.global_mem.db[trk.gid]['fusionado_con']}")
|
||||||
|
trk.gid = self.global_mem.db[trk.gid]['fusionado_con']
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ⚡ FILTRO ANTI-FANTASMAS
|
||||||
|
vivos_predict = []
|
||||||
|
for trk in self.trackers:
|
||||||
|
caja_predicha = trk.predict(turno_activo=turno_activo)
|
||||||
|
# Si el tracker lleva más de 5 frames ciego, devuelve None. Lo ignoramos.
|
||||||
|
if caja_predicha is not None:
|
||||||
|
vivos_predict.append(trk)
|
||||||
|
|
||||||
|
self.trackers = vivos_predict # Actualizamos la lista solo con los vivos
|
||||||
|
|
||||||
if not turno_activo: return self.trackers
|
if not turno_activo: return self.trackers
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# Aquí sigue tu código normal:
|
||||||
matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now)
|
matched, unmatched_dets, unmatched_trks = self._asignar(boxes, now)
|
||||||
|
|
||||||
for t_idx, d_idx in matched:
|
|
||||||
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)
|
|
||||||
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])
|
|
||||||
|
|
||||||
# IGNORAMOS VECTORES MUTANTES DE GRUPOS
|
for t_idx, d_idx in matched:
|
||||||
if trk.gid is None and trk.listo_para_id and not trk.en_grupo:
|
trk = self.trackers[t_idx]
|
||||||
# ⚡ Usamos el frame_hd
|
box = boxes[d_idx]
|
||||||
|
|
||||||
|
es_grupo_ahora = self._detectar_grupo(trk, box, self.trackers)
|
||||||
|
|
||||||
|
if not getattr(trk, 'en_grupo', False) and es_grupo_ahora:
|
||||||
|
if trk.gid is not None:
|
||||||
|
with self.global_mem.lock:
|
||||||
|
firmas = self.global_mem.db.get(trk.gid, {}).get('firmas', [])
|
||||||
|
if firmas:
|
||||||
|
trk.firma_pre_grupo = firmas[-1]
|
||||||
|
trk.validado_post_grupo = False
|
||||||
|
print(f" [GRUPO] ID {trk.gid} entró a grupo. Protegiendo firma.")
|
||||||
|
trk.ts_salio_grupo = 0.0
|
||||||
|
|
||||||
|
elif getattr(trk, 'en_grupo', False) and not es_grupo_ahora:
|
||||||
|
trk.ts_salio_grupo = now
|
||||||
|
print(f" [GRUPO] ID {trk.gid} salió de grupo. Cuarentena de 3s.")
|
||||||
|
|
||||||
|
trk.en_grupo = es_grupo_ahora
|
||||||
|
trk.update(box, es_grupo_ahora, now)
|
||||||
|
|
||||||
|
area_actual = (box[2] - box[0]) * (box[3] - box[1])
|
||||||
|
tiempo_desde_separacion = now - getattr(trk, 'ts_salio_grupo', 0)
|
||||||
|
en_cuarentena = (tiempo_desde_separacion < 3.0) and (getattr(trk, 'ts_salio_grupo', 0) > 0)
|
||||||
|
|
||||||
|
extracciones_hoy = 0 # ⚡ EL SALVAVIDAS: Contador para el frame actual
|
||||||
|
|
||||||
|
if not trk.en_grupo and not en_cuarentena:
|
||||||
|
|
||||||
|
# A) Bautizo de IDs Nuevos (Con Aduana Temporal)
|
||||||
|
if trk.gid is None and trk.listo_para_id:
|
||||||
firma = extraer_firma_hibrida(frame_hd, box)
|
firma = extraer_firma_hibrida(frame_hd, box)
|
||||||
if firma is not None:
|
if firma is not None:
|
||||||
# ⚡ DETECCIÓN DE ZONA DE NACIMIENTO
|
|
||||||
fh, fw = frame_hd.shape[:2]
|
fh, fw = frame_hd.shape[:2]
|
||||||
bx1, by1, bx2, by2 = map(int, box)
|
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)
|
nace_en_borde = (bx1 < 25 or by1 < 25 or bx2 > fw - 25 or by2 > fh - 25)
|
||||||
|
|
||||||
# Mandamos esa información al identificador
|
candidato_gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde)
|
||||||
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
|
|
||||||
|
|
||||||
elif trk.gid is not None and not trk.en_grupo:
|
if candidato_gid is not None:
|
||||||
|
# ⚡ BLOQUEO INMEDIATO: Reservamos el ID en este milisegundo
|
||||||
|
# para que ninguna otra persona en esta cámara pueda evaluarlo.
|
||||||
|
active_gids.add(candidato_gid)
|
||||||
|
|
||||||
|
# Si la memoria dice "Es un desconocido nuevo", lo bautizamos al instante
|
||||||
|
if not es_reid:
|
||||||
|
trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, False, area_actual
|
||||||
|
|
||||||
|
# ⚡ ADUANA TEMPORAL
|
||||||
|
else:
|
||||||
|
if getattr(trk, 'candidato_temporal', None) == candidato_gid:
|
||||||
|
trk.votos_reid = getattr(trk, 'votos_reid', 0) + 1
|
||||||
|
else:
|
||||||
|
# Si cambia de opinión, reiniciamos sin restar
|
||||||
|
trk.candidato_temporal = candidato_gid
|
||||||
|
trk.votos_reid = 1
|
||||||
|
|
||||||
|
if trk.votos_reid >= 2:
|
||||||
|
trk.gid, trk.origen_global, trk.area_referencia = candidato_gid, True, area_actual
|
||||||
|
print(f" [ADUANA] ID {candidato_gid} validado.")
|
||||||
|
|
||||||
|
# B) Aprendizaje Continuo (Captura de Ángulos)
|
||||||
|
elif trk.gid is not None:
|
||||||
tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0)
|
tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0)
|
||||||
|
|
||||||
# ⚡ APRENDIZAJE RÁPIDO: Bajamos de 1.5s a 0.5s para que llene la memoria volando
|
# ⚡ APRENDIZAJE ESCALONADO:
|
||||||
if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box):
|
# Mantenemos tus 0.5s perfectos, pero solo 1 persona por frame puede saturar la CPU.
|
||||||
|
if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box) and extracciones_hoy < 1:
|
||||||
fh, fw = frame_hd.shape[:2]
|
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)
|
||||||
@ -435,21 +627,36 @@ class CamManager:
|
|||||||
if not en_borde:
|
if not en_borde:
|
||||||
firma_nueva = extraer_firma_hibrida(frame_hd, box)
|
firma_nueva = extraer_firma_hibrida(frame_hd, box)
|
||||||
if firma_nueva is not None:
|
if firma_nueva is not None:
|
||||||
|
extracciones_hoy += 1 # ⚡ Cerramos la compuerta para los demás en este frame
|
||||||
|
|
||||||
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']:
|
||||||
|
|
||||||
# ⚡ 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]
|
firma_reciente = self.global_mem.db[trk.gid]['firmas'][-1]
|
||||||
sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente)
|
firma_original = self.global_mem.db[trk.gid]['firmas'][0]
|
||||||
|
|
||||||
# Tolerancia relajada a 0.50 para permitir la transición de la espalda
|
sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente)
|
||||||
if sim_coherencia > 0.50:
|
sim_raiz = similitud_hibrida(firma_nueva, firma_original)
|
||||||
|
|
||||||
|
# ⚡ EL BOTÓN DE PÁNICO (Anti ID-Switch)
|
||||||
|
# Si la ropa de la caja actual no se parece en NADA a la original (< 0.35),
|
||||||
|
# significa que Kalman le pegó el ID a la persona equivocada en un cruce.
|
||||||
|
if sim_raiz < 0.35:
|
||||||
|
print(f"[ID SWITCH] Ropa de ID {trk.gid} cambió drásticamente. Revocando ID.")
|
||||||
|
trk.gid = None
|
||||||
|
trk.listo_para_id = False
|
||||||
|
trk.frames_buena_calidad = 0
|
||||||
|
continue # Rompemos el ciclo para que nazca como alguien nuevo
|
||||||
|
|
||||||
|
ya_bautizado = self.global_mem.db[trk.gid].get('nombre') is not None
|
||||||
|
umbral_raiz = 0.52 if ya_bautizado else 0.62
|
||||||
|
|
||||||
|
if sim_coherencia > 0.60 and sim_raiz > umbral_raiz:
|
||||||
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_raiz:
|
||||||
es_coherente = False
|
es_coherente = False
|
||||||
break
|
break
|
||||||
if es_coherente:
|
if es_coherente:
|
||||||
@ -460,64 +667,92 @@ 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_show.shape[:2] # Usamos frame_show para evaluar los bordes de la caja 480
|
fh, fw = frame_show.shape[:2]
|
||||||
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 < 20 or y1 < 20 or x2 > fw - 20 or y2 > fh - 20)
|
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
|
||||||
|
|
||||||
# ⚡ MUERTE JUSTA: Si es anónimo en el borde, muere rápido.
|
# ⚡ LIMPIEZA AGRESIVA PARA PROTEGER LA CPU DE MEMORY LEAKS
|
||||||
# Si ya tiene un ID asignado, le damos al menos 3 segundos de gracia.
|
if t.gid is None:
|
||||||
if t.gid is None and toca_borde:
|
# Si es un ID gris (no bautizado), lo matamos rápido si YOLO lo pierde
|
||||||
limite_vida = 1.0
|
limite_vida = 1.0 if toca_borde else 2.5
|
||||||
elif t.gid is not None and toca_borde:
|
|
||||||
limite_vida = 3.0
|
|
||||||
else:
|
else:
|
||||||
limite_vida = 10.0
|
# Si es VIP, le damos paciencia para que OSNet no se fragmente
|
||||||
|
limite_vida = 3.0 if toca_borde else 8.0
|
||||||
|
|
||||||
if tiempo_oculto < limite_vida:
|
if tiempo_oculto < limite_vida:
|
||||||
vivos.append(t)
|
vivos.append(t)
|
||||||
|
|
||||||
self.trackers = vivos
|
self.trackers = vivos
|
||||||
|
|
||||||
|
self._gestionar_aprendizaje_post_grupo(now, frame_hd)
|
||||||
|
|
||||||
return self.trackers
|
return self.trackers
|
||||||
|
|
||||||
|
|
||||||
def _asignar(self, boxes, now):
|
def _asignar(self, boxes, now):
|
||||||
n_trk = len(self.trackers); n_det = len(boxes)
|
n_trk = len(self.trackers); 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):
|
||||||
|
tiempo_oculto = now - trk.ts_ultima_deteccion
|
||||||
|
|
||||||
|
# ⚡ 2A. Aumentamos el radio dinámico mínimo para no perder gente rápida
|
||||||
|
radio_dinamico = min(350.0, max(150.0, 300.0 * tiempo_oculto))
|
||||||
|
|
||||||
|
# La incertidumbre crece con el tiempo, pero la topamos en 0.5
|
||||||
|
incertidumbre = min(0.5, tiempo_oculto * 0.2)
|
||||||
|
|
||||||
|
es_fantasma = getattr(trk, 'time_since_update', 0) > 1
|
||||||
|
|
||||||
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_pixel = np.sqrt((cx_t-cx_d)**2 + (cy_t-cy_d)**2)
|
||||||
|
|
||||||
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])
|
||||||
|
|
||||||
|
if dist_pixel > radio_dinamico:
|
||||||
|
cost_mat[t, d] = 100.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ⚡ 2B. REDUCIMOS CASTIGOS INJUSTOS
|
||||||
|
# Eliminamos el "castigo_secuestro" que penalizaba excesivamente a los trackers sin IOU.
|
||||||
|
# Reducimos la penalización por cambio de tamaño de 0.4 a 0.2.
|
||||||
|
|
||||||
|
dist_norm = dist_pixel / radio_dinamico
|
||||||
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
|
|
||||||
|
|
||||||
tiempo_oculto = now - trk.ts_ultima_deteccion
|
# Si la caja de YOLO es mucho MÁS PEQUEÑA que la predicción de Kalman,
|
||||||
if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10:
|
# penalizamos fuerte (evita que la caja encoja mágicamente y pierda a la persona).
|
||||||
fantasma_penalty = 5.0
|
if area_det < area_trk:
|
||||||
else: fantasma_penalty = 0.0
|
castigo_tam = (ratio_area - 1.0) * 0.5
|
||||||
|
else:
|
||||||
|
# Si la caja de YOLO es MÁS GRANDE (ej. persona acercándose/subiendo escaleras),
|
||||||
|
# somos muy permisivos para que Kalman acepte el nuevo tamaño sin soltarse.
|
||||||
|
castigo_tam = (ratio_area - 1.0) * 0.15
|
||||||
|
|
||||||
if iou >= 0.05 or dist_norm < 0.80:
|
# Fórmula equilibrada
|
||||||
cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tam
|
cost_mat[t, d] = (1.0 - iou) + (0.5 * dist_norm) + castigo_tam + incertidumbre
|
||||||
else: cost_mat[t, d] = 100.0
|
|
||||||
|
|
||||||
|
from scipy.optimize import linear_sum_assignment
|
||||||
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):
|
||||||
# ⚡ CAJAS PEGAJOSAS: 6.0 evita que suelte el ID si te mueves rápido
|
# ⚡ UMBRAL RELAJADO: De 2.5 a 3.5.
|
||||||
if cost_mat[r, c] > 7.0:
|
# Esto evita que rechace la caja por la latencia del procesador.
|
||||||
|
if cost_mat[r, c] > 3.5:
|
||||||
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))
|
||||||
|
|
||||||
for t in range(n_trk):
|
for t in range(n_trk):
|
||||||
if t not in [m[0] for m in matched]: unmatched_trks.append(t)
|
if t not in [m[0] for m in matched]: unmatched_trks.append(t)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user