文章有点长...

代码地址:https://github.com/ZQPei/deep...

traker是一个类,负责对多个track的进行操作,包括预测和更新。

self.tracker.predict()
self.tracker.update(detections)

tracker预测阶段是对每个track进行预测,包括

  • 卡尔曼预测
  • track年龄 age+1
  • time_since_update+1,此变量用于记录track上次更新的时间

代码如下:

 def predict(self, kf):
        """Propagate the state distribution to the current time step using a
        Kalman filter prediction step.

        Parameters
        ----------
        kf : kalman_filter.KalmanFilter
            The Kalman filter.

        """
        self.mean, self.covariance = kf.predict(self.mean, self.covariance)
        self.age += 1
        self.time_since_update += 1

tracker更新是对多个track更新:

  • track和det的匹配
  • track更新
  • 距离指标更新

代码如下:

 def update(self, detections):
        """Perform measurement update and track management.

        Parameters
        ----------
        detections : List[deep_sort.detection.Detection]
            A list of detections at the current time step.

        """
        # Run matching cascade.
        matches, unmatched_tracks, unmatched_detections = \
            self._match(detections)
        print("matches:",matches, "unmatched_tracks:",unmatched_tracks, "unmatched_detections:", unmatched_detections)

        # Update track set.
        for track_idx, detection_idx in matches:
            self.tracks[track_idx].update(
                self.kf, detections[detection_idx])
        for track_idx in unmatched_tracks:
            self.tracks[track_idx].mark_missed()
        for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx])
        self.tracks = [t for t in self.tracks if not t.is_deleted()]

        # Update distance metric.
        active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
        features, targets = [], []
        for track in self.tracks:
            if not track.is_confirmed():
                continue
            features += track.features
            print("1 features",track.track_id,np.array(features).shape)
            targets += [track.track_id for _ in track.features]
            print("1 targets_id",track.track_id,targets)
            track.features = []
        self.metric.partial_fit(
            np.asarray(features), np.asarray(targets), active_targets)

第一帧

检测结果如下:

det [array([307,  97, 105, 345]), array([546, 151,  72, 207]), array([215, 154,  59, 184]), array([400, 181,  45, 126])]

得到检测结果后进入track predict阶段,但是第一帧还没有track,所以没有predict结果。

#code1
for track in self.tracks:
            track.predict(self.kf)

接着进入track update阶段,首先对检测结果进行匹配

matches, unmatched_tracks, unmatched_detections = \
            self._match(detections)

在匹配中,首先要将track分成confirmed_track和unconfirmed_track,

  confirmed_tracks = [
            i for i, t in enumerate(self.tracks) if t.is_confirmed()]
        unconfirmed_tracks = [
            i for i, t in enumerate(self.tracks) if not t.is_confirmed()]

显然

confirmed_track: [] unconfirmed_track: []

对confirmed_track进行级连匹配

        matches_a, unmatched_tracks_a, unmatched_detections = \
            linear_assignment.matching_cascade(
                gated_metric, self.metric.matching_threshold, self.max_age,
                self.tracks, detections, confirmed_tracks)

显然,没有匹配的track,也没有没匹配的track,只有没匹配的检测。

matches_a [] unmatched_track_a [] unmatched_detections  [0, 1, 2, 3]

对所有检测创建新的track:

   for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx])

初始化代码如下:

def _initiate_track(self, detection):
        mean, covariance = self.kf.initiate(detection.to_xyah())
        self.tracks.append(Track(
            mean, covariance, self._next_id, self.n_init, self.max_age,
            detection.feature))
        self._next_id += 1

通过Track类初始化一个track, self._next_id += 1,因为创建一个track后,id也多一个了。

每个track初始化的属性如下:

self.mean = mean
self.covariance = covariance
self.track_id = track_id
self.hits = 1
self.age = 1
self.time_since_update = 0

self.state = TrackState.Tentative
self.features = []
if feature is not None:
    self.features.append(feature)

self._n_init = n_init
self._max_age = max_age

初始化的track,状态为Tentative,age=1,time_since_update = 0,features=[]。

默然3帧以内track的状态都是tentative。3帧以后便是conformed。30帧不更新则是deleted

第二帧

检测结果:

det [array([227, 152,  52, 189]), array([546, 153,  66, 203]), array([ 35,  52, 114, 466]), array([339, 130,  92, 278]), array([273, 134,  90, 268])]

