Zählungen, Wärmekarten und Geschwindigkeitsschätzung
Erstelle auf Basis von Tracking Linienzählungen, Belegungsmessungen für Zonen, Wärmekarten und Geschwindigkeitsschätzungen und kalibriere sie zuverlässig.
Die meisten Bildverarbeitungssysteme im Produktiveinsatz bestehen nicht nur aus model.predict(). Sie kombinieren einen Tracker mit etwas Geometrie: einer Linie, die Objekte überqueren, einer Zone, in der sie sich aufhalten, und einem Maßstab, der Pixel in Meter umrechnet. Bei der Geometrie passieren die Fehler: Im Democlip dieser Lektion schätzt ein Geschwindigkeitssystem die Gehgeschwindigkeit von Fußgängern auf 1 km/h, also ein Viertel ihres tatsächlichen Tempos, weil eine Zahl geraten wurde.
Praxisnah
Tracking und Geometrie#
Jede Lösungsklasse in ultralytics.solutions führt intern den Tracking-Modus aus und wendet dann ihre eigene Geometrie auf den Track-Verlauf an. Installiere die Zusatzpakete einmalig mit pip install "ultralytics[solutions]".
| Klasse | Geometrie | Wichtige Ergebnisse |
|---|---|---|
ObjectCounter | 2 Punkte = Linie, 3+ Punkte = Polygon | in_count, out_count, classwise_count |
RegionCounter | Ein oder mehrere benannte Polygone | region_counts |
Heatmap | Sammelt Boxflächen im Zeitverlauf | plot_im (zuzüglich Zählungen, wenn eine Region angegeben ist) |
SpeedEstimator | Track-Verschiebung × meter_per_pixel | speed_dict (km/h pro Track-ID) |
Lösungen verwenden standardmäßig tracker="tracktrack.yaml"; übergib den Tracker, den du in der vorherigen Lektion validiert hast.
Linienüberquerungen zählen#

Eine Zähllinie ist ein Liniensegment. Ein Objekt wird einmal gezählt, wenn der Mittelpunkt seiner Box zwischen zwei Bildern die Linie überquert. Die Seite, von der es kommt, bestimmt IN oder OUT. Deshalb ist Tracking beim Förderband oben nötig: Ohne ID würde Box #015 in jedem Bild erneut gezählt, in dem sie die Linie berührt.
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}}In den zwei Sekunden des Clips überquerten vier Menschen die Mitte des Platzes, zwei in jede Richtung. Mit der TrackTrack-Standardkonfiguration meldet dasselbe Skript in=0 out=1, denn weniger Tracks bedeuten weniger Überquerungen. Öffne counted.mp4 und überprüfe jede Überquerung mit eigenen Augen, bevor du einer Zahl vertraust.

Auf einer Straße teilt dieselbe Linie den Verkehr nach Fahrtrichtung auf: Aufwärts fahrende Fahrzeuge werden als IN gezählt, abwärts fahrende als OUT. Platziere die Linie dort, wo Objekte sie sauber und im rechten Winkel überqueren, weit entfernt von Bildrändern, Verdeckungen und dem hinteren Straßenabschnitt, wo die Boxen sehr klein sind.
Belegung einer Zone zählen#
Eine Polygonzone beantwortet die Frage: „Wie viele Objekte befinden sich gerade darin?“ Die Anleitung zur Bereichszählung enthält eine fertige RegionCounter; im Kern wird für jede verfolgte Box geprüft, ob ein Punkt innerhalb des Polygons liegt:
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] ist der Mittelpunkt jeder Box. Für Warteschlangen und Verweildauer speicherst du pro ID einen Zeitstempel dafür, wann das Objekt die Zone betreten hat.
Geschwindigkeit schätzen#

