

# 目标跟踪（track by detection）

![](pic/tracker.gif)

在视频分析（或视频结构化）应用开发中，多目标跟踪是非常重要的一个环节。它能有效弥补上一个目标检测环节中算法的不足，如检测算法输出坐标不稳定、漏检等。与此同时，跟踪算法输出的目标轨迹（track-id）对于应用下阶段的行为分析环节也有着至关重要的作用。下面是常见视频分析类应用系统结构：

![](pic/video_process.png)

目标检测算法输出单帧检测结果，目标跟踪算法负责将前后2帧中的目标关联起来、给予唯一标识track-id。假设t帧中检测到了M个目标，t+1帧中检测到了N个目标，跟踪算法本质上是M->N的匹配关联过程。

![](pic/t_and_t+1_match.png)

匹配过程中，目标可以分为以下三大类：

1. `matched_tracks`，t帧目标出现，t+1帧该目标仍然出现，算法匹配上。
2. `unmatched_tracks`，t帧目标出现，t+1帧该目标消失，算法未匹配上。
3. `unmatched_detections`，t帧目标不存在，t+1帧该目标出现，新增检测目标。

其中，对于2和3来说，跟踪算法需要考虑：
> t帧目标出现，t+1帧目标其实仍然存在，但是检测算法出现短暂漏检，误认为其消失。此时的解决方案是： 某帧未被匹配到的tracks不要立即清除，而是做若干帧的缓存，等待若干帧后检测算法恢复检测<br>
> t帧目标不存在，t+1帧该目标仍然不存在，但是检测算法出现短暂误检，误认为其出现。此时的解决方案是：新增的检测目标不要立即生效，而是做若干帧的缓存，等检测算法连续检测超过若干帧、并且都能匹配关联上后再生效

之所以要考虑以上2点，主要原因是对于连续视频帧而言，大部分检测算法基本无法做到100%连续、稳定检测，出现短暂的误检、漏检非常正常。

<br/>

**题目上下文说明**:

现假设有以下跟踪代码，

```python
# 定义跟踪算法类
class Tracker(object):
  # 初始化参数
  def __init__(self, max_age=1, 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

  # 跟踪函数，每帧检测结果返回后，调用一次update
  def update(self, dets=np.empty((0, 5))):
    self.frame_count += 1
    
    trks = np.zeros((len(self.trackers), 5))
    to_del = []
    ret = []
    for t, trk in enumerate(trks):
      pos = self.trackers[t].predict()[0]
      trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
      if np.any(np.isnan(pos)):
        to_del.append(t)
    
    trks = np.ma.compress_rows(np.ma.masked_invalid(trks))

    for t in reversed(to_del):
      self.trackers.pop(t)
    
    # 匹配关联
    matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks, self.iou_threshold)

    # 后处理逻辑
    # TO-DO your code...

    # 返回跟踪结果 [[left, top, right, bottom, track-id]...]
    if(len(ret) > 0):
      return np.concatenate(ret)
    return np.empty((0, 5))
```

其中：

1. `self.max_age`代表跟踪算法允许出现的最大漏检帧数
2. `self.min_hints`代表跟踪算法要求的最低连续匹配帧数
3. `self.trackers`代表跟踪算法维持的目标集合（已生成track-id）
4. `update(self, dets)`代表跟踪函数，其中参数`dets`代表t+1帧中目标检测结果list[[left, top, right, bottom, score]...]，即t+1帧中待匹配的detections
5. `associate_detections_to_trackers(...)` 代表IOU+卡尔曼滤波匹配算法，返回上面提到的`matched_tracks`，`unmatched_tracks`，`unmatched_detections`三个值
6. `time_since_update`代表目标当前漏检帧数
7. `hit_streak`代表目标当前连续匹配帧数

请你根据上面陈述补充TO-DO部分代码。

## 正确答案

```python
    # 更新matched_tracks
    for m in matched:
      self.trackers[m[1]].update(dets[m[0], :])

    # 初始化unmatched_detections，假设是当前帧新出现的检测目标
    for i in unmatched_dets:
        trk = KalmanBoxTracker(dets[i,:])
        self.trackers.append(trk)

    i = len(self.trackers)
    for trk in reversed(self.trackers):
        d = trk.get_state()[0]

        # 输出满足条件的tracks
        if (trk.time_since_update <= self.max_age) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
          ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1))
        
        i -= 1
        # 移除超过self.max_age次的漏检目标
        if(trk.time_since_update > self.max_age):
          self.trackers.pop(i)
```

## 未考虑unmatched_detections

```python
    # 更新matched_tracks
    for m in matched:
      self.trackers[m[1]].update(dets[m[0], :])

    i = len(self.trackers)
    for trk in reversed(self.trackers):
        d = trk.get_state()[0]
        
        # 输出满足条件的tracks
        if (trk.time_since_update <= self.max_age) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
          ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1))
        
        i -= 1
        # 移除超过self.max_age次的漏检目标
        if(trk.time_since_update > self.max_age):
          self.trackers.pop(i)
```

## 未考虑移除长时间未检测到的缓存数据

```python
    # 更新matched_tracks
    for m in matched:
      self.trackers[m[1]].update(dets[m[0], :])

    # 初始化unmatched_detections，假设是当前帧新出现的检测目标
    for i in unmatched_dets:
        trk = KalmanBoxTracker(dets[i,:])
        self.trackers.append(trk)
    
    for trk in reversed(self.trackers):
        d = trk.get_state()[0]

        # 输出满足条件的tracks
        if (trk.time_since_update <= self.max_age) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
          ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1))
```

## 未考虑需要连续检出并匹配self.min_hints次才生效

```python
    # 更新matched_tracks
    for m in matched:
      self.trackers[m[1]].update(dets[m[0], :])

    # 初始化unmatched_detections，假设是当前帧新出现的检测目标
    for i in unmatched_dets:
        trk = KalmanBoxTracker(dets[i,:])
        self.trackers.append(trk)

    i = len(self.trackers)
    for trk in reversed(self.trackers):
        d = trk.get_state()[0]

        # 输出满足条件的tracks
        if (trk.time_since_update <= self.max_age):
          ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1))
        
        i -= 1
        # 移除超过self.max_age次的漏检目标
        if(trk.time_since_update > self.max_age):
          self.trackers.pop(i)
```