因为第一帧得到了4个track,每个track进入predict阶段,进行卡尔曼预测,age和time_since_update都分别+1

  def predict(self, kf):
        """Propagate the state distribution to the current time step using a
        Kalman filter prediction step.

        Parameters
        ----------
        kf : kalman_filter.KalmanFilter
            The Kalman filter.

        """
        self.mean, self.covariance = kf.predict(self.mean, self.covariance)
        self.age += 1
        self.time_since_update += 1

此时,每个track的age和time_since_update分别为:

track update age 2 time_since_update 1
track update age 2 time_since_update 1
track update age 2 time_since_update 1
track update age 2 time_since_update 1

预测后进入track更新阶段

对预测的检测结果与之前得到的track进行匹配,首先将之前的track划分tracks为 confirmed_tracks 和unconfirmed_tracks,结果为:

confirmed_track [] unconfirmed_track [0, 1, 2, 3]

因confirmed_track为空,所以级联匹配结果为:

matches_a [] unmatched_track_a [] unmatched_detections  [0, 1, 2, 3,4]

接着,unconfirmed_track跟级联匹配结果的unmatched_track_a中time_since_update=1(上一帧得到更新)的track组成候选track。

   iou_track_candidates = unconfirmed_tracks + [
            k for k in unmatched_tracks_a if
            self.tracks[k].time_since_update == 1]

        unmatched_tracks_a = [
            k for k in unmatched_tracks_a if
            self.tracks[k].time_since_update != 1]

候选track跟unmatched_track_a结果为:

iou_track_candidates [0, 1, 2, 3],unmatched_track_a []

候选track没匹配的检测进行iou匹配

 matches_b, unmatched_tracks_b, unmatched_detections = \
            linear_assignment.min_cost_matching(
                iou_matching.iou_cost, self.max_iou_distance, self.tracks,
                detections, iou_track_candidates, unmatched_detections)

IOU匹配的结果为:

matches_b [(0, 0), (1, 1), (2, 2), (3, 3)] unmatches_track_b [] unmatched_detections [4]

最后将结果合并,级联匹配到的track跟iou匹配到track合并成最终的匹配结果,级联匹配中time_since_update!=1的track和iou没匹配到的track合并成最终的没匹配的track。可以看出,上一帧有更新的confirmed track会进行级联匹配和iou匹配,上一帧没更新的confirmed track会直接成为没匹配的track,从概率上说,上一帧有更新的track,当前帧会继续更新的概率会更大。

 matches = matches_a + matches_b
 unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))

最后结果为:

matches: [(0, 0), (1, 1), (2, 2), (3, 3)] unmatched_tracks: [] unmatched_detections: [4]

匹配完后,会有三种结果,分别是匹配到检测,未匹配到的track和未匹配的检测框。

接下来进入track数据更新阶段

对于匹配的结果,执行

 for track_idx, detection_idx in matches:
            self.tracks[track_idx].update(
                self.kf, detections[detection_idx])

每个track进行update

  • 卡尔曼
  • 检测边框特征,每个track都会存储一系列的特征,用作特征匹配
  • hits
  • time_since_update置0
  • track状态,判断能够将状态设置为confirmed
   def update(self, kf, detection):
        """Perform Kalman filter measurement update step and update the feature
        cache.

        Parameters
        ----------
        kf : kalman_filter.KalmanFilter
            The Kalman filter.
        detection : Detection
            The associated detection.

        """
        self.mean, self.covariance = kf.update(
            self.mean, self.covariance, detection.to_xyah())
        self.features.append(detection.feature)

        self.hits += 1
        self.time_since_update = 0
        if self.state == TrackState.Tentative and self.hits >= self._n_init:
            self.state = TrackState.Confirmed

此时,所有track都能匹配到,他们的time_since_update都是0,。

对于未匹配到的track,对其状态进行标记,如果当前track状态为tentative,则该状态更新为deleted。如果太久没更新,time_since_update>max_age,该状态也将更新为deleted。

for track_idx in unmatched_tracks:
            self.tracks[track_idx].mark_missed()
 def mark_missed(self):
        """Mark this track as missed (no association at the current time step).
        """
        if self.state == TrackState.Tentative:
            self.state = TrackState.Deleted
        elif self.time_since_update > self._max_age:
            self.state = TrackState.Deleted

对于没有匹配到检测,创建新的track

   for detection_idx in unmatched_detections:
            self._initiate_track(detections[detection_idx])

然后检查所有track,将deleted状态的track删除。

 self.tracks = [t for t in self.tracks if not t.is_deleted()]

第三帧

检测结果为:

