YOLOv10 多目标追踪注册机制源码解析:trackers/track.py中的回调管线与追踪器初始化
【免费下载链接】yolov10YOLOv10: Real-Time End-to-End Object Detection [NeurIPS 2024]项目地址: https://gitcode.com/GitHub_Trending/yo/yolov10
本篇文章聚焦 YOLOv10 仓库中多目标追踪的核心接线模块 ultralytics/trackers/track.py,深入拆解on_predict_start、on_predict_postprocess_end与register_tracker三个函数如何把 ByteTrack / BoT-SORT 追踪器无缝接入预测流水线。读完本文,你将掌握追踪器何时被创建、检测结果如何被追踪结果替换、追踪 ID 如何跨帧保持,以及如何通过配置文件自定义追踪行为。
一、模块定位:追踪功能在预测流水线中的接线层
在 YOLOv10 中,多目标追踪(MOT)并非独立于推理之外的另一套流程,而是以回调(callback)方式挂载在标准预测管线之上。track.py就是这一接线层的实现:它不自己实现卡尔曼滤波或数据关联,而是负责在合适的时机创建 ByteTrack / BoT-SORT 实例、把每帧检测结果喂给追踪器、再把带 ID 的追踪结果写回Results对象。
从源码结构看,追踪链路涉及以下文件:
- ultralytics/trackers/track.py:回调定义与追踪器注册(本文主角)
- ultralytics/trackers/byte_tracker.py:
BYTETracker与STrack实现 - ultralytics/trackers/bot_sort.py:
BOTSORT与BOTrack实现 - ultralytics/trackers/init.py:对外导出
register_tracker - ultralytics/cfg/trackers/botsort.yaml 与 ultralytics/cfg/trackers/bytetrack.yaml:追踪器参数配置
track.py只暴露 3 个公开函数,模块结构非常精简:
| 函数 | 职责 |
|---|---|
on_predict_start(predictor, persist) | 预测开始前初始化追踪器实例 |
on_predict_postprocess_end(predictor, persist) | 每帧后处理结束后执行追踪更新 |
register_tracker(model, persist) | 把前两个函数注册为模型回调 |
二、TRACKER_MAP:追踪器类型注册表
track.py顶部定义了一张将配置名映射到追踪器类的字典:
from .bot_sort import BOTSORT from .byte_tracker import BYTETracker # A mapping of tracker types to corresponding tracker classes TRACKER_MAP = {"bytetrack": BYTETracker, "botsort": BOTSORT}这张表决定了tracker_type配置项可用的取值范围。从 ultralytics/trackers/byte_tracker.py 与 ultralytics/trackers/bot_sort.py 的类定义可以看到两者差异:
- BYTETracker:基于卡尔曼滤波(
KalmanFilterXYAH,即中心点 x/y + 宽高比 + 高度)的两阶段关联算法,通过track_high_thresh/track_low_thresh区分高、低置信度检测框,低置信度框用于二次关联找回遮挡目标; - BOTSORT:继承自
BYTETracker的扩展版本,改用KalmanFilterXYWH,额外引入 GMC(Global Motion Compensation,全局运动补偿,默认sparseOptFlow)与可选的 ReID 外观特征(with_reid),在相机运动场景下更鲁棒。
__init__.py中还对外导出了BOTSORT与BYTETracker,允许开发者直接 import 这两个追踪器类做更底层的调用。
三、on_predict_start:预测开始时初始化追踪器
def on_predict_start(predictor: object, persist: bool = False) -> None: if hasattr(predictor, "trackers") and persist: return tracker = check_yaml(predictor.args.tracker) cfg = IterableSimpleNamespace(**yaml_load(tracker)) if cfg.tracker_type not in ["bytetrack", "botsort"]: raise AssertionError(f"Only 'bytetrack' and 'botsort' are supported for now, but got '{cfg.tracker_type}'") trackers = [] for _ in range(predictor.dataset.bs): tracker = TRACKER_MAPcfg.tracker_type trackers.append(tracker) if predictor.dataset.mode != "stream": # only need one tracker for other modes. break predictor.trackers = trackers predictor.vid_path = [None] * predictor.dataset.bs # for determining when to reset tracker on new video该函数执行三个关键动作:
- 幂等检查:若
persist=True且 predictor 已有trackers属性,直接返回,避免重复创建追踪器; - 配置装载:通过
check_yaml解析predictor.args.tracker(默认botsort.yaml,见 ultralytics/cfg/default.yaml),再经yaml_load载入并用IterableSimpleNamespace包装,使cfg.tracker_type等字段可以点号访问。如果tracker_type不是bytetrack或botsort,会抛出AssertionError; - 按 batch 大小创建追踪器:
for _ in range(predictor.dataset.bs)循环按批次大小实例化追踪器,frame_rate固定传 30。stream 模式的特殊处理是这里的重点——只有在流式输入(dataset.mode == "stream")下才为 batch 中每个视频各建一个追踪器,其余模式只创建 1 个。
初始化完成后,函数还在 predictor 上挂载vid_path = [None] * bs,用于后续判断是否切换到了新视频文件,从而决定何时重置追踪器状态。
四、on_predict_postprocess_end:每帧后处理结束时的追踪更新
def on_predict_postprocess_end(predictor: object, persist: bool = False) -> None: path, im0s = predictor.batch[:2] is_obb = predictor.args.task == "obb" is_stream = predictor.dataset.mode == "stream" for i in range(len(im0s)): tracker = predictor.trackers[i if is_stream else 0] vid_path = predictor.save_dir / Path(path[i]).name if not persist and predictor.vid_path[i if is_stream else 0] != vid_path: tracker.reset() predictor.vid_path[i if is_stream else 0] = vid_path det = (predictor.results[i].obb if is_obb else predictor.results[i].boxes).cpu().numpy() if len(det) == 0: continue tracks = tracker.update(det, im0s[i]) if len(tracks) == 0: continue idx = tracks[:, -1].astype(int) predictor.results[i] = predictor.results[i][idx] update_args = dict() update_args["obb" if is_obb else "boxes"] = torch.as_tensor(tracks[:, :-1]) predictor.results[i].update(**update_args)这是追踪管线的核心数据通路,逻辑可拆解为五步:
- 视频切换重置:
vid_path用于检测当前帧是否属于新视频。当persist=False且视频路径发生变化时,调用tracker.reset()清空上一视频的追踪状态(reset()方法在 ultralytics/trackers/byte_tracker.py 中定义,会重置tracked_stracks、lost_stracks、frame_id与卡尔曼滤波器); - 提取检测结果:根据任务类型(
obb或普通检测)取出results[i].obb或results[i].boxes转为 NumPy 数组。这意味着追踪不仅支持 detect 任务,也支持旋转框 OBB 任务; - 调用追踪器:
tracker.update(det, im0s[i])把原始图像与检测框交给 ByteTrack / BoT-SORT,内部完成卡尔曼预测、IoU 关联、轨迹状态迁移,返回形如[x1, y1, x2, y2, id, conf, cls, idx](OBB 场景为[x, y, w, h, angle, id, conf, cls, idx])的轨迹数组; - 按追踪索引重排结果:
tracks[:, -1]是检测索引,predictor.results[i][idx]把原始检测结果按被追踪保留的检测框重新索引,保证后续可视化时框与轨迹一一对应; - 写回追踪坐标:去掉索引列后,把带 ID 的轨迹通过
results.update(...)(定义于 ultralytics/engine/results.py)替换原有 boxes/OBB 数据。之后用户即可通过results[i].boxes.id读取每个目标的追踪 ID。
五、register_tracker:回调注册的入口
def register_tracker(model: object, persist: bool) -> None: model.add_callback("on_predict_start", partial(on_predict_start, persist=persist)) model.add_callback("on_predict_postprocess_end", partial(on_predict_postprocess_end, persist=persist))函数本身极简,但意义重大:它用functools.partial把persist参数固化进两个回调函数,再注册到模型的两个预测阶段钩子上。实际触发点在 ultralytics/engine/predictor.py:
- 第 237 行:
self.run_callbacks("on_predict_start")—— 数据遍历开始前执行; - 第 256 行:
self.run_callbacks("on_predict_postprocess_end")—— 每帧postprocess完成之后执行。
因此整个追踪生命周期可以概括为:
model.track(source, persist=...) └─> model.track() 首次调用时检测到 predictor 无 trackers └─> register_tracker(self, persist) 注册两个回调 └─> predictor 进入预测循环 ├─> on_predict_start → 解析 tracker yaml,实例化追踪器 └─> 每帧: inference → postprocess └─> on_predict_postprocess_end → tracker.update() → 写回结果六、persist参数与model.track()的关联
persist是理解追踪行为的关键开关,它有两个作用点:
- 在
on_predict_start中避免重复初始化(hasattr(predictor, "trackers") and persist); - 在
on_predict_postprocess_end中决定是否在新视频上重置追踪器。
入口位于 ultralytics/engine/model.py 的track()方法:
if not hasattr(self.predictor, "trackers"): from ultralytics.trackers import register_tracker register_tracker(self, persist) kwargs["conf"] = kwargs.get("conf") or 0.1 # ByteTrack-based method needs low confidence predictions as input kwargs["batch"] = kwargs.get("batch") or 1 # batch-size 1 for tracking in videos kwargs["mode"] = "track" return self.predict(source=source, stream=stream, **kwargs)值得注意的是track()还强制了两条追踪前置条件:置信度阈值默认降为 0.1(ByteTrack 系算法依赖低置信度框做二次关联,太高会丢失遮挡目标的轨迹),以及batch 强制为 1(视频追踪按帧顺序处理,且on_predict_start中按dataset.bs创建追踪器)。
典型用法:
from ultralytics import YOLO model = YOLO("yolov8n.pt") # 视频文件:非 persist 场景,切换视频自动重置追踪器 results = model.track(source="video.mp4", show=True) # 逐帧循环:persist=True 告诉追踪器当前帧是上一帧的延续 cap = cv2.VideoCapture("video.mp4") while cap.isOpened(): success, frame = cap.read() if success: results = model.track(frame, persist=True) annotated = results[0].plot()CLI 等价命令为:
yolo track model=yolov8n.pt source=video.mp4 yolo track model=yolov8n.pt source=video.mp4 tracker=bytetrack.yaml conf=0.3七、追踪器配置参数详解
tracker_type决定实例化哪个类,其余参数在on_predict_start中被装载进cfg并传给追踪器构造函数。仓库内置两份配置文件:
ultralytics/cfg/trackers/bytetrack.yaml:
tracker_type: bytetrack # tracker type, ['botsort', 'bytetrack'] track_high_thresh: 0.5 # threshold for the first association track_low_thresh: 0.1 # threshold for the second association new_track_thresh: 0.6 # threshold for init new track if the detection does not match any tracks track_buffer: 30 # buffer to calculate the time when to remove tracks match_thresh: 0.8 # threshold for matching tracksultralytics/cfg/trackers/botsort.yaml 在此基础上追加 BoT-SORT 专有项:
tracker_type: botsort track_high_thresh: 0.5 track_low_thresh: 0.1 new_track_thresh: 0.6 track_buffer: 30 match_thresh: 0.8 # BoT-SORT settings gmc_method: sparseOptFlow # method of global motion compensation proximity_thresh: 0.5 appearance_thresh: 0.25 with_reid: False各参数在源码中的实际作用:
| 参数 | 默认值 | 源码消费位置 | 含义 |
|---|---|---|---|
track_high_thresh | 0.5 | byte_tracker.update() | 第一轮关联的高分检测框阈值 |
track_low_thresh | 0.1 | byte_tracker.update() | 第二轮关联的低分检测框阈值 |
new_track_thresh | 0.6 | byte_tracker.update() | 未匹配检测框新建轨迹的最低分数 |
track_buffer | 30 | BYTETracker.__init__ | 经int(frame_rate/30*args.track_buffer)换算为max_time_lost,超过该帧数未匹配的 lost 轨迹将被移除 |
match_thresh | 0.8 | 两轮linear_assignment | 关联距离阈值 |
gmc_method | sparseOptFlow | BOTSORT.__init__中GMC(method=...) | 全局运动补偿方法(BoT-SORT) |
proximity_thresh | 0.5 | BOTSORT.get_dists | 空间邻近度阈值(IoU) |
appearance_thresh | 0.25 | BOTSORT.get_dists | ReID 外观相似度阈值 |
with_reid | False | BOTSORT.__init__/get_dists | 是否启用 ReID 外观特征(当前encoder仍为 None,即尚未实际启用) |
自定义追踪器只需复制上述任一 yaml 并修改参数(tracker_type除外),通过tracker=custom_tracker.yaml传入即可。
八、底层原理:BYTETracker 的一轮更新
tracker.update(det, im0s[i])内部(ultralytics/trackers/byte_tracker.py 的BYTETracker.update)执行的正是 ByteTrack 论文中的经典五步流程:
- 按
track_high_thresh划分高置信度检测框,按[low, high)区间收集低置信度检测框; - 第一轮关联:联合
tracked_stracks与lost_stracks,用共享卡尔曼滤波器multi_predict预测新位置,以 IoU + 分数融合距离(fuse_score)做线性分配; - 第二轮关联:对未匹配轨迹用低置信度检测框(
iou_distance+ 阈值 0.5)二次匹配,找回被遮挡的物体; - 新轨迹激活:仍未匹配且分数达
new_track_thresh的检测框,通过track.activate(kalman_filter, frame_id)建立新轨迹(STrack.activate会分配新的全局track_id); - 状态清理:超过
max_time_lost的 lost 轨迹被mark_removed,重复轨迹按 IoU 距离与存活时长去重,removed_stracks列表裁剪至 1000 以内。
BoT-SORT 的差异在于:卡尔曼状态量是xywh而非xyah,update前先用 GMC 估计单应矩阵并通过STrack.multi_gmc校正全局运动,get_dists中在 IoU 基础上叠加(尚未启用的)ReID 外观距离。
九、验证与测试
仓库测试 tests/test_engine.py 覆盖了回调机制本身:通过pred.add_callback("on_predict_start", test_func)注册自定义回调,再断言test_func in pred.callbacks["on_predict_start"],验证了run_callbacks/add_callback框架的正确性——register_tracker使用的正是同一套回调 API。这从侧面印证了track.py三个函数的定位:它们不是特殊逻辑,而是标准预测回调机制的两个挂载点加上一个注册入口。
十、小结
ultralytics/trackers/track.py是 YOLOv10 多目标追踪的"神经中枢":
register_tracker用两行代码把追踪能力注入模型预测管线;on_predict_start按 batch 与 stream 模式合理实例化追踪器,并强制校验tracker_type;on_predict_postprocess_end完成"检测 → 关联 → 写回"的每帧闭环,支持 detect 与 OBB 两类任务;persist与vid_path共同管理跨视频、跨帧的追踪状态生命周期。
理解这个模块后,无论是更换追踪算法、自定义阈值参数,还是基于回调机制扩展新的追踪策略,你都能在源码层面找到清晰的落点。
【免费下载链接】yolov10YOLOv10: Real-Time End-to-End Object Detection [NeurIPS 2024]项目地址: https://gitcode.com/GitHub_Trending/yo/yolov10
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考