先解读Sort算法:Simple online and realtime tracking
论文地址 https://arxiv.org/abs/1602.00763
代码地址
https://github.com/abewley/sort
https://github.com/YunYang1994/openwork/tree/master/sort

SORT 流程简介:

整个流程如下图所示:在第 1 帧时,人体检测器 detector 输出 3 个 bbox(黑色),模型会分别为这 3 个 bbox 创建卡尔曼滤波追踪器【tracker】 kf1,kf2 和 kf3。所以注意第一帧的追踪器是用目标检测的框创建的,ID也是我们手动赋予的。
对应人的编号为 1,2,3 。在第 2 帧的过程 a 中,如下图frame-2a,这 3 个跟踪器【每个人都有一个tracker】会利用上一帧的状态分别输出棕红色 bbox、黄色 bbox 和 青绿色 bbox。

由于frame1中三个黑色bbox只是目标检测模型的输出,它们是没有bbox id的,所以,我们需要把目标检测框,和卡尔曼滤波的预测框进行一种关联,使得目标检测框的id就是滤波器的预测框id。
在SORT算法中,关联的核心是目标检测框和滤波器预测框之间的iou + 匈牙利算法的匹配。
我们计算一下Frame1 和 Frame2-a框之间的iou 如下:

iou 黑色bbox1 黑色bbox2 黑色bbox3
棕红色 bbox 0.91 0 0
黄色 bbox 0 0.98 0
青绿色 bbox 0 0 0.99

上述表格可以抽象为一个矩阵。矩阵中每一个值都是目标检测框到滤波器预测框之间的iou 。
我们若要求使得这个iou矩阵的值最大,那么这个矩阵就被称为利益矩阵profit matrix,若要使之最小,则被称为花费矩阵cost matrix。我们如果想让跟踪状态完美,就要求得这个iou矩阵的最大利益矩阵。
而SORT算法对iou值进行了一个变换,将iou值都变成1-iou,求的是1-iou矩阵的最小值。
这里的1-iou被定义为:目标检测框到滤波器预测框之间的iou距离。并且使用匈牙利算法进行最小化求解,这里用1-iou 或者-iou 都可以算法复杂度为O(n^3)。例子如下:

import numpy as np
from scipy.optimize import linear_sum_assignmentcost_matrix = np.array([[0.09, 1.00, 1.00],[1.00, 0.02, 1.00],[1.00, 1.00, 0.01]])rows, cols = linear_sum_assignment(cost_matrix)
matches = list(zip(rows, cols))        # [(0, 0), (1, 1), (2, 2)] 得到匹配队列

这样我们就根据iou的值得到了匹配队列。

图解1:

图解2

KalmanBoxTracker

注意对应代码一起看

2.1卡尔曼滤波参数

状态变量 x 的设定是一个 7维向量:x=[u, v, s, r, u^, v^, s^]T。u、v
分别表示目标框的中心点位置的 x、y 坐标,s 表示目标框的面积,r 表示目标框的宽高比。 u^ ,v^ ,s^ 分别表示横向、纵向 、面积 s 的运动变化速率

参数初始化
u、v、s、r 初始化:根据第一帧的观测结果进行初始化。
u^ ,v^ ,s^ 初始化:当第一帧开始的时候初始化为0,到后面帧时会根据预测的结果来进行变化。

转换函数1:
def convert_bbox_to_z(bbox): 将 bbox 从 [x1,y1,x2,y2] 格式变成 [x,y,s,r] 4x1格式
转换函数2:
def convert_x_to_bbox(x,score=None): 将 bbox 从 [x,y,s,r] 格式变成 [x1,y1,x2,y2]格式,score是optional项
定义如下:

def convert_bbox_to_z(bbox):            """将 bbox 从 [x1,y1,x2,y2] 格式变成 [u,v,s,r] 格式,s是框的面积,r是w/h比例"""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 arear = w / float(h)return np.array([x, y, s, r]).reshape((4, 1))def convert_x_to_bbox(x,score=None):     """将 bbox 从 [u,v,s,r] 格式变成 [x1,y1,x2,y2] 格式"""s = x[2] #w*hratio = x[3] # w/hw = np.sqrt(s * r)h = s / 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))

