numba是⼀个⽤于编译Python数组和数值计算函数的编译器,这个编译器能够⼤幅提⾼直接使⽤Python编写的函数的运算速度。
使用方法
numba对代码进⾏加速时,给要优化的函数加上@jit优化器即可。使⽤jit的时候可以让numba来决定什么时候以及怎么做优化。
numba也能够基于输⼊的类型编译⽣成特定的代码,即输入整型,输出会是整型;输入是浮点型,输出也会是浮点型
小结:numba装饰器的优点①加速 ②基于输⼊的类型编译⽣成特定的类型
imutils是在OPenCV基础上的⼀个封装,达到更为简结的调⽤OPenCV接⼝的⽬的,它可以轻松的实现图像的平移,旋转,缩放,骨架化等⼀系列的操作。
层融合
层融合通过对⽹络结构的分析,把多个层合并到⼀起,从⽽降低⽹络复杂度和减少运算量。
作⽤:根据输⼊图像,创建维度N(图⽚的个数),通道数C,⾼H和宽W次序的blobs
blobFromImage(image,
scalefactor=None,
size=None,
mean=None,
swapRB=None,
crop=None,
ddepth=None):
参数:
作⽤:根据给定的检测boxes和对应的scores进⾏NMS(⾮极⼤值抑制)处理
原型:
NMSBoxes(bboxes,
scores,
score_threshold,
nms_threshold,
eta=None,
top_k=None)
参数:
作⽤:加载深度学习⽹络及其模型参数
原型:
readNet(model, config=None, framework=None)
参数:
学习目标
随着城市交通量的迅猛增加,⻋流量统计已成为智能交通系统中⼀项关键技术和热门研究⽅向。⾼效⽽精确的⻋流量检测可以交通管理者和决策者,以及驾驶员提供数据⽀撑,从⽽为交通调度,降低拥堵情况的发⽣,提⾼道路利⽤率有⾮常重要的意义。
⻋流量统计主要有以下⼏种⽅式:
该项⽬对输⼊的视频进⾏处理,主要包括以下⼏个步骤:
总结
学习目标:
多⽬标跟踪,即MOT(Multi-Object Tracking),也就是在⼀段视频中同时跟踪多个目标。MOT主要应⽤在安防监控和自动驾驶等领域中。
多⽬标跟踪可看做多变量估计问题,即给定⼀个图像序列,sti 表示第 t 帧第 i 个目标的状态,St= (st1, st2, …, stM )表示第 t 帧所有⽬标的状态序列,si is:ie= (s , …, s )表示第 i 个⽬标的状态序列,其中is和ie 分别表示⽬标 i 出现的第⼀帧和最后⼀帧,S1:t = (S1 , S2 , …, St )表示所有目标从第⼀帧到第t帧的状态序列。这⾥的状态可以理解为⽬标对应图像中哪个位置,或者是否存在于此图像中,通过匹配得到对应的观测⽬标:O1:t = (O 1, O 2, …, Ot )。
多目标跟踪问题中并不是所有目标都会在第⼀帧出现,也并不是所有目标都会出现在每⼀帧。那如何对出现的目标进⾏初始化,可以作为跟踪算法的分类表征。常见的初始化方法分为两⼤类,⼀个是【基于检测的跟踪】Detection-Based-Tracking(DBT),⼀个是【不基于检测的跟踪】Detection-Free-Tracking(DFT)。下图⽐较形象地说明了两类算法的区别。
MOT也存在着不同的处理模式,在线Online 和 离线Ofline两大类,其主要区别在于是否用到了后续帧的信息。下图形象解释了Online与Offine跟踪的区别。
为了简化多目标跟踪的难度,我们引入运动模型类简化求解过程,运动模型捕捉目标的动态行为,它估计目标在未来帧中的潜在位置,从而减少搜索空间。在大多数情况下,假设目标在现实中是平缓运动的,那么在图像空间也是如此。对于车辆的运动,大致可分为线性和非线性两种运动:
多目标跟踪中基于神经网络的算法,端到端的算法并不多,主要还在实验室的刷榜阶段,模型复杂,速度慢,追踪结果也不好,我们就不再介绍,主要给大家介绍以下两种主流的算法
该类算法能达到实时性,但依赖于检测算法效果要好,特征区分要好 (输出最终结果的好坏依赖于较强的检测算法,而基于卡尔曼加匈牙利匹配的追踪算法作用在于能够输出检测目标的id,其次能保证追踪算法的实时性),这样追踪效果会好,id切换少。代表性的算法是SORT / DeepSORT.
SORT 是一种实用的多目标跟踪算法,引入了线性速度模型与卡尔曼滤波来进行位置预测,在无合适匹配检测框的情况下,使用运动模型来预测物体的位置。匈牙利算法是一种寻找二分图的最大匹配的算法,在多目标跟踪问题中可以简单理解为寻找前后两帧的若干目标的匹配最优解的一种算法。而卡尔曼滤波可以看作是一种运动模型,用来对目标的轨迹进行预测,并且使用确信度较高的跟踪结果进行预测结果的修正。
多目标追踪一般接在目标检测后。在工业界目标检测采用比较多的是yolo检测网络,单阶段式,速度快,精度不差,部署在NV的平台帧率可以达到30fps以上。所以要实现目标检测代码和多目标追踪代码集成的任务,需要先将两者的框架统一:先实现目标检测网络,检测的输出结果主要是将检测框的位置信息输入到多目标追踪算法中。
这类算法特点是跟踪效果会很好,因为其为每一类物体都单独分配了一个跟踪器但该算法对目标尺度变化要求较大,参数调试需要合理,同时该算法极耗cpu资源,实时性不高,代表算法是利用KCF进行目标跟踪。
多目标追踪本质上是多个目标同时运动的问题,所以有提出将单目标跟踪器引入到多目标追踪的问题,为每一个目标分配一个KCF跟踪器,然后间接地使用匹配算法来修正那些跟踪失败或新出现的目标。代表性的单目标跟踪算法为核相关滤波算法(KCF)在精度和速度上能够同时达到很高的水平,是当时单目标跟踪最优秀的算法之一,后来的很多单目标跟踪算法都是基于此做的改进。
实际应用过程中会为每个目标分配一个KCF跟踪器并采用多线程的方式来组织这些跟踪器。同时因为实际硬件条件的限制,不可能提供强大的计算力资源,会采用检测器与跟踪器交替进行的跟踪策略。由于检测的帧率不高,使得跟踪的维持效果出现滞后或框飘的现象较为严重,实用性不大。
多目标跟踪,即MOT (Multi-Object Tracking) ,在一段视频中同时跟踪多个目标
学习目标
IOU是交并比 (Intersection-over-Union) 是目标检测中使用的一个概念是产生的候选框 (candidate bound) 与原标记框 (ground truth bound) 的交叠率,即它们的交集与并集的比值。最理想情况是完全重叠,即比值为1。在多目标跟踪中,用来判别跟踪框和目标检测框之间的相似度。
IoU是两个区域的交除以两个区域的并得出的结果。
拿到IOU后,使IOU跟一个阈值做比较,来判断跟踪框(目标)和检测框是否是同一个目标。
def iou(bb_test, bb_gt):
"""
在两个box间计算IOU
:param bb_test: box1 = [x1y1x2y2]
:param bb_gt: box2 = [x1y1x2y2]
:return: 交并⽐IOU
"""
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[3] - bb_gt[1]) - wh)
return o
在该项目中候选框有两种表示形式
这两种方式要进行相互的转换:将候选框从坐标形式转换为中心点坐标和面积的形式
# 将左上角/右下角坐标[x1,y1,x2,y2]转换成中心点坐标[x,y,s,r]
def convert_bbox_to_z(bbox):
"""
将[x1,y1,x2,y2]形式的检测框转为滤波器的状态表示形式[x,y,s,r]。其中x,y是框的中⼼坐标,
: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] # 宽
h = bbox[3] - bbox[1] # 高
x = bbox[0] + w/2. # 中心点坐标x
y = bbox[1] + w/2. # 中心点坐标y
s = w*h # 面积
r = w/float(h) # 宽高比
return np.array([x,y,s,r]).reshape(4,1) # 4行格式
# 将[x,y,s,r]转换成[x1,y1,x2,y2]
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
:param score: 置信度
:return:[x1,y1,x2,y2],左上⻆坐标和右下⻆坐标
"""
w = np.sqrt(x[2] * x[3]) # 宽:x[2]是s是面积(w*h),x[3]是r是宽高比(w/h),二者相乘即w²,所以开方即得w
h = x[2]/w # 高
x1 = x[0] - w/2 # 左上角x
y1 = x[1] - h/2 # 左上角y
x2 = x[0] + w/2 # 右下角x
y2 = x[1] + h/2 # 右下角y
if score is None: # 若置信度为空
return np.array([x1,y1,x2,y2]).reshape(1,4)
else:
return np.array([x1,y1,x2,y2,score]).reshape(1,5)
学习目标
FilterPy是一个实现了各种滤波器的Python模块,它实现著名的卡尔曼滤波和粒子滤波器。我们可以直接调用该库完成卡尔曼滤波器实现。其中的主要模块包括:
开源代码在:
https://github.com/rlabbe/filterpy/tree/master/filterpy/kalman
我们介绍下卡尔曼滤波器的实现,主要分为 预测和更新 两个阶段,在进行滤波之前,需要先进行初始化:
卡尔曼滤波 (Kalman)无论是在单目标还是多目标领域都是很常用的一种算法我们将卡尔曼滤波看做一种运动模型,用来对目标的位置进行预测,并且利用预测结果对跟踪的目标进行修正,属于自动控制理论中的一种方法。
在对视频中的目标进行跟踪时,当目标运动速度较慢时,很容易将前后两帧的目标进行关联,如下图所示:
如果目标运动速度比较快,或者进行隔帧检测时,在后续帧中,目标A已运动到前一帧B所在的位置,这时再进行关联就会得到错误的结果,将A与B关联在一起。
那怎么才能避免这种出现关联误差呢?我们可以在进行目标关联之前,对目标在后续帧中出现的位置进行预测,然后与预测结果进行对比关联,如下图所示:
我们在对比关联之前,先预测出A和B在下一顿中的位置,然后再使用实际的检测位置与预测的位置进行对比关联,只要预测足够精确,几乎不会出现由于速度太快而存在的误差。
卡尔曼滤波就可以用来预测目标在后续帧中出现的位置,如下图所示,卡尔曼滤波器就可以根据前面五帧数据目标的位置,预测第6帧目标的位置。
卡尔曼滤波器最大的优点是采用递归的方法来解决线性滤波的问题,它只需要当前的测量值 和 前一个周期的预测值就能够进行状态估计。由于这种递归方法不需要大量的存储空间,每一步的计算量小,计算步骤清晰,非常适合计算机处理,因此卡尔曼滤波受到了普遍的欢迎,在各种领域具有广泛的应用前景。
我们假设一个简单的场景,有一辆小车在行驶,它的速度是v,可以通过观测得到它的位置p,也就是说我们可以实时的观测小车的状态。
场景描述
小车在某一时刻的状态表示为一个向量
虽然我们比较确定小车此时的状态,无论是计算还是检测都会存在一定的误差,所以我们只能认为当前状态是其真实状态的一个最优估计。那么我们不妨认为当前状态服从一个高斯分布,如下图所示
因为我们有两个变量,所以可以⽤⼀个协⽅差矩阵Pk来表示数据之间的相关性和离散程度:
预测下⼀时刻的状态
下面我们需要通过小车的当前状态,运用一些物理学的知识来预测它的下一个状态,即通过k-1时刻的位置和速度,可以推测下一个时刻的状态为:
系统的不确定性和相关性 可以 通过协方差矩阵描述,那根据当前协方差矩阵预测下一时刻的协方差矩阵:
在这⾥我们⽤到了协⽅差的性质:
如果x的协方差矩阵是Σ,那x乘一个矩阵A 的协方差矩阵,就是AΣAT
增加系统的内部控制
我们需要对小车进行控制,比如加速和减速,假设某个时刻我们施加的加速度是a,那么下一时刻的位置和速度则应该为:
其中,Bk我们称为状态控制矩阵,而Uk称为状态控制向量,含义很明显,前者表明的是加速减速如何改变小车的状态,而后者则表明控制的力度大小和方向
考虑系统的外部影响
外界有很多影响因素能够对小车的状态产生影响,比如说风速等,在这里我们假设外部的不确定因素对小车造成的系统状态误差wk服从高斯分布wk~ N(0, Qk),至此我们就能得到Kalman滤波中完整的状态预测方程:
一般情况下,我们假设wk为0即可,如果明确均值不为0,就不能省略了,要看实际的应用场景。
对观测数据的预测
前面我们通过小车的上一个状态,对它的当前状态做了预测,此时我们要考虑对于小车的状态能够观测到什么呢?
小车的当前状态和观测到的数据应该具备某种特定的关系,假设这个关系通过矩阵表示为Hk,如下图所示
在此前对小车所做的预测状态下,我们的观测值为
卡尔曼滤波需要做的最重要的最核心的事就是融合预测和观测的结果,充分利用两者的不确定性来得到更加准确的估计。通俗来说就是怎么从上面的两个椭圆中来得到中间淡黄色部分的高斯分布,看起来这是预测和观测高斯分布的重合部分,也就是概率比较高的部分。
前⾯我们已经得到了预测结果和观测结果服从的两个⾼斯分布,如下:
所以我们可以进⾏如下推导,来得到卡尔曼滤波对当前状态(基于预测和观测的)最优估计的计算⽅程:
- 最重要的是,我们要时刻关注不断迭代的系统变量,分别是系统的状态:x, 其误差协⽅差矩阵:P,和卡尔曼增益:K。
- 在实际应⽤时,对 xxx 和 xxx 的选择要依据实际情况来定,可以不断调试来寻找 ⼀个最优解,也可以是可变的,只要最终效果能够更好。
在利⽤卡尔曼滤波对小车的运动状态进行预测。主要流程如下所示:
下⾯我们看下整个流程实现:
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
from filterpy.kalman import KalmanFilter
# ⽣成1000个位置,从1到1000,是⼩⻋的实际位置
z = np.linspace(1,1000,1000)
# 添加噪声
mu,sigma = 0,1 # mu-期望,sigma-方差
noise = np.random.normal(mu,sigma,1000)
# ⼩⻋位置的观测值
z_nosie = z+noise
# dim_x 状态向量size,在该例中为[p,v],即位置和速度,size=2
# dim_z 测量向量size,假设⼩⻋为匀速,速度为1,测量向量只观测位置,size=1
my_filter = KalmanFilter(dim_x=2, dim_z=1)
# 定义卡尔曼滤波中所需的参数
# x 初始状态为[0,0],即初始位置为0,速度为0.
# 这个初始值不是⾮常重要,在利⽤观测值进⾏更新迭代后会接近于真实值
my_filter.x = np.array([[0.], [0.]])
# p 协⽅差矩阵,表示状态向量内位置与速度的相关性
# 假设速度与位置没关系,协⽅差矩阵为[[1,0],[0,1]]
my_filter.P = np.array([[1., 0.], [0., 1.]])
# F 初始的状态转移矩阵,假设为匀速运动模型,可将其设为如下所示
my_filter.F = np.array([[1., 1.], [0., 1.]])
# Q 状态转移协⽅差矩阵,也就是外界噪声,
# 在该例中假设⼩⻋匀速,外界⼲扰⼩,所以我们对F⾮常确定,觉得F⼀定不会出错,所以Q设的很⼩
my_filter.Q = np.array([[0.0001, 0.], [0., 0.0001]])
# 观测矩阵 Hx = p
# 利⽤观测数据对预测进⾏更新,观测矩阵的左边⼀项不能设置成0
my_filter.H = np.array([[1, 0]])
# R 测量噪声,⽅差为1
my_filter.R = 1
# 保存卡尔曼滤波过程中的位置和速度
z_new_list = []
v_new_list = []
# 对于每⼀个观测值,进⾏⼀次卡尔曼滤波
for k in range(len(z_nosie)):
# 预测过程
my_filter.predict()
# 利⽤观测值进⾏更新
my_filter.update(z_nosie[k])
# do something with the output
x = my_filter.x
# 收集卡尔曼滤波后的速度和位置信息
z_new_list.append(x[0][0])
v_new_list.append(x[1][0])
①预测误差的可视化
预测偏差 = 预测结果 - 真实结果
# 位移的偏差
dif_list = []
for k in range(len(z)):
dif_list.append(z_new_list[k]-z[k])
# 速度的偏差
v_dif_list = []
for k in range(len(z)):
v_dif_list.append(v_new_list[k]-1)
plt.figure(figsize=(20,9))
plt.subplot(1,2,1)
plt.xlim(-50,1050)
plt.ylim(-3.0,3.0)
plt.scatter(range(len(z)),dif_list,color ='b',label = "位置偏差")
plt.scatter(range(len(z)),v_dif_list,color ='y',label = "速度偏差")
plt.legend()
② 卡尔曼滤波器参数的变化
⾸先定义⽅法将卡尔曼滤波器的参数堆叠成⼀个矩阵,右下⻆补0,我们看⼀下参数的变化。
# 定义⼀个⽅法将卡尔曼滤波器的参数堆叠成⼀个矩阵,右下⻆补0
def filter_comb(p, f, q, h, r):
a = np.hstack((p, f))
b = np.array([r, 0])
b = np.vstack([h, b])
b = np.hstack((q, b))
a = np.vstack((a, b))
return a
P- 协方差矩阵
F- 状态转移矩阵
Q- 过程激励噪声的协方差矩阵
H - 量测矩阵
R - 测量噪声的协⽅差对参数变化进⾏可视化:
# 保存卡尔曼滤波过程中的位置和速度
z_new_list = []
v_new_list = []
# 对于每⼀个观测值,进⾏⼀次卡尔曼滤波
for k in range(1):
# 预测过程
my_filter.predict()
# 利⽤观测值进⾏更新
my_filter.update(z_nosie[k])
# do something with the output
x = my_filter.x
# 对P、F、Q、H、R五个矩阵进行堆叠
c = filter_comb(my_filter.P,my_filter.F,my_filter.Q,my_filter.H,my_filter.R)
# 绘制
plt.figure(figsize=(32,18))
sns.set(font_scale=4)
#sns.heatmap(c,square=True,annot=True,xticklabels=False,yticklabels==False
sns.heatmap(c,square=True,annot=True,xticklabels=False,yticklabels=False,cbar=False)
③ 概率密度函数
为了验证卡尔曼滤波的结果 优于 测量的结果,绘制预测结果误差和测量误差的概率密度函数:
# ⽣成概率密度图像
z_noise_list_std = np.std(noise)
z_noise_list_avg = np.mean(noise)
z_filterd_list_std = np.std(dif_list)
import seaborn as sns
plt.figure(figsize=(16,9))
ax = sns.kdeplot(noise,shade=True,color="r",label="std=%.3f"%z_noise_list_std)
ax = sns.kdeplot(dif_list,shade=True,color="g",label="std=%.3f"%z_filterd_list_std)
了解filterpy⼯具包
FilterPy是一个实现了各种滤波器的Python模块,它实现著名的卡尔曼滤波和粒子滤波器。直接调用该库完成卡尔曼滤波器实现
背景介绍
卡尔曼滤波器通过预测目标在后续帧中的位置,避免在进行目标关联时出现误差
卡尔曼滤波器的原理
滤波器根据上一时刻 (k-1 时刻) 的值来估计当前时刻 (k 时刻) 的状态,得到k时刻的先验估计值; 然后使用当前时刻的测量值来更正这个估计值,得到当前时刻的估计值。
卡尔曼滤波的实现过程
卡尔曼滤波器的实现,主要分为预测和更新两个阶段,在进行滤波之前,需要先进行初始化
能够利用卡尔曼滤波器完成小车目标状态的预测
学习⽬标
主要完成卡尔曼滤波器进⾏跟踪的相关内容的实现
状态量x的设定是⼀个七维向量:分别表示⽬标中⼼位置的x,y坐标,⾯积s和当前⽬标框的纵横⽐r,最后三个是:横向,纵向,⾯积的变化速率,其中速度部分初始化为0,其他根据观测进⾏输⼊。
初始化卡尔曼滤波器参数,7个状态变量和4个观测输⼊,运动形式和转换矩阵的确定都是基于匀速运动模型,状态转移矩阵F根据运动学公式确定:
量测矩阵H是4*7的矩阵,将观测值与状态变量相对应:
以及相应的协⽅差参数的设定,根据经验值进⾏设定。
def __init__(self, bbox):
# 定义等速模型
# 内部使⽤KalmanFilter,7个状态变量和4个观测输⼊
self.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, 0],[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*7的矩阵
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]])
# R是测量噪声的协⽅差,即真实值与测量值差的协⽅差
self.kf.R[2:, 2:] *= 10.
# P是先验估计的协⽅差
self.kf.P[4:, 4:] *= 1000. # give high uncertainty to the unobserva
self.kf.P *= 10.
# Q是过程激励噪声的协⽅差
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 # 预测的次数
使⽤观测到的⽬标框,更新状态变量(前面估计的结果)
def update(self, bbox):
# 重置
self.time_since_update = 0
# 清空history
self.history = []
# hits计数加1
self.hits += 1
self.hit_streak += 1
# 根据观测结果修改内部状态x
self.kf.update(convert_bbox_to_z(bbox))
推进状态变量并返回预测的边界框结果
def predict(self):
# 推进状态变量
if (self.kf.x[6] + self.kf.x[2]) <= 0:
self.kf.x[6] *= 0.0
# 进⾏预测
self.kf.predict()
# 卡尔曼滤波的次数
self.age += 1
# 若过程中未更新过,将hit_streak置为0
if self.time_since_update > 0:
self.hit_streak = 0
self.time_since_update += 1
# 将预测结果追加到history中
self.history.append(convert_x_to_bbox(self.kf.x))
return self.history[-1]
============================
整个代码如下所示:
class KalmanBoxTracker(object):
count = 0
def __init__(self, bbox):
"""
初始化边界框和跟踪器
:param bbox:
"""
# 定义等速模型
# 内部使⽤KalmanFilter,7个状态变量和4个观测输⼊
self.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, 0],[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*7的矩阵
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]])
# R是测量噪声的协⽅差,即真实值与测量值差的协⽅差
self.kf.R[2:, 2:] *= 10.
# P是先验估计的协⽅差
self.kf.P[4:, 4:] *= 1000. # give high uncertainty to the unobserva
self.kf.P *= 10.
# Q是过程激励噪声的协⽅差
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 # 预测的次数
def update(self, bbox):
"""
使⽤观察到的⽬标框更新状态向量。filterpy.kalman.KalmanFilter.update 会根据观测
重置self.time_since_update,清空self.history。
:param bbox:⽬标框
:return:
"""
# 重置
self.time_since_update = 0
# 清空history
self.history = []
# hits计数加1
self.hits += 1
self.hit_streak += 1
# 根据观测结果修改内部状态x
self.kf.update(convert_bbox_to_z(bbox))
def predict(self):
"""
推进状态向量并返回预测的边界框估计。
将预测结果追加到self.history。由于 get_state 直接访问 self.kf.x,所以self.his
:return:
"""
# 推进状态变量
if (self.kf.x[6] + self.kf.x[2]) <= 0:
self.kf.x[6] *= 0.0
# 进⾏预测
self.kf.predict()
# 卡尔曼滤波的次数
self.age += 1
# 若过程中未更新过,将hit_streak置为0
if self.time_since_update > 0:
self.hit_streak = 0
self.time_since_update += 1
# 将预测结果追加到history中
self.history.append(convert_x_to_bbox(self.kf.x))
return self.history[-1]
def get_state(self):
"""
返回当前边界框估计值(转为左上角/右下角坐标)
:return:
"""
return convert_x_to_bbox(self.kf.x)
学习目标:
匈牙利算法 (Hungarian Agorithm) 与KM算法 (Kuhn-Munkres Algorithm) 是用来解决多目标跟踪中的数据关联问题,匈牙利算法与KM算法都是为了求解二分图的最大匹配问题。
有一种很特别的图,就做二分图,那什么是二分图呢? 就是能分成两组,U,V。其中,U上的点不能相互连通,只能连去V中的点,同理,V中的点不能相互连通,只能连去U中的点。这样,就叫做二分图。
可以把二分图理解为视频中连续两帧中的所有检测框,第一顿所有检测框的集合称为U,第二帧所有检测框的集合称为V。同一帧的不同检测框不会为同一个目标所以不需要互相关联,相邻两帧的检测框需要相互联通,最终将相邻两帧的检测框尽量完美地两两匹配起来。而求解这个问题的最优解就要用到匈牙利算法或者KM算法。
匈牙利算法是一种在多项式时间内求解任务分配问题的组合优化算法。美国数学家哈罗德·库恩于1955年提出该算法。此算法之所以被称作匈牙利算法,是因为算法很大一部分是基于以前匈牙利数学家Denes K6nig和Jen Egervary的工作之上创建起来的。
我们以目标跟踪的方法介绍匈牙利算法,以下图为例,假设左边的四张图是我们在第N帧检测到的目标 (U),右边四张图是我们在第N+1检测到的目标 (V) 。红线连起来的图,是我们的算法认为是同一行人可能性较大的目标。由于算法并不是绝对理想的,因此并不一定会保证每张图都有一对一的匹配,一对二甚至一对多,再甚至多对多的情况都时有发生。这时我们怎么获得最终的一对一跟踪结果呢? 我们来看匈牙利算法是怎么做的。
匈牙利算法的流程大家看到了,有一个很明显的问题相信大家也发现了,按这个思路找到的最大匹配往往不是我们心中的最优。匈牙利算法将每个匹配对象的地位视为相同,在这个前提下求解最大匹配。这个和我们研究的多目标跟踪问题有些不合,因为每个匹配对象不可能是同等地位的,总有一个真实目标是我们要找的最佳匹配,而这个真实目标应该拥有更高的权重,在此基础上匹配的结果才能更贴近真实情况。
KM算法就能比较好地解决这个问题,我们下面来看看KM算法
KM算法解决的是带权二分图的最优匹配问题
还是用上面的图来举例子,这次给每条连接关系加入了权重,也就是我们算法中其他模块给出的置信度分值。
KM算法解决问题的步骤是这样的。
至此KM算法流程结束,三对目标成功匹配,甚至在左三目标预测不够准确的情况下也进行了正确匹配。可见在引入了权重之后,匹配成功率大大提高。
最后还有一点值得注意,匈牙利算法得到的最大匹配并不是唯一的,预设匹配边或者匹配顺序不同等,都可能会导致有多种最大匹配情况,所以有一种替代KM算法的想法是,我们只需要用匈牙利算法找到所有的最大匹配,比较每个最大匹配的权重,再选出最大权重的最优匹配即可得到更贴近真实情况的匹配结果。但这种方法时间复杂度较高,会随着目标数越来越多,消耗的时间大大增加,实际使用中并不推荐。
使用:
scipy.optimize.linear_sum_assignment(cost_matrix)
希望用最小代价实现匹配,所以是相似度矩阵求负号,进行线性规划
实现KM算法,其中const matrix表示代价矩阵。比如第一帧有a,b,c,d四个目标,第二顿图像有p,q,r,s四个目标,其相似度矩阵如下所示:
from scipy.optimize import linear_sum_assignment
import numpy as np
# 代价矩阵
cost =np.array([[0.9,0.6,0,0],[0,0.3,0.9,0],[0.5,0.9,0,0],[0,0,0.2,0]])
# 匹配结果:该⽅法的⽬的是代价最⼩,这⾥是求最⼤匹配,所以将cost取负数
row_ind,col_ind=linear_sum_assignment(-cost)
#对应的⾏索引
print("⾏索引:\n{}".format(row_ind))
#对应⾏索引的最优指派的列索引
print("列索引:\n{}".format(col_ind))
#提取每个⾏索引的最优指派列索引所在的元素,形成数组
print("匹配度:\n{}".format(cost[row_ind,col_ind]))
学习目标
主要思想:在这里我们对检测框和跟踪框进行匹配,整个流程是遍历检测框和跟踪框,并进行匹配,匹配成功的将其保留,未成功的将其删除。
该数据关联函数有3个输入和3个输出
# 将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
"""
# [Step1] 跟踪⽬标/跟踪目标数量为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)
# [Step2] 如何评估检测框和跟踪框的相似度-IOU
# iou 不⽀持数组计算。逐个计算两两间的交并⽐,调⽤ linear_assignment 进⾏匹配
# 创造全0数组(行-检测框的个数, 列-跟踪框的个数),存储IOU交并比矩阵
iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32)
# 遍历⽬标检测的bbox集合,每个检测框的标识为d
for d, det in enumerate(detections):
# 遍历跟踪框(卡尔曼滤波器预测)bbox集合,每个跟踪框标识为t
for t, trk in enumerate(trackers):
iou_matrix[d, t] = iou(det, trk) # 检测框和跟踪框的交并比
# 通过匈⽛利算法将跟踪框和检测框以[[d,t]...]的⼆维矩阵的形式存储在match_indices中
result = linear_sum_assignment(-iou_matrix) # result由行索引、列索引组成,检测框和跟踪框匹配后的行、列索引
matched_indices = np.array(list(zip(*result)))
# [Step3] 记录返回的结果
# [Step3.1] 记录未匹配的检测框
# 未匹配的检测框放⼊unmatched_detections中,表示有新的⽬标进⼊画⾯,要新增跟踪器跟踪⽬
unmatched_detections = [] # 新建空列表
for d, det in enumerate(detections):
if d not in matched_indices[:, 0]: # 没匹配成功
unmatched_detections.append(d) # 增加索引
# [Step3.2] 记录未匹配的跟踪框
# 未匹配的跟踪框放⼊unmatched_trackers中,表示⽬标离开之前的画⾯,应删除对应的跟踪器
unmatched_trackers = []
for t, trk in enumerate(trackers):
if t not in matched_indices[:, 1]:
unmatched_trackers.append(t)
# [Step3.3] 记录匹配的检测框和跟踪框
# 将匹配成功的跟踪框放⼊matches中
matches = []
for m in matched_indices:
# [低于阈值] 过滤掉IOU低的匹配,将其放⼊到unmatched_detections和unmatched_trackers
if iou_matrix[m[0], m[1]] < iou_threshold:
unmatched_detections.append(m[0])
unmatched_trackers.append(m[1])
# [大于阈值] 才算匹配成功,满⾜条件的以[[d,t]...]的形式放⼊matches中
else:
matches.append(m.reshape(1, 2))
# [Step4] 构建返回的结果
# 初始化matches,以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)
学习目标:
上一节给大家介绍了一下多目标跟踪MOT的一些基础知识。SORT和DeepSORT是多目标跟踪中两个知名度比较高的算法。DeepSORT是原团队对SORT的改进版本。现在来解析一下SORT和DeepSORT的基本思路。
SORT核心是卡尔曼滤波和匈牙利匹配两个算法。流程图如下所示,可以看到整体可以拆分为两个部分,分别是匹配过程和卡尔曼预测加更新过程,都用灰色框标出来了。
SORT算法的关键步骤:轨迹卡尔曼滤波预测一 使用匈牙利算法将预测后的tracks 和 当前帧中的detecions进行匹配(IOU匹配),卡尔曼滤波更新
卡尔曼滤波分为两个过程:预测和更新。SORT引入了线性速度模型与卡尔曼滤波来进行位置预测,先进行位置预测然后再进行匹配。运动模型的结果可以用来预测物体的位置。
匈牙利算法解决的是一个分配问题,用IOU距离作为权重 (也叫cost矩阵),并且当IOU小于一定数值时,不认为是同一个目标,理论基础是视频中两帧之间物体移动不会过多。在代码中选取的阙值是0.3。scipy库的linear_sum_assignment都实现了这一算法,只需要输入cost_matrix即代价矩阵就能得到最优匹配。
DeepSORT是SORT的续作,整体框架没有大改,还是延续了卡尔曼滤波加匈牙利算法的思路,在这个基础上增加了鉴别网络Deep Association Metric(行人重识别)
下图是deepSORT流程图,和SORT基本一样,就多了级联匹配(MatchingCascade) 和新轨迹的确认 (confirmed)
DeepSORT算法的关键步骤:轨迹卡尔曼滤波预测一 使用牙利算法将预测后的tracks和当前顿中的detecions进行匹配 (级联匹配和IOU匹配),卡尔曼滤波更新
级联匹配:
学习目标
主要实现了一个多目标跟踪器,管理多个卡尔曼滤波器对象,主要包括以下内容:
def __init__(self, max_age=1, min_hits=3):
"""
初始化:设置SORT算法的关键参数
:param max_age:最⼤检测数,当目标消失超过该数,就删除该目标
:param min_hits:最小次数,最少要超过该数,才认为该目标在跟踪当中
"""
# 最⼤检测数:⽬标未被检测到的帧数,超过之后会被删
self.max_age = max_age
# ⽬标命中的最⼩次数,⼩于该次数不返回
self.min_hits = min_hits
# 卡尔曼跟踪器
self.trackers = []
# 帧计数
self.frame_count = 0
我们将上述两个⽅法封装在⼀个类中。
class Sort(object):
def __init__(self, max_age=1, min_hits=3):
"""
初始化:设置SORT算法的关键参数
:param max_age:最⼤检测数,当目标消失超过该数,就删除该目标
:param min_hits:最小次数,最少要超过该数,才认为该目标在跟踪当中
"""
# 最⼤检测数:⽬标未被检测到的帧数,超过之后会被删
self.max_age = max_age
# ⽬标命中的最⼩次数,⼩于该次数不返回
self.min_hits = min_hits
# 卡尔曼跟踪器
self.trackers = []
# 帧计数:当前处理了多少帧
self.frame_count = 0
def update(self, dets):
self.frame_count += 1 # 到了新一帧的图像
# [Step 1] 获取卡尔曼滤波的跟踪框结果
# 在当前帧逐个预测轨迹位置,记录状态异常的跟踪器索引
# 根据当前所有的卡尔曼跟踪器个数(即上⼀帧中跟踪的⽬标个数)创建⼆维数组
# ⼆维数组:⾏号为卡尔曼滤波器的标识, 列向量为跟踪框的位置和ID(坐标+ID,所以是5)
trks = np.zeros((len(self.trackers), 5)) # 存储跟踪器的预测
to_del = [] # 存储要删除的⽬标框
ret = [] # 存储要返回的追踪⽬标框
# 循环遍历卡尔曼跟踪器列表
for t, trk in enumerate(trks):
# 使⽤第t个卡尔曼跟踪器产⽣对应⽬标的跟踪框,pos是目标框的位置
pos = self.trackers[t].predict()[0]
# 遍历完成后,trk中存储了上⼀帧中跟踪的⽬标的预测跟踪框
trk[:] = [pos[0], pos[1], pos[2], pos[3], 0] # 左上角坐标2个,右下角坐标2个,置信度0
# 如果跟踪框中包含空值,则将该跟踪框添加到要删除的列表中
if np.any(np.isnan(pos)):
to_del.append(t)
# numpy.ma.masked_invalid 屏蔽出现⽆效值的数组(NaN 或 inf)
# numpy.ma.compress_rows 压缩包含掩码值的2-D 数组的整⾏,将包含掩码值的整⾏
# trks中存储了上⼀帧中跟踪的⽬标 并且 在当前帧中的预测跟踪框
trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
# 逆向删除异常的跟踪器,防⽌破坏索引
for t in reversed(to_del): # 反向遍历
self.trackers.pop(t)
# [Step 2] 数据关联:利⽤匈⽛利算法对⽬标检测框和目标跟踪框进⾏关联
# 将⽬标检测框与卡尔曼滤波器预测的跟踪框关联获取跟踪成功的⽬标,新增的⽬标,离开画面的目标
matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets, trks)
# [Step 3] 对匹配结果的处理
# [Step 3.1]将跟踪成功的⽬标框,更新到对应的卡尔曼滤波器
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])
# [Step 3.2]为新增的⽬标,创建新的卡尔曼滤波器对象进⾏跟踪
for i in unmatched_dets:
trk = KalmanBoxTracker(dets[i, :])
self.trackers.append(trk)
#
# ⾃后向前遍历,仅返回 (1)在当前帧出现 (2)且命中周期⼤于self.min_hits(除⾮跟踪刚开始)的跟踪结果
# 如果未命中时间大于self.max_age则删除跟踪器
# hit_streak忽略⽬标初始的若⼲帧
i = len(self.trackers)
for trk in reversed(self.trackers): # 反转遍历
# 返回当前边界框的估计值
d = trk.get_state()[0]
# 跟踪成功⽬标的box与id放⼊ret列表中
if (trk.time_since_update < 1) and \
(trk.hit_streak >= self.min_hits or self.frame_count<=self.max_age):
ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1))
i -= 1
# 跟踪失败或离开画⾯的⽬标从卡尔曼跟踪器中删除
if trk.time_since_update > self.max_age:
self.trackers.pop(i)
# 返回当前画⾯中所有⽬标的box与id,以⼆维矩阵形式返回
if len(ret) > 0:
return np.concatenate(ret)
# 若要返回的追踪⽬标框的长度为0,,则返回空数组
return np.empty((0, 5))
总结
了解sort进⾏多⽬标跟踪的实现
学习目标
yoloV3以V1,V2为基础进行的改进,主要有: 利用多尺度特征进行目标测; 先验框更丰富; 调整了网络结构; 对象分类使用logistic代替了softmax,更适用于多标签分类任务。
YOLOv3是YOLO(You Only Look Once)系列目标检测算法中的第三版,相比之前的算法,尤其是针对小目标,精度有显著提升。
yoloV3的流程如下图所示,对于每一幅输入图像,YOLOv3会预测三个不同尺度的输出,目的是检测出不同大小的目标
通常一幅图像包含各种不同的物体,并且有大有小。比较理想的是一次就可以将所有大小的物体同时检测出来。因此,网络必须具备能够“看到”不同大小的物体的能力。因为网络越深,特征图就会越小,所以网络越深小的物体也就越难检测出来。
在实际的feature map中,随着网络深度的加深,浅层的feature map中主要包含低级的信息(物体边缘,颜色,初级位置信息等),深层的feature map中包含高等信息 (例如物体的语义信息: 狗,猫,汽车等等)。因此在不同级别的featuremap对应不同的scale,所以我们可以在不同级别的特征图中进行目标检测。如下图展示了多种scale变换的经典方法。
(a) 这种方法首先建立图像金字塔,不同尺度的金字塔图像被输入到对应的网络当中,用于不同scale物体的检测。但这样做的结果就是每个级别的金字塔都需要进行一次处理,速度很慢,在SPPNet使用的就是这种方式
(b)检测只在最后一层feature map阶段进行,这个结构无法检测不同大小的物体
© 对不同深度的feature map分别进行目标检测。SSD中采用的便是这样的结构。这样小的物体会在浅层的feature map中被检测出来,而大的物体会在深层的feature map被检测出来,从而达到对应不同scale的物体的目的,缺点是每一个feature map获得的信息仅来源于之前的层,之后的层的特征信息无法获取并加以利用。
(d) 与©很接近,但不同的是,当前层的feature map会对未来层的feature map进行上采样,并加以利用。因为有了这样一个结构,当前的feature map就可以获得“未来”层的信息,这样的话低阶特征与高阶特征就有机融合起来了,提升检测精度。在YOLOv3中,就是采用这种方式来实现目标多尺度的变换的。
在基本的图像特征提取方面,YOLO3采用了Darknet-53的网络结构 (含有53个卷积层),它借鉴了残差网络ResNet的做法,在层之间设置了shortcut,来解决深层网络梯度的问题,shortcut如下图所示: 包含两个卷积层和一个shortcut connections
YoloV3的模型结构如下所示:
整个v3结构里面,没有池化层和全连接层,网络的下采样是通过设置卷积的stride为2来达到的,每当通过这个卷积层之后图像的尺寸就会减小到一半。残差模块中的1x,2x,8x,8x 等表示残差模块的个数。
YOLOv3的网络结构由基础特征提取网络、multi-scale特征融合层和输出层组成。
yoloV3采用K-means聚类得到先验框的尺寸,为每种尺度设定3种先验框,总共聚类出9种尺寸的先验框。
在COCO数据集这9个先验框是: (10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。在最小的(13x13)特征图上(有最大的感受野) 应用较大的先验框(116x90),(156x198),(373x326),适合检测较大的对象。中等的(26x26)特征图上 (中等感受野) 应用中等的先验框(30x61),(62x45),(59x119),适合检测中等大小的对象。较大的(52x52)特征图上(较小的感受野) 应用,其中较小的先验框(10x13),(16x30),(33x23),适合检测较小的对象。
直观上感受9种先验框的尺寸,下图中蓝色框为聚类得到的先验框。黄色框式ground truth,红框是对象中心点所在的网格。
预测对象类别时不使用softmax,而是被替换为一个1x1的卷积层+logistic激活函数的结构。使用softmax层的时候其实已经假设每个输出仅对应某一个单个的class.但是在某些class存在重叠情况 (例如woman和person)的数据集中,使用softmax就不能使网络对数据进行很好的预测。
YoloV3的输⼊输出形式如下图所示:
输入416x416x3的图像,通过darknet网络得到三种不同尺度的预测结果,每个尺度都对应N个通道,包含着预测的信息;
每个网格每个尺寸的anchors的预测结果。
YOLOv3共有13x13x3 + 26x26x3 + 52x52x3个预测 。每个预测对应85维,分别是4 (坐标值)、1 (置信度分数) 、80 (coco类别概率)
在这里我们进行的目标检测是基于OPenCV的利用voloV3进行目标检测,不涉及yoloV3的模型结构、理论及训练过程,只是利用训练好的模型进行目标检测,整个流程如下:
基于OPenCV中的DNN模块
绘制最终检测结果,并存入到ndarray中,供目标追踪使用
代码如下:
加载yolov3模型及其权重参数
# 1.加载可以识别物体的名称,将其存放在LABELS中,⼀共有80种,在这我们只使⽤car
labelsPath = "./yolo-coco/coco.names"
LABELS = open(labelsPath).read().strip().split("\n")
# 设置随机数种⼦,⽣成多种不同的颜⾊,当⼀个画⾯中有多个⽬标时,使⽤不同颜⾊的框将其框起来
np.random.seed(42)
COLORS = np.random.randint(0, 255, size=(200, 3),dtype="uint8")
# 加载已训练好的yolov3⽹络的权重和相应的配置数据
weightsPath = "./yolo-coco/yolov3.weights"
configPath = "./yolo-coco/yolov3.cfg"
# 加载好数据之后,开始利⽤上述数据恢复yolo神经⽹络(Darknet对应yolov3模型)
net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)
# 获取YOLO中每⼀⽹络层的名称:['conv_0', 'bn_0', 'relu_0', 'conv_1', 'bn_1', 'relu_1','conv_2']
ln = net.getLayerNames()
# 获取输出层在⽹络中的索引位置,并以列表的形式:['yolo_82', 'yolo_94', 'yolo_106']
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
要处理的图像转换成输入到模型中的blobs
# 2. 读取图像
frame = cv2.imread("./images/car1.jpg")
# 视频的宽度和⾼度,即帧尺⼨
(W, H) = (None, None)
if W is None or H is None:
(H, W) = frame.shape[:2]
# 根据输⼊图像构造blob,利⽤OPenCV进⾏深度⽹路的计算时,⼀般将图像转换为blob形式,对图⽚进⾏预处理,包括缩放、减均值、通道变换等
# 还可以设置尺⼨,⼀般设置为在进⾏⽹络训练时的图像的⼤⼩
blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False)
利⽤模型对⽬标进⾏检测
# 3.将blob输⼊到前向⽹络中,并进⾏预测
net.setInput(blob)
start = time.time()
# yolo前馈计算,获取边界和相应的概率
# 输出layerOutsputs介绍:
# 是YOLO算法在图⽚中检测到的bbx的信息
# 由于YOLO v3有三个输出,也就是上⾯提到的['yolo_82', 'yolo_94', 'yolo_106']
# 因此layerOutsputs是⼀个⻓度为3的列表
# 其中,列表中每⼀个元素的维度是(num_detection, 85)
# num_detections表示该层输出检测到bbx的个数
# 85:因为该模型在COCO数据集上训练,[5:]表示类别概率;[0:4]表示bbx的位置信息;[5]表示置信度
layerOutputs = net.forward(ln)
遍历检测结果,获得检测框
# 判定每⼀个bbx的置信度是否⾜够的⾼,以及执⾏NMS算法去除冗余的bbx
boxes = [] # ⽤于存放识别物体的框的信息,包括框的左上⻆横坐标x和纵坐标y以及框的⾼h和宽w
confidences = [] # 表示识别⽬标是某种物体的可信度
classIDs = [] # 表示识别的⽬标归属于哪⼀类,['person', 'bicycle', 'car', 'motorbike',...]
# 4. 遍历每⼀个输出层的输出
for output in layerOutputs:
# 遍历某个输出层中的每⼀个⽬标
for detection in output:
scores = detection[5:] # 当前⽬标属于某⼀类别的概率
classID = np.argmax(scores) # ⽬标的类别ID
confidence = scores[classID] # 得到⽬标属于该类别的置信度
# 只保留置信度⼤于0.3的边界框,若图⽚质量较差,可以将置信度调低⼀点
if confidence > 0.3:
# 将边界框的坐标还原⾄与原图⽚匹配,YOLO返回的是边界框的中⼼坐标以及边界框的宽度
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int") # 使⽤ astype
x = int(centerX - (width / 2)) # 计算边界框的左上⻆的横坐标
y = int(centerY - (height / 2)) # 计算边界框的左上⻆的纵坐标
# 更新检测到的⽬标框,置信度和类别ID
boxes.append([x, y, int(width), int(height)]) # 将边框的信息添加到列表boxes
confidences.append(float(confidence)) # 将识别出是某种物体的置信度添加到confidences
classIDs.append(classID) # 将识别物体归属于哪⼀类的信息添加到列表classIDs
⾮极⼤值抑制
# 5. ⾮极⼤值抑制
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.3)
最终检测结果,绘制,并存入到ndarray中,供目标追踪使用
# 6. 获得最终的检测结果
dets = [] # 存放检测框的信息,包括左上⻆横坐标,纵坐标,右下⻆横坐标,纵坐标,以及检测到的物体的置信度,用于目标跟踪
if len(idxs) > 0: # 存在检测框的话(即检测框个数⼤于0)
for i in idxs.flatten(): # 循环检测出的每⼀个box
# yolo模型可以识别很多⽬标,因为我们在这⾥只是识别⻋,所以只有⽬标是⻋的我们进⾏检测,其他的忽略
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]]) # 将检测框的信息的放入dets中
# 设置数据类型,将整型数据转换为浮点数类型,且保留⼩数点后三位
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
# 将检测框数据转换为ndarray,其数据类型为浮点型
dets = np.asarray(dets)
plt.imshow(frame[:,:,::-1])
在视频中进⾏⽬标检测:
labelsPath = "./yolo-coco/coco.names"
LABELS = open(labelsPath).read().strip().split("\n")
# 设置随机数种⼦,⽣成多种不同的颜⾊,当⼀个画⾯中有多个⽬标时,使⽤不同颜⾊的框将其框起来
np.random.seed(42)
COLORS = np.random.randint(0, 255, size=(200, 3),dtype="uint8")
# 加载已训练好的yolov3⽹络的权重和相应的配置数据
weightsPath = "./yolo-coco/yolov3.weights"
configPath = "./yolo-coco/yolov3.cfg"
# 加载好数据之后,开始利⽤上述数据恢复yolo神经⽹络
net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)
# 获取YOLO中每⼀⽹络层的名称:['conv_0', 'bn_0', 'relu_0', 'conv_1', 'bn_1', 'relu_
ln = net.getLayerNames()
# 获取输出层在⽹络中的索引位置,并以列表的形式:['yolo_82', 'yolo_94', 'yolo_106']
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
"""
视频处理类
"""
# 初始化vediocapture类,参数指定打开的视频⽂件,也可以是摄像头
vs = cv2.VideoCapture('./input/test_1.mp4')
# 视频的宽度和⾼度,即帧尺⼨
(W, H) = (None, None)
# 视频⽂件写对象
writer = None
try:
# 确定获取视频帧数的⽅式
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 frames in video".format(total))
except:
print("[INFO] could not determine # of frames in video")
print("[INFO] no approx. completion time can be provided")
total = -1
# 循环读取视频中的每⼀帧画⾯
while True:
# 读取帧:grabbed是bool,表示是否成功捕获帧,frame是捕获的帧
(grabbed, frame) = vs.read()
# 若未捕获帧,则退出循环
if not grabbed:
break
# 若W和H为空,则将第⼀帧画⾯的⼤⼩赋值给他
if W is None or H is None:
(H, W) = frame.shape[:2]
# 根据输⼊图像构造blob,利⽤OPenCV进⾏深度⽹路的计算时,⼀般将图像转换为blob形式,对图⽚
# 还可以设置尺⼨,⼀般设置为在进⾏⽹络训练时的图像的⼤⼩
blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False)
# 将blob输⼊到前向⽹络中
net.setInput(blob)
start = time.time()
# yolo前馈计算,获取边界和相应的概率
layerOutputs = net.forward(ln)
"""
输出layerOutsputs介绍:
是YOLO算法在图⽚中检测到的bbx的信息
由于YOLO v3有三个输出,也就是上⾯提到的['yolo_82', 'yolo_94', 'yolo_106']
因此layerOutsputs是⼀个⻓度为3的列表
其中,列表中每⼀个元素的维度是(num_detection, 85)
num_detections表示该层输出检测到bbx的个数
85:因为该模型在COCO数据集上训练,[5:]表示类别概率;[0:4]表示bbx的位置信息;[5]表示置
"""
end = time.time()
"""
下⾯对⽹络输出的bbx进⾏检查:
判定每⼀个bbx的置信度是否⾜够的⾼,以及执⾏NMS算法去除冗余的bbx
"""
boxes = [] # ⽤于存放识别物体的框的信息,包括框的左上⻆横坐标x和纵坐标y以及框的⾼h和宽
confidences = [] # 表示识别⽬标是某种物体的可信度
classIDs = [] # 表示识别的⽬标归属于哪⼀类,['person', 'bicycle', 'car', 'moto
# 遍历每⼀个输出层的输出
for output in layerOutputs:
# 遍历某个输出层中的每⼀个⽬标
for detection in output:
scores = detection[5:] # 当前⽬标属于某⼀类别的概率
"""
# scores = detection[5:] ---> [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
# 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
# 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0
# 0. 0. 0. 0. 0. 0. 0. 0.]
# scores的⼤⼩应该是1*80,因为在训练yolo模型时是80类⽬标
"""
classID = np.argmax(scores) # ⽬标的类别ID
confidence = scores[classID] # 得到⽬标属于该类别的置信度
# 只保留置信度⼤于0.3的边界框,若图⽚质量较差,可以将置信度调低⼀点
if confidence > 0.3:
# 将边界框的坐标还原⾄与原图⽚匹配,YOLO返回的是边界框的中⼼坐标以及边界框的宽度和高度
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int") # 使⽤ astype("int")对上述
x = int(centerX - (width / 2)) # 计算边界框的左上⻆的横坐标
y = int(centerY - (height / 2)) # 计算边界框的左上⻆的纵坐标
# 更新检测到的⽬标框,置信度和类别ID
boxes.append([x, y, int(width), int(height)]) # 将边框的信息添加
confidences.append(float(confidence)) # 将识别出是某种物体的置信度
classIDs.append(classID) # 将识别物体归属于哪⼀类的信息添加到列表class
# 上⼀步中已经得到yolo的检测框,但其中会存在冗余的bbox,即⼀个⽬标对应多个检测框,所以使
# 利⽤OpenCV内置的NMS DNN模块实现即可实现⾮最⼤值抑制 ,所需要的参数是边界 框、 置信度
# 第⼀个参数是存放边界框的列表,第⼆个参数是存放置信度的列表,第三个参数是⾃⼰设置的置信度
# 返回的idxs是⼀个⼀维数组,数组中的元素是保留下来的检测框boxes的索引位置
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.3)
dets = [] # 存放检测框的信息,包括左上⻆横坐标,纵坐标,右下⻆横坐标,纵坐标,以及检测
if len(idxs) > 0: # 存在检测框的话(即检测框个数⼤于0)
for i in idxs.flatten(): # 循环检测出的每⼀个box
# yolo模型可以识别很多⽬标,因为我们在这⾥只是识别⻋,所以只有⽬标是⻋的我们进⾏
if LABELS[classIDs[i]] == "car":
(x, y) = (boxes[i][0], boxes[i][1]) # 得到检测框的左上⻆坐标
(w, h) = (boxes[i][2], boxes[i][3]) # 得到检测框的宽和⾼
dets.append([x, y, x + w, y + h, confidences[i]]) # 将检测框的信息放入dets中
# 设置数据类型,将整型数据转换为浮点数类型,且保留⼩数点后三位
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
# 将检测框数据转换为ndarray,其数据类型为浮点型
dets = np.asarray(dets)
yoloV3的多尺度检测方法
在YOLOv3中采用FPN结构来提高对应多尺度目标检测的精度,当前的feature map利用“未来”层的信息,将低阶特征与高阶特征进行融合,提升检测精度。
yoloV3模型的网络结构
以darknet-53为基础,借鉴resnet的思想,在网络中加入了残差模块,利于解决深层次网络的梯度问题
整个v3结构里面,没有池化层和全连接层,只有卷积层
网络的下采样是通过设置卷积的stride为2来达到的
yoloV3模型先验框设计的方法
采用K-means聚类得到先验框的尺寸,为每种尺度设定3种先验框,总共聚类出9种尺寸的先验框。
yoloV3模型为什么适用于多标签的目标分类
因为预测对象类别时不使用softmax,而是使用logistic的输出进行预测
yoloV3模型的输入输出
对于416x416x3的输入图像,在每个尺度的特征图的每个网格设置3个先验框,总共有13x13x3 +26x26x3 + 52x52x3= 10647 个预测。每一个预测是一个(4+1+80)=85维向量,这个85维向量包含边框坐标 (4个数值),边框置信度 (1个数值),对象类别的概率 (对于COCO数据集,有80种对象)
基于OPenCV的DNN模块利用yoloV3模型进行目标检测
学习目标
基于虚拟线圈的车流量统计算法原理与交通道路上的常见的传统的物理线圈类似由于物理线圈需要埋设在路面之下,因此会有安装、维护费用高,造成路面破坏等问题,而采用基于视频的虚拟线圈的车辆计数方法完全避免了以上问题,且可以针对多个感兴趣区域进行检测。
虚拟线圈车辆计数法的原理是在采集到的交通流视频中,在需要进行车辆计数的道路或路段上设置一条或一条以上的检测线对通过车辆进行检测,从而完成计数工作。检测线的设置原则一般是在检测车道上设置一条垂直于车道线,居中的虚拟线段,通过判断其与通过车辆的相对位置的变化,完成车流量统计的工作。如下图所示,绿色的线就是虚拟检测线:
在该项目中我们进行检测的方法是,计算前后两帧图像的车辆检测框的中心点连线,若该连线与检测线相交,则计数加一,否则计数不变。那怎么判断两条线段是否相交呢?
假设有两条线段AB,CD,若AB,CD相交,我们可以确定
1.线段AB与CD所在的直线相交,即点A和点B分别在直线CD的两边
2.线段CD与AB所在的直线相交,即点C和点D分别在直线AB的两边
上面两个条件同时满足是两线段相交的充要条件,所以我们只需要证明点A和点B分别在直线CD的两边,点C和点D分别在直线AB的两边,这样便可以证明线段AB与CD相交了。
在上图中,线段AB与线段CD相交,于是我们可以得到两个向量AC,AD,C和D分别在AB的两边,向量AC在向量AB的逆时针方向,ABxAC > 0;向量AD在向量AB的顺时针方向,ABxAD < 0,两叉乘结果异号。
这样,方法就出来了:如果线段CD的两个端点C和D,与另一条线段的一个端点(A或B,只能是其中一个) 连成的向量,与向量AB做叉乘,若结果异号,表示C和D分别在直线AB的两边,若结果同号,则表示CD两点都在AB的一边,则肯定不相交。
所以我们利用叉乘的方法来判断车辆是否经过检测线
实现车流量检测的代码如下
检测AB和CD两条直线是否相交
# 计算有A,B,C三点构成的向量CA,BA之间的关系,
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)
遍历跟踪框判断其与检测线是否相交,并进行车辆计数
# 基准线
line = [(8,150),(2560,158)]
# 车辆总数
counter = 0
#正向车道的车辆数据
counter_up = 0
#逆向车道的车辆数据
counter_down = 0
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])) # 获取上⼀帧画⾯的宽和高
p0 = (int(x + (w - x) / 2), int(y + (h - y) / 2)) # 获取当前帧检测框的中⼼点
p1 = (int(x2 + (w2 - x2) / 2), int(y2 + (h2 - y2) / 2)) # 获取上⼀帧检测框的中心点
cv2.line(frame, p0, p1, color, 3) # 将前后两帧图像的检测结果中⼼连接起来
"""
进⾏碰撞检测-前后两帧检测框中⼼点的连线穿过基准线,则进⾏计数
:param line:基准线
:param p0-p1:构成的线
"""
if intersect(p0, p1, line[0], line[1]):
# 总计数加1
counter += 1
# 判断⾏进的⽅向
if y2 < y:
counter_down += 1 # 逆向⾏驶+1
else:
counter_up += 1 # 正向⾏驶+1
i += 1
总结
学习目标
前面我们已经完成了视频中车辆的检测功能,下面我们对车辆进行跟踪,并将跟踪结果绘制在视频中
主要分为以下步骤:
对目标进行追踪
# yolo中检测结果为0时,传⼊跟踪器中会出现错误,在这⾥判断下,未检测到⽬标时不进⾏⽬标追踪
if np.size(dets) == 0:
continue
else:
tracks = tracker.update(dets) # 将检测结果传⼊跟踪器中,返回当前画⾯中跟踪成功,包含5个信息
# 对跟踪器返回的结果进⾏处理
boxes = [] # 存放tracks中的前四个值:⽬标框的左上⻆横纵坐标和右下⻆的横纵坐标
indexIDs = [] # 存放tracks中的最后⼀个值:置信度,⽤来作为memory中跟踪框的Key
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] # 将跟踪框以key为:置信度,value为:跟踪框坐标形式存入memory中
绘制⻋辆计数结果
# 绘制⻋辆计数的相关信息
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)
# 将处理后的帧写⼊到视频中
writer.write(frame)
# 显示当前帧的结果
cv2.imshow("", frame)
# 按下q键退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
总结
项目简介
汽车的日益普及在给人们带来极大便利的同时,也导致了拥堵的交通路况,以及更为频发的交通事故。而自动驾驶技术的出现可以有效的缓解了此类问题,减少交通事故,提升出行效率。自动驾驶的首要任务就是准确的识别出车道线并根据车道线的指示进行行驶。
国内外检测车道线的方法主要有两类:一类是基于模型的检测方法,还有一类是基于特征的检测方法
本项目针对车载相机获得的道路图像进行提取
学习目标
我们所处的世界是三维的,而照片是二维的,我们可以把相机认为是一个函数,输入量是一个场景,输出量是一幅灰度图。这个从三维到二维的过程的函数是不可逆的。
相机标定的⼀个⽬的是要找⼀个合适的数学模型,求出这个模型的参数,能够近似从三维到⼆维的过程,使这个三维到⼆维的过程的函数找到反函数。
这个逼近的过程就是相机标定,我们⽤简单的数学模型来表达复杂的成像过程。
由此可知,相机标定的一个目的就是建立像素坐标系和世界坐标系之间的关系。原理是根据摄像机的模型,由已知特征点的图像坐标求解摄像机的模型参数,并求出成像的反过程,从而从图像中恢复出空间点的三维坐标,达到三维重建的目的。
另外相机标定还可以进行图像校正,由于透镜的制造精度以及组装工艺的偏差会引入畸变,导致图形失真,所以我们可以求解畸变参数,对图像进行去畸变。
在介绍相机标定之前,我们首先来看下相机的成像模型。也就是,现实物体上的个点在相机采集到的图像中所在的位置是怎么确定的。我们采用的模型是针孔模型,也就是小孔成像。
小孔成像是利用了光线直线传播的原理。比如说,远处有一棵大树,而我们有个盒子,在这个盒子的对着大树的那一面上用针尖戳一个小孔。我们任选这棵大树上的任何的一个点,它都会向四周去反射无数条光线。但是因为光线是直线传播的,所以这些光线要么没有一条能进入盒子中,要么就只有一条光线是进入到这个盒子里面的。进入到盒子中的光线会在盒子里的一面上形成一个光点。这个光点跟大树上的某个点是对应的,颜色也是一致的,这就建立了一对一的关系。如果我们把感光胶片或者感光的传感器放在盒子里,就可以做成一个针孔相机来得到大树的彩色图像了。如下图所示:
由于大树上每个点反射的无数条光线只有一条进入到盒子中,所以图像是很暗的。而加大孔径,虽然可以提高图像的亮度,却会使物体上的某一个点会反射一小束光进入到相机里。这一小束光会在感光传感器上形成一个光斑,而不是一个点;从而相机失去了物体与图像上的点的一一对应关系,进而导致图像模糊甚至无法成像。当然实际的针孔相机不可能是让每个点只有一条光线进入相机。因为光具有波粒二象性,是可以衍射的,所以很小的针孔,也会导致图像模糊。根据可见光的波长理论计算的小孔最佳直径是 0.25mm 左右,相应的光圈值大概是 f/200
所以用一个直径比针孔直径大许多的凸透镜来替代针孔。凸透镜可以把物体上的一个点所反射的那一小束通过透镜的光重新汇聚成一个点,这样,不但图像亮度增大了,而且物体和图像上点的又可以一一对应起来了。这就是我们现在常用的相机的基本工作原理。
注意:也有很多人把图像坐标系和像素坐标系合在一起,称作三大坐标系,也有人分开,称为四大坐标系。
下面我们进行一系列的变换,引入多个参数矩阵,实现从世界坐标系到像素坐标的转换。已知一个现实世界中的物体点的在世界坐标系中的坐标为 (Xw,Yw,Zw)经过相机拍摄得到图片,在图片上的像素坐标为(u,v)。假设在图像坐标系中的坐标为(x,y),在相机坐标系中的坐标为 (Xc,Yc,Zc)。各个坐标之间的转化流程如下图所示:
世界坐标系转换到相机坐标系时不会产生形变,所以将世界坐标系进行刚体变换就可转换为相机坐标系。三维空间中,当物体不发生形变时,而只进行旋转平移的运动,就是刚体变换。空间中的一个坐标系总可以通过刚体变换,即平移和旋转,就可以转换为另一个坐标系,如下图所示:
世界坐标系转换到相机坐标系时不会产生形变,所以将世界坐标系进行刚体变换就可转换为相机坐标系。三维空间中,当物体不发生形变时,而只进行旋转平移的运动,就是刚体变换。空间中的一个坐标系总可以通过刚体变换,即平移和旋转,就可以转换为另一个坐标系,如下图所示
两个坐标系间刚体变换的数学表达式如下所示:
可以将其写为⻬次坐标的形式:
其中:XC代表摄像机坐标系,Xw代表世界坐标系。R是3*3的正交单位矩阵(即旋转矩阵,r1/r2/r3表示在x/y/z轴是旋转矩阵),t为平移矩阵,表示可以理解为两个坐标原点之间的距离。R、与摄像机无关,所以这两个参数为摄像机的外参数(extrinsic parameter)。一般情况下,我们假定在世界坐标系中物点所在平面过世界坐标系原点且与Zw轴垂直 (也即图像平面与Xw-Yw平面重合,目的在于方便后续计算),则Zw=0.
下面我们看下旋转矩阵的计算,现在假设坐标系沿着Z轴旋转,如下图所示:
则有:(假设其中x’,y’表示世界坐标系,x,y是相机坐标系)同理,绕x,y轴旋转⻆度φ时,则有:
所以旋转矩阵:
因为R受x,y,z三个⽅向上的分量的控制,所以具有三个⾃由度。
从相机坐标系到图像坐标系,是从3d到2d的过程,属于透射投影关系:
如上图所示,图像坐标系为o-xy,摄像机坐标系为Oc-xcyczc。记空间点P在相机坐标系中的坐标为(Xc, Yc, Zc),在图像中的坐标是(x,y),根据三⻆形相似定理 有:
我们把上式写成矩阵形式,并使⽤⻬次坐标,则有:
由于⻬次坐标的伸缩不变性,z [x, y, 1]T 和(x, y, 1)T 表示的是同⼀点。
像素坐标系和图像坐标系都在成像平⾯上,只是各⾃的原点和度量单位不⼀样。图像坐标系的原点为相机光轴与成像平⾯的交点,通常情况下是成像平⾯的中点。图像坐标系的单位是mm,属于物理单位,⽽像素坐标系的原点在图像的左上⻆,单位是pixel,也就是我们说的⼏⾏⼏列。如下图所示: 所以这⼆者之间的转换如下:
其中dx和dy表示每⼀列和每⼀⾏分别代表多少mm,即1pixel=dx mm。我们将其⽤⻬次坐标表示,并写成矩阵形式:
我们已经介绍了各个坐标系之间的转换过程,但是我们想知道的是如何从世界坐标系转换到像素坐标系,因此我们需要把上⾯介绍到的联系起来,将三者相乘,可以把这三个过程和在⼀起,写成⼀个矩阵:
我们假设转换矩阵为P,则有:
内参:(相机坐标 到 图像坐标 到像素坐标的相关参数)确定相机从三维空间到⼆维图像的投影关系,畸变系数也属于内参,我们在下⾯进⾏介绍。
外参:(世界坐标 到 相机坐标 的参数)决定相机坐标与世界坐标系之间相对位置关系,主要包括旋转和平移两部分。
我们在相机坐标系到图像坐标系变换时谈到透视投影。相机拍照时通过透镜把实物投影到像平面上,但是透镜由于制造精度以及组装工艺的偏差会引入畸变,导致原始图像的失真。因此我们需要考虑成像畸变的问题。透镜的畸变主要分为径向畸变和切向畸变两类
顾名思义,径向畸变就是沿着透镜半径方向分布的畸变,产生原因是光线在原理透镜中心的地方比靠近中心的地方更加弯曲,这种畸变在普通的镜头中表现更加明显,径向畸变主要包括桶形畸变和枕形畸变两种。以下分别是枕形和桶形畸变示意图:(从左到右依次是:正常无畸变,桶形畸变和枕形畸变)从上图中可以看出,径向畸变以某一个中心往外延伸,且越往外,畸变越大;显然畸变与距离成一种非线性的变换关系,可以用多项式来近似。径向畸变的数学模型可以用主点(principle point)周围的泰勒级数展开式的前几项进行描述,通常使用前两项,即k1和k2,对于畸变很大的镜头,如鱼眼镜头,可以增加使用第三项k3来进行描述,成像仪上某点根据其在径向方向上的分布位置,调节公式为:
式里(x0,y0)是畸变点在像平面的原始位置, (x,y)是畸变较正后新的位置下图是距离光心不同距离上的点经过透镜径向畸变后点位的偏移示意图,可以看到,距离光心越远,径向位移越大,表示畸变也越大,在光心附近,几乎没有偏移
切向畸变是由于透镜本身与相机传感器平面(像平面)或图像平面不平行而产生的,这种情况多是由于透镜被粘贴到镜头模组上的安装偏差导致。在相机传感器和镜头不平行的情况下,因为存在夹角,所以光透过镜头传到图像传感器上时,成像位置发生了变化,如下图所示:
畸变模型可以⽤两个额外的参数p1和p2来描述:下图显示某个透镜的切向畸变示意图,⼤体上畸变位移相对于左下—右上⻆的连线是对称的,这跟凸透镜与传感器之间的夹⻆有关:
径向畸变和切向畸变模型中⼀共有5个畸变参数:k1、k2、p1、p2、k3,得到这五个参数,就可以进⾏图像的去畸变。这些都属于相机的内参。
相机标定方法一般分为三类,分为传统的标定算法,自标定法和基于主动视觉的标定法。分别介绍如下:
张氏标定法是张正友博士在1999年发表在国际顶级会议ICCV上的论文《FlexibleCamera Calibration By Viewing a Plane From Unknown Orientations》中,提出的一种利用平面棋盘格进行相机标定的实用方法。
该方法介于传统标定法和自标定法之间,既克服了传统标定法需要的高精度三维标定物的缺点,又解决了自标定法鲁棒性差的难题。标定过程不需要特殊的标定物只需使用一张打印出来的棋盘格,并从不同方向拍摄几组图片即可,不仅实用灵活方便,而且精度很高,鲁棒性好。因此很快被全世界广泛采用,极大的促进了三维计算机视觉从实验室走向真实世界的进程。
棋盘是一块由黑白方块间隔组成的标定板,我们用它来作为相机标定的标定物(从真实世界映射到数字图像内的对象)。之所以我们用棋盘作为标定物是因为平面棋盘模式更容易处理(相对于复杂的三维物体),但与此同时,二维物体相对于三维物体会缺少一部分信息,于是我们会多次改变棋盘的方位来捕捉图像,以求获得更丰富的坐标信息。如下图所示,是相机在不同方向下拍摄的同一个棋盘图像。如下图所示:
张氏校正法是基于平面棋盘格的标定,首先我们介绍下两个平面中的单应性映射在计算机视觉中,单应性(Homography) 指从一个平面到另一个平面的投影映射,所以在标定物平面与图像平面之间存在单应性。
上文中我们已经得到了像素坐标系和世界坐标系下的坐标映射关系,因为标定物是平面,我们假设标定棋盘位于世界坐标中Z=0平面,然后进行单应性计算。化简前文中的公式有:
其中: u,v表示像素坐标系中的坐标,矩阵A是内参矩阵,其中a=f/dx,B=f/dy,u0,x0,Y(由于制造误差产生的两个坐标轴偏斜参数,通常很小,如果按上文中矩阵运算得到的值即为0)表示5个相机内参,r1,r2,示相机外参,xw,yw,zw 表示世界坐标系中的坐标.a,B和物理焦距f之间的关系为: a =fsx和B=fsy。其中sx=1/dx表示x方向上的1毫米长度所代表像素值,即像素/单位毫米,a,B是在相机标定中整体计算出来的。
⾸先我们回顾下极⼤似然估计:极⼤似然估计是⼀种估计总体未知参数的⽅法。它主要⽤于点估计问题。所谓点估计是指⽤⼀个估计量的观测值来估计未知参数的真值,即在参数空间中选取使得样本取得观测值的概率最⼤的参数。
张氏标定就是利用一张打印的棋盘格,然后对每个角点进行标记其在像素坐标系的像素点坐标,以及在世界坐标系的坐标,张氏标定证明通过4组以上的点就可以求解出H矩阵的值,但是为了减少误差,具有更强的鲁棒性,我们一般会拍摄许多张照片,选取大量的角点进行标定。具体过程如下:
相机标定的意义
建立世界坐标系和像素坐标之间的关系,可用于三维重建,图像校正等
成像原理
小孔成像
相机成像模型
图像畸变
相机标定方法分类
传统的标定方法,自标定法,基于主动视觉的标定方法
张氏标定法
利用棋盘格图像对相机进行标定:单应性矩阵,利用约束条件求解内参矩阵根据内参矩阵估计外参矩阵,利用极大似然方法估计参数,优化方法:牛顿法,高斯牛顿法和LM算法
标定流程:
双目校正
双目较正,就是利用单目校正得到每个相机的参数后,在计算两个相机之间的相对位置。
学习目标
根据张正友校正算法,利用棋盘格数据校正对车载相机进行校正,计算其内参矩阵,外参矩阵和畸变系数。
标定的流程是:
标定的图片需要使用棋盘格数据在不同位置、不同角度、不同姿态下拍摄的图片最少需要3张,当然多多益善,通常是10-20张。该项目中我们使用了20张图片,如下图所示:
把这些图⽚存放在项⽬路径中的camera_cal⽂件夹中。
下⾯我们对相机进⾏校正,OPenCV中提供了对相机进⾏校正的代码,在本项⽬中直接使⽤opencv中的API进⾏相机的校正,如下所示:
# 1. 参数设定:定义棋盘横向和纵向的⻆点个数并指定校正图像的位置
nx = 9
ny = 6
file_paths = glob.glob("./camera_cal/calibration*.jpg")
# 2. 计算相机的内外参数及畸变系数
def cal_calibrate_params(file_paths):
object_points = [] # 三维空间中的点:3D
image_points = [] # 图像空间中的点:2d
# 2.1 ⽣成真实的交点坐标:类似(0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)的三维点
objp = np.zeros((nx * ny, 3), np.float32)
objp[:, :2] = np.mgrid[0:nx, 0:ny].T.reshape(-1, 2)
# 2.2 检测每幅图像⻆点坐标
for file_path in file_paths:
img = cv2.imread(file_path)
# 将图像转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# ⾃动检测棋盘格内4个棋盘格的⻆点(2⽩2⿊的交点)
rect, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
# 若检测到⻆点,则将其存储到object_points和image_points
if rect == True:
object_points.append(objp)
image_points.append(corners)
# 2.3 获取相机参数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(object_points, image_points, gray.shape[::-1], None, None)
return ret, mtx, dist, rvecs, tvecs
在这⾥有⼏个API给⼤家介绍下:
寻找棋盘图中的棋盘⻆点
rect, corners = cv2.findChessboardCorners(image, pattern_size, flags)
参数:
返回:
检测完角点之后我们可以将将测到的角点绘制在图像上,使用的API是
cv2.drawChessboardCorners(img,pattern size, corners, rect)
参数:
注意:如果发现了所有的角点,那么角点将用不同颜色绘制(每行使用单独的颜色绘制),并且把角点以一定顺序用线连接起来,如下图所示
利⽤定标的结果计算内外参数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(object_points, image_points
参数:
返回:
上⼀步中我们已经得到相机的内参及畸变系数,我们利⽤其进⾏图像的去畸变,最直接的⽅法就是调⽤opencv中的函数得到去畸变的图像:
def img_undistort(img, mtx, dist):
dst = cv2.undistort(img, mtx, dist, None, mtx)
return dst
我们看下求畸变的API:
dst = cv2.undistort(img, mtx, dist, None, mtx)
参数:
学习目标
我们基于图像的梯度和颜色特征,定位车道线的位置
在这里选用Sobel边缘提取算法,Sobel相比于Canny的优秀之处在于,它可以选择横向或纵向的边缘进行提取。从车道的拍摄图像可以看出,我们关心的正是车道线在横向上的边缘突变。OpenCV提供的cv2.Sobel()函数,将进行边缘提取后的图像做二进制图的转化,即提取到边缘的像素点显示为白色(值为1),未提取到边缘的像素点显示为黑色(值为0)。由于只使用边缘检测,在有树木阴影覆盖的区域时,虽然能提取出车道线的大致轮廓,但会同时引入的噪声,给后续处理带来麻烦。所以在这里我们引入颜色阈值来解决这个问题
在车道线检测中,我们使用的是HSL颜色空间,其中H表示色相,即颜色,S表示饱和度,即颜色的纯度,L表示颜色的明亮程度
HSL的H(hue)分量,代表的是人眼所能感知的颜色范围,这些颜色分布在一个平面的色相环上,取值范围是0°到360°的圆心角,每个角度可以代表一种颜色。色相值的意义在于,我们可以在不改变光感的情况下,通过旋转色相环来改变颜色。在实际应用中,我们需要记住色相环上的六大主色,用作基本参照: 360°/0红、60°黄、120°绿、180°青、240°蓝、300°洋红,它们在色相环上按照60°圆心角的间隔排列:
HSL的S(saturation)分量,指的是⾊彩的饱和度,描述了相同⾊相、明度下⾊彩纯度的变化。数值越⼤,颜⾊中的灰⾊越少,颜⾊越鲜艳,呈现⼀种从灰度到纯⾊的变化。因为车道线是⻩⾊或⽩⾊,所以利用s通道阈值来检测车道线。
HSL的L(lightness)分量,指的是色彩的明度,作用是控制色彩的明暗变化。数值越小,色彩越暗,越接近于黑色;数值越大,色彩越亮,越接近于白色
⻋道线提取的代码如下所示:
# ⻋道线提取代码
# 颜色空间转换一》边缘检测一》颜色阈值-》合并并且使用L通道进行白的区的抑制
def pipeline(img, s_thresh=(170, 255), sx_thresh=(40, 200)):
img = np.copy(img)
#1.将图像转换为HLS⾊彩空间,并分离各个通道
hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS).astype(np.float)
h_channel = hls[:, :, 0]
l_channel = hls[:, :, 1]
s_channel = hls[:, :, 2]
#2.利⽤sobel计算x⽅向的梯度
sobelx = cv2.Sobel(l_channel, cv2.CV_64F, 1, 0)
abs_sobelx = np.absolute(sobelx)
# 将导数转换为8bit整数
scaled_sobel = np.uint8(255 * abs_sobelx / np.max(abs_sobelx))
sxbinary = np.zeros_like(scaled_sobel)
sxbinary[(scaled_sobel >= sx_thresh[0]) & (scaled_sobel <= sx_thresh[1])] = 1
# 3.对s通道进⾏阈值处理
s_binary = np.zeros_like(s_channel)
s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 1
# 4. 将边缘检测的结果和颜⾊空间阈值的结果合并,并结合l通道的取值,确定⻋道提取的⼆值图结果
color_binary = np.zeros_like(sxbinary)
color_binary[((sxbinary == 1) | (s_binary == 1)) & (l_channel > 100)] = 1
return color_binary
我们来看下整个流程:
⾸先我们是把图像转换为HLS颜⾊空间,然后利⽤边缘检测和阈值的⽅法检测⻋道线,我们以下图为例,来看下检测结果:
总结:
学习⽬标
为了方便后续的直方图滑窗对车道线进行准确的定位,我们在这里利用透视变换将图像转换成俯视图,也可将俯视图恢复成原有的图像,代码如下:
计算透视变换所需的参数矩阵:
def cal_perspective_params(img, points):
offset_x = 330
offset_y = 0
img_size = (img.shape[1], img.shape[0])
src = np.float32(points)
# 俯视图中四点的位置
dst = np.float32([[offset_x, offset_y], [img_size[0] - offset_x, offset_y]
[offset_x, img_size[1] - offset_y],
[img_size[0] - offset_x, img_size[1] - offset_y]
])
# 从原始图像转换为俯视图的透视变换的参数矩阵
M = cv2.getPerspectiveTransform(src, dst)
# 从俯视图转换为原始图像的透视变换参数矩阵
M_inverse = cv2.getPerspectiveTransform(dst, src)
return M, M_inverse
透视变换:
def img_perspect_transform(img, M):
img_size = (img.shape[1], img.shape[0])
return cv2.warpPerspective(img, M, img_size)
下面我们调用上述两个方法看下透视变换的结果:在原始图像中我们绘制道路检测的结果,然后通过透视变换转换为俯视图
img = cv2.imread("./test/straight_lines2.jpg")
img = cv2.line(img, (601, 448), (683, 448), (0, 0, 255), 3)
img = cv2.line(img, (683, 448), (1097, 717), (0, 0, 255), 3)
img = cv2.line(img, (1097, 717), (230, 717), (0, 0, 255), 3)
img = cv2.line(img, (230, 717), (601, 448), (0, 0, 255), 3)
points = [[601, 448], [683, 448], [230, 717], [1097, 717]]
M, M_inverse = cal_perspective_params(img, points)
transform_img = img_perspect_transform(img, M)
plt.figure(figsize=(20,8))
plt.subplot(1,2,1)
plt.title('原始图像')
plt.imshow(img[:,:,::-1])
plt.subplot(1,2,2)
plt.title('俯视图')
plt.imshow(transform_img[:,:,::-1])
plt.show()
学习目标
我们根据前面检测出的车道线信息,利用直方图和滑动窗口的方法,精确定位车道线,并进行拟合。
下图是我们检测到的车道线结果
沿x轴方向统计每一列中白色像素点的个数,横坐标是图像的列数,纵坐标表示每列中白色点的数量,那么这幅图就是“直方图”,如下图所示:
对比上述两图,可以发现直方图左半边最大值对应的列数,即为左车道线所在的位置,直方图右半边最大值对应的列数,是右车道线所在的位置。
确定左右车道线的大致位置后,使用”滑动窗口“的方法,在图中对左右车道线的点进行搜索。
滑动窗口的搜索过程:
设置搜索窗口大小(width和height) :一般情况下width为手工设定,height为图片大小除以设置搜索窗口数目计算得到。
对每个搜索窗口分别做水平和垂直方向直方图统计,统计在搜索框区域内非零像素个数,并过滤掉非零像素数目小于50的框。
计算非零像素坐标的均值作为当前搜索框的中心,并对这些中心点做一个二阶的多项式拟合,得到当前搜寻对应的车道线曲线参数。
实现代码如下:
def cal_line_param(binary_warped):
# 1.确定左右⻋道线的位置
# 统计直⽅图
histogram = np.sum(binary_warped[:, :], axis=0)
# 在统计结果中找到左右最⼤的点的位置,作为左右⻋道检测的开始点
# 将统计结果⼀分为⼆,划分为左右两个部分,分别定位峰值位置,即为两条⻋道的搜索位置
midpoint = np.int(histogram.shape[0] / 2)
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
# 2.滑动窗⼝检测⻋道线
# 设置滑动窗⼝的数量,计算每⼀个窗⼝的⾼度
nwindows = 9
window_height = np.int(binary_warped.shape[0] / nwindows)
# 获取图像中不为0的点
nonzero = binary_warped.nonzero()
nonzeroy = np.array(nonzero[0])
nonzerox = np.array(nonzero[1])
# ⻋道检测的当前位置
leftx_current = leftx_base
rightx_current = rightx_base
# 设置x的检测范围,滑动窗⼝的宽度的⼀半,⼿动指定
margin = 100
# 设置最⼩像素点,阈值⽤于统计滑动窗⼝区域内的⾮零像素个数,⼩于50的窗⼝不对x的中⼼值进⾏更新
minpix = 50
# ⽤来记录搜索窗⼝中⾮零点在nonzeroy和nonzerox中的索引
left_lane_inds = []
right_lane_inds = []
# 遍历该副图像中的每⼀个窗⼝
for window in range(nwindows):
# 设置窗⼝的y的检测范围,因为图像是(⾏列),shape[0]表示y⽅向的结果,上⾯是0
win_y_low = binary_warped.shape[0] - (window + 1) * window_height
win_y_high = binary_warped.shape[0] - window * window_height
# 左⻋道x的范围
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
# 右⻋道x的范围
win_xright_low = rightx_current - margin
win_xright_high = rightx_current + margin
# 确定⾮零点的位置x,y是否在搜索窗⼝中,将在搜索窗⼝内的x,y的索引存⼊left_lane_ind
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
# 如果获取的点的个数⼤于最⼩个数,则利⽤其更新滑动窗⼝在x轴的位置
if len(good_left_inds) > minpix:
leftx_current = np.int(np.mean(nonzerox[good_left_inds]))
if len(good_right_inds) > minpix:
rightx_current = np.int(np.mean(nonzerox[good_right_inds]))
# 将检测出的左右⻋道点转换为array
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)
# 获取检测出的左右⻋道点在图像中的位置
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
# 3.⽤曲线拟合检测出的点,⼆次多项式拟合,返回的结果是系数
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
return left_fit, right_fit
⻋道线的检测的拟合结果如下图所示:
其中绿色的方框是滑动窗口的结果,中间的黄线是车道线拟合的结果下面我们将车道区域绘制处理,即在检测出的车道线中间绘制多边形,代码如下:
def fill_lane_poly(img, left_fit, right_fit):
# 获取图像的⾏数
y_max = img.shape[0]
# 设置输出图像的⼤⼩,并将⽩⾊位置设为255
out_img = np.dstack((img, img, img)) * 255
# 在拟合曲线中获取左右⻋道线的像素位置
left_points = [[left_fit[0] * y ** 2 + left_fit[1] * y + left_fit[2], y] for y in range(y_max)]
right_points = [[right_fit[0] * y ** 2 + right_fit[1] * y + right_fit[2], y] for y in range(y_max -1,-1,-1)]
# 将左右⻋道的像素点进⾏合并
line_points = np.vstack((left_points, right_points))
# 根据左右⻋道线的像素位置绘制多边形
cv2.fillPoly(out_img, np.int_([line_points]), (0, 255, 0))
return out_img
其中两个方法给大家介绍下
np.vstack:按垂直方向(行顺序) 堆叠数组构成一个新的数组np.dstack:按水平方向(列顺序) 堆叠数组构成一个新的数组
将检测出的车道逆投影到原始图像,直接调用透视变换的方法即可
transform img_inverse = img_perspect_transform(result, M inverse)
学习目标
曲线的曲率就是针对曲线上某个点的切线方向角对弧长的转动率,通过微分来定义,表明曲线偏离直线的程度。数学上表明曲线在某一点的弯曲程度的数值。曲率越大,表示曲线的弯曲程度越大。曲率的倒数就是曲率半径
下面有三个球体,网球、篮球、地球,半径越小的越容易看出是圆的,所以随着半径的增加,圆的程度就越来越弱了
我们根据上述的计算曲率半径的⽅法,代码实现如下:
def cal_radius(img, left_fit, right_fit):
# 图像中像素个数与实际中距离的⽐率
# 沿⻋⾏进的⽅向⻓度⼤概覆盖了30⽶,按照美国⾼速公路的标准,宽度为3.7⽶(经验值)
ym_per_pix = 30 / 720 # y⽅向像素个数与距离的⽐例
xm_per_pix = 3.7 / 700 # x⽅向像素个数与距离的⽐例
# 计算得到曲线上的每个点
left_y_axis = np.linspace(0, img.shape[0], img.shape[0] - 1)
left_x_axis = left_fit[0] * left_y_axis ** 2 + left_fit[1] * left_y_axis + left_fit[2]
right_y_axis = np.linspace(0, img.shape[0], img.shape[0] - 1)
right_x_axis = right_fit[0] * right_y_axis ** 2 + right_fit[1] * right_y_axis + right_fit[2]
# 获取真实环境中的曲线
left_fit_cr = np.polyfit(left_y_axis * ym_per_pix, left_x_axis * xm_per_pix,2)
right_fit_cr = np.polyfit(right_y_axis * ym_per_pix, right_x_axis * xm_per_pix,2)
# 获得真实环境中的曲线曲率
left_curverad = ((1 + (2 * left_fit_cr[0] * left_y_axis * ym_per_pix + left_fit_cr[1])**2)**1.5) \
/np.absolute(2 * left_fit_cr[0])
right_curverad = ((1 + (2 * right_fit_cr[0] * right_y_axis * ym_per_pix + right_fit_cr[1])**2)**1.5) \
/np.absolute(2 * right_fit_cr[0])
# 在图像上显示曲率
cv2.putText(img, 'Radius of Curvature = {}(m)'.format(np.mean(left_curverad)),(20,50),cv2.FONT_ITALIC,1,
(255, 255, 255), 5)
return img
# 1. 定义函数计算图像的中⼼点位置
def cal_line__center(img):
undistort_img = img_undistort(img, mtx, dist)
rigin_pipline_img = pipeline(undistort_img)
transform_img = img_perspect_transform(rigin_pipline_img, M)
left_fit, right_fit = cal_line_param(transform_img)
y_max = img.shape[0]
left_x = left_fit[0] * y_max ** 2 + left_fit[1] * y_max + left_fit[2]
right_x = right_fit[0] * y_max ** 2 + right_fit[1] * y_max + right_fit[2]
return (left_x + right_x) / 2
# 2. 假设straight_lines2_line.jpg,这张图⽚是位于⻋道的中央,实际情况可以根据测量验证.
img =cv2.imread("./test/straight_lines2_line.jpg")
lane_center = cal_line__center(img)
print("⻋道的中⼼点为:{}".format(lane_center))
# 3. 计算偏离中⼼的距离
def cal_center_departure(img, left_fit, right_fit):
# 计算中⼼点
y_max = img.shape[0]
left_x = left_fit[0] * y_max ** 2 + left_fit[1] * y_max + left_fit[2]
right_x = right_fit[0] * y_max ** 2 + right_fit[1] * y_max + right_fit[2]
xm_per_pix = 3.7 / 700
center_depart = ((left_x + right_x) / 2 - lane_center) * xm_per_pix
# 在图像上显示偏移
if center_depart > 0:
cv2.putText(img, 'Vehicle is {}m right of center'.format(center_depart),(20,100),
(255, 255, 255), 5)
elif center_depart < 0:
cv2.putText(img, 'Vehicle is {}m left of center'.format(-center_depart),(20,100),
(255, 255, 255), 5)
else:
cv2.putText(img, 'Vehicle is in the center', (20, 100), cv2.FONT_ITALIC,1,(255,255,255))
return img
总结