ByteTrack 轨迹算法改进点
目标:只改进轨迹算法(bytetrack/ 核心代码),检测器不是目标。
测试视频与场景对应:dense.mp4(密集交叉)、turn.mp4(转弯减速)。
基线测试记录(源码,未加任何改进)
| 场景 | 视频源 | 基线输出目录 | 结果文件 | 人工标注 |
|---|---|---|---|---|
| 密集交叉 | video/dense.mp4 | test/out/bytetrk_yolov8s_dense_0915_114254/ | dense.txt | dense_anno.txt |
| 转弯减速 | video/turn.mp4 | test/out/bytetrk_yolov8s_turn_0916_140556/ | turn.txt | turn_anno.txt |
基线测试命令(检测器均为 yolov8s.pt,跟踪参数均为源码默认值;后续每个改进的对比结果与此基线对照):
bash
# 密集交叉基线
python ./test/test_bytetrack.py --model model/yolov8s.pt --video video/dense.mp4 --conf 0.25 --classes 2 --track-thresh 0.3 --new-track-thresh 0.35
# 转弯减速基线
python ./test/test_bytetrack.py --model model/yolov8s.pt --video video/turn.mp4 --conf 0.25 --classes 2 --track-thresh 0.3 --new-track-thresh 0.35
人工标注记录规则
人工标注 = 人眼观察视频,为每个物理目标记录完整的 ID 生命时间线。辅助工具:test/frame_viewer.py(看片器:←→ 单帧、↑↓ 大步进、空格 播放、g 跳帧、e 设步长、q 退出)。
- 文件位置与命名 :与被标注的运行结果放在同一目录,命名
<场景>_anno.txt(如dense_anno.txt)。看哪次运行就标哪次,不同运行的 ID 完全不同,不能混用 - 文件头 :首行注明视频 + 结果文件,如
dense.mp4 + dense.txt - 格式 :一行一个目标,用
|分列:- 第 1 列:目标编号(目标1、目标2...按出现顺序编,同一物理目标全程沿用同一编号)
- 后续每列 :
起帧-止帧:内容,内容四种- 数字 = 该段的轨迹 ID,如
46-51:27 消失= 无可见遮挡但轨迹断档遮挡= 被其他目标挡住看不见出画= 目标离开画面(一般作为时间线的结尾段)
- 数字 = 该段的轨迹 ID,如
- 备注列 (可选,放最后):
备注:xxx,如备注:237-240同时出现53和58
- 记录范围:只记发生过 ID 变化的目标;全程一个 ID 的目标不用记
- 同一 ID 贴到不同目标(幽灵轨迹现象):两个目标各自如实记录,冲突由分析端识别,标注者不用合并
- 只记看得懂的:拿不准的事件不记、原因拿不准宁可不填
示例(真实记录节选):
dense.mp4 + dense.txt
目标1|46-51:27|52-60:消失|61-64:31|65-394:出画|
目标2|222-236:53|237-241:58|242-270:消失|271-394:68|备注:237-240同时出现53和58
目标8|59-216:30|217-217:消失|218-227:30|228-235:消失|236-251:30|252-252:消失|253-253:30|254-256:消失|257-259:30|260-282:4|283-293:消失|294-308:4|309-394:出画|
改进一:双轨迹抑制(dense.mp4测试结果分析来源)
当前现象:同一目标同时存在两条轨迹------检测器对同一辆车输出两个框且都被建档,多出的轨迹只存活 1~5 帧。本改进目的:在跟踪端利用时序信息抑制这类双框轨迹,净化输出。
1. 源码问题
检测器(YOLO + NMS iou=0.7)偶尔对同一辆车输出两个框:两框 IoU 落在 0.15~0.7 区间时 NMS 不抑制(阈值 0.7 才抑),ByteTrack 的 remove_duplicate_stracks 也不管(只清 IoU > 0.85)。两个框都被采纳:一个框匹配老轨迹,另一个框(分数 ≥ new_track_thresh)在 Step 4 建档成新轨迹。
人工标注实证(dense_anno.txt,共 5 处):
- 目标2:
54[222-236] → 59[237-241] → ...------轨迹59 存活 5 帧后消失(备注:237-239 54和59同时存在) - 目标2:
69[271-392] → 93[393] → 69[394]------轨迹93 只活 1 帧,69 继续(备注:393 69和93同时存在) - 目标9:
40[135-173] → 45[174] → 40[175-302]------轨迹45 只活 1 帧,40 继续(备注:174 40和45同时存在) - 目标12:
44[160-241] → 61[241] → 44[242-261]------轨迹61 只活 1 帧,44 继续(备注:241 44和61同时存在) - 目标13:
48[193-293] → 73[294] → 48[295-342] → ...------轨迹73 只活 1 帧,48 继续(备注:294 48和73同时存在)
即:双框建档产生一条只活 1~5 帧的垃圾新轨迹,与老轨迹并存几帧后自行消失,老轨迹不受影响。危害是输出污染:假 ID、画面多框。
2. 源码位置
bytetrack/byte_tracker.py Step 4(274~281 行):
python
""" Step 4: Init new stracks"""
for inew in u_detection:
track = detections[inew]
if track.score < self.det_thresh:
continue
track.activate(self.kalman_filter, self.frame_id)
det_status[det_indices[inew]] = track.track_id # 新增,赋值新建档的轨迹id
activated_starcks.append(track)
检测端 NMS 只看单帧几何,无法区分"双框"和"紧贴的两辆车";跟踪端有时序信息------已稳定存活多帧的老轨迹旁边,突然出生的新轨迹大概率是双框。
3. 改进方案
Step 4 建档前加一道校验:新轨迹(尚未激活)与所有存活 ≥ dup_age 帧的 Tracked 老轨迹(含本帧已激活的新轨迹,防两个新框互为双框漏网)计算 IoU,大于 dup_iou 则判为双框,直接丢弃新轨迹(不激活、不进入任何池)。老轨迹不受影响。
- 开关
use_dup_suppression(默认 False)/dup_iou(默认 0.55)/dup_age(建议 5) - 不传参数时行为与原版完全一致
- 断档后重新出现的目标不受影响:老轨迹已 Lost,不在 Tracked 池,不参与比较
4. 改进代码
byte_track.py 的 ByteTrackArgs.__init__ 新增双轨迹抑制三个参数:use_dup_suppression、dup_iou、dup_age:
python
self.use_dup_suppression = getattr(args, 'use_dup_suppression', False) # 是否启用双轨迹抑制
self.dup_iou = getattr(args, 'dup_iou', 0.55) # 双框判定 IoU 阈值
self.dup_age = getattr(args, 'dup_age', 5) # 老轨迹最小存活帧数
bytetrack/byte_tracker.py 的 BYTETracker 类新增方法(放在 update 方法之前):
python
def _is_dup_track(self, track, activated_now):
"""判断新轨迹是否为双框轨迹:与本帧新轨迹或存活足够久的老轨迹高度重叠即判为双框
输入:
track: 待建档的新轨迹(STrack,尚未激活)
activated_now: 本帧已激活的新轨迹列表(list[STrack]),双框互斥直接比较不设年龄门槛
返回:
是双框轨迹: True
不是双框轨迹: False
"""
for t_new in activated_now:
if matching.ious([track.tlbr], [t_new.tlbr])[0][0] > self.args.dup_iou:
return True
for t_old in self.tracked_stracks:
if self.frame_id - t_old.start_frame < self.args.dup_age:
continue
if matching.ious([track.tlbr], [t_old.tlbr])[0][0] > self.args.dup_iou:
return True
return False
bytetrack/byte_tracker.py Step 4 改为:
python
""" Step 4: Init new stracks"""
activated_now = [] # 新增:本帧已激活的新轨迹(双框互斥也要比)
for inew in u_detection:
track = detections[inew]
if track.score < self.det_thresh:
continue
# 新增:双轨迹抑制------命中双框判定则丢弃新轨迹
if self.args.use_dup_suppression and self._is_dup_track(track, activated_now):
continue
track.activate(self.kalman_filter, self.frame_id)
det_status[det_indices[inew]] = track.track_id # 新增,赋值新建档的轨迹id
activated_starcks.append(track)
activated_now.append(track) # 新增
test/test_bytetrack.py 新增命令行参数:
python
parser.add_argument("--use-dup-suppression", action="store_true", help="是否启用双轨迹抑制(默认关闭)")
parser.add_argument("--dup-iou", type=float, default=0.55, help="双框判定 IoU 阈值(默认 0.55)")
parser.add_argument("--dup-age", type=int, default=5, help="老轨迹最小存活帧数(默认 5)")
5. 测试方式
bash
python ./test/test_bytetrack.py --model model/yolov8s.pt --video video/dense.mp4 --conf 0.25 --classes 2 --track-thresh 0.3 --new-track-thresh 0.35 --use-dup-suppression
改进开关参数说明:
| 参数 | 作用 | 取值范围 |
|---|---|---|
--use-dup-suppression |
双轨迹抑制总开关:不传则关闭,行为与原版完全一致 | 开关型:传了即开启 |
--dup-iou 0.55 |
重叠门槛:新出生的轨迹若和一条老轨迹的框重叠程度(0~1,1 表示完全重合)超过此值,就认为是同一辆车被检测出两个框,丢弃新轨迹。值越小越容易触发丢弃 | 0~1(小数),默认 0.55,建议 0.5~0.7 |
--dup-age 5 |
老轨迹的"资历"门槛:老轨迹至少已跟踪满这么多帧,新轨迹才拿它来比较(本帧同时新建的轨迹之间不受此限制,直接互斥) | ≥ 1 的整数,建议 3~10 |
(基线 = 同命令去掉后三个参数,即 test/out/bytetrk_yolov8s_dense_0915_114254;可用 --dup-iou 0.5 / 0.7 做敏感性对比)
6. 对比结果
现象记录(每次测试追加一行):
| 参数 | 测试目录 | 现象 |
|---|---|---|
| 基线 | test/out/bytetrk_yolov8s_dense_0915_114254 | 目标2:第237-239帧 54和59 同时存在 目标2:第393帧 69和93 同时存在 目标9:第174帧 40和45 同时存在 目标12:第241帧 44和61 同时存在 目标13:第294帧 48和73 同时存在 |
--use-dup-suppression --dup-iou 0.6 --dup-age 5 |
test/out/bytetrk_yolov8s_dense_0915_174340 | 目标2:第393帧 轨迹88 与轨迹66 并存 |
--use-dup-suppression --dup-iou 0.55 --dup-age 5 |
test/out/bytetrk_yolov8s_dense_0916_091132 | 无双轨迹并存 |
指标对比:
| 指标 | 基线 | --dup-iou 0.6 |
--dup-iou 0.55 |
|---|---|---|---|
| 双轨迹并存(标注确认) | 5 处 | 1 处 | 0 处 |
| 轨迹总条数 | 93 | 88 | 86 |
| 单帧轨迹条数 | 19 | 16 | 14 |
| 逐目标切换次数 | 23 | 20 | 14 |
| 目标2 切换次数 | 4 | 3 | 1 |
| 目标9 切换次数 | 3 | 2 | 1 |
| 目标12 切换次数 | 2 | 2 | 0 |
| 目标13 切换次数 | 2 | 1 | 0 |
| 其余 9 目标切换次数 | 持平 | 持平 | 持平 |
| 误杀(长轨迹消失) | --- | 无 | 无 |
7. 总结
建议开启,推荐直接使用 --use-dup-suppression(dup_iou 默认已为 0.55、dup_age 默认 5)。--dup-iou 往小调捕获能力更强:0.6 时漏掉 IoU 0.59 的 1 处双框,0.55 时全部捕获且无长轨迹误杀,不建议再低于 0.55(误杀风险上升,未实测)。--dup-age 调小(如 3)抑制更激进,刚进画面几帧的相邻新车易被误杀;调大(如 10)会漏掉出生 5~10 帧的双框,5 为实测无副作用值,一般不需调整。
改进二:未确认轨迹宽限期(turn.mp4测试结果分析来源)
当前现象:刚建档的轨迹只要断档一帧就被立即删除,检测框随后回来也无法配回,只能以新 ID 重新建档------目标反复换 ID。本改进目的:给转正失败的未确认轨迹短暂宽限期,检测恢复时配回原轨迹,减少这类切换。
1. 源码问题
目标检测短暂波动(漏检一帧或置信度短暂下降)时,已转正轨迹 会进入丢失池,受 track_buffer(默认 30 帧)保护,检测恢复后可配回原 ID;但未确认轨迹 (刚建档、还没通过转正考试)一帧没配上就被 mark_removed 立即删除,不进丢失池、无任何保护 。同一目标只因断档发生在转正前后,命运完全不同:转正后断档能等 30 帧,转正前断档一帧即死。刚建档的轨迹死后,检测框即使原地回来(IoU 再高),池中已无该轨迹可配,只能等置信度重新过建档线后以新 ID 重生------这就是目标在 track_buffer 帧内连续换 ID 的根因。
人工标注实证(turn_anno.txt,共 11 处,涉及 7 个目标):
- 目标3(右转弯):
33[1-232] → 133[281] → 134[285-300]------轨迹133281:第281帧建档,第282帧一帧漏检即被删除;第283/284帧检测框原地回来(conf 0.28/0.34)已无轨迹可配;第285帧以新 ID 134 重生 - 目标7:
41[8] → 47[11] → 80[100-266]------轨迹418:建档次帧(第9帧)检测框在原位(conf 0.31)但轨迹已删;轨迹4711:建档次帧(第12帧)框在原位(conf 0.30)同样已删;第12~99帧目标长期无稳定轨迹,直到 100 帧以新 ID 80 重生 - 目标8:
35[6] → 42[8] → 45[10-15] → 87[121-269]------轨迹356:次帧(第7帧)框在原位(conf 0.29)即删;轨迹428:次帧(第9帧)框在原位(conf 0.32)即删;直到轨迹4510 转正成功才暂时稳定,第121帧又以新 ID 87 重生 - 目标13:
77[94] → 93[129] → 105[185] → 115[219] → 118[223] → 121[225] → 125[231-300]------第219~231帧内连续三个单帧 ID 接力:115 建档后次帧框在(conf 0.31)即删,220~222 无框,223 建 118 又即删(次帧框 conf 0.29),225 建 121 再即删,直到 231 帧 125 才存活 - 目标14:
98[152] → 99[154-162] → 107[193] → 108[201] → 109[204-300]------轨迹98152:次帧(第153帧)框在原位(conf 0.32)即删,第154帧以新 ID 99 重生;轨迹107193:次帧框在(conf 0.32)即删;轨迹108201:次帧框在(conf 0.31)即删,204 帧以 109 重生 - 目标15:
27[1-5] → 71[78] → 72[79-205]------轨迹7178:第78帧建档,次帧(第79帧)检测框就在原位(conf 0.36),若轨迹还在即可配回,实际已被删,该帧以新 ID 72 重生 - 目标18:
11[1-26] → 66[65] → 67[66]------连续两帧各换一个 ID:第65帧 66 建档即死(次帧框 conf 0.38 在原位),第66帧又建 67,67 次帧无框再删
未确认轨迹死于"一帧没配上"的两种情况:一是建档次帧漏检 (无检测框,转正考试无人应考);二是次帧有框但转正考试挂科 ------第三轮判分公式 cost = 1 − IoU×conf 要求 ≤ 0.7,conf 被直接乘进分数,临界置信度的框容错极小:例如 conf 0.36 的框,即使与预测框 IoU 达 0.80(几乎重合),cost = 1 − 0.80×0.36 = 0.71 > 0.7,转正失败;而 conf 0.90 的框只需 IoU ≥ 0.78。即临界置信度的框必须与预测框几乎完全重合才能转正,框稍有抖动即死。track_thresh(0.3)只是参与配对的入场券,不保证配得上。
turn.mp4 换 ID 的构成与本改进的覆盖范围:
| 换 ID 类型 | 机制 | 宽限期能否解决 |
|---|---|---|
| 建档次帧漏检 | 次帧无检测框,转正考试无人应考,轨迹一帧即删;检测恢复时池中无轨迹可配 | 能:进宽限池,检测框回来时经第一轮匹配配回 |
| 次帧有框但转正考试挂科 | 临界 conf 的框 cost = 1 − IoU×conf > 0.7,转正失败即删,同帧以新 ID 重建档 | 能:进宽限池且 state 保持 Tracked,下一帧第一轮(阈值 0.8 比转正考试 0.7 宽)或第二轮低分框(纯 IoU 配对)均可配回 |
2. 源码位置
bytetrack/byte_tracker.py 第三轮关联的未确认轨迹处理(286~290 行):
python
for it in u_unconfirmed:
track = unconfirmed[it]
track.mark_removed()
removed_stracks.append(track)
3. 改进方案
转正失败的未确认轨迹不立即删除:转入独立的宽限池(不进丢失池、不 mark_lost,state 保持 Tracked),打上宽限截止帧标记(当前帧 + grace)。宽限轨迹参与第一轮(高分框)与第二轮(低分框,纯 IoU 配对)匹配,配上即转正回归在跟池;超过截止帧仍无匹配才真正移除。丢失池与已转正轨迹的超时逻辑完全不变。同时抑制同帧重建档:导致挂科的检测框若与宽限中的轨迹高度重叠,本帧不再用它新建档,把框留给宽限轨迹下一帧配回,避免闪现 1 帧的新 ID。
- 开关
use_unconfirmed_grace(默认 False)/grace(建议 3)/grace_iou(默认 0.7) - 不传参数时行为与原版完全一致
- 误配风险控制:宽限期极短(3 帧,远小于 buffer),且仍受 IoU 匹配门槛约束
- 与双轨迹抑制无冲突(宽限池轨迹不参与双框判定)
4. 改进代码
byte_track.py 的 ByteTrackArgs.__init__ 新增未确认宽限三个参数:use_unconfirmed_grace、grace、grace_iou:
python
self.use_unconfirmed_grace = getattr(args, 'use_unconfirmed_grace', False) # 是否启用未确认轨迹宽限期
self.grace = getattr(args, 'grace', 3) # 未确认轨迹宽限帧数
self.grace_iou = getattr(args, 'grace_iou', 0.7) # 同帧重建档抑制的重叠门槛
bytetrack/byte_tracker.py 的 STrack.__init__ 新增宽限标记:
python
self.grace_deadline = None # 未确认宽限截止帧(None 表示非宽限状态)
bytetrack/byte_tracker.py 的 BYTETracker.__init__ 新增宽限池:
python
self.grace_tracks = [] # 新增:宽限池(第三轮挂科的未确认轨迹,独立于丢失池管理)
bytetrack/byte_tracker.py 第一轮匹配池纳入宽限轨迹:
python
strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
# 新增:宽限池轨迹参与第一轮(开关关闭时跳过,不白跑合并)
if self.args.use_unconfirmed_grace:
strack_pool = joint_stracks(strack_pool, self.grace_tracks)
bytetrack/byte_tracker.py 第二轮落单处理跳过宽限轨迹(不判丢、不进丢失池,留在宽限池等下一帧):
python
for it in u_track:
track = r_tracked_stracks[it]
if self.grace_tracks and track in self.grace_tracks: # 新增:宽限轨迹不判丢,留在宽限池等下一帧
continue
if not track.state == TrackState.Lost:
track.mark_lost()
lost_stracks.append(track)
bytetrack/byte_tracker.py 第三轮未确认轨迹处理改为:
python
grace_now = [] # 新增:本帧挂科进宽限的轨迹(同帧建档抑制用)
for it in u_unconfirmed:
track = unconfirmed[it]
# 新增:未确认轨迹宽限期------转正失败不立即删除,进宽限池等检测回来
if self.args.use_unconfirmed_grace:
track.grace_deadline = self.frame_id + self.args.grace
if track in self.tracked_stracks:
self.tracked_stracks.remove(track) # 从在跟池移出,统一由宽限池管理
self.grace_tracks.append(track)
grace_now.append(track)
else:
track.mark_removed()
removed_stracks.append(track)
bytetrack/byte_tracker.py 的 BYTETracker 类新增方法(放在 update 方法之前):
python
def _overlap_grace(self, track, grace_tracks):
"""判断待建档轨迹与挂科进宽限的轨迹是否高度重叠(同帧重建抑制用)
输入:
track: 待建档的新轨迹(STrack,尚未激活)
grace_tracks: 本帧挂科进宽限的轨迹列表(list[STrack])
返回:
高重叠: True
不重叠: False
"""
for tg in grace_tracks:
if matching.ious([track.tlbr], [tg.tlbr])[0][0] > self.args.grace_iou:
return True
return False
bytetrack/byte_tracker.py Step 4 建档循环内新增同帧重建档抑制:
python
# 新增:双轨迹抑制------命中双框判定则丢弃新轨迹
if self.args.use_dup_suppression and self._is_dup_track(track, activated_now):
continue
# 新增:宽限同帧建档抑制------挂科进宽限的轨迹与该框高度重叠时不建档,
# 把框留给宽限轨迹下一帧第一轮配回,消除 1 帧闪现新 ID
if self.args.use_unconfirmed_grace and grace_now and self._overlap_grace(track, grace_now):
continue
bytetrack/byte_tracker.py Step 5 超时判断改为(宽限轨迹用截止帧,其余走原逻辑):
python
""" Step 5: Update state"""
# 新增:宽限池维护------超期者移除,转正/移除者移出(宽限轨迹不进丢失池,不走下方超时逻辑)
# 开关关闭时池恒为空,跳过不白跑
if self.grace_tracks:
for track in self.grace_tracks:
if self.frame_id > track.grace_deadline:
track.mark_removed()
removed_stracks.append(track)
self.grace_tracks = [t for t in self.grace_tracks
if t.state == TrackState.Tracked and not t.is_activated]
for track in self.lost_stracks:
if self.frame_id - track.end_frame > self.max_time_lost:
track.mark_removed()
removed_stracks.append(track)
test/test_bytetrack.py 新增命令行参数:
python
parser.add_argument("--use-unconfirmed-grace", action="store_true", help="是否启用未确认轨迹宽限期(默认关闭)")
parser.add_argument("--grace", type=int, default=3, help="未确认轨迹宽限帧数(默认 3)")
parser.add_argument("--grace-iou", type=float, default=0.7, help="同帧重建档抑制的重叠门槛(默认 0.7)")
5. 测试方式
bash
python ./test/test_bytetrack.py --model model/yolov8s.pt --video video/turn.mp4 --conf 0.25 --classes 2 --track-thresh 0.3 --new-track-thresh 0.35 --use-unconfirmed-grace --grace 3
(基线 = 同命令去掉 --use-unconfirmed-grace --grace 3,即 test/out/bytetrk_yolov8s_turn_0916_140556)
改进开关参数说明:
| 参数 | 作用 | 取值范围 |
|---|---|---|
--use-unconfirmed-grace |
未确认轨迹宽限期总开关:不传则关闭,行为与原版完全一致 | 开关型:传了即开启 |
--grace 3 |
宽限帧数:转正失败的未确认轨迹在宽限池保留这么多帧,期间检测框回来(第一轮高分框或第二轮低分框)可配回转正,超期仍未配上才删除 | ≥ 1 的整数,建议 2~5 |
--grace-iou 0.7 |
同帧重建档抑制的重叠门槛:挂科进宽限的同一帧,若待建档框与宽限轨迹重叠超过此值则不建档,把框留给宽限轨迹下一帧配回。值越小越容易抑制(密集车流中易误抑制相邻新目标),越大越保守 | 0~1(小数),默认 0.7。依据:覆盖边界------挂科时 IoU×conf ≤ 0.3,中速目标(帧间位移 ≤ 框宽 15%,30fps)挂科框 IoU 下限 ≈ 0.70;误杀边界------dense.mp4 全片 10 万个同帧框对实测,不同目标的框 IoU 最高 0.65。0.70 覆盖中速目标且与误杀上限保持 0.05 边距。15fps 视频帧间位移加倍,需降至约 0.6 |
6. 对比结果
现象记录(每次测试追加一行):
| 参数 | 测试目录 | 现象 |
|---|---|---|
| 基线 | test/out/bytetrk_yolov8s_turn_0916_140556 | 目标3:第281帧建档轨迹133,第282帧检测未出现轨迹丢弃,第285帧重新建档换轨迹134 目标7:第8帧建档轨迹41,第9帧有检测未通过转正考试轨迹丢弃;第11帧重新建档轨迹47,第12帧有检测仍未通过轨迹丢弃,第100帧重新建档换轨迹80 目标8:第6帧建档轨迹35,第7帧有检测未通过轨迹丢弃;第8帧重新建档轨迹42,第9帧有检测未通过轨迹丢弃,第10帧重新建档轨迹45 存活 目标13:第219帧建档轨迹115,第220帧有检测未通过轨迹丢弃;第223帧重新建档轨迹118,第224帧有检测未通过轨迹丢弃;第225帧重新建档轨迹121,第226帧检测未出现轨迹丢弃,第231帧重新建档换轨迹125 目标14:第152帧建档轨迹98,第153帧有检测未通过轨迹丢弃,第154帧重新建档换轨迹99;第193帧建档轨迹107,第194帧有检测未通过轨迹丢弃;第201帧建档轨迹108,第202帧有检测未通过轨迹丢弃,第204帧重新建档换轨迹109 目标15:第78帧建档轨迹71,第79帧有检测未通过轨迹丢弃,同帧重新建档换轨迹72 目标18:第65帧建档轨迹66,第66帧有检测未通过轨迹丢弃,同帧重新建档换轨迹67,第67帧检测未出现轨迹丢弃 |
--use-unconfirmed-grace --grace 3 --grace-iou 0.7 |
test/out/bytetrk_yolov8s_turn_0917_184227 | 目标3 已解决:第281→285帧轨迹变换;未解决:第232→281帧轨迹变换,断档 48 帧已超过 30 帧范围,不属于宽限期改进的目标 目标7 已解决:第8→11帧轨迹变换;未解决:第13→100帧轨迹变换,断档 86 帧已超过 30 帧范围 目标8 已解决:第6→8帧、第8→10帧轨迹变换;未解决:第15→121帧轨迹变换,断档 105 帧已超过 30 帧范围 目标13 已解决:第219→231帧三个单帧 ID 接力;未解决:第94→129帧(断 34 帧)、第134→185帧(断 50 帧)轨迹变换,已超过 30 帧范围;第192→219帧轨迹变换(断 26 帧,30 帧内未接回:断档期第208/210帧低分框本可配上但丢失池收不到;第219帧重现 cost 0.817 超阈值 0.8 仅 0.017 贴线卡死) 目标14 已解决:第152→154帧、第193→201帧轨迹变换(轨迹94 经宽限期低分框配回连续保持至第300帧)、第201→204帧轨迹变换;未解决:第171→193帧轨迹变换(断 21 帧,30 帧内未接回:断档期车移动 40px 且检测框漂移,高分框与外推框 IoU 仅 0~0.29,第193帧重现 cost 0.85 以上) 目标15 已解决:第78→79帧轨迹变换;未解决:第13→78帧轨迹变换,断档 64 帧已超过 30 帧范围 目标18 已解决:第65→67帧轨迹闪现变换;未解决:第39→65帧轨迹变换(断 25 帧,30 帧内未接回:断档期检测漏检;第65帧重现外推框 IoU 0.544 × conf 0.37,cost 0.799 差 0.001 贴线卡死) |
7 个目标逐个对比(基线链 → 宽限开启链,格式为 轨迹ID帧段):
| 目标 | 基线链 | 宽限开启链 | 切换次数 |
|---|---|---|---|
| 目标3 | 331-232 → 133281 → 134285-300 | 331-232 → 113281-300 | 2→1 |
| 目标7 | 418 → 4711 → 80100-266 | 418-13 → 72100-266 | 2→1 |
| 目标8 | 356 → 428 → 4510-15 → 87121-269 | 356-15 → 77121-269 | 3→1 |
| 目标13 | 7794 → 93129 → 105185 → 115219 → 118223 → 121225 → 125231-300 | 6994 → 82129-134 → 92185-192 → 100219-300 | 6→3 |
| 目标14 | 98152 → 99154-162 → 107193 → 108201 → 109204-300 | 86152-171 → 94193-300 | 4→1 |
| 目标15 | 271-5 → 7178 → 7279-205 | 271-13 → 6678-205 | 2→1 |
| 目标18 | 111-26 → 6665 → 6766 | 111-39 → 6265-67 | 2→1 |
整体指标:
| 指标 | 基线 | 宽限开启 |
|---|---|---|
| 7 目标切换次数 | 21 | 9 |
| 轨迹总条数 | 141 | 120 |
7. 总结
建议开启,推荐 --grace 3(grace_iou 默认 0.7 无需显式传)。建档即死类切换基本解决,剩余换 ID 的直接原因均为断档后配对失败、以新 ID 重建,按失败环节分两类:
| 墙 | 卡住的案例 | 原因 |
|---|---|---|
| 断档超 30 帧,轨迹已移除 | 目标3(第232→281帧)、目标7(第13→100帧)、目标8(第15→121帧)、目标15(第13→78帧)、目标13(第94→129帧、第134→185帧) | 框回来时轨迹已超时移除,无配对资格,直接以新 ID 建档 |
| 匹配分数(IoU×conf)不够过线 | 贴线档:目标18 的轨迹62(cost 0.799,差 0.001)、目标13 的轨迹92(断档期第208/210帧低分框可配但丢失池收不到,重现帧 cost 0.817,超 0.017)------放宽 match_thresh 到 0.85 即可覆盖 远离档:目标14 的轨迹86(断档期车移 40px 且检测框漂移 20~60px,cost 0.85 以上,需放宽到 0.9,误配风险需评估) | 断档期低 conf(0.3~0.4)与外推漂移叠加,fuse_score 把 cost 压在 0.8~0.9 贴线带 |
未解决换 ID 中 6 处轨迹已移除(无配对资格)、3 处匹配分数差距过大(放宽阈值将致误配失控)。
| 描述 | 链接 |
|---|---|
| 项目 | 基于 Ultralytics + ByteTrack 的多目标跟踪与改进 |
| 往期回顾 | 链接 |
| 数据集 | 数据集汇总 |
| 软件开发 | 深度学习软件开发汇总 |