Ultralytics YOLO27: Únete a la lista de espera.
Get Started
Lecciónintermedio

Combina tareas en un único flujo de trabajo

Crea una cascada de detección y clasificación, clasifica cada objeto rastreado una sola vez en lugar de en cada fotograma y convierte los puntos clave de pose en series temporales por persona.

Los sistemas más útiles que van más allá de la detección suelen combinar dos modelos, no uno. Un detector localiza a cada trabajador y un clasificador determina si cada casco es del tipo correcto; un modelo de pose localiza las articulaciones y un rastreador las convierte en una cronología que permite contar repeticiones. El código es breve. Lo que falla en estos sistemas es que el conjunto de datos de la segunda etapa no coincide con los recortes de la primera y que el coste se multiplica por cada objeto de cada fotograma: si se ejecuta sin optimizar, un flujo de 30 FPS con diez personas requiere 300 clasificaciones por segundo.

Resultado

Ejecuta un script de detección y clasificación, una versión que clasifique a cada persona rastreada una sola vez y un rastreador de pose que conserve los ángulos de rodilla por persona; crea los datos de entrenamiento del clasificador a partir de los propios recortes del detector.

Vía rápida
Si ya tienes experiencia, aquí tienes un resumen.
  1. Detecta, recorta con result.orig_img[y1:y2, x1:x2] y clasifica todos los recortes en una única llamada por lotes.

  2. Crea el conjunto de datos del clasificador a partir de los recortes de tu detector (save_crop=True), no de fotos de estudio.

  3. En vídeo, rastrea con persist=True y clasifica cada result.boxes.id una sola vez; después, reutiliza la etiqueta.

  4. Para combinar pose y seguimiento, usa el ID de seguimiento como clave del historial de puntos clave de cada persona.

Práctico

Dos patrones que cubren la mayoría de los productos#

Ambos parten del mismo fotograma y de la misma API de Ultralytics; se diferencian en lo que consume la segunda etapa. La selección y el ajuste del rastreador, así como la gestión de flujos para el seguimiento de objetos, se tratan en Sigue objetos con ByteTrack, BoT-SORT y TrackTrack; aquí nos centramos en combinar las tareas.

Detecta y luego clasifica los recortes#

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5)[0]
crops, labels = [], []
for box in result.boxes:
    x1, y1, x2, y2 = box.xyxy[0].int().tolist()
    crops.append(result.orig_img[y1:y2, x1:x2])  # BGR numpy crop, same as the detector saw
    labels.append(result.names[int(box.cls)])

for label, crop_result in zip(labels, classifier(crops, verbose=False)):  # one batched call for all crops
    probs = crop_result.probs
    print(f"{label:6s} -> {crop_result.names[probs.top1]} ({float(probs.top1conf):.2f})")
bus    -> bullet_train (0.31)
person -> sweatshirt (0.24)
person -> abaya (0.50)
person -> loudspeaker (0.49)
person -> frilled_lizard (0.24)

La integración es correcta: los recortes proceden de orig_img a resolución completa y en las propias coordenadas de píxel del detector; el clasificador los procesa todos en una única llamada por lotes. Las etiquetas son deliberadamente absurdas.

Una segunda etapa solo es tan buena como su correspondencia con los recortes

El clasificador de ImageNet nunca se entrenó con figuras urbanas recortadas y encuadradas de cerca, así que hace conjeturas: fíjate en que ninguna confianza supera 0.50. Lo mismo ocurre con un clasificador entrenado con fotos de productos nítidas al recibir recortes borrosos del detector.

Crea el conjunto de datos de la segunda etapa a partir de tus propios recortes#

La forma fiable de obtener datos que coincidan consiste en dejar que el detector los genere:

