OpenCV 实时对象跟踪(质心跟踪)

本文章先介绍对象跟踪过程,考虑对象跟踪的特点决定使用:质心跟踪算法,然后会一步一步说明质心跟踪算法的实现;最后是如何用python代码实现。

实验效果如下:

对象跟踪过程

  1. 进行一组初始的对象检测(如:边界框坐标的输入集)
  2. 为每个初始检测创建唯一的ID(每个独立对象有唯一的ID)
  3. 然后跟踪每个对象在视频中的帧中移动时的情况,并保持唯一ID的分配

补充第二点:对象跟踪允许我们将唯一的ID应用于每个被跟踪的对象,从而使我们能够对视频中的唯一对象进行计数。

好了下一步我们应该考虑对象跟踪算法的特点。

对象跟踪算法的特点(理想下)

  • 仅要求对象检测阶段一次(即,最初检测到对象时)
  • 将是非常快- 比实际运行的物体探测器本身更快
  • 能够处理被跟踪对象何时“消失”或移动到视频帧的边界之外
  • 遮挡力强
  • 能够拾取帧之间“丢失”的对象

不妨使用OpenCV实现质心跟踪,OpenCV是一种易于理解但非常有效的跟踪算法。

质心跟踪依赖于

(1)现有对象质心(即检测出对象,然后计算对象的质心)

(2)实时图像数据连续帧之间,新对象质心之间的欧氏距离(EUCLIDEAN DISTAN)  

备注:如果对欧氏距离不了解的可以到文章底部看看。

质心跟踪算法实现

步骤1:接受边界框坐标并计算质心

图1:要使用质心跟踪构建简单的对象跟踪算法,第一步是接受来自对象检测器的边界框坐标,然后使用它们来计算质心。

质心追踪算法假定我们传递一组边界框的(X,Y)坐标-对于每个检测到的对象的每一个帧

1)这些边界框可以由大家想要的任何类型的对象检测器生成(颜色阈值+轮廓提取,Haar级联,HOG +线性SVM,SSD,Faster R-CNN等),前提是要针对帧中的每一帧进行计算视频。

2)有了边界框坐标后,我们必须计算“质心”或更简单地计算边界框的中心(x,y)坐标。 上面的图1演示了接受一组边界框坐标并计算质心。

3)由于这些是提供给我们算法的边界框的第一组初始集合,因此我们将为其分配唯一的ID。

 

步骤2:计算新边界框与现有对象之间的欧氏距离(EUCLIDEAN DISTAN)

图2:此图像中存在三个对象,用于使用Python和OpenCV进行简单的对象跟踪。我们需要计算每对原始质心(紫)和新质心(黄)之间的欧氏距离(EUCLIDEAN DISTAN)。

可能你会疑惑,为什么下面说一共检测到了三个对象,图中就有5个质心了?

答:本来是有两个对象(其质心为紫色),当下一帧数据时,这两个对象移动了,原来的质心位置自然也会改变的,会产生新的质心(黄色的);于是在原来两个紫色质心,由于原对象移动,产生两个新质心(黄色),再加上一个新对象的质心,就变成了3个黄色质心+2个紫色质心。

对于视频流中的每个后续帧,我们应用计算对象质心的步骤#1但是,我们首先需要确定是否可以将新的对象质心(黄色)与旧的对象质心(紫色)相关联,而不是为每个检测到的对象分配新的唯一ID(这会破坏对象跟踪的目的)。为了完成此过程,我们计算了每对现有对象质心和输入对象质心之间的欧氏距离(EUCLIDEAN DISTAN)(用绿色箭头突出显示)。

图2中可以看到,这次我们在图像中检测到三个对象。靠近的两对是两个现有对象。

其实这里简单来说,通过计算原始质心(黄色)和新质心(紫色)之间的欧氏距离(EUCLIDEAN DISTAN)后,计算出最小值的(箭头最短),是指向原来的对象;步骤3会进一步将的。

 

