Contagem, mapas de calor e estimativa de velocidade
Cria contadores de linhas, ocupação de zonas, mapas de calor e estimativas de velocidade com base no rastreamento, e calibra-os de forma rigorosa.
A maioria dos sistemas de visão em produção não é model.predict(). São um rastreador e um pouco de geometria: uma linha que os objetos atravessam, uma zona que ocupam, uma escala que converte píxeis em metros. É na geometria que os resultados falham: um estimador de velocidade aplicado ao clipe de demonstração desta lição indica que peões a caminhar se deslocam a 1 km/h, um quarto da velocidade real, devido a um único valor estimado.
Prático
Rastreamento e geometria#
Cada classe de solução em ultralytics.solutions executa internamente o modo de rastreamento e, em seguida, aplica a sua própria geometria ao histórico dos rastos. Instala as dependências adicionais uma vez com pip install "ultralytics[solutions]".
| Classe | Geometria | Principais resultados |
|---|---|---|
ObjectCounter | 2 pontos = linha, 3 ou mais pontos = polígono | in_count, out_count, classwise_count |
RegionCounter | Um ou mais polígonos nomeados | region_counts |
Heatmap | Acumula as áreas das caixas ao longo do tempo | plot_im (mais contagens, se for indicada uma região) |
SpeedEstimator | Deslocamento do rasto × meter_per_pixel | speed_dict (km/h por ID de rasto) |
As soluções usam tracker="tracktrack.yaml" por predefinição; passa o rastreador que validaste na lição anterior.
Conta as passagens por linha#

Uma linha de contagem é um segmento. Um objeto é contado uma vez, quando o centro da sua caixa atravessa a linha entre dois fotogramas; o lado de onde vem determina se é IN ou OUT. Por isso, o tapete rolante acima precisa de rastreamento: sem o ID, a caixa #015 seria contada de novo em cada fotograma em que tocasse na linha.
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}}Quatro pessoas atravessaram o centro da praça nos dois segundos do clipe: duas em cada direção. Com a configuração predefinida do TrackTrack, o mesmo script indica in=0 out=1, porque menos rastos significam menos passagens. Abre counted.mp4 e verifica visualmente cada passagem antes de confiares num número.

Numa estrada, a mesma linha separa o tráfego por direção: os veículos que a atravessam para cima contam como IN; os que a atravessam para baixo, como OUT. Coloca a linha onde os objetos a atravessem claramente e em ângulo reto, longe dos limites do fotograma, de oclusores e do extremo da estrada, onde as caixas são pequenas.
Conta a ocupação de uma zona#
Uma zona poligonal responde à pergunta «quantos objetos estão dentro neste momento?». O guia de contagem por região inclui um RegionCounter pronto a usar; por baixo, é um teste de ponto no polígono por cada caixa rastreada:
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] é o centro de cada caixa. Para filas e tempos de permanência, mantém um registo temporal por ID da hora a que entrou na zona.
Estima a velocidade#

Cada etiqueta acima é calculada com base no rasto atrás do veículo: a distância percorrida pelo rasto em píxeis durante um número conhecido de fotogramas. Velocidade = distância em píxeis × metros por píxel ÷ tempo decorrido. Dois valores determinam se o resultado está correto:
- Metros por píxel. Mede um comprimento conhecido na cena (um ladrilho, uma marcação de faixa, um lugar de estacionamento) à profundidade a que os objetos se deslocam. Para câmaras inclinadas, mapeia antes o plano do solo com uma homografia de quatro pontos.
- A taxa real de captura. A velocidade varia com o FPS; se o vídeo for recodificado ou desacelerado, todas as velocidades variam pelo mesmo fator.
Este loop mede a partir do centro inferior de cada caixa (onde o objeto toca o chão) e calcula a média de cada trajetória ao longo de 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/hAs pessoas caminham a 4–5 km/h, então 2,1 km/h indica que a escala de exemplo está errada para esta cena: o valor pixels_per_meter não foi medido aqui. Essa verificação de plausibilidade, comparando a mediana com uma velocidade conhecida, é o hábito que importa.
solutions.SpeedEstimator empacota a mesma ideia para produção: mede o centro da caixa de cada trajetória ao longo de max_hist frames, converte com meter_per_pixel e fixa uma velocidade por ID (com limite 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/hNovamente, cerca de 4× de diferença, pelo mesmo motivo: uma escala estimada pelo tamanho do corpo. O centro da caixa funciona bem para câmeras aéreas como esta; em câmeras inclinadas, prefira o loop do centro inferior ou uma homografia.
Acumule um mapa de calor#

Um mapa de calor adiciona calor sob cada caixa rastreada em cada frame, fazendo com que as faixas ou corredores com mais tráfego brilhem mais. Forneça um region e ele também contará objetos, como na rodovia acima:
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 frame com a sobreposição de calor acumuladaAo longo de horas de vídeo, a mesma sobreposição mostra corredores congestionados, zonas não utilizadas e os caminhos que as pessoas realmente percorrem. O monitoramento de treinos e uma interface de inferência ao vivo com Streamlit são construídos com os mesmos componentes.
Armadilhas#
Sintoma: um pipeline de contagem funciona com metade do FPS esperado. Solução: ObjectCounter, Heatmap e SpeedEstimator fazem a detecção e o rastreamento internamente. Não os envolva no seu próprio loop model.track() nem passe r.orig_img; forneça quadros brutos.
Sintoma: pedestres a 1 km/h ou carros a 300 km/h. Solução: meça meter_per_pixel usando um comprimento conhecido no local onde os objetos se movem, confirme o FPS real da câmera e compare a mediana com uma velocidade conhecida (ritmo de caminhada ou limite de velocidade sinalizado).
O centro geométrico sobe à medida que um objeto se aproxima da câmera e sua caixa aumenta. A parte inferior central aproxima o ponto em que o objeto toca o chão, então as distâncias medidas a partir dela acompanham o movimento real com muito mais precisão.
Mostrar solução
Calibra com algo que possas medir no frame e, depois, verifica a plausibilidade:
# 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.0300Coloca pixels_per_meter em speed_loop.py (ou meter_per_pixel em SpeedEstimator) e executa novamente. Se as pessoas ainda aparecerem com velocidades muito diferentes de 4–5 km/h, a escala foi medida à profundidade errada ou o FPS está incorreto: um clipe desacelerado para metade da velocidade reduz todas as leituras pela metade. No clipe de demonstração, ambos os scripts usaram a mesma escala estimada (0,03 m por pixel são 33 pixels por metro) e indicaram velocidades 2–4× inferiores; portanto, ou cada pixel aqui cobre mais terreno do que o estimado ou a taxa real de captura do clipe não é de 30 FPS. Só uma medição na cena permite saber qual das hipóteses está correta.