Ultralytics YOLO27 : Inscris-toi sur la liste d’attente.
Get Started
Leçonintermédiaire

Combiner des tâches dans un seul pipeline

Construis une cascade détection puis classification, classe chaque objet suivi une seule fois plutôt qu’à chaque image, et transforme les points clés de pose en séries temporelles par personne.

Les systèmes les plus utiles au-delà de la détection reposent généralement sur deux modèles, pas un seul. Un détecteur repère chaque ouvrier et un classificateur détermine si chaque casque est du bon type ; un modèle de pose repère les articulations et un traqueur les transforme en une chronologie qui permet de compter les répétitions. Le code est court. Ces systèmes échouent lorsque le jeu de données du deuxième étage ne correspond pas aux recadrages du premier, ou lorsque le coût se multiplie pour chaque objet dans chaque image — exécuté sans optimisation, un flux à 30 FPS avec dix personnes nécessite 300 classifications par seconde.

Résultat

Exécute un script de détection puis classification, une version qui ne classe chaque personne suivie qu’une seule fois, ainsi qu’un traqueur de pose qui conserve les angles des genoux de chaque personne ; crée les données d’entraînement du classificateur à partir des recadrages du détecteur lui-même.

Parcours accéléré
Si tu connais déjà les bases, voici la version courte.
  1. Détecte, recadre avec result.orig_img[y1:y2, x1:x2] et classe tous les recadrages en un seul appel groupé.

  2. Crée le jeu de données du classificateur à partir des recadrages de ton détecteur (save_crop=True), et non de photos en studio.

  3. En vidéo, effectue le suivi avec persist=True, classe chaque result.boxes.id une seule fois, puis réutilise l’étiquette.

  4. Pour combiner pose et suivi, indexe l’historique des points clés de chaque personne à l’aide de son ID de suivi.

Pratique

Deux modèles qui couvrent la plupart des produits#

Les deux partent de la même image et de la même API Ultralytics ; la différence tient à ce que le deuxième étage traite. Le choix et le réglage du traqueur, ainsi que la gestion des flux pour le suivi d’objets, sont abordés dans Suivre des objets avec ByteTrack, BoT-SORT et TrackTrack ; ici, nous nous concentrons sur l’enchaînement des tâches.

Détecter, puis classifier les recadrages#

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)

Le traitement est bien conçu : les recadrages proviennent de orig_img à leur résolution d’origine, dans les coordonnées en pixels du détecteur, et le classificateur les traite tous en un seul appel groupé. Les étiquettes sont volontairement absurdes.

La qualité d'une deuxième étape dépend de sa correspondance avec les recadrages

Le classificateur ImageNet n'a jamais été entraîné sur des silhouettes de rue serrées et partiellement recadrées : il devine, et aucune confiance ne dépasse 0.50. La même chose se produit avec un classificateur entraîné sur des photos de produits nettes auquel on fournit des recadrages flous issus du détecteur.

Créer le jeu de données du deuxième étage à partir de tes propres recadrages#

Pour obtenir des données correspondantes de manière fiable, laisse le détecteur les produire :

Grille de recadrages numérotés de valises extraits par Ultralytics YOLO26 de séquences de tapis à bagages d’aéroport, prêts à être triés dans des dossiers de classes pour le classificateur

  1. Exécute le détecteur sur des séquences représentatives avec save_crop=True, par exemple yolo predict model=best.pt source=footage/ save_crop=True. Les recadrages sont enregistrés dans runs/detect/predict/crops/<class>/.
  2. Trie les recadrages de chaque classe dans tes dossiers de classes fines (helmet_ok/, helmet_missing/) — c’est déjà la structure de classification présentée dans la leçon précédente.
  3. Entraîne le classificateur sur ces dossiers et valide-le sur des recadrages provenant d’un autre jour ou d’une autre caméra.

Puisque le détecteur a effectué les recadrages, ceux-ci reproduisent son cadrage réel, le flou et les occultations — exactement ce que le classificateur verra en production — et leur ajustement fin relève de l’apprentissage par transfert classique.

Classifier une seule fois par ID de suivi#