Cuadrícula de recortes numerados de maletas guardados por Ultralytics YOLO26 a partir de imágenes de cintas transportadoras de aeropuerto, listos para clasificarlos en carpetas

  1. Ejecuta el detector sobre grabaciones representativas con save_crop=True, por ejemplo, yolo predict model=best.pt source=footage/ save_crop=True. Los recortes se guardan en runs/detect/predict/crops/<class>/.
  2. Ordena los recortes de cada clase en tus carpetas de categorías específicas (helmet_ok/, helmet_missing/): esa es ya la estructura de clasificación de la lección anterior.
  3. Entrena el clasificador con esas carpetas y valídalo con recortes de otro día o cámara.

Como el detector ha generado los recortes, estos conservan el encuadre, el desenfoque y las oclusiones reales: exactamente lo que verá el clasificador en producción; además, el ajuste fino es un caso habitual de aprendizaje por transferencia.

Clasifica una sola vez por ID de seguimiento#

Un casco no cambia de un fotograma a otro, así que clasificar cada caja en cada fotograma es trabajo desperdiciado. Rastrea las detecciones y clasifica cada ID de seguimiento nuevo una sola vez:

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"

labels = {}  # track id -> (label, confidence), filled once per person
frames = boxes_seen = 0
for result in detector.track(source, classes=[0], stream=True, persist=True, verbose=False):  # people only
    frames += 1
    if result.boxes.id is None:
        continue
    for track_id, (x1, y1, x2, y2) in zip(result.boxes.id.int().tolist(), result.boxes.xyxy.int().tolist()):
        boxes_seen += 1
        if track_id not in labels:  # first sighting: classify once, then reuse
            probs = classifier(result.orig_img[y1:y2, x1:x2], verbose=False)[0].probs
            labels[track_id] = (classifier.names[probs.top1], round(float(probs.top1conf), 2))

print(f"{frames} frames, {boxes_seen} person boxes, {len(labels)} classifier calls")
print(labels)
11 frames, 33 person boxes, 3 classifier calls
{1: ('basketball', 0.51), 2: ('maillot_(tights)', 0.14), 3: ('bulletproof_vest', 0.23)}

Tres llamadas al clasificador en lugar de 33 en un clip de 11 fotogramas; en un flujo largo, el ahorro aumenta con cada fotograma que una persona permanece a la vista. Las etiquetas siguen siendo estimaciones de ImageNet: sustitúyelo por tu clasificador ajustado y obtendrás la respuesta por persona.

Criterio de decisión

Clasifica una vez por track cuando el atributo no pueda cambiar mientras el objeto esté a la vista (tipo de casco, marca del vehículo, SKU del producto). Vuelve a clasificar cada N fotogramas cuando sí pueda cambiar (puerta abierta, mano levantada). Si las primeras detecciones suelen ser pequeñas y borrosas, clasifica el fotograma en el que la caja del track sea más grande.

Pose y seguimiento#

Estimación de pose de Ultralytics YOLO que cuenta flexiones, con el esqueleto corporal, un ángulo de codo de 110 grados y un contador de repeticiones

El modo de seguimiento asigna un result.boxes.id persistente a cada persona, por lo que los puntos clave se convierten en una serie temporal. Este script registra el ángulo de la rodilla izquierda de cada persona a lo largo del mismo vídeo corto:

from collections import defaultdict

import numpy as np

from ultralytics import YOLO

def angle(a, b, c):
    """Angle at b, in degrees, formed by points a-b-c."""
    ba, bc = a - b, c - b
    return np.degrees(np.arccos(np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))))

model = YOLO("yolo26n-pose.pt")
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"
HIP, KNEE, ANKLE = 11, 13, 15  # left leg, COCO keypoint order
knee_angles = defaultdict(list)  # track id -> knee angle per frame

for result in model.track(source, stream=True, persist=True, verbose=False):
    if result.boxes.id is None:  # no confirmed tracks in this frame
        continue
    kpts = result.keypoints.cpu().numpy()
    for track_id, xy, conf in zip(result.boxes.id.int().tolist(), kpts.xy, kpts.conf):
        if conf[[HIP, KNEE, ANKLE]].min() > 0.5:  # skip occluded legs
            knee_angles[track_id].append(angle(xy[HIP], xy[KNEE], xy[ANKLE]))

