Recuentos, mapas de calor y estimación de velocidad
Crea contadores de líneas, ocupación de zonas, mapas de calor y estimaciones de velocidad a partir del seguimiento, y calibra los valores con rigor.
La mayoría de los sistemas de visión en producción no son model.predict(). Son un rastreador más un poco de geometría: una línea que cruzan los objetos, una zona que ocupan, una escala que convierte píxeles en metros. La geometría es donde los resultados fallan: en el clip de demostración de esta lección, un estimador de velocidad indica que los peatones caminan a 1 km/h, una cuarta parte de su velocidad real, debido a un único valor estimado.
Práctico
Seguimiento más geometría#
Cada clase de solución de ultralytics.solutions ejecuta internamente el modo Track y, después, aplica su propia geometría al historial de trayectorias. Instala los extras una sola vez con pip install "ultralytics[solutions]".
| Clase | Geometría | Resultados clave |
|---|---|---|
ObjectCounter | 2 puntos = línea, 3 o más puntos = polígono | in_count, out_count, classwise_count |
RegionCounter | Uno o varios polígonos con nombre | region_counts |
Heatmap | Acumula las áreas de las cajas a lo largo del tiempo | plot_im (más recuentos si se proporciona una región) |
SpeedEstimator | Desplazamiento de la trayectoria × meter_per_pixel | speed_dict (km/h por ID de trayectoria) |
Las soluciones usan tracker="tracktrack.yaml" de forma predeterminada; pasa el rastreador que validaste en la lección anterior.
Cuenta los cruces de línea#

Una línea de recuento es un segmento. Un objeto se cuenta una sola vez, cuando el centro de su caja cruza la línea entre dos fotogramas; el lado del que procede determina si se cuenta como IN o OUT. Por eso la cinta transportadora de arriba necesita seguimiento: sin el ID, la caja #015 se volvería a contar en cada fotograma en el que toca la línea.
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}}En los dos segundos del clip, cuatro personas cruzaron el centro de la plaza: dos en cada dirección. Con la configuración predeterminada de TrackTrack, el mismo script devuelve in=0 out=1, porque un menor número de trayectorias implica menos cruces. Abre counted.mp4 y comprueba a simple vista cada cruce antes de fiarte del recuento.

En una carretera, la misma línea separa el tráfico por dirección: los vehículos que cruzan hacia arriba cuentan como IN y los que cruzan hacia abajo, como OUT. Coloca la línea donde los objetos la crucen limpiamente y en ángulo recto, lejos de los bordes del fotograma, los elementos que los ocluyen y el extremo más lejano de la carretera, donde las cajas son diminutas.
Cuenta la ocupación de una zona#
Una zona poligonal responde a la pregunta «¿cuántos objetos hay dentro en este momento?». La guía de recuento por regiones incluye un RegionCounter listo para usar; por debajo, consiste en una prueba de punto dentro del polígono por cada caja seguida:
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] es el centro de cada caja. Para las colas y el tiempo de permanencia, guarda una marca de tiempo por ID que indique cuándo entró en la zona.
Estima la velocidad#

Cada etiqueta de arriba se calcula a partir del rastro del vehículo: la distancia que ha recorrido su trayectoria en píxeles durante un número conocido de fotogramas. Velocidad = distancia en píxeles × metros por píxel ÷ tiempo transcurrido. Dos datos determinan si el resultado es correcto:
- Metros por píxel. Mide una longitud conocida de la escena (una baldosa, una marca de carril o una plaza de aparcamiento) a la profundidad a la que se mueven los objetos. En el caso de cámaras anguladas, calibra el plano del suelo con una homografía de cuatro puntos.
- La velocidad real de captura. La velocidad escala con los FPS; volver a codificar o ralentizar el vídeo escala todas las velocidades por el mismo factor.
Este bucle mide desde el centro inferior de cada caja (donde el objeto toca el suelo) y calcula la media de cada seguimiento a lo largo de 10 fotogramas:
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/hLa gente camina a 4–5 km/h, así que 2,1 km/h indica que la escala de ejemplo no es correcta para esta escena: el valor pixels_per_meter no se midió aquí. Esa comprobación de sentido común, que compara la mediana con una velocidad que conoces, es el hábito que importa.
solutions.SpeedEstimator lleva la misma idea a producción: mide el centro de la caja de cada seguimiento a lo largo de max_hist fotogramas, convierte con meter_per_pixel y fija una velocidad por ID (con un límite de 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/hVuelve a desviarse unas 4 veces, por el mismo motivo: una escala estimada a partir del tamaño del cuerpo. El centro de la caja funciona con cámaras cenitales como esta; con cámaras inclinadas, es preferible usar el bucle del centro inferior o una homografía.
Acumular un mapa de calor#

Un mapa de calor añade calor debajo de cada caja seguida en cada fotograma, de modo que los carriles o pasillos con más tráfico brillan con más intensidad. Dale un region y también contará los objetos, como en la autopista de arriba:
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) # último fotograma con la superposición de calor acumuladaA lo largo de horas de vídeo, la misma superposición muestra los pasillos congestionados, las zonas sin usar y las rutas que sigue la gente en realidad. La supervisión de entrenamientos y una interfaz de inferencia en directo con Streamlit se crean con los mismos componentes.
Errores habituales#
Síntoma: una cadena de procesamiento con contador funciona a la mitad de los FPS esperados. Solución: ObjectCounter, Heatmap y SpeedEstimator detectan y rastrean internamente. No los envuelvas en tu propio bucle model.track() ni les pases r.orig_img; aliméntalos con fotogramas sin procesar.
Síntoma: peatones a 1 km/h o coches a 300 km/h. Solución: mide meter_per_pixel a partir de una longitud conocida por la que se desplacen los objetos, confirma los FPS reales de la cámara y compara la mediana con una velocidad conocida (el ritmo al caminar o el límite indicado).
El centro geométrico sube a medida que un objeto se acerca a la cámara y su caja aumenta de tamaño. La parte inferior central aproxima el punto en el que el objeto toca el suelo, por lo que las distancias medidas desde ahí reflejan mucho mejor el movimiento real.
Mostrar solución
Calibra con algo que puedas medir en el fotograma y luego comprueba que el resultado tenga sentido:
# 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.0300Pon pixels_per_meter en speed_loop.py (o meter_per_pixel en SpeedEstimator) y vuelve a ejecutarlo. Si las personas siguen apareciendo muy lejos de los 4–5 km/h, la escala se midió a una profundidad incorrecta o los FPS no son correctos: si un clip se reproduce a la mitad de velocidad, todas las lecturas se reducen a la mitad. En el clip de demostración, ambos scripts usaron la misma escala estimada (0,03 m por píxel son 33 píxeles por metro) y marcaron velocidades entre 2 y 4 veces inferiores; por tanto, o bien cada píxel cubre más terreno del estimado o la frecuencia de captura real del clip no es de 30 FPS. Solo una medición en la escena permite saber cuál de las dos cosas ocurre.