SORT 多目标跟踪算法笔记[通俗易懂]

SORT 多目标跟踪算法笔记[通俗易懂]sort是一种简单的在线实时多目标跟踪算法。文章要点为:以IoU作为前后帧间目标关系度量指标;利用卡尔曼滤波器预测当前位置;通过匈牙利算法关联检测框到目标;应用试探期甄别虚检;使用FasterR-CNN,证明检测好跟踪可以很简单。技术方案所提方法以检测作为关键组件,传播目标状态到未来帧中,将当前检测与现有目标相关联,并管理跟踪目标的生命周期。Detection为从…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

SORT 是一种简单的在线实时多目标跟踪算法。文章要点为:

  • 以 IoU 作为前后帧间目标关系度量指标;
  • 利用卡尔曼滤波器预测当前位置;
  • 通过匈牙利算法关联检测框到目标;
  • 应用试探期甄别虚检;
  • 使用 Faster R-CNN,证明检测好跟踪可以很简单。

技术方案

SORT 算法以检测作为关键组件,传播目标状态到未来帧中,将当前检测与现有目标相关联,并管理跟踪目标的生命周期。

检测

跟踪框架使用 Faster R-CNN 并应用其在 PASCAL VOC 挑战中的默认参数,只输出概率大于50%的行人检测结果而忽略其他类。
文章在实验中替换 MDP 和所提方法的检测,发现检测质量对跟踪性能有显著影响。在这里插入图片描述

估计模型

目标模型,即用于将目标身份传播到下一帧的表示和运动模型。SORT 算法用一个独立于其他物体和相机运动的线性等速模型来近似每个物体的帧间位移。每个目标的状态建模为:

x = [ u , v , s , r , u ˙ , v ˙ , s ˙ ] T , \mathbf{x} = [u,v,s,r,\dot{u},\dot{v},\dot{s}]^T, x=[u,v,s,r,u˙,v˙,s˙]T,

其中 u u u v v v 分别代表目标中心的水平和垂直像素位置,而 s s s r r r 分别代表目标边界框的比例(面积)和纵横比。注意,纵横比被认为是常数。关联检测到目标后,用检测到的边界框更新目标状态,其中速度分量通过卡尔曼滤波器框架进行优化求解。如果没有与目标相关的检测,则使用线性速度模型简单地预测其状态而不进行校正。

数据关联

在将检测分配给现有目标时:

  • 预测每个目标在当前帧中的新位置,估计其边界框形状;
  • 由每个检测与现有目标的所有预测边界框之间的交并比(IoU)计算分配成本矩阵;
  • 使用匈牙利算法对分配进行优化求解;
  • 拒绝检测与目标重叠小于 I O U m i n IOU_{min} IOUmin 的分配。

文章发现边界框的 IoU 距离隐式处理由目标经过引起的短时遮挡。具体地说,当遮挡物盖过目标时,只检测到遮挡物。尽管隐藏目标离检测框中心更近,但 IoU 距离更倾向于具有相似比例的检测。这使得可以在不影响覆盖目标的情况下,通过检测对遮挡目标进行校正。

创建和删除轨迹标识

当目标进入和离开图像时,需要相应地创建或销毁唯一标识。对于创建跟踪程序,文中认为任何重叠小于 I o U m i n IoU_{min} IoUmin 的检测都表示存在未跟踪的目标。使用速度设置为零的边界框信息初始化跟踪器。由于此时无法观测到速度,因此速度分量的协方差用较大的值初始化,反映出这种不确定性。此外,新的跟踪器将经历一个试用期,其中目标需要与检测相关联以积累足够的证据以防止误报的跟踪。

如果 T L o s t T_{Lost} TLost 帧未检测到,则终止轨迹。这可以防止跟踪器数量的无限增长以及由于无检测校正下预测时间过长而导致的定位错误。在所有实验中, T L o s t T_{Lost} TLost 设为1有以下原因:

  • 首先,等速模型对真实动力学的预测能力较差;
  • 其次,我们主要关注逐帧跟踪,目标重识别超出本工作范畴;
  • 此外,早期删除丢失的目标有助于提高效率。如果目标重新出现,跟踪将在新标识下隐式恢复。

