先把两者区别写出来:
Sort的问题:
匹配的时候用的是匈牙利算法,但是当物体遮挡之后,前后帧的物体失去关联,物体重新出现后,会切换id。
在实时视频流中,不可能保留全局视频帧中的检测框数据,因此想要解决ID频繁切换的问题,需要利用之前保存的外观特征,分配该物体受遮挡前的ID编号,从而解决ID切换的问题。
在实际效果中:低FPS(3-5)的场景下可能会跟丢,但是FPS超过8之后,效果会好很多,两者重叠后也会继续追踪,大家可以用yolov5+deepsort试试,买个萤石(简单场景可以使用,但是要关闭内置的一些算法,会影响推流速度)或者使用ffmpeg循环推送视频流来观察效果。
论文中提到,检测质量对跟踪性能有很大的影响。
如下图是SORT作者使用的追踪器,发现当FrRCNN(VGG16)作为最佳检测器,在ID的切换数量和 Multi-object tracking accuracy(MOTA)多目标追踪精度上都有较好的表现。
我们在实验时使用yolov5-l 结合deep-sort,发现表现很好,正常情况下不会出现丢失的情况。
外观特征的提取器:
这个网络最后输出的是一个128维的向量,值得关注的是,由于DeepSORT主要被用来做行人追踪的,那么输入的大小为128(高)x 64(宽)的矩形框。如果你需要做其他物体追踪,可能要把网络模型的输入进行修改。
SORT中的卡尔曼滤波算法使用的状态是一个7维的向量
x = [ u , v , s , r , u ˙ , v ˙ , s ˙ ] T x = [u,v,s,r,\dot{u},\dot{v},\dot{s}]^T x=[u,v,s,r,u˙,v˙,s˙]T
其中
当检测框与目标关联上之后,这个检测框会用于更新目标状态。通过卡尔曼滤波,会实时更新速度分量,保证目标的最新状态。
若没有关联上目标(未检测或被遮挡),则使用线性速度模型简单的预测状态,但不会进行校正。
上述是SORT中的应用,但在DeepSORT中,添加了一个新的变量,变成了
( u , v , γ , h , u ˙ , v ˙ , γ ˙ , h ˙ ) (u,v,\gamma,h,\dot{u},\dot{v},\dot{\gamma},\dot{h}) (u,v,γ,h,u˙,v˙,γ˙,h˙)
多了一个长宽比(aspect ratio)的变化率,分别表示bounding box中心的位置、纵横比、高度以及在图像坐标中对应的速度信息。因为SORT中假设检测框的长宽比是固定的,但是在实际情况下,随着物体和相机的相对运动,距离会发生变化,物体的长宽比也是会发生变化的。
然后使用一个kalman滤波器预测更新轨迹,该卡尔曼滤波器采用匀速模型和线性观测模型。
SORT:
作者使用匈牙利指派算法进行数据关联,使用的cost矩阵为原有目标在当前帧中的预测位置和当前帧目标检测框之间的IOU。当然小于指定IOU阈值的指派结果是无效的。作者发现使用IOU能够解决目标的短时被遮挡问题。这是因为目标被遮挡时,检测到了遮挡物,没有检测到原有目标,假设把遮挡物和原有目标进行了关联。那么在遮挡结束后,因为在相近大小的目标IOU往往较大,因此很快就可以恢复正确的关联。这是建立在遮挡物面积大于目标的基础上的。
缺点:遮挡结束后,车辆检测可能又将被继续执行,那么SORT只能分配给该物体一个新的ID编号,代表一个新的追踪片段的开始。所以受遮挡等情况影响较大,会有大量的ID切换。
DeepSort:
由于SORT忽略了被检测物体的表面特征,因此只有在物体状态估计不确定性较低是才会准确,在Deep SORT中,我们使用更加可靠的度量来代替关联度量,并使用CNN网络在大规模行人数据集进行训练,并提取特征,已增加网络对遗失和障碍的鲁棒性。
DeepSORT中采用了一个简单(运算量不大)的CNN来提取被检测物体(检测框物体中)的外观特征(低维向量表示),在每次(每帧)检测+追踪后,进行一次物体外观特征的提取并保存。
后面每进行一次updata,都要计算当前帧被检测物体外观特征和之前存储的外观特征的相似度,这是作为一个重要的判别依据。(由两部分组成,运动特征+外观特征,运动特征是卡尔曼滤波提供的)
过程:
PS:如果实时视频处理后FPS过低的话,可能目标框未捕捉到3帧就消失,不会在屏幕上显示。
DeepSort中使用了马氏距离来计算卡尔曼滤波状态和新获得的测量值(检测框)之间的距离。
公式如下:
( y i , S i ) (y_i,S_i) (yi,Si)表示第i个追踪分布在测量空间上的投影, y i y_i yi为均值, S i S_i Si为协方差。
i代表追踪的序号,j表示检测框的序号。
马氏距离通过测量卡尔曼滤波器的追踪位置均值(mean track location)之间的标准差与检测框来计算状态估计间的不确定性,即 d 1 ( i , j ) d^1(i,j) d1(i,j)为第i个追踪分布和第j个检测框之间的马氏距离(不确定度)。
论文中使用 t = 9.4877 t = 9.4877 t=9.4877来作为马氏距离的阈值上限,超过则直接忽略。t是由倒卡方分布计算出来的95%置信区间作为阈值。
def gating_distance(self, mean, covariance, measurements,
only_position=False):
"""Compute gating distance between state distribution and measurements.
A suitable distance threshold can be obtained from `chi2inv95`. If
`only_position` is False, the chi-square distribution has 4 degrees of
freedom, otherwise 2.
Parameters
----------
mean : ndarray
Mean vector over the state distribution (8 dimensional).
covariance : ndarray
Covariance of the state distribution (8x8 dimensional).
measurements : ndarray
An Nx4 dimensional matrix of N measurements, each in
format (x, y, a, h) where (x, y) is the bounding box center
position, a the aspect ratio, and h the height.
only_position : Optional[bool]
If True, distance computation is done with respect to the bounding
box center position only.
Returns
-------
ndarray
Returns an array of length N, where the i-th element contains the
squared Mahalanobis distance between (mean, covariance) and
`measurements[i]`.
"""
mean, covariance = self.project(mean, covariance)
if only_position:
mean, covariance = mean[:2], covariance[:2, :2]
measurements = measurements[:, :2]
cholesky_factor = np.linalg.cholesky(covariance)
d = measurements - mean
z = scipy.linalg.solve_triangular(
cholesky_factor, d.T, lower=True, check_finite=False,
overwrite_b=True)
squared_maha = np.sum(z * z, axis=0)
return squared_maha
通过CNN(其实是一个小型的残差网络,两个卷积层和六个残差网络)将人体检测框reshape成128*64的物体输入,输出一个128维的向量。通过归一化之后,向量模长为1。
该CNN已经在大规模的人员重新识别数据集上进行了训练,该数据集包含超过1100000张1261名行人的图像,非常适合在人体跟踪环境中进行深度学习。在1050 GPU上向前传递32个边界框大约需要30毫秒。
会为每一个目标K创建一个gallery,用于存储该目标中不同帧的外观特征。代码中的NN_BUDGET就代表了gallery最大存储的大小,默认为100,即最多只能存储目标K当前时刻前100帧的目标外观特征。
第二个距离计算公式:
通过其求解所有已知的gallery中的外观特征与获得的检测框(编号为j)的外观特征的最小余弦距离。然后设置一个 t 2 t^2 t2来判断关联是否合理
通过上面的 d 1 d_1 d1 和 d 2 d_2 d2,将这两个尺度融合为:
cost matrix C C C
gate matrix B B B
c i , j = λ d 1 ( i , j ) + ( 1 − λ ) d 2 ( i , j ) c_{i,j} =\lambda d^1(i,j)+(1-\lambda)d^2(i,j) ci,j=λd1(i,j)+(1−λ)d2(i,j)
本文还提出了一种级联匹配的策略来提高匹配精度,主要由于当一个目标被遮挡很长时间,Kalman滤波的不确定性就会大大增加,并会导致连续预测的概率弥散,假设本来协方差矩阵是一个正态分布,那么连续的预测不更新就会导致这个正态分布的方差越来越大,那么离均值欧氏距离远的点可能和之前分布中离得较近的点获得同样的马氏距离值。
简单描述下:
def min_cost_matching(
distance_metric, max_distance, tracks, detections, track_indices=None,
detection_indices=None):
"""Solve linear assignment problem.
Parameters
----------
distance_metric : Callable[List[Track], List[Detection], List[int], List[int]) -> ndarray
The distance metric is given a list of tracks and detections as well as
a list of N track indices and M detection indices. The metric should
return the NxM dimensional cost matrix, where element (i, j) is the
association cost between the i-th track in the given track indices and
the j-th detection in the given detection_indices.
max_distance : float
Gating threshold. Associations with cost larger than this value are
disregarded.
tracks : List[track.Track]
A list of predicted tracks at the current time step.
detections : List[detection.Detection]
A list of detections at the current time step.
track_indices : List[int]
List of track indices that maps rows in `cost_matrix` to tracks in
`tracks` (see description above).
detection_indices : List[int]
List of detection indices that maps columns in `cost_matrix` to
detections in `detections` (see description above).
Returns
-------
(List[(int, int)], List[int], List[int])
Returns a tuple with the following three entries:
* A list of matched track and detection indices.
* A list of unmatched track indices.
* A list of unmatched detection indices.
"""
if track_indices is None:
track_indices = np.arange(len(tracks))
if detection_indices is None:
detection_indices = np.arange(len(detections))
if len(detection_indices) == 0 or len(track_indices) == 0:
return [], track_indices, detection_indices # Nothing to match.
cost_matrix = distance_metric(
tracks, detections, track_indices, detection_indices)
cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5
row_indices, col_indices = linear_assignment(cost_matrix)
matches, unmatched_tracks, unmatched_detections = [], [], []
for col, detection_idx in enumerate(detection_indices):
if col not in col_indices:
unmatched_detections.append(detection_idx)
for row, track_idx in enumerate(track_indices):
if row not in row_indices:
unmatched_tracks.append(track_idx)
for row, col in zip(row_indices, col_indices):
track_idx = track_indices[row]
detection_idx = detection_indices[col]
if cost_matrix[row, col] > max_distance:
unmatched_tracks.append(track_idx)
unmatched_detections.append(detection_idx)
else:
matches.append((track_idx, detection_idx))
return matches, unmatched_tracks, unmatched_detections
在最后阶段,作者使用之前SORT算法中的IOU关联去匹配n=1的unconfirmed和unmatched的轨迹。这可以缓解因为表观突变或者部分遮挡导致的较大变化。当然有好处就有坏处,这样做也有可能导致一些新产生的追踪被连接到了一些旧的追踪上(重叠的时候容易发生)。
使用CNN提取的特征进行匹配,大大减少了SORT中的ID switches, 经作者实验证明减少了大约45%, 在高速率视频流中也达到了很好的水准!