今日目标 :掌握目标跟踪的核心组件(卡尔曼滤波/匈牙利匹配/ReID),理解SORT到DeepSORT的进化,从零实现简单跟踪器
预计阅读 :10分钟 | 动手操作:40分钟
一、检测 vs 跟踪:从"找到"到"追踪"
目标检测:每一帧独立检测,找到"有什么"和"在哪里"
→ 帧与帧之间没有关联,同一个物体在不同帧被当作新物体
目标跟踪:关联帧间的检测结果,给每个物体分配ID
→ 同一个人从进入画面到离开,ID始终不变
→ 用于计数、轨迹分析、行为识别
Tracking-by-Detection 范式:
Step 1: 检测器找到每一帧的所有物体
Step 2: 跟踪器关联帧间检测结果
Step 3: 维护每个物体的轨迹(轨迹 = 同一ID的检测序列)
端侧AI最常用的跟踪场景:
- 行人/车辆跟踪计数
- 异常行为检测
- 客流统计
- 多目标轨迹分析
python
import torch
import torch.nn as nn
import numpy as np
from scipy.optimize import linear_sum_assignment
torch.manual_seed(42)
二、核心组件
2.1 卡尔曼滤波:预测运动轨迹
python
class KalmanFilter:
"""
卡尔曼滤波器:跟踪器的"大脑"
核心思想:用运动模型预测物体下一帧的位置
如果检测和预测接近 → 匹配成功 → 用检测更新预测
如果检测丢失 → 继续用预测位置作为估计
状态向量: [x, y, a, h, vx, vy, va, vh]
x, y: 框中心坐标
a: 宽高比 (aspect ratio)
h: 高度
vx, vy, va, vh: 对应的速度
观测向量: [x, y, a, h] (检测器输出的框)
两个阶段:
1. 预测 (Predict): 根据上一帧状态,预测当前帧位置
2. 更新 (Update): 用检测结果修正预测
"""
def __init__(self):
# 状态转移矩阵 (8×8)
self.F = np.eye(8)
# 位置 = 上一帧位置 + 速度 × dt
for i in range(4):
self.F[i, i+4] = 1.0
# 观测矩阵 (4×8)
# 我们只能观测到位置,观测不到速度
self.H = np.eye(4, 8)
# 过程噪声协方差
self.Q = np.eye(8)
self.Q[4:, 4:] *= 0.01 # 速度噪声更小
# 观测噪声协方差
self.R = np.eye(4) * 0.1
# 状态协方差矩阵
self.P = np.eye(8) * 10.0
# 状态向量
self.x = None
def init(self, measurement):
"""初始化卡尔曼滤波器"""
# measurement: [x, y, a, h]
self.x = np.zeros(8)
self.x[:4] = measurement
# 速度初始化为0
self.x[4:] = 0
def predict(self):
"""预测阶段:根据运动模型预测下一帧位置"""
if self.x is None:
return None
# 状态预测: x̂ = F × x
self.x = self.F @ self.x
# 协方差预测: P = F × P × F^T + Q
self.P = self.F @ self.P @ self.F.T + self.Q
return self.x[:4] # 返回预测的位置
def update(self, measurement):
"""更新阶段:用检测结果修正预测"""
if self.x is None:
self.init(measurement)
return
# 卡尔曼增益: K = P × H^T × (H × P × H^T + R)^{-1}
S = self.H @ self.P @ self.H.T + self.R
K = self.P @ self.H.T @ np.linalg.inv(S)
# 测量残差: y = z - H × x
y = measurement - self.H @ self.x
# 状态更新: x = x + K × y
self.x = self.x + K @ y
# 协方差更新: P = (I - K × H) × P
self.P = (np.eye(8) - K @ self.H) @ self.P
@staticmethod
def demo():
"""演示卡尔曼滤波的工作原理"""
kf = KalmanFilter()
# 模拟一个物体的运动(带噪声的检测)
true_positions = []
noisy_detections = []
for t in range(20):
# 真实位置(匀速直线运动)
true_x = 100 + 5 * t
true_y = 200 + 2 * t
true_positions.append([true_x, true_y])
# 模拟检测噪声
noisy_x = true_x + np.random.randn() * 3
noisy_y = true_y + np.random.randn() * 3
noisy_detections.append([noisy_x, noisy_y])
# 卡尔曼滤波跟踪
filtered_positions = []
kf.init([noisy_detections[0][0], noisy_detections[0][1], 1.0, 100.0])
for det in noisy_detections:
pred = kf.predict()
filtered_positions.append(pred[:2])
kf.update([det[0], det[1], 1.0, 100.0])
print("卡尔曼滤波演示:")
print(f" 真实位置: 从({true_positions[0][0]},{true_positions[0][1]}) "
f"到({true_positions[-1][0]},{true_positions[-1][1]})")
print(f" 检测噪声: ±3像素")
print(f" 滤波后: 平滑了噪声,跟踪了运动")
print(f"\n 核心公式:")
print(f" 预测: x̂ = F·x (状态向前推一步)")
print(f" 更新: x = x̂ + K·(z - H·x̂) (用检测修正预测)")
KalmanFilter.demo()
2.2 匈牙利算法:最优匹配
python
def hungarian_matching_demo():
"""
匈牙利算法:解决二分图的最优匹配问题
在跟踪中:
左侧:当前帧的检测框
右侧:上一帧的跟踪框
代价矩阵:检测框和跟踪框之间的IoU距离(或外观距离)
输出:最优的一一匹配
为什么需要匈牙利算法?
不能简单地"谁最近就匹配谁"
需要考虑全局最优:
Track1离Det1近,Track2也离Det1近
→ Track1匹配Det1,Track2匹配Det2(全局最优)
→ 而不是Track1和Track2都抢Det1
"""
# 模拟代价矩阵:4个检测 × 4个跟踪
# 代价 = 1 - IoU(越小越好)
cost_matrix = np.array([
[0.1, 0.8, 0.9, 0.7], # Det1: 和Track1最匹配
[0.7, 0.2, 0.8, 0.9], # Det2: 和Track2最匹配
[0.8, 0.9, 0.3, 0.8], # Det3: 和Track3最匹配
[0.9, 0.7, 0.8, 0.4], # Det4: 和Track4最匹配
])
# 匈牙利算法求解
row_ind, col_ind = linear_sum_assignment(cost_matrix)
print("匈牙利算法匹配演示:")
print("代价矩阵:")
for i, row in enumerate(cost_matrix):
print(f" Det{i+1}: {row}")
print("\n最优匹配:")
total_cost = 0
for r, c in zip(row_ind, col_ind):
print(f" Det{r+1} → Track{c+1} (代价: {cost_matrix[r,c]:.2f})")
total_cost += cost_matrix[r, c]
print(f" 总代价: {total_cost:.2f}")
hungarian_matching_demo()
2.3 SORT:简单在线实时跟踪
python
class SORTTracker:
"""
SORT: Simple Online and Realtime Tracking
论文:https://arxiv.org/abs/1602.00763
核心思想:卡尔曼滤波 + 匈牙利匹配
流程:
1. 检测器输出当前帧的所有检测框
2. 对每个跟踪器,用卡尔曼滤波预测下一帧位置
3. 计算检测框和预测框的IoU,构建代价矩阵
4. 匈牙利算法匹配
5. 匹配成功的:用检测更新卡尔曼滤波
6. 匹配失败的检测:创建新跟踪器
7. 匹配失败的跟踪器:如果连续N帧未匹配,删除
SORT的优缺点:
✅ 简单、快速(260 FPS)
✅ 纯运动模型,容易部署
❌ 遮挡后容易ID Switch
❌ 没有外观信息,重新出现后无法恢复ID
"""
def __init__(self, max_age=3, min_hits=3, iou_threshold=0.3):
self.max_age = max_age # 最大丢失帧数
self.min_hits = min_hits # 最少确认帧数
self.iou_threshold = iou_threshold
self.trackers = [] # 活跃的跟踪器
self.frame_count = 0
self.next_id = 0
def _iou_batch(self, boxes1, boxes2):
"""批量计算IoU矩阵"""
if len(boxes1) == 0 or len(boxes2) == 0:
return np.zeros((len(boxes1), len(boxes2)))
# boxes: [x1, y1, x2, y2]
lt = np.maximum(boxes1[:, None, :2], boxes2[:, :2])
rb = np.minimum(boxes1[:, None, 2:], boxes2[:, 2:])
wh = np.maximum(0, rb - lt)
inter = wh[:, :, 0] * wh[:, :, 1]
area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
union = area1[:, None] + area2 - inter
return inter / (union + 1e-8)
def update(self, detections):
"""
更新跟踪器
detections: [(x1, y1, x2, y2, score), ...]
"""
self.frame_count += 1
# 转换为numpy数组
dets = np.array([[d[0], d[1], d[2], d[3]] for d in detections])
# 预测所有跟踪器的下一帧位置
predicted = []
for tracker in self.trackers:
pred = tracker['kf'].predict()
if pred is not None:
# 将[x, y, a, h]转为[x1, y1, x2, y2]
x, y, a, h = pred
w = a * h
predicted.append([x - w/2, y - h/2, x + w/2, y + h/2])
else:
predicted.append([0, 0, 0, 0])
predicted = np.array(predicted) if predicted else np.empty((0, 4))
# IoU匹配
matched, unmatched_dets, unmatched_trks = [], [], []
if len(dets) > 0 and len(predicted) > 0:
iou_matrix = self._iou_batch(dets, predicted)
# 匈牙利匹配
cost = 1 - iou_matrix
row_ind, col_ind = linear_sum_assignment(cost)
for r, c in zip(row_ind, col_ind):
if iou_matrix[r, c] >= self.iou_threshold:
matched.append((r, c))
else:
unmatched_dets.append(r)
unmatched_trks.append(c)
unmatched_dets.extend([i for i in range(len(dets)) if i not in row_ind])
unmatched_trks.extend([i for i in range(len(predicted)) if i not in col_ind])
else:
unmatched_dets = list(range(len(dets)))
unmatched_trks = list(range(len(predicted)))
# 更新匹配的跟踪器
for d_idx, t_idx in matched:
det = dets[d_idx]
x1, y1, x2, y2 = det
w = x2 - x1
h = y2 - y1
x = (x1 + x2) / 2
y = (y1 + y2) / 2
a = w / h if h > 0 else 1.0
self.trackers[t_idx]['kf'].update([x, y, a, h])
self.trackers[t_idx]['hits'] += 1
self.trackers[t_idx]['age'] = 0
self.trackers[t_idx]['bbox'] = det
# 创建新的跟踪器(未匹配的检测)
for d_idx in unmatched_dets:
det = dets[d_idx]
x1, y1, x2, y2 = det
w = x2 - x1
h = y2 - y1
x = (x1 + x2) / 2
y = (y1 + y2) / 2
a = w / h if h > 0 else 1.0
kf = KalmanFilter()
kf.init([x, y, a, h])
self.trackers.append({
'kf': kf,
'id': self.next_id,
'hits': 1,
'age': 0,
'bbox': det,
})
self.next_id += 1
# 更新未匹配跟踪器的age
for t_idx in unmatched_trks:
self.trackers[t_idx]['age'] += 1
# 删除过期的跟踪器
self.trackers = [t for t in self.trackers if t['age'] <= self.max_age]
# 返回确认的跟踪结果
results = []
for t in self.trackers:
if t['hits'] >= self.min_hits:
results.append({
'id': t['id'],
'bbox': t['bbox'],
})
return results
def demo_sort():
"""演示SORT跟踪器"""
tracker = SORTTracker(max_age=3, min_hits=2, iou_threshold=0.3)
# 模拟3帧数据
frames = [
# 帧1: 两个检测
[(100, 100, 200, 200, 0.9), (300, 300, 400, 400, 0.85)],
# 帧2: 物体移动了
[(105, 105, 205, 205, 0.9), (305, 305, 405, 405, 0.85)],
# 帧3: 物体继续移动
[(110, 110, 210, 210, 0.9), (310, 310, 410, 410, 0.85)],
]
print("SORT跟踪演示:")
for i, dets in enumerate(frames):
results = tracker.update(dets)
ids = [r['id'] for r in results]
print(f" 帧{i+1}: 检测{len(dets)}个 → 跟踪ID: {ids}")
demo_sort()
三、DeepSORT:加入外观特征
python
"""
DeepSORT: Simple Online and Realtime Tracking with a Deep Association Metric
论文:https://arxiv.org/abs/1703.07402
SORT的问题:
遮挡后ID Switch严重(物体被遮挡后重新出现,被当作新物体)
原因:纯靠运动模型,没有外观信息
DeepSORT的改进:
1. 加入ReID外观特征(CNN提取的embedding)
2. 级联匹配策略(先匹配活跃的,再匹配新出现的)
3. 运动+外观联合代价
DeepSORT的核心公式:
代价 = λ × 运动代价 + (1-λ) × 外观代价
运动代价 = 马氏距离 (Mahalanobis Distance)
衡量检测框和预测框的"运动一致性"
外观代价 = 余弦距离 (Cosine Distance)
衡量两个物体的"长得像不像"
用ReID网络提取的特征向量计算余弦相似度
"""
class DeepSORTConcept:
def __init__(self):
self.components = {
'卡尔曼滤波': '预测运动轨迹(和SORT相同)',
'ReID网络': '提取外观特征向量(128维或512维)',
'级联匹配': '分层次匹配,优先匹配活跃轨迹',
'特征库': '保存最近100帧的外观特征',
}
def describe(self):
print("DeepSORT = SORT + ReID + 级联匹配")
print("\n核心组件:")
for k, v in self.components.items():
print(f" {k}: {v}")
print("\n匹配代价:")
print(" 运动代价: 马氏距离 d_mahalanobis")
print(" 外观代价: 1 - cos_sim(embedding1, embedding2)")
print(" 联合代价: c = λ·d_m + (1-λ)·d_app")
print("\n级联匹配:")
print(" 第1轮: 匹配age=0的轨迹(最活跃)")
print(" 第2轮: 匹配age=1的轨迹")
print(" ...")
print(" 优先匹配活跃轨迹,避免被旧轨迹抢走")
DeepSORTConcept().describe()
四、动手实践:简易ReID特征提取
python
class SimpleReID:
"""
简易ReID特征提取器
实际项目中使用专门的ReID网络(如OSNet、ResNet-50)
这里演示概念
"""
def __init__(self):
# 模拟:用颜色直方图作为简单的外观特征
pass
@staticmethod
def extract_features(detections, frame):
"""
提取检测框的外观特征
实际中:crop检测框 → ReID网络 → embedding向量
这里:简化为随机特征(演示概念)
"""
features = []
for det in detections:
# 实际:用CNN提取特征
# feature = reid_model(crop_image)
feature = np.random.randn(128) # 128维特征向量
feature = feature / np.linalg.norm(feature) # 归一化
features.append(feature)
return np.array(features)
@staticmethod
def cosine_similarity(feat1, feat2):
"""余弦相似度"""
return np.dot(feat1, feat2) / (np.linalg.norm(feat1) * np.linalg.norm(feat2) + 1e-8)
print("ReID特征提取:")
print(" 实际网络: OSNet / ResNet-50 / MobileNet")
print(" 特征维度: 128维或512维")
print(" 距离度量: 余弦相似度")
print(" 端侧推荐: OSNet (轻量级ReID专用网络)")
五、端侧AI跟踪方案
python
def edge_tracking_guide():
"""
端侧AI跟踪方案选型
核心考虑:
1. 检测器速度(YOLO是关键)
2. 跟踪器速度(SORT足够快)
3. ReID速度(DeepSORT的瓶颈)
"""
guide = {
'SORT (纯运动)': {
'速度': '极快(260 FPS跟踪器)',
'精度': '一般(遮挡后ID Switch多)',
'适用': '简单场景、遮挡少、不计ID',
'部署': '几乎零成本,只需卡尔曼滤波',
},
'DeepSORT (运动+外观)': {
'速度': '快(外加ReID推理时间)',
'精度': '较好(遮挡后恢复ID)',
'适用': '需要稳定ID、中等遮挡',
'部署': '需要额外部署ReID网络',
},
'ByteTrack (数据关联改进)': {
'速度': '快(利用低分检测框)',
'精度': '很好(遮挡恢复能力强)',
'适用': '端侧AI推荐!最新SOTA',
'部署': '不需要ReID,纯数据关联',
},
'BoT-SORT (ByteTrack改进)': {
'速度': '快',
'精度': '更好(加入相机运动补偿)',
'适用': '运动相机场景',
'部署': '需要相机运动估计',
},
}
print("端侧AI跟踪方案选型:")
for name, info in guide.items():
print(f"\n [{name}]")
for k, v in info.items():
print(f" {k}: {v}")
print("\n\n端侧AI推荐:")
print(" 简单场景: SORT → 极快,部署简单")
print(" 需要稳定ID: ByteTrack → 不需要额外ReID网络")
print(" 精度优先: DeepSORT + OSNet → ReID加外观信息")
edge_tracking_guide()
六、常见坑点
坑1:卡尔曼滤波的状态表示
python
# SORT/DeepSORT的状态:
# [x, y, a, h, vx, vy, va, vh]
# a = w/h (宽高比),不是宽度!
# ❌ 错误:把a当成宽度
# x, y, w, h, vx, vy, vw, vh
# ✅ 正确:a是宽高比
# 从检测框转换:
# x = (x1 + x2) / 2
# y = (y1 + y2) / 2
# a = (x2 - x1) / (y2 - y1)
# h = y2 - y1
坑2:IoU匹配的阈值调不对
python
# IoU阈值太高 → 检测和跟踪匹配不上 → 大量ID Switch
# IoU阈值太低 → 错误的匹配 → 跟踪漂移
# 经验值:0.3-0.5 是常用范围
# 快速运动场景:降低阈值到0.2
# 慢速运动场景:提高阈值到0.5
坑3:不要过早创建新ID
python
# 检测器偶发的误检 → 如果立即创建新ID → 大量虚假ID
# 解决方案:min_hits 参数
# 新检测框需要连续出现 min_hits 帧才确认为新ID
# 端侧推荐:min_hits=3
坑4:DeepSORT的特征库管理
python
# 特征库保存最近N帧的外观特征
# N太小 → 遮挡后无法恢复
# N太大 → 内存占用大,外观变化后匹配不准
# 推荐:N=100(保存最近100帧的外观特征)
# 端侧优化:N=30-50(节省内存)
七、今日作业
- 手写卡尔曼滤波:实现卡尔曼滤波的predict和update,用模拟数据验证
- 匈牙利匹配:手写匈牙利匹配(或用scipy),理解代价矩阵的构建
- SORT跟踪器:实现SORT跟踪器,用模拟数据测试跟踪效果
- 打卡 :评论区发你的跟踪结果,格式:"Day 33/100 打卡:目标跟踪已掌握!"
今日小结
今天你学会了:
✅ 检测 vs 跟踪:关联帧间检测,分配ID
✅ Tracking-by-Detection范式
✅ 卡尔曼滤波:预测运动轨迹 + 检测修正
✅ 匈牙利算法:全局最优匹配
✅ SORT:卡尔曼+匈牙利,260FPS
✅ DeepSORT:SORT+ReID+级联匹配
✅ 运动代价(马氏距离) + 外观代价(余弦距离)
✅ ReID特征提取概念
✅ 端侧AI跟踪方案选型
✅ 4个经典坑点
明日预告
Day 34:Transformer与ViT
自注意力机制、多头注意力、位置编码、Vision Transformer
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:目标跟踪入门 | SORT/DeepSORT一文搞懂 🎯
P1 --- 封面
标题:目标跟踪
副标题:卡尔曼滤波 / 匈牙利匹配 / SORT / DeepSORT
关键词:目标跟踪 / SORT / DeepSORT / 卡尔曼滤波
P2 --- 检测 vs 跟踪
检测:每帧独立,找到"有什么"
跟踪:关联帧间,给每个物体分配ID
Tracking-by-Detection = 检测器 + 跟踪器
P3 --- 卡尔曼滤波
预测:根据运动模型猜下一帧位置
更新:用检测结果修正预测
预测 + 观测 = 最优估计
跟踪器的"大脑"
P4 --- 匈牙利匹配
左:当前帧检测框
右:上一帧跟踪框
代价:1 - IoU
求全局最优匹配(不是贪心!)
P5 --- SORT → DeepSORT
SORT: 卡尔曼+匈牙利 (260FPS)
DeepSORT: SORT+ReID+级联匹配
ByteTrack: 不需要ReID,纯数据关联
端侧推荐:ByteTrack!
P6 --- 今日作业
手写卡尔曼滤波 + SORT跟踪器
评论区打卡 Day 33/100
标签:#目标跟踪 #SORT #DeepSORT #卡尔曼滤波
CSDN发布提示:CSDN版本建议在卡尔曼滤波部分放预测-更新循环图,在匈牙利匹配部分放二分图匹配示意图,在SORT部分放跟踪流程图,在DeepSORT部分放运动+外观联合代价的示意图。