站内推广的方法,东莞市企业信息公示网,深圳东门眼镜城,视频网站开发的论文目录 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.py4.2 完整代码4.3 结果展示 1. 作者介绍
吴思雨… 目录 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.py4.2 完整代码4.3 结果展示 1. 作者介绍
吴思雨女西安工程大学电子信息学院2023级研究生张宏伟人工智能课题组。 研究方向机器视觉与人工智能 电子邮件2879944563qq.com
2. 卡尔曼滤波器
2.1 卡尔曼滤波概述
卡尔曼滤波(Kalman Filtering是一种利用线性系统状态方程通过系统输入观测数据对系统状态进行最优估计的算法。由于观测数据中包括系统中的噪声和干扰的影响所以最优估计也可看作是滤波过程。Kalman滤波在测量方差已知的情况下能够从一系列存在测量噪声的数据中估计动态系统的状态。常在控制、制导、导航、通讯等领域使用。目前已经发展了很多变体扩展到更多领域如计算机视觉。
2.2 标志性发展
扩展卡尔曼滤波器Extended Kalman FilterEKF扩展卡尔曼滤波器是卡尔曼滤波器的非线性扩展。它通过在状态预测和状态更新中使用线性化的近似模型来处理非线性系统。EKF广泛应用于机器人定位、目标跟踪等领域。
无迹卡尔曼滤波器Unscented Kalman FilterUKF无迹卡尔曼滤波器是对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]
#其中xy是框的中心坐标s是面积尺度r是宽高比
def convert_bbox_to_z(bbox):将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r]。其中xy是框的中心坐标s是面积尺度r是宽高比:param bbox: [x1,y1,x2,y2] 分别是左上角坐标和右下角坐标:return: [ x, y, s, r ] 4行1列其中x,y是box中心位置的坐标s是面积r是纵横比w/hw 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]的形式
#即将[cxcysr]的目标框表示转为[x_miny_minx_maxy_max]的形式
def convert_x_to_bbox(x,scoreNone):将[cxcysr]的目标框表示转为[x_miny_minx_maxy_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) ww 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 xw/2.0 #检测框中心位置的x坐标 宽 / 2y2 x[1]h/2.0 #右下角y坐标:y2 yh/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.目标框预测总次数age1。3.if time_since_update 0:hit_streak 0time_since_update 11.连续预测的次数每执行predict一次即进行time_since_update1。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_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。-- 2.预测的目标和检测的目标之间的相似度匹配成功则进入update更新阶段反之匹配失败则删除跟踪目标。3.第三步卡尔曼滤波器update更新阶段如果一次预测然后“预测目标和检测目标之间的”相似度匹配成功的话那么然后就会进入更新阶段。kf.update([x,y,s,r])使用的是通过yoloV3得到的“并且和预测框相匹配的”检测框来更新预测框。-- 1.目标位置信息更新到检测目标链/跟踪目标链 1.目标框更新总次数hits1。2.history []time_since_update 0hit_streak 11.history列表用于在预测阶段保存单个目标框连续预测的多个结果一旦执行update就会清空history列表。2.连续更新的次数每执行update一次即进行hit_streak1。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_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。-- 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.测量噪声的协方差矩阵Rdiag([1,1,10,10]T)2.先验估计的协方差矩阵Pdiag([10,10,10,10,1e4,1e4,1e4]T)。1e41x10的4次方。3.过程激励噪声的协方差矩阵Qdiag([1,1,1,1,0.01,0.01,1e-4]T)。1e-41x10的-4次方。4.1e数字的含义1e41x10的4次方1e-41x10的-4次方5.diag表示对角矩阵写作为diag(a1a2,...,an)的对角矩阵实际表示为主对角线上的值依次为a1a2,...,an而主对角线之外的元素皆为0的矩阵。对角矩阵(diagonal matrix)是一个主对角线之外的元素皆为0的矩阵常写为diaga1a2,...,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_x7, dim_z4)定义一个卡尔曼滤波器利用这个卡尔曼滤波器对目标的状态进行估计。dim_x7定义是一个7维的状态更新向量x(状态变量x)x[u,v,s,r,u^,v^,s^]T。dim_z4定义是一个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是测量噪声的协方差矩阵即真实值与测量值差的协方差。Rdiag([1,1,10,10]T)kf.R[2:, 2:] * 10.2.P是先验估计的协方差矩阵diag([10,10,10,10,1e4,1e4,1e4]T)。1e41x10的4次方。kf.P[4:, 4:] * 1000. # 设置了一个较大的值给无法观测的初始速度带来很大的不确定性kf.P * 10.3.Q是过程激励噪声的协方差矩阵diag([1,1,1,1,0.01,0.01,1e-4]T)。1e-41x10的-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实例对象(卡尔曼滤波器)那么类属性count1。2.统计一个目标框对应的卡尔曼滤波器中各参数统计的次数1.age 0该目标框进行预测的总次数。每执行predict一次便age1。2.hits 0该目标框进行更新的总次数。每执行update一次便hits1。3.time_since_update 01.连续预测的次数每执行predict一次即进行time_since_update1。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_streak1。2.在连续更新(连续执行update)的过程中一旦开始连续执行predict两次或以上的情况下当连续第一次执行predict时因为time_since_update仍然为0并不会把hit_streak重置为0然后才会进行time_since_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。7.history []保存单个目标框连续预测的多个结果到history列表中一旦执行update就会清空history列表。将预测的候选框从中心面积的形式[x,y,s,r]转换为坐标的形式[x1,y1,x2,y2] 的bbox 再保存到 history列表中。# 内部使用KalmanFilter7个状态变量和4个观测输入def __init__(self,bbox):初始化边界框和跟踪器:param bbox:#等速模型#卡尔曼滤波状态转移矩阵7观测输入矩阵:4self.kf KalmanFilter(dim_x7,dim_z4) #初始化卡尔曼滤波器# 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 0update(bbox)使用观测到的目标框bbox更新状态更新向量x(状态变量x)1.time_since_update 01.连续预测的次数每执行predict一次即进行time_since_update1。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一次便hits1。4.hit_streak 11.连续更新的次数每执行update一次即进行hit_streak1。2.在连续更新(连续执行update)的过程中一旦开始连续执行predict两次或以上的情况下当连续第一次执行predict时因为time_since_update仍然为0并不会把hit_streak重置为0然后才会进行time_since_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。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一次便age1。4.if time_since_update 0:hit_streak 0time_since_update 11.连续预测的次数每执行predict一次即进行time_since_update1。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_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。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_streak0self.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_threshold0.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), dtypeint), np.arange(len(detections)), np.empty((0, 5), dtypeint)# 跟踪/检测不为0时# iou 不支持数组计算故IOU 逐个进行交并比计算构造矩阵scipy.linear_assignment进行匹配iou_matrix np.zeros((len(detections), len(trackers)), dtypenp.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), dtypeint)else:matches np.concatenate(matches, axis0)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_hitsmin_hits# 卡尔曼滤波跟踪器链存储多个 KalmanBoxTracker 卡尔曼滤波对象self.trackers []#帧计数self.frame_count 0update(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_count1self.frame_count 11.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), dtypefloat64)输出值类型class numpy.ndarray2.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), dtypefloat64)。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的索引值trkndarray类型的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的索引值trktrackers列表中每个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 11.time_since_update记录了该目标对应的卡尔曼滤波器中的预测框(跟踪框)进行连续预测的次数每执行predict一次即进行time_since_update1。在连续预测(连续执行predict)的过程中一旦执行update的话time_since_update就会被重置为0。2. time_since_update 1该目标对应的卡尔曼滤波器一旦update更新的话该变量值便重置为0因此要求该目标对应的卡尔曼滤波器必须执行update更新步骤。update更新代表了使用检测框来更新状态更新向量x(状态变量x)的操作实际即代表了使用“通过yoloV3得到的并且和预测框(跟踪框)相匹配的”检测框来更新该目标对应的卡尔曼滤波器中的预测框(跟踪框)。2.trk.hit_streak min_hits1.hit_streak1.连续更新的次数每执行update一次即进行hit_streak1。2.在连续更新(连续执行update)的过程中一旦开始连续执行predict两次或以上的情况下当连续第一次执行predict时因为time_since_update仍然为0并不会把hit_streak重置为0然后才会进行time_since_update1当连续第二次执行predict时因为time_since_update已经为1那么便会把hit_streak重置为0然后继续进行time_since_update1。 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 - 1trk.time_since_update max_age1.time_since_update记录了该目标对应的卡尔曼滤波器中的预测框(跟踪框)进行连续预测的次数每执行predict一次即进行time_since_update1。在连续预测(连续执行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),dtypeuint8)#加载预训练的模型权重 配置信息、进行恢复模型
#weights_path模型权重文件的路径
weightsPath ./yolo-coco/yolov3.weights
#configPath模型配置文件的路径
configPath ./yolo-coco/yolov3.cfgnet 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),swapRBTrue,cropFalse)#将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),(xw,yh),(0,255,0),2)dets.append([x,y,xw,yh,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 结果展示
如上图所示绿⾊的线就是虚拟检测线。红色数字代表正向道路检测到的车辆数。绿色为逆向道路通过的车辆蓝色数字为总和。