Ultralytics YOLO27: Únete a la lista de espera.
Get Started
Ultralytics YOLO en producción·Seguimiento y creación de pipelines·Lección 6/9
Lecciónintermedio

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.

Resultado

Crea un contador de líneas, un recuento de zonas, un mapa de calor y una estimación de velocidad con un vídeo real, y calibra la velocidad con una referencia conocida.

Vía rápida
Si ya tienes experiencia, aquí tienes un resumen.
  1. Cruce de líneas: solutions.ObjectCounter(region=[p1, p2]); cuando el centro de una trayectoria cruza la línea, se suma a IN o OUT.

  2. Ocupación de zonas: una prueba de punto dentro del polígono por cada caja seguida indica quién está dentro en ese momento.

  3. Mapa de calor: solutions.Heatmap acumula las zonas en las que los objetos seguidos pasan tiempo.

  4. Velocidad: desplazamiento en píxeles × metros por píxel ÷ tiempo. Mide la escala de la escena y confirma los FPS reales.

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]".

ClaseGeometríaResultados clave
ObjectCounter2 puntos = línea, 3 o más puntos = polígonoin_count, out_count, classwise_count
RegionCounterUno o varios polígonos con nombreregion_counts
HeatmapAcumula las áreas de las cajas a lo largo del tiempoplot_im (más recuentos si se proporciona una región)
SpeedEstimatorDesplazamiento de la trayectoria × meter_per_pixelspeed_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#

Recuento de paquetes en una cinta transportadora de almacén con Ultralytics YOLO: cada caja tiene un ID de seguimiento (#013 a #018) y el contador IN marca 12 donde las cajas cruzan la línea de recuento cian

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.

Contador de cruce de línea con Ultralytics YOLO en una carretera dividida: coches y un camión seguidos cruzan una línea cian, con flechas que indican la dirección y un recuento de vehículos de 24 de entrada y 18 de salida

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 zone

xywh[:, :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#

Estimación de velocidad en una autopista: cada coche y camión seguido lleva una etiqueta con la velocidad estimada en km/h (48 a 65) y deja un rastro corto que muestra su movimiento

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:

  1. 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.
  2. 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/h

La 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/h

Vuelve 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#

Mapa de calor de Ultralytics YOLO en una autopista urbana: una superposición que va del azul al rojo alcanza su máxima intensidad en los carriles más transitados, con cajas alrededor de coches y camiones y un recuento de 12 coches y 2 camiones

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 acumulada

A 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#

Ejecutar el modelo dos veces por fotograma

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.

Confiar en una velocidad sin calibrar

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

Usa la parte inferior central de la caja con cámaras en ángulo

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.

Pruébalo

Con un vídeo de tu propio dominio (o el clip de demostración), crea el contador de línea y comprueba visualmente cada cruce en counted.mp4. Después, mide una longitud real en la escena, calcula la escala con el script de solución, vuelve a ejecutar speed_loop.py y compara la velocidad mediana con una referencia conocida. Entrega los recuentos, la escala y las velocidades medianas antes y después.

Cuándo se considera terminado
Has terminado la lección cuando se cumplen todas estas condiciones.
  • Los valores IN y OUT de tu contador de línea coinciden con un recuento manual del mismo clip.

  • Tu estimación de velocidad utiliza una escala medida en la escena y su mediana no se desvía más de un 25 % de una velocidad de referencia conocida.

  • Puedes explicar por qué el centro inferior de la caja es el punto adecuado para calcular la velocidad con una cámara inclinada.

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.0300

Pon 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.

Qué viene después

Un modelo y un flujo son manejables. A continuación: varios flujos en paralelo y las prácticas de inferencia segura para hilos que conlleva.