使用 ByteTrack、BoT-SORT 和 TrackTrack 跟踪对象
为每个对象在不同帧中分配稳定的 ID,选择跟踪器,并根据检测器调整其阈值。
每个计数、停留时间和速度估算都取决于一个问题:这一帧中的 ID 17,是否与上一帧中的 ID 17 是同一个对象?对于同一段包含约 30 名行人的 62 帧视频片段,使用同一个检测器时,ByteTrack 找到了 32 个 ID,而默认的 TrackTrack 找到了 9 个。选择和调整跟踪器是模型工作的一部分;如果选错,后续的每个数字都会在不知不觉中出错。
实践操作
跟踪器的作用#

这一帧中的标签和轨迹线就是跟踪器生成的结果。每个 ID 都是一个跟踪目标,表示对某个真实对象的假设;轨迹线则记录了该目标最近的运动历史。每一帧中,跟踪器都会通过卡尔曼滤波器预测每个跟踪目标的位置,然后根据框的重叠程度(以及可选的外观特征)将新检测结果与这些预测相匹配:
未匹配的跟踪目标在 track_buffer 帧后会被删除。整个机制就是如此简单,而阈值决定了之后的一切。
选择跟踪器#
跟踪模式内置六种跟踪器:BoT-SORT、ByteTrack、OC-SORT、Deep OC-SORT、FastTracker 和 TrackTrack(未传入 tracker 时的默认选项)。其中三种可以满足大多数需求:
| 跟踪器 | 匹配依据(默认配置) | 适用场景 |
|---|---|---|
| ByteTrack | 运动和框重叠程度,以及用于挽回低置信度框的第二轮匹配 | 固定摄像头、小型或不确定性较高的检测器,以及注重吞吐量的场景 |
| BoT-SORT | 类似 ByteTrack 的匹配方式,并加入摄像头运动补偿(gmc_method: sparseOptFlow) | 摄像头移动或抖动的场景;人群密集且遮挡时间较长时,设置 with_reid: True |
| TrackTrack | 多线索代价(框形状、置信度、角度)、考虑跟踪状态的初始化、摄像头运动补偿 | 由强大且训练良好的模型产生的高置信度检测结果 |
默认情况下,BoT-SORT 的外观匹配(重识别)处于关闭状态;设置 model: auto 后,它会复用检测器自身的特征,因此启用它只会带来很小的额外开销。
在同一段视频片段上比较跟踪器#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
for tracker in ("bytetrack.yaml", "botsort.yaml", "tracktrack.yaml"):
ids, frames = set(), 0
for r in model.track(video, stream=True, tracker=tracker, classes=[0], verbose=False):
frames += 1
if r.boxes.is_track:
ids.update(r.boxes.id.int().tolist())
print(f"{tracker:16s} {frames} frames, {len(ids)} unique person IDs")bytetrack.yaml 62 frames, 32 unique person IDs
botsort.yaml 62 frames, 33 unique person IDs
tracktrack.yaml 62 frames, 9 unique person IDs这段视频从上方拍摄,画面中约有 30 人穿过广场,因此 ByteTrack 和 BoT-SORT 的结果接近实际人数。TrackTrack 并没有出错:它只会在置信度高于 new_track_thresh: 0.7 时启动跟踪,并且只会在置信度高于 track_high_thresh: 0.6 时首次进行匹配;而 YOLO26n 在这段视频中对行人检测的置信度中位数为 0.53,只有 17% 的检测结果达到 0.7。大多数人都没有获得 ID。决策原则:在相信任何计数结果之前,先将跟踪器阈值与检测器的置信度分布进行比较。
在流水线中使用 ID#
from collections import defaultdict
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
counts = defaultdict(set)
for r in model.track(video, stream=True, tracker="bytetrack.yaml", verbose=False):
if not r.boxes.is_track: # no confirmed tracks in this frame yet
continue
for cls, tid in zip(r.boxes.cls.int().tolist(), r.boxes.id.int().tolist()):
counts[r.names[cls]].add(tid)
for cls, ids in counts.items():
print(f"{cls}: {len(ids)} unique objects")person: 32 unique objects每个类别使用一个集合来计数,每个 ID 只计一次,不管它出现了多少帧,因此结果统计的是对象而不是检测结果。stream=True 会逐帧返回结果,而不是将整段视频载入内存。区域计数、热力图和距离计算都是在这个循环之上构建的轻量功能。
跟踪你自己读取的帧#
传入路径或 URL 时,跟踪器状态会自动处理。如果帧来自你自己的解码器、网络摄像头或 RTSP 摄像头,请传入 persist=True,让每次调用都延续同一序列(预测模式文档列出了所有来源类型):
import cv2
from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download
model = YOLO("yolo26n.pt")
video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video)) # or 0 for a webcam, or an rtsp:// URL
seen = set()
while True:
ok, frame = cap.read()
if not ok:
break
r = model.track(frame, persist=True, tracker="bytetrack.yaml", classes=[0], verbose=False)[0]
if r.boxes.is_track:
seen.update(r.boxes.id.int().tolist()) # your logic: dwell time, trajectories, alerts
cap.release()
print(f"{len(seen)} unique IDs")32 unique IDs结果与传入路径时相同,因为 persist=True 在全部 62 次调用中一直保持同一个跟踪器运行。
根据检测器调整跟踪器#
复制一份随包提供的配置,调低不适合你的检测器的阈值,然后传入新文件:
from pathlib import Path
from ultralytics import YOLO
from ultralytics.utils import ROOT
cfg = (ROOT / "cfg/trackers/tracktrack.yaml").read_text() # packaged defaults
for old, new in (
("track_high_thresh: 0.6", "track_high_thresh: 0.3"),
("track_low_thresh: 0.25", "track_low_thresh: 0.1"),
("new_track_thresh: 0.7", "new_track_thresh: 0.35"),
):
cfg = cfg.replace(old, new)
Path("tracktrack_tuned.yaml").write_text(cfg)
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
for tracker in ("tracktrack.yaml", "tracktrack_tuned.yaml"):
ids = set()
for r in model.track(video, stream=True, tracker=tracker, classes=[0], verbose=False):
if r.boxes.is_track:
ids.update(r.boxes.id.int().tolist())
print(f"{tracker:22s} {len(ids)} unique person IDs")tracktrack.yaml 9 unique person IDs
tracktrack_tuned.yaml 27 unique person IDs将阈值设置在略低于检测器置信度中位数的位置后,TrackTrack 的 ID 数量从 9 个升至 27 个,接近人工计数结果。所有跟踪器共有的可调参数:
| 设置项 | ByteTrack 默认值 | 调高后的效果 | 调低后的效果 |
|---|---|---|---|
track_high_thresh | 0.25 | 在首次匹配中忽略较弱的检测结果 | 跟踪低置信度检测器的结果 |
new_track_thresh | 0.25 | 减少短暂出现的误检跟踪目标 | 为模糊对象启动跟踪 |
track_buffer | 30 帧 | 应对更长时间的遮挡 | 更快释放已离开对象的 ID |
match_thresh | 0.8 | 放宽匹配条件(快速运动、框位置跳动) | 收紧匹配条件(人群密集) |
常见问题#
症状:ID 数量远少于目标数量,小模型上尤其明显。解决方法:打印检测置信度的中位数(见解决方案),并将 new_track_thresh 和 track_high_thresh 设在该值略低的位置。
症状:你自己调用 model.track(frame) 时,ID 每帧都会重置;或者两个摄像头输入同一个模型时,ID 会在目标之间跳转。解决方法:为同一视频流的连续帧传入 persist=True,并为每个摄像头创建独立的模型实例(或使用 .streams 数据源,它会为每个流保留一个跟踪器)。
症状:停着的汽车每隔几帧就会更换 ID。解决方法:跟踪器正在创建新轨迹,而不是匹配旧轨迹。先检查检测器对该目标的置信度,然后提高 match_thresh 和 track_buffer。
显示解答
先测量检测器的置信度,再将阈值设在略低于该置信度的位置:
import torch
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
conf = torch.cat([r.boxes.conf for r in model.predict(video, stream=True, classes=[0], verbose=False)])
print(f"median {conf.median():.2f}, share >= 0.7: {(conf >= 0.7).float().mean():.0%}")median 0.53, share >= 0.7: 17%广场视频片段的表格数据为:ByteTrack 32、BoT-SORT 33、TrackTrack 9、调整后的 TrackTrack 27、人工计数约 30。只有 17% 的检测结果达到 TrackTrack 的 new_track_thresh: 0.7 阈值,因此将其降至 0.35(并设置 track_high_thresh: 0.3)是正确的修复方案;对于这台固定的俯视摄像头,直接使用 ByteTrack 更简单。