sort.py

算法和程序都比较简单。程序依赖 scikit-learn 所提供的 linear_assignment 实现匈牙利匹配。KalmanFilterFilterPy 提供。

matplotlib.pyplot.ion() 打开交互模式。

  # all train
  sequences = ['PETS09-S2L1','TUD-Campus','TUD-Stadtmitte','ETH-Bahnhof','ETH-Sunnyday','ETH-Pedcross2','KITTI-13','KITTI-17','ADL-Rundle-6','ADL-Rundle-8','Venice-2']
  args = parse_args()
  display = args.display
  phase = 'train'
  total_time = 0.0
  total_frames = 0
  colours = np.random.rand(32,3) #used only for display
  if(display):
    if not os.path.exists('mot_benchmark'):
      print('\n\tERROR: mot_benchmark link not found!\n\n Create a symbolic link to the MOT benchmark\n (https://motchallenge.net/data/2D_MOT_2015/#download). E.g.:\n\n $ ln -s /path/to/MOT2015_challenge/2DMOT2015 mot_benchmark\n\n')
      exit()
    plt.ion()
    fig = plt.figure() 
  
  if not os.path.exists('output'):
    os.makedirs('output')

Jetbrains全家桶1年46,售后保障稳定

对于每个序列,创建一个 SORT 跟踪器实例。
加载序列的检测数据。检测框格式为[x1,y1,w,h]

  for seq in sequences:
    mot_tracker = Sort() #create instance of the SORT tracker
    seq_dets = np.loadtxt('data/%s/det.txt'%(seq),delimiter=',') #load detections
    with open('output/%s.txt'%(seq),'w') as out_file:
      print("Processing %s."%(seq))
      for frame in range(int(seq_dets[:,0].max())):
        frame += 1 #detection and frame numbers begin at 1
        dets = seq_dets[seq_dets[:,0]==frame,2:7]
        dets[:,2:4] += dets[:,0:2] #convert to [x1,y1,w,h] to [x1,y1,x2,y2]
        total_frames += 1

skimage.io.imread 从文件加载图像。

        if(display):
          ax1 = fig.add_subplot(111, aspect='equal')
          fn = 'mot_benchmark/%s/%s/img1/%06d.jpg'%(phase,seq,frame)
          im =io.imread(fn)
          ax1.imshow(im)
          plt.title(seq+' Tracked Targets')

update 由检测框更新轨迹。trackers命名有问题。

        start_time = time.time()
        trackers = mot_tracker.update(dets)
        cycle_time = time.time() - start_time
        total_time += cycle_time

matplotlib.axes.Axes.add_patch 将补丁p添加到轴补丁列表中;剪辑框将设置为 Axes 剪切框。 如果未设置变换,则将其设置为 transData。返回补丁。
matplotlib.axes.Axes.set_adjustable 定义 Axes 将更改哪个参数以实现给定面。

        for d in trackers:
          print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1'%(frame,d[4],d[0],d[1],d[2]-d[0],d[3]-d[1]),file=out_file)
          if(display):
            d = d.astype(np.int32)
            ax1.add_patch(patches.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fill=False,lw=3,ec=colours[d[4]%32,:]))
            ax1.set_adjustable('box-forced')

        if(display):
          fig.canvas.flush_events()
          plt.draw()
          ax1.cla()
  print("Total Tracking took: %.3f for %d frames or %.1f FPS"%(total_time,total_frames,total_frames/total_time))
  if(display):
    print("Note: to get real runtime results run without the option: --display")

Sort

Sort 是一个多目标跟踪器,管理多个 KalmanBoxTracker 对象。

  def __init__(self,max_age=1,min_hits=3):
    """ Sets key parameters for SORT """
    self.max_age = max_age
    self.min_hits = min_hits
    self.trackers = []
    self.frame_count = 0

update

