Ultralytics YOLO27: 加入候补名单。
Get Started
用于生产环境的 Ultralytics YOLO·跟踪并构建流水线·课程 6/9
课程中级

计数、热力图和速度估算

基于跟踪构建越线计数器、区域占用统计、热力图和速度估算,并如实校准这些结果。

大多数生产级视觉系统并不只是 model.predict(),而是跟踪器加上一点几何计算:对象穿过的一条线、对象所在的一个区域,以及将像素转换为米的比例尺。结果出错往往是因为几何计算:本课演示视频中的速度估算器将步行行人的速度算成 1 km/h,只有实际速度的四分之一,原因只是猜错了一个数字。

学习成果

在真实视频上构建越线计数器、区域计数、热力图和速度估算,并使用已知参考值校准速度。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 越线计数:solutions.ObjectCounter(region=[p1, p2]);跟踪目标的中心穿过线时,IN 或 OUT 计数会增加。

  2. 区域占用:对每个跟踪框进行一次点在多边形内测试,即可得知当前有哪些对象位于区域内。

  3. 热力图:solutions.Heatmap 会累积跟踪对象停留的位置。

  4. 速度:像素位移 × 每像素对应的米数 ÷ 时间。测量场景中的比例尺,并确认实际 FPS。

实践操作

跟踪加几何计算#

ultralytics.solutions 中的每个解决方案类都会在内部运行跟踪模式,然后对跟踪历史应用自己的几何计算。使用 pip install "ultralytics[solutions]" 一次性安装额外依赖。

类别几何形状主要结果
ObjectCounter2 个点 = 线,3 个或更多点 = 多边形in_count, out_count, classwise_count
RegionCounter一个或多个带名称的多边形region_counts
Heatmap随时间累积框的面积plot_im(提供区域时也会计数)
SpeedEstimator跟踪目标位移 × meter_per_pixelspeed_dict(每个跟踪 ID 的 km/h)

解决方案默认使用 tracker="tracktrack.yaml";请传入你在上一课中验证过的跟踪器。

统计越线次数#

Ultralytics YOLO 统计仓库传送带上的包裹:每个箱子都有一个跟踪 ID(#013 至 #018),包裹经过青色计数线时,IN 计数器显示 12

计数线是一条线段。对象的框中心在两帧之间穿过计数线时,只会计数一次;对象来自哪一侧决定了它被计为 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,逐一目视检查每次越线,再相信计数结果。

Ultralytics YOLO 在分隔道路上进行越线计数:被跟踪的汽车和卡车穿过青色计数线,箭头标示行驶方向,车辆计数为 IN 24、OUT 18

在道路上,同一条计数线可按行驶方向统计交通流:向上穿过计数线的车辆计为 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 zone

xywh[:, :2] 是每个框的中心。对于队列和停留时间,需要为每个 ID 记录进入区域的时间戳。

估算速度#

高速公路上的速度估算:每辆被跟踪的汽车和卡车都标注了估算速度(km/h,48 至 65),并带有一小段运动轨迹线

上面的每个标签都来自车辆后方的轨迹:跟踪目标在已知帧数内移动的像素距离。速度 = 像素距离 × 每像素对应的米数 ÷ 经过时间。两个输入决定结果是否准确:

  1. 每像素对应的米数。在对象移动所在的深度处,测量场景中的已知长度(地砖、车道线、停车位)。对于倾斜拍摄的摄像头,则使用四点单应性映射地平面。
  2. 真实采集帧率。速度会随 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 倍,原因相同:根据人体大小猜测比例尺。对于这样的俯视摄像头,框中心没问题;对于倾斜摄像头,最好使用底部中心循环或单应性变换。

累积热力图#

Ultralytics YOLO 在城市高速公路上的热力图:蓝到红的叠加层在车流最繁忙的车道上最亮,画有汽车和卡车的框,并显示正在累积的计数:12 辆汽车和 2 辆卡车

热力图会在每一帧的每个跟踪框下方增加热度,因此车流量最大的车道或过道会最亮。为它提供一个 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,并将中位数与已知速度(步行速度或限速)进行核对。

对于倾斜视角的摄像头,使用边界框底部中心点

目标接近摄像头、边界框变大时,几何中心会随之上移。底部中心点近似代表目标与地面接触的位置,因此根据该点测得的距离能更准确地反映实际移动情况。

试试看

使用来自你所在领域的视频(或演示片段),构建线计数器,并用肉眼检查 counted.mp4 中的每次穿越。然后测量场景中的一个实际长度,使用解决方案脚本计算比例尺,重新运行 speed_loop.py,并将速度中位数与已知参考值进行比较。提交计数、比例尺以及校准前后的速度中位数。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你的线计数器统计的 IN 和 OUT 与对同一片段进行的人工计数一致。

  • 你的速度估算使用了在场景中测得的比例尺,其中位数与已知参考速度相差不超过 25%。

  • 你能解释为什么在倾斜摄像头下,应使用框的底部中心点来计算速度。

显示解答

根据画面中可测量的物体进行校准,然后进行合理性检查:

# 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。只有在场景中进行测量,才能确定究竟是哪种情况。

接下来做什么

一个模型处理一个视频流是可行的。下一步:并行处理多个视频流,并遵循相应的线程安全推理规范。

登录以保存学习进度