目标跟踪是对摄像头视频中的移动目标进行定位的过程,对视频的处理分析也越来越成为计算机视觉的主流,而本质上视频是由一帧帧的图像组成,所以视频处理最终还是要归结于图像处理。关于视频帧如何获取的,在GUI特性那一章节已经说过,这里不再讲述。
1、 基本的运动检测
为了检测视频中的目标物体,首要任务就是识别视频帧中耳钉那些可能包含移动目标的区域。有不少实现视频目标检测的办法;例如,当检测所有移动的目标时,帧之间的差异会变得有用。当检测视频中移动的手时,基于皮肤颜色的均值漂移就是最好的解决方案。当知道跟踪对象的一方面时,模板匹配会是不做的选择。下面分析一个简单的例子:
import cv2
import numpy as np
camera = cv2.VideoCapture(0)
es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10,10))
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)# 再进行一次模糊处理(平滑)
continue
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)# 膨胀(dilate)图像,从而对孔(hole)和缺陷(imperfection)进行归一化处理
image, cnts, hierarchy = cv2.findContours(diff.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 计算一幅图中目标的轮廓
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("dif", diff)
if cv2.waitKey(int(1000 / 12)) & 0xff == ord("q"):
break
cv2.destroyAllWindows()
camera.release()
补充说明:进行模糊处理的原因:每个输入的视频丢回自然震动、光照变化或者摄像头本身等原因而产生噪声,对噪声进行平滑是为了避免在运动和跟踪时将噪声检测出来。当然这里是使用的OpenCV自带的UI控件,如果想用到工程项目中,显示在需要的地方,可以使用PyQt结合Matplotlib将画面完美的显示出来。感兴趣的同学可以参考一下我的PyQT结合OpenCV的Chat中的文章:http://gitbook.cn/gitchat/activity/5a433b3ffee1cd074a5cef06
效果如下:
对于这个简单的技术,其结果还算可以,但是也是有很大的缺点的,最明显的问题是需要通过提前设置‘默认’帧作为背景。在一些情况下,由于光照变化频繁,这种处理方法就显得相当不灵活。
2、 背景分割器
在OpenCV 3中有两种种背景分割器:K-Nearest(KNN),Mixture of Gaussians(MOG2);他们对应的算法用来计算背景分割。OpenCV提供了一个称为BackgroundSubtractor的类,在分割前景和背景时很方便,它是一个功能很全的类,不仅执行背景分割,而且能够提高背景检测的效果,并提供将分类结果保存到文件的功能。
2.1 BackgroundSubtractorMOG2
这是个以高斯混合模型为基础的背景/前景分割算法,这个算法的一个特点是它为每一个像素选择一个合适数目的高斯分布。这样就会对由于亮度等发生变化引起的场景变化产生更好的适应。首先需要创建一个背景对象。但在这里可以选择是否检测阴影。如果 detectShadows = T rue(默认值),它就会检测并将物体标记出来,但是这样做会降低处理速度。
下面看一个使用BackgroundSubtractorMOG2的简单的例子:
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
mog = cv2.createBackgroundSubtractorMOG2()
while(1):
ret, frame = cap.read()
fgmask = mog.apply(frame)
cv2.imshow('frame', fgmask)
if cv2.waitKey(int(1000 / 12)) & 0xff == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
你会发现在视频中不动的物体会慢慢变成背景(黑色),运动的物体会变成前景(白色)。
效果如下:
2.2 BackgroundSubtractorKNN
下面看一下BackgroundSubtractorKNN来实现的运动检测:
import cv2
import numpy as np
bs = cv2.createBackgroundSubtractorKNN(detectShadows=True)
camera = cv2.VideoCapture("shipin.flv")
while(1):
ret, frame = camera.read()
fgmask = bs.apply(frame)# 计算获得前景掩码
th = cv2.threshold(fgmask.copy(), 244, 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:
(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)
if cv2.waitKey(int(1000 / 12)) & 0xff == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
说明:为了方便这里使用了一段视频文件代替了摄像头,可以看出效果非常不错。
3、 均值漂移和CAMShift
背景分割是一种有效的技术,但并不是进行视频中目标跟踪的唯一可用的技术。均值漂移(Meanshift)是一种目标跟踪算法,该算法寻找概率函数离散样本的最大密度,并重新计算在下一帧中的最大密度,给出了目标的移动方向。
3.1 均值漂移
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
# cap = cv2.VideoCapture('surveillance_demo/768x576.avi')
# capture the first frame
ret,frame = cap.read()
# mark the ROI
r,h,c,w = 10, 200, 10, 200
# wrap in a tuple
track_window = (c,r,w,h)
# extract the ROI for tracking
roi = frame[r:r+h, c:c+w]# 提取感兴趣区域ROI
# switch to HSV
hsv_roi = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)# 转换为HSV色彩空间
# create a mask with upper and lower boundaries of colors you want to track
# # 下面是创建一个包含具有HSV值的ROI所有像素的掩码,HSV值在上界与下界之间
mask = cv2.inRange(hsv_roi, np.array((100., 30.,32.)), np.array((180.,120.,255.)))
# calculate histograms of roi
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])# 计算图像的色彩直方图
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)# 计算直方图后,响应的值被归一化到0-255范围内。
# Setup the termination criteria, either 10 iteration or move by atleast 1 pt
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )# 指定均值漂移终止一系列计算行为的方式
# 这里的停止条件为:均值漂移迭代10次后或者中心移动至少1个像素时,均值漂移就停止计算中心漂移
# 第一个标志(EPS或CRITERIA_COUNT)表示将使用两个条件的任意一个(计数或‘epsilon’,意味着哪个条件最先达到就停止)
while(1):
ret ,frame = cap.read()
if ret == True:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)
# print dst
# apply meanshift to get the new location
ret, track_window = cv2.meanShift(dst, track_window, term_crit)
# Draw it on image
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()
在你移动的过程中你会发现蓝色框的大小是固定的,如果由远及近的运动的话,固定的大小是不合适的,所有我们需要根据目标的大小和角度来对窗口的大小和角度进行修订。这就需要使用CAMShift技术了。
3.2 CAMShift
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
# take first frame of the video
ret,frame = cap.read()
# setup initial location of window
r,h,c,w = 10,200,10,200 # simply hardcoded the values
track_window = (c,r,w,h)
roi = frame[r:r+h, c:c+w]# 提取感兴趣区域ROI
hsv_roi = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)# 转换为HSV色彩空间
# 下面是创建一个包含具有HSV值的ROI所有像素的掩码,HSV值在上界与下界之间
mask = cv2.inRange(hsv_roi, np.array((100., 30.,32.)), np.array((180.,120.,255.)))
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])# 计算图像的色彩直方图
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)# 计算直方图后,响应的值被归一化到0-255范围内。
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )# 指定均值漂移终止一系列计算行为的方式
# 这里的停止条件为:均值漂移迭代10次后或者中心移动至少1个像素时,均值漂移就停止计算中心漂移
# 第一个标志(EPS或CRITERIA_COUNT)表示将使用两个条件的任意一个(计数或‘epsilon’,意味着哪个条件最先达到就停止)
while(1):
ret ,frame = cap.read()
if ret == True:
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)# 直方图反向投影 得到一个矩阵(每个像素以概率的形式表示)
ret, track_window = cv2.CamShift(dst, track_window, term_crit)
pts = cv2.boxPoints(ret)# 找到被旋转矩形的顶点,而折线函数会在帧上绘制矩形的线段
pts = np.int0(pts)
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()
4、光流法追踪运动物体
由于目标对象或者摄像机的移动造成的图像对象在连续两帧图像中的移动被称为光流。它是一个 2D 向量场,可以用来显示一个点从第一帧图像到第二帧图像之间的移动。如下图所示(https://en.wikipedia.org/wiki/Optical_flow):
图中显示了一个点在连续的5帧图像中的移动,箭头表示光流场的向量,光流在很多领域中都很有用:如由运动重建结构,视频压缩等。
4.1 OpenCV 中的 Lucas-Kanade 光流
import numpy as np
import cv2
cap = cv2.VideoCapture('shipin.flv')
# params for ShiTomasi corner detection
feature_params = dict( maxCorners = 100,qualityLevel = 0.3,
minDistance = 7,blockSize = 7 )
# Parameters for lucas kanade optical flow
#maxLevel 为使用的图像金字塔层数
lk_params = dict( winSize = (15,15),
maxLevel = 2,
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# Create some random colors
color = np.random.randint(0,255,(100,3))
# Take first frame and find corners in it
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **feature_params)
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)
while(1):
ret,frame = cap.read()
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# calculate optical flow 能够获取点的新位置
p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
# Select good points
good_new = p1[st==1]
good_old = p0[st==1]
# draw the tracks
for i,(new,old) in enumerate(zip(good_new,good_old)):
a,b = new.ravel()
c,d = old.ravel()
mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2)
frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1)
img = cv2.add(frame,mask)
cv2.imshow('frame',img)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
# Now update the previous frame and previous points
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1,1,2)
cv2.destroyAllWindows()
cap.release()
import cv2
import numpy as np
cap = cv2.VideoCapture("surveillance_demo/768x576.avi")
ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1,cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[...,1] = 255
while(1):
ret, frame2 = cap.read()
next = cv2.cvtColor(frame2,cv2.COLOR_BGR2GRAY)
flow = cv2.calcOpticalFlowFarneback(prvs,next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
#cv2.cartToPolar Calculates the magnitude and angle of 2D vectors.
mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])
hsv[...,0] = ang*180/np.pi/2
hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX)
rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)
cv2.imshow('frame2',rgb)
k = cv2.waitKey(30) & 0xff
if k == 27:
break
elif k == ord('s'):
cv2.imwrite('opticalfb.png',frame2)
cv2.imwrite('opticalhsv.png',rgb)
prvs = next
cap.release()
cv2.destroyAllWindows()
结语:本节就到此,谢谢。