参数dets:格式为[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]的 numpy 检测数组。
要求:即使空检测,也必须为每个帧调用此方法一次。返回一个类似的数组,其中最后一列是对象 ID。

注意:返回的对象数可能与提供的检测数不同。

update 的输入参数dets为 numpy.array,然而 KalmanBoxTracker 要求的输入为列表。

Created with Raphaël 2.2.0 update dets KalmanBoxTracker.predict associate_detections_to_trackers KalmanBoxTracker.update KalmanBoxTracker tracks End

从现有跟踪器获取预测位置。
predict 推进状态向量并返回预测的边界框估计。

在当前帧逐个预测轨迹位置,记录状态异常的跟踪器索引。trks存储跟踪器的预测,不幸与下面的跟踪器重名。

    self.frame_count += 1
    #get predicted locations from existing trackers.
    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)

numpy.ma.masked_invalid 屏蔽出现无效值的数组(NaN 或 inf)。
numpy.ma.compress_rows 压缩包含掩码值的2-D 数组的整行。这相当于np.ma.compress_rowcols(a, 0),有关详细信息,请参阅 extras.compress_rowcols
reversed 返回反向 iterator. seq 必须是具有 __reversed__() 方法的对象,或者支持序列协议(__len__() 方法和 __getitem__() 方法,整数参数从0开始)。

逆向删除异常的跟踪器,防止破坏索引。压缩能够保证在数组中的位置不变。
associate_detections_to_trackers 将检测分配给跟踪对象(均以边界框表示)。返回3个列表:matchesunmatched_detectionsunmatched_trackers

    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)

使用分配的检测更新匹配的跟踪器。为什么不通过matched存储的索引选择跟踪器?
update 使用观测边界框更新状态向量。

    #update matched trackers with assigned detections
    for t,trk in enumerate(self.trackers):
      if(t not in unmatched_trks):
        d = matched[np.where(matched[:,1]==t)[0],0]
        trk.update(dets[d,:][0])

由未匹配的检测创建和初始化新的跟踪器。

    #create and initialise new trackers for unmatched detections
    for i in unmatched_dets:
        trk = KalmanBoxTracker(dets[i,:]) 
        self.trackers.append(trk)

get_state 返回当前边界框估计值。
ret格式为[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]

自后向前遍历,仅返回在当前帧出现且命中周期大于self.min_hits(除非跟踪刚开始)的跟踪结果;如果未命中时间大于self.max_age则删除跟踪器。
hit_streak忽略目标初始的若干帧。

    i = len(self.trackers)
    for trk in reversed(self.trackers):
        d = trk.get_state()[0]
        if((trk.time_since_update < 1) 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)) # +1 as MOT benchmark requires positive
        i -= 1
        #remove dead tracklet
        if(trk.time_since_update > self.max_age):
          self.trackers.pop(i)
    if(len(ret)>0):
      return np.concatenate(ret)
    return np.empty((0,5))

associate_detections_to_trackers

这里命名不准确,应该是将检测框关联到跟踪目标(objects)或者轨迹(tracks),而不是跟踪器(trackers)。
跟踪器数量为0则直接构造结果。

  if(len(trackers)==0):
    return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)
  iou_matrix = np.zeros((len(detections),len(trackers)),dtype=np.float32)

iou 不支持数组计算。
逐个计算两两间的交并比,调用 linear_assignment 进行匹配。

  for d,det in enumerate(detections):
    for t,trk in enumerate(trackers):
      iou_matrix[d,t] = iou(det,trk)
  matched_indices = linear_assignment(-iou_matrix)

记录未匹配的检测框及轨迹。

  unmatched_detections = []
  for d,det in enumerate(detections):
    if(d not in matched_indices[:,0]):
      unmatched_detections.append(d)
  unmatched_trackers = []
  for t,trk in enumerate(trackers):
    if(t not in matched_indices[:,1]):
      unmatched_trackers.append(t)

过滤掉 IoU 低的匹配。

  #filter out matched with low IOU
  matches = []
  for m in matched_indices:
    if(iou_matrix[m[0],m[1]]<iou_threshold):
      unmatched_detections.append(m[0])
      unmatched_trackers.append(m[1])
    else:
      matches.append(m.reshape(1,2))

