目录
一、均值漂移(MeanShift)
二、流程
三、代码
3.1 meanshift+固定框的代码
3.2 优化:meanshift+鼠标选择
3.3 meanshift+自己实现函数
四、补充知识
4.1 直方图
4.2 归一化
4.3 直方图反投影
一、均值漂移(MeanShift)
该算法寻找离散样本的最大密度,并且重新计算下一帧的最大密度,这个算法的特点就是可以给出目标移动的方向。
meanshift算法的原理很简单。假设你有一堆点集,还有一个小的窗口,这个窗口可能是圆形的,现在你可能要移动这个窗口到点集密度最大的区域当中。
最开始的窗口是蓝色圆环的区域,命名为C1。蓝色圆环的圆心用一个蓝色的矩形标注,命名为C1_o。
而窗口中所有点的点集构成的质心在蓝色圆形点C1_r处,显然圆环的形心和质心并不重合。所以,移动蓝色的窗口,使得形心与之前得到的质心重合。在新移动后的圆环的区域当中再次寻找圆环当中所包围点集的质心,然后再次移动,通常情况下,形心和质心是不重合的。不断执行上面的移动过程,直到形心和质心大致重合结束。这样,最后圆形的窗口会落到像素分布最大的地方,也就是图中的绿色圈,命名为C2。
meanshift算法除了应用在视频追踪当中,在聚类,平滑等等各种涉及到数据以及非监督学习的场合当中均有重要应用,是一个应用广泛的算法。
如果不知道预先要跟踪的目标,就可以采用这种巧妙地办法,加设定条件,使能动态的开始跟踪(和停止跟踪)视频的某些区域,(如可以采用预先训练好的SVM进行目标的检测,然后开始使用均值漂移MeanShift跟踪检测到的目标)
所以一般是分两个步骤:1.标记感兴趣区域 2.跟踪该区域
二、流程
图像是一个矩阵信息,如何在一个视频当中使用meanshift算法来追踪一个运动的物体呢?大致流程如下:
meanshift流程 |
1.首先在图像上选定一个目标区域 |
2.计算选定区域的直方图分布,一般是HSV色彩空间的直方图 |
3.对下一帧图像 b 同样计算直方图分布 |
4.计算图像 b 当中与选定区域直方图分布最为相似的区域,使用meanshift算法将选定区域沿着最为相似的部分进行移动,直到找到最相似的区域,便完成了在图像b中的目标追踪。 |
5.重复3到4的过程,就完成整个视频目标追踪。 |
通常情况下我们使用直方图反向投影得到的图像和第一帧目标对象的起始位置,当目标对象的移动会反映到直方图反向投影图中,meanshift算法就把我们的窗口移动到反向投影图像中灰度密度最大的区域了。 |
直方图反向投影
实现Meanshift的主要流程是︰
- 读取视频文件:cv.videoCapture()
- 感兴趣区域设置:获取第一帧图像,并设置目标区域,即感兴趣区域
- 计算直方图:计算感兴趣区域的HSV直方图,并进行归一化
- 目标追踪︰设置窗口搜索停止条件,直方图反向投影,进行目标追踪,并在目标位置绘制矩形框
三、代码
opencv API
cv2.meanShift(probImage, window, criteria)
参数:
- probImage:ROI区域,即目标直方图的反向投影
- window:初始搜索窗口,就是定义ROI的rect
- criteria:确定窗口搜索停止的准则,主要有迭代次数达到设置的最大值,窗口中心的漂移值大于某个设定的限值等
(Python)从零开始,简单快速学机器仿人视觉Opencv---运用五:物体运动跟踪 - 古月居
3.1 meanshift+固定框的代码
import cv2 as cv
# 创建读取视频的对象
cap = cv.VideoCapture("E:\Python-Code/videodataset/enn.mp4")
# 获取第一帧位置,并指定目标位置
ret, frame = cap.read()
c, r, h, w = 530, 160, 300, 320
track_window = (c, r, h, w)
# 指定感兴趣区域
roi = frame[r:r + h, c:c + w]
# 计算直方图
# 转换色彩空间
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# 计算直方图
roi_hist = cv.calcHist([hsv_roi], [0], None, [180], [0, 180])
# 归一化
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)
# 目标追踪
# 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值
term_crit = (cv.TermCriteria_EPS | cv.TERM_CRITERIA_COUNT, 10, 1)
while True:
ret, frame = cap.read()
if ret:
# 计算直方图的反向投影
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
# 进行meanshift追踪
ret, track_window = cv.meanShift(dst, track_window, term_crit)
# 将追踪的位置绘制在视频上,并进行显示
x, y, w, h = track_window
img = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2)
cv.imshow("frame", img)
if cv.waitKey(20) & 0xFF == ord('q'):
break
else:
break
# 资源释放
cap.release()
cv.destroyAllWindows()
缺点:这种就是一开始设定了具体框的大小和位置,不能根据实际情况自己进行更改。
初始框的位置很重要。以Meanshift为例,它的工作原理是根据概率密度来寻找最大的密度区域,但如果我们一开始将跟踪框放置在了一个直方图反向投影图中全黑的区域(密度为0),这会导致其无法正确向物体方向进行移动,从而导致卡死在那里。
我们已追踪视频的初始帧(第一帧)为例,我们假设想要跟踪其中的一个物体,我们就得将跟踪框放置到跟踪物体周边的区域才能让程序正常运行,但我们其实很难知道一张图片中跟踪物体的具体位置。 举个简单的例子,比如我现在有一张图片,我们要跟踪图片右下角的一个物体,但是我不知道这个物体的坐标范围,所以我只能一次一次的去尝试(在代码中修改初始框的位置后,看看程序的运行情况)来保证代码能够正常运行,但这样代码的普适性很差,因为每当要更改跟踪对象的时候,都需要反复的修改,才能应对当前的情况,这样实在是有点麻烦。
3.2 优化:meanshift+鼠标选择
这里介绍一个函数,起名为:cv2.selectROI,使用这个函数,我们就能够实现手动画取我们的跟踪框,其函数语法如下所示:
track_window=cv2.selectROI('frameName', frame)
参数:
- framename:显示窗口的画布名
- frame:具体的帧
import cv2
import numpy as np
# 读取视频
cap=cv2.VideoCapture('E:\Python-Code/videodataset/enn.mp4')
# 获取第一帧位置,参数ret 为True 或者False,代表有没有读取到图片 第二个参数frame表示截取到一帧的图片
ret,frame=cap.read()
#我这里画面太大了所以缩小点——但是缩小后我的就会报错
# frame=cv2.resize(frame,None,None,fx=1/2,fy=1/2,interpolation=cv2.INTER_CUBIC)
#跟踪框
track_window=cv2.selectROI('img', frame)
#获得绿色的直方图
# 转换色彩空间
hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
mask=cv2.inRange(hsv,np.array((35,43,46)),np.array((77,255,255)))
# 计算直方图
hist=cv2.calcHist([hsv],[0],mask,[181],[0,180])
# hist=cv2.calcHist([hsv],[0],[None],[180],[0,180])
# 归一化
cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)
# 目标追踪
# 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值
term_crit=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
while True:
ret,frame=cap.read()
# frame = cv2.resize(frame, None, None, fx=1 / 2, fy=1 / 2, interpolation=cv2.INTER_CUBIC)
if ret== True:
# 计算直方图的反向投影
hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
# 进行meanshift追踪
ret,track_window=cv2.meanShift(dst,track_window,term_crit)
# 将追踪的位置绘制在视频上,并进行显示
x,y,w,h = track_window
img = cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
(x,y)=img.shape[:2]
cv2.imshow('img',img)
if cv2.waitKey(1)==ord('q'):
break
else:
break
# 资源释放
cap.release()
cv2.destroyAllWindows()
运行上面的代码,会跳出一个窗口,窗口上显示的就是我们载入的视频的第一帧,我们用鼠标拖动,画出我们要跟踪的物体的位置
但是这里需要注意的是,这个函数每次调用只能画一个矩形(C++版本中的OpenCV可以一次画好多个),如果想画好多个矩形的话,可以使用while循环:
bboxes = []
colors = []
while 1:
bbox = cv2.selectROI('MultiTracker', frame)
bboxes.append(bbox)
colors.append((randint(0, 255), randint(0, 255), randint(0, 255)))
print("按下q键退出,按下其他键继续画下一个框")
if cv2.waitKey(0) & 0xFF==ord('q'):
break
print('选取的边框为{}'.format(bboxes))
但是完整的代码并没有跑通
3.3 meanshift+自己实现函数
这个效果最好,但是不懂为什么会这样?为什么调用库函数反而效果不好呢?
import math
import numpy as np
import cv2
def get_tr(img):
# 定义需要返回的参数
mouse_params = {'x': None, 'width': None, 'height': None,
'y': None, 'temp': None}
cv2.namedWindow('image')
# 鼠标框选操作函数
cv2.setMouseCallback('image', on_mouse, mouse_params)
cv2.imshow('image', img)
cv2.waitKey(0)
return [mouse_params['x'], mouse_params['y'], mouse_params['width'],
mouse_params['height']], mouse_params['temp']
def on_mouse(event, x, y, flags, param):
global img, point1
img2 = img.copy()
if event == cv2.EVENT_LBUTTONDOWN: # 左键点击
point1 = (x, y)
cv2.circle(img2, point1, 10, (0, 255, 0), 5)
cv2.imshow('image', img2)
elif event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_LBUTTON): # 按住左键拖曳
cv2.rectangle(img2, point1, (x, y), (255, 0, 0), 5)
cv2.imshow('image', img2)
elif event == cv2.EVENT_LBUTTONUP: # 左键释放
point2 = (x, y)
cv2.rectangle(img2, point1, point2, (0, 0, 255), 5)
cv2.imshow('image', img2)
# 返回框选矩形左上角点的坐标、矩形宽度、高度以及矩形包含的图像
param['x'] = min(point1[0], point2[0])
param['y'] = min(point1[1], point2[1])
param['width'] = abs(point1[0] - point2[0])
param['height'] = abs(point1[1] - point2[1])
param['temp'] = img[param['y']:param['y'] + param['height'],
param['x']:param['x'] + param['width']]
def main():
global img
cap = cv2.VideoCapture("E:\Python-Code/videodataset/enn.mp4")
# 获取视频第一帧
ret, frame = cap.read()
img = frame
# 框选目标并返回相应信息:rect为四个信息,temp为框选出来的图像
rect, temp = get_tr(img)
print(temp)
(a, b, c) = temp.shape
y = [a / 2, b / 2]
# 计算目标图像的权值矩阵
m_wei = np.zeros((a, b))
for i in range(a):
for j in range(b):
z = (i - y[0]) ** 2 + (j - y[1]) ** 2
m_wei[i, j] = 1 - z / (y[0] ** 2 + y[1] ** 2)
# 计算目标权值直方图
C = 1 / sum(sum(m_wei))
hist1 = np.zeros(16 ** 3)
for i in range(a):
for j in range(b):
q_b = math.floor(float(temp[i, j, 0]) / 16)
q_g = math.floor(float(temp[i, j, 1]) / 16)
q_r = math.floor(float(temp[i, j, 2]) / 16)
q_temp1 = q_r * 256 + q_g * 16 + q_b
hist1[int(q_temp1)] = hist1[int(q_temp1)] + m_wei[i, j]
hist1 = hist1 * C
# 接着读取视频并进行目标跟踪
while (1):
ret, frame = cap.read()
if ret == True:
Img = frame
num = 0
Y = [1, 1]
# mean shift迭代
while (np.sqrt(Y[0] ** 2 + Y[1] ** 2) > 0.5) & (num < 20):
num = num + 1
# 计算候选区域直方图
temp2 = Img[int(rect[1]):int(rect[1] + rect[3]), int(rect[0]):int(rect[0] + rect[2])]
hist2 = np.zeros(16 ** 3)
q_temp2 = np.zeros((a, b))
for i in range(a):
for j in range(b):
q_b = math.floor(float(temp2[i, j, 0]) / 16)
q_g = math.floor(float(temp2[i, j, 1]) / 16)
q_r = math.floor(float(temp2[i, j, 2]) / 16)
q_temp2[i, j] = q_r * 256 + q_g * 16 + q_b
hist2[int(q_temp2[i, j])] = hist2[int(q_temp2[i, j])] + m_wei[i, j]
hist2 = hist2 * C
w = np.zeros(16 ** 3)
for i in range(16 ** 3):
if hist2[i] != 0:
w[i] = math.sqrt(hist1[i] / hist2[i])
else:
w[i] = 0
sum_w = 0
sum_xw = [0, 0]
for i in range(a):
for j in range(b):
sum_w = sum_w + w[int(q_temp2[i, j])]
sum_xw = sum_xw + w[int(q_temp2[i, j])] * np.array([i - y[0], j - y[1]])
Y = sum_xw / sum_w
# 位置更新
rect[0] = rect[0] + Y[1]
rect[1] = rect[1] + Y[0]
v0 = int(rect[0])
v1 = int(rect[1])
v2 = int(rect[2])
v3 = int(rect[3])
pt1 = (v0, v1)
pt2 = (v0 + v2, v1 + v3)
# 画矩形
IMG = cv2.rectangle(Img, pt1, pt2, (0, 0, 255), 2)
cv2.imshow('IMG', IMG)
k = cv2.waitKey(60) & 0xff
if k == 27:
break
else:
break
if __name__ == '__main__':
main()
四、补充知识
4.1 直方图
构建图像的直方图需要使用到函数cv2.calcHist,其常用函数语法如下所示:
hist=cv2.calcHist(images, channels, mask, histSize, ranges)
images:输入的图像
channels:选择图像的通道,如果是三通道的话就可以是[0],[1],[2]
mask:掩膜,是一个大小和image一样的np数组,其中把需要处理的部分指定为1,不需要处理的部分指定为0,一般设置为None,如果有mask,会先对输入图像进行掩膜操作
histSize:使用多少个bin(柱子),一般为256,但如果是H值就是181
ranges:像素值的范围,一般为[0,255]表示0~255,对于H通道而言就是[0,180]
需要注意的是,这里除了mask以外,其余的几个参数都要加上[],如下所示:
hist=cv2.calcHist([img],[0],mask,[181],[0,180])
4.2 归一化
这个时候我们还需要使用一种归一化的方法来对彩色直方图中的数量值进行规范化。 现有的直方图中的数值为对应像素的数量,其中图中出现数量最多的像素的数量值(最高的柱子对应的y轴数值)我们记为max的话,整个直方图y方向上的取值范围就是[0,max],我们需要把这个范围缩减到[0,255],
这里我们需要使用到cv2.normalize函数,函数主要语法如下所示:
cv2.normalize(src,dst, alpha,beta, norm_type)
·src-输入数组。
·dst-与SRC大小相同的输出数组。
·α-范数值在范围归一化的情况下归一化到较低的范围边界。
·β-上限范围在范围归一化的情况下;它不用于范数归一化。
·范式-规范化类型(见下面详细介绍)。
这里我们需要注意的是范式-规范化类型,这里有以下几种选择。
NORM_MINMAX:数组的数值被平移或缩放到一个指定的范围,线性归一化。
NORM_INF:归一化数组的(切比雪夫距离)L∞范数(绝对值的最大值)
NORM_L1: 归一化数组的(曼哈顿距离)L1-范数(绝对值的和)
NORM_L2: 归一化数组的(欧几里德距离)L2-范数
上面的名词看起来很高大上,其实是很简单,我们一一讲解下。(不是很感兴趣的只要看下第一个NORM_MINMAX即可,剩下的三个可以不看)
首先是NORM_MINMAX,这个是我们最常用的一种归一化方法。举个例子,我们上面提到的最高的柱子对应的y轴坐标为max,如果我们使用这种方法,想要缩放到的指定的范围为[0,255],那么max就会直接被赋值为255,其余的柱子也会随之一样被压缩(类似于相似三角形那样的缩放感觉)。 没错,很简单得就介绍完了一种,不是很想了解其他几个的读者可以直接跳过本小节剩下来的内容了,因为剩下三种不是很常用。
这里有介绍剩下的几种
4.3 直方图反投影
简单来说,它会输出与输入图像(待搜索)同样大小的图像,其中的每一个像素值代表了输入图像上对应点属于目标对象(我们需要跟踪的目标)的概率。用更简单的话来解释,输出图像中像素值越高(越白)的点就越可能代表我们要搜索的目标 (在输入图像所在的位置)。 而对于灰度图而言,其只有一个通道,取值范围为0到255,所以我们之前在归一化的时候将直方图的y轴坐标的取值范围压缩到了0-255的范围内,就是为了这里可以直接赋值。
直方图反向投影1 直方图反向投影2
越暗的地方说明属于跟踪部分的可能性越低,越亮的地方属于跟踪部分的可能性越高。 这里使用到的函数为cv2.calcBackProject,函数语法如下所示:
dst=cv2.calcBackProject(image,channel,hist,range,scale)
image:输入图像
channel:用来计算反向投影的通道数,与产生直方图对应的通道应一致
hist:作为输入的直方图
range:直方图的取值范围
scale:输出图像的缩放比,一般为1,保持与输入图像一样的大小
dst:输出图像
注意:除了hist和scale外,其他的参数都要加上[]
例如:
dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)