Phase 04 - Lesson 27

Seguimiento de Multiples Objetos y Memoria de Video

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

El seguimiento es deteccion mas asociacion. Detecta cada fotograma. Empareja las detecciones de este fotograma con las pistas del fotograma anterior por ID.

Tipo: Build Lenguajes: Python Requisitos previos: Fase 4 Leccion 06 (Deteccion con YOLO), Fase 4 Leccion 08 (Mask R-CNN), Fase 4 Leccion 24 (SAM 3) Tiempo: ~60 minutos

Objetivos de Aprendizaje

  • Distinguir el seguimiento por deteccion del seguimiento basado en consultas y nombrar las familias de algoritmos (SORT, DeepSORT, ByteTrack, BoT-SORT, rastreador por memoria SAM 2, SAM 3.1 Object Multiplex)
  • Implementar la asignacion IoU + Hungaro desde cero para el clasico seguimiento por deteccion
  • Explicar el banco de memoria de SAM 2 y por que maneja mejor la oclusion que la asociacion basada en IoU
  • Leer las tres metricas de seguimiento (MOTA, IDF1, HOTA) y elegir cual importa para un caso de uso dado

El Problema

Un detector te dice donde estan los objetos en un solo fotograma. Un rastreador te dice que deteccion en el fotograma t es el mismo objeto que una deteccion en el fotograma t-1. Sin eso, no puedes contar objetos que cruzan una linea, seguir una pelota a traves de una oclusion ni saber que "el auto #4 lleva 8 segundos en el carril".

El seguimiento es esencial para todo producto orientado a video: analitica deportiva, vigilancia, conduccion autonoma, analisis de video medico, monitoreo de fauna, conteo de marcas. Los bloques de construccion centrales son compartidos: un detector por fotograma, un modelo de movimiento (filtro de Kalman o algo mas rico), un paso de asociacion (algoritmo Hungaro sobre IoU / coseno / atributos aprendidos) y un ciclo de vida de la pista (nacimiento, actualizacion, muerte).

2026 trajo dos nuevos patrones: seguimiento basado en memoria de SAM 2 (asociacion por memoria de atributos en lugar de modelo de movimiento) y SAM 3.1 Object Multiplex (memoria compartida para muchas instancias del mismo concepto). Esta leccion recorre primero la pila clasica y luego el enfoque basado en memoria.

El Concepto

Seguimiento por deteccion

flowchart LR
    F1["Fotograma t"] --> DET["Detector"] --> D1["Detecciones en t"]
    PREV["Pistas hasta t-1"] --> PREDICT["Prediccion de movimiento<br/>(Kalman)"]
    PREDICT --> PRED["Pistas predichas en t"]
    D1 --> ASSOC["Asignacion Hungara<br/>(IoU / coseno / movimiento)"]
    PRED --> ASSOC
    ASSOC --> UPDATE["Actualizar pistas emparejadas"]
    ASSOC --> NEW["Crear nuevas pistas"]
    ASSOC --> DEAD["Envejecer pistas sin emparejar; eliminar tras N"]
    UPDATE --> NEXT["Pistas en t"]
    NEW --> NEXT
    DEAD --> NEXT

    style DET fill:#dbeafe,stroke:#2563eb
    style ASSOC fill:#fef3c7,stroke:#d97706
    style NEXT fill:#dcfce7,stroke:#16a34a

Todo rastreador que encontraras en 2026 es una variacion de este bucle. Las diferencias:

  • SORT (2016): filtro de Kalman + IoU Hungaro. Simple, rapido, sin modelo de apariencia.
  • DeepSORT (2017): SORT + un atributo de apariencia basado en CNN por pista (embedding de ReID). Maneja mejor los cruces.
  • ByteTrack (2021): asocia detecciones de baja confianza como una segunda etapa; no necesita atributos de apariencia, pero es el de mejor desempeno en MOT17.
  • BoT-SORT (2022): Byte + compensacion de movimiento de camara + ReID.
  • StrongSORT / OC-SORT — descendientes de ByteTrack con mejor movimiento y apariencia.

Filtro de Kalman en un parrafo