初始化用列表,返回值用 Numpy.array。

  if(len(matches)==0):
    matches = np.empty((0,2),dtype=int)
  else:
    matches = np.concatenate(matches,axis=0)

  return matches, np.array(unmatched_detections), np.array(unmatched_trackers)

KalmanBoxTracker

此类表示观测目标框所对应跟踪对象的内部状态。
定义等速模型。
内部使用 KalmanFilter,7个状态变量,4个观测输入。
F是状态变换模型,H是观测函数,R为测量噪声矩阵,P为协方差矩阵,Q为过程噪声矩阵。
状态转移矩阵A根据运动学公式确定
x = [ u , v , s , r , u ˙ , v ˙ , s ˙ ] T , \mathbf{x} = [u,v,s,r,\dot{u},\dot{v},\dot{s}]^T, x=[u,v,s,r,u˙,v˙,s˙]T,
F = [ 1 0 0 0 Δ u 0 0 0 1 0 0 0 Δ v 0 0 0 1 0 0 0 Δ s 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 ] F=\begin{bmatrix} 1 &amp; 0 &amp; 0 &amp; 0 &amp; \Delta u &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; \Delta v &amp; 0 \\ 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; \Delta s \\ 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 \end{bmatrix} F=1000000010000000100000001000Δu0001000Δv0001000Δs0001

H = [ 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 ] H=\begin{bmatrix} 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1 &amp; 0 &amp; 0 &amp; 0 \end{bmatrix} H=1000010000100001000000000000

  count = 0
  def __init__(self,bbox):
    """ Initialises a tracker using initial bounding box. """
    #define constant velocity model
    self.kf = KalmanFilter(dim_x=7, dim_z=4)
    self.kf.F = np.array([
    [1,0,0,0,1,0,0],
    [0,1,0,0,0,1,0],
    [0,0,1,0,0,0,1],
    [0,0,0,1,0,0,0],  
    [0,0,0,0,1,0,0],
    [0,0,0,0,0,1,0],
    [0,0,0,0,0,0,1]])
    self.kf.H = np.array([
    [1,0,0,0,0,0,0],
    [0,1,0,0,0,0,0],
    [0,0,1,0,0,0,0],
    [0,0,0,1,0,0,0]])

    self.kf.R[2:,2:] *= 10.
    self.kf.P[4:,4:] *= 1000. #give high uncertainty to the unobservable initial velocities
    self.kf.P *= 10.
    self.kf.Q[-1,-1] *= 0.01
    self.kf.Q[4:,4:] *= 0.01

    self.kf.x[:4] = convert_bbox_to_z(bbox)
    self.time_since_update = 0
    self.id = KalmanBoxTracker.count
    KalmanBoxTracker.count += 1
    self.history = []
    self.hits = 0
    self.hit_streak = 0
    self.age = 0

update

使用观察到的目标框更新状态向量。filterpy.kalman.KalmanFilter.update 会根据观测修改内部状态估计self.kf.x
重置self.time_since_update,清空self.history

    self.time_since_update = 0
    self.history = []
    self.hits += 1
    self.hit_streak += 1
    self.kf.update(convert_bbox_to_z(bbox))

predict

推进状态向量并返回预测的边界框估计。
将预测结果追加到self.history。由于 get_state 直接访问 self.kf.x,所以self.history没有用到。

    if((self.kf.x[6]+self.kf.x[2])<=0):
      self.kf.x[6] *= 0.0
    self.kf.predict()
    self.age += 1
    if(self.time_since_update>0):
      self.hit_streak = 0
    self.time_since_update += 1
    self.history.append(convert_x_to_bbox(self.kf.x))
    return self.history[-1]

get_state

get_state

convert_x_to_bbox

返回当前边界框估计值。

    return convert_x_to_bbox(self.kf.x)

iou

@numba.jit 即时编译修饰函数以生成高效的机器代码。所有参数都是可选的。

