SORT(Simple Online and Realtime Tracking)是一种经典的实时多目标跟踪算法,它在计算机视觉领域有着广泛的应用。我第一次接触SORT是在一个智能监控项目中,当时需要实时追踪商场内的人流。相比复杂的深度学习跟踪器,SORT以其简洁高效的特点完美满足了我们的需求。
这个算法最吸引人的地方在于它巧妙结合了卡尔曼滤波和匈牙利算法,在保证实时性的同时达到了不错的跟踪精度。在1080p视频上,用Python实现的基础版SORT就能跑到100+ FPS,这对很多实际应用场景来说已经足够。下面我就结合自己多年的实战经验,带大家深入理解SORT的核心原理,并手把手教你用Python实现一个完整的跟踪系统。
卡尔曼滤波是SORT的核心预测组件。在实际编码时,我们通常用8维状态向量表示目标:[x,y,a,h,vx,vy,va,vh]。其中(x,y)是框中心坐标,a是长宽比,h是高度,后面四个是它们对应的速度。
注意:卡尔曼滤波的噪声参数Q和R需要根据具体场景调整。室内场景建议Q=0.01,室外复杂场景可能需要Q=0.1
我常用的初始化代码如下:
python复制self.kf = KalmanFilter(dim_x=8, dim_z=4)
self.kf.F = np.array([[1,0,0,0,1,0,0,0], # 状态转移矩阵
[0,1,0,0,0,1,0,0],
[0,0,1,0,0,0,1,0],
[0,0,0,1,0,0,0,1],
[0,0,0,0,1,0,0,0],
[0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,1,0],
[0,0,0,0,0,0,0,1]])
self.kf.H = np.array([[1,0,0,0,0,0,0,0], # 观测矩阵
[0,1,0,0,0,0,0,0],
[0,0,1,0,0,0,0,0],
[0,0,0,1,0,0,0,0]])
数据关联是跟踪的核心难点。SORT使用匈牙利算法解决检测框和预测框的匹配问题。关键是要设计合理的代价矩阵,我们通常用IoU作为匹配代价:
python复制def iou_batch(bb_test, bb_gt):
"""
计算两组框之间的IoU
bb_test: [N,4]
bb_gt: [M,4]
"""
bb_gt = np.expand_dims(bb_gt, 0)
bb_test = np.expand_dims(bb_test, 1)
xx1 = np.maximum(bb_test[..., 0], bb_gt[..., 0])
yy1 = np.maximum(bb_test[..., 1], bb_gt[..., 1])
xx2 = np.minimum(bb_test[..., 2], bb_gt[..., 2])
yy2 = np.minimum(bb_test[..., 3], bb_gt[..., 3])
w = np.maximum(0., xx2 - xx1)
h = np.maximum(0., yy2 - yy1)
intersection = w * h
union = (bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1]) + \
(bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - intersection
return intersection / union
实操技巧:当目标快速移动时,可以适当降低IoU阈值(如从0.3降到0.2),但会增加误匹配风险
一个完整的SORT实现包含三个核心类:
我推荐以下依赖库:
python复制import numpy as np
from filterpy.kalman import KalmanFilter
from scipy.optimize import linear_sum_assignment
每个跟踪目标都需要维护一个独立的状态:
python复制class KalmanBoxTracker:
count = 0 # 静态计数器
def __init__(self, bbox):
self.id = KalmanBoxTracker.count
KalmanBoxTracker.count += 1
self.kf = KalmanFilter(dim_x=8, dim_z=4)
self.time_since_update = 0
self.history = []
self.hits = 0
self.hit_streak = 0
self.age = 0
# 初始化卡尔曼滤波器参数...
SORT的每一帧处理流程如下:
python复制def update(self, dets):
# 步骤1:预测所有跟踪器的位置
trks = np.zeros((len(self.trackers), 5))
for t, trk in enumerate(trks):
pos = self.trackers[t].predict()[0]
trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
# 步骤2:计算IoU代价矩阵
iou_matrix = iou_batch(dets, trks)
# 步骤3:匈牙利算法匹配
matched_idx = linear_sum_assignment(-iou_matrix)
# 步骤4:更新匹配成功的跟踪器
for m in matched_idx:
if iou_matrix[m[0], m[1]] < self.iou_threshold:
continue
self.trackers[m[1]].update(dets[m[0], :])
# 步骤5:创建新跟踪器
for d in range(dets.shape[0]):
if d not in matched_idx[:,0]:
self.trackers.append(KalmanBoxTracker(dets[d,:]))
# 步骤6:删除丢失的跟踪器
self.trackers = [t for t in self.trackers
if not (t.time_since_update > self.max_age)]
return self.trackers
python复制# 低效写法
for i in range(len(dets)):
for j in range(len(trks)):
iou_matrix[i,j] = iou(dets[i], trks[j])
# 高效写法
iou_matrix = iou_batch(dets, trks)
python复制self.kf.Q[:4,:4] *= 0.01 # 位置噪声
self.kf.Q[4:,4:] *= 0.01 # 速度噪声
当两个目标交叉时容易出现ID交换,这是SORT的固有缺陷。临时解决方案:
目标被遮挡时容易丢失,建议:
python复制if t.time_since_update < max_age:
ret.append(np.concatenate((d, [t.id+1])).reshape(1,-1))
当跟踪目标过多时性能下降,可以:
DeepSORT在SORT基础上增加了深度外观特征,显著提升了跟踪精度。主要改进:
标准SORT只支持单类别跟踪。多类别改进方案:
python复制class MultiClassSort:
def __init__(self):
self.trackers = {} # {class_id: SORT实例}
def update(self, dets, classes):
results = []
for cls in set(classes):
mask = classes == cls
if cls not in self.trackers:
self.trackers[cls] = Sort()
tracks = self.trackers[cls].update(dets[mask])
results.extend([(*t, cls) for t in tracks])
return results
对于生产环境,我建议采用以下架构:
一个典型的调用示例:
python复制# 初始化
mot_tracker = Sort(max_age=5, min_hits=3, iou_threshold=0.3)
# 逐帧处理
while cap.isOpened():
ret, frame = cap.read()
if not ret: break
# 目标检测 (示例使用虚拟检测结果)
dets = detect(frame) # shape: (n,5) [x1,y1,x2,y2,score]
# 目标跟踪
trackers = mot_tracker.update(dets)
# 可视化
for d in trackers:
plot_box(frame, d[:4], id=d[4])
在实际项目中,我发现这些参数效果较好:
跟踪器的生命周期管理是关键,需要根据场景特点调整max_age(最大丢失帧数)和min_hits(最小连续匹配帧数)。太严格的设置会导致跟踪不稳定,太宽松又会产生大量冗余轨迹。