Un filtro de Kalman mantiene un estado por pista (x, y, w, h, dx, dy, dw, dh) con una covarianza. En cada fotograma, predice el estado usando un modelo de velocidad constante y luego actualiza con la deteccion emparejada. La actualizacion confia mas en la deteccion cuando la incertidumbre de la prediccion es alta. Esto da trayectorias suaves y la capacidad de continuar una pista a traves de una oclusion corta (1-5 fotogramas).

Todo rastreador clasico usa un filtro de Kalman en el paso de prediccion de movimiento.

El algoritmo Hungaro

Dada una matriz de costo M x N (pistas x detecciones), encuentra la asignacion uno-a-uno que minimiza el costo total. El costo suele ser 1 - IoU(track_bbox, detection_bbox) o la similitud de coseno negativa de atributos de apariencia. El tiempo de ejecucion es O((M+N)^3); para M, N de hasta ~1000 es suficientemente rapido en Python via scipy.optimize.linear_sum_assignment.

La idea clave de ByteTrack

Los rastreadores estandar descartan las detecciones de baja confianza (< 0.5). ByteTrack las mantiene como candidatas de segunda etapa: tras emparejar las pistas con las detecciones de alta confianza, las pistas sin emparejar intentan coincidir con detecciones de baja confianza usando un umbral de IoU un poco mas flexible. Recupera oclusiones cortas y cambios de ID cerca de multitudes.

Seguimiento basado en memoria de SAM 2

SAM 2 maneja video manteniendo un banco de memoria de atributos espacio-temporales por instancia. Dado un prompt (clic, caja, texto) en un fotograma, codifica la instancia en la memoria. En los fotogramas siguientes, la memoria recibe atencion cruzada contra los atributos del nuevo fotograma, y el decodificador produce una mascara para la misma instancia en el nuevo fotograma.

Sin filtro de Kalman, sin asignacion Hungara. La asociacion es implicita en la operacion de atencion a la memoria.

Pros:

  • Robusto ante grandes oclusiones (la memoria lleva la identidad de la instancia a traves de muchos fotogramas).
  • Vocabulario abierto cuando se combina con los prompts de texto de SAM 3.
  • Funciona sin un modelo de movimiento separado.

Contras:

  • Mas lento que ByteTrack para seguimiento de muchos objetos.
  • El banco de memoria crece; limita la ventana de contexto.

SAM 3.1 Object Multiplex

El seguimiento previo de SAM 2 / SAM 3 mantiene un banco de memoria separado por instancia. Para 50 objetos, 50 bancos de memoria. Object Multiplex (marzo de 2026) los colapsa en una unica memoria compartida con tokens de consulta por instancia. El costo escala de forma sublineal en el numero de instancias.

Multiplex es el nuevo estandar para el seguimiento de multitudes en 2026: multitudes en conciertos, trabajadores de almacen, intersecciones de trafico.

Tres metricas que debes conocer

  • MOTA (Multi-Object Tracking Accuracy) — 1 - (FN + FP + cambios de ID) / GT. Ponderada por tipo de error; una sola metrica que mezcla fallas de deteccion y de asociacion.
  • IDF1 (ID F1) — media armonica entre precision y exhaustividad de ID. Se enfoca especificamente en que tan bien cada pista de referencia mantiene su ID a lo largo del tiempo. Mejor que MOTA para tareas sensibles a los cambios de ID.
  • HOTA (Higher Order Tracking Accuracy) — se descompone en precision de deteccion (DetA) y precision de asociacion (AssA). El estandar de la comunidad desde 2020; el mas completo.

Para vigilancia (quien es quien): IDF1 es lo que reportas. Para analitica deportiva (contar pases): HOTA. Para comparacion academica general: HOTA.

Construyelo

Paso 1: Matriz de costo basada en IoU

import numpy as np