状态转移矩阵 F
F被定义为一个7x7的矩阵,跟踪的目标被一个匀速运动目标,通过 7x7 的状态转移矩阵F 乘以 7*1 的状态变量 x 即可得到一个更新后的 7x1 的状态更新向量x
观测矩阵H
H被定义为一个 4x7 的矩阵,乘以 7x1 的状态更新向量 x 即可得到一个 4x1 的 [u,v,s,r] 的估计值。
协方差矩阵 RPQ
测量噪声的协方差矩阵 R: diag([1,1,10,10].T)
先验估计的协方差矩阵 P:diag([10,10,10,10,1e4,1e4,1e4].T)
过程激励噪声的协方差矩阵 Q:diag([1,1,1,1,0.01,0.01,1e-4].T)

hits = 总的匹配次数
hit_streak 连续匹配次数
time_since_update 连续没有匹配到目标检测框的次数
id = KalmanBoxTracker.count 记录当前追踪器的id

2.2 predict 追踪器预测阶段

在预测阶段,追踪器不仅需要预测 bbox,还要记录它自己的当前匹配情况。如果这个追踪器连续多次预测而没有进行一次更新操作,那么表明该跟踪器可能已经“失活”了。因为它没有和检测框匹配上,说明它之前记录的目标有可能已经消失或者误匹配了。但是也不一定会发生这种情况,还一种结果是目标在连续几帧消失后又出现在画面里。
考虑到这种情况,使用 time_since_update 记录了追踪器连续没有匹配上的次数,该变量在每次 predict 时都会加 1,每次 update 时都会归 0。并且使用了 max_age 设置了追踪器的最大存活期限,如果跟踪器出现超过连续 max_age 帧都没有匹配关联上,
即当 tracker.time_since_update > max_age 时,该跟踪器则会被判定失活而被移除列表

2.3 update 更新阶段

大家都知道,卡尔曼滤波器的更新阶段是使用了观测值 z 来校正误差矩阵和更新卡尔曼增益,并计算出先验估计值和测量值之间的加权结果,该加权结果即为后验估计值。

class KalmanBoxTracker(object):"""This class represents the internal state of individual tracked objects observed as bbox."""count = 0def __init__(self,bbox):"""用初始目标检测框来初始化追踪器"""#定义匀速运动模型self.kf = KalmanFilter(dim_x=7, dim_z=4) #状态变量是7维, 观测值是4维的,按照需要的维度构建目标# 状态变量x的定义见下文self.kf.F = np.array([[1,0,0,0,1,0,0],      # 状态转移矩阵 7x7 维度[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],      # 观测矩阵,4x7 维度[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 velocitiesself.kf.P *= 10.self.kf.Q[-1,-1] *= 0.01self.kf.Q[4:,4:] *= 0.01self.kf.x[:4] = convert_bbox_to_z(bbox)self.time_since_update = 0self.id = KalmanBoxTracker.countKalmanBoxTracker.count += 1self.history = [] # 存放着多个  [x1,y1,x2,y2]self.hits = 0 # 总的匹配次数self.hit_streak = 0 # 连续匹配次数self.age = 0def update(self,bbox):"""用检测框更新追踪框"""self.time_since_update = 0self.history = []self.hits += 1self.hit_streak += 1self.kf.update(convert_bbox_to_z(bbox))  # bbox 是观测值 [x1,y1,x2,y2] --> [u,v,s,r]def predict(self):"""Advances the state vector and returns the predicted bounding box estimate."""if((self.kf.x[6]+self.kf.x[2])<=0):self.kf.x[6] *= 0.0self.kf.predict()self.age += 1if(self.time_since_update>0): # 一旦出现不匹配的情况,连续匹配次数归0self.hit_streak = 0self.time_since_update += 1 # 否则连续匹配次数+1self.history.append(convert_x_to_bbox(self.kf.x)) #  # [u,v,s,r] --> [x1,y1,x2,y2]return self.history[-1]def get_state(self):"""Returns the current bounding box estimate."""return convert_x_to_bbox(self.kf.x)

3. bbox 关联匹配

