目标跟踪

目标跟踪是对视频中的移动目标进行定位的过程,实时目标跟踪是许多计算机视觉应用的重要任务,例如监控、基于感知的用户界面、增强现实、基于对象的视频压缩以及辅助驾驶。
本文学习如何识别移动目标,并跨帧跟踪这些目标。其中最首要的任务是识别视频帧中哪些可能包含移动目标的区域。
当跟踪所有移动的目标时,帧之间的差异会变得有用,当跟踪视频移动的收的时候,基于皮肤颜色的均值漂移的方法是最好的解决方案,当知道跟踪对象的一方面时,模板匹配会是不错的技术。

基于运动的检测

最直观的方法就是计算帧之间的差异,并考虑背景帧和其他帧之间的差异。

import cv2
import numpy as np
cameracapture = cv2.VideoCapture(0)
print("showing camera feed. click window or press any key to stop.")
success,frame = cameracapture.read()
es =cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(9,4))
kernel = np.ones((5,5),np.uint8)
background = None
while (True):
    ret, frame = cameracapture.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)
    #计算与背景帧的差异,侵蚀和膨胀也可以作为噪声滤波器,通过cv2.morphologyEx函数来获得
    diff = cv2.absdiff(background,gray_frame)
    #将第一帧设置为整个输入的背景,对于每个从该点以后读取的帧都会计算其与背景之间的差异
    diff = cv2.threshold(diff,25,255,cv2.THRESH_BINARY)[1]
    diff = cv2.dilate(diff,es,iterations=2)
    #显示矩形 cv2.findContours计算一副图像中目标的轮廓,cv2.boundingRect计算矩形的边界框。
    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),(0,255,0),2)
    cv2.imshow("contours",frame)
    cv2.imshow('diff',diff)
    if cv2.waitKey(1000/12) & 0xff == ord("q"):
        break
cv2.destroyAllWindows()
camera.release()

背景分割器:KNN、MOG2、GMG

OpenCV提供了一个成为BackgroundSubtractor的类,在分割前景和背景的时候很方便,该类不仅执行背景分割,而且能够通过机器学习的方法提高背景检测的效果,并提供将分类结果保存到文件的功能。

import cv2
import numpy as np
cameracapture = cv2.VideoCapture(0)
#BackgroundSubtractor类是专门用于视频分析的,会对每一帧的环境进行学习,用GMG来指定用户初始化视频分析的帧数,默认120帧
#按照时间推移来提高运动分析的结果;
#BackgroundSubtractor类的另外一个特征是它可以计算阴影,对于精确读取视频帧是重要的
mog = cv2.createBackgroundSubtractorMOG2()
while(1):
    ret,frame = cameracapture.read()
    fgmask = mog.apply(frame)
    cv2.imshow('frame',fgmask)
    if cv2.waitKey(30) & 0xff:
        break
    cap.release()
    cv2.destroyAllWindows()

使用BackgroundSubtractorKNN来实现运动检测

#使用BackgroundSubtractorKNN来实现运动检测
import cv2
import numpy as np
#createBackgroundSubtractorKNN 计算前景掩码
bs = cv2.createBackgroundSubtractorKNN(detectShadows=True)
camera = cv2.VideoCapture(0)
while True:
    ret,frame = camera.read()
    fgmask = bs.apply(frame)
    #前景掩码含有前景的白色值以及阴影的灰色值,在阈值图像中,经非纯白色的所有像素都设置为0,而不是1.
    th = cv2.threshold(fgmask.copy(),244,255,cv2.THRESH_BINARY)[1]
    #识别目标
    dilated = cv2.dilate(th,cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)),iterations=2)
    #检测轮廓
    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(30) & 0xff == 27:
        break
camera.release()
cv2.destroyAllWindows()

均值漂移和CAMShift

背景分割不是视频中目标跟踪唯一的技术,均值漂移是一种目标跟踪算法,该算法寻找概率函数离散样本的最大密度,并且重新计算在下一帧中的最大密度,该算法给出了目标的移动方向。
均值漂移在跟踪视频感兴趣区域时很有效,可采样训练好的SVM进行目标检测,然后开始使用均值漂移跟踪检测到的目标。