步骤#3:更新(x,y) -现有对象的坐标

图3:我们的简单质心对象跟踪方法将关联的对象的对象距离最小化。但是,我们该如何处理左下角的对象呢?

质心跟踪算法的主要假设是一个给定的对象将潜在地移动在后续的帧之间,但距离为帧中的质心之间_F_ {t + 1}比对象之间的所有其它距离。

因此,如果我们选择将质心与后续帧之间的最小距离相关联,则可以构建对象跟踪器。

图3中,大家可以看到我们的质心跟踪器算法如何选择将最小化其欧氏距离(EUCLIDEAN DISTAN)的质心关联起来。

但是左下角的孤独点呢?

它没有任何关联-我们如何处理它?

 

步骤4:注册新对象

图4:在使用Python和OpenCV进行对象跟踪的示例中,我们有一个与现有对象不匹配的新对象,因此将其注册为对象ID#3。

如果输入检测的数量多于被跟踪的现有对象,则我们需要注册新对象。“注册”仅表示通过以下方式将新对象添加到跟踪对象列表中:

  1. 给它分配一个新的对象ID
  2. 存储该对象的边界框坐标的质心

然后,我们可以返回到步骤2,并为视频流中的每个帧重复步骤流水线。

图4演示了使用最小欧氏距离(EUCLIDEAN DISTAN)关联现有对象ID,然后注册新对象的过程。

 

步骤5:注销旧对象

任何合理的对象跟踪算法都必须能够处理对象丢失,消失或离开视野时的情况。

实际情况下,您如何处理这些情况实际上取决于对象跟踪器的部署位置,但是对于此实现,我们将在旧对象无法与任何现有对象匹配的情况下(总共N个后续帧)注销旧对象。

 

质心跟踪代码实现:centroidtracker.py

# import the necessary packages
from scipy.spatial import distance as dist
from collections import OrderedDict
import numpy as np

