前段时间因为项目需要,需要使用yolo+deepsort来进行目标跟踪,此博客作为学习笔记与大家共勉,欢迎大家批评指正。接下来,将从整体流程以及各个小模块来进行介绍。
本文主要讲解deepsort流程,下图为yolo+deepsort主要模块的流程图:
yolo预测的位置信息作为观测值输入到deepsort后,卡尔曼滤波先判断是否存在track,如果存在对其位置信息进行先验概率预测,得到先验预测 x t − 1 − x^{-}_{t-1} xt−1−,先验预测 x t − 1 − x^{-}_{t-1} xt−1−在匹配模块先后进行级联匹配和IOU匹配,最终得到匹配成功列表,包含先验预测track、观测值detection,以及没有匹配到的track和detection,在卡尔曼更新模块,对匹配成功的元素进行后验预测,得到最终的修正坐标,更新卡尔曼增益、协方差矩阵、feature set等参数,完成一帧检测,一直循环上述操作直至处理完全部视频。
对t-1时刻的track进行先验预测,使用卡尔曼滤波公式的公式1与公式2。公式如下:
x t − = F x t − 1 + B u t − 1 x^{-}_{t}=Fx_{t-1}+Bu_{t-1} xt−=Fxt−1+But−1 公式1
P t − = F P t − 1 F T + Q P^{-}_{t}=FP_{t-1}F^{T}+Q Pt−=FPt−1FT+Q 公式2
其中公式1中的 u t − 1 u_{t-1} ut−1 为表示可选的控制输入 B B B表示控制输入到当前状态的转换矩阵, B u t − 1 Bu_{t-1} But−1 一般在实际使用中忽略,因此在实际代码公式1形如 x t − = F x t − 1 x^{-}_{t}=Fx_{t-1} xt−=Fxt−1, x t − 1 x_{t-1} xt−1为t-1时刻track的状态信息矩阵, F F F为t-1时刻到t时刻的状态的转移矩阵, x t − x^{-}_{t} xt−为先验预测。其中 x t − 1 x_{t-1} xt−1为一个八维的长向量:[x,y,w,h,vx,vy,vw,vh],表示位置信息以及相应的速度信息, F F F为 8 ∗ 8 8*8 8∗8数值为1的对角阵的基础上进行
for i in range(ndim):
self._motion_mat[i, ndim + i] = dt
的矩阵(这里其实不是很明白为啥要进行这个操作)。
公式2中 P t − 1 P_{t-1} Pt−1和 P t − P^{-}_{t} Pt−分别为t-1时刻 8 ∗ 8 8*8 8∗8的协方差矩阵和其t时刻的先验预测。其中当track被初始化时,其协方差矩阵和均值矩阵由目标框的高度来生成和目标框坐标长宽信息生成。 Q Q Q为卡尔曼滤波器的运动估计误差,代表不确定程度。
此为卡尔曼预测代码
```python
def predict(self, mean, covariance):
"""Run Kalman filter prediction step.
Parameters
----------
mean : ndarray
The 8 dimensional mean vector of the object state at the previous
time step.
covariance : ndarray
The 8x8 dimensional covariance matrix of the object state at the
previous time step.
Returns
-------
(ndarray, ndarray)
Returns the mean vector and covariance matrix of the predicted
state. Unobserved velocities are initialized to 0 mean.
"""
std_pos = [
self._std_weight_position * mean[3],
self._std_weight_position * mean[3],
1e-2,
self._std_weight_position * mean[3]]
std_vel = [
self._std_weight_velocity * mean[3],
self._std_weight_velocity * mean[3],
1e-5,
self._std_weight_velocity * mean[3]]
motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
mean = np.dot(self._motion_mat, mean)
covariance = np.linalg.multi_dot((
self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
return mean, covariance
先验预测得到后,需要进行track和detection匹配,deepsort使用了级联匹配+IOU匹配串联的策略,track根据状态分为Confirmed、Tentative以及Deleted类型。级联匹配只对Confirmed的track执行,根据其距离上一次匹配的时间距离长短进行分批次匹配,用来匹配的代价矩阵由特征的余弦相似度距离与马氏距离来构建,使用匈牙利算法对代价矩阵进行计算得到matched和unmatched。
级联匹配代码
def matching_cascade(
distance_metric, max_distance, cascade_depth, tracks, detections,
track_indices=None, detection_indices=None):
if track_indices is None:
track_indices = list(range(len(tracks)))
if detection_indices is None:
detection_indices = list(range(len(detections)))
unmatched_detections = detection_indices
matches = []
for level in range(cascade_depth):
if len(unmatched_detections) == 0: # No detections left
break
track_indices_l = [
k for k in track_indices
if tracks[k].time_since_update == 1 + level
]
if len(track_indices_l) == 0: # Nothing to match at this level
continue
matches_l, _, unmatched_detections = \
min_cost_matching(
distance_metric, max_distance, tracks, detections,
track_indices_l, unmatched_detections)
matches += matches_l
unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches))
return matches, unmatched_tracks, unmatched_detections
row_indices, col_indices = linear_assignment(cost_matrix)
得到相应索引后,根据其代价矩阵是否大于阈值来确定是否匹配成功。Confirmed的track匹配完成后,将匹配失败的与Tentative的track组合成新的集进行IOU匹配。IOU匹配直接将所有track和detection由iou作为元素构建成IOU代价矩阵,使用匈牙利算法来进行匹配,匹配方法与级联匹配相同。
上一小节中匹配依靠的标注就是代价矩阵,因此代价矩阵的构建是匹配过程中非常重要的一步。首先我们先来介绍级联匹配的代价矩阵,先看代码:
def gated_metric(tracks, dets, track_indices, detection_indices):
features = np.array([dets[i].feature for i in detection_indices])
targets = np.array([tracks[i].track_id for i in track_indices])
cost_matrix = self.metric.distance(features, targets)# 通过最近邻计算出代价矩阵 cosine distance
cost_matrix = linear_assignment.gate_cost_matrix(#2. 计算马氏距离,得到新的状态矩阵
self.kf, cost_matrix, tracks, dets, track_indices,
detection_indices)
return cost_matrix
首先根据每个track以及detection对应的feature来计算余弦距离(余弦距离=1-余弦相似度),其中代价矩阵中的每一个元素为余弦距离。在得到第一个代价矩阵后,我们再通过马氏距离进行调整,如果代价矩阵中某一个元素的马氏距离大于阈值,则修改数值。修正完成后即为级联匹配最终的代价矩阵。
余弦距离计算
distances = _cosine_distance(x, y)
---------------------------------------
def _cosine_distance(a, b, data_is_normalized=False):
if not data_is_normalized:
a = np.asarray(a) / np.linalg.norm(a, axis=1, keepdims=True)
# np.linalg.norm 操作是求向量的范式,默认是L2范式,等同于求向量的欧式距离。
b = np.asarray(b) / np.linalg.norm(b, axis=1, keepdims=True)
return 1. - np.dot(a, b.T)
马氏距离计算以及修正代价矩阵
def gate_cost_matrix(
kf, cost_matrix, tracks, detections, track_indices, detection_indices,
gated_cost=INFTY_COST, only_position=False):
gating_dim = 2 if only_position else 4
gating_threshold = kalman_filter.chi2inv95[gating_dim]
measurements = np.asarray(
[detections[i].to_xyah() for i in detection_indices])
for row, track_idx in enumerate(track_indices):
track = tracks[track_idx]
gating_distance = kf.gating_distance(
track.mean, track.covariance, measurements, only_position)
cost_matrix[row, gating_distance > gating_threshold] = gated_cost
return cost_matrix
IOU代价矩阵计算
iou_cost(tracks, detections, track_indices=None,
detection_indices=None):
if track_indices is None:
track_indices = np.arange(len(tracks))
if detection_indices is None:
detection_indices = np.arange(len(detections))
cost_matrix = np.zeros((len(track_indices), len(detection_indices)))
for row, track_idx in enumerate(track_indices):
if tracks[track_idx].time_since_update > 1:
cost_matrix[row, :] = linear_assignment.INFTY_COST
continue
bbox = tracks[track_idx].to_tlwh()
candidates = np.asarray(
[detections[i].tlwh for i in detection_indices])
cost_matrix[row, :] = 1. - iou(bbox, candidates)#计算1-IOU 得到代价矩阵
return cost_matrix
经过匹配之后得到了matched、unmatched_track和unmatched_det。依次对匹配成功的track进行修正、对匹配事变的tack进行状态更新、对未匹配的detection将其转为track、对匹配成功的track更新特征集。
卡尔曼更新用到公式3、公式4、公司5:
K t = P t − C T C P t − C T + R K_{t}={\frac{P_{t}^{-}C^{T}}{CP_{t}^{-}C^{T}+R}} Kt=CPt−CT+RPt−CT 公式3
x t + = x t − + K t ( y k − C x t − ) x_{t}^{+}=x_{t}^{-}+K_{t}(y_{k}-Cx_{t}^{-}) xt+=xt−+Kt(yk−Cxt−)公式4
P t + = ( 1 − K t C ) P t − P_{t}^{+}=(1-K_{t}C)P_{t}^{-} Pt+=(1−KtC)Pt−公式5
根据匹配好的track和detection进行坐标修订,依次计算卡尔曼增益(kalman_gain)、 C x t − Cx^{-}_{t} Cxt−(projected_mean),以及修正的结果(new_mean)以及后验协方差矩阵(new_covariance)。代码如下:
self.mean, self.covariance = kf.update(
self.mean,self.covariance,detection.to_xyah())
def update(self, mean, covariance, measurement):
projected_mean, projected_cov = self.project(mean, covariance)
#scipy.linalg.cho_factor 返回上三角 或者下三角矩阵 其他位置元素 随机
chol_factor, lower = scipy.linalg.cho_factor(
projected_cov, lower=True, check_finite=False)
kalman_gain = scipy.linalg.cho_solve(
(chol_factor, lower), np.dot(covariance, self._update_mat.T).T,
check_finite=False).T
innovation = measurement - projected_mean
new_mean = mean + np.dot(innovation, kalman_gain.T)
new_covariance = covariance - np.linalg.multi_dot((
kalman_gain, projected_cov, kalman_gain.T))
return new_mean, new_covariance
卡尔曼更新完成后 deepsort的核心操作即全部完成,后续操作为更新每个track的状态、删除死亡track、更新confirmed track的feature set,完成所有更新进入下一帧检测追踪。