Un casque ne change pas d’une image à l’autre ; classifier chaque boîte dans chaque image est donc un gaspillage. Suis les détections et ne classe chaque nouvel ID de suivi qu’une seule fois :

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)}

Trois appels au classificateur au lieu de 33 sur un clip de 11 images ; sur un long flux, le gain augmente à chaque image pendant laquelle une personne reste visible. Les étiquettes sont encore des prédictions d’ImageNet — remplace le modèle par ton classificateur ajusté finement et elles deviendront la réponse pour chaque personne.

Règle de décision

Effectue une seule classification par piste lorsque l'attribut ne peut pas changer tant que l'objet est visible (type de casque, marque du véhicule, référence du produit). Recommence la classification toutes les N images lorsque l'attribut peut changer (porte ouverte, main levée). Si les premières détections sont généralement petites et floues, classe plutôt l'image où la boîte de la piste est la plus grande.

Pose et suivi#

Estimation de pose Ultralytics YOLO comptant des pompes, avec le squelette du corps, un angle de coude de 110 degrés et un compteur de répétitions

Le mode suivi ajoute un result.boxes.id persistant à chaque personne, ce qui transforme les points clés en série temporelle. Ce script enregistre l’angle du genou gauche de chaque personne dans la même courte vidéo :

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

Le genou plié de l’ID 2 correspond à la personne assise dans le clip ; les deux autres personnes sont debout, les jambes tendues, à près de 180°. À partir de là, un compteur de répétitions détecte un franchissement de seuil dans la série de chaque ID — vers le bas sous un angle, puis vers le haut au-dessus d’un autre — comme le fait la solution de suivi des entraînements.

Évaluer le coût de la chaîne#

Facteur de coûtDétecter, puis classifierPose et suivi
Évolue en fonction desobjets par imageimages
Principal levierextrais des recadrages ; classe une seule fois par ID de suivi et mets le résultat en cachediminue imgsz ou vid_stride lorsque le mouvement est lent
Mode d’échecle classificateur voit des recadrages différents de ses données d’entraînementles changements d’ID divisent l’historique d’une personne en deux
Les changements d'ID interrompent les historiques

Symptôme : un compteur de répétitions se réinitialise au milieu d'une série, ou une personne apparaît sous deux ID. Cause : le suivi a perdu la personne (occultation, virage rapide) et lui a attribué un nouvel ID. Solution : règle le suivi comme dans le cours sur la mise en production et fais en sorte que la logique par ID tolère les interruptions brèves : l'étiquette de classe mise en cache sera simplement recalculée pour le nouvel ID.

Essayer

Lance two_stage.py en ajoutant save_crop=True à l’appel du détecteur et vérifie que les recadrages apparaissent dans des dossiers par classe. Lance classify_per_track.py et compare les appels du classificateur aux boîtes des personnes. Étends ensuite pose_track.py pour afficher, pour chaque ID de suivi, le nombre d’images où l’angle du genou est inférieur à 120°.

Terminé lorsque
Tu as terminé la leçon lorsque tous les éléments suivants sont réunis.
  • Les recadrages se trouvent sous crops/<class>/, et tu peux expliquer comment les trier dans des dossiers de classificateur.

  • Tu peux indiquer combien d’appels au classificateur la mise en cache par suivi a permis d’économiser sur le clip d’exemple.

  • pose_track.py affiche une ligne par ID de suivi, avec son nombre d’images et son nombre d’angles inférieurs à 120°.

Afficher la solution

Ajoute save_crop=True à l’appel du détecteur :

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

Les recadrages apparaissent sous runs/detect/predict*/crops/person/ et crops/bus/ (le nom du dossier d’exécution s’incrémente à chaque prédiction). La mise en cache par suivi a réduit à 3 les appels au classificateur pour 33 boîtes de personnes, soit 30 appels économisés sur un clip de 11 images.

Pour compter les angles des genoux, remplace la boucle finale 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

L’ID 2, la personne assise, a un angle inférieur à 120° dans les 11 images ; ce n’est jamais le cas des ID 1 et 3.

Et ensuite ?

Enfin, nous évaluons chaque tâche avec la métrique appropriée et exportons chaque modèle pour le déploiement.