计数、热力图和速度估算
基于跟踪构建越线计数器、区域占用统计、热力图和速度估算,并如实校准这些结果。
大多数生产级视觉系统并不只是 model.predict(),而是跟踪器加上一点几何计算:对象穿过的一条线、对象所在的一个区域,以及将像素转换为米的比例尺。结果出错往往是因为几何计算:本课演示视频中的速度估算器将步行行人的速度算成 1 km/h,只有实际速度的四分之一,原因只是猜错了一个数字。
实践操作
跟踪加几何计算#
ultralytics.solutions 中的每个解决方案类都会在内部运行跟踪模式,然后对跟踪历史应用自己的几何计算。使用 pip install "ultralytics[solutions]" 一次性安装额外依赖。
| 类别 | 几何形状 | 主要结果 |
|---|---|---|
ObjectCounter | 2 个点 = 线,3 个或更多点 = 多边形 | in_count, out_count, classwise_count |
RegionCounter | 一个或多个带名称的多边形 | region_counts |
Heatmap | 随时间累积框的面积 | plot_im(提供区域时也会计数) |
SpeedEstimator | 跟踪目标位移 × meter_per_pixel | speed_dict(每个跟踪 ID 的 km/h) |
解决方案默认使用 tracker="tracktrack.yaml";请传入你在上一课中验证过的跟踪器。
统计越线次数#

计数线是一条线段。对象的框中心在两帧之间穿过计数线时,只会计数一次;对象来自哪一侧决定了它被计为 IN 还是 OUT。这就是上方传送带场景需要跟踪的原因:没有 ID 的话,箱子 #015 每次接触计数线都会被重复计数。
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}}视频片段的两秒内有四个人穿过广场中央,两个方向各两人。使用默认的 TrackTrack 配置时,同一脚本报告 in=0 out=1,因为跟踪目标更少,越线次数也更少。打开 counted.mp4,逐一目视检查每次越线,再相信计数结果。

在道路上,同一条计数线可按行驶方向统计交通流:向上穿过计数线的车辆计为 IN,向下穿过的车辆计为 OUT。将计数线放在对象能以直角干净穿过的位置,并远离画面边缘、遮挡物,以及框尺寸很小的道路远端。
统计区域内的对象数量#
多边形区域可以回答“当前有多少对象在里面?”区域计数指南提供了一个现成的 RegionCounter;其底层原理是对每个跟踪框进行一次点在多边形内测试:
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] 是每个框的中心。对于队列和停留时间,需要为每个 ID 记录进入区域的时间戳。
估算速度#

上面的每个标签都来自车辆后方的轨迹:跟踪目标在已知帧数内移动的像素距离。速度 = 像素距离 × 每像素对应的米数 ÷ 经过时间。两个输入决定结果是否准确:
- 每像素对应的米数。在对象移动所在的深度处,测量场景中的已知长度(地砖、车道线、停车位)。对于倾斜拍摄的摄像头,则使用四点单应性映射地平面。
- 真实采集帧率。速度会随 FPS 成比例变化;重新编码或减速后的录像中,所有速度都会按相同比例变化。
这个循环测量每个框底部中心的位置(即物体与地面接触的位置),并对每条轨迹的 10 帧数据取平均:
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人的步行速度是 4–5 km/h,因此 2.1 km/h 说明这个示例场景的比例尺不对:这里没有测量 pixels_per_meter 值。这个合理性检查——将中位数与已知速度进行比较——才是关键的习惯。
solutions.SpeedEstimator 将同样的思路封装起来用于生产环境:它测量每条轨迹在 max_hist 帧内的框中心位置,通过 meter_per_pixel 换算,并为每个 ID 固定一个速度(上限为 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又差了大约 4 倍,原因相同:根据人体大小猜测比例尺。对于这样的俯视摄像头,框中心没问题;对于倾斜摄像头,最好使用底部中心循环或单应性变换。
累积热力图#

热力图会在每一帧的每个跟踪框下方增加热度,因此车流量最大的车道或过道会最亮。为它提供一个 region,它还会统计物体数量,如上方高速公路示例所示:
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) # 最后一帧及累积的热力叠加层在数小时的录像中,同一个叠加层会显示拥堵的过道、未使用的区域以及人们实际行走的路径。锻炼监测和 Streamlit 实时推理界面都由相同的组件构建。
常见问题#
症状:计数管线的运行速度只有预期 FPS 的一半。解决方法:ObjectCounter、Heatmap 和 SpeedEstimator 会在内部完成检测和跟踪。不要在自己的 model.track() 循环中再次调用它们并传入 r.orig_img;直接传入原始帧。
症状:行人速度达到 1 km/h,或汽车速度达到 300 km/h。解决方法:根据目标经过的已知距离测量 meter_per_pixel,确认摄像头的实际 FPS,并将中位数与已知速度(步行速度或限速)进行核对。
目标接近摄像头、边界框变大时,几何中心会随之上移。底部中心点近似代表目标与地面接触的位置,因此根据该点测得的距离能更准确地反映实际移动情况。
显示解答
根据画面中可测量的物体进行校准,然后进行合理性检查:
# 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将 pixels_per_meter 填入 speed_loop.py(或将 meter_per_pixel 填入 SpeedEstimator),然后重新运行。如果行人的读数仍与 4–5 km/h 相差很大,说明比例尺是在错误的深度位置测量的,或者 FPS 不正确:以半速播放的片段会让所有读数减半。在演示片段中,两个脚本都使用了相同的猜测比例尺(每像素 0.03 m,即每米 33 像素),读数都低了 2–4 倍;这说明此处一个像素覆盖的实际距离可能比猜测值更大,或者片段的实际采集帧率并非 30 FPS。只有在场景中进行测量,才能确定究竟是哪种情况。