Jede Beschriftung oben ergibt sich aus der Spur hinter dem Fahrzeug: der Pixelstrecke, die sein Track über eine bekannte Anzahl von Bildern zurückgelegt hat. Geschwindigkeit = Pixelstrecke × Meter pro Pixel ÷ verstrichene Zeit. Zwei Eingaben bestimmen, ob das Ergebnis stimmt:
- Meter pro Pixel. Miss eine bekannte Länge in der Szene (eine Bodenfliese, eine Fahrbahnmarkierung, einen Parkplatz) in der Tiefe, in der sich die Objekte bewegen. Bei schräg ausgerichteten Kameras kannst du stattdessen die Bodenebene mit einer Homografie aus vier Punkten abbilden.
- Die tatsächliche Aufnahmebildrate. Die Geschwindigkeit skaliert mit der FPS; neu kodiertes oder verlangsamtes Videomaterial skaliert jede Geschwindigkeit um denselben Faktor.
Diese Schleife misst den unteren Mittelpunkt jeder Box (dort, wo das Objekt den Boden berührt) und mittelt jede Spur über 10 Frames:
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/hMenschen gehen mit 4–5 km/h. Die 2,1 km/h zeigen also, dass die beispielhafte Skalierung für diese Szene falsch ist: Der Wert pixels_per_meter wurde hier nicht gemessen. Genau diese Plausibilitätsprüfung ist entscheidend: den Median mit einer Geschwindigkeit vergleichen, die du kennst.
solutions.SpeedEstimator verpackt dieselbe Idee für den Produktionseinsatz: Es misst den Mittelpunkt der Box jeder Spur über max_hist Frames, rechnet mit meter_per_pixel um und legt pro ID eine Geschwindigkeit fest (begrenzt auf 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/hWieder um etwa den Faktor 4 daneben, aus demselben Grund: Die Skalierung wurde anhand der Körpergröße geschätzt. Der Boxmittelpunkt eignet sich für Kameras mit Blick von oben wie diese; bei schräg ausgerichteten Kameras solltest du die Schleife mit dem unteren Mittelpunkt oder eine Homografie bevorzugen.
Eine Heatmap erstellen#

Eine Heatmap fügt in jedem Frame unter jeder verfolgten Box Wärme hinzu, sodass stark frequentierte Fahrspuren oder Gänge am hellsten leuchten. Gib ihr region, und sie zählt auch Objekte – wie oben auf der Autobahn:
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) # letzter Frame mit der akkumulierten WärmeüberlagerungBei stundenlangem Videomaterial zeigt dieselbe Überlagerung überfüllte Gänge, ungenutzte Bereiche und die Wege, die Menschen tatsächlich nehmen. Trainingsüberwachung und eine Streamlit-Oberfläche für Live-Inferenz basieren auf denselben Bausteinen.
Stolperfallen#
Symptom: Eine Zähler-Pipeline läuft mit der Hälfte der erwarteten FPS. Lösung: ObjectCounter, Heatmap und SpeedEstimator erkennen und verfolgen Objekte intern. Verpacke sie nicht in eine eigene model.track()-Schleife und übergib r.orig_img; übergib ihnen stattdessen rohe Frames.
Symptom: Fußgänger mit 1 km/h oder Autos mit 300 km/h. Lösung: Miss meter_per_pixel anhand einer bekannten Länge, an der sich Objekte bewegen, bestätige die tatsächliche FPS-Zahl der Kamera und vergleiche den Median mit einer bekannten Geschwindigkeit (Gehgeschwindigkeit oder ausgeschriebenes Tempolimit).
Der geometrische Mittelpunkt steigt, wenn sich ein Objekt der Kamera nähert und seine Box größer wird. Der untere Mittelpunkt entspricht annähernd dem Punkt, an dem das Objekt den Boden berührt, sodass die von dort gemessenen Entfernungen die tatsächliche Bewegung viel genauer wiedergeben.
Lösung anzeigen
Kalibriere anhand eines Objekts, dessen Größe du im Bild messen kannst, und prüfe das Ergebnis anschließend auf Plausibilität:
# 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.0300Trage pixels_per_meter in speed_loop.py ein (oder meter_per_pixel in SpeedEstimator) und führe das Skript erneut aus. Wenn die angezeigten Gehgeschwindigkeiten weiterhin deutlich von 4–5 km/h abweichen, wurde die Skalierung in der falschen Tiefe gemessen oder die FPS sind falsch: Ein auf halbe Geschwindigkeit verlangsamter Clip halbiert jeden Messwert. Beim Demo-Clip verwendeten beide Skripte dieselbe geschätzte Skalierung (0,03 m pro Pixel entsprechen 33 Pixeln pro Meter) und zeigten um den Faktor 2–4 zu niedrige Werte. Entweder deckt ein Pixel hier mehr Fläche ab als angenommen, oder die tatsächliche Aufnahmebildrate des Clips beträgt nicht 30 FPS. Nur eine Messung in der Szene kann klären, was zutrifft.