for track_id, angles in sorted(knee_angles.items()):
    print(f"id {track_id}: {len(angles)} frames, left knee {min(angles):.0f}-{max(angles):.0f} deg")
id 1: 11 frames, left knee 170-176 deg
id 2: 11 frames, left knee 96-118 deg
id 3: 11 frames, left knee 177-180 deg

La rodilla flexionada del ID 2 corresponde a la persona sentada del clip; las otras dos personas están de pie con las piernas rectas, cerca de los 180°. A partir de aquí, un contador de repeticiones detecta un cruce de umbral en la serie de cada ID: por debajo de un ángulo y luego por encima de otro; eso es lo que te ofrece la solución de monitorización de entrenamientos.

Calcula el coste de la cadena#

Factor de costeDetectar y luego clasificarPose y seguimiento
Aumenta conobjetos por fotogramafotogramas
Palanca principalrecorta los lotes; clasifica una vez por ID de seguimiento y almacena el resultado en cachéreduce imgsz o vid_stride cuando el movimiento sea lento
Modo de falloel clasificador recibe recortes distintos de los datos de entrenamientolos cambios de ID dividen el historial de una persona en dos
Los cambios de ID rompen el historial

Síntoma: el contador de repeticiones se reinicia a mitad de una serie o una persona aparece con dos ID. Causa: el rastreador perdió a la persona (por oclusión o un giro rápido) y le asignó un ID nuevo. Solución: ajusta el rastreador como se explica en el curso de producción y haz que la lógica por ID tolere interrupciones breves: la etiqueta del clasificador almacenada en caché simplemente se vuelve a calcular para el ID nuevo.

Pruébalo

Ejecuta two_stage.py con save_crop=True añadido a la llamada al detector y confirma que los recortes aparecen en carpetas por clase. Ejecuta classify_per_track.py y compara las llamadas al clasificador con las cajas de las personas. Después amplía pose_track.py para que muestre, por ID de seguimiento, cuántos fotogramas tienen un ángulo de rodilla inferior a 120°.

Cuándo se considera terminado
Has terminado la lección cuando se cumplen todas estas condiciones.
  • Los recortes están en crops/<class>/ y puedes explicar cómo los organizarías en carpetas para el clasificador.

  • Puedes indicar cuántas llamadas al clasificador ha ahorrado la caché por seguimiento en el clip de muestra.

  • pose_track.py muestra una línea por ID de seguimiento con el número de fotogramas y el número de ángulos inferiores a 120°.

Mostrar solución

Añade save_crop=True a la llamada al detector:

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5, save_crop=True)[0]

Los recortes aparecen en runs/detect/predict*/crops/person/ y crops/bus/ (el nombre de la carpeta de ejecución aumenta con cada predicción). La caché por seguimiento realizó 3 llamadas al clasificador para 33 cajas de personas, lo que supone un ahorro de 30 llamadas en un clip de 11 fotogramas.

Para contar los ángulos de rodilla, sustituye el bucle final de pose_track.py:

for track_id, angles in sorted(knee_angles.items()):
    bent = sum(a < 120 for a in angles)
    print(f"id {track_id}: {len(angles)} frames, {bent} with knee below 120 deg")
id 1: 11 frames, 0 with knee below 120 deg
id 2: 11 frames, 11 with knee below 120 deg
id 3: 11 frames, 0 with knee below 120 deg

El ID 2, correspondiente a la persona sentada, tiene un ángulo inferior a 120° en los 11 fotogramas; los ID 1 y 3 nunca lo tienen.

Qué viene después

Por último, evaluamos cada tarea con la métrica adecuada y exportamos cada modelo para su implementación.