目录
- 1. 作者介绍
- 2. 卡尔曼滤波器
- 2.1 卡尔曼滤波概述
- 2.2 标志性发展
- 2.3 卡尔曼公式理解
- 3. 车流量检测
- 3.1 背景介绍
- 3.2 实现过程
- 3.2.1 YOLOv3网络模型结构
- 3.2.2 SORT算法
- 3.2.3 基于虚拟线圈法的车辆统计
- 4. 算法实现
- 4.1 Kalman.py
- 4.2 完整代码
- 4.3 结果展示
1. 作者介绍
吴思雨,女,西安工程大学电子信息学院,2023级研究生,张宏伟人工智能课题组。
研究方向:机器视觉与人工智能
电子邮件:2879944563@qq.com
2. 卡尔曼滤波器
2.1 卡尔曼滤波概述
卡尔曼滤波(Kalman Filtering)是一种利用线性系统状态方程,通过系统输入观测数据,对系统状态进行最优估计的算法。由于观测数据中包括系统中的噪声和干扰的影响,所以最优估计也可看作是滤波过程。Kalman滤波在测量方差已知的情况下能够从一系列存在测量噪声的数据中,估计动态系统的状态。常在控制、制导、导航、通讯等领域使用。目前已经发展了很多变体,扩展到更多领域,如计算机视觉。
2.2 标志性发展
扩展卡尔曼滤波器(Extended Kalman Filter,EKF):扩展卡尔曼滤波器是卡尔曼滤波器的非线性扩展。它通过在状态预测和状态更新中使用线性化的近似模型,来处理非线性系统。EKF广泛应用于机器人定位、目标跟踪等领域。
无迹卡尔曼滤波器(Unscented Kalman Filter,UKF):无迹卡尔曼滤波器是对EKF的改进,通过使用无迹变换来更准确地估计非线性系统的状态。UKF通过选择一组特定的采样点来近似非线性函数的均值和协方差,从而避免了线性化带来的误差。
平滑卡尔曼滤波器(Kalman Smoother):平滑卡尔曼滤波器是一种在已有测量数据的情况下,对过去的状态进行重新估计的方法。它通过使用后向递推的方法,结合过去的状态估计。
非线性卡尔曼滤波器(Nonlinear Kalman Filter):非线性卡尔曼滤波器是一类用于处理非线性系统的滤波器。除了EKF和UKF之外,还有一些其他的非线性卡尔曼滤波器,如粒子滤波器(Particle Filter)和拟线性滤波器(Quasi-Linear Filter)等。
这些卡尔曼滤波器的变体和改进算法,都是为了更好地处理非线性系统、提高估计的精度和稳定性而设计的。
2.3 卡尔曼公式理解
实现过程:使用上一时刻的最优结果预测这一时刻的预测值,同时使用这一时刻观测值(传感器测得的数据)修正这一时刻预测值,得到这一时刻的最优结果。
注:当状态值是一维的时候,H和I可以看作是1。
预测:
1.上一时刻的最优估计值,推出这一时刻的预测值:
2.上一时刻最优估计值方差/协方差和超参数Q推出这一时刻预测值方差/协方差:
深入理解,Q其实对应的是过程噪声的方差。
更新:
1.这一时刻预测值方差/协方差和超参数R推出卡尔曼增益Kt:
2.这一时刻预测值、这一时刻观测值、卡尔曼增益推出这一时刻最优估计值:
Zt为这一时刻观测值。
3.这一时刻预测值方差/协方差、卡尔曼增益推出这一时刻最优估计值方差/协方差:
若想了解更多关于卡尔曼滤波的知识,可以阅读点击下面的链接:
链接: http://t.csdn.cn/UiuvS
链接: http://t.csdn.cn/YH5D8
3. 车流量检测
3.1 背景介绍
卡尔曼滤波(Kalman)无论是在单目标还是多目标领域都是很常用的一种算法,将卡尔曼滤波看做一种运动模型,用来对目标的位置进行预测,并且利用预测结果对跟踪的目标进行修正,属于自动控制理论中的一种方法。
在对视频中的目标车辆进行跟踪时,当目标运动速度较慢时,很容易将前后两帧的目标进行关联,如下图所示:
如果目标运动速度比较快,或者进行隔帧检测时,在后续帧中,目标A已运动到前一帧B所在的位置,这时再进行关联就会得到错误的结果,将A‘与B关联在一起。
那怎么才能避免这种出现关联误差?可以在进行目标关联之前,对目标在后续帧中出现的位置进行预测,然后与预测结果进行对比关联,如下图所示:
在对比关联之前,先预测出A和B在下一帧中的位置,然后再使用实际的检测位置与预测的位置进行对比关联,只要预测足够精确,几乎不会出现由于速度太快而存在的误差。
卡尔曼滤波就可以用来预测目标在后续帧中出现的位置,如下图所示,卡尔曼滤波器就可以根据前面五帧数据目标的位置,预测第6帧目标的位置。
卡尔曼滤波器最大的优点是采用递归的方法来解决线性滤波的问题,它只需要当前的测量值和前一个周期的预测值就能够进行状态估计。由于这种递归方法不需要大量的存储空间,每一步的计算量小,计算步骤清晰,非常适合计算机处理,因此卡尔曼滤波受到了普遍的欢迎,在各种领域具有广泛的应用前景。
3.2 实现过程
该项⽬对输⼊的视频进⾏处理,主要包括以下⼏个步骤:
1)使⽤YoloV3模型进⾏⽬标检测。
2)然后使⽤SORT算法进⾏⽬标追踪,使⽤卡尔曼滤波器进⾏⽬标位置预测,并利⽤匈⽛利算法对⽐⽬标的相似度,完成⻋辆⽬标追踪。
3)利⽤虚拟线圈的思想实现⻋辆⽬标的计数,完成⻋流量的统计。
流程如下图所示:
3.2.1 YOLOv3网络模型结构
YOLOv3是YOLO (You Only Look Once)系列⽬标检测算法中的第三版,相⽐之前
的算法,尤其是针对⼩⽬标,精度有显著提升。
YOLOv3的流程如下图所示,对于每⼀幅输入图像,YOLOv3会预测三个不同尺度的输出,目的是检测出不同大小的目标。
在基本的图像特征提取方面,YOLO3采用了Darknet-53的网络结构(含有53个卷
积层),它借鉴了残差网络ResNet的做法,在层之间设置了shortcut,来解决深层
网络梯度的问题,shortcut如下图所示:包含两个卷积层和⼀个shortcut connections。
YOLOv3的模型结构如下所示:
整个v3结构里面,没有池化层和全连接层,网络的下采样是通过设置卷积的stride为2来达到的,每当通过这个卷积层之后图像的尺寸就会减小到⼀半。残差模块中的1×,2×,8×,8× 等表示残差模块的个数。
3.2.2 SORT算法
SORT算法核心是卡尔曼滤波和匈⽛利匹配两个算法。流程图如下所示,可以看到整体可以拆分为两个部分,分别是匹配过程和卡尔曼预测加更新过程,都用·灰⾊框标出来了。
关键步骤:轨迹卡尔曼滤波预测→ 使⽤匈⽛利算法将预测后的tracks和当前帧中的detecions进⾏匹配(IOU匹配) → 卡尔曼滤波更新。
卡尔曼滤波分为两个过程:预测和更新。SORT引入了线性速度模型与卡尔曼滤波
来进行位置预测,先进行位置预测然后再进行匹配。运动模型的结果可以用来预测物体的位置。
匈⽛利算法解决的是⼀个分配问题,用IOU距离作为权重(也叫cost矩阵),并且
当IOU小于⼀定数值时,不认为是同⼀个目标,理论基础是视频中两帧之间物体移动不会过多。在代码中选取的阈值是0.3。scipy库的linear_sum_assignment都实现了这⼀算法,只需要输⼊cost_matrix即代价矩阵就能得到最优匹配。
3.2.3 基于虚拟线圈法的车辆统计
虚拟线圈车辆计数法的原理是在采集到的交通流视频中,在需要进行车辆计数的道路或路段上设置⼀条或⼀条以上的检测线对通过车辆进行检测,从而完成计数工作。检测线的设置原则⼀般是在检测车道上设置⼀条垂直于车道线,居中的虚拟线段,通过判断其与通过⻋辆的相对位置的变化,完成⻋流量统计的工作。如下图所示,绿⾊的线就是虚拟检测线:
在该项目中进行检测的方法是,计算前后两帧图像的车辆检测框的中心点连
线,若该连线与检测线相交,则计数加⼀,否则计数不变。
4. 算法实现
提供目标检测权重文件及结果视频,通过下述百度网盘链接自行提取!
链接: https://pan.baidu.com/s/1JepatBgrLPOI3Yj451KSDw
提取码:3de4
4.1 Kalman.py
在这里插入代码片# encoding:utf-8
from __future__ import print_function
# from numba import jit
import numpy as np
from scipy.optimize import linear_sum_assignment
from filterpy.kalman import KalmanFilter#计算IOU(交并比)
# @jit
def iou(bb_test,bb_gt):"""在两个box间计算IOU:param bb_test: box1 = [x1,y1,x2,y2] 左上角坐标:param bb_gt: box2 = [x1,y1,x2,y2] 右下角坐标:return: 交并比IOU"""#在两个box间的左上角坐标的最大值xx1 = np.maximum(bb_test[0],bb_gt[0])#左上角坐标x的最大值yy1 = np.maximum(bb_test[1],bb_gt[1])#左上角坐标y的最大值#在两个box间的右下角坐标的最小值xx2 = np.minimum(bb_test[2],bb_gt[2])#右下角坐标x的最小值yy2 = np.minimum(bb_test[3],bb_test[3])#右下角坐标y的最小值#交的宽高w = np.maximum(0,xx2-xx1)h = np.maximum(0,yy2-yy1)#交的面积wh = w*h#并的面积s = ((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)#计算IOU并且返回IOUo_rate = wh/sreturn o_rate#左上角坐标[x1,y1]和右下角坐标[x2,y2],
#将候选框从坐标形式[x1,y1,x2,y2]转换为中心点坐标和面积的形式[x,y,s,r]
#其中x,y是框的中心坐标,s是面积,尺度,r是宽高比
def convert_bbox_to_z(bbox):"""将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r]。其中x,y是框的中心坐标,s是面积,尺度,r是宽高比:param bbox: [x1,y1,x2,y2] 分别是左上角坐标和右下角坐标:return: [ x, y, s, r ] 4行1列,其中x,y是box中心位置的坐标,s是面积,r是纵横比w/h"""w = bbox[2] - bbox[0]#宽 x2-x1:#右下角的x坐标 - 左上角的x坐标 = 检测框的宽h = bbox[3] - bbox[1]#高 y2-y1:#右下角的y坐标 - 左上角的y坐标 = 检测框的高x = bbox[0] + w/2.0#检测框的中心坐标x: x1+(x2-x1)/2.0 #左上角的x坐标 + 宽/2 = 检测框中心位置的x坐标y = bbox[1] + h/2.0#检测框的中心坐标y: y1+(y2-y1)/2.0 #左上角的y坐标 + 高/2 = 检测框中心位置的y坐标s = w*h #检测框的面积 #检测框的宽 * 高 = 检测框面积r = w/float(h) #检测框的宽高比# 因为卡尔曼滤波器的输入格式要求为4行1列,因此该[x, y, s, r]的形状要转换为4行1列再输入到卡尔曼滤波器return np.array([x,y,s,r]).reshape([4,1]) #kalman需要四行一列的形式#将候选框从中心面积[x,y,s,r]的形式转换成左上角坐标和右下角坐标[x1,y1,x2,y2]的形式
#即:将[cx,cy,s,r]的目标框表示转为[x_min,y_min,x_max,y_max]的形式
def convert_x_to_bbox(x,score=None):"""将[cx,cy,s,r]的目标框表示转为[x_min,y_min,x_max,y_max]的形式:param x:[ x, y, s, r ],其中x,y是box中心位置的坐标,s是面积,r是纵横比w/h:param score: 置信度:return:[x1,y1,x2,y2],左上角坐标和右下角坐标""""""x[2]:s是面积,原公式s的来源为s = w * h,即检测框的宽 * 高 = 检测框面积。x[3]:r是纵横比w/h,原公式r的来源为r = w / float(h),即检测框的宽w / 高h = 宽高比。x[2] * x[3]:s*r 即(w * h) * (w / float(h)) = w^2sqrt(x[2] * x[3]):sqrt(w^2) = w"""w = np.sqrt(x[2] * x[3]) #w =sqrt(s*r)=sqrt(s*w/h)=sqrt(w*h * w/h)=sqrt(w*w)h = x[2]/w #h =s/w =w*h/w =hx1 = x[0]-w/2.0 #左上角x坐标:x1 = x-w/2.0 #检测框中心位置的x坐标 - 宽 / 2y1 = x[1]-h/2.0 #左上角y坐标:y1 = y-h/2.0 #检测框中心位置的y坐标 - 高 / 2x2 = x[0]+w/2.0 #右下角x坐标:x2 = x+w/2.0 #检测框中心位置的x坐标 + 宽 / 2y2 = x[1]+h/2.0 #右下角y坐标:y2 = y+h/2.0 #检测框中心位置的y坐标 + 高 / 2if score is None:return np.array([x1,y1,x2,y2]).reshape((1,4))else:return np.array([x1,x1,x2,y2,score]).reshape((1,5))"""
卡尔曼滤波器进行跟踪的相关内容的实现目标估计模型:1.根据上一帧的目标框结果来预测当前帧的目标框状态,预测边界框(目标框)的模型定义为一个等速运动/匀速运动模型。2.每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。 3.yoloV3、卡尔曼滤波器预测/更新流程步骤1.第一步:yoloV3目标检测阶段:--> 1.检测到目标则创建检测目标链/跟踪目标链,反之检测不到目标则重新循环目标检测。--> 2.检测目标链/跟踪目标链不为空则进入卡尔曼滤波器predict预测阶段,反之为空则重新循环目标检测。2.第二步:卡尔曼滤波器predict预测阶段:连续多次预测而不进行一次更新操作,那么代表了每次预测之后所进行的“预测目标和检测目标之间的”相似度匹配都不成功,所以才会出现连续多次的“预测然后相似度匹配失败的”情况,导致不会进入一次更新阶段。如果一次预测然后相似度匹配成功的话,那么然后就会进入更新阶段。--> 1.目标位置预测1.kf.predict():目标位置预测2.目标框预测总次数:age+=1。3.if time_since_update > 0:hit_streak = 0time_since_update += 11.连续预测的次数,每执行predict一次即进行time_since_update+=1。2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,即连续预测的过程中没有执行过一次update。4.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。--> 2.预测的目标和检测的目标之间的相似度匹配成功则进入update更新阶段,反之匹配失败则删除跟踪目标。3.第三步:卡尔曼滤波器update更新阶段:如果一次预测然后“预测目标和检测目标之间的”相似度匹配成功的话,那么然后就会进入更新阶段。kf.update([x,y,s,r]):使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新预测框。--> 1.目标位置信息更新到检测目标链/跟踪目标链 1.目标框更新总次数:hits+=1。2.history = []time_since_update = 0hit_streak += 11.history列表用于在预测阶段保存单个目标框连续预测的多个结果,一旦执行update就会清空history列表。2.连续更新的次数,每执行update一次即进行hit_streak+=1。3.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。4.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,即连续预测的过程中没有执行过一次update。5.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。--> 2.目标位置修正。1.kf.update([x,y,s,r]):使用观测到的目标框bbox更新状态变量x(状态更新向量x)。使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新卡尔曼滤波器得到的预测框。1.初始化、预测、更新1.__init__(bbox):初始化卡尔曼滤波器的状态更新向量x(状态变量x)、观测输入[x,y,s,r](通过[x1,y1,x2,y2]转化而来)、状态转移矩阵F、量测矩阵H(观测矩阵H)、测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q。2.update(bbox):根据观测输入来对状态更新向量x(状态变量x)进行更新3.predict():根据状态更新向量x(状态变量x)更新的结果来预测目标的边界框2.状态变量、状态转移矩阵F、量测矩阵H(观测矩阵H)、测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q1.状态更新向量x(状态变量x)状态更新向量x(状态变量x)的设定是一个7维向量:x=[u,v,s,r,u^,v^,s^]T。u、v分别表示目标框的中心点位置的x、y坐标,s表示目标框的面积,r表示目标框的纵横比/宽高比。u^、v^、s^分别表示横向u(x方向)、纵向v(y方向)、面积s的运动变化速率。u、v、s、r初始化:根据第一帧的观测结果进行初始化。u^、v^、s^初始化:当第一帧开始的时候初始化为0,到后面帧时会根据预测的结果来进行变化。2.状态转移矩阵F定义的是一个7*7的方阵(其对角线上的值都是1)。。运动形式和转换矩阵的确定都是基于匀速运动模型,状态转移矩阵F根据运动学公式确定,跟踪的目标假设为一个匀速运动的目标。通过7*7的状态转移矩阵F 乘以 7*1的状态更新向量x(状态变量x)即可得到一个更新后的7*1的状态更新向量x,其中更新后的u、v、s即为当前帧结果。3.量测矩阵H(观测矩阵H)量测矩阵H(观测矩阵H),定义的是一个4*7的矩阵。通过4*7的量测矩阵H(观测矩阵H) 乘以 7*1的状态更新向量x(状态变量x) 即可得到一个 4*1的[u,v,s,r]的估计值。4.测量噪声的协方差矩阵R、先验估计的协方差矩阵P、过程激励噪声的协方差矩阵Q1.测量噪声的协方差矩阵R:diag([1,1,10,10]T)2.先验估计的协方差矩阵P:diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。3.过程激励噪声的协方差矩阵Q:diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。4.1e数字的含义1e4:1x10的4次方1e-4:1x10的-4次方5.diag表示对角矩阵,写作为diag(a1,a2,...,an)的对角矩阵实际表示为主对角线上的值依次为a1,a2,...,an,而主对角线之外的元素皆为0的矩阵。对角矩阵(diagonal matrix)是一个主对角线之外的元素皆为0的矩阵,常写为diag(a1,a2,...,an) 。对角矩阵可以认为是矩阵中最简单的一种,值得一提的是:对角线上的元素可以为 0 或其他值,对角线上元素相等的对角矩阵称为数量矩阵;对角线上元素全为1的对角矩阵称为单位矩阵。对角矩阵的运算包括和、差运算、数乘运算、同阶对角阵的乘积运算,且结果仍为对角阵。
"""
"""
1.跟踪器链(列表):实际就是多个的卡尔曼滤波KalmanBoxTracker自定义类的实例对象组成的列表。每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。把每个卡尔曼滤波器(KalmanBoxTracker实例对象)都存储到跟踪器链(列表)中。
2.unmatched_detections(列表):1.检测框中出现新目标,但此时预测框(跟踪框)中仍不不存在该目标,那么就需要在创建新目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象),然后把新目标对应的KalmanBoxTracker类的实例对象放到跟踪器链(列表)中。2.同时如果因为“跟踪框和检测框之间的”两两组合的匹配度IOU值小于iou阈值,则也要把目标检测框放到unmatched_detections中。
3.unmatched_trackers(列表):1.当跟踪目标失败或目标离开了画面时,也即目标从检测框中消失了,就应把目标对应的跟踪框(预测框)从跟踪器链中删除。unmatched_trackers列表中保存的正是跟踪失败即离开画面的目标,但该目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象)此时仍然存在于跟踪器链(列表)中,因此就需要把该目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象)从跟踪器链(列表)中删除出去。2.同时如果因为“跟踪框和检测框之间的”两两组合的匹配度IOU值小于iou阈值,则也要把跟踪目标框放到unmatched_trackers中。
"""#卡尔曼滤波:对于目标框的状态进行预测
class KalmanBoxTracker(object):"""每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。"""#记录跟踪框的个数count = 0 #类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象"""__init__(bbox)使用目标框bbox为卡尔曼滤波的状态进行初始化。初始化时传入bbox,即根据观测到的检测框的结果来进行初始化。每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。1.kf = KalmanFilter(dim_x=7, dim_z=4)定义一个卡尔曼滤波器,利用这个卡尔曼滤波器对目标的状态进行估计。dim_x=7定义是一个7维的状态更新向量x(状态变量x):x=[u,v,s,r,u^,v^,s^]T。dim_z=4定义是一个4维的观测输入,即中心面积的形式[x,y,s,r],即[检测框中心位置的x坐标,y坐标,面积,宽高比]。2.kf.F = np.array(7*7的方阵)状态转移矩阵F,定义的是一个7*7的方阵其(对角线上的值都是1)。通过7*7的状态转移矩阵F 乘以 7*1的状态更新向量x(状态变量x)即可得到一个更新后的7*1的状态更新向量x,其中更新后的u、v、s即为当前帧结果。通过状态转移矩阵对当前的观测结果进行估计获得预测的结果,然后用当前的预测的结果来作为下一次估计预测的基础。3.kf.H = np.array(4*7的矩阵)量测矩阵H(观测矩阵H),定义的是一个4*7的矩阵。通过4*7的量测矩阵H(观测矩阵H) 乘以 7*1的状态更新向量x(状态变量x) 即可得到一个 4*1的[u,v,s,r]的估计值。4.相应的协方差参数的设定,根据经验值进行设定。1.R是测量噪声的协方差矩阵,即真实值与测量值差的协方差。R=diag([1,1,10,10]T)kf.R[2:, 2:] *= 10.2.P是先验估计的协方差矩阵diag([10,10,10,10,1e4,1e4,1e4]T)。1e4:1x10的4次方。kf.P[4:, 4:] *= 1000. # 设置了一个较大的值,给无法观测的初始速度带来很大的不确定性kf.P *= 10.3.Q是过程激励噪声的协方差矩阵diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-4:1x10的-4次方。kf.Q[-1, -1] *= 0.01kf.Q[4:, 4:] *= 0.015.kf.x[:4] = convert_bbox_to_z(bbox)convert_bbox_to_z负责将[x1,y1,x2,y2]形式的检测框bbox转为中心面积的形式[x,y,s,r]。状态更新向量x(状态变量x)设定是一个七维向量:x=[u,v,s,r,u^,v^,s^]T。x[:4]即表示 u、v、s、r初始化为第一帧bbox观测到的结果[x,y,s,r]。6.单个目标框对应的单个卡尔曼滤波器中的统计参数的更新每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。1.卡尔曼滤波器的个数有多少个目标框就有多少个卡尔曼滤波器,每个目标框都会有一个卡尔曼滤波器,即每个目标框都会有一个KalmanBoxTracker实例对象。count = 0:类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象。id = KalmanBoxTracker.count:卡尔曼滤波器的个数,即目标框的个数。KalmanBoxTracker.count += 1:每增加一个目标框,即增加一个KalmanBoxTracker实例对象(卡尔曼滤波器),那么类属性count+=1。2.统计一个目标框对应的卡尔曼滤波器中各参数统计的次数1.age = 0:该目标框进行预测的总次数。每执行predict一次,便age+=1。2.hits = 0:该目标框进行更新的总次数。每执行update一次,便hits+=1。3.time_since_update = 01.连续预测的次数,每执行predict一次即进行time_since_update+=1。2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,即连续预测的过程中没有执行过一次update。4.hit_streak = 01.连续更新的次数,每执行update一次即进行hit_streak+=1。2.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。7.history = []:保存单个目标框连续预测的多个结果到history列表中,一旦执行update就会清空history列表。将预测的候选框从中心面积的形式[x,y,s,r]转换为坐标的形式[x1,y1,x2,y2] 的bbox 再保存到 history列表中。"""# 内部使用KalmanFilter,7个状态变量和4个观测输入def __init__(self,bbox):"""初始化边界框和跟踪器:param bbox:"""#等速模型#卡尔曼滤波:状态转移矩阵:7,观测输入矩阵:4self.kf = KalmanFilter(dim_x=7,dim_z=4) #初始化卡尔曼滤波器# F:状态转移/状态变化矩阵 7*7 用当前的矩阵预测下一次的估计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]])#H:量测矩阵/观测矩阵:4*7self.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]])#R:测量噪声的协方差,即真实值与测量值差的协方差self.kf.R[2:,2:] *= 10#P:先验估计的协方差self.kf.P[4:,4:] *= 1000 #give high uncertainty to the unobservable initial velocities 对不可观测的初始速度给予高度不确定性self.kf.P *= 10#Q:过程激励噪声的的协方差self.kf.Q[-1,-1] *= 0.01self.kf.Q[4:, 4:] *= 0.01#X:观测结果、状态估计self.kf.x[:4] = convert_bbox_to_z(bbox)#参数的更新self.time_since_update = 0self.id = KalmanBoxTracker.countKalmanBoxTracker.count += 1self.history=[]self.hits = 0self.hit_streak = 0self.age = 0"""update(bbox):使用观测到的目标框bbox更新状态更新向量x(状态变量x)1.time_since_update = 01.连续预测的次数,每执行predict一次即进行time_since_update+=1。2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。2.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,即连续预测的过程中没有执行过一次update。2.history = [] 清空history列表。history列表保存的是单个目标框连续预测的多个结果([x,y,s,r]转换后的[x1,y1,x2,y2]),一旦执行update就会清空history列表。3.hits += 1:该目标框进行更新的总次数。每执行update一次,便hits+=1。4.hit_streak += 11.连续更新的次数,每执行update一次即进行hit_streak+=1。2.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。5.kf.update(convert_bbox_to_z(bbox))convert_bbox_to_z负责将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r],那么传入的为kf.update([x,y,s,r])。然后根据观测结果修改内部状态x(状态更新向量x)。使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新卡尔曼滤波器得到的预测框。"""#使用观测到的目标框更新状态变量def update(self,bbox):"""使用观察到的目标框更新状态向量。filterpy.kalman.KalmanFilter.update 会根据观测修改内部状态估计self.kf.x。重置self.time_since_update,清空self.history。:param bbox:目标框:return:"""#重置部分参数self.time_since_update = 0#清空self.history = []#hitsself.hits += 1self.hit_streak += 1#根据观测结果修改内部状态xself.kf.update(convert_bbox_to_z(bbox))"""predict:进行目标框的预测并返回预测的边界框结果1.if(kf.x[6] + kf.x[2]) <= 0:self.kf.x[6] *= 0.0状态更新向量x(状态变量x)为[u,v,s,r,u^,v^,s^]T,那么x[6]为s^,x[2]为s。如果x[6]+x[2]<= 0,那么x[6] *= 0.0,即把s^置为0.0。2.kf.predict()进行目标框的预测。3.age += 1该目标框进行预测的总次数。每执行predict一次,便age+=1。4.if time_since_update > 0:hit_streak = 0time_since_update += 11.连续预测的次数,每执行predict一次即进行time_since_update+=1。2.在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。3.在连续预测(连续执行predict)的过程中,只要连续预测的次数time_since_update大于0的话,就会把hit_streak(连续更新的次数)重置为0,表示连续预测的过程中没有出现过一次更新状态更新向量x(状态变量x)的操作,即连续预测的过程中没有执行过一次update。4.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。5.history.append(convert_x_to_bbox(kf.x))convert_x_to_bbox(kf.x):将目标框所预测的结果从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2] 的bbox。history列表保存的是单个目标框连续预测的多个结果([x,y,s,r]转换后的[x1,y1,x2,y2]),一旦执行update就会清空history列表。6.predict 返回值:history[-1]把目标框当前该次的预测的结果([x,y,s,r]转换后的[x1,y1,x2,y2])进行返回输出。"""#进行目标框的预测:推进状态变量并返回预测的边界框结果def predict(self):"""推进状态向量并返回预测的边界框估计。将预测结果追加到self.history。由于 get_state 直接访问 self.kf.x,所以self.history没有用到:return:"""#状态变量if(self.kf.x[6] + self.kf.x[2]) <= 0:self.kf.x[6] *= 0# 进行预测self.kf.predict()#卡尔曼滤波的预测次数self.age += 1#若过程中未进行更新,则将hit_streak置为0if self.time_since_update > 0:self.hit_streak=0self.time_since_update += 1#将预测结果追加到hietory中self.history.append(convert_x_to_bbox(self.kf.x))return self.history[-1]"""get_state():获取当前目标框预测的结果([x,y,s,r]转换后的[x1,y1,x2,y2])。return convert_x_to_bbox(kf.x):将候选框从中心面积的形式[x,y,s,r] 转换为 坐标的形式[x1,y1,x2,y2] 的bbox并进行返回输出。直接访问 kf.x并进行返回,所以history没有用到。"""#获取到当前的边界框的预测结果def get_state(self):"""返回当前边界框估计值:return:"""return convert_x_to_bbox(self.kf.x)# 将YOLO模型的检测框和卡尔曼滤波的跟踪框进行匹配def associate_detections_to_trackers(detections,trackers,iou_threshold=0.3):"""将检测框bbox与卡尔曼滤波器的跟踪框进行关联匹配:param detections:检测框:param trackers:跟踪框,即跟踪目标:param iou_threshold:IOU阈值:return:跟踪成功目标的矩阵:matchs新增目标的矩阵:unmatched_detections跟踪失败即离开画面的目标矩阵:unmatched_trackers"""#跟踪/检测为0时:直接构造返回结果if (len(trackers) == 0) or (len(detections) == 0):return np.empty((0, 2), dtype=int), np.arange(len(detections)), np.empty((0, 5), dtype=int)# 跟踪/检测不为0时:# iou 不支持数组计算,故IOU 逐个进行交并比计算,构造矩阵scipy.linear_assignment进行匹配iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32)# 遍历目标检测的bbox集合,每个检测框的标识为dfor d,det in enumerate(detections):# 遍历跟踪框(卡尔曼滤波器预测)bbox集合,每个跟踪框标识为tfor t,trk in enumerate(trackers):iou_matrix[d,t] = iou(det,trk)#通过匈牙利算法(linear_assignment)将跟踪框和检测框以[[d,t]...]的二维矩阵的形式存储在match_indices中result = linear_sum_assignment(-iou_matrix)#将匹配结果以 [[d,t]]的形式存储匹配结果matched_indices = np.array(list(zip(*result)))#记录未匹配的检测框及跟踪框#未匹配的检测框放入unmatched_detections中,表示有新的目标进入画面,要新增所要跟踪的目标序列unmatched_detections = []for d,det in enumerate(detections):if d not in matched_indices[:,0]:unmatched_detections.append(d)#未匹配的跟踪框放入unmatched_trackers中,表示目标离开之前的画面,应删除对应的跟踪器unmatched_trackers = []for t,trk in enumerate(trackers):if t not in matched_indices[:,1]:unmatched_trackers.append(t)#将匹配成功的跟踪框放入matches中进行存储matches = []for m in matched_indices:# 过滤掉IOU低的匹配,将其放入到unmatched_detections和unmatched_trackersif 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))#格式转换:初始化matchs,以np.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)"""
利用sort算法完成多目标追踪在这里我们主要实现了一个多目标跟踪器,管理多个卡尔曼滤波器对象,主要包括以下内容:1.初始化:最大检测数,目标未被检测的最大帧数2.目标跟踪结果的更新,即跟踪成功和失败的目标的更新该方法实现了SORT算法,输入是当前帧中所有物体的检测框的集合,包括目标的score,输出是当前帧的跟踪框集合,包括目标的跟踪的id要求是即使检测框为空,也必须对每一帧调用此方法,返回一个类似的输出数组,最后一列是目标对像的id。需要注意的是,返回的目标对象数量可能与检测框的数量不同。
"""# 1.SORT目标跟踪:
# 1.第一帧刚开始时:对第一帧所有的检测框生成对应的新跟踪框。
# 2.第二帧开始到以后所有帧:
# 上一帧成功跟踪并且保留下来的的跟踪框 在当前帧中 进行新一轮的预测新的跟踪框,
# 并且针对所预测的新跟踪框和当前帧中的检测框进行iou计算和使用匈牙利算法对该两者进行关联匹配,
# 通过上述操作后成功返回跟踪目标成功的跟踪框(即和当前帧中的目标检测框相匹配的跟踪框),
# 并且另外发现了新出现目标的检测框、跟踪目标失败的跟踪框(即目标离开了画面/两者匹配度IOU值小于iou阈值),
# 那么首先使用当前帧中的检测框对“成功关联匹配的跟踪框中的”状态向量进行更新,
# 然后对新增目标的检测框生成对应新的跟踪框,最后把跟踪目标失败的跟踪框从跟踪器链列表中移除出去。
# 2.传入的检测框dets:[检测框的左上角的x/y坐标, 检测框的右下角的x/y坐标, 检测框的预测类别的概率值]
# 3.返回值tracks:
# 当前帧中跟踪目标成功的跟踪框/预测框的集合,包含目标的跟踪的id(也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个)
# 第一种返回值方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度, trk.id] ...]
# 第二种返回值方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, trk.id] ...]
# d:[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]
# trk.id:卡尔曼滤波器的个数/目标框的个数,也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个。
#Sort多目标跟踪 管理多个卡尔曼滤波器
class Sort(object):"""Sort 是一个多目标跟踪器的管理类,管理多个 跟踪器链中的多个 KalmanBoxTracker 卡尔曼滤波对象"""#设置Sort算法的参数def __init__(self,max_age = 1,min_hits = 3):"""初始化:设置SORT算法的关键参数:param max_age: 最大检测数:目标未被检测到的帧数,超过之后会被删除:param min_hits: 目标命中的最小次数,小于该次数update函数不返回该目标的KalmanBoxTracker卡尔曼滤波对象""""""max_age:跟踪框的最大连续跟丢帧数。如果当前跟踪框连续N帧大于最大连续跟丢帧数的话,则从跟踪器链中删除该卡尔曼滤波对象的预测框(跟踪框)。min_hits:跟踪框连续成功跟踪到目标的最小次数(目标连续命中的最小次数),也即跟踪框至少需要连续min_hits次成功跟踪到目标。trackers:卡尔曼滤波跟踪器链,存储多个 KalmanBoxTracker 卡尔曼滤波对象frame_count:当前视频经过了多少帧的计数"""# 最大检测数:目标未被检测到的帧数,超过之后会被删self.max_age = max_age# 目标连续命中的最小次数,小于该次数update函数不返回该目标的KalmanBoxTracker卡尔曼滤波对象self.min_hits=min_hits# 卡尔曼滤波跟踪器链,存储多个 KalmanBoxTracker 卡尔曼滤波对象self.trackers = []#帧计数self.frame_count = 0"""update(dets):输入dets:当前帧中yolo所检测出的所有目标的检测框的集合,包含每个目标的score以[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]形式输入的numpy.arrayx1、y1 代表检测框的左上角坐标;x2、y2代表检测框的右上角坐标;score代表检测框对应预测类别的概率值。输出ret:当前帧中跟踪目标成功的跟踪框/预测框的集合,包含目标的跟踪的id(也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个)第一种返回值方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度, trk.id] ...]第二种返回值方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, trk.id] ...]d:[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]trk.id:卡尔曼滤波器的个数/目标框的个数,也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个。注意:即使检测框为空,也必须对每一帧调用此方法,返回一个类似的输出数组,最后一列是目标对像的id。返回的目标对象数量可能与检测框的数量不同。"""#更新数值def update(self,dets):"""该方法实现了SORT算法,输入是当前帧中所有物体的检测框的集合,包括目标的score,输出是当前帧目标的跟踪框集合,包括目标的跟踪的id要求是即使检测框为空,也必须对每一帧调用此方法,返回一个类似的输出数组,最后一列是目标对像的id注意:返回的目标对象数量可能与检测框的数量不同:param dets:以[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]形式输入的numpy.array:return:"""""" 每经过一帧,frame_count+=1"""self.frame_count +=1"""1.trackers:上一帧中的跟踪器链(列表),保存的是上一帧中成功跟踪目标的跟踪框,也即上一帧中成功跟踪目标的KalmanBoxTracker卡尔曼滤波对象。2.trks = np.zeros((len(trackers), 5))上一帧中的跟踪器链(列表)中的所有跟踪框(卡尔曼滤波对象)在当前帧中成功进行predict预测新跟踪框后返回的值。所有新跟踪框的左上角的x坐标和y坐标、右下角的x坐标和y坐标、置信度 的一共5个值。1.因为一开始第一帧时,trackers跟踪器链(列表)仍然为空,所以此时的trks初始化如下:np.zeros((0, 5)) 输出值:array([], shape=(0, 5), dtype=float64)输出值类型:<class 'numpy.ndarray'>2.np.zeros((len(trackers), 5)) 创建目的:1.用于存储上一帧中的跟踪器链中所有跟踪框(KalmanBoxTracker卡尔曼滤波对象)在当前帧中进行predict预测新跟踪框后返回的值,之所以创建的numpy数组的列数为5,是因为一个跟踪框在当前帧中进行predict预测新跟踪框后返回的值为1行5列的矩阵,返回值分别为新跟踪框的左上角的x坐标和y坐标、右下角的x坐标和y坐标、置信度 的一共5个值。2.如果是在视频的第一帧中,那么因为跟踪器链不存在任何跟踪框(KalmanBoxTracker卡尔曼滤波对象),因此np.zeros((len(trackers), 5))创建的是空列表:array([], shape=(0, 5), dtype=float64)。3.trackers:跟踪器链(列表)1.跟踪器链中存储了上一帧中成功跟踪目标并且在当前帧中的预测框(跟踪框),同时也存储了“为了当前帧中的检测框中的新增目标所创建的”新预测框(新跟踪框),但是同时不存储当前帧中预测跟踪失败的预测框(跟踪框),同时也不存储2.跟踪器链实际就是多个的卡尔曼滤波KalmanBoxTracker自定义类的实例对象组成的列表。每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),KalmanBoxTracker类中的实例属性专门负责记录其对应的一个目标框中各种统计参数,并且使用类属性负责记录卡尔曼滤波器的创建个数,增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。把每个卡尔曼滤波器(KalmanBoxTracker实例对象)都存储到跟踪器链(列表)中。"""# 存储跟踪器在当前帧逐个预测轨迹位置,记录状态异常的跟踪器索引# 根据当前所有的卡尔曼跟踪器个数(即上一帧中跟踪的目标个数)创建二维数组:行号为卡尔曼滤波器的标识索引,列向量为跟踪框的位置和ID# trks = np.zeros(len(self.trackers),5)#跟踪器对当前帧的图像预测结果trks = np.zeros((len(self.trackers), 5))#跟踪器对当前帧的图像预测结果""" to_del:存储“跟踪器链中某个要删除的”KalmanBoxTracker卡尔曼滤波对象的索引 """to_del = []#存储要删除的目标框ret = []#返回的跟踪目标#遍历卡尔曼滤波器中的跟踪框"""for t, trk in enumerate(ndarray类型的trks)t:为从0到列表长度-1的索引值trk:ndarray类型的trks中每个(1, 5)形状的一维数组"""""" 遍历trks 用于存储上一帧中的跟踪器链中所有跟踪框(KalmanBoxTracker卡尔曼滤波对象)在当前帧中进行predict预测新跟踪框后返回的值 """for t,trk in enumerate(trks):""" 上一帧中的跟踪器链中所有跟踪框(KalmanBoxTracker卡尔曼滤波对象)在当前帧中进行predict预测新跟踪框 """#使用卡尔曼跟踪器t产生对应目标的跟踪框,即对目标进行预测pos = self.trackers[t].predict()[0]""" 新跟踪框的左上角的x坐标和y坐标、右下角的x坐标和y坐标、置信度 的一共5个值。trk中存储了上一帧中目标的跟踪框在当前帧中新的跟踪框的信息值。"""# 遍历完成后,trk中存储了上一帧中跟踪的目标的预测结果的跟踪框trk[:] = [pos[0],pos[1],pos[2],pos[3],0]""" 如果预测的新的跟踪框的信息(1行5列一共5个值)中包含空值的话,则将该跟踪框在跟踪器链(列表)中的索引值t放到to_del列表中。使用np.any(np.isnan(pos))即能判断这1行5列一共5个值是否包含空值。后面下一步将会根据to_del列表中保存的跟踪框的索引值到跟踪器链(列表)中将该跟踪框从其中移除出去。"""#若预测结果pos中包含空值,添加到del中if np.any(np.isnan(pos)):to_del.append(t)""" np.ma.masked_invalid(跟踪器链trks矩阵):将会对跟踪器链trks矩阵中出现了NaN或inf的某行进行生成掩码,用于屏蔽出现无效值该整行的跟踪器框。np.ma.compress_rows(包含掩码值的跟踪器链trks矩阵):将包含掩码值的整行从中进行移除出去。最终跟踪器链trks矩阵:只包含“上一帧中的跟踪器链中所有跟踪框在当前帧中成功进行predict预测”的新跟踪框。"""#trks中去除无效值的行,保存根据上一帧结果预测当前帧的内容# numpy.ma.masked_invalid 屏蔽出现无效值的数组(NaN 或 inf)# numpy.ma.compress_rows 压缩包含掩码值的2-D 数组的整行,将包含掩码值的整行去除# trks中存储了上一帧中跟踪的目标并且在当前帧中的预测跟踪框trks = np.ma.compress_rows(np.ma.masked_invalid(trks))"""1.for t in reversed(列表):1.t:列表中的元素值2.要想从List列表中删除任意索引位置的元素的话,必须不能从列表头开始遍历删除元素,必须从列表尾向列表头的方向进行遍历删除元素,因为如果从列表头开始遍历删除元素的话,便会导致后面的元素会移动补充到被删除元素的索引位置上,那么再向后进行遍历时便会出现漏遍历的元素,也即防止破坏索引,因此删除列表中元素时需要从列表尾向列表头的方向进行遍历。2.for t in reversed(to_del)1.t:列表中的元素值2.此处to_del列表中的元素值保存的是trackers跟踪器链(列表)中要删除元素的索引值,因此从to_del列表的列表尾向列表头的方向进行遍历出“trackers跟踪器链(列表)中要删除元素的”索引值。然后使用trackers.pop(t)根据trackers跟踪器链(列表)中元素的索引值t自动从列表中移除该元素。3.List pop()方法1.pop()方法语法:list.pop([index=-1])2.pop()函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。3.pop(可选参数)中参数:可选参数,要移除列表元素的索引值,不能超过列表总长度,默认为 index=-1,删除最后一个列表值。4.pop()返回值:该方法返回从列表中被移除的元素对象。5.pop(要移除的列表中元素的索引值):根据列表中元素的索引值自动从列表中移除"""#删除nan的结果,逆向删除异常的跟踪器,防止破坏索引for t in reversed(to_del):"""根据to_del列表中保存的跟踪框的索引值到跟踪器链(列表)中将该跟踪框从其中移除出去。trackers:上一帧中的跟踪器链(列表),保存的是上一帧中成功跟踪目标的跟踪框,也即成功跟踪目标的KalmanBoxTracker卡尔曼滤波对象。trackers.pop(要移除的某个跟踪框的索引值):即能根据该索引值从跟踪器链(列表)中把该跟踪框移除出去"""# pop(要移除的列表中元素的索引值):根据列表中元素的索引值自动从列表中移除self.trackers.pop(t)"""matches:[[检测框的索引值, 跟踪框的索引值] [检测框的索引值, 跟踪框的索引值] 。。。]跟踪成功并且两两匹配组合的IOU值大于iou阈值的检测框和跟踪框组成的矩阵unmatched_detections:[检测框的索引值,。。。]1.新增目标的检测框在detections检测框列表中的索引位置2.两两匹配组合的IOU值小于iou阈值的检测框在detections检测框列表中的索引位置unmatched_trackers:[跟踪框的索引值,。。。]1.跟踪失败的跟踪框/预测框在trackers跟踪框列表中的索引位置2.两两匹配组合的IOU值小于iou阈值的跟踪框/预测框在trackers跟踪框列表中的索引位置1.matched:跟踪成功目标的矩阵。即前后帧都存在的目标,并且匹配成功同时大于iou阈值。2.unmatched_detections(列表):1.检测框中出现新目标,但此时预测框(跟踪框)中仍不不存在该目标,那么就需要在创建新目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象),然后把新目标对应的KalmanBoxTracker类的实例对象放到跟踪器链(列表)中。2.同时如果因为“跟踪框和检测框之间的”两两组合的匹配度IOU值小于iou阈值,则也要把目标检测框放到unmatched_detections中。3.unmatched_trackers(列表):1.当跟踪目标失败或目标离开了画面时,也即目标从检测框中消失了,就应把目标对应的跟踪框(预测框)从跟踪器链中删除。unmatched_trackers列表中保存的正是跟踪失败即离开画面的目标,但该目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象)此时仍然存在于跟踪器链(列表)中,因此就需要把该目标对应的预测框/跟踪框(KalmanBoxTracker类的实例对象)从跟踪器链(列表)中删除出去。2.同时如果因为“跟踪框和检测框之间的”两两组合的匹配度IOU值小于iou阈值,则也要把跟踪目标框放到unmatched_trackers中。"""#使用匈牙利算法:将目标检测框和卡尔曼滤波器预测的跟踪框进行匹配,分别获取跟踪成功的目标,新增的目标,离开画面的目标matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks)"""for t, trk in enumerate(trackers列表)t:为从0到列表长度-1的索引值trk:trackers列表中每个KalmanBoxTracker卡尔曼滤波对象"""#将跟踪成功的目标更新到对应的卡尔曼滤波器for t,trk in enumerate(self.trackers):""" 1.trackers:上一帧中的跟踪器链(列表),保存的是上一帧中成功跟踪目标的跟踪框,也即成功跟踪目标的KalmanBoxTracker卡尔曼滤波对象。2.for t, trk in enumerate(trackers):遍历上一帧中的跟踪器链(列表)中从0到列表长度-1的索引值t 和 每个KalmanBoxTracker卡尔曼滤波对象trk。3.if t not in unmatched_trks:如果上一帧中的跟踪框(KalmanBoxTracker卡尔曼滤波对)的索引值不在当前帧中的unmatched_trackers(列表)中的话,即代表上一帧中的跟踪框在当前帧中成功跟踪到目标,并且代表了“上一帧中的跟踪框在当前帧中的”预测框和当前帧中的检测框的匹配度IOU值大于iou阈值。4.matched[:, 1]:获取的是跟踪框的索引值,即[[检测框的索引值, 跟踪框的索引值] 。。。]中的跟踪框的索引值。5.np.where(matched[:, 1] == t)[0]:where返回的为符合条件的“[检测框的索引值, 跟踪框的索引值]”数组在matched矩阵中的索引值,即行值。因此最后使用[0]就是从array([索引值/行值])中把索引值/行值取出来。6.matched[索引值/行值, 0]:根据索引值/行值获取出matched矩阵中的[检测框的索引值, 跟踪框的索引值],然后获取出第一列的“检测框的索引值”。7.dets[d, :]:根据检测框的索引值/行值从当前帧中的dets检测框列表获取出该检测框的所有列值,最终返回的是一个二维矩阵如下所示:第一种方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度]]第二种方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]]8.dets[d, :][0]:获取出[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]9.trk.update(检测框的5个值的列表):使用检测框进行更新状态更新向量x(状态变量x),也即使用检测框更新跟踪框。"""if t not in unmatched_trks:d = matched[np.where(matched[:, 1] == t)[0], 0]# 使用观测的边界框更新状态向量trk.update(dets[d, :][0])"""unmatched_detections(列表)保存了出现新目标的检测框的索引值,还保存了“因为跟踪框和检测框之间的两两组合的匹配度IOU值小于iou阈值的”目标检测框的索引值。dets[i, :]:根据索引值从当前帧中的检测框列表dets中获取对应的检测框,即该行的所有列值。该检测框的值为:第一种方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度]]第二种方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]]KalmanBoxTracker(dets[i, :]):传入检测框进行创建该新目标对应的跟踪框KalmanBoxTracker卡尔曼滤波对象trk。每个目标框都有对应的一个卡尔曼滤波器(KalmanBoxTracker实例对象),增加一个目标框就增加一个卡尔曼滤波器(KalmanBoxTracker实例对象)。trackers.append(trk):把新增的卡尔曼滤波器(KalmanBoxTracker实例对象trk)存储到跟踪器链(列表)trackers中"""#为新增目标创建新的卡尔曼滤波器的跟踪器for i in unmatched_dets:# trk = KalmanBoxTracker(dets[i,0])trk = KalmanBoxTracker(dets[i, :])self.trackers.append(trk)# 自后向前遍历,仅返回在当前帧出现且命中周期大于self.min_hits(除非跟踪刚开始)的跟踪结果;如果未命中时间大于self.max_age则删除跟踪器。# hit_streak忽略目标初始的若干帧""" i为trackers跟踪器链(列表)长度,从列表尾向列表头的方向 每遍历trackers跟踪器链(列表)一次 即进行 i-=1 """i = len(self.trackers)""" reversed逆向遍历trackers跟踪器链(列表),目的为删除列表中的元素的同时不会造成漏遍历元素的问题 """# 逆向遍历for trk in reversed(self.trackers):""" (跟踪框)KalmanBoxTracker卡尔曼滤波对象trk.get_state():获取跟踪框所预测的在当前帧中的预测结果(已经从[x,y,s,r]转换为[x1,y1,x2,y2]) [x1,y1,x2,y2]即为[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]。get_state()[0] 中使用[0] 是因为返回的为二维矩阵如下: 第一种方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度]]第二种方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]]"""#返回当前边界框的估计值d = trk.get_state()[0]"""1.trk.time_since_update < 1:1.time_since_update:记录了该目标对应的卡尔曼滤波器中的预测框(跟踪框)进行连续预测的次数,每执行predict一次即进行time_since_update+=1。在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。2. time_since_update < 1:该目标对应的卡尔曼滤波器一旦update更新的话该变量值便重置为0,因此要求该目标对应的卡尔曼滤波器必须执行update更新步骤。update更新代表了使用检测框来更新状态更新向量x(状态变量x)的操作,实际即代表了使用“通过yoloV3得到的并且和预测框(跟踪框)相匹配的”检测框来更新该目标对应的卡尔曼滤波器中的预测框(跟踪框)。2.trk.hit_streak >= min_hits:1.hit_streak1.连续更新的次数,每执行update一次即进行hit_streak+=1。2.在连续更新(连续执行update)的过程中,一旦开始连续执行predict两次或以上的情况下,当连续第一次执行predict时,因为time_since_update仍然为0,并不会把hit_streak重置为0,然后才会进行time_since_update+=1;当连续第二次执行predict时,因为time_since_update已经为1,那么便会把hit_streak重置为0,然后继续进行time_since_update+=1。 2.min_hits跟踪框连续成功跟踪到目标的最小次数,也即跟踪框至少需要连续min_hits次成功跟踪到目标。3.hit_streak >= min_hits跟踪框连续更新的次数hit_streak必须大于等于min_hits。而小于该min_hits次数的话update函数不返回该目标的KalmanBoxTracker卡尔曼滤波对象。3.frame_count <= min_hits:因为视频的一开始frame_count为0,而需要每经过一帧frame_count才会+=1。因此在视频的一开始前N帧中,即使frame_count 小于等于min_hits 也可以。"""# 跟踪成功目标的box与id放入ret列表中if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):""" 1.ret:当前帧中跟踪目标成功的跟踪框/预测框的集合,包含目标的跟踪的id(也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个)第一种返回值方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度, trk.id] ...]第二种返回值方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, trk.id] ...]d:[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]trk.id:卡尔曼滤波器的个数/目标框的个数,也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个。2.np.concatenate((d, [trk.id + 1])).reshape(1, -1)[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, 该跟踪框是创建出来的第几个]]"""ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1)) # +1 as MOT benchmark requires positive""" i为trackers跟踪器链(列表)长度,从列表尾向列表头的方向 每遍历trackers跟踪器链(列表)一次 即进行 i-=1 """i -= 1"""trk.time_since_update > max_age1.time_since_update:记录了该目标对应的卡尔曼滤波器中的预测框(跟踪框)进行连续预测的次数,每执行predict一次即进行time_since_update+=1。在连续预测(连续执行predict)的过程中,一旦执行update的话,time_since_update就会被重置为0。2.max_age:最大跟丢帧数。如果当前连续N帧大于最大跟丢帧数的话,则从跟踪器链中删除该卡尔曼滤波对象的预测框(跟踪框)。3.time_since_update > max_age:每预测一帧time_since_update就会+=1,只有预测的跟踪框跟踪到目标(即预测的跟踪框和检测框相似度匹配)才会执行update更新,那么time_since_update才会被重置为0。那么当连续time_since_update帧都没有跟踪到目标的话,即当连续time_since_update帧大于最大跟丢帧数时,那么就需要根据该跟踪失败的跟踪器框的索引把该跟踪器框从跟踪器链(列表)trackers中进行移除出去。"""# 跟踪失败或离开画面的目标从卡尔曼跟踪器中删除if trk.time_since_update > self.max_age:"""trackers:上一帧中的跟踪器链(列表),保存的是上一帧中成功跟踪目标的跟踪框,也即成功跟踪目标的KalmanBoxTracker卡尔曼滤波对象。trackers.pop(要移除的某个跟踪框的索引值):即能根据该索引值从跟踪器链(列表)中把该跟踪框移除出去"""# pop(要移除的列表中元素的索引值):根据列表中元素的索引值自动从列表中移除self.trackers.pop(i)# 返回当前画面中所有目标的box与id,以二维矩阵形式返回if len(ret) > 0:""" ret:当前帧中跟踪目标成功的跟踪框/预测框的集合,包含目标的跟踪的id(也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个)第一种返回值方案:[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度, trk.id] ...]第二种返回值方案(当前使用的为该种):[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, trk.id] ...]d:[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标]trk.id:卡尔曼滤波器的个数/目标框的个数,也即该跟踪框(卡尔曼滤波实例对象)是创建出来的第几个。[[左上角的x坐标, 左上角的x坐标y坐标, 右下角的x坐标, 右下角的y坐标, yolo识别目标是某种物体的可信度, 该跟踪框是创建出来的第几个] [...][...]]"""return np.concatenate(ret)return np.empty((0, 5))
4.2 完整代码
# encoding:utf-8
import imutils
import time
import cv2
import numpy as np
from kalman import *
import matplotlib.pyplot as plt#根据摄像头的图像尺寸进行设置
line = [(0,150),(2560,150)]
#车辆总数
counter = 0
#正向车道的车辆数
counter_up = 0
#逆向车道的车辆数
counter_down = 0#创建跟踪器的对象
tracker = Sort()
memory = {}#线与线的碰撞检测--二维叉乘的方法检测两个直线之间是否相交
# 计算叉乘符号
def ccw(A, B, C):return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])# 检测AB和CD两条直线是否相交
def intersect(A, B, C, D):return ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D)#利用yolov3模型进行目标检测
#加载模型相关信息
#加载可以检测的目标的类型#labelPath:类别标签文件的路径
labelPath = "./yolo-coco/coco.names"# 加载类别标签文件
LABELS = open(labelPath).read().strip().split("\n")#生成多种不同的颜色的检测框 用来标注物体
np.random.seed(42)
COLORS = np.random.randint(0,255,size=(200,3),dtype='uint8')#加载预训练的模型:权重 配置信息、进行恢复模型
#weights_path:模型权重文件的路径
weightsPath = "./yolo-coco/yolov3.weights"
#configPath:模型配置文件的路径
configPath = "./yolo-coco/yolov3.cfg"net = cv2.dnn.readNetFromDarknet(configPath,weightsPath)
#获取YOLO每一层的名称
#getLayerNames():获取网络所有层的名称。
ln = net.getLayerNames()
# 获取输出层的名称: [yolo-82,yolo-94,yolo-106]
# getUnconnectedOutLayers():获取输出层的索引
ln = [ln[i - 1] for i in net.getUnconnectedOutLayers()]#读取视频
vs = cv2.VideoCapture('input/test_1.mp4')
#获取宽和高
(W,H)=(None,None)
writer = Nonetry:prop = cv2.cv.CV_CAP_PROP_Frame_COUNT if imutils.is_cv2() else cv2.CAP_PROP_FRAME_COUNT#获取视频的总帧数total = int(vs.get(prop))print("INFO:{} total Frame in video".format(total))
except:print("[INFO] could not determine in video")#遍历每一帧的图像
while True:#获取帧的结果(grabed,frame)=vs.read()#如果没有 则跳出循环if not grabed:breakif W is None or H is None:(H, W) = frame.shape[:2]# 将图片构建成一个blob,设置图片尺寸,然后执行一次前向传播# YOLO前馈网络计算,最终获取边界框和相应概率blob = cv2.dnn.blobFromImage(frame,1/255.0,(416,416),swapRB=True,crop=False)#将blob送入网络net.setInput(blob)start = time.time()#前向传播,进行预测,返回目标框的边界和响应的概率layerOutouts = net.forward(ln)end = time.time()#存放目标的检测框boxes = []#置信度confidences = []#目标类别classIDs = []# 迭代每个输出层,总共三个for output in layerOutouts:#遍历每个检测结果for detection in output:# 提取类别ID和置信度#detction:1*85 [5:]表示类别,[0:4]bbox的位置信息 [5]置信度、可信度scores = detection[5:]classID = np.argmax(scores)confidence= scores[classID]# 只保留置信度大于某值的边界框if confidence >0.3:# 将边界框的坐标还原至与原图片相匹配,记住YOLO返回的是边界框的中心坐标以及边界框的宽度和高度box = detection[0:4] * np.array([W, H, W, H])(centerX,centerY,width,height) = box.astype("int")# 计算边界框的左上角位置x = int(centerX-width/2)y = int(centerY-height/2)# 更新目标框,置信度(概率)以及类别boxes.append([x,y,int(width),int(height)])confidences.append(float(confidence))classIDs.append(classID)# 使用非极大值抑制方法抑制弱、重叠的目标框idxs = cv2.dnn.NMSBoxes(boxes,confidences,0.5,0.3)#检测框的结果:左上角坐标、右下角坐标dets = []# 确保至少有一个边界框if len(idxs)>0:# 迭代每个边界框for i in idxs.flatten():# 提取边界框的坐标if LABELS[classIDs[i]] == "car":(x,y)=(boxes[i][0],boxes[i][1])(w,h)=(boxes[i][2],boxes[i][3])cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)dets.append([x,y,x+w,y+h,confidences[i]])# 类型设置np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})dets = np.asarray(dets)#SORT目标跟踪if np.size(dets) == 0:continueelse:tracks = tracker.update(dets)# 存放跟踪框boxes = []#存储置信度/可靠性indexIDs = []#上一帧的跟踪结果previous = memory.copy()memory = {}for track in tracks:boxes.append([track[0],track[1],track[2],track[3]])indexIDs.append(int(track[4]))memory[indexIDs[-1]] = boxes[-1]#从SORT跟踪框的结果中进行碰撞检测if len(boxes)>0:i = int(0)#遍历跟踪框for box in boxes:(x, y) = (int(box[0]), int(box[1])) #左上角坐标(w, h) = (int(box[2]), int(box[3])) #宽高color = [int(c) for c in COLORS[indexIDs[i]%len(COLORS)]]cv2.rectangle(frame, (x, y), (w, h), color, 2)#根据在上一帧的检测结果与当前帧的检测结果,利用虚拟线圈完成车辆的计数if indexIDs[i] in previous:previous_box = previous[indexIDs[i]]#上一帧图像的左上角坐标(x2,y2) = (int(previous_box[0]),int(previous_box[1]))# 上一帧图像的宽高(w2,h2) = (int(previous_box[2]),int(previous_box[3]))#上一帧的中心点的坐标p1 = (int(x2 + (w2 - x2) / 2), int(y2 + (h2 - y2) / 2))# 当前帧的中心点的坐标p0 = (int(x + (w - x) / 2), int(y + (h - y) / 2))# 利用p0,p1与line进行碰撞检测if intersect(p0, p1, line[0], line[1]):counter += 1# 判断行进方向if y2 > y:counter_down += 1else:counter_up += 1i += 1# 将车辆计数的相关结果放在视频上print("将车辆计数的相关结果放在视频上")cv2.line(frame, line[0], line[1], (0, 255, 0), 3)cv2.putText(frame, str(counter), (30, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (255, 0, 0), 3)cv2.putText(frame, str(counter_up), (130, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (0, 255, 0), 3)cv2.putText(frame, str(counter_down), (230, 80), cv2.FONT_HERSHEY_DUPLEX, 3.0, (0, 0, 255), 3)# 将检测结果保存在视频
# 未设置视频的编解码信息时,执行以下代码if writer is None:# 设置编码格式fourcc = cv2.VideoWriter_fourcc(*"mp4v")# 视频信息设置writer = cv2.VideoWriter("./output/output.mp4",fourcc,30,(frame.shape[1], frame.shape[0]),True)# 将处理后的帧写入到视频中print ("将处理后的帧写入到视频中")writer.write(frame)# 显示当前帧的结果cv2.imshow("", frame)# 按下q键退出if cv2.waitKey(1) & 0xFF == ord('q'):break# "释放资源"
writer.release()
vs.release()
cv2.destroyAllWindows()
4.3 结果展示
如上图所示,绿⾊的线就是虚拟检测线。红色数字代表正向道路检测到的车辆数。绿色为逆向道路通过的车辆,蓝色数字为总和。