[array([307, 105, 108, 325]), array([547, 148,  70, 211]), array([216, 151,  59, 190]), array([402, 183,  43, 124]), array([ 35,  87,  70, 376])]

跟踪过程跟上一帧差不多,这里检测结果跟之前的track都能匹配上,track年龄和time_since_update为

track update age 3 time_since_update 1
track update age 3 time_since_update 1
track update age 3 time_since_update 1
track update age 3 time_since_update 1
track update age 2 time_since_update 1

匹配完之后,track set的更新会将部分track的状态更新为confirmed。

我们直接看第四帧。

第四帧

检测结果:

[array([318, 119, 105, 301]), array([545, 146,  71, 215]), array([216, 151,  59, 192]), array([ 30,  75,  82, 398]), array([403, 185,  41, 121])]

得到检测结果后进入预测阶段,track更新卡尔曼预测,age和time_since_update。

track update age 4 time_since_update 1
track update age 4 time_since_update 1
track update age 4 time_since_update 1
track update age 4 time_since_update 1
track update age 3 time_since_update 1

预测完成后进入track更新阶段

首先是检测的结果跟track匹配,在匹配中,要将track分成confirmed_track和unconfirmed_track,结果如下:

confirmed_t [0, 1, 2, 3] unconfirmed [4]

因为第4个det是第2帧才检出,所以状态还是unconfirmed。

接着对confirmed的track进行级联匹配

首先是对dets和confirmed_tracks创建索引

 if track_indices is None:
        track_indices = list(range(len(tracks)))
    if detection_indices is None:
        detection_indices = list(range(len(detections)))

结果为:

track_indices [0, 1, 2, 3] detection_indices [0, 1, 2, 3, 4]

当level=0时候,track_indices_l 索引中对应的time_since_update都是1,然后得到matches_l的匹配结果 ,当然level=1时候,track_indices_l 索引中对应的time_since_update都是2,然后再次得到匹配结果与之间结果进行合并,如此循环...,也就是先匹配最近有更新的track,由近到远...,保证了最近更新track的优先级。

 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))

经过级联匹配后,得到的结果为:

matches_a [(0, 0), (1, 1), (2, 2), (3, 4)] unmatched_track_a [] unmatched_detections  [3]

剩下了一个没匹配的det。

unconfirmed_tracks和级联匹配中未匹配并且time_since_update =1的track组成了候选tracks。

iou_track_candidates [4]

候选tracks跟未匹配的det进行IOU匹配,结果如下:

matches_b [(4, 3)] unmatches_track_b [] unmatched_detections []

最终结果如下:

matches: [(0, 0), (1, 1), (2, 2), (3, 4), (4, 3)] unmatched_tracks: [] unmatched_detections: []

匹配结束后,将当前帧dets的feature更新到map(trackid->feature)中。

  active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
        features, targets = [], []
        for track in self.tracks:
            if not track.is_confirmed():
                continue
            features += track.features
            targets += [track.track_id for _ in track.features]
            track.features = []
        self.metric.partial_fit(
            np.asarray(features), np.asarray(targets), active_targets)
 def partial_fit(self, features, targets, active_targets):
        for feature, target in zip(features, targets):
            self.samples.setdefault(target, []).append(feature)
            if self.budget is not None:
                self.samples[target] = self.samples[target][-self.budget:]
        
        self.samples = {k: self.samples[k] for k in active_targets}

整个deepsort过程就这样子了,我们来看看更加细节的问题。

IOU匹配

如何得到代价矩阵?

初始化代价矩阵,矩阵(i,j)代表track i和det j的代价。然后计算卡尔曼滤波预测的bbx和det的IOU,代价=1-IOU。但是如果track已经有一帧以上(包含)没有更新,那么cost将会设置得很大,即为INFTY( 1e+5)。

def 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)
    return cost_matrix

得到代价矩阵后,如果元素大于max_distance,该元素会设置为max_distance + 1e-5

cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5

第二帧代价矩阵为:

[[0.04281178 1.         1.         0.96899767 1.        ]
 [1.         0.03566279 1.         1.         1.        ]
 [1.         1.         0.04389799 1.         1.        ]
 [0.95802783 1.         1.         0.08525083 1.        ]]
 
 #处理后
 [[0.04281178 0.70001    0.70001    0.70001    0.70001   ]
 [0.70001    0.03566279 0.70001    0.70001    0.70001   ]
 [0.70001    0.70001    0.04389799 0.70001    0.70001   ]
 [0.70001    0.70001    0.70001    0.08525083 0.70001   ]]

