ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ByteTrack自定义目标跟踪实战:从VOC数据训练到摄像头实时部署

ByteTrack自定义目标跟踪实战:从VOC数据训练到摄像头实时部署 简介面向目标检测与跟踪领域的学生、研究者和开发者这是一套ByteTrack算法从入门到落地的完整教程包。内容以VOC格式数据集为主线讲解如何准备并标注自己的数据、组织目录结构和生成标注文件随后逐步完成模型训练与精度优化并最终将模型部署到摄像头实时视频流中实现稳定检测与多目标跟踪。教程既介绍原理也提供可直接参考的Python和C代码覆盖算法实现、Tracker封装、编译依赖与使用说明等关键环节。包内共251个文件除Python主程序外还包含pyc编译文件、Markdown说明文档、头文件、配置脚本等整体仅1.6MB条理清晰便于按需查阅和实践。同时整理有训练思路与常见排错要点适合对照代码逐步复现整个流程。目前已有523人学习下载对希望快速掌握ByteTrack实战用法的读者有很高参考价值。1. 先把“跟踪”和“检测”的关系摆正ByteTrack 训练的本质是什么跑通 ByteTrack 自带 demo 的那天你会觉得跟踪这件事已经被解决了视频里的人被稳稳框住ID 不乱跳换遮挡再出现还能认回来。可一旦你把摄像头对准自己的场景比如厂区里的叉车、仓库里的货架、小区门口的电动车效果立刻崩盘。原因不复杂ByteTrack 仓库里给的权重是在 MOT17 行人数据集上训练的模型只认识 person 这一类。而 ByteTrack 的核心并不是一个端到端的跟踪网络它由 YOLOX 检测器加一个在线关联后处理组成。训练自己的数据集训练的是里面的 YOLOX 检测器ByteTrack 的跟踪器部分不需要训练只需要在推理时把检测结果喂进去。这篇教程解决的就是这个链路把 VOC 格式的标注数据整理妥当在 ByteTrack 源码里跑通训练再把训好的权重接到摄像头实时画面上做检测和跟踪。适合手里有标注数据、想脱离行人预训练模型的工程师也适合想搞清楚 ByteTrack 各部分怎么串联的读者。理解了这个链路后面换 COCO 格式、换检测器、换跟踪阈值都是同一套方法论。2. VOC 格式的数据怎么喂进 ByteTrack从目录到类别名的对齐2.1 先确认 ByteTrack 训练时到底消费什么格式ByteTrack 仓库里的训练脚本tools/train.py并不直接读 VOC 标注它读取的是 YOLOX 的DataLoader和数据集类。仓库自带了对 COCO 和 MOT 格式的完整支持VOC 的支持来自 YOLOX 的数据集实现yolox/data/datasets/voc.py。因此要想训练 VOC 格式的数据最顺的路径不是去改 ByteTrack 的 MOT 数据管线而是走 YOLOX 的 VOC 数据集类复制一个属于你自己的 exp 配置文件出来改。常见的错误是把 ByteTrack 的exps/example/mot/yolox_x_mot17.py直接拿来改类别数。这条路不是不能走但它要求你把 VOC 标注先转成 MOT 的 json 格式多一道转换还容易在 val_ann 和 test_ann 的路径上踩坑。我一般会直接以exps/example/yolox_voc/yolox_voc_s.py为模板复制出一份my_voc_exp.py改数据目录和类别数训练和推理都用同一个 exp省去格式转换的中间步骤。2.2 准备好 VOC 的标准目录和 ImageSetsVOC 格式的目录结构有三个关键部分JPEGImages放图片Annotations放 XML 标注ImageSets/Main放训练验证集的划分文件。YOLOX 的 VOC 数据集类读取ImageSets/Main下的train.txt、val.txt从里面取出不带后缀的图片名再去Annotations找同名 XML。VOCdevkit/ └── VOC2007/ ├── JPEGImages/ │ ├── 000001.jpg │ └── 000002.jpg ├── Annotations/ │ ├── 000001.xml │ └── 000002.xml └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── trainval.txt划分文件里的每行是一个文件名不带.jpg后缀。如果数据集是用 LabelImg 标注的XML 里通常只有annotation根节点YOLOX 能直接解析。如果是从网上爬的标注包先检查 XML 和图片是不是同名、是否一一对应cd VOCdevkit/VOC2007 ls JPEGImages | wc -l ls Annotations | wc -l comm -3 (ls JPEGImages | sed s/.jpg$// | sort) (ls Annotations | sed s/.xml$// | sort)第三行命令会列出两边对不上的文件名。只要出现输出说明数据集本身残缺直接进训练大概率会在某个 epoch 突然撑爆内存或者FileNotFoundError。2.3 用一段脚本核对 XML 标注与类别表标注里最大的隐藏问题不是坐标错而是类别名不一致。比如名称为forklift、ForkLift、fork_lift的三种写法在 XML 里都会被当作不同类别但训练时只保留一个其余标注会被过滤掉。训练前先跑一段统计脚本import xml.etree.ElementTree as ET import glob from collections import Counter counter Counter() files glob.glob(VOCdevkit/VOC2007/Annotations/*.xml) for f in files: tree ET.parse(f) for obj in tree.findall(object): name obj.findtext(name).strip() counter[name] 1 for name, cnt in counter.most_common(): print(f{name}: {cnt}) print(f\ntotal objects: {sum(counter.values())})这段脚本把所有 XML 里的name标签统计一遍。输出里如果出现大小写不一致的同类目标用 LabelImg 批量改掉后再继续。VOC 数据集的类别名由代码里的列表决定不是由文件系统决定的所以标注里的名字必须训练配置文件里的类别表完全一致差一个空格都算另一类。2.4 在 exp 文件里把数据集和类别接上复制一份yolox_voc_s.py出来最需要改的是三处data_dir指向 VOCdevkit 的上级目录、num_classes改成你自己的类别数、max_epoch按数据量调整。以 3 类目标为例改动后的关键配置如下class Exp(MyExp): def __init__(self): super(Exp, self).__init__() self.num_classes 3 self.data_dir VOCdevkit self.train_ann VOC2007 self.val_ann VOC2007 self.max_epoch 80 self.data_num_workers 4 self.input_size (640, 640)data_dir的值不是VOCdevkit/VOC2007而是VOCdevkit所在的位置。YOLOX 的VOCDetection类内部会拿data_dir拼上train_ann组成真实路径写错一级目录会在训练第一轮就报找不到图像。同时要改yolox/data/datasets/voc.py里的VOC_CLASSES列表。这一步很多人漏掉因为训练能正常开始loss 也在降但推理时类别输出会错位。列表要改成和 XML 里name完全一致的内容。配置项作用注意点data_dirVOC 数据集根目录写到 VOCdevkit 这一层不是 VOC2007num_classes模型输出的类别数必须等于 VOC_CLASSES 列表长度input_size训练时的输入尺寸小目标多就 640目标大可以 800max_epoch最大训练轮数数据量小建议 80数据量大可到 200input_size不建议一上来就调大。检测器在 640 下收敛稳定跟踪器对分辨率不敏感先保证训练能收敛再考虑用小目标增强来提升召回。416 这种小分辨率训练速度快但小目标容易丢摄像头场景里不建议用。3. 跑通训练用 ByteTrack 自带工具初始化自己的模型训练3.1 最小训练命令和它背后发生的四件事确认数据没问题后训练命令本身很简单。在 ByteTrack 源码根目录执行python tools/train.py \ -f exps/example/yolox_voc/my_voc_exp.py \ -d 1 \ -b 8 \ --fp16 \ -o \ -c pretrained_weights/yolox_s.pth命令执行后程序会依次做四件事加载 exp 配置并构建模型用-c指定的预训练权重初始化主干网络启动数据加载器读取 VOC 标注进入 YOLOX 的训练循环。这里的-d 1是 GPU 卡号-b 8是总 batch size--fp16开启混合精度训练-o表示用输出目录里已有的最优权重覆盖当前启动状态日常开发中这个参数加-o不为过省去手动清理缓存的麻烦。yolox_s.pth是 YOLOX 官方开源的 COCO 预训练权重ByteTrack 的检测器结构就是 YOLOX加载这个权重做迁移学习比从零训练少花一半时间。如果你的标注类目和 COCO 的 80 类完全无关比如全是工业零件预训练权重依然有效因为它学到的是通用视觉特征不依赖具体类别。但num_classes变了模型最后的分类头会随机初始化所以训练初期的 loss 下降慢是正常的不用焦虑。3.2 改参数前先看懂这三个配置字段训练跑起来后最常调的是 exp 文件里的base_lr、ema和enable_mixup。这三个字段直接决定训练是否能收敛以及收敛出来的模型会不会在跟踪阶段出现大量误检。self.base_lr 0.01 self.ema True self.enable_mixup Truebase_lr是初始学习率YOLOX 内部会根据 batch size 做线性缩放。搬用默认的0.01在小 batch 下会显得偏大改成0.0025起步更稳。ema保持开启它会维护一组滑动平均权重训练结束后用best_ckpt.pth或last_ckpt.pth推理时实际上用的就是 EMA 之后的参数边框抖动更小跟踪阶段少很多由检测框抖动引发的 ID 切换。enable_mixup在小数据集上建议关掉Mosaic 增强已经够用mixup 会让模型在类别数极少时收敛变慢。参数建议值什么时候要动base_lr0.0025 ~ 0.01数据量少于 2000 张就向下调emaTrue不建议关enable_mixup数据量大于 5000 才开小数据关掉loss 下降更快max_epoch80 ~ 200看验证集 loss 是否仍在下降3.3 断点续训、迁移权重和训练日志实时观察训练中断是常态显卡松动、显存溢出、服务器重启都遇到过。续训用-r参数python tools/train.py \ -f exps/example/yolox_voc/my_voc_exp.py \ -d 1 -b 8 --fp16 \ -r YOLOX_outputs/yolox_voc_s/last_ckpt.pth-r和-c的区别在于-c只加载模型权重优化器状态、学习率调度器都从零开始-r则是把训练状态整个恢复。续训时不要用-c否则学习率重新回到初始值会在训练后期带你绕一个陡坡。训练日志在YOLOX_outputs/yolox_voc_s/下log结尾的文本文件记录了每个 iteration 的 loss。看日志有个实用技巧关注loss数值的同时看 top1 准确率和 avg 5 的曲线。如果 loss 在降但 top1 一直趴在 0.2 附近大概率是类别表错位或者标注里大量目标被过滤了停掉训练回去查 XML 比硬跑完 80 个 epoch 有价值。4. 摄像头实时检测跟踪把模型接到 ByteTrack 的在线关联上4.1 demo_track.py 一行命令接摄像头训练好的权重在YOLOX_outputs/yolox_voc_s/下摄像头推理可以直接用 ByteTrack 自带的tools/demo_track.pypython tools/demo_track.py \ -f exps/example/yolox_voc/my_voc_exp.py \ -c YOLOX_outputs/yolox_voc_s/best_ckpt.pth \ --fp16 \ --fuse \ -v 0-v 0表示使用系统默认摄像头如果有两个摄像头改成-v 1切换。--fuse会把卷积层和 batch norm 层合并模型前向更快--fp16在支持半精度计算的 GPU 上能显著提升帧率。以上两个参数不影响跟踪逻辑可以放心开。如果画面里检测框在跳、ID 老变问题几乎不出在模型上而是出在下一节的三个阈值。4.2 跟踪参数 track_thresh、match_thresh、track_buffer 怎么配合ByteTrack 的跟踪器在yolox/tracker/byte_tracker.py里它本身不学习参数靠的是检测框的置信度分数做两阶段匹配。推理时改动三个参数效果会肉眼可见from yolox.tracker.byte_tracker import BYTETracker args.track_thresh 0.5 # 高置信度检测框阈值 args.track_buffer 30 # 轨迹保留帧数 args.match_thresh 0.8 # 匹配时的 IoU 阈值 tracker BYTETracker(args, frame_rate30)track_thresh决定哪些框进入第一轮匹配低于这个阈值的框被视为低分框进入第二轮和第二轮匹配。它的调校要跟随检测器的 AP 走训练集里目标都被完整框住track_thresh可以提到 0.6如果摄像头角度斜、目标有遮挡降到 0.4 能让召回上来代价是误检增多短 ID 变多。track_buffer控制一个轨迹在丢失目标后存活多少帧。摄像头画面里有反复遮挡的时候比如叉车从货架后面穿过track_buffer调到 60 能保住原 ID但设太高会让已经消失的目标继续占用轨迹资源新出现的目标迟迟拿不到新 ID。match_thresh是第二轮低分匹配的 IoU 下限调高会让低分框更难关联成功减少 ID 切换调低则更容易把同一个目标在不同帧里的碎片接起来。我一般按这个顺序调先固定track_thresh0.5再动match_thresh最后才动track_buffer。参数作用变大的效果变小的效果track_thresh高分框判定线误检少漏检多召回高短 ID 多track_buffer轨迹丢失存活帧遮挡保 IDID 切换更频繁match_thresh低分框关联 IoU关联更严格碎片化减小4.3 自己写一段视频流推理循环demo_track.py够演示用但要接进业务系统通常要自己写读取循环。核心逻辑可以这样拆import cv2 import torch from yolox.exp import get_exp from yolox.utils import postprocess from yolox.tracker.byte_tracker import BYTETracker from yolox.data.data_augment import ValTransform exp get_exp(exps/example/yolox_voc/my_voc_exp.py) model exp.get_model() ckpt torch.load(YOLOX_outputs/yolox_voc_s/best_ckpt.pth, map_locationcpu) model.load_state_dict(ckpt[model]) model.eval() cap cv2.VideoCapture(0) tracker BYTETracker(args, frame_rate30) while cap.isOpened(): _, frame cap.read() # 缩放 归一化输入尺寸必须和训练时一致 inputs, ratio, tarl exp.preprocess(frame, exp.test_size) with torch.no_grad(): outputs model(inputs.unsqueeze(0)) outputs postprocess(outputs, exp.num_classes, 0.01, 0.65) # 转换成 ByteTrack 需要的 [x1, y1, x2, y2, score, class] 格式 online_targets tracker.update(outputs[0], [frame.shape[0], frame.shape[1]], exp.test_size) for t in online_targets: x1, y1, x2, y2 map(int, t.tlwh[:4] 的转换结果) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow(ByteTrack, frame) if cv2.waitKey(1) ord(q): break需要注意tracker.update接收的检测输出是后处理后的[x1, y1, x2, y2, score, class]格式坐标是相对原图的不要在上面的循环里把检测框提前画到图上再传给 tracker那样关联时用的坐标就变成了像素坐标match_thresh会完全失效。5. 验证自训练效果的三个动作落盘回放、类别过滤、阈值对比5.1 把摄像头流落盘逐帧核对轨迹实时画面里判断 ID 切换是否过多靠眼睛盯着屏幕记数是不靠谱的。把推理结果写进视频文件离线回放才能逐帧对比。用cv2.VideoWriter把画好框的帧写盘即可延时可以不管回放时逐帧对比同一个目标在遮挡前后的 ID 是否延续。writer cv2.VideoWriter(track_result.mp4, cv2.VideoWriter_fourcc(*mp4v), 20.0, (frame_width, frame_height)) writer.write(frame)回放时重点看两类位置目标走到画面边缘再返回时 ID 是否变化目标被障碍物完全遮住 2 秒以上再出现时 ID 是否变化。这两类场景是 BYTETracker 在摄像头应用中掉链子的高发区。5.2 只跟踪你需要的类别ByteTrack 自带的demo_track.py默认只输出 person 类别你用自训练模型跑的时候如果 VOC 里有多类目标画面里会出现所有类别的框。想只跟踪其中一类在tracker.update之前过滤检测结果output outputs[0] # 假设目标类别是 2保留分数大于 0.3、类别为 2 的框 keep (output[:, 6] 2) (output[:, 4] 0.3) filtered output[keep]过滤放在 tracker 外部而不是内部原因是 ByteTrack 的关联策略依赖高分框和低分框之间的互补。把低分框提前剃掉跟踪器就退化成纯 SORT遮挡场景的 ID 切换会明显变多。所以类别过滤只做类别维度的筛选分数阈值交给track_thresh。5.3 用两组阈值对比 ID Switch验证调参效果最直接的办法是在同一个固定摄像头画面下跑两组参数分别记录一段时间内的 ID 切换次数。一组用track_thresh0.5, match_thresh0.8另一组用track_thresh0.4, match_thresh0.7。各跑 3 分钟统计画面中新增的 ID 数量。新增 ID 数越少说明跟踪的连续性越好。如果第二组在 ID 连续性上有明显改善但同时出现大量碎框那就是track_thresh放太低了折中到 0.45 再试。# 对比记录 threshold0.5 match0.8 - 新增ID: 7 threshold0.4 match0.7 - 新增ID: 5这个对比表格值得记进你的工程笔记。很多人把模型练好了却觉得跟踪效果不好方向都放在重新训练上实际上检测器能稳定出框的情况下跟踪效果差基本都是阈值没匹配好当前场景。把这两组数据留档以后再换摄像头位置或者换光线环境直接拿出来作为调整基线比你从参数默认值重新摸索要快得多。本文还有配套的精品资源点击获取
返回列表