Conteggi, mappe di calore e stima della velocità
Crea contatori di attraversamento, conteggi di occupazione delle zone, mappe di calore e stime della velocità basati sul tracciamento, e calibrali in modo rigoroso.
La maggior parte dei sistemi di visione in produzione non è model.predict(). È composta da un tracker e un po' di geometria: una linea che gli oggetti attraversano, una zona che occupano, una scala che converte i pixel in metri. È nella geometria che i risultati diventano errati: lo stimatore di velocità sulla clip dimostrativa di questa lezione indica 1 km/h per i pedoni che camminano, un quarto della loro velocità reale, a causa di un solo valore ipotizzato.
Pratico
Tracciamento e geometria#
Ogni classe di soluzione in ultralytics.solutions esegue internamente la modalità di tracciamento, quindi applica la propria geometria alla cronologia della traccia. Installa una sola volta i pacchetti aggiuntivi con pip install "ultralytics[solutions]".
| Classe | Geometria | Risultati principali |
|---|---|---|
ObjectCounter | 2 punti = linea, 3 o più punti = poligono | in_count, out_count, classwise_count |
RegionCounter | Uno o più poligoni denominati | region_counts |
Heatmap | Accumula nel tempo le aree dei riquadri | plot_im (più i conteggi se viene specificata una regione) |
SpeedEstimator | Spostamento della traccia × meter_per_pixel | speed_dict (km/h per ID della traccia) |
Le soluzioni usano tracker="tracktrack.yaml" per impostazione predefinita; passa il tracker che hai convalidato nella lezione precedente.
Conta gli attraversamenti della linea#

Una linea di conteggio è un segmento. Un oggetto viene contato una sola volta, quando il centro del suo riquadro attraversa la linea tra due fotogrammi; il lato da cui proviene determina se viene conteggiato come IN o OUT. Ecco perché il nastro trasportatore qui sopra richiede il tracciamento: senza l'ID, la scatola #015 verrebbe conteggiata di nuovo a ogni fotogramma in cui tocca la linea.
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}}Nei due secondi della clip, quattro persone hanno attraversato il centro della piazza: due in ciascuna direzione. Con la configurazione predefinita di TrackTrack, lo stesso script restituisce in=0 out=1, perché un numero inferiore di tracce comporta meno attraversamenti. Apri counted.mp4 e verifica a vista ogni attraversamento prima di fidarti del conteggio.

Su una strada, la stessa linea separa il traffico per direzione: i veicoli che attraversano verso l'alto vengono conteggiati come IN, quelli diretti verso il basso come OUT. Posiziona la linea dove gli oggetti la attraversano nettamente e ad angolo retto, lontano dai bordi del fotogramma, dagli ostacoli e dall'estremità lontana della strada, dove i riquadri sono minuscoli.
Conta l'occupazione di una zona#
Una zona poligonale risponde alla domanda «quanti oggetti si trovano all'interno in questo momento?». La guida al conteggio nelle regioni include un RegionCounter già pronto; alla base c'è un test di inclusione in un poligono per ogni riquadro tracciato:
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 zonexywh[:, :2] è il centro di ogni riquadro. Per le code e i tempi di permanenza, conserva per ogni ID il timestamp del suo ingresso nella zona.
Stima la velocità#

Ogni etichetta qui sopra deriva dalla traccia dietro al veicolo: la distanza percorsa dalla sua traccia in pixel in un numero noto di fotogrammi. Velocità = distanza in pixel × metri per pixel ÷ tempo trascorso. Due valori determinano se il risultato è corretto:
- Metri per pixel. Misura una lunghezza nota nella scena (una piastrella, la segnaletica di una corsia, un posto auto) alla profondità in cui si muovono gli oggetti. Per le telecamere inclinate, mappa invece il piano stradale con un'omografia a quattro punti.
- La frequenza di acquisizione effettiva. La velocità varia in proporzione agli FPS; se il filmato viene ricodificato o rallentato, tutte le velocità variano dello stesso fattore.
Questo ciclo misura dal centro inferiore di ogni riquadro (dove l'oggetto tocca il suolo) e calcola la media di ogni traccia su 10 frame:
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/hLe persone camminano a 4–5 km/h, quindi 2.1 km/h indica che la scala d'esempio è errata per questa scena: il valore pixels_per_meter non è stato misurato qui. Questa verifica di buon senso, che confronta la mediana con una velocità che conosci, è l'abitudine che conta.
solutions.SpeedEstimator integra la stessa idea per la produzione: misura il centro del riquadro di ogni traccia su max_hist frame, converte con meter_per_pixel e assegna una sola velocità per ID (con un limite di 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/hDi nuovo, una differenza di circa 4×, per lo stesso motivo: una scala stimata in base alle dimensioni del corpo. Il centro del riquadro va bene per telecamere dall'alto come questa; con telecamere inclinate, preferisci il ciclo basato sul centro inferiore o un'omografia.
Crea una mappa di calore#

Una mappa di calore aggiunge intensità sotto ogni riquadro tracciato a ogni frame, così le corsie o i corridoi più trafficati risaltano maggiormente. Assegna un region e conterà anche gli oggetti, come nell'autostrada qui sopra:
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) # ultimo frame con la sovrapposizione di calore accumulataSu ore di filmato, la stessa sovrapposizione mostra i corridoi congestionati, le zone inutilizzate e i percorsi effettivamente seguiti dalle persone. Il monitoraggio degli allenamenti e un'interfaccia Streamlit per l'inferenza in tempo reale sono realizzati con gli stessi componenti.
Insidie#
Sintomo: una pipeline di conteggio funziona alla metà degli FPS previsti. Soluzione: ObjectCounter, Heatmap e SpeedEstimator eseguono internamente rilevamento e tracking. Non inserirli in un ciclo model.track() personalizzato passando r.orig_img; fornisci loro i fotogrammi grezzi.
Sintomo: pedoni a 1 km/h o auto a 300 km/h. Soluzione: misura meter_per_pixel su una lunghezza nota lungo il percorso degli oggetti, verifica gli FPS effettivi della telecamera e confronta la mediana con una velocità nota (passo di camminata o limite indicato).
Il centro geometrico si sposta verso l'alto quando un oggetto si avvicina alla telecamera e il suo riquadro si ingrandisce. Il centro inferiore approssima il punto in cui l'oggetto tocca terra, quindi le distanze misurate da lì seguono molto più fedelmente il movimento reale.
Mostra la soluzione
Calibra usando qualcosa che puoi misurare nel frame, poi verifica che il risultato sia plausibile:
# 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.0300Inserisci pixels_per_meter in speed_loop.py (oppure meter_per_pixel in SpeedEstimator) e riesegui. Se le velocità dei pedoni si discostano ancora molto da 4–5 km/h, la scala è stata misurata a una profondità errata oppure gli FPS non sono corretti: una clip rallentata a metà velocità dimezza ogni valore. Nella clip dimostrativa, entrambi gli script hanno usato la stessa scala stimata (0.03 m per pixel equivale a 33 pixel per metro) e hanno rilevato velocità inferiori di 2–4×; quindi un pixel qui potrebbe coprire più spazio del previsto oppure la frequenza di acquisizione effettiva della clip potrebbe non essere 30 FPS. Solo una misura nella scena può chiarire quale delle due ipotesi è corretta.