Ultralytics YOLO27 : Inscris-toi sur la liste d’attente.
Get Started
Ultralytics YOLO en production·Suivre les objets et créer des pipelines·Leçon 6/9
Leçonintermédiaire

Comptage, cartes thermiques et estimation de la vitesse

Crée des compteurs de franchissement de ligne, des décomptes d’occupation de zone, des cartes thermiques et des estimations de vitesse à partir du suivi, puis calibre-les avec rigueur.

La plupart des systèmes de vision en production ne sont pas model.predict(). Ils reposent sur un tracker et un peu de géométrie : une ligne que les objets franchissent, une zone qu’ils occupent, une échelle qui convertit les pixels en mètres. C’est dans cette géométrie que les résultats déraillent : l’estimateur de vitesse appliqué à l’extrait de démonstration de cette leçon donne 1 km/h pour des piétons qui marchent, soit un quart de leur vitesse réelle, à cause d’une seule valeur estimée.

Résultat

Crée un compteur de franchissement de ligne, un décompte de zone, une carte thermique et une estimation de vitesse à partir d’une vraie vidéo, puis calibre la vitesse à l’aide d’une référence connue.

Parcours accéléré
Si tu connais déjà les bases, voici la version courte.
  1. Franchissement de ligne : solutions.ObjectCounter(region=[p1, p2]) ; le centre d’une piste franchit la ligne et incrémente le compteur IN ou OUT.

  2. Occupation de zone : un test d’appartenance à un polygone pour chaque boîte suivie indique qui se trouve à l’intérieur à cet instant.

  3. Carte thermique : solutions.Heatmap accumule les endroits où les objets suivis passent du temps.

  4. Vitesse : déplacement en pixels × mètres par pixel ÷ durée. Mesure l’échelle dans la scène et confirme le nombre réel d’images par seconde.

Pratique

Suivi et géométrie#

Chaque classe de solution dans ultralytics.solutions exécute le mode Track en interne, puis applique sa propre géométrie à l’historique des pistes. Installe une seule fois les dépendances supplémentaires avec pip install "ultralytics[solutions]".

ClasseGéométriePrincipaux résultats
ObjectCounter2 points = ligne, 3 points ou plus = polygonein_count, out_count, classwise_count
RegionCounterUn ou plusieurs polygones nommésregion_counts
HeatmapAccumule les surfaces des boîtes au fil du tempsplot_im (avec les décomptes si une région est fournie)
SpeedEstimatorDéplacement de la piste × meter_per_pixelspeed_dict (km/h par ID de piste)

Les solutions utilisent tracker="tracktrack.yaml" par défaut ; passe le tracker que tu as validé dans la leçon précédente.

Compter les franchissements de ligne#