class CentroidTracker():
	def __init__(self, maxDisappeared=50):

		self.nextObjectID = 0
		self.objects = OrderedDict()
		self.disappeared = OrderedDict()
		#存储给定对象被允许标记为“消失”的最大连续帧数,直到我们需要从跟踪中注销该对象
		self.maxDisappeared = maxDisappeared

	def register(self, centroid):

		#注册对象时,我们使用下一个可用的对象ID来存储质心
		self.objects[self.nextObjectID] = centroid
		self.disappeared[self.nextObjectID] = 0
		self.nextObjectID += 1

	def deregister(self, objectID):

		#要注销注册对象ID,我们从两个字典中都删除了该对象ID
		del self.objects[objectID]
		del self.disappeared[objectID]

	def update(self, rects):

		# 检查输入边界框矩形的列表是否为空
		if len(rects) == 0:
			#遍历任何现有的跟踪对象并将其标记为消失
			for objectID in list(self.disappeared.keys()):
				self.disappeared[objectID] += 1
				#如果达到给定对象被标记为丢失的最大连续帧数,请取消注册
				if self.disappeared[objectID] > self.maxDisappeared:
					self.deregister(objectID)
			#由于没有质心或跟踪信息要更新,请尽早返回
			return self.objects

		# 初始化当前帧的输入质心数组
		inputCentroids = np.zeros((len(rects), 2), dtype="int")
		#在边界框矩形上循环
		for (i, (startX, startY, endX, endY)) in enumerate(rects):
			# use the bounding box coordinates to derive the centroid
			cX = int((startX + endX) / 2.0)
			cY = int((startY + endY) / 2.0)
			inputCentroids[i] = (cX, cY)

		#如果我们当前未跟踪任何对象,请输入输入质心并注册每个质心
		if len(self.objects) == 0:
			for i in range(0, len(inputCentroids)):
				self.register(inputCentroids[i])
		#否则,当前正在跟踪对象,因此我们需要尝试将输入质心与现有对象质心进行匹配
		else:
			#抓取一组对象ID和相应的质心
			objectIDs = list(self.objects.keys())
			objectCentroids = list(self.objects.values())
			#分别计算每对对象质心和输入质心之间的距离-我们的目标是将输入质心与现有对象质心匹配
			D = dist.cdist(np.array(objectCentroids), inputCentroids)
			#为了执行此匹配,我们必须(1)在每行中找到最小值,然后(2)根据行索引的最小值对行索引进行排序,以使具有最小值的行位于索引列表的* front *处
			rows = D.min(axis=1).argsort()
			#接下来,我们在列上执行类似的过程,方法是在每一列中找到最小值,然后使用先前计算的行索引列表进行排序
			cols = D.argmin(axis=1)[rows]
			# 为了确定是否需要更新,注册或注销对象,我们需要跟踪已经检查过的行索引和列索引
			usedRows = set()
			usedCols = set()

			#循环遍历(行,列)索引元组的组合
			for (row, col) in zip(rows, cols):
				#如果我们之前已经检查过行或列的值,请忽略它
				if row in usedRows or col in usedCols:
					continue
				#否则,获取当前行的对象ID,设置其新的质心,然后重置消失的计数器
				objectID = objectIDs[row]
				self.objects[objectID] = inputCentroids[col]
				self.disappeared[objectID] = 0
				#表示我们已经分别检查了行索引和列索引
				usedRows.add(row)
				usedCols.add(col)
			#计算我们尚未检查的行和列索引
			unusedRows = set(range(0, D.shape[0])).difference(usedRows)
			unusedCols = set(range(0, D.shape[1])).difference(usedCols)
			#如果对象质心的数量等于或大于输入质心的数量
			#我们需要检查一下其中的某些对象是否已潜在消失
			if D.shape[0] >= D.shape[1]:
				# loop over the unused row indexes
				for row in unusedRows:
					#抓取相应行索引的对象ID并增加消失的计数器
					objectID = objectIDs[row]
					self.disappeared[objectID] += 1
					#检查是否已将该对象标记为“消失”的连续帧数以用于注销该对象的手令
					if self.disappeared[objectID] > self.maxDisappeared:
						self.deregister(objectID)
			#否则,如果输入质心的数量大于现有对象质心的数量,我们需要将每个新的输入质心注册为可跟踪对象
			else:
				for col in unusedCols:
					self.register(inputCentroids[col])

		# return the set of trackable objects
		return self.objects

 

主函数代码实验:object_tracker.py