import cv2
import numpy as np
#获取初始感兴趣的区域
camera = cv2.VideoCapture(0)
ret,frame = camera.read()
r,h,c,w = 10,200,10,200
track_window =(c,r,w,h)
#提取roi区域并将其转换成HSV颜色空间
roi = frame[r:r+h,c:c+w]
hsv_roi = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
#创建一个包含roi所有像素的掩码区间
mask = cv2.inRange(hsv_roi,np.array((100.,30.,32.)),np.array((180.,120.,255.)))
#构建彩色直方图即彩色图像的颜色分布,H:0-180,S,V:0-360
#calcHist从图像中提取彩色直方图,对图像的颜色进行统计并进行展示
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])
#对直方图数据进行标准化处理
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)
#设定均值漂移停止的方式
term_crit = (cv2.TERM_CRITERIA_EPS|cv2.TERM_CRITERIA_COUNT,10,1)
while True:
    ret,frame = camera.read()
    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        #calcBackProject构建彩色直方图,可用来计算图像的每个像素属于原始图像的概率,结果是一个矩阵。
        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+2,y+h),255,2)
        cv2.imshow('img2',img2)
        k=cv2.waitKey(60) & 0xff
        if k == 27:
            break
cv2.destroyAllWindows()
camera.release()

存在问题:窗口大小并不与被跟踪帧的目标大小一起变化。

CAMShift连续自适应均值漂移

均值漂移收敛时会调整跟踪窗口的大小
在调用CAMShift后,会根据具体的旋转来绘制矩阵,这种旋转会与被跟踪对象一起旋转。

import cv2
import numpy as np
#获取初始感兴趣的区域
camera = cv2.VideoCapture(0)
ret,frame = camera.read()
r,h,c,w = 300,200,400,300
track_window =(c,r,w,h)
#提取roi区域并将其转换成HSV颜色空间
roi = frame[r:r+h,c:c+w]
hsv_roi = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
#创建一个包含roi所有像素的掩码区间
mask = cv2.inRange(hsv_roi,np.array((100.,30.,32.)),np.array((180.,120.,255.)))
#构建彩色直方图即彩色图像的颜色分布,H:0-180,S,V:0-360
#calcHist从图像中提取彩色直方图,对图像的颜色进行统计并进行展示
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180])
#对直方图数据进行标准化处理
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)
#设定均值漂移停止的方式
term_crit = (cv2.TERM_CRITERIA_EPS|cv2.TERM_CRITERIA_COUNT,10,1)
while True:
    ret,frame = camera.read()
    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        #calcBackProject构建彩色直方图,可用来计算图像的每个像素属于原始图像的概率,结果是一个矩阵。
        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)
        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()
camera.release()

卡尔曼滤波器

卡尔曼滤波器对含有噪声的数据流进行递归操作,并产生视频中位置在统计学意义上的最优估计。
卡尔曼滤波器分为两个阶段:
预测:使用由当前点计算的协方差来估计目标的新位置
更新:记录目标的位置没并为下一次循环计算修正协方差。
卡尔曼滤波函数用predict()函数来估算目标位置,并用correct()来修正卡尔曼滤波器的预测结果。

import cv2
import numpy as np
#创建一个大小为800*800的空帧
frame = np.zeros((800,800,3),np.uint8) #创建空帧
last_measurement = current_measurement = np.array((2,1),np.float32)
last_prediction = current_prediction = np.zeros((2,1),np.float32)
def mousemove(event,x,y,s,p):
    global frame, current_measurement, measurements, last_measurement,current_prediction,last_prediction
    last_prediction = current_prediction
    last_measurement = current_measurement
    current_measurement = np.array([[np.float32(x)],[np.float32(y)]])
    kalman.correct(current_measurement)
    current_prediction =  kalman.predict()      
    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))
    cv2.line(frame, (lpx,lpy), (cpx,cpy), (0,0,200))
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,0,1],np.float32))
kalman.processNoiseCov = np.array(([1,0,0,0],[0,1,0,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()

一个基于行人跟踪的例子

步骤:
1、检查第一帧
2、检查后面输入的帧,从场景的开始通过背景分割器来识别场景中的行人;
3、为每一个行人建立ROI,并利用卡尔曼滤波和CAMShift来跟踪行人ID
4、检查下一帧是否由进入场景的新行人。
每一个被跟踪的对象都需要一个卡尔曼滤波器
根据所涉及变量状态的不同,函数结果将随时间而改变。