From e10bfb7bf409bae618bb300c2601e0648274b846 Mon Sep 17 00:00:00 2001 From: rodrigo Date: Wed, 18 Mar 2026 12:27:26 -0600 Subject: [PATCH] version estable del seguimiento --- .Rhistory | 0 seguimiento2.py | 646 ++++++++++++++++++------------------------------ 2 files changed, 238 insertions(+), 408 deletions(-) create mode 100644 .Rhistory diff --git a/.Rhistory b/.Rhistory new file mode 100644 index 0000000..e69de29 diff --git a/seguimiento2.py b/seguimiento2.py index dc355a3..b9ea6ec 100644 --- a/seguimiento2.py +++ b/seguimiento2.py @@ -15,7 +15,7 @@ USUARIO, PASSWORD, IP_DVR = "admin", "TCA200503", "192.168.1.65" SECUENCIA = [1, 7, 5, 8, 3, 6] os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" 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" VECINOS = { "1": ["7"], "7": ["1", "5"], "5": ["7", "8"], @@ -23,84 +23,67 @@ VECINOS = { } # ─── PARÁMETROS TÉCNICOS -ASPECT_RATIO_MIN = 0.5 -ASPECT_RATIO_MAX = 4.0 -AREA_MIN_CALIDAD = 1200 -FRAMES_CALIDAD = 2 -TIEMPO_MAX_AUSENCIA = 800.0 +ASPECT_RATIO_MIN = 0.5 +ASPECT_RATIO_MAX = 4.0 +AREA_MIN_CALIDAD = 1200 +FRAMES_CALIDAD = 2 +TIEMPO_MIN_TRANSITO_NO_VECINO = 10.0 +TIEMPO_MAX_AUSENCIA = 800.0 -# ─── TIEMPOS DE VIDA DE TRACKERS -TIEMPO_PACIENCIA_BORDE_CON_ID = 5.0 # Segundos antes de matar un ID conocido en borde -TIEMPO_PACIENCIA_BORDE_SIN_ID = 1.0 # Segundos antes de matar un candidato en borde -TIEMPO_PACIENCIA_INTERIOR = 8.0 # Vida para ID conocido en el interior -TIEMPO_PACIENCIA_INTERIOR_NUEVO = 1.5 # Vida para candidato sin ID en interior +# ─── UMBRALES DE RE-ID (VERSIÓN ESTABLE) +UMBRAL_REID_MISMA_CAM = 0.65 +UMBRAL_REID_VECINO = 0.55 +UMBRAL_REID_NO_VECINO = 0.72 +MAX_FIRMAS_MEMORIA = 15 -# ─── RE-ADQUISICIÓN RÁPIDA (persona que sale y entra por la misma puerta) -RADIO_REENTRADA = 80 # Píxeles: si reaparece a menos de esta distancia, re-adquirir -TIEMPO_REENTRADA = 15.0 # Segundos máximos para considerar una reentrada - -# ─── UMBRALES DE RE-ID -# ⚡ UMBRAL_REID_MISMA_CAM ajustado a 0.65 para recuperar estabilidad en sombras y giros -UMBRAL_REID_MISMA_CAM = 0.65 -UMBRAL_REID_VECINO = 0.62 -UMBRAL_REID_NO_VECINO = 0.80 -MARGEN_MINIMO_REID = 0.07 -MAX_FIRMAS_MEMORIA = 15 - -# ─── TIEMPO ENTRE TURNOS ACTIVOS (para el anti-fantasma dinámico) -TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035 - -# ─── COLORES Y FUENTE -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_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 DEL MOTOR DEEP LEARNING (ONNX) # ────────────────────────────────────────────────────────────────────────────── -print("Cargando motor de Re-Identificación (OSNet ONNX en CPU)...") +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("OSNet cargado.") + 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}. {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. MOTOR HÍBRIDO DE FIRMA +# 1. MOTOR HÍBRIDO ESTABLE (Sin filtros destructivos) # ────────────────────────────────────────────────────────────────────────────── def analizar_calidad(box): x1, y1, x2, y2 = box w, h = x2 - x1, y2 - y1 - if w <= 0 or h <= 0: - return False + 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 + 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(roi): - h_roi = roi.shape[0] - t1 = int(h_roi * 0.15) - t2 = int(h_roi * 0.55) - zonas = [roi[:t1, :], roi[t1:t2, :], roi[t2:, :]] +def extraer_color_zonas(img): + h_roi = img.shape[0] + t1 = int(h_roi * 0.15) + t2 = 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) + 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() @@ -111,117 +94,93 @@ def extraer_firma_hibrida(frame, box): try: x1, y1, x2, y2 = map(int, box) fh, fw = frame.shape[:2] - x1_c = max(0, x1); y1_c = max(0, y1) - x2_c = min(fw, x2); y2_c = 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 - + 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) - # ⚡ ROI CRUDA: Pasamos la imagen tal cual, sin alterar su contraste artificialmente 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 + if norma > 0: deep_feat = deep_feat / norma - color_feat = extraer_color_zonas(roi) + color_feat = extraer_color_zonas(roi) return {'deep': deep_feat, 'color': color_feat, 'calidad': calidad_area} - - except Exception: + except Exception as e: return None -def similitud_hibrida(f1, f2, cross_cam=False): - if f1 is None or f2 is None: - return 0.0 - +def similitud_hibrida(f1, f2): + if f1 is None or f2 is None: return 0.0 sim_deep = max(0.0, 1.0 - cosine(f1['deep'], f2['deep'])) - + 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_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 cross_cam: - return (sim_deep * 0.75) + (sim_color * 0.25) - else: - return (sim_deep * 0.85) + (sim_color * 0.15) + else: sim_color = 0.0 + + return (sim_deep * 0.90) + (sim_color * 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) + [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.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 + 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 - self.ultimo_aprendizaje = 0.0 + 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. + 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 = float(x[0].item()); cy = float(x[1].item()) - s = float(x[2].item()); r = float(x[3].item()) - w = np.sqrt(s * r); h = s / (w + 1e-6) + 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 + 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 + 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.time_since_update = 0 + self.box = list(box) self.en_grupo = en_grupo self.kf.correct(self._convert_bbox_to_z(box)) @@ -233,446 +192,317 @@ class KalmanTrack: self.frames_buena_calidad = max(0, self.frames_buena_calidad - 1) # ────────────────────────────────────────────────────────────────────────────── -# 3. MEMORIA GLOBAL +# 3. MEMORIA GLOBAL (Top-3 Ponderado y Anti-Robo) # ────────────────────────────────────────────────────────────────────────────── class GlobalMemory: def __init__(self): - self.db = {} + self.db = {} self.next_gid = 100 - self.lock = threading.Lock() + self.lock = threading.Lock() def _es_transito_posible(self, data, cam_destino, now): - ultima_cam = str(data['last_cam']) + ultima_cam = str(data['last_cam']) cam_destino = str(cam_destino) - dt = now - data['ts'] - if ultima_cam == cam_destino: - return True + dt = now - data['ts'] + + if ultima_cam == cam_destino: return True vecinos = VECINOS.get(ultima_cam, []) - if cam_destino in vecinos: - return dt >= -0.5 - return dt >= 4.0 + 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 + def _sim_robusta(self, firma_nueva, firmas_guardadas): + if not firmas_guardadas: return 0.0 + sims = sorted( - [similitud_hibrida(firma_nueva, f, cross_cam=cross_cam) - for f in firmas_guardadas], + [similitud_hibrida(firma_nueva, f) for f in firmas_guardadas], reverse=True ) + if len(sims) == 1: return sims[0] - elif len(sims) <= 3: - return (sims[0] * 0.60) + (sims[1] * 0.40) + elif len(sims) <= 4: + return (sims[0] * 0.6) + (sims[1] * 0.4) else: return (sims[0] * 0.50) + (sims[1] * 0.30) + (sims[2] * 0.20) def identificar_candidato(self, firma_hibrida, cam_id, now, active_gids): with self.lock: - candidatos = [] - vecinos = VECINOS.get(str(cam_id), []) + best_gid, best_score = None, -1.0 + vecinos = VECINOS.get(str(cam_id), []) for gid, data in self.db.items(): - if gid in active_gids: - continue + if gid in active_gids: continue dt = now - data['ts'] - if dt > TIEMPO_MAX_AUSENCIA: - continue - if not self._es_transito_posible(data, cam_id, now): - continue - if not data['firmas']: - continue + if dt > TIEMPO_MAX_AUSENCIA or not self._es_transito_posible(data, cam_id, now): continue + if not data['firmas']: continue - cross_cam = str(data['last_cam']) != str(cam_id) - sim = self._sim_robusta(firma_hibrida, data['firmas'], cross_cam=cross_cam) + sim = self._sim_robusta(firma_hibrida, data['firmas']) misma_cam = str(data['last_cam']) == str(cam_id) es_vecino = str(data['last_cam']) in vecinos - if misma_cam: - umbral = UMBRAL_REID_MISMA_CAM - elif es_vecino: - umbral = UMBRAL_REID_VECINO - else: - umbral = UMBRAL_REID_NO_VECINO + if misma_cam: umbral = UMBRAL_REID_MISMA_CAM + elif es_vecino: umbral = UMBRAL_REID_VECINO + else: umbral = UMBRAL_REID_NO_VECINO - if sim > umbral: - candidatos.append((sim, gid)) + if sim > best_score and sim > umbral: + best_score = sim + best_gid = gid - if not candidatos: - nid = self.next_gid - self.next_gid += 1 + if best_gid is not None: + self._actualizar_sin_lock(best_gid, firma_hibrida, cam_id, now) + return best_gid, True + else: + 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) - sim_1, best_gid = candidatos[0] - - if len(candidatos) >= 2: - sim_2 = candidatos[1][0] - margen = sim_1 - sim_2 - if margen < MARGEN_MINIMO_REID: - 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 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] + vieja_ancla = firmas_list[0] firmas_list[0] = firma_dict - firma_dict = vieja_ancla - + firma_dict = vieja_ancla + if len(firmas_list) >= MAX_FIRMAS_MEMORIA: max_sim_interna = -1.0 - idx_redundante = 1 + 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 = float(np.mean(sims_con_otras)) if sims_con_otras else 0.0 + sim_promedio = np.mean(sims_con_otras) if sims_con_otras else 0.0 + if sim_promedio > max_sim_interna: max_sim_interna = sim_promedio - idx_redundante = i + 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 + 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) + with self.lock: self._actualizar_sin_lock(gid, firma, cam_id, now) # ────────────────────────────────────────────────────────────────────────────── -# 4. GESTOR LOCAL +# 4. GESTOR LOCAL # ────────────────────────────────────────────────────────────────────────────── def iou_overlap(boxA, boxB): - xA = max(boxA[0], boxB[0]); yA = max(boxA[1], boxB[1]) - xB = min(boxA[2], boxB[2]); yB = 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]) + 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 = cam_id - self.global_mem = global_mem - self.trackers = [] + 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 + for trk in self.trackers: trk.predict(turno_activo=turno_activo) + if not turno_activo: return self.trackers - matched, unmatched_dets, _ = self._asignar(boxes, now) - fh, fw = frame.shape[:2] + 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_local = {t.gid for t in self.trackers if t.gid is not None} + 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]) if trk.gid is None and trk.listo_para_id: - firma = extraer_firma_hibrida(frame, box) + firma = extraer_firma_hibrida(frame, box) if firma is not None: - gid, es_reid = self.global_mem.identificar_candidato( - firma, self.cam_id, now, active_gids_local - ) - trk.gid = gid - trk.origen_global = es_reid - trk.area_referencia = area_actual - - elif trk.gid is None and not trk.listo_para_id: - firma_rapida = extraer_firma_hibrida(frame, box) - if firma_rapida is not None: - cx_nuevo = (box[0] + box[2]) / 2 - cy_nuevo = (box[1] + box[3]) / 2 - - with self.global_mem.lock: - for gid_cand, data_cand in self.global_mem.db.items(): - if gid_cand in active_gids_local: - continue - if str(data_cand.get('last_cam', '')) != str(self.cam_id): - continue - if (now - data_cand['ts']) > TIEMPO_REENTRADA: - continue - if not data_cand['firmas']: - continue - - ultima_box = data_cand.get('last_box') - if ultima_box is None: - continue - - cx_ult = (ultima_box[0] + ultima_box[2]) / 2 - cy_ult = (ultima_box[1] + ultima_box[3]) / 2 - distancia = np.sqrt((cx_nuevo-cx_ult)**2 + (cy_nuevo-cy_ult)**2) - - if distancia < RADIO_REENTRADA: - sim = similitud_hibrida(firma_rapida, data_cand['firmas'][0]) - if sim > UMBRAL_REID_MISMA_CAM: - trk.gid = gid_cand - trk.origen_global = True - trk.area_referencia = area_actual - trk.listo_para_id = True - self.global_mem._actualizar_sin_lock( - gid_cand, firma_rapida, self.cam_id, now - ) - break - + gid, es_reid = self.global_mem.identificar_candidato(firma, self.cam_id, now, active_gids) + 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 = trk.ultimo_aprendizaje + tiempo_ultima_firma = getattr(trk, 'ultimo_aprendizaje', 0) + if (now - tiempo_ultima_firma) > 1.5 and analizar_calidad(box): - x1b, y1b, x2b, y2b = map(int, box) - en_borde = (x1b < 15 or y1b < 15 or x2b > fw-15 or y2b > fh-15) - + 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) + 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']): - - firma_ancla = self.global_mem.db[trk.gid]['firmas'][0] + if trk.gid in self.global_mem.db and self.global_mem.db[trk.gid]['firmas']: + firma_ancla = self.global_mem.db[trk.gid]['firmas'][0] sim_coherencia = similitud_hibrida(firma_nueva, firma_ancla) - + if sim_coherencia > 0.55: 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 > 0.55: + 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 - ) + self.global_mem._actualizar_sin_lock(trk.gid, firma_nueva, self.cam_id, now) trk.ultimo_aprendizaje = now trk.aprendiendo = True - for trk in self.trackers: - if trk.time_since_update == 0 and trk.gid is not None: - with self.global_mem.lock: - if trk.gid in self.global_mem.db: - self.global_mem.db[trk.gid]['last_box'] = list(trk.box) - - 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 = [] + fh, fw = frame.shape[:2] for t in self.trackers: x1, y1, x2, y2 = t.box - toca_borde = (x1 < 5 or y1 < 5 or x2 > fw-5 or y2 > fh-5) + toca_borde = (x1 < 5 or y1 < 5 or x2 > fw - 5 or y2 > fh - 5) tiempo_oculto = now - t.ts_ultima_deteccion - - if toca_borde: - limite = TIEMPO_PACIENCIA_BORDE_CON_ID if t.gid else TIEMPO_PACIENCIA_BORDE_SIN_ID - else: - limite = TIEMPO_PACIENCIA_INTERIOR if t.gid else TIEMPO_PACIENCIA_INTERIOR_NUEVO - - if tiempo_oculto < limite: + + if toca_borde and tiempo_oculto > 1.0: + continue + + limite_vida = 5.0 if t.gid else 1.0 + if tiempo_oculto < limite_vida: vivos.append(t) - + self.trackers = vivos return self.trackers - def _asignar(self, boxes, now): + 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) - + for t, trk in enumerate(self.trackers): for d, det in enumerate(boxes): iou = iou_overlap(trk.box, det) - cx_t = (trk.box[0]+trk.box[2]) / 2 - cy_t = (trk.box[1]+trk.box[3]) / 2 - cx_d = (det[0]+det[2]) / 2 - cy_d = (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.4 - + 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_tamano = (ratio_area - 1.0) * 0.4 + tiempo_oculto = now - trk.ts_ultima_deteccion + + TIEMPO_TURNO_ROTATIVO = len(SECUENCIA) * 0.035 + if tiempo_oculto > (TIEMPO_TURNO_ROTATIVO * 2) and iou < 0.10: fantasma_penalty = 5.0 else: fantasma_penalty = 0.0 - + if iou >= 0.05 or dist_norm < 0.50: - cost_mat[t, d] = ((1.0 - iou) - + (dist_norm * 2.0) - + fantasma_penalty - + castigo_tam) + cost_mat[t, d] = (1.0 - iou) + (dist_norm * 2.0) + fantasma_penalty + castigo_tamano 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): - if cost_mat[r, c] > 4.0: - unmatched_trks.append(r) - unmatched_dets.append(c) - else: - matched.append((r, c)) - - matched_t = {m[0] for m in matched} - matched_d = {m[1] for m in matched} + if cost_mat[r, c] > 4.0: + unmatched_trks.append(r); unmatched_dets.append(c) + else: matched.append((r, c)) + for t in range(n_trk): - if t not in matched_t: unmatched_trks.append(t) + if t not in [m[0] for m in matched]: unmatched_trks.append(t) for d in range(n_det): - if d not in matched_d: unmatched_dets.append(d) - + if d not in [m[1] for m in matched]: unmatched_dets.append(d) + return matched, unmatched_dets, unmatched_trks # ────────────────────────────────────────────────────────────────────────────── -# 5. STREAM DE CÁMARA +# 5. STREAM Y MAIN LOOP (Standalone) # ────────────────────────────────────────────────────────────────────────────── class CamStream: def __init__(self, url): - self.url = url - self.cap = cv2.VideoCapture(url) - self.frame = None - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + 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: + if ret: self.frame = f - time.sleep(0.01) - else: + time.sleep(0.01) + else: time.sleep(2) self.cap.open(self.url) -# ────────────────────────────────────────────────────────────────────────────── -# 6. DIBUJADO -# ────────────────────────────────────────────────────────────────────────────── 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}" - + 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) - + cv2.putText(frame_show, label, (x1+1, y1-4), FUENTE, 0.55, (0,0,0), 1) + if trk.gid is None: pct = min(trk.frames_buena_calidad / FRAMES_CALIDAD, 1.0) - bw = x2 - x1 - cv2.rectangle(frame_show, (x1, y2+2), (x2, y2+7), (50, 50, 50), -1) - cv2.rectangle(frame_show, (x1, y2+2), (x1+int(bw*pct), y2+7), (0, 220, 220), -1) + bw = x2 - x1; cv2.rectangle(frame_show, (x1, y2+2), (x2, y2+7), (50,50,50), -1) + cv2.rectangle(frame_show, (x1, y2+2), (x1+int(bw*pct), y2+7), (0,220,220), -1) -# ────────────────────────────────────────────────────────────────────────────── -# 7. MAIN LOOP (para pruebas standalone) -# ────────────────────────────────────────────────────────────────────────────── def main(): - print("SmartSoft") - model = YOLO("yolov8n.pt") + print("Iniciando Sistema V-PRO — Tracker Resiliente (Rollback Estable)") + 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] + 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, tiles = time.time(), [] + 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) - + 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.40, iou=0.50, - classes=[0], verbose=False, imgsz=480 - ) - if res[0].boxes: - boxes = res[0].boxes.xyxy.cpu().numpy().tolist() - + res = model.predict(frame_show, conf=0.50, iou=0.40, 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 == 0: - 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) + 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]) - ])) - + + 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 - + if cv2.waitKey(1) == ord('q'): break cv2.destroyAllWindows() if __name__ == "__main__":