# 执行程序,运行如下命令
# python object_tracker.py --prototxt deploy.prototxt --model res10_300x300_ssd_iter_140000.caffemodel
# 导入必要的软件包
from centroidtracker import CentroidTracker
from imutils.video import VideoStream
import numpy as np
import argparse
import imutils
import time
import cv2
# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-p", "--prototxt", required=True,help="path to Caffe 'deploy' prototxt file")
ap.add_argument("-m", "--model", required=True,help="path to Caffe pre-trained model")
ap.add_argument("-c", "--confidence", type=float, default=0.5,help="minimum probability to filter weak detections")
args = vars(ap.parse_args())
# 初始化质心跟踪器和框架尺寸
ct = CentroidTracker()
(H, W) = (None, None)
# 从磁盘加载我们的检测人脸的模型
print("[INFO] loading model...")
net = cv2.dnn.readNetFromCaffe(args["prototxt"], args["model"])
# 初始化视频流并允许相机传感器预热
print("[INFO] starting video stream...")
vs = VideoStream(src=0).start()
time.sleep(2.0)
# 循环播放图像流中的帧
while True:
	# 从视频流中读取下一帧并调整其大小
	frame = vs.read()
	frame = imutils.resize(frame, width=600)
	# 如果帧中尺寸为“无”,则抓住它们
	if W is None or H is None:
		(H, W) = frame.shape[:2]
    #从帧中构造一个Blob,将其通过网络,
    #获取输出预测,并初始化边界框矩形的列表
	blob = cv2.dnn.blobFromImage(frame, 1.0, (W, H),
		(104.0, 177.0, 123.0))
	net.setInput(blob)
	detections = net.forward()
	rects = []
	#循环检测
	for i in range(0, detections.shape[2]):
		#通过确保预测的概率大于最小阈值来过滤掉弱检测
		if detections[0, 0, i, 2] > args["confidence"]:
			# 计算对象边界框的(x,y)坐标,然后更新边界框矩形列表
			box = detections[0, 0, i, 3:7] * np.array([W, H, W, H])
			rects.append(box.astype("int"))
			# 在对象周围画一个边界框,以便我们可视化它
			(startX, startY, endX, endY) = box.astype("int")
			cv2.rectangle(frame, (startX, startY), (endX, endY),
				(0, 255, 0), 2)
	# 使用边界框矩形的计算集更新质心跟踪器
	objects = ct.update(rects)
	# 循环跟踪对象
	for (objectID, centroid) in objects.items():
		# 在输出帧上绘制对象的ID和对象的质心
		text = "ID {}".format(objectID)
		cv2.putText(frame, text, (centroid[0] - 10, centroid[1] - 10),
			cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
		cv2.circle(frame, (centroid[0], centroid[1]), 4, (0, 255, 0), -1)
	# 显示输出画面
	cv2.imshow("Frame", frame)
	key = cv2.waitKey(1) & 0xFF
	# 如果按下“ q”键,则退出循环
	if key == ord("q"):
		break
cv2.destroyAllWindows()
vs.stop()

 

执行程序

实验环境:pyCharm中安装了opencv3.4,和一些基本库(NumPy,SciPy 和 imutils)

pip install numpy scipy imutils              #执行此命令进行安装

执行命令:python object_tracker.py   --prototxt deploy.prototxt    --model res10_300x300_ssd_iter_140000.caffemodel

注意:这里需要指定的两个参数文件 deploy.prototxt 为检测人脸时的配置(基于opencv深度学习)

res10_300x300_ssd_iter_140000.caffemodel 为检测人脸的模型,可以直接使用(在文章后面会给大家下载)

在pyCharm的命令窗口中执行:

 

实验效果:

 

文章中代码,放到网盘了,有需要可以去提取。

链接: https://pan.baidu.com/s/1h13-FPlnP2JyikHGesQRUA   提取码: erd4

备注:本程序只给大家学习研究,请不要作为商业用途,大家加油。

 


 

补充一下 欧氏距离(EUCLIDEAN DISTANCE)

欧氏距离定义: 欧氏距离( Euclidean distance)是一个通常采用的距离定义,它是在m维空间中两个点之间的真实距离。

在二维和三维空间中的欧式距离的就是两点之间的距离,二维的公式是
d = sqrt((x1-x2)^+(y1-y2)^)

三维的公式是
d=sqrt(x1-x2)^+(y1-y2)^+(z1-z2)^)

推广到n维空间,欧式距离的公式是
d=sqrt( ∑(xi1-xi2)^ ) 这里i=1,2..n    ( xi1表示第一个点的第i维坐标,xi2表示第二个点的第i维坐标 )
n维欧氏空间是一个点集,它的每个点可以表示为(x(1),x(2),...x(n)),其中x(i)(i=1,2...n)是实数,称为x的第i个坐标,两个点x和y=(y(1),y(2)...y(n))之间的距离d(x,y)定义为上面的公式.

欧氏距离看作信号的相似程度。 距离越近就越相似,就越容易相互干扰,误码率就越高。

看下图解释:求ab边两端点的距离

设a(x1,y1),b(x2,y2),c边两点距离其实就是:sqrt((x1-x2)^+(y1-y2)^)

 

希望对你有帮助。

 

 

 

 

发布了177 篇原创文章 · 获赞 387 · 访问量 33万+

猜你喜欢

转载自blog.csdn.net/qq_41204464/article/details/104459925