第 15 / 30 课。 前置:上一课。本篇的运行范围以实验说明和验证记录为准,构造样例不代表真实任务精度或板端性能。
低分框并不全是背景
遮挡、模糊和目标变小会降低检测分数。如果先把所有低分框丢掉,跟踪器可能失去本来可以恢复的观测。ByteTrack 的重要思路是先用高分检测匹配,再让未匹配轨迹尝试与低分检测关联。
低分框在这里帮助已有轨迹延续,不是无条件创建新目标。否则背景低分噪声会迅速变成大量轨迹。本课实现两阶段关联的核心教学流程,省略官方实现中的若干轨迹状态与激活细节。
两次匹配的集合不同
第一轮输入所有可用轨迹与高分框。第二轮只处理第一轮剩余轨迹和低分框。已经匹配的轨迹不能再次被更新,低分框也不能被多个轨迹重复消费。
阈值需要区分:检测分数门限控制候选集,IoU 门限控制几何匹配,max_age 控制未观测寿命。它们不是一个“置信度阈值”。
对照实验
构造目标连续运动,其中两帧分数从 0.9 降为 0.3,另加入远处低分噪声。单阈值方案删除短寿命轨迹,之后创建新 ID;两阶段方案使用低分观测维持原 ID,且不会为低分噪声创建 ID。
这不是实际视频的性能评测。几何一致的错误低分框仍可能拉偏轨迹;真实效果应统计 ID 切换、轨迹碎片和误关联,并保留失败回放。
练习
把低分噪声移动到预测框附近,观察关联歧义;让真实目标消失超过 max_age,再出现时能否保证原 ID?答案是不能,本实现没有长期身份记忆。需要外观特征时进入下一课,而不是无限增加 max_age。
配套实验与运行记录
实验:ByteTrack 思路:用低分框恢复已有轨迹
查看完整代码:main.py
"""比较单阈值与两阶段关联,理解低分观测的用途及身份切换风险。"""
from tracker import Tracker
def run(two_stage):
tracker = Tracker(max_age=1)
ids = []
for frame, score in enumerate([0.9, 0.9, 0.3, 0.3, 0.9]):
det = {
"box": [10 + 2 * frame, 10, 30 + 2 * frame, 30],
"score": score,
"class_id": 0,
}
high = [det] if score >= 0.6 else []
low = [det] if 0.1 <= score < 0.6 else []
low.append({"box": [200, 200, 220, 220], "score": 0.2, "class_id": 0})
rows = tracker.step(high, low=low if two_stage else None)
ids.append([r["id"] for r in rows if r["matched"]])
return ids
def main():
baseline = run(False)
two = run(True)
print("high only", baseline, "two stage", two)
assert baseline[-1] == [2] and all(row == [1] for row in two)
if __name__ == "__main__":
main()
查看完整代码:tracker.py
"""教学版恒速 Kalman + IoU/Hungarian 跟踪;非官方 SORT/ByteTrack 实现。"""
import numpy as np
from scipy.optimize import linear_sum_assignment
def iou(a, b):
a = np.asarray(a, dtype=float)
b = np.asarray(b, dtype=float)
lo = np.maximum(a[:2], b[:2])
hi = np.minimum(a[2:], b[2:])
inter = np.maximum(hi - lo, 0).prod()
union = (
np.maximum(a[2:] - a[:2], 0).prod()
+ np.maximum(b[2:] - b[:2], 0).prod()
- inter
)
return float(inter / union) if union > 0 else 0.0
def measure(box):
box = np.asarray(box, dtype=float)
return np.r_[(box[:2] + box[2:]) / 2, box[2:] - box[:2]]
class Track:
def __init__(self, ident, detection):
self.id = ident
self.label = detection.get("class_id", 0)
self.x = np.r_[measure(detection["box"]), np.zeros(4)]
self.p = np.diag([10.0] * 4 + [100.0] * 4)
self.missed = 0
self.hits = 1
self.matched = True
def box(self):
cx, cy, w, h = self.x[:4]
w, h = max(w, 1), max(h, 1)
return np.array([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])
def predict(self, dt):
f = np.eye(8)
f[:4, 4:] = np.eye(4) * dt
self.x = f @ self.x
self.p = f @ self.p @ f.T + np.eye(8) * 0.1
self.missed += 1
self.matched = False
def update(self, detection):
h = np.eye(4, 8)
r = np.eye(4)
innovation = measure(detection["box"]) - h @ self.x
s = h @ self.p @ h.T + r
k = np.linalg.solve(s, (self.p @ h.T).T).T
self.x += k @ innovation
a = np.eye(8) - k @ h
self.p = a @ self.p @ a.T + k @ r @ k.T
self.missed = 0
self.hits += 1
self.matched = True
class Tracker:
def __init__(self, max_age=2, threshold=0.2):
self.max_age = max_age
self.threshold = threshold
self.tracks = []
self.next_id = 1
def associate(self, indices, detections):
if not indices or not detections:
return [], list(indices), list(range(len(detections)))
cost = np.full((len(indices), len(detections)), 1e6)
for r, index in enumerate(indices):
track = self.tracks[index]
for c, det in enumerate(detections):
overlap = iou(track.box, det["box"])
if track.label == det.get("class_id", 0) and overlap >= self.threshold:
cost[r, c] = 1 - overlap
rows, cols = linear_sum_assignment(cost)
pairs = [(indices[r], int(c)) for r, c in zip(rows, cols) if cost[r, c] < 1e6]
return (
pairs,
[i for i in indices if i not in {a for a, b in pairs}],
[j for j in range(len(detections)) if j not in {b for a, b in pairs}],
)
def step(self, detections, dt=1.0, low=None):
if not np.isfinite(dt) or dt <= 0:
raise ValueError("positive dt required")
for det in list(detections) + list(low or []):
b = np.asarray(det["box"])
if b.shape != (4,) or not np.isfinite(b).all() or np.any(b[2:] <= b[:2]):
raise ValueError("invalid xyxy")
for track in self.tracks:
track.predict(dt)
pairs, unmatched, new = self.associate(
list(range(len(self.tracks))), detections
)
for i, j in pairs:
self.tracks[i].update(detections[j])
if low:
pairs, _, _ = self.associate(unmatched, low)
for i, j in pairs:
self.tracks[i].update(low[j])
for j in new:
self.tracks.append(Track(self.next_id, detections[j]))
self.next_id += 1
self.tracks = [t for t in self.tracks if t.missed <= self.max_age]
return [
{
"id": t.id,
"box": t.box.tolist(),
"matched": t.matched,
"missed": t.missed,
"class_id": t.label,
}
for t in self.tracks
]
在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。
cd 'content/posts/AI-深度学习/嵌入式视觉模型进阶/15-ByteTrack 思路/code' && python3 main.py
在本篇代码目录执行 python3 main.py。依赖安装、可选入口见包内 README.md;实际运行结果见 verification.md。下载包包含本课使用的辅助模块与样例。
参考与进一步阅读
原始论文或官方文档用于核对结构、接口和适用范围。本篇代码是围绕教学目标编写的最小实验;除明确注明的完整入口外,不视为对应论文模型的完整复现。