934 lines
45 KiB
Python
934 lines
45 KiB
Python
"""
|
|
Tracker Multi-Cámara Definitivo (SmartSoft IA)
|
|
Características:
|
|
1. OSNet dominante con EMA fluida.
|
|
2. Color LAB con Veto Cruzado Letal (anti-robos de identidad).
|
|
3. Huella ANATÓMICA por keypoints (anti-uniformes).
|
|
4. Sistema Inmune (Hard Negatives): Aprende de tus correcciones (tecla 'n').
|
|
5. Memoria Persistente: No olvida las correcciones al reiniciar.
|
|
"""
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import time
|
|
import threading
|
|
from scipy.optimize import linear_sum_assignment
|
|
from scipy.spatial.distance import cosine
|
|
from ultralytics import YOLO
|
|
import onnxruntime as ort
|
|
import os
|
|
from datetime import datetime
|
|
import json
|
|
import csv
|
|
import math
|
|
import queue
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# CONFIGURACIÓN
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.244"
|
|
SECUENCIA = [1, 7, 5, 8, 3, 6]
|
|
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]
|
|
ONNX_MODEL_PATH = "osnet_dinamico_int8.onnx"
|
|
|
|
VECINOS = {
|
|
"1": ["7"], "7": ["1", "5"], "5": ["7", "8"],
|
|
"8": ["5", "3"], "3": ["8", "6"], "6": ["3"]
|
|
}
|
|
|
|
# ⚡ TIEMPOS REALES: Bloqueo estricto de teletransportaciones
|
|
TIEMPO_MIN_POR_DISTANCIA = {
|
|
0: 0.0,
|
|
1: 1.0, # Vecino directo (casi instantáneo)
|
|
2: 15.0, # Un salto intermedio -> Mínimo 15 seg.
|
|
3: 40.0, # Dos o más saltos -> Mínimo 40 seg.
|
|
}
|
|
|
|
TIEMPO_MAX_AUSENCIA = 800.0
|
|
C_CANDIDATO = (150, 150, 150)
|
|
C_LOCAL = (0, 255, 0)
|
|
C_GLOBAL = (0, 165, 255)
|
|
C_GRUPO = (0, 0, 255)
|
|
C_APRENDIZAJE = (255, 255, 0)
|
|
C_FEEDBACK = (255, 0, 255)
|
|
FUENTE = cv2.FONT_HERSHEY_SIMPLEX
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# OSNET
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
print("Cargando OSNet...")
|
|
try:
|
|
ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider'])
|
|
input_name = ort_session.get_inputs()[0].name
|
|
print("OSNet listo para CPU.")
|
|
except Exception as e:
|
|
print(f"ERROR FATAL: {e}"); exit()
|
|
|
|
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(3, 1, 1)
|
|
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(3, 1, 1)
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# HUELLA ANATÓMICA (Anti-uniformes)
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
def extraer_proporciones_anatomicas(kpts):
|
|
if kpts is None or len(kpts) < 17: return None
|
|
kpts = np.array(kpts)
|
|
if kpts.shape[0] < 17: return None
|
|
|
|
puntos = {i: kpts[i] for i in range(17)}
|
|
nariz_ok = puntos[0][2] > 0.30
|
|
hombro_izq = puntos[5][2] > 0.30
|
|
hombro_der = puntos[6][2] > 0.30
|
|
cadera_izq = puntos[11][2] > 0.30
|
|
cadera_der = puntos[12][2] > 0.30
|
|
|
|
if not nariz_ok or (not hombro_izq and not hombro_der) or (not cadera_izq and not cadera_der):
|
|
return None
|
|
|
|
puntos_validos = sum(1 for p in puntos.values() if p[2] > 0.30)
|
|
if puntos_validos < 7: return None
|
|
|
|
def dist(i, j):
|
|
if puntos[i][2] < 0.30 or puntos[j][2] < 0.30: return 0.0
|
|
return float(np.linalg.norm(puntos[i][:2] - puntos[j][:2]))
|
|
|
|
hombros = dist(5, 6)
|
|
caderas = dist(11, 12)
|
|
torso_izq = dist(5, 11)
|
|
torso_der = dist(6, 12)
|
|
pierna_izq = dist(11, 13) + dist(13, 15) if puntos[13][2] > 0.30 and puntos[15][2] > 0.30 else 0
|
|
pierna_der = dist(12, 14) + dist(14, 16) if puntos[14][2] > 0.30 and puntos[16][2] > 0.30 else 0
|
|
brazo_izq = dist(5, 7) + dist(7, 9) if puntos[7][2] > 0.30 and puntos[9][2] > 0.30 else 0
|
|
brazo_der = dist(6, 8) + dist(8, 10) if puntos[8][2] > 0.30 and puntos[10][2] > 0.30 else 0
|
|
|
|
altura_approx = max(torso_izq, torso_der) + max(pierna_izq, pierna_der)
|
|
if altura_approx < 10.0: return None
|
|
|
|
feats = np.array([
|
|
hombros / altura_approx,
|
|
caderas / altura_approx,
|
|
max(torso_izq, torso_der) / altura_approx,
|
|
max(pierna_izq, pierna_der) / altura_approx,
|
|
max(brazo_izq, brazo_der) / altura_approx,
|
|
hombros / max(caderas, 1e-6),
|
|
max(pierna_izq, pierna_der) / max(max(torso_izq, torso_der), 1e-6),
|
|
(brazo_izq + brazo_der) / max(max(torso_izq, torso_der) * 2, 1e-6),
|
|
], dtype=np.float32)
|
|
|
|
feats = np.clip(feats, 0.0, 3.0)
|
|
n = np.linalg.norm(feats)
|
|
if n > 0: feats /= n
|
|
return feats
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# EXTRACCIÓN DE FIRMAS
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
def analizar_calidad(box, kpts=None, estricto=False):
|
|
x1, y1, x2, y2 = box
|
|
w, h = x2-x1, y2-y1
|
|
if w <= 0 or h <= 0: return False
|
|
|
|
ratio = h / float(w)
|
|
puntos_buenos = sum(1 for kx, ky, conf in kpts if conf > 0.40) if kpts is not None else 0
|
|
|
|
if puntos_buenos >= 4:
|
|
ratio_min = 0.60
|
|
else:
|
|
ratio_min = 1.25 if estricto else 1.20
|
|
|
|
ratio_max = 4.0 if estricto else 5.0
|
|
area_min = 800 if estricto else 400
|
|
|
|
return (ratio_min < ratio < ratio_max) and (w*h > area_min)
|
|
|
|
def preprocess_onnx(roi):
|
|
img = cv2.resize(roi, (128, 256))
|
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
img = img.transpose(2, 0, 1).astype(np.float32) / 255.0
|
|
return np.expand_dims((img - MEAN) / STD, 0)
|
|
|
|
def extraer_color_zonas(img):
|
|
if img is None or img.size == 0 or img.shape[0] < 5 or img.shape[1] < 5:
|
|
return np.zeros(512 * 3, dtype=np.float32)
|
|
|
|
h, w = img.shape[:2]
|
|
t1, t2 = int(h * 0.15), int(h * 0.55)
|
|
m_w = int(w * 0.20)
|
|
core_img = img[:, m_w:(w - m_w)]
|
|
if core_img.size == 0 or core_img.shape[1] < 4:
|
|
core_img = img
|
|
|
|
lab = cv2.cvtColor(core_img, cv2.COLOR_BGR2LAB)
|
|
l, a, b = cv2.split(lab)
|
|
if l.shape[0] >= 8 and l.shape[1] >= 8:
|
|
l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l)
|
|
lab_eq = cv2.merge((l, a, b))
|
|
|
|
def hzone(z):
|
|
if z is None or z.size == 0 or z.shape[0] < 2 or z.shape[1] < 2:
|
|
return np.zeros(512, dtype=np.float32)
|
|
hist = cv2.calcHist([z], [0,1,2], None, [8,8,8], [0,256, 0,256, 0,256])
|
|
cv2.normalize(hist, hist, alpha=1.0, norm_type=cv2.NORM_L1)
|
|
return hist.flatten()
|
|
|
|
return np.concatenate([hzone(lab_eq[:t1,:]), hzone(lab_eq[t1:t2,:]), hzone(lab_eq[t2:,:])])
|
|
|
|
def calidad_real(roi_shape, x1_c, y1_c, x2_c, y2_c, w_hd, h_hd):
|
|
area = (x2_c - x1_c) * (y2_c - y1_c)
|
|
ratio = roi_shape[0] / max(roi_shape[1], 1)
|
|
factor_ratio = 1.0 if 1.8 < ratio < 3.2 else 0.5
|
|
en_borde = (x1_c < 20 or y1_c < 20 or x2_c > w_hd-20 or y2_c > h_hd-20)
|
|
factor_borde = 0.3 if en_borde else 1.0
|
|
return area * factor_ratio * factor_borde
|
|
|
|
def extraer_firma_hibrida(frame_hd, box_480, kpts=None):
|
|
try:
|
|
h_hd, w_hd = frame_hd.shape[:2]
|
|
x1, y1, x2, y2 = box_480
|
|
w, h = x2-x1, y2-y1
|
|
px, py = w*0.08, h*0.05
|
|
|
|
x1_c = max(0, int((x1-px)*(w_hd/480.0)))
|
|
y1_c = max(0, int((y1-py)*(h_hd/270.0)))
|
|
x2_c = min(w_hd, int((x2+px)*(w_hd/480.0)))
|
|
y2_c = min(h_hd, int((y2+py)*(h_hd/270.0)))
|
|
|
|
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
|
|
|
|
cal = calidad_real(roi.shape, x1_c, y1_c, x2_c, y2_c, w_hd, h_hd)
|
|
|
|
blob = preprocess_onnx(roi)
|
|
deep_f = ort_session.run(None, {input_name: blob})[0][0].flatten()
|
|
n = np.linalg.norm(deep_f)
|
|
if n > 0: deep_f /= n
|
|
|
|
color_f = extraer_color_zonas(roi)
|
|
anat_f = extraer_proporciones_anatomicas(kpts)
|
|
|
|
return {
|
|
'deep': deep_f,
|
|
'color': color_f,
|
|
'anatomia': anat_f,
|
|
'ratio_hw': roi.shape[0] / max(roi.shape[1], 1),
|
|
'calidad': cal,
|
|
}
|
|
except Exception as e:
|
|
print(f"[Firma Error] {e}")
|
|
return None
|
|
|
|
def similitud_hibrida(f1, f2, cross_cam=False, confianza_fisica=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_head = sim_torso = sim_legs = 1.0
|
|
if f1['color'].shape == f2['color'].shape and f1['color'].size > 1:
|
|
L = len(f1['color'])//3
|
|
def hcmp(a,b):
|
|
if np.sum(a) == 0 or np.sum(b) == 0: return 1.0
|
|
dist = cv2.compareHist(a.astype(np.float32), b.astype(np.float32), cv2.HISTCMP_BHATTACHARYYA)
|
|
return max(0.0, 1.0 - float(dist))
|
|
|
|
sim_head = hcmp(f1['color'][:L], f2['color'][:L])
|
|
sim_torso = hcmp(f1['color'][L:2*L], f2['color'][L:2*L])
|
|
sim_legs = hcmp(f1['color'][2*L:], f2['color'][2*L:])
|
|
|
|
sim_color = (sim_head * 0.15) + (sim_torso * 0.50) + (sim_legs * 0.35)
|
|
|
|
sim_anat = 1.0
|
|
if 'anatomia' in f1 and 'anatomia' in f2:
|
|
a1, a2 = f1['anatomia'], f2['anatomia']
|
|
if a1 is not None and a2 is not None:
|
|
sim_anat = max(0.0, float(np.dot(a1, a2)))
|
|
|
|
penal_sil = 0.0
|
|
if 'ratio_hw' in f1 and 'ratio_hw' in f2:
|
|
diff = abs(f1['ratio_hw'] - f2['ratio_hw'])
|
|
tol = 0.70 if cross_cam else (0.40 + confianza_fisica * 0.35)
|
|
if diff > tol:
|
|
penal_sil = min(0.20, (diff-tol) * 0.25) * (1.0 - confianza_fisica)
|
|
|
|
if cross_cam:
|
|
# ⚡ VETO CRUZADO LETAL: Evita que alguien de claro le robe el ID a alguien oscuro
|
|
veto_cruzado = 0.0
|
|
msg_veto = ""
|
|
if sim_torso < 0.20:
|
|
veto_cruzado = 0.60
|
|
msg_veto = "[VETO LETAL COLOR -0.60]"
|
|
elif sim_torso < 0.38: # ⚡ Subimos el límite para atrapar el 0.34 de tu log
|
|
veto_cruzado = 0.35
|
|
msg_veto = "[VETO FUERTE COLOR -0.35]"
|
|
elif sim_torso < 0.48: # ⚡ Nuevo nivel de advertencia
|
|
veto_cruzado = 0.15
|
|
msg_veto = "[VETO LEVE COLOR -0.15]"
|
|
|
|
# Veto Biométrico Anti-Uniformes
|
|
if sim_anat < 0.70:
|
|
veto_cruzado += 0.20
|
|
msg_veto += " [VETO BIOMÉTRICO]"
|
|
|
|
resultado = max(0.0, sim_deep - veto_cruzado - penal_sil)
|
|
|
|
if sim_deep > 0.40:
|
|
print(f" ↳ [CROSS-CAM] Deep:{sim_deep:.2f} Anat:{sim_anat:.2f} Col_Torso:{sim_torso:.2f} {msg_veto} Sil:{-penal_sil:.2f} => FINAL:{resultado:.2f}")
|
|
else:
|
|
resultado = max(0.0, (sim_deep * 0.80) + (sim_color * 0.10) + (sim_anat * 0.10) - penal_sil)
|
|
|
|
return resultado
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# KALMAN TRACKER
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
class KalmanTrack:
|
|
_count = 0
|
|
|
|
def __init__(self, box, now):
|
|
kf = cv2.KalmanFilter(7, 4)
|
|
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)
|
|
kf.transitionMatrix = np.eye(7, dtype=np.float32)
|
|
kf.transitionMatrix[0,4] = kf.transitionMatrix[1,5] = kf.transitionMatrix[2,6] = 1
|
|
kf.processNoiseCov = np.diag([2.0, 4.0, 15.0, 0.01, 1.0, 2.0, 1.0]).astype(np.float32)
|
|
kf.measurementNoiseCov = np.diag([10.0, 10.0, 50.0, 0.10]).astype(np.float32)
|
|
kf.errorCovPost = np.diag([10., 10., 25., 1., 500., 500., 100.]).astype(np.float32)
|
|
kf.statePost = np.zeros((7,1), np.float32)
|
|
kf.statePost[:4] = self._z(box)
|
|
self.kf = kf
|
|
|
|
self.local_id = KalmanTrack._count; KalmanTrack._count += 1
|
|
self.gid, self.origen_global, self.aprendiendo = None, False, False
|
|
self.box = list(box)
|
|
self.ts_creacion = self.ts_ultima_deteccion = now
|
|
self.time_since_update, self.en_grupo, self.frames_buena_calidad = 0, False, 0
|
|
self.listo_para_id = False
|
|
self.firma_pre_grupo, self.ts_salio_grupo, self.fallos_post_grupo = None, 0.0, 0
|
|
self.ultimo_aprendizaje, self.frames_continuos, self.frames_observados = 0.0, 0, 0
|
|
self.firma_ema = None
|
|
|
|
def _z(self, bbox):
|
|
w = bbox[2]-bbox[0]; h = bbox[3]-bbox[1]
|
|
x = bbox[0]+w/2.; y = bbox[1]+h/2.
|
|
return np.array([[x],[y],[w*h],[w/max(h,1e-6)]]).astype(np.float32)
|
|
|
|
def _bbox(self, x):
|
|
cx, cy = float(x[0].item()), float(x[1].item())
|
|
s, r = float(x[2].item()), float(x[3].item())
|
|
w = np.sqrt(max(s * r, 1e-6)); h = s / (w + 1e-6)
|
|
return [cx - w / 2., cy - h / 2., cx + w / 2., cy + h / 2.]
|
|
|
|
@property
|
|
def confianza_fisica(self):
|
|
return min(1.0, self.frames_continuos / 20.0)
|
|
|
|
def actualizar_ema(self, firma, alpha=0.82):
|
|
if firma is None: return
|
|
if self.firma_ema is None:
|
|
self.firma_ema = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in firma.items()}
|
|
if 'anatomia' in self.firma_ema and self.firma_ema['anatomia'] is not None:
|
|
self.firma_ema['anatomia'] = self.firma_ema['anatomia'].copy()
|
|
return
|
|
for key in ['deep','color']:
|
|
if key in firma:
|
|
self.firma_ema[key] = alpha*self.firma_ema[key] + (1-alpha)*firma[key]
|
|
n = np.linalg.norm(self.firma_ema[key])
|
|
if n > 0: self.firma_ema[key] /= n
|
|
self.firma_ema['ratio_hw'] = alpha*self.firma_ema['ratio_hw'] + (1-alpha)*firma['ratio_hw']
|
|
if 'anatomia' in firma and firma['anatomia'] is not None:
|
|
if 'anatomia' not in self.firma_ema or self.firma_ema['anatomia'] is None:
|
|
self.firma_ema['anatomia'] = firma['anatomia'].copy()
|
|
else:
|
|
self.firma_ema['anatomia'] = alpha*self.firma_ema['anatomia'] + (1-alpha)*firma['anatomia']
|
|
n = np.linalg.norm(self.firma_ema['anatomia'])
|
|
if n > 0: self.firma_ema['anatomia'] /= n
|
|
|
|
def predict(self, turno_activo=True):
|
|
if self.time_since_update > 4: return None
|
|
if getattr(self, 'en_grupo', False):
|
|
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._bbox(self.kf.statePre)
|
|
return self.box
|
|
|
|
if self.time_since_update > 0:
|
|
self.kf.statePost[4] *= 0.5
|
|
self.kf.statePost[5] *= 0.5
|
|
self.kf.statePost[6] = 0.0
|
|
self.frames_continuos = 0
|
|
|
|
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._bbox(self.kf.statePre)
|
|
return self.box
|
|
|
|
def update(self, box, en_grupo, now, kpts=None):
|
|
self.ts_ultima_deteccion = now
|
|
self.time_since_update, self.en_grupo = 0, en_grupo
|
|
self.box = list(box)
|
|
self.kf.correct(self._z(box))
|
|
|
|
if not en_grupo:
|
|
self.frames_observados += 1
|
|
self.frames_continuos += 1
|
|
|
|
if analizar_calidad(box, kpts=kpts, estricto=False) and not en_grupo:
|
|
self.frames_buena_calidad += 1
|
|
if self.frames_buena_calidad >= 3:
|
|
self.listo_para_id = True
|
|
elif self.gid is None:
|
|
self.frames_buena_calidad = max(0, self.frames_buena_calidad-1)
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# MEMORIA GLOBAL
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
class GlobalMemory:
|
|
def __init__(self):
|
|
self.db = {}
|
|
self.next_gid = 100
|
|
self.lock = threading.RLock()
|
|
self.ruta_memoria = os.path.join("cache_nombres", "memoria_ia.json")
|
|
os.makedirs("cache_nombres", exist_ok=True)
|
|
self._cargar_memoria_inmune()
|
|
|
|
def _guardar_memoria_inmune(self):
|
|
"""Guarda los anticuerpos (Hard Negatives) en el disco"""
|
|
datos = {}
|
|
for gid, data in self.db.items():
|
|
if data.get('hard_negatives'):
|
|
datos[str(gid)] = [hn.tolist() for hn in data['hard_negatives']]
|
|
try:
|
|
with open(self.ruta_memoria, 'w') as f: json.dump(datos, f)
|
|
except Exception as e: print(f"[Persistencia] Error guardando: {e}")
|
|
|
|
def _cargar_memoria_inmune(self):
|
|
"""Carga los anticuerpos al iniciar el sistema"""
|
|
if os.path.exists(self.ruta_memoria):
|
|
try:
|
|
with open(self.ruta_memoria, 'r') as f:
|
|
datos = json.load(f)
|
|
for gid_str, hn_list in datos.items():
|
|
gid = int(gid_str)
|
|
if gid not in self.db:
|
|
self.db[gid] = {'ema': None, 'last_cam': '1', 'ts': time.time(), 'hard_negatives': []}
|
|
self.db[gid]['hard_negatives'] = [np.array(hn, dtype=np.float32) for hn in hn_list]
|
|
print(f"[Persistencia] Memoria Inmune cargada para {len(datos)} IDs.")
|
|
except Exception as e: print(f"[Persistencia] Error cargando: {e}")
|
|
|
|
def _actualizar_sin_lock(self, gid, firma, cam_id, now):
|
|
ALPHA = 0.75
|
|
if gid not in self.db:
|
|
self.db[gid] = {
|
|
'ema': firma,
|
|
'last_cam': cam_id,
|
|
'ts': now,
|
|
'nombre': None,
|
|
'hard_negatives': [] # ⚡ Lista negra de firmas usurpadoras
|
|
}
|
|
return
|
|
|
|
rec = self.db[gid]
|
|
ema = rec.get('ema')
|
|
if ema is None:
|
|
rec['ema'] = firma
|
|
else:
|
|
for key in ['deep','color']:
|
|
if key in firma:
|
|
ema[key] = ALPHA*ema[key] + (1-ALPHA)*firma[key]
|
|
n = np.linalg.norm(ema[key])
|
|
if n > 0: ema[key] /= n
|
|
|
|
if 'ratio_hw' in firma and 'ratio_hw' in ema:
|
|
ema['ratio_hw'] = ALPHA*ema['ratio_hw'] + (1-ALPHA)*firma['ratio_hw']
|
|
|
|
if 'anatomia' in firma and firma['anatomia'] is not None:
|
|
if 'anatomia' not in ema or ema['anatomia'] is None:
|
|
ema['anatomia'] = firma['anatomia'].copy()
|
|
else:
|
|
ema['anatomia'] = ALPHA*ema['anatomia'] + (1-ALPHA)*firma['anatomia']
|
|
n = np.linalg.norm(ema['anatomia'])
|
|
if n > 0: ema['anatomia'] /= n
|
|
ema['calidad'] = firma['calidad']
|
|
|
|
rec['last_cam'] = cam_id
|
|
rec['ts'] = now
|
|
|
|
def _sim_robusta(self, firma_nueva, gid_data, cross_cam, confianza_fisica=0.0):
|
|
ema = gid_data.get('ema')
|
|
if not ema: return 0.0
|
|
return similitud_hibrida(firma_nueva, ema, cross_cam, confianza_fisica)
|
|
|
|
def identificar_candidato(self, firma, cam_id, now, active_gids, en_borde=False, firma_ema_local=None, confianza_fisica=0.0):
|
|
self.limpiar_fantasmas()
|
|
with self.lock:
|
|
candidatos = []
|
|
for gid, data in self.db.items():
|
|
distancia = self._distancia_topologica(str(data['last_cam']), str(cam_id))
|
|
misma_cam = (distancia == 0)
|
|
es_vecino = (distancia == 1)
|
|
es_salto = (distancia == 2)
|
|
es_cross = not misma_cam
|
|
|
|
if gid in active_gids:
|
|
if misma_cam or distancia >= 3: continue
|
|
|
|
dt = now - data.get('ts', now)
|
|
|
|
# ⚡ Validación Estricta de Tiempo Mínimo (Evita Teletransportes)
|
|
tiempo_min = TIEMPO_MIN_POR_DISTANCIA.get(distancia, 40.0)
|
|
if not misma_cam and dt < tiempo_min:
|
|
continue
|
|
|
|
if dt > TIEMPO_MAX_AUSENCIA: continue
|
|
if data.get('ema') is None: continue
|
|
|
|
sim = self._sim_robusta(firma, data, es_cross, confianza_fisica)
|
|
|
|
# ⚡ SISTEMA INMUNE: Comparamos contra la Lista Negra del ID
|
|
anticuerpos = data.get('hard_negatives', [])
|
|
for anticuerpo_deep in anticuerpos:
|
|
sim_contra_error = max(0.0, 1.0 - cosine(firma['deep'], anticuerpo_deep))
|
|
if sim_contra_error > 0.85:
|
|
sim -= 0.40 # Veto letal por parecerse al usurpador conocido
|
|
print(f" [INMUNE] ID {gid} protegido. Usurpador detectado (Sim: {sim_contra_error:.2f})")
|
|
break
|
|
|
|
if firma_ema_local is not None:
|
|
sim_local = self._sim_robusta(firma_ema_local, data, es_cross, confianza_fisica)
|
|
sim = 0.60*sim + 0.40*sim_local
|
|
|
|
penal_multitud = min(0.04, max(0, len(active_gids)-10) * 0.002)
|
|
|
|
if misma_cam:
|
|
tipo_distancia = "MISMA_CAM"
|
|
umbral = 0.60 if dt < 5.0 else (0.65 if dt < 30.0 else 0.68)
|
|
elif es_vecino:
|
|
tipo_distancia = "VECINO"
|
|
umbral = 0.60 if dt < 10.0 else 0.75
|
|
umbral -= confianza_fisica * 0.05
|
|
elif es_salto:
|
|
tipo_distancia = "SALTO(2)"
|
|
umbral = 0.82
|
|
else:
|
|
tipo_distancia = "MURO_LEJANO(3+)" # ⚡ LÍNEA FALTANTE
|
|
umbral = 0.94
|
|
|
|
piso_seguridad = 0.58 if (es_vecino and dt < 10.0) else 0.65
|
|
umbral = max(piso_seguridad, umbral + penal_multitud - (0.03 if data.get('nombre') and not misma_cam else 0.0))
|
|
|
|
if sim > 0.40 and not misma_cam:
|
|
estado = "ACEPTADO" if sim > umbral else "RECHAZADO"
|
|
faltante = umbral - sim if sim <= umbral else 0.0
|
|
info_faltante = f"(Faltó {faltante:.2f})" if faltante > 0 else ""
|
|
print(f" [EVAL] Cam{cam_id} evalúa ID{gid}(Viene de Cam{data['last_cam']} hace {dt:.1f}s) | Tipo: {tipo_distancia}")
|
|
print(f" -> Sim_Final={sim:.2f} vs Umbral={umbral:.2f} {info_faltante} -> {estado}")
|
|
|
|
if sim > umbral: candidatos.append((sim, gid, misma_cam, es_vecino))
|
|
|
|
if not candidatos:
|
|
nid = self.next_gid; self.next_gid += 1
|
|
self._actualizar_sin_lock(nid, firma, cam_id, now)
|
|
return nid, False
|
|
|
|
candidatos.sort(reverse=True)
|
|
best_sim, best_gid, best_misma, best_vecino = candidatos[0]
|
|
|
|
self._actualizar_sin_lock(best_gid, firma, cam_id, now)
|
|
return best_gid, True
|
|
|
|
def limpiar_fantasmas(self):
|
|
with self.lock:
|
|
ahora = time.time()
|
|
muertos = [g for g,d in self.db.items() if (d.get('nombre') is None and ahora-d.get('ts',ahora)>600) or (d.get('nombre') is not None and ahora-d.get('ts',ahora)>900)]
|
|
for g in muertos:
|
|
# Solo borrar si no tiene anticuerpos importantes
|
|
if not self.db[g].get('hard_negatives'):
|
|
del self.db[g]
|
|
|
|
def _distancia_topologica(self, cam_o, cam_d):
|
|
if cam_o == cam_d: return 0
|
|
vecinos_directos = VECINOS.get(cam_o, [])
|
|
if cam_d in vecinos_directos: return 1
|
|
for v in vecinos_directos:
|
|
if cam_d in VECINOS.get(v, []): return 2
|
|
return 3
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# FEEDBACK EN VIVO (Sistema de Aprendizaje)
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
class FeedbackCorrector:
|
|
def __init__(self, global_mem, managers):
|
|
self.global_mem = global_mem
|
|
self.managers = managers
|
|
self.modo_fusion = False
|
|
self.gid_fusion_origen = None
|
|
|
|
def _tracker_bajo_mouse(self, tile_idx, mx, my):
|
|
if tile_idx < 0 or tile_idx >= len(SECUENCIA): return None, None
|
|
cid = str(SECUENCIA[tile_idx])
|
|
mgr = self.managers.get(cid)
|
|
if not mgr: return None, None
|
|
for trk in mgr.trackers:
|
|
x1, y1, x2, y2 = trk.box
|
|
if x1 <= mx <= x2 and y1 <= my <= y2: return trk, cid
|
|
return None, None
|
|
|
|
def procesar_tecla(self, key, mouse_state):
|
|
tile_idx = mouse_state.get('tile_idx', -1)
|
|
mx = mouse_state.get('tile_x', 0)
|
|
my = mouse_state.get('tile_y', 0)
|
|
|
|
if key == ord('n'):
|
|
# ⚡ CREAR NUEVO ID + INYECTAR ANTICUERPO AL ORIGINAL
|
|
trk, cid = self._tracker_bajo_mouse(tile_idx, mx, my)
|
|
if trk and trk.gid is not None and trk.firma_ema is not None:
|
|
viejo_gid = trk.gid
|
|
with self.global_mem.lock:
|
|
# Inyectar el error en la lista negra del ID legítimo
|
|
if 'hard_negatives' not in self.global_mem.db[viejo_gid]:
|
|
self.global_mem.db[viejo_gid]['hard_negatives'] = []
|
|
self.global_mem.db[viejo_gid]['hard_negatives'].append(trk.firma_ema['deep'].copy())
|
|
self.global_mem._guardar_memoria_inmune() # Guardar en disco
|
|
|
|
# Nace el nuevo ID limpio
|
|
nid = self.global_mem.next_gid
|
|
self.global_mem.next_gid += 1
|
|
self.global_mem._actualizar_sin_lock(nid, trk.firma_ema, cid, time.time())
|
|
|
|
trk.gid = nid
|
|
trk.origen_global = False
|
|
print(f"\n[APRENDIZAJE ACTIVO] ¡Error corregido!")
|
|
print(f" -> La firma intrusa se inyectó como Anticuerpo en el ID {viejo_gid}.")
|
|
print(f" -> Nace el ID {nid} de forma independiente.")
|
|
return True
|
|
|
|
elif key == ord('m'):
|
|
# FUSIONAR IDs
|
|
trk, cid = self._tracker_bajo_mouse(tile_idx, mx, my)
|
|
if trk and trk.gid is not None:
|
|
if not self.modo_fusion:
|
|
self.modo_fusion = True
|
|
self.gid_fusion_origen = trk.gid
|
|
print(f"\n[FUSIÓN] Seleccionaste ID {trk.gid} como ORIGEN. Ahora clickea el DESTINO y presiona 'm'.")
|
|
else:
|
|
gid_destino = trk.gid
|
|
if gid_destino != self.gid_fusion_origen:
|
|
with self.global_mem.lock:
|
|
if gid_destino in self.global_mem.db and self.gid_fusion_origen in self.global_mem.db:
|
|
origen_firmas = self.global_mem.db[self.gid_fusion_origen].get('ema')
|
|
if origen_firmas:
|
|
self.global_mem._actualizar_sin_lock(gid_destino, origen_firmas, cid, time.time())
|
|
self.global_mem.db[self.gid_fusion_origen]['fusionado_con'] = gid_destino
|
|
for m in self.managers.values():
|
|
for t in m.trackers:
|
|
if t.gid == self.gid_fusion_origen: t.gid = gid_destino
|
|
print(f"\n[FUSIÓN] Éxito: ID {self.gid_fusion_origen} absorbido por ID {gid_destino}")
|
|
self.modo_fusion = False
|
|
self.gid_fusion_origen = None
|
|
return True
|
|
|
|
elif key == ord('c'):
|
|
if self.modo_fusion:
|
|
print("\n[FUSIÓN] Modo fusión cancelado.")
|
|
self.modo_fusion = False
|
|
self.gid_fusion_origen = None
|
|
return True
|
|
return False
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# GESTOR LOCAL Y RESTO DEL CÓDIGO
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
def iou_overlap(A, B):
|
|
xA,yA = max(A[0],B[0]),max(A[1],B[1])
|
|
xB,yB = min(A[2],B[2]),min(A[3],B[3])
|
|
inter = max(0,xB-xA)*max(0,yB-yA)
|
|
return inter/((A[2]-A[0])*(A[3]-A[1])+(B[2]-B[0])*(B[3]-B[1])-inter+1e-6)
|
|
|
|
class CamManager:
|
|
def __init__(self, cam_id, global_mem):
|
|
self.cam_id, self.global_mem, self.trackers = cam_id, global_mem, []
|
|
|
|
def _detectar_grupo(self, trk, box, todos):
|
|
x1,y1,x2,y2 = box
|
|
w,h = x2-x1, y2-y1
|
|
cx,cy = (x1+x2)/2, (y1+y2)/2
|
|
fx, fy = (1.1, 0.45) if getattr(trk,'en_grupo',False) else (0.9, 0.35)
|
|
for o in todos:
|
|
if o is trk or not hasattr(o,'box') or o.box is None or o.time_since_update > 2: continue
|
|
if iou_overlap(box, o.box) > 0.05: return True
|
|
ocx,ocy = (o.box[0]+o.box[2])/2, (o.box[1]+o.box[3])/2
|
|
if abs(cx-ocx)<w*fx and abs(cy-ocy)<h*fy: return True
|
|
return False
|
|
|
|
def _asignar(self, boxes, confidences, frame_hd, now, keypoints_list):
|
|
n_trk, n_det = len(self.trackers), len(boxes)
|
|
firmas = [None] * n_det
|
|
|
|
def obtener_firma(d_idx):
|
|
if firmas[d_idx] is None:
|
|
kpts = keypoints_list[d_idx] if keypoints_list and d_idx < len(keypoints_list) else None
|
|
firmas[d_idx] = extraer_firma_hibrida(frame_hd, boxes[d_idx], kpts)
|
|
return firmas[d_idx]
|
|
|
|
if n_trk == 0: return [], list(range(n_det)), [], firmas
|
|
if n_det == 0: return [], [], list(range(n_trk)), firmas
|
|
|
|
alta = [(d,boxes[d]) for d,c in enumerate(confidences) if c >= 0.45]
|
|
baja = [(d,boxes[d]) for d,c in enumerate(confidences) if c < 0.45]
|
|
matched, sin_match_trk = [], list(range(n_trk))
|
|
|
|
if alta:
|
|
C = np.full((n_trk, len(alta)), 100.0, np.float32)
|
|
for t, trk in enumerate(self.trackers):
|
|
dt_oculto = now - trk.ts_ultima_deteccion
|
|
radio = min(400., max(150., 300.*math.sqrt(max(0.1,dt_oculto))))
|
|
ref_firma = trk.firma_ema or (self.global_mem.db.get(trk.gid, {}).get('ema') if trk.gid else None)
|
|
|
|
for idx, (d, det) in enumerate(alta):
|
|
iou = iou_overlap(trk.box, det)
|
|
dist = math.sqrt(((trk.box[0]+trk.box[2])/2-(det[0]+det[2])/2)**2 + ((trk.box[1]+trk.box[3])/2-(det[1]+det[3])/2)**2)
|
|
if dist > radio: continue
|
|
|
|
a_trk = (trk.box[2]-trk.box[0])*(trk.box[3]-trk.box[1])
|
|
a_det = (det[2]-det[0])*(det[3]-det[1])
|
|
costo = (1.5*(1-iou)) + (0.5*(dist/radio)) + ((max(a_trk,a_det)/(min(a_trk,a_det)+1e-6)-1)*0.2) + (abs((trk.box[3]-trk.box[1])/(trk.box[2]-trk.box[0]+1e-6) - (det[3]-det[1])/(det[2]-det[0]+1e-6))*0.2) + min(0.5, dt_oculto*0.2)
|
|
|
|
if ref_firma is not None and obtener_firma(d) is not None:
|
|
sim_ap = similitud_hibrida(ref_firma, firmas[d])
|
|
if sim_ap < 0.25: costo = 100.0 if trk.frames_observados >= 5 else costo + 1.2
|
|
elif sim_ap < 0.45: costo += (0.45-sim_ap)*(3.5 if trk.frames_observados >= 5 else 1.5)
|
|
|
|
C[t,idx] = costo
|
|
|
|
for r,c in zip(*linear_sum_assignment(C)):
|
|
if C[r,c] <= 6.5:
|
|
matched.append((r, alta[c][0])); sin_match_trk.remove(r)
|
|
|
|
if sin_match_trk and baja:
|
|
C2 = np.ones((len(sin_match_trk), len(baja)), np.float32)
|
|
for ti, trk_i in enumerate(sin_match_trk):
|
|
for dj, (d,det) in enumerate(baja): C2[ti,dj] = 1.0 - iou_overlap(self.trackers[trk_i].box, det)
|
|
nuevos_sin = []
|
|
for r,c in zip(*linear_sum_assignment(C2)):
|
|
if C2[r,c] < 0.72: matched.append((sin_match_trk[r], baja[c][0]))
|
|
else: nuevos_sin.append(sin_match_trk[r])
|
|
sin_match_trk = nuevos_sin
|
|
|
|
return matched, [d for d in range(n_det) if d not in [m[1] for m in matched] and confidences[d] >= 0.52], sin_match_trk, firmas
|
|
|
|
def update(self, boxes, confidences, keypoints, frame_show, frame_hd, now, turno_activo):
|
|
for trk in self.trackers:
|
|
if trk.gid:
|
|
with self.global_mem.lock:
|
|
d = self.global_mem.db.get(trk.gid)
|
|
if d and 'fusionado_con' in d: trk.gid = d['fusionado_con']
|
|
|
|
self.trackers = [t for t in self.trackers if t.predict(turno_activo) is not None]
|
|
if not turno_activo: return self.trackers
|
|
|
|
matched, new_dets, _, firmas = self._asignar(boxes, confidences, frame_hd, now, keypoints)
|
|
active_gids = {t.gid for t in self.trackers if t.gid is not None}
|
|
fh, fw = frame_hd.shape[:2]
|
|
|
|
for t_idx, d_idx in matched:
|
|
trk, box = self.trackers[t_idx], boxes[d_idx]
|
|
if firmas[d_idx] is None:
|
|
kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None
|
|
firmas[d_idx] = extraer_firma_hibrida(frame_hd, box, kpts)
|
|
|
|
firma_det = firmas[d_idx]
|
|
es_grupo = self._detectar_grupo(trk, box, self.trackers)
|
|
|
|
if not trk.en_grupo and es_grupo:
|
|
if trk.gid:
|
|
with self.global_mem.lock: trk.firma_pre_grupo = self.global_mem.db.get(trk.gid,{}).get('ema')
|
|
trk.ts_salio_grupo = 0.0
|
|
elif trk.en_grupo and not es_grupo: trk.ts_salio_grupo = now
|
|
|
|
trk.en_grupo = es_grupo
|
|
kpts_persona = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None
|
|
trk.update(box, es_grupo, now, kpts=kpts_persona)
|
|
|
|
if firma_det is not None and not es_grupo and (trk.gid is None or trk.frames_observados < 30 or (box[0]<fw*.15 or box[1]<fh*.15 or box[2]>fw*.85 or box[3]>fh*.85)):
|
|
trk.actualizar_ema(firma_det)
|
|
|
|
if not trk.en_grupo and not ((now - getattr(trk,'ts_salio_grupo',0) < 2.0) and getattr(trk,'ts_salio_grupo',0) > 0):
|
|
if trk.gid is None and trk.listo_para_id and firma_det is not None and not (box[0]<5 or box[1]<5 or box[2]>fw-5 or box[3]>fh-5):
|
|
gid_c, es_reid = self.global_mem.identificar_candidato(
|
|
firma_det, self.cam_id, now, active_gids,
|
|
en_borde=(box[0]<35 or box[1]<35 or box[2]>fw-35 or box[3]>fh-35),
|
|
firma_ema_local=trk.firma_ema,
|
|
confianza_fisica=trk.confianza_fisica
|
|
)
|
|
if gid_c:
|
|
active_gids.add(gid_c); trk.gid = gid_c; trk.origen_global = es_reid
|
|
|
|
elif trk.gid is not None and now-trk.ultimo_aprendizaje > 1.5 and (box[0]<fw*.15 or box[1]<fh*.15 or box[2]>fw*.85 or box[3]>fh*.85) and analizar_calidad(box, kpts=kpts_persona, estricto=True) and firma_det is not None and not (box[0]<15 or box[1]<15 or box[2]>fw-15 or box[3]>fh-15):
|
|
with self.global_mem.lock:
|
|
rec = self.global_mem.db.get(trk.gid)
|
|
if rec and rec.get('ema'):
|
|
sim_ema = similitud_hibrida(firma_det, rec['ema'], confianza_fisica=trk.confianza_fisica)
|
|
if sim_ema < 0.28 and trk.confianza_fisica < 0.30:
|
|
trk.gid = None; trk.listo_para_id = False; trk.frames_buena_calidad = 0; continue
|
|
if sim_ema > max(0.30, 0.48 - trk.confianza_fisica*0.15):
|
|
self.global_mem._actualizar_sin_lock(trk.gid, firma_det, self.cam_id, now)
|
|
trk.ultimo_aprendizaje = now; trk.aprendiendo = True
|
|
|
|
for d_idx in new_dets:
|
|
nt = KalmanTrack(boxes[d_idx], now)
|
|
kpts = keypoints[d_idx] if keypoints and d_idx < len(keypoints) else None
|
|
if firmas[d_idx] is None: firmas[d_idx] = extraer_firma_hibrida(frame_hd, boxes[d_idx], kpts)
|
|
if firmas[d_idx]: nt.actualizar_ema(firmas[d_idx])
|
|
self.trackers.append(nt)
|
|
|
|
self.trackers = [t for t in self.trackers if not (t.gid is None and (now - t.ts_creacion) > 4.0) and (now - t.ts_ultima_deteccion) < ((1.0 if (t.box[0]<20 or t.box[1]<20 or t.box[2]>fw-20 or t.box[3]>fh-20) else 2.5) if t.gid is None else (3.0 if (t.box[0]<20 or t.box[1]<20 or t.box[2]>fw-20 or t.box[3]>fh-20) else 8.0))]
|
|
return self.trackers
|
|
|
|
class CamStream:
|
|
def __init__(self, url):
|
|
self.url = url; self.cap = cv2.VideoCapture(url)
|
|
self.q = queue.Queue(maxsize=1); self.stopped = False
|
|
threading.Thread(target=self._run, daemon=True).start()
|
|
def _run(self):
|
|
while not self.stopped:
|
|
ret, f = self.cap.read()
|
|
if not ret: time.sleep(2); self.cap.open(self.url); continue
|
|
if self.q.full():
|
|
try: self.q.get_nowait()
|
|
except queue.Empty: pass
|
|
self.q.put(f)
|
|
@property
|
|
def frame(self):
|
|
try: return self.q.get(timeout=0.5)
|
|
except queue.Empty: return None
|
|
def stop(self): self.stopped = True; self.cap.release()
|
|
|
|
def dibujar_track(frame_show, trk, seleccionado=False):
|
|
try: x1,y1,x2,y2 = map(int,trk.box)
|
|
except: return
|
|
if seleccionado:
|
|
color, label = C_FEEDBACK, f"SEL:{trk.gid if trk.gid is not None else trk.local_id}"
|
|
elif trk.gid is None: color,label = C_CANDIDATO, f"?{trk.local_id}"
|
|
elif getattr(trk,'en_grupo',False): color,label = C_GRUPO, f"ID:{trk.gid}[G]"
|
|
elif getattr(trk,'aprendiendo',False): color,label = C_APRENDIZAJE, f"ID:{trk.gid}[+]"
|
|
elif getattr(trk,'origen_global',False): color,label = C_GLOBAL, f"ID:{trk.gid}[R]"
|
|
else: color,label = C_LOCAL, 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)
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# MAIN
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
def main():
|
|
print("\n--- Iniciando Sistema SmartSoft (Versión Anti-Uniformes e Inmunológica) ---")
|
|
print("Controles de Aprendizaje en Vivo (Apunta con el Mouse):")
|
|
print(" [n] - ¡Te equivocaste! Separar este ID y marcarlo como usurpador (Anticuerpo).")
|
|
print(" [m] - ¡Son el mismo! Fusionar IDs (click origen -> click destino -> 'm').")
|
|
print(" [c] - Cancelar fusión.")
|
|
print(" [q] - Salir.\n")
|
|
|
|
model = YOLO("yolov8n-pose.pt")
|
|
global_mem = GlobalMemory()
|
|
managers = {str(c): CamManager(c, global_mem) for c in SECUENCIA}
|
|
cams = [CamStream(u) for u in URLS]
|
|
feedback = FeedbackCorrector(global_mem, managers)
|
|
|
|
cv2.namedWindow("SmartSoft", cv2.WINDOW_AUTOSIZE)
|
|
|
|
mouse_state = {'x': 0, 'y': 0, 'tile_idx': -1, 'tile_x': 0, 'tile_y': 0}
|
|
def on_mouse(event, x, y, flags, param):
|
|
mouse_state['x'] = x; mouse_state['y'] = y
|
|
col, row = x // 480, y // 270
|
|
if 0 <= col < 3 and 0 <= row < 2:
|
|
mouse_state['tile_idx'] = row * 3 + col
|
|
mouse_state['tile_x'] = x % 480
|
|
mouse_state['tile_y'] = y % 270
|
|
else: mouse_state['tile_idx'] = -1
|
|
cv2.setMouseCallback("SmartSoft", on_mouse)
|
|
|
|
idx = 0
|
|
while True:
|
|
now, tiles = time.time(), []
|
|
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,confs,kpts = [],[],[]
|
|
turno_activo = (i == cam_ia)
|
|
|
|
if turno_activo:
|
|
res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu')
|
|
if res[0].boxes:
|
|
raw_boxes = res[0].boxes.xyxy.cpu().numpy().tolist()
|
|
raw_confs = res[0].boxes.conf.cpu().numpy().tolist()
|
|
raw_kpts = res[0].keypoints.data.cpu().numpy().tolist() if res[0].keypoints is not None else []
|
|
|
|
for d_idx, (box, conf) in enumerate(zip(raw_boxes, raw_confs)):
|
|
kpt_persona = raw_kpts[d_idx] if d_idx < len(raw_kpts) else None
|
|
es_humano = False
|
|
|
|
if kpt_persona is not None:
|
|
p_validos = sum(1 for p in kpt_persona if p[2] > 0.30)
|
|
nariz_ok = kpt_persona[0][2] > 0.30
|
|
hombro_ok = kpt_persona[5][2] > 0.30 or kpt_persona[6][2] > 0.30
|
|
cadera_ok = kpt_persona[11][2] > 0.30 or kpt_persona[12][2] > 0.30
|
|
if p_validos >= 7 and nariz_ok and hombro_ok and cadera_ok: es_humano = True
|
|
else:
|
|
if analizar_calidad(box, kpts=None, estricto=True): es_humano = True
|
|
|
|
if es_humano:
|
|
boxes.append(box); confs.append(conf); kpts.append(kpt_persona)
|
|
|
|
ESQUELETO = [(0,1),(0,2),(1,3),(2,4),(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)]
|
|
for pk in kpts:
|
|
if pk is None: continue
|
|
for a,b in ESQUELETO:
|
|
if pk[a][2]>0.40 and pk[b][2]>0.40:
|
|
cv2.line(frame_show, (int(pk[a][0]),int(pk[a][1])), (int(pk[b][0]),int(pk[b][1])), (255,0,255), 2)
|
|
for kx, ky, kc in pk:
|
|
if kc > 0.40: cv2.circle(frame_show, (int(kx), int(ky)), 3, (0, 255, 255), -1)
|
|
|
|
tracks = managers[cid].update(boxes, confs, kpts, frame_show, frame, now, turno_activo)
|
|
|
|
sel_trk = None
|
|
if mouse_state['tile_idx'] == i:
|
|
for trk in tracks:
|
|
x1,y1,x2,y2 = trk.box
|
|
if x1<=mouse_state['tile_x']<=x2 and y1<=mouse_state['tile_y']<=y2:
|
|
sel_trk = trk; break
|
|
|
|
for trk in tracks:
|
|
if trk.time_since_update <= 1: dibujar_track(frame_show, trk, seleccionado=(trk is sel_trk))
|
|
if turno_activo: cv2.circle(frame_show,(460,20),6,(0,0,255),-1)
|
|
|
|
con_id = sum(1 for t in tracks if getattr(t,'gid',None) and t.time_since_update==0)
|
|
cv2.putText(frame_show,f"CAM {cid} [{con_id} ID]",(10,28),FUENTE,0.7,(255,255,255),2)
|
|
if feedback.modo_fusion and mouse_state['tile_idx'] == i:
|
|
cv2.putText(frame_show, "MODO FUSION", (10, 55), FUENTE, 0.6, (0, 0, 255), 2)
|
|
|
|
tiles.append(frame_show)
|
|
|
|
if len(tiles)==6:
|
|
cv2.imshow("SmartSoft", np.vstack([np.hstack(tiles[:3]),np.hstack(tiles[3:])]))
|
|
|
|
idx += 1
|
|
key = cv2.waitKey(1) & 0xFF
|
|
if key in [ord('n'), ord('m'), ord('c')]: feedback.procesar_tecla(key, mouse_state)
|
|
elif key == ord('q'): break
|
|
|
|
cv2.destroyAllWindows()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|