def bbox_iou(a, b):
    """
    a, b: (N, 4) arrays of [x1, y1, x2, y2].
    Returns (N_a, N_b) IoU matrix.
    """
    ax1, ay1, ax2, ay2 = a[:, 0], a[:, 1], a[:, 2], a[:, 3]
    bx1, by1, bx2, by2 = b[:, 0], b[:, 1], b[:, 2], b[:, 3]
    inter_x1 = np.maximum(ax1[:, None], bx1[None, :])
    inter_y1 = np.maximum(ay1[:, None], by1[None, :])
    inter_x2 = np.minimum(ax2[:, None], bx2[None, :])
    inter_y2 = np.minimum(ay2[:, None], by2[None, :])
    inter = np.clip(inter_x2 - inter_x1, 0, None) * np.clip(inter_y2 - inter_y1, 0, None)
    area_a = (ax2 - ax1) * (ay2 - ay1)
    area_b = (bx2 - bx1) * (by2 - by1)
    union = area_a[:, None] + area_b[None, :] - inter
    return inter / np.clip(union, 1e-8, None)

Paso 2: Rastreador minimo al estilo SORT

El Kalman de velocidad constante fija se omite por brevedad — aqui usamos una asociacion simple por IoU; en produccion la prediccion de Kalman es esencial. El paquete Python sort proporciona la version completa.

from scipy.optimize import linear_sum_assignment


class Track:
    def __init__(self, tid, bbox, frame):
        self.id = tid
        self.bbox = bbox
        self.last_frame = frame
        self.hits = 1

    def update(self, bbox, frame):
        self.bbox = bbox
        self.last_frame = frame
        self.hits += 1


class SimpleTracker:
    def __init__(self, iou_threshold=0.3, max_age=5):
        self.tracks = []
        self.next_id = 1
        self.iou_threshold = iou_threshold
        self.max_age = max_age

    def step(self, detections, frame):
        if not self.tracks:
            for d in detections:
                self.tracks.append(Track(self.next_id, d, frame))
                self.next_id += 1
            return [(t.id, t.bbox) for t in self.tracks]

        track_boxes = np.array([t.bbox for t in self.tracks])
        det_boxes = np.array(detections) if len(detections) else np.empty((0, 4))

        iou = bbox_iou(track_boxes, det_boxes) if len(det_boxes) else np.zeros((len(track_boxes), 0))
        cost = 1 - iou
        cost[iou < self.iou_threshold] = 1e6

        matched_track = set()
        matched_det = set()
        if cost.size > 0:
            row, col = linear_sum_assignment(cost)
            for r, c in zip(row, col):
                if cost[r, c] < 1.0:
                    self.tracks[r].update(det_boxes[c], frame)
                    matched_track.add(r); matched_det.add(c)

        for i, d in enumerate(det_boxes):
            if i not in matched_det:
                self.tracks.append(Track(self.next_id, d, frame))
                self.next_id += 1

        self.tracks = [t for t in self.tracks if frame - t.last_frame <= self.max_age]
        return [(t.id, t.bbox) for t in self.tracks]

60 lineas. Recibe detecciones por fotograma, devuelve IDs de pista por fotograma. Los sistemas reales agregan la prediccion de Kalman, la re-asociacion de segunda etapa de ByteTrack y atributos de apariencia.

Paso 3: Prueba de trayectoria sintetica

def synthetic_frames(num_frames=20, num_objects=3, H=240, W=320, seed=0):
    rng = np.random.default_rng(seed)
    starts = rng.uniform(20, 200, size=(num_objects, 2))
    velocities = rng.uniform(-5, 5, size=(num_objects, 2))
    frames = []
    for f in range(num_frames):
        dets = []
        for i in range(num_objects):
            cx, cy = starts[i] + f * velocities[i]
            dets.append([cx - 10, cy - 10, cx + 10, cy + 10])
        frames.append(dets)
    return frames


tracker = SimpleTracker()
for f, dets in enumerate(synthetic_frames()):
    tracks = tracker.step(dets, f)

Tres objetos que se mueven en lineas rectas deben conservar sus IDs a lo largo de los 20 fotogramas.

Paso 4: Metrica de cambio de ID