@jit
def iou(bb_test,bb_gt):
  """ Computes IUO between two bboxes in the form [x1,y1,x2,y2] """
  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)
  wh = w * h
  o = wh / ((bb_test[2]-bb_test[0])*(bb_test[3]-bb_test[1])
    + (bb_gt[2]-bb_gt[0])*(bb_gt[3]-bb_gt[1]) - wh)
  return(o)

convert_bbox_to_z

[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r]。其中xy是框的中心,s是比例/区域,r是宽高比。

  w = bbox[2]-bbox[0]
  h = bbox[3]-bbox[1]
  x = bbox[0]+w/2.
  y = bbox[1]+h/2.
  s = w*h    #scale is just area
  r = w/float(h)
  return np.array([x,y,s,r]).reshape((4,1))

convert_x_to_bbox

[cx,cy,s,r]的目标框表示转为[x_min,y_min,x_max,y_max]的形式。

  w = np.sqrt(x[2]*x[3])
  h = x[2]/w
  if(score==None):
    return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.]).reshape((1,4))
  else:
    return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.,score]).reshape((1,5))

改进思路

Sort 算法受限于在线的定位,直接忽略了所有目标的考察期输出。这未免有些因噎废食。对于目标的甄别期较短,可以考虑延时判断后再行输出。

参考资料:

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/203506.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • FPS游戏:实现GDI方框透视「建议收藏」

    FPS游戏:实现GDI方框透视「建议收藏」FPS游戏可以说一直都比较热门,典型的代表有反恐精英,穿越火线,绝地求生等,基本上只要是FPS游戏都会有透视挂的存在,而透视挂还分为很多种类型,常见的有D3D透视,方框透视,还有一些比较高端的显卡透视

  • 办公技巧 SecureCRTPortable如何设置自动保存日志[通俗易懂]

    办公技巧 SecureCRTPortable如何设置自动保存日志[通俗易懂]1.选项-全局选项-常规-默认回话-编辑默认设置-日志文件2.将日志文件名改为:路径+%H_%M%D_%h%m%s3.在连接时自动加入:[%Y%M%D_%h:%m:%s]4.在每行自动加入:[%h:%m:%s]5.保存,选择所有回话,即可在下一次连接时自动保存日志文件显示项可选:%H:主机名%S:串口名%Y:年%M:月%D:日%h:时%m:分%s:秒…

  • 二叉树的5个重要性质「建议收藏」

    二叉树的5个重要性质「建议收藏」1.在二叉树的第i层上最多有2 i-1 个节点。(i>=1) 用归纳法证明:归纳基:i=1层时,只有一个根结点,          2i-1=20=1;归纳假设:假设i=k时,命题成立;归纳证明:二叉树上每个结点至多有两棵子树,则第k+1层的结点数最多为2k-12=2k+1-1。

  • html生日快乐源代码「建议收藏」

    html生日快乐源代码「建议收藏」html生日快乐源代码源码下载》html生日快乐源代码.7z主页截图如下:默认的账号为root,密码为root如果想改的话,修改js文件夹下面verification.js的内容想要的,可以在此博客的最上面下载链接进行下载。…

  • 二进制与十进制的讲解与转换「建议收藏」

    二进制与十进制的讲解与转换「建议收藏」文章目录?什么是十进制呢??什么是二进制??十进制和二进制之间可以转换吗??十进制与二进制之间的转换?二进制转十进制?十进制转二进制?写在最后哈喽,大家好!这是我的第一篇博客,就先来介绍一下自己吧,我是Why,一名在读学生,目前刚刚开始进入自己的编程学习生涯。虽然学习起步较晚,但我坚信做了才有0或1的可能。学了一段时间以后也是选择在CSDN上分享自己的日常笔记,也希望能够在众多道友的大家庭中打成一片。本文主要讲解二进制以及进制之间的转换,如果大家读后觉得有用的话,还请大家多多支持博主:欢迎❤️点赞

    2022年10月18日
  • 序列化模块、导入模块

    序列化模块、导入模块

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号