IdentificacionIA/vesiones_seguras.txt

1259 lines
57 KiB
Plaintext
Raw Normal View History

############################################################ seguimiento2.py
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
# ──────────────────────────────────────────────────────────────────────────────
# CONFIGURACIÓN DEL SISTEMA
# ──────────────────────────────────────────────────────────────────────────────
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65"
SECUENCIA = [1, 7, 5, 8, 3, 6]
# 🛡️ RED ESTABILIZADA (Timeout de 3s para evitar congelamientos de FFmpeg)
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|stimeout;3000000"
URLS = [f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/{i}02" for i in SECUENCIA]
ONNX_MODEL_PATH = "osnet_x0_25_msmt17.onnx"
VECINOS = {
"1": ["7"], "7": ["1", "5"], "5": ["7", "8"],
"8": ["5", "3"], "3": ["8", "6"], "6": ["3"]
}
ASPECT_RATIO_MIN = 0.5
ASPECT_RATIO_MAX = 4.0
AREA_MIN_CALIDAD = 1200
FRAMES_CALIDAD = 2
TIEMPO_MAX_AUSENCIA = 800.0
# ⚡ UMBRALES MAESTROS: Tolerancia altísima entre cámaras vecinas para ignorar cambios de luz
UMBRAL_REID_MISMA_CAM = 0.62
UMBRAL_REID_VECINO = 0.53
UMBRAL_REID_NO_VECINO = 0.72
MAX_FIRMAS_MEMORIA = 15
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)
FUENTE = cv2.FONT_HERSHEY_SIMPLEX
# ──────────────────────────────────────────────────────────────────────────────
# INICIALIZACIÓN OSNET
# ──────────────────────────────────────────────────────────────────────────────
print("Cargando cerebro de Re-Identificación (OSNet)...")
try:
ort_session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider'])
input_name = ort_session.get_inputs()[0].name
print("Modelo OSNet cargado exitosamente.")
except Exception as e:
print(f"ERROR FATAL: No se pudo cargar {ONNX_MODEL_PATH}.")
exit()
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 3, 1, 1)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 3, 1, 1)
# ──────────────────────────────────────────────────────────────────────────────
# 1. EXTRACCIÓN DE FIRMAS (Deep + Color + Textura)
# ──────────────────────────────────────────────────────────────────────────────
def analizar_calidad(box):
x1, y1, x2, y2 = box
w, h = x2 - x1, y2 - y1
if w <= 0 or h <= 0: return False
return (ASPECT_RATIO_MIN < (h / w) < ASPECT_RATIO_MAX) and ((w * h) > AREA_MIN_CALIDAD)
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
img = np.expand_dims(img, axis=0)
img = (img - MEAN) / STD
return img
def extraer_color_zonas(img):
h_roi = img.shape[0]
t1, t2 = int(h_roi * 0.15), int(h_roi * 0.55)
zonas = [img[:t1, :], img[t1:t2, :], img[t2:, :]]
def hist_zona(z):
if z.size == 0: return np.zeros(16 * 8)
hsv = cv2.cvtColor(z, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [16, 8], [0, 180, 0, 256])
cv2.normalize(hist, hist)
return hist.flatten()
return np.concatenate([hist_zona(z) for z in zonas])
def extraer_textura_rapida(roi):
if roi.size == 0: return np.zeros(16)
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray_eq = cv2.equalizeHist(gray)
gx = cv2.Sobel(gray_eq, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(gray_eq, cv2.CV_32F, 0, 1, ksize=3)
mag, _ = cv2.cartToPolar(gx, gy)
hist = cv2.calcHist([mag], [0], None, [16], [0, 256])
cv2.normalize(hist, hist)
return hist.flatten()
def extraer_firma_hibrida(frame, box):
try:
x1, y1, x2, y2 = map(int, box)
fh, fw = frame.shape[:2]
x1_c, y1_c = max(0, x1), max(0, y1)
x2_c, y2_c = min(fw, x2), min(fh, y2)
roi = frame[y1_c:y2_c, x1_c:x2_c]
if roi.size == 0 or roi.shape[0] < 20 or roi.shape[1] < 10: return None
calidad_area = (x2_c - x1_c) * (y2_c - y1_c)
blob = preprocess_onnx(roi)
blob_16 = np.zeros((16, 3, 256, 128), dtype=np.float32)
blob_16[0] = blob[0]
deep_feat = ort_session.run(None, {input_name: blob_16})[0][0].flatten()
norma = np.linalg.norm(deep_feat)
if norma > 0: deep_feat = deep_feat / norma
color_feat = extraer_color_zonas(roi)
textura_feat = extraer_textura_rapida(roi)
return {'deep': deep_feat, 'color': color_feat, 'textura': textura_feat, 'calidad': calidad_area}
except Exception:
return None
# ⚡ EL SECRETO: 100% IA entre cámaras. Textura solo en la misma cámara.
def similitud_hibrida(f1, f2, cross_cam=False):
if f1 is None or f2 is None: return 0.0
sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep']))
if cross_cam:
# Si saltó de cámara, la luz cambia. Ignoramos color y textura. Confiamos 100% en OSNet.
return sim_deep
# Si está en la misma cámara, usamos color y textura para separar a los vestidos de negro.
if f1['color'].shape == f2['color'].shape and f1['color'].size > 1:
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_torso = max(0.0, float(cv2.compareHist(f1['color'][L:2*L].astype(np.float32), f2['color'][L:2*L].astype(np.float32), cv2.HISTCMP_CORREL)))
sim_legs = max(0.0, float(cv2.compareHist(f1['color'][2*L:].astype(np.float32), f2['color'][2*L:].astype(np.float32), cv2.HISTCMP_CORREL)))
sim_color = (0.10 * sim_head) + (0.60 * sim_torso) + (0.30 * sim_legs)
else: sim_color = 0.0
if 'textura' in f1 and 'textura' in f2 and f1['textura'].size > 1:
sim_textura = max(0.0, float(cv2.compareHist(f1['textura'].astype(np.float32), f2['textura'].astype(np.float32), cv2.HISTCMP_CORREL)))
else: sim_textura = 0.0
return (sim_deep * 0.80) + (sim_color * 0.10) + (sim_textura * 0.10)
# ──────────────────────────────────────────────────────────────────────────────
# 2. KALMAN TRACKER
# ──────────────────────────────────────────────────────────────────────────────
class KalmanTrack:
_count = 0
def __init__(self, box, now):
self.kf = cv2.KalmanFilter(7, 4)
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[0,4] = 1; self.kf.transitionMatrix[1,5] = 1; self.kf.transitionMatrix[2,6] = 1
self.kf.processNoiseCov *= 0.03
self.kf.statePost = np.zeros((7, 1), np.float32)
self.kf.statePost[:4] = self._convert_bbox_to_z(box)
self.local_id = KalmanTrack._count
KalmanTrack._count += 1
self.gid = None
self.origen_global = False
self.aprendiendo = False
self.box = list(box)
self.ts_creacion = now
self.ts_ultima_deteccion = now
self.time_since_update = 0
self.en_grupo = False
self.frames_buena_calidad = 0
self.listo_para_id = False
self.area_referencia = 0.0
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.
return np.array([[x],[y],[w*h],[w/float(h+1e-6)]]).astype(np.float32)
def _convert_x_to_bbox(self, x):
cx, cy, s, r = float(x[0].item()), float(x[1].item()), float(x[2].item()), float(x[3].item())
w = np.sqrt(s * r); h = s / (w + 1e-6)
return [cx-w/2., cy-h/2., cx+w/2., cy+h/2.]
def predict(self, turno_activo=True):
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 update(self, box, en_grupo, now):
self.ts_ultima_deteccion = now
self.time_since_update = 0
self.box = list(box)
self.en_grupo = en_grupo
self.kf.correct(self._convert_bbox_to_z(box))
if analizar_calidad(box) and not en_grupo:
self.frames_buena_calidad += 1
if self.frames_buena_calidad >= FRAMES_CALIDAD:
self.listo_para_id = True
elif self.gid is None:
self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1)
# ──────────────────────────────────────────────────────────────────────────────
# 3. MEMORIA GLOBAL (Anti-Robos y Físicas de Tiempo)
# ──────────────────────────────────────────────────────────────────────────────
class GlobalMemory:
def __init__(self):
self.db = {}
self.next_gid = 100
self.lock = threading.Lock()
def _es_transito_posible(self, data, cam_destino, now):
ultima_cam = str(data['last_cam'])
cam_destino = str(cam_destino)
dt = now - data['ts']
if ultima_cam == cam_destino: return True
vecinos = VECINOS.get(ultima_cam, [])
# Permite teletransportación mínima (-0.5s) para que no te fragmente en los pasillos conectados
if cam_destino in vecinos: return dt >= -0.5
return dt >= 4.0
def _sim_robusta(self, firma_nueva, firmas_guardadas, cross_cam=False):
if not firmas_guardadas: return 0.0
sims = sorted([similitud_hibrida(firma_nueva, f, cross_cam) for f in firmas_guardadas], reverse=True)
if len(sims) == 1: return sims[0]
elif len(sims) <= 4: return (sims[0] * 0.6) + (sims[1] * 0.4)
else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20)
# ⚡ SE AGREGÓ 'en_borde' A LOS PARÁMETROS
def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids, en_borde=True):
with self.lock:
candidatos = []
vecinos = VECINOS.get(str(cam_id), [])
for gid, data in self.db.items():
if gid in active_gids: continue
dt = now - data['ts']
if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue
if not data['firmas']: continue
misma_cam = (str(data['last_cam']) == str(cam_id))
es_cross_cam = not misma_cam
es_vecino = str(data['last_cam']) in vecinos
# ⚡ FÍSICA DE PUERTAS: Si "nació" en el centro de la pantalla, NO viene caminando del pasillo adyacente.
if es_vecino and not en_borde:
es_vecino = False
sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=es_cross_cam)
if misma_cam: umbral = UMBRAL_REID_MISMA_CAM
elif es_vecino: umbral = UMBRAL_REID_VECINO
else: umbral = UMBRAL_REID_NO_VECINO
# 🛡️ PROTECCIÓN VIP: Si este ID ya tiene un nombre real asignado por ArcFace,
# nos volvemos súper estrictos (+0.08) para que un desconocido no se lo robe.
if data.get('nombre') is not None:
umbral += 0.08
if sim > umbral:
candidatos.append((sim, gid))
if not candidatos:
nid = self.next_gid; self.next_gid += 1
self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now)
return nid, False
candidatos.sort(reverse=True)
best_sim, best_gid = candidatos[0]
if len(candidatos) >= 2:
segunda_sim, segundo_gid = candidatos[1]
margen = best_sim - segunda_sim
if margen <= 0.02 and best_sim < 0.75:
print(f"\n[⚠️ ALERTA ROPA SIMILAR] Empate técnico entre ID {best_gid} ({best_sim:.2f}) y ID {segundo_gid} ({segunda_sim:.2f}). Se asigna ID temporal nuevo.")
nid = self.next_gid; self.next_gid += 1
self._actualizar_sin_lock(nid, firma_hibrida, cam_id, now)
return nid, False
self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now)
return best_gid, True
def _actualizar_sin_lock(self, gid, firma_dict, cam_id, now):
if gid not in self.db: self.db[gid] = {'firmas': [], 'last_cam': cam_id, 'ts': now}
if firma_dict is not None:
firmas_list = self.db[gid]['firmas']
if not firmas_list:
firmas_list.append(firma_dict)
else:
if firma_dict['calidad'] > (firmas_list[0]['calidad'] * 1.50):
vieja_ancla = firmas_list[0]; firmas_list[0] = firma_dict; firma_dict = vieja_ancla
if len(firmas_list) >= MAX_FIRMAS_MEMORIA:
max_sim_interna = -1.0; idx_redundante = 1
for i in range(1, len(firmas_list)):
sims_con_otras = [similitud_hibrida(firmas_list[i], firmas_list[j]) for j in range(1, len(firmas_list)) if j != i]
sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0
if sim_promedio > max_sim_interna: max_sim_interna = sim_promedio; idx_redundante = i
firmas_list[idx_redundante] = firma_dict
else:
firmas_list.append(firma_dict)
self.db[gid]['last_cam'] = cam_id
self.db[gid]['ts'] = now
def actualizar(self, gid, firma, cam_id, now):
with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now)
# ──────────────────────────────────────────────────────────────────────────────
# 4. GESTOR LOCAL (Kalman Elasticity & Ghost Killer)
# ──────────────────────────────────────────────────────────────────────────────
def iou_overlap(boxA, boxB):
xA, yA, xB, yB = max(boxA[0], boxB[0]), max(boxA[1], boxB[1]), min(boxA[2], boxB[2]), min(boxA[3], boxB[3])
inter = max(0, xB-xA) * max(0, yB-yA)
areaA = (boxA[2]-boxA[0]) * (boxA[3]-boxA[1]); areaB = (boxB[2]-boxB[0]) * (boxB[3]-boxB[1])
return inter / (areaA + areaB - 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 update(self, boxes, frame, now, turno_activo):
for trk in self.trackers: trk.predict(turno_activo=turno_activo)
if not turno_activo: return self.trackers
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}
area_actual = (box[2] - box[0]) * (box[3] - box[1])
# IGNORAMOS VECTORES MUTANTES DE GRUPOS
if trk.gid is None and trk.listo_para_id and not trk.en_grupo:
firma = extraer_firma_hibrida(frame, box)
if firma is not None:
# ⚡ DETECCIÓN DE ZONA DE NACIMIENTO
fh, fw = frame.shape[:2]
bx1, by1, bx2, by2 = map(int, box)
# Si nace a menos de 40 píxeles del margen, entró por el pasillo
nace_en_borde = (bx1 < 80 or by1 < 80 or bx2 > fw - 80 or by2 > fh - 80)
# Mandamos esa información al identificador
gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids, en_borde=nace_en_borde)
trk.gid, trk.origen_global, trk.area_referencia = gid, es_reid, area_actual
elif trk.gid is not None and not trk.en_grupo:
tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0)
# ⚡ APRENDIZAJE RÁPIDO: Bajamos de 1.5s a 0.5s para que llene la memoria volando
if (now - tiempo_ultima_firma) > 0.5 and analizar_calidad(box):
fh, fw = frame.shape[:2]
x1, y1, x2, y2 = map(int, box)
en_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15)
if not en_borde:
firma_nueva = extraer_firma_hibrida(frame, box)
if firma_nueva is not None:
with self.global_mem.lock:
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]
sim_coherencia = similitud_hibrida(firma_nueva, firma_reciente)
# Tolerancia relajada a 0.50 para permitir la transición de la espalda
if sim_coherencia > 0.50:
es_coherente = True
for otro_gid, otro_data in self.global_mem.db.items():
if otro_gid == trk.gid or not otro_data['firmas']: continue
sim_intruso = similitud_hibrida(firma_nueva, otro_data['firmas'][0])
if sim_intruso > sim_coherencia:
es_coherente = False
break
if es_coherente:
self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now)
trk.ultimo_aprendizaje = now
trk.aprendiendo = True
for d_idx in unmatched_dets: self.trackers.append(KalmanTrack(boxes[d_idx], now))
vivos = []
fh, fw = frame.shape[:2]
for t in self.trackers:
x1, y1, x2, y2 = t.box
toca_borde = (x1 < 15 or y1 < 15 or x2 > fw - 15 or y2 > fh - 15)
tiempo_oculto = now - t.ts_ultima_deteccion
# ⚡ MUERTE DE FANTASMAS: Si toca el borde muere en 1s. Evita robo de IDs.
limite_vida = 1.0 if toca_borde else 10.0
if tiempo_oculto < limite_vida:
vivos.append(t)
self.trackers = vivos
return self.trackers
def _asignar(self, boxes, now):
n_trk = len(self.trackers); n_det = len(boxes)
if n_trk == 0: return [], list(range(n_det)), []
if n_det == 0: return [], [], list(range(n_trk))
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 d, det in enumerate(boxes):
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_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
area_trk = (trk.box[2] - trk.box[0]) * (trk.box[3] - trk.box[1])
area_det = (det[2] - det[0]) * (det[3] - det[1])
ratio_area = max(area_trk, area_det) / (min(area_trk, area_det) + 1e-6)
castigo_tam = (ratio_area - 1.0) * 0.7
tiempo_oculto = now - trk.ts_ultima_deteccion
if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10:
fantasma_penalty = 5.0
else: fantasma_penalty = 0.0
if iou >= 0.05 or dist_norm < 0.80:
cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tam
else: cost_mat[t, d] = 100.0
row_ind, col_ind = linear_sum_assignment(cost_mat)
matched, unmatched_dets, unmatched_trks = [], [], []
for r, c in zip(row_ind, col_ind):
# ⚡ CAJAS PEGAJOSAS: 6.0 evita que suelte el ID si te mueves rápido
if cost_mat[r, c] > 7.0:
unmatched_trks.append(r); unmatched_dets.append(c)
else: matched.append((r, c))
for t in range(n_trk):
if t not in [m[0] for m in matched]: unmatched_trks.append(t)
for d in range(n_det):
if d not in [m[1] for m in matched]: unmatched_dets.append(d)
return matched, unmatched_dets, unmatched_trks
# ──────────────────────────────────────────────────────────────────────────────
# 5. STREAM Y MAIN LOOP (Standalone)
# ──────────────────────────────────────────────────────────────────────────────
class CamStream:
def __init__(self, url):
self.url, self.cap = url, 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(frame_show, trk):
try: x1, y1, x2, y2 = map(int, trk.box)
except Exception: return
if trk.gid is None: color, label = C_CANDIDATO, f"?{trk.local_id}"
elif trk.en_grupo: color, label = C_GRUPO, f"ID:{trk.gid} [grp]"
elif trk.aprendiendo: color, label = C_APRENDIZAJE, f"ID:{trk.gid} [++]"
elif trk.origen_global: color, label = C_GLOBAL, f"ID:{trk.gid} [re-id]"
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)
def main():
print("Iniciando Sistema V-PRO — Tracker Resiliente (Código Unificado Maestro)")
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]
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:
res = model.predict(frame_show, conf=0.50, iou=0.40, classes=[0], verbose=False, imgsz=480, device='cpu')
if res[0].boxes: boxes = res[0].boxes.xyxy.cpu().numpy().tolist()
tracks = managers[cid].update(boxes, frame_show, now, turno_activo)
for trk in tracks:
if trk.time_since_update <= 1: dibujar_track(frame_show, trk)
if turno_activo: cv2.circle(frame_show, (460, 20), 6, (0, 0, 255), -1)
con_id = sum(1 for t in tracks if t.gid 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)
tiles.append(frame_show)
if len(tiles) == 6: cv2.imshow("SmartSoft", np.vstack([np.hstack(tiles[0:3]), np.hstack(tiles[3:6])]))
idx += 1
if cv2.waitKey(1) == ord('q'): break
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
############################################################### fusion.py
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"
import cv2
import numpy as np
import time
import threading
from queue import Queue
from deepface import DeepFace
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 y audio
from reconocimiento2 import (
gestionar_vectores,
detectar_rostros_yunet,
buscar_mejor_match,
hilo_bienvenida,
UMBRAL_SIM,
COOLDOWN_TIME
)
# ──────────────────────────────────────────────────────────────────────────────
# 2. PROTECCIONES MULTIHILO E INICIALIZACIÓN
# ──────────────────────────────────────────────────────────────────────────────
COLA_ROSTROS = Queue(maxsize=4)
YUNET_LOCK = threading.Lock()
IA_LOCK = threading.Lock()
# Inicializamos la base de datos usando tu función importada
print("\nIniciando carga de base de datos...")
BASE_DATOS_ROSTROS = gestionar_vectores(actualizar=True)
# ──────────────────────────────────────────────────────────────────────────────
# 3. MOTOR ASÍNCRONO
# ──────────────────────────────────────────────────────────────────────────────
def procesar_rostro_async(frame_hd, box_480, gid, cam_id, global_mem, trk):
""" Toma el recorte del tracker, escala a Alta Definición, usa YuNet y hace la Fusión Mágica """
try:
if not BASE_DATOS_ROSTROS: return
# ──────────────────────────────────────────────────────────
# 1. VALIDACIÓN DEL FRAME HD Y ESCALADO MATEMÁTICO
# ──────────────────────────────────────────────────────────
h_real, w_real = frame_hd.shape[:2]
# ⚡ TRAMPA ANTI-BUGS: Si esto salta, corrige la llamada en tu main_fusion.py
if w_real <= 480:
print(f"[❌ ERROR CAM {cam_id}] Le estás pasando el frame_show (480x270) a ArcFace, no el HD.")
escala_x = w_real / 480.0
escala_y = h_real / 270.0
x_min, y_min, x_max, y_max = box_480
h_box = y_max - y_min
y_min_expandido = max(0, y_min - (h_box * 0.15))
y_max_cabeza = min(270, y_min + (h_box * 0.40)) # Límite máximo en la escala de 270
x1_hd = int(max(0, x_min) * escala_x)
y1_hd = int(y_min_expandido * escala_y)
x2_hd = int(min(480, x_max) * escala_x)
y2_hd = int(y_max_cabeza * escala_y)
roi_cabeza = frame_hd[y1_hd:y2_hd, x1_hd:x2_hd]
# Si la cabeza HD mide menos de 60x60, está demasiado lejos incluso en HD
if roi_cabeza.size == 0 or roi_cabeza.shape[0] < 60 or roi_cabeza.shape[1] < 60:
return
h_roi, w_roi = roi_cabeza.shape[:2]
# ──────────────────────────────────────────────────────────
# 2. DETECCIÓN DE ROSTRO CON YUNET (Ahora operando en HD)
# ──────────────────────────────────────────────────────────
faces = detectar_rostros_yunet(roi_cabeza, lock=YUNET_LOCK)
for (rx, ry, rw, rh, score) in faces:
rx, ry = max(0, rx), max(0, ry)
rw, rh = min(w_roi - rx, rw), min(h_roi - ry, rh)
area_rostro_actual = rw * rh
with global_mem.lock:
data = global_mem.db.get(gid, {})
nombre_actual = data.get('nombre')
area_ref = data.get('area_rostro_ref', 0)
necesita_saludo = False
if str(cam_id) == "7":
if not hasattr(global_mem, 'ultimos_saludos'):
global_mem.ultimos_saludos = {}
ultimo = global_mem.ultimos_saludos.get(nombre_actual if nombre_actual else "", 0)
if (time.time() - ultimo) > COOLDOWN_TIME:
necesita_saludo = True
if nombre_actual is None or area_rostro_actual >= (area_ref * 1.5) or necesita_saludo:
# ⚡ MÁRGENES MÁS AMPLIOS: ArcFace necesita ver frente y barbilla (25%)
m_x = int(rw * 0.25)
m_y = int(rh * 0.25)
roi_rostro = roi_cabeza[max(0, ry-m_y):min(h_roi, ry+rh+m_y),
max(0, rx-m_x):min(w_roi, rx+rw+m_x)]
if roi_rostro.size == 0 or roi_rostro.shape[0] < 60 or roi_rostro.shape[1] < 60:
continue
# ⚡ Laplaciano ajustado para imágenes HD (30.0 es más justo)
gray_roi = cv2.cvtColor(roi_rostro, cv2.COLOR_BGR2GRAY)
nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var()
if nitidez < 15.0:
continue
# ──────────────────────────────────────────────────────────
# 3. RECONOCIMIENTO FACIAL ARCFACE
# ──────────────────────────────────────────────────────────
with IA_LOCK:
try:
# ⚡ CAMBIO DRÁSTICO: Usamos RetinaFace para alinear la cabeza obligatoriamente.
# Si RetinaFace no logra enderezar la cara (ej. estás totalmente de perfil),
# lanzará una excepción y abortará, evitando falsos positivos.
# Así DEBE estar en main_fusion.py para que sea compatible con tu nueva DB
res = DeepFace.represent(
img_path=roi_cabeza,
model_name="ArcFace",
detector_backend="retinaface", # Obligatorio
align=True, # Obligatorio
enforce_detection=True # Obligatorio
)
emb = np.array(res[0]["embedding"], dtype=np.float32)
mejor_match, max_sim = buscar_mejor_match(emb, BASE_DATOS_ROSTROS)
except Exception:
# Si falla la alineación o estás muy borroso, lo ignoramos en silencio.
continue
print(f"[DEBUG CAM {cam_id}] ArcFace: {mejor_match} al {max_sim:.2f} (Umbral: {UMBRAL_SIM})")
if max_sim >= UMBRAL_SIM and mejor_match:
nombre_limpio = mejor_match.split('_')[0]
with global_mem.lock:
global_mem.db[gid]['nombre'] = nombre_limpio
global_mem.db[gid]['area_rostro_ref'] = area_rostro_actual
global_mem.db[gid]['ts'] = time.time()
ids_a_borrar = []
firma_actual = global_mem.db[gid]['firmas'][0] if global_mem.db[gid]['firmas'] else None
for otro_gid, datos_otro in list(global_mem.db.items()):
if otro_gid == gid: continue
if datos_otro.get('nombre') == nombre_limpio:
ids_a_borrar.append(otro_gid)
elif datos_otro.get('nombre') is None and firma_actual and datos_otro['firmas']:
sim_huerfano = similitud_hibrida(firma_actual, datos_otro['firmas'][0])
if sim_huerfano > 0.75:
ids_a_borrar.append(otro_gid)
for id_basura in ids_a_borrar:
del global_mem.db[id_basura]
print(f"[🧹 LIMPIEZA] ID huérfano/clon {id_basura} eliminado tras reconocer a {nombre_limpio}.")
if str(cam_id) == "7" and necesita_saludo:
global_mem.ultimos_saludos[nombre_limpio] = time.time()
try:
with IA_LOCK:
analisis = DeepFace.analyze(roi_rostro, actions=['gender'], enforce_detection=False)[0]
genero = analisis.get('dominant_gender', 'Man')
except Exception:
genero = "Man"
threading.Thread(target=hilo_bienvenida, args=(nombre_limpio, genero), daemon=True).start()
break
except Exception as e:
pass
finally:
trk.procesando_rostro = False
def worker_rostros(global_mem):
""" Consumidor de la cola multihilo """
while True:
frame, box, gid, cam_id, trk = COLA_ROSTROS.get()
procesar_rostro_async(frame, box, 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 trk.en_grupo: color, label = (0, 0, 255), f"ID:{trk.gid} [grp]"
elif trk.aprendiendo: color, label = (255, 255, 0), f"ID:{trk.gid} [++]"
elif trk.origen_global: 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]
for _ in range(2):
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:
res = model.predict(frame_show, conf=0.50, iou=0.50, classes=[0], verbose=False, imgsz=480)
if res[0].boxes:
boxes = res[0].boxes.xyxy.cpu().numpy().tolist()
tracks = managers[cid].update(boxes, frame_show, now, turno_activo)
for trk in tracks:
if trk.time_since_update <= 1:
dibujar_track_fusion(frame_show, trk, global_mem)
if turno_activo and trk.gid is not None and not getattr(trk, 'procesando_rostro', False):
if not COLA_ROSTROS.full():
trk.procesando_rostro = True
COLA_ROSTROS.put((frame.copy(), trk.box, trk.gid, cid, trk))
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 t.time_since_update==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
if cv2.waitKey(1) == ord('q'):
break
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
################################################################### reconocimeito2.py
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
import cv2
import numpy as np
from deepface import DeepFace
import pickle
import time
import threading
import asyncio
import edge_tts
import subprocess
from datetime import datetime
import warnings
import urllib.request
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 # Por encima → identificado. Por debajo → desconocido.
COOLDOWN_TIME = 15 # Segundos entre saludos
USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65"
RTSP_URL = f"rtsp://{USUARIO}:{PASSWORD}@{IP_DVR}:554/Streaming/Channels/702"
for path in [DB_PATH, CACHE_PATH]:
os.makedirs(path, exist_ok=True)
# ──────────────────────────────────────────────────────────────────────────────
# YUNET — Detector facial rápido en CPU
# ──────────────────────────────────────────────────────────────────────────────
YUNET_MODEL_PATH = "face_detection_yunet_2023mar.onnx"
if not os.path.exists(YUNET_MODEL_PATH):
print(f"Descargando YuNet ({YUNET_MODEL_PATH})...")
url = ("https://github.com/opencv/opencv_zoo/raw/main/models/"
"face_detection_yunet/face_detection_yunet_2023mar.onnx")
urllib.request.urlretrieve(url, YUNET_MODEL_PATH)
print("YuNet descargado.")
# Detector estricto para ROIs grandes (persona cerca)
detector_yunet = cv2.FaceDetectorYN.create(
model=YUNET_MODEL_PATH, config="",
input_size=(320, 320),
score_threshold=0.70,
nms_threshold=0.3,
top_k=5000
)
# Detector permisivo para ROIs pequeños (persona lejos)
detector_yunet_lejano = cv2.FaceDetectorYN.create(
model=YUNET_MODEL_PATH, config="",
input_size=(320, 320),
score_threshold=0.45,
nms_threshold=0.3,
top_k=5000
)
def detectar_rostros_yunet(roi, lock=None):
"""
Elige automáticamente el detector según el tamaño del ROI.
"""
h_roi, w_roi = roi.shape[:2]
area = w_roi * h_roi
det = detector_yunet if area > 8000 else detector_yunet_lejano
try:
if lock:
with lock:
det.setInputSize((w_roi, h_roi))
_, faces = det.detect(roi)
else:
det.setInputSize((w_roi, h_roi))
_, faces = det.detect(roi)
except Exception:
return []
if faces is None:
return []
resultado = []
for face in faces:
try:
fx, fy, fw, fh = map(int, face[:4])
score = float(face[14]) if len(face) > 14 else 1.0
resultado.append((fx, fy, fw, fh, score))
except (ValueError, OverflowError, TypeError):
continue
return resultado
# ──────────────────────────────────────────────────────────────────────────────
# SISTEMA DE AUDIO
# ──────────────────────────────────────────────────────────────────────────────
def obtener_audios_humanos(genero):
hora = datetime.now().hour
es_mujer = genero.lower() == 'woman'
suffix = "_m.mp3" if es_mujer else "_h.mp3"
if 5 <= hora < 12:
intro = "dias.mp3"
elif 12 <= hora < 19:
intro = "tarde.mp3"
else:
intro = "noches.mp3"
cierre = ("fin_noche" if (hora >= 19 or hora < 5) else "fin_dia") + suffix
return intro, cierre
async def sintetizar_nombre(nombre, ruta):
nombre_limpio = nombre.replace('_', ' ')
try:
comunicador = edge_tts.Communicate(nombre_limpio, "es-MX-DaliaNeural", rate="+10%")
await comunicador.save(ruta)
except Exception:
pass
def reproducir(archivo):
if os.path.exists(archivo):
subprocess.Popen(
["mpv", "--no-video", "--volume=100", archivo],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def hilo_bienvenida(nombre, genero):
archivo_nombre = os.path.join(CACHE_PATH, f"nombre_{nombre}.mp3")
if not os.path.exists(archivo_nombre):
try:
asyncio.run(sintetizar_nombre(nombre, archivo_nombre))
except Exception:
pass
intro, cierre = obtener_audios_humanos(genero)
archivos = [f for f in [intro, archivo_nombre, cierre] if os.path.exists(f)]
if archivos:
subprocess.Popen(
["mpv", "--no-video", "--volume=100"] + archivos,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# ──────────────────────────────────────────────────────────────────────────────
# GESTIÓN DE BASE DE DATOS (AHORA CON RETINAFACE Y ALINEACIÓN)
# ──────────────────────────────────────────────────────────────────────────────
def gestionar_vectores(actualizar=False):
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 = {}
print("\nACTUALIZANDO BASE DE DATOS (Alineación con RetinaFace)...")
imagenes = [f for f in os.listdir(DB_PATH) if f.lower().endswith(('.jpg', '.png'))]
nombres_en_disco = set()
hubo_cambios = 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)
if nombre_archivo in vectores_actuales and ts_actual == ts_guardado:
continue
try:
# ⚡ MAGIA 1: RetinaFace alinea matemáticamente los rostros de la base de datos
res = DeepFace.represent(
img_path=ruta_img,
model_name="ArcFace",
detector_backend="retinaface", # Localiza ojos/nariz
align=True, # Rota la imagen para alinear
enforce_detection=True # Obliga a que haya cara válida
)
emb = np.array(res[0]["embedding"], dtype=np.float32)
# ⚡ MAGIA 2: Normalización L2 al guardar (Elimina el "Efecto Rosa María")
norma = np.linalg.norm(emb)
if norma > 0:
emb = emb / norma
vectores_actuales[nombre_archivo] = emb
timestamps[nombre_archivo] = ts_actual
hubo_cambios = True
print(f" ✅ Procesado y alineado: {nombre_archivo}")
except Exception as e:
print(f" ❌ Rostro no válido en '{archivo}', omitido. Error: {e}")
for nombre in list(vectores_actuales.keys()):
if nombre not in nombres_en_disco:
del vectores_actuales[nombre]
timestamps.pop(nombre, None)
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)
print(" Sincronización terminada.\n")
else:
print(" Sin cambios. Base de datos al día.\n")
return vectores_actuales
# ──────────────────────────────────────────────────────────────────────────────
# BÚSQUEDA BLINDADA (Similitud Coseno estricta)
# ──────────────────────────────────────────────────────────────────────────────
def buscar_mejor_match(emb_consulta, base_datos):
# ⚡ MAGIA 3: Normalización L2 del vector entrante
norma = np.linalg.norm(emb_consulta)
if norma > 0:
emb_consulta = emb_consulta / norma
mejor_match, max_sim = None, -1.0
for nombre, vec in base_datos.items():
# Como ambos están normalizados, esto es Similitud Coseno pura (-1.0 a 1.0)
sim = float(np.dot(emb_consulta, vec))
if sim > max_sim:
max_sim = sim
mejor_match = nombre
return mejor_match, max_sim
# ──────────────────────────────────────────────────────────────────────────────
# LOOP DE PRUEBA Y REGISTRO
# ──────────────────────────────────────────────────────────────────────────────
def sistema_interactivo():
base_datos = gestionar_vectores(actualizar=False)
cap = cv2.VideoCapture(RTSP_URL)
ultimo_saludo = 0
persona_actual = None
confirmaciones = 0
print("\n" + "=" * 50)
print(" MÓDULO DE REGISTRO Y DEPURACIÓN")
print(" [R] Registrar nuevo rostro | [Q] Salir")
print("=" * 50 + "\n")
faces_ultimo_frame = []
while True:
ret, frame = cap.read()
if not ret:
time.sleep(2)
cap.open(RTSP_URL)
continue
h, w = frame.shape[:2]
display_frame = frame.copy()
tiempo_actual = time.time()
faces_raw = detectar_rostros_yunet(frame)
faces_ultimo_frame = faces_raw
for (fx, fy, fw, fh, score_yunet) in faces_raw:
fx = max(0, fx); fy = max(0, fy)
fw = min(w - fx, fw); fh = min(h - fy, fh)
if fw <= 0 or fh <= 0:
continue
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (255, 200, 0), 2)
cv2.putText(display_frame, f"YN:{score_yunet:.2f}",
(fx, fy - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 200, 0), 1)
if (tiempo_actual - ultimo_saludo) <= COOLDOWN_TIME:
continue
m = int(fw * 0.15)
roi = frame[max(0, fy-m): min(h, fy+fh+m),
max(0, fx-m): min(w, fx+fw+m)]
if roi.size == 0 or roi.shape[0] < 40 or roi.shape[1] < 40:
cv2.putText(display_frame, "muy pequeño",
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 100, 255), 1)
continue
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
nitidez = cv2.Laplacian(gray_roi, cv2.CV_64F).var()
if nitidez < 50.0:
cv2.putText(display_frame, f"blur({nitidez:.0f})",
(fx, fy-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
continue
try:
# ⚡ En el modo de prueba interactivo usamos las reglas viejas
# para que sea rápido y puedas registrar fotos fácilmente.
res = DeepFace.represent(
img_path=roi, model_name="ArcFace", enforce_detection=False
)
emb = np.array(res[0]["embedding"], dtype=np.float32)
mejor_match, max_sim = buscar_mejor_match(emb, base_datos)
except Exception as e:
print(f"[ERROR ArcFace]: {e}")
continue
estado = " IDENTIFICADO" if max_sim > UMBRAL_SIM else "DESCONOCIDO"
nombre_d = mejor_match.split('_')[0] if mejor_match else "nadie"
n_bloques = int(max_sim * 20)
barra = "█" * n_bloques + "░" * (20 - n_bloques)
print(f"[REGISTRO] {estado} | {nombre_d:<14} | {barra} | "
f"{max_sim*100:.1f}% (umbral: {UMBRAL_SIM*100:.0f}%)")
if max_sim > UMBRAL_SIM and mejor_match:
color = (0, 255, 0)
texto = f"{mejor_match.split('_')[0]} ({max_sim:.2f})"
if mejor_match == persona_actual:
confirmaciones += 1
else:
persona_actual, confirmaciones = mejor_match, 1
if confirmaciones >= 2:
cv2.rectangle(display_frame, (fx, fy), (fx+fw, fy+fh), (0, 255, 0), 3)
try:
analisis = DeepFace.analyze(
roi, actions=['gender'], enforce_detection=False
)[0]
genero = analisis['dominant_gender']
except Exception:
genero = "Man"
threading.Thread(
target=hilo_bienvenida,
args=(mejor_match, genero),
daemon=True
).start()
ultimo_saludo = tiempo_actual
confirmaciones = 0
else:
color = (0, 0, 255)
texto = f"? ({max_sim:.2f})"
confirmaciones = max(0, confirmaciones - 1)
cv2.putText(display_frame, texto,
(fx, fy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
cv2.imshow("Módulo de Registro", display_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('r'):
if faces_ultimo_frame:
areas = [fw * fh for (fx, fy, fw, fh, _) in faces_ultimo_frame]
fx, fy, fw, fh, _ = faces_ultimo_frame[np.argmax(areas)]
# Le damos más margen al registro (30%) para que RetinaFace no falle
# cuando procese la foto en la carpeta.
m_x = int(fw * 0.30)
m_y = int(fh * 0.30)
face_roi = frame[max(0, fy-m_y): min(h, fy+fh+m_y),
max(0, fx-m_x): min(w, fx+fw+m_x)]
if face_roi.size > 0:
nom = input("\nNombre de la persona: ").strip()
if nom:
foto_path = os.path.join(DB_PATH, f"{nom}.jpg")
cv2.imwrite(foto_path, face_roi)
print(f"[OK] Rostro de '{nom}' guardado. Sincronizando...")
# ⚡ Al sincronizar, RetinaFace alineará esta foto guardada.
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()