Comptage de colis sur un convoyeur d’entrepôt avec Ultralytics YOLO : chaque boîte porte un ID de suivi (de #013 à #018) et le compteur IN affiche 12 au niveau de la ligne de comptage cyan

Une ligne de comptage est un segment. Un objet est compté une seule fois, lorsque le centre de sa boîte franchit la ligne entre deux images ; le côté d’où il vient détermine s’il s’agit de IN ou de OUT. C’est pourquoi le convoyeur ci-dessus nécessite un suivi : sans ID, la boîte #015 serait comptée à nouveau à chaque image où elle touche la ligne.

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))  # 640x360, 30 FPS, 62 frames
w, h, fps = (int(cap.get(p)) for p in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
writer = cv2.VideoWriter("counted.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))

counter = solutions.ObjectCounter(
    model="yolo26n.pt",
    region=[(0, 180), (640, 180)],  # two points: a horizontal line across the middle of the frame
    classes=[0],  # person
    tracker="bytetrack.yaml",
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = counter(frame)  # detect + track + count in one call
    writer.write(results.plot_im)

cap.release()
writer.release()
print(f"in={results.in_count} out={results.out_count} classwise={results.classwise_count}")
in=2 out=2 classwise={'person': {'IN': 2, 'OUT': 2}}

Quatre personnes ont traversé le milieu de la place en deux secondes dans l’extrait : deux dans chaque sens. Avec la configuration TrackTrack par défaut, le même script renvoie in=0 out=1, car moins de pistes signifie moins de franchissements. Ouvre counted.mp4 et vérifie visuellement chaque franchissement avant de te fier au résultat.

Compteur de franchissement de ligne Ultralytics YOLO sur une route à chaussées séparées : des voitures et un camion suivis franchissent une ligne cyan, des flèches indiquent le sens et le décompte des véhicules affiche 24 à l’entrée et 18 à la sortie

Sur une route, la même ligne sépare le trafic selon sa direction : les véhicules qui la franchissent vers le haut sont comptés comme IN, ceux qui la franchissent vers le bas comme OUT. Place la ligne là où les objets la franchissent proprement et à angle droit, à l’écart des bords de l’image, des obstacles et du fond de la route où les boîtes sont minuscules.

Compter l’occupation d’une zone#

Un polygone répond à la question « combien d’objets se trouvent à l’intérieur en ce moment ? ». Le guide du comptage par région fournit un RegionCounter prêt à l’emploi ; en dessous, il s’agit d’un test d’appartenance à un polygone pour chaque boîte suivie :

from shapely.geometry import Point, Polygon

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
zone = Polygon([(100, 100), (500, 100), (500, 300), (100, 300)])  # pixel coordinates

peak = 0
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    inside = sum(zone.contains(Point(cx, cy)) for cx, cy in r.boxes.xywh[:, :2].tolist())
    peak = max(peak, inside)
print(f"peak occupancy: {peak} people inside the zone")
peak occupancy: 10 people inside the zone

xywh[:, :2] correspond au centre de chaque boîte. Pour les files d’attente et les durées de présence, conserve un horodatage par ID indiquant quand l’objet est entré dans la zone.

Estimer la vitesse#

Estimation de la vitesse sur une autoroute : chaque voiture et camion suivi porte une étiquette indiquant sa vitesse estimée en km/h (de 48 à 65) et laisse une courte ligne de mouvement derrière lui

Chaque étiquette ci-dessus est calculée à partir de la trajectoire laissée derrière le véhicule : la distance parcourue par sa piste en pixels sur un nombre connu d’images. Vitesse = distance en pixels × mètres par pixel ÷ durée écoulée. Deux paramètres déterminent la justesse du résultat :

  1. Mètres par pixel. Mesure une longueur connue dans la scène (un carreau de sol, un marquage de voie, une place de stationnement) à la profondeur où les objets se déplacent. Pour les caméras inclinées, établis plutôt une homographie à quatre points du plan du sol.
  2. Le véritable débit de capture. La vitesse varie proportionnellement au FPS ; une vidéo réencodée ou ralentie fait varier toutes les vitesses dans la même proportion.

Cette boucle mesure le centre inférieur de chaque boîte (là où l’objet touche le sol) et calcule la moyenne de chaque trajectoire sur 10 images :

from collections import defaultdict, deque

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
fps = 30  # the real capture rate, not the playback rate
pixels_per_meter = 33  # example: a 1.2 m floor feature spans 40 px; measure your own scene

prev = {}  # track ID -> last bottom-center point
window = defaultdict(lambda: deque(maxlen=10))  # about 0.3 s of speeds per track at 30 FPS
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    for (x1, y1, x2, y2), tid in zip(r.boxes.xyxy.tolist(), r.boxes.id.int().tolist()):
        cx, cy = (x1 + x2) / 2, y2  # bottom-center sits on the ground plane
        if tid in prev:
            px, py = prev[tid]
            meters = ((cx - px) ** 2 + (cy - py) ** 2) ** 0.5 / pixels_per_meter
            window[tid].append(meters * fps * 3.6)  # km/h
        prev[tid] = (cx, cy)

smoothed = sorted(sum(s) / len(s) for s in window.values() if len(s) == s.maxlen)
print(f"{len(smoothed)} tracks; median {smoothed[len(smoothed) // 2]:.1f} km/h")
29 tracks; median 2.1 km/h

Les gens marchent à 4–5 km/h ; la valeur de 2,1 km/h indique donc que l’échelle de l’exemple ne convient pas à cette scène : la valeur pixels_per_meter n’a pas été mesurée ici. Cette vérification rapide, qui consiste à comparer la médiane à une vitesse que tu connais, est le bon réflexe.

solutions.SpeedEstimator reprend la même idée pour la production : il mesure le centre de la boîte de chaque trajectoire sur max_hist images, convertit la mesure avec meter_per_pixel et fixe une vitesse par ID (plafonnée à max_speed) :

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
speed = solutions.SpeedEstimator(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    fps=30,  # the real capture rate, not the playback rate
    meter_per_pixel=0.03,  # a guess: a ~0.45 m shoulder width spans ~15 px here
    max_hist=15,  # 0.5 s of history before a speed is locked
    verbose=False,
)

speeds = {}
while True:
    ok, frame = cap.read()
    if not ok:
        break
    speeds.update(speed(frame).speed_dict)  # {track_id: km/h} once locked
cap.release()

kmh = sorted(speeds.values())
print(f"{len(kmh)} tracks; median {kmh[len(kmh) // 2]} km/h, range {kmh[0]}-{kmh[-1]} km/h")
25 tracks; median 1 km/h, range 0-4 km/h

Là encore, la valeur est environ 4 fois trop faible, pour la même raison : une échelle estimée à partir de la taille du corps. Le centre de la boîte convient aux caméras en vue du dessus comme celle-ci ; avec des caméras inclinées, préfère la boucle utilisant le centre inférieur ou une homographie.

Accumuler une carte thermique#

Carte thermique Ultralytics YOLO sur une autoroute urbaine : un dégradé bleu-rouge est le plus lumineux sur les voies les plus fréquentées, avec des boîtes autour des voitures et des camions et un décompte en cours de 12 voitures et 2 camions

Une carte thermique ajoute de la chaleur sous chaque boîte suivie à chaque image ; les voies ou les allées les plus fréquentées sont donc les plus lumineuses. Donne-lui un region et elle comptera aussi les objets, comme sur l’autoroute ci-dessus :

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
heatmap = solutions.Heatmap(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    colormap=cv2.COLORMAP_PARULA,
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = heatmap(frame)
cap.release()
cv2.imwrite("heatmap.jpg", results.plot_im)  # dernière image avec la superposition thermique accumulée

Sur plusieurs heures de vidéo, la même superposition révèle les allées encombrées, les zones inutilisées et les trajets réellement empruntés par les gens. Le suivi des entraînements et une interface Streamlit d’inférence en direct sont construits à partir des mêmes composants.

Pièges#

Exécuter le modèle deux fois par image

Symptôme : un pipeline de comptage fonctionne à la moitié du FPS attendu. Solution : ObjectCounter, Heatmap et SpeedEstimator effectuent la détection et le suivi en interne. Ne les place pas dans ta propre boucle model.track() et ne leur transmets pas r.orig_img ; fournis-leur des images brutes.

Se fier à une vitesse non calibrée

Symptôme : des piétons à 1 km/h ou des voitures à 300 km/h. Solution : mesure meter_per_pixel à partir d'une longueur connue sur le trajet des objets, confirme le véritable FPS de la caméra et compare la médiane à une vitesse connue (allure de marche ou limite affichée).

Utiliser le bas-centre de la boîte avec les caméras inclinées

Le centre géométrique monte lorsque l'objet s'approche de la caméra et que sa boîte s'agrandit. Le bas-centre estime l'endroit où l'objet touche le sol : les distances mesurées à partir de ce point suivent donc bien mieux le déplacement réel.

Essayer

Sur une vidéo de ton propre domaine (ou sur le clip de démonstration), construis le compteur de lignes et vérifie visuellement chaque franchissement dans counted.mp4. Mesure ensuite une longueur réelle dans la scène, calcule l’échelle avec le script de solution, relance speed_loop.py et compare la vitesse médiane à une référence connue. Présente les décomptes, l’échelle et les vitesses médianes avant et après.

Terminé lorsque
Tu as terminé la leçon lorsque tous les éléments suivants sont réunis.
  • Les valeurs IN et OUT de ton compteur de lignes correspondent au décompte manuel du même clip.

  • Ton estimation de vitesse utilise une échelle mesurée dans la scène, et sa médiane se situe à moins de 25 % d’une vitesse de référence connue.

  • Tu peux expliquer pourquoi le centre inférieur de la boîte est le point adapté au calcul de la vitesse avec une caméra inclinée.

Afficher la solution

Calibre à partir d’un élément mesurable dans l’image, puis vérifie la cohérence du résultat :

# Two points along a floor feature whose real length you know (read them off a saved frame)
(x1, y1), (x2, y2) = (100, 300), (140, 300)
real_length_m = 1.2  # e.g. measured tile width or lane marking

pixels = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(f"pixels_per_meter = {pixels / real_length_m:.1f}, meter_per_pixel = {real_length_m / pixels:.4f}")
pixels_per_meter = 33.3, meter_per_pixel = 0.0300

Place pixels_per_meter dans speed_loop.py (ou meter_per_pixel dans SpeedEstimator), puis relance le script. Si la vitesse des piétons reste très éloignée de 4–5 km/h, l’échelle a été mesurée à une profondeur incorrecte ou le FPS est erroné : un clip ralenti à la moitié de sa vitesse divise toutes les mesures par deux. Sur le clip de démonstration, les deux scripts ont utilisé la même échelle estimée (0,03 m par pixel, soit 33 pixels par mètre) et ont obtenu des valeurs 2 à 4 fois trop faibles ; un pixel couvre donc peut-être plus de terrain que l’estimation, ou le débit de capture réel du clip n’est pas de 30 FPS. Seule une mesure dans la scène permet de trancher.

Et ensuite ?

Un modèle et un flux, c’est gérable. Étape suivante : traiter de nombreux flux en parallèle et appliquer les bonnes pratiques d’inférence thread-safe.