def count_id_switches(tracks_per_frame, gt_per_frame):
    """
    tracks_per_frame:  list of list of (track_id, bbox)
    gt_per_frame:      list of list of (gt_id, bbox)
    Returns number of ID switches.
    """
    prev_assignment = {}
    switches = 0
    for tracks, gts in zip(tracks_per_frame, gt_per_frame):
        if not tracks or not gts:
            continue
        t_boxes = np.array([b for _, b in tracks])
        g_boxes = np.array([b for _, b in gts])
        iou = bbox_iou(g_boxes, t_boxes)
        for g_idx, (gt_id, _) in enumerate(gts):
            j = iou[g_idx].argmax()
            if iou[g_idx, j] > 0.5:
                t_id = tracks[j][0]
                if gt_id in prev_assignment and prev_assignment[gt_id] != t_id:
                    switches += 1
                prev_assignment[gt_id] = t_id
    return switches

Esta es una metrica simplificada cercana al IDF1: cuenta cuantas veces un objeto de referencia cambia el ID de la pista predicha asignada a el. Las herramientas reales de MOTA / IDF1 / HOTA viven en py-motmetrics y TrackEval.

Usalo

Rastreadores de produccion en 2026:

  • ultralytics — YOLOv8 + ByteTrack / BoT-SORT integrados. results = model.track(source, tracker="bytetrack.yaml"). El estandar.
  • supervision (Roboflow) — wrappers de ByteTrack mas utilidades de anotacion.
  • SAM 2 / SAM 3.1 — seguimiento basado en memoria via processor.track().
  • Pila personalizada: detector (YOLOv8 / RT-DETR) + sort-tracker / OC-SORT / StrongSORT.

Eligiendo:

  • Peatones / autos / cajas a 30+ fps: ByteTrack con ultralytics.
  • Muchas instancias de una clase en una multitud: SAM 3.1 Object Multiplex.
  • Oclusiones intensas con apariencia identificable: DeepSORT / StrongSORT (atributos de ReID).
  • Deportes / interacciones complejas: BoT-SORT o rastreadores aprendidos (MOTRv3).

Entregalo

Esta leccion produce:

  • outputs/prompt-tracker-picker.md — elige SORT / ByteTrack / BoT-SORT / SAM 2 / SAM 3.1 segun el tipo de escena, los patrones de oclusion y el presupuesto de latencia.
  • outputs/skill-mot-evaluator.md — escribe un harness de evaluacion completo para MOTA / IDF1 / HOTA contra pistas de referencia.

Ejercicios

  1. (Facil) Ejecuta el rastreador sintetico anterior con 3, 10 y 30 objetos. Reporta el conteo de cambios de ID en cada caso. Identifica donde la asociacion simple solo por IoU comienza a fallar.
  2. (Medio) Agrega un paso de prediccion de Kalman de velocidad constante antes de la asociacion. Muestra que las oclusiones cortas (2-3 fotogramas) ya no causan cambios de ID.
  3. (Dificil) Integra el rastreador basado en memoria de SAM 2 (via transformers) como un backend de seguimiento alternativo. Ejecuta tanto SimpleTracker como SAM 2 en un clip de 30 segundos de una multitud y compara los conteos de cambios de ID, etiquetando manualmente los IDs de referencia de 5 personas destacadas.

Terminos Clave

Termino Lo que la gente dice Lo que realmente significa
Seguimiento por deteccion "Detectar y luego asociar" Detector por fotograma + asignacion Hungara sobre IoU / apariencia
Filtro de Kalman "Prediccion de movimiento" Dinamica lineal + covarianza para predicciones suaves de pista y manejo de oclusion
Algoritmo Hungaro "Asignacion optima" Resuelve el problema de emparejamiento bipartito de costo minimo; scipy.optimize.linear_sum_assignment
ByteTrack "Segunda pasada de baja confianza" Re-asocia pistas sin emparejar a detecciones de baja confianza para recuperar oclusiones cortas
DeepSORT "SORT + apariencia" Agrega un atributo de ReID para emparejamiento entre fotogramas; mejor para la preservacion de ID
Banco de memoria "Truco de SAM 2" Atributos espacio-temporales por instancia almacenados a lo largo de los fotogramas; la atencion cruzada reemplaza la asociacion explicita
Object Multiplex "Memoria compartida de SAM 3.1" Memoria unica compartida con consultas por instancia para seguimiento rapido de muchos objetos
HOTA "Metrica moderna de seguimiento" Se descompone en precision de deteccion y de asociacion; estandar de la comunidad

Lectura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).