目标跟踪就是识别移动目标的过程,并且跨帧跟踪这些目标,为了跟踪视屏中的目标,首先要做的就是识别出可能包含目标的区域。
目前有很多视频目标跟踪的方法:
1.基本的运动检测
import cv2
import numpy as np
camera = cv2.VideoCapture(0)#打开系统默认摄像头
es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(9,4))
kernel = np.ones((5,5),np.uint8)
background = None
while (True):
ret,frame = camera.read()
if background is None:
background = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
background = cv2.GaussianBlur(background,(21,21),0)
#第一次循环时,background是空的,所以将第一针作为背景
#将背景灰度处理,平滑处理之后赋给background
continue
#第二次循环开始,background已经有了,将第二帧开始的图片灰度化 + 平滑处理
#减小光照 震动等原因产生的噪声影响
gray_frame = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
gray_frame = cv2.GaussianBlur(gray_frame,(21,21),0)
diff = cv2.absdiff(background,gray_frame)#背景与帧做插分操作
diff = cv2.threshold(diff,25,255,cv2.THRESH_BINARY)[1]#阈值得到黑白图
diff = cv2.dilate(diff,es,iterations = 2)#膨胀运算,用椭圆框来膨胀,迭代次数是2
#搜索轮廓
image,cnts,hierarchy = cv2.findContours(diff.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#image是改变后的图片
#cnts是轮廓
#hierarchy是每条轮廓的属性
for c in cnts:
if cv2.contourArea(c) < 1500:
continue #如果轮廓太小了,以面积看,就不显示
(x,y,w,h) = cv2.boundingRect(c)
cv2.rectangle(frame,(x,y),(x + w,y + h),(255,255,0),2)
cv2.imshow('contours',frame)
cv2.imshow('diff',diff)
#关闭窗口指令
if cv2.waitKey(5) & 0xff == ord('q'):
break
cv2.destroyAllWindows()
camera.release()
diff = cv2.threshold(diff,25,255,cv2.THRESH_BINARY)
调用格式:
cv2.threshold(src, thresh, maxval, type[, dst]) → retval, dst
src:图片
thresh:表示阈值
maxval:表示最大值
type: 表示这里的划分采用什么类型的算法,一般是0(cv2.THRESH_BINARY)
所以就是如果灰度值大于25,一律按255算,否则按照0 算,变成二值图像
返回[ret,thresh]第一个是处理之后的二值化图像,第二个是阈值
cv2.getStructuringElement( ) 返回指定形状和尺寸的结构元素
这个函数的第一个参数表示内核的形状,有三种形状可以选择:
矩形:MORPH_RECT;
交叉形:MORPH_CROSS;
椭圆形:MORPH_ELLIPSE;
第二和第三个参数分别是内核的尺寸以及锚点的位置。一般在调用erode(腐蚀)以及dilate(膨胀)函数之前,先定义一个Mat类型的变量来获得
getStructuringElement函数的返回值: 对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心点。element形状唯一依赖锚点位置,其他情况下,锚点只是影响了形态学运算结果的偏移。
diff = cv2.dilate(diff,es,iterations = 2)
调用格式:
cv2.dilate(src, kernel, iteration)
src表示输入的图片, kernel表示方框的大小, iteration表示迭代的次数
这里采用椭圆框膨胀,迭代次数2
cv2.findContours(diff.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
第一个参数是寻找轮廓的图像;
第二个参数表示轮廓的检索模式,有四种(本文介绍的都是新的cv2接口):
cv2.RETR_EXTERNAL表示只检测外轮廓
cv2.RETR_LIST检测的轮廓不建立等级关系
cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
cv2.RETR_TREE建立一个等级树结构的轮廓。
第三个参数method为轮廓的近似办法
cv2.CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
返回值返回,第一个是处理改变了的图像,第二个是轮廓本身,第三个是每条轮廓对应的属性。
cv2.boundingRect(c)
计算轮廓的边界框
综上:
这种方法,需要设定“默认”的一帧作为背景,这在光照变化的户外就显得很不灵活,所以需要引入更智能的方法——背景分割器
2.背景分割器
opencv提供了一个BackgroundSubtractor的类,BackgroundSubtractor的类是一个功能完全的类,能执行背景分割,而且能够通过机器学习方法提高背景检测效果
背景分割器
opencv3中有三种背景分割器:
BackgroundSubtractor类是专门用于视频分析的,BackgroundSubstractor会对每帧的环境进行学习
BackgroundSubtractor另一个特点是可以检测阴影,有助于将目标轮廓按原始形状进行还原
视频数据来自:https://blog.csdn.net/thefutureisour/article/details/7476482#comments
import numpy as np
import cv2
camera = cv2.VideoCapture("E:/highwayI_raw.avi")
bs = cv2.createBackgroundSubtractorKNN()#类实例一个对象
while(1):
ret,frame = camera.read()
fgmask = bs.apply(frame)
cv2.imshow('frame',fgmask)
if cv2.waitKey(100) & 0xff == 27:#ESC退出键的ASCII码是27
break
camera.release()
cv2.destroyAllWindows()
if cv2.waitKey(100) & 0xff == 27:
我们告诉OpenCv等待用户触发事件,等待时间为100ms,如果在这个时间段内, 用户按下ESC(ASCII码为27),则跳出循环,否则,则跳出循环
th = cv2.threshold(fgmask.copy(),244,255,cv2.THRESH_BINARY)[1]#阈值得到黑白图
cv2.imshow('frame',th)
这里fgmask = bs.apply(frame)
得到的是前景掩码
frame和fgmask的不同在于:frame是三通道的,而fgmask是一通道的
前景掩码含有前景的白色值以及阴影的灰色值;
而后面的二值化处理将224以上的像素全部处理成255;
后面就是正常的检测轮廓,然后画出矩形框;
完整代码:
import numpy as np
import cv2
from skimage.segmentation import slic,mark_boundaries
camera = cv2.VideoCapture("highwayI_raw.avi")
bs = cv2.createBackgroundSubtractorKNN(detectShadows = True)#类实例一个对象
while(camera.isOpened()):
ret,frame = camera.read()
fgmask = bs.apply(frame) #前景掩码的获取
th = cv2.threshold(fgmask.copy(),224,255,cv2.THRESH_BINARY)[1]#阈值得到黑白图
dilated = cv2.dilate(th,cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)),
iterations = 2)#膨胀操作
image,contours,hier = cv2.findContours(dilated,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if cv2.contourArea(c) > 1600:#用面积来限制显示的识别对象,面积大于1600时画矩形
(x,y,w,h) = cv2.boundingRect(c)
cv2.rectangle(frame,(x,y),(x + w,y + h),(255,255,0),2)
cv2.imshow('mog',fgmask)
cv2.imshow('thresh',th)
cv2.imshow('detection',frame)
# cv2.imshow('frame',fgmask)
if cv2.waitKey(100) & 0xff == 27:#ESC退出键的ASCII码是27
break
camera.release()
cv2.destroyAllWindows()
当然用MOG2分割器也可以
#bs = cv2.createBackgroundSubtractorMOG2()
在opencv3.0以后的版本中,只有createBackgroundSubtractorKNN和createBackgroundSubtractorMOG2函数,而createBackgroundSubtractorGMG与createBackgroundSubtractorMOG被移动到opencv_contrib包中了
参考
3.均值漂移
背景分割是一种以有效的技术,但是并不是唯一可用的视频目标跟踪办法。
均值漂移(MeanShift)
该算法寻找离散样本的最大密度,并且重新计算下一帧的最大密度,这个算法的特点就是可以给出目标移动的方向
如果不知道预先要跟踪的目标,就可以采用这种巧妙地办法,加设定条件,使能动态的开始跟踪(和停止跟踪)视频的某些区域,(如可以采用预先训练好的SVM进行目标的检测,然后开始使用均值漂移MeanShift跟踪检测到的目标)
所以一般是分两个步骤:1.标记感兴趣区域 2.跟踪该区域
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
ret,frame = cap.read()#读取开摄像头的第一针帧图像
#标记初始感兴趣区域
r,h,c,w = 10,200,10,200
track_window = (c,r,w,h)
roi = frame[r:r + h,c:c + w] #感兴趣区域
hsv_roi = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)#将第一帧图像转变为HSV空间上的图像
#创建一个包含具有HSV值的ROI所有像素的掩码,HSV值在上下界之间才保留
#如果hsv_roi的第一个通道值在100以下视为0
#如果hsv_roi的第一个通道值在180以上视为0
#第二三通道一样
mask = cv2.inRange(hsv_roi,np.array((100.,30.,32.)),np.array((180.,120.,225.)))
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])#计算感兴趣区域的彩色直方图
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)#计算了直方图之后,归一化到0-255之间,输入的是roi_hist,输出还是roi_hist
#指定停止条件
term_crit = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
#停止条件解释为:均值漂移迭代10次或者中心移动至少一个像素,就停止计算中心移动
#COUNT到10 或者 EPS到1 就结束
#哪个条件先到达就停止
while True:
ret,frame = cap.read()
if ret == True:
#先将读取到的帧转化为HSV色彩空间
hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
#执行直方图反向投影,获得一个矩阵,里面的每个值以概率的形式表示
dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)
#将这个概率形式传给meanshift
ret,track_window = cv2.meanShift(dst,track_window,term_crit)
#在每一帧显示
x,y,w,h = track_window
img2 = cv2.rectangle(frame,(x,y),(x + w,y + h),255,2)
cv2.imshow('img2',img2)
k = cv2.waitKey(60) & 0xff
if k == 27:
break
else:
break
cv2.destroyAllWindows()
cap.release()
这里calcHist函数用来计算图像的彩色直方图
彩色直方图:图像的颜色分布,x轴是色彩值,y轴是对应色彩值的像素数量
调用格式:
hist = calcHist(image,channels,mask,histSize,ranges[,hist[,accumulate]])
imaes:输入的图像
channels:选择图像的通道
mask:掩膜,是一个大小和image一样的np数组,其中把需要处理的部分指定为1,不需要处理的部分指定为0,一般设置为None,表示处理整幅图像
histSize:使用多少个bin(柱子),一般gbr为256,这里hsv为180
ranges:像素值的范围,一般为gbr[0,255]表示0~255,hsv表示为[0,180]
由于前面将GBR图像换成了HSV图像,而opencv里面的H值为0-180,所以这里范围是0-180(其他的系统不一定,可能是0-360,0-255之类的)
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])
这里表示彩色直方图有180列,每一列是0-180的范围
calcBackProject函数
直方图反向投影(histogram back project),在均值漂移算法(Meanshift)中发挥着重要的地位。
将直方图投影到一幅图像上面,得到一个概率,即每个像素属于起初幅图生成的直方图的概率,因此calcBackProject得到的是一个概率:一幅图像类似于或者等于模型图像的概率。
总而言之,calcHist函数从图像中提取彩色直方图;calcBackProject用来计算图像每个像素属于原图像的概率
cv2.inRange(hsv_roi,np.array((100.,30.,32.)),np.array((180.,120.,225.)))
利用cv2.inRange函数设阈值,去除背景部分,这里将三个通道h,s,v的三个通道限制在一定的范围内,低于这个范围的赋值为0,高于这个范围的赋值为0
HSV空间中,H表示色彩/色度,取值范围 [0,179],S表示饱和度,取值范围 [0,255],V表示亮度,取值范围 [0,255]。但是不同的软件使用值不同
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)
语法格式:参考
cv2.normalize(src[, dst[, alpha[, beta[, norm_type[, dtype[, mask]]]]]]) → dst
src:输入数组。
dst:与src大小相同的输出数组。
α:范数值在范围归一化的情况下归一化到较低的范围边界。
β:上限范围在范围归一化的情况下;它不用于范数归一化。
norm_type:
NORM_MINMAX:数组的数值被平移或缩放到一个指定的范围,线性归一化。
NORM_INF: 归一化数组的(切比雪夫距离)L∞范数(绝对值的最大值)。
NORM_L1 : 归一化数组的(曼哈顿距离)L1-范数(绝对值的和)。
NORM_L2: 归一化数组的(欧几里德距离)L2-范数。
停止条件参考
Meanshift在新的一帧中找目标(如果直接检测就完全不需要跟踪啦。跟踪的目的就是减少检测的次数,因为检测太费劲!)那么就需要找出这一帧(新的一帧)中哪个区域与目标(第一帧的box)比较相似了。判断两个图像的相似性,可以通过计算两个图像直方图分布来计算。计算box图像与图像块1的相关性,可以得到一个[0,1]之间的数。这样得到一个相关性图(概率图),每一个像素的值都是[0,1]的小数。将[0,1]映射到[0,255]就产生一张灰度图了(dst)。相关性越高的地方,在灰度图中就越亮。也就是第一张图中的各种点。
这里的窗口大小不能跟随目标大小一起变化,若需要窗口大小一起变化,可以参考:连续自适应均值漂移的算法。
4.CAMShift
CAMShift绘制的矩形会根据被跟踪对象一起旋转
其基本思想是对视频序列的所有图像帧都作MeanShift运算,并将上一帧的结果(即搜索窗口的中心位置和窗口大小)作为下一帧MeanShift算法的搜索窗口的初始值,如此迭代下去。简单点说,meanShift是针对单张图片寻找最优迭代结果,而camShift则是针对视频序列来处理,并对该序列中的每一帧图片都调用meanShift来寻找最优迭代结果。正是由于camShift针对一个视频序列进行处理,从而保证其可以不断调整窗口的大小,如此一来,当目标的大小发生变化的时候,该算法就可以自适应地调整目标区域继续跟踪。
参考
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
ret,frame = cap.read()#读取开摄像头的第一针帧图像
#标记初始感兴趣区域
r,h,c,w = 300,400,400,400
track_window = (c,r,w,h)
roi = frame[r:r + h,c:c + w] #感兴趣区域
hsv_roi = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)#将第一帧图像转变为HSV空间上的图像
#创建一个包含具有HSV值的ROI所有像素的掩码,HSV值在上下界之间才保留
#如果hsv_roi的第一个通道值在100以下视为0
#如果hsv_roi的第一个通道值在180以上视为0
#第二三通道一样
mask = cv2.inRange(hsv_roi,np.array((100.,30.,32.)),np.array((180.,120.,225.)))
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])#计算感兴趣区域的彩色直方图
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)#计算了直方图之后,归一化到0-255之间,输入的是roi_hist,输出还是roi_hist
#指定停止条件
term_crit = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
#停止条件解释为:均值漂移迭代10次或者中心移动至少一个像素,就停止计算中心移动
#COUNT到10 或者 EPS到1 就结束
#哪个条件先到达就停止
while True:
ret,frame = cap.read()
if ret == True:
#先将读取到的帧转化为HSV色彩空间
hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
#执行直方图反向投影,获得一个矩阵,里面的每个值以概率的形式表示
dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)
#将这个概率形式传给meanshift
ret,track_window = cv2.CamShift(dst,track_window,term_crit)
pts = cv2.boxPoints(ret)#获取矩形框的四个顶点坐标
pts = np.int0(pts)#int0 意味是 64位整数
img2 = cv2.polylines(frame,[pts],True,255,2) #多边形绘制
cv2.imshow('img2',img2)
k = cv2.waitKey(60) & 0xff
if k == 27:
break
else:
break
cv2.destroyAllWindows()
cap.release()
boxPoints函数会找到旋转矩形的顶点,而折线函数会在帧上绘制矩形的线段
5.卡尔曼滤波器
卡尔曼滤波器会对含噪声的输入数据流(如视频输入)进行递归操作(recursive)操作,并产生底层系统状态(如视频中的位置)在统计意义上的的最优估计
卡尔曼滤波算法可分为两个阶段:
预测:这个阶段卡尔曼滤波器使用当前点计算的协方差来估计目标的新位置
更新:这个阶段卡尔曼滤波器记录目标的位置,并为下一次循环计算修正协方差
鼠标跟踪小例子
在一个空帧上绘制两条线:一条是鼠标的实际动作路线,一条是对应于卡尔曼滤波器的预测轨迹
import cv2
import numpy as np
frame = np.zeros((800,800,3),np.uint8) #创建一个800*800 的空帧
#初始化测量坐标和鼠标运动预测
last_measurement = current_measurement = np.array((2,1),np.float32)#测量坐标初始化为(2,1)T
last_prediction = current_prediction = np.zeros((2,1),np.float32)#预测坐标初始化为(0,0)T
#跟踪的机制就是:存储上一次的测量和预测,用当前的测量来校正卡尔曼滤波器,计算卡尔曼滤波器的预测值
#绘制预测曲线,测量曲线
def mousemove(event,x,y,s,p):
global frame,current_measurement,measurements,lase_measurement,current_prediction,last_prediction
#没执行一次,上一次的current就变成了只一次的last
last_prediction = current_prediction
last_measurement = current_measurement
#这一次的current重新由鼠标实际测量(传入的参数x,y)赋值
current_measurement = np.array([[np.float32(x)],[np.float32(y)]])
kalman.correct(current_measurement)#用当前的测量来校正kalman滤波器
current_prediction = kalman.predict()#计算kalman滤波器的预测值
lmx,lmy = last_measurement[0],last_measurement[1]
cmx,cmy = current_measurement[0],current_measurement[1]
lpx,lpy = last_prediction[0],last_prediction[1]
cpx,cpy = current_prediction[0],current_prediction[1]
cv2.line(frame,(lmx,lmy),(cmx,cmy),(0,100,0),3)#测量的曲线由上一次测量到这一次测量的连线绘制而成(绿色)
cv2.line(frame,(lpx,lpy),(cpx,cpy),(0,0,200),3)#预测的曲线由上一次预测到这一次预测的连线绘制而成(红色)
#窗口初始化、定义鼠标回调函数(Callback)跟踪绘制结果
#opencv采用setMouseCallback函数处理鼠标事件
cv2.namedWindow("kalman_tracker")
cv2.setMouseCallback("kalman_tracker",mousemove)
#创建卡尔曼滤波器
kalman = cv2.KalmanFilter(4,2)
kalman.measurementMatrix = np.array([[1,0,0,0],[0,1,0,0]],np.float32)#设置测量矩阵
kalman.transitionMatrix = np.array([[1,0,1,0],[0,1,0,1],[0,0,1,0],[0,0,0,1]],np.float32)#设置转移矩阵
kalman.processNoiseCov = np.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],np.float32)*0.03#设置过程噪音协方差矩阵
while True:
cv2.imshow("kalman_tracker",frame)
if (cv2.waitKey(30) & 0xff) == 27:
break
cv2.destroyAllWindows()
setMousecallback(winname, MouseCallback onMouse, userdata=0)
winname:窗口的名字
onMouse:鼠标响应函数,回调函数。指定窗口里每次鼠标时间发生的时候,被调用的函数指针。这个函数的原型应该为on_Mouse(event, x,y, s, p);
userdate:传给回调函数的参数
on_Mouse(event, x, y, s, p)
event是 CV_EVENT_*变量之一
x和y是鼠标指针在图像坐标系的坐标(不是窗口坐标系)
s是CV_EVENT_FLAG的组合
param是用户定义的传递到setMouseCallback函数调用的参数。
常用的event:
CV_EVENT_MOUSEMOVE
CV_EVENT_LBUTTONDOWN
CV_EVENT_RBUTTONDOWN
CV_EVENT_LBUTTONUP
CV_EVENT_RBUTTONUP
和标志位flags有关的:
CV_EVENT_FLAG_LBUTTON
kalman滤波器
Kalman这个类需要初始化下面变量:
转移矩阵,测量矩阵,控制向量(没有的话,就是0),
过程噪声协方差矩阵,测量噪声协方差矩阵,
后验错误协方差矩阵,前一状态校正后的值,当前观察值。
在此cv2.KalmanFilter(4,2)表示转移矩阵维度为4,测量矩阵维度为2
卡尔曼滤波模型假设k时刻的真实状态是从(k − 1)时刻的状态演化而来,符合下式:
X(k) = F(k) * X(k-1) + B(k)*U(k) + W(k)
其中
F(k) 是作用在xk−1上的状态变换模型(/矩阵/矢量)。
B(k) 是作用在控制器向量uk上的输入-控制模型。
W(k) 是过程噪声,并假定其符合均值为零,协方差矩阵为Qk的多元正态分布。