得到代价矩阵后,将其输入到匈牙利算法中

row_indices, col_indices = linear_assignment(cost_matrix)

当然也不是所有track和det都能得到匹配,iou匹配中把大于max_distacne的被认为是不匹配的。

 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))

级联匹配

看看如何得到代价矩阵。一个track中保存了多个det的特征,所以该track跟当前帧某个det的特征会有多个余弦距离,取最小值作为该track与该det的最终余弦距离,然后再结合马氏矩阵进行处理。

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) #计算代价矩阵
            cost_matrix = linear_assignment.gate_cost_matrix( #结合马氏矩阵进行处理
                self.kf, cost_matrix, tracks, dets, track_indices, #
                detection_indices)
            return cost_matrix
  def distance(self, features, targets):
        cost_matrix = np.zeros((len(targets), len(features)))
        for i, target in enumerate(targets):
            cost_matrix[i, :] = self._metric(self.samples[target], features)
        return cost_matrix
def _nn_cosine_distance(x, y):
    distances = _cosine_distance(x, y)
    return distances.min(axis=0) #取最小值

首先将det转换成xyah格式,

  measurements = np.asarray(
        [detections[i].to_xyah() for i in detection_indices])

接着计算track预测结果和检测结果的马氏距离,将马氏距离中大于gating_threshold( 9.4877 )的代价设置为gated_cost(100000.0)

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

最后将代价矩阵中大于max_distance的设置为max_distance(级接匹配中设为0.2) + 1e-5。

在第四帧中,余弦距离得到的代价矩阵为

[[0.02467382 0.29672492 0.14992237 0.20593166 0.25746107]
 [0.27289903 0.01389802 0.2490201  0.26275396 0.18523771]
 [0.1549592  0.25630915 0.00923228 0.10906434 0.27596951]
 [0.26783013 0.19509423 0.26934785 0.24842238 0.01052856]]

计算马氏距离,将马氏距离作用于余弦距离,将马氏大于gating_threshold的余弦代价设置为gated_cost(100000.0)。

然后得到的结果为

[[2.46738195e-02 1.00000000e+05 1.00000000e+05 1.00000000e+05
  1.00000000e+05]
 [1.00000000e+05 1.38980150e-02 1.00000000e+05 1.00000000e+05
  1.00000000e+05]
 [1.00000000e+05 1.00000000e+05 9.23228264e-03 1.00000000e+05
  1.00000000e+05]
 [1.00000000e+05 1.00000000e+05 1.00000000e+05 1.00000000e+05
  1.05285645e-02]]

代价矩阵中大于max_distance的设置为max_distance(级接匹配中设为0.2) + 1e-5,最终得到的代价矩阵为:

[[0.02467382 0.20001    0.20001    0.20001    0.20001   ]
 [0.20001    0.01389802 0.20001    0.20001    0.20001   ]
 [0.20001    0.20001    0.00923228 0.20001    0.20001   ]
 [0.20001    0.20001    0.20001    0.20001    0.01052856]]

然后将代价矩阵输入到匈牙利算法中求解。

deepsrot步骤如下

  • track划分为uncomfirmed_track和comfirmed_track
  • confirmed_track和det进行级联匹配

    • 1.计算track和检测结果的特征余弦距离cost matrix
    • 2.计算马氏距离,将马氏距离作用与cost matrix,若马氏距离中大于gating_threshold,cost matrix中相应的代价设置为gated_cost。
    • 3.将const matrix中大于max_distance的设置为max_distance
    • 4.匈牙利求解,删除匹配值较大的结果。
    • 根据track的time_since_update,循环1-4,并合并结果。
  • unconfirmed_track和级联匹配中未能匹配并且time_since_update=1的track组成候选track,候选track和没匹配的det进行iou匹配

    • 对预测结果和检测结果计算iou代价矩阵
    • 匈牙利求解
  • 合并级联匹配和iou匹配结果。
  • 对于最终匹配到track进行以下操作

    • 卡尔曼更新
    • 存储边框特征
    • hits+1
    • time_since_update置0
    • track状态更新,判断能够将状态设置为confirmed
  • 对于最终未能匹配到的track进行以下操作

    • 判断保留还是删除track,如果30帧没能更新,就删除。
  • 对于最终未能匹配到的det创建新的track

整个流程如下图

ref:

Deep Sort 算法代码解读

[SIMPLE ONLINE AND REALTIME TRACKING WITH A DEEP ASSOCIATION METRIC](https:/


WeifaGan
36 声望48 粉丝