bbox 的关联匹配过程在前面已经讲得很详细了,它是将 tracker 输出的预测框(注意是先验估计值)和 detector 输出的检测框相关联匹配起来。输入是 dets: [[x1,y1,x2,y2,score],…] 和 trks: [[x1,y1,x2,y2,tracking_id],…] 以及一个设定的 iou 阈值,该门槛是为了过滤掉那些低重合度的目标。
代码中linear assigment使用的匈牙利算法我们在本文最后面详细介绍。

def associate_detections_to_trackers(dets, trks, iou_threshold = 0.3):"""Assigns detections to tracked object (both represented as bounding boxes)dets:[[x1,y1,x2,y2,score],...]trks:[[x1,y1,x2,y2,tracking_id],...]Returns 3 lists of matches, unmatched_detections and unmatched_trackers"""

该过程返回三个列表:
matches(已经匹配成功的追踪器),
unmatched_detections(没有匹配成功的检测目标)
unmatched_trackers(没有匹配成功的跟踪器)

对于已经匹配成功的追踪器,则需要用观测值(目标检测框)去更新校正 tracker 并输出修正后的 bbox对于没有匹配成功的检测目标,则需要新增 tracker 与之对应
对于没有匹配成功的跟踪器,如果长时间处于失活状态,则可以考虑删除了。
所以整理多目标跟踪MOT算法的流程如下:

看完上图流程图,我们来仔细看看objec detection box和 Kalman Filter tracker之间关联的具体逻辑:

def associate_detections_to_trackers(detections,trackers,iou_threshold = 0.3):  """detections[ x1, y1 ,x2, y2, score, ... ]  Nx5trackers [:, x1, y1, x2, y3, tracker_id, ... ] Mx5图中IOU Match版块用于将检测与跟踪进行关联将目标检测框匹配到滤波器预测框tracker。返回 三个列表 matches, unmatched_detections and unmatched_trackers"""if(len(trackers)==0):  #如果跟踪器为空return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)#初始化检测器与跟踪器IOU Matrixiou_matrix = np.zeros((len(detections),len(trackers)),dtype=np.float32) #计算det 和 tracker 之间的iou matrixfor d, det in enumerate(detections):for t, trk in enumerate(trackers):iou_matrix[d,t] = iou(det,trk)  #计算检测器与跟踪器的IOU并赋值给 iou matrix 对应位置#最终iou_matrix的shape是NxMmatched_indices = linear_assignment(-iou_matrix)  #只是粗匹配,后面还要过滤iou低的# 这里的linear assignment使用的就是匈牙利算法# 加上负号是因为linear_assignment求的是最小代价组合,而我们需要的是IOU最大的组合方式,所以取负号 # 参考的是:https://blog.csdn.net/herr_kun/article/details/86509591    unmatched_detections = []    #未匹配上的检测器for d, det in enumerate(detections):if(d not in matched_indices[:, 0]):  #如果检测器中第d个检测结果不在匹配结果索引中,则d未匹配上unmatched_detections.append(d)unmatched_trackers = []      #未匹配上的跟踪器for t, trk in enumerate(trackers):if(t not in matched_indices[:,1]):  #如果跟踪器中第t个跟踪结果不在匹配结果索引中,则t未匹配上unmatched_trackers.append(t)# filter out matched pair with low IOU,过滤掉那些IOU较小的匹配对matches = []  #存放过滤掉低iou之后的最终匹配结果for m in matched_indices:   #遍历粗匹配结果if(iou_matrix[m[0], m[1]] < iou_threshold):  # m[0]是检测框ID, m[1]是跟踪框ID,如果它们的IOU小于阈值则将它们视为未匹配成功unmatched_detections.append(m[0])unmatched_trackers.append(m[1])else:matches.append(m.reshape(1,2))         # 匹配上的则以 [[d,t]...] 形式放入 matches 矩阵if(len(matches)==0):           #如果过滤后匹配结果为空,那么返回空的匹配结果matches = np.empty((0,2),dtype=int)  else:      #如果过滤后匹配结果非空,则按0轴【纵向】继续添加匹配对matches = np.concatenate(matches,axis=0) # 返回:跟踪成功的矩阵,新增物体的矩阵, 消失物体的矩阵return matches, np.array(unmatched_detections), np.array(unmatched_trackers)  # matches有2列,第1列是检测框id,第2列是预测框id# unmatched_detections有5列,前4列是xyxy,第5列是score# 其中跟踪器数组是5列的(最后一列是ID)

将上述所有流程串联起来就得到了SORT算法

class Sort(object):def __init__(self, max_age=1, min_hits=3, iou_threshold=0.3):"""Sets key parameters for SORT"""self.max_age = max_age      # 在没有目标检测关联的情况下追踪器存活的最大帧数self.min_hits = min_hits    # 追踪器初始化前的最小关联检测数量self.iou_threshold = iou_thresholdself.trackers = []          # 用于存储卡尔曼滤波追踪器的列表self.frame_count = 0        # 当前追踪帧的编号def update(self, dets=np.empty((0, 5))):"""Params:输入的是目标检测矩阵,形式是[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]这个方法对每一帧都必须使用一次,即使该帧没有检测到任何目标。返回相似度矩阵,矩阵最后一列是追踪框IDNOTE:输入的检测框个数 和 返回的框个数,可能是不同的"""self.frame_count += 1 # 帧数+1trks = [] # 用于存放跟踪预测的 bbox: [x1,y1,x2,y2,id]# 初始化的时候self.trackers是空的,跳过下面的for循环for i, tracker in enumerate(self.trackers): # 遍历卡尔曼跟踪列表pos = tracker.predict()[0]   # 用卡尔曼跟踪器 trackers预测 bboxif not np.any(np.isnan(pos)):     # 如果卡尔曼的预测框有效trks.append([pos[0], pos[1], pos[2], pos[3], 0])   # 存放上一帧所有物体预测有效的 bboxelse:self.trackers.remove(tracker)    # 如果无效, 删除该滤波器trks = np.array(trks)self.trks = trks     # 为了显示跟踪器预测的框,把它拿出来# 将目标检测的 bbox 和卡尔曼滤波预测的跟踪 bbox 匹配# 获得 跟踪成功的矩阵,新增物体的矩阵,消失物体的矩阵matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks, self.iou_threshold)# 跟踪成功的物体 bbox 信息更新到对应的卡尔曼滤波器for m in matched:self.trackers[m[1]].update(dets[m[0], :])# 为新增物体创建新的卡尔曼滤波跟踪器for i in unmatched_dets:tracker = KalmanBoxTracker(dets[i,:])self.trackers.append(tracker)# 跟踪器更新校正后,输出最新的 bbox 和 idret = []for tracker in self.trackers:if (tracker.time_since_update < 1) and (tracker.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):d = tracker.get_state()[0]ret.append([d[0], d[1], d[2], d[3], tracker.id+1]) # +1 as MOT benchmark requires positive# 长时间离开画面/跟踪失败的物体从卡尔曼跟踪器列表中删除if(tracker.time_since_update > self.max_age):self.trackers.remove(tracker)# 返回当前画面中所有被跟踪物体的 bbox 和 id,矩阵形式为 [[x1,y1,x2,y2,id]...]return np.array(ret) if len(ret) > 0 else np.empty((0,5))

目标跟踪中的卡尔曼滤波和匈牙利算法解读。相关推荐

  1. 交互式多模型-扩展卡尔曼滤波IMM-EKF——机动目标跟踪中的应用

    交互式多模型-扩展卡尔曼滤波IMM-EKF--机动目标跟踪中的应用 原创不易,路过的各位大佬请点个赞 针对机动目标跟踪的探讨.技术支持欢迎联系,也可以站内私信 WX: ZB823618313 机动目标 ...

  2. 目标跟踪--卡尔曼滤波 与 匈牙利算法

    目前主流的目标跟踪算法都是基于Tracking-by-Detecton策略,即基于目标检测的结果来进行目标跟踪. 跟踪结果中,每个bbox左上角的数字是用来标识某个人的唯一ID号.那么问题就来了,视频 ...

  3. 无迹卡尔曼滤波UKF—目标跟踪中的应用(算法部分)

    无迹卡尔曼滤波UKF-目标跟踪中的应用(算法部分) 原创不易,路过的各位大佬请点个赞 机动目标跟踪/非线性滤波/传感器融合/导航等探讨代码联系WX: ZB823618313 仿真部分见博客: [无迹卡 ...

  4. 容积卡尔曼滤波CKF—目标跟踪中的应用(算法部分—I)

    容积卡尔曼滤波CKF-目标跟踪中的应用(算法部分) 原创不易,路过的各位大佬请点个赞 机动目标跟踪/非线性滤波/传感器融合/导航等探讨代码联系WX: ZB823618313 作者:823618313@ ...

  5. 无迹卡尔曼滤波UKF—目标跟踪中的应用(仿真部分)

    无迹卡尔曼滤波UKF-目标跟踪中的应用(仿真部分) 原创不易,路过的各位大佬请点个赞 机动目标跟踪/非线性滤波/传感器融合/导航等探讨联系WX: ZB823618313 算法部分见博客: [无迹卡尔曼 ...

  6. 交互式多模型算法IMM——机动目标跟踪中的应用

    机动目标跟踪--交互式多模型算法IMM 原创不易,路过的各位大佬请点个赞 WX: ZB823618313 机动目标跟踪--交互式多模型算法IMM 机动目标跟踪--交互式多模型算法IMM 1. 对机动目 ...

  7. 容积卡尔曼滤波CKF—目标跟踪中的应用(仿真部分—II)

    容积卡尔曼滤波CKF-目标跟踪中的应用(算法部分-II) 原创不易,路过的各位大佬请点个赞 机动目标跟踪/非线性滤波/传感器融合/导航等探讨联系WX: ZB823618313 作者:823618313 ...

  8. 车流量检测实现:多目标追踪、卡尔曼滤波器、匈牙利算法、SORT/DeepSORT、yoloV3、虚拟线圈法、交并比IOU计算

    日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) CNN:RCNN.SPPNet.Fast RCNN.Faste ...

  9. 多目标跟踪卡尔曼滤波和匈牙利算法

    多目标跟踪关联匹配算法(匈牙利算法和KM算法原理讲解和代码实现) 目录 多目标跟踪关联匹配算法 多目标跟踪关联匹配算法(匈牙利算法和KM算法原理讲解和代码实现) 0.多目标跟踪算法流程 1.卡尔曼滤波 ...

最新文章

  1. pg_dump 详解/使用举例
  2. div块内的CSS中心文本(水平和垂直)
  3. 浙江大学远程教育学院计算机考试,浙江大学远程教育学院机试题 Windows操作题(共15分,若有做错请酌情 ....docx...
  4. 关于嵌入式系统内存地址空间的一些疑问(.text、.data、.bass、堆\栈空间)
  5. springmvc 中@Controller和@RestController的区别
  6. linux那些事之page fault(AMD64架构)(user space)(2)
  7. 2月25日 局域不变特征的目标跟踪,SURF算子,KLT算子
  8. java 多线程局域网快速传输文件,java大文件复制最高效方法多线程FileChannel
  9. linux系统 32位 64位 jdk下载 jdk6 jdk7
  10. ZYNQ系统中实现FAT32文件系统的SD卡读写 之一 硬件介绍
  11. python 图片文字化处理_Python图像处理之图片文字识别功能(OCR)
  12. 英语3500词(20/20)dream主题(2022.4.30)
  13. python web面试题部分汇总
  14. 433模块 防冲撞 解决多发一收 mesh自组网 方案实现
  15. 服务器接显示器卡顿,外接屏幕会出现卡顿、掉帧等问题怎么解决?
  16. cholesky 分解加速求解线性方程组
  17. ESP32自动更新气象站
  18. 一键式打造DAO,M-DAO或成Web3新宠儿
  19. PHP代码从数据库中获取数据
  20. Latex 强制文本两端左右对齐

热门文章

  1. 机械师f117笔记本重装Win11系统教程
  2. 关于小程序支付后的强制关注公众号问题总结
  3. 电子学堂YY语音群课 加入方法
  4. 常用的字符串截取方法
  5. 2021中国大学生程序设计竞赛(CCPC),烤仔与你不见不散!
  6. 地质员小哥转行IT:人生若波澜,世路有屈曲
  7. 三星Galaxy手机配备6800mAh电池
  8. 达梦数据库常用sql语句大全
  9. 如何用手机备忘录扫描图片文件并保存到相册
  10. 13-实现视频投稿、分页带条件查询视频列表、在线观看视频(分片下载)