主题

本章我们要学习的是运动物体的跟踪,现代图像处理中经典的几种跟踪方法主要是:meanshift(均值漂移),Camshift(meanshift的优化版本),KCF,光流法等。 我们本章主要介绍的是前两种,meanshift(均值漂移)以及Camshift(meanshift的优化版本)  

均值漂移

首先我们需要了解什么是均值漂移,该算法是一种寻找概率函数离散样本的最大密度区域的算法,我们可以认为我们图像中感兴趣的区域就是离散样本密度最大的区域(这句话看不懂没关系,先往下看,后面就明白了) 首先我们要有一个均值漂移的基本概念:这个算法会向我们需要跟踪的区域的方向那慢慢移动过去。 设想在一个有N个样本点的特征空间 初始确定一个中心点center(随便选一个),计算在设置的半径为D的圆形空间内所有的点(xi)与中心点center的向量 计算整个圆形空间内所有向量的平均值,得到一个偏移均值 将中心点center移动到偏移均值所指向的位置 重复移动,直到没有办法继续增加圈内的点或移动距离过小后结束 整个过程如下图所示,points的值为圈内点的数量:   1   使用这种算法,就能让我们的圆逐步向点多的地方走去,这就是均值漂移的基本概念。  

彩色直方图

接下来我们需要学习彩色直方图的概念。 彩色直方图的x轴表示色彩的值,y轴表示这个色彩的像素有多少个,灰度图因为只有一个通道所以只有一个直方图,但如果是HSV或者BGR这种的,就可以对每一个通道进行直方图的构建,这里我们只需要对HSV图像的H通道进行跟踪即可。(因为我们接下来是根据普通的颜色来进行的跟踪,一个通道就足够了)   彩色直方图的输出如下图所示(对不同通道使用出来的直方图也不同,这张图并不是H通道的直方图,只是让大家明白直方图长啥样):   2   构建图像的直方图需要使用到函数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]) 
  这个时候我们还需要使用一种归一化的方法来对彩色直方图中的数量值进行规范化。 现有的直方图中的数值为对应像素的数量,其中图中出现数量最多的像素的数量值(最高的柱子对应的y轴数值)我们记为max的话,整个直方图y方向上的取值范围就是[0,max],我们需要把这个范围缩减到[0,255],为什么是255后面会进行解释。

归一化:cv2.normalize

这里我们需要使用到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,其余的柱子也会随之一样被压缩(类似于相似三角形那样的缩放感觉)。   没错,很简单得就介绍完了一种,不是很想了解其他几个的读者可以直接跳过本小节剩下来的内容了,因为剩下三种不是很常用。   接下来是NORM_INF,他会对我们每一个柱子的y轴坐标进行如下操作:用当前柱子的y轴坐标,除以所有柱子中y值的绝对值最大的那个作为新的y轴的值,公式如下所示(借几张图,自己做有点麻烦(咕咕咕))。   3   接下来是NORM_L1,他会对我们每一个柱子的y轴坐标进行如下操作:用当前柱子的y轴坐标,除以(所有柱子的y值的和)的绝对值作为新的y轴的值,公式如下所示:   4   接下来是NORM_L2,他会对我们每一个柱子的y轴坐标进行如下操作:用当前柱子的y轴坐标,除以(所有柱子的y值的平方和)的根号作为新的y轴的值,公式如下所示:   5  

直方图反投影

之前我们最上面提到过一个叫做“离散样本密度”的玩意儿,我们现在来更加具体化的讲解下。 这里我们需要名为“直方图反投影”的一些知识,简单来说,它会输出与输入图像(待搜索)同样大小的图像,其中的每一个像素值代表了输入图像上对应点属于目标对象(我们需要跟踪的目标)的概率。用更简单的话来解释,输出图像中像素值越高(越白)的点就越可能代表我们要搜索的目标 (在输入图像所在的位置)。 而对于灰度图而言,其只有一个通道,取值范围为0到255,所以我们之前在归一化的时候将直方图的y轴坐标的取值范围压缩到了0-255的范围内,就是为了这里可以直接赋值。 上面一段可能讲的还是有些含糊,下面我举一个例子来说明:   6   我们假设上面这张图就是我们要跟踪的对象所对应的彩色直方图(一个很大的跟踪对象由很多种不同的像素组成,然后我们将他们统计一下,每一种颜色分别对应着几个像素,有的颜色可能有很多像素,有的颜色可能一个也没有)。   上图左边红色的那个突出的柱子对应的是最大y轴值,他有100000个点,其他的柱子对应的像素的数目就是在[0,100000]之间,然后我们将彩色直方图归一化到[0,255]之间后,红色突出的柱子对应的最大y轴值就变成了255,其他的颜色对应的y轴的值也一一等比例改变到了0-255之间,这个我们要跟踪物体所构成的归一化彩色直方图我们称为Hist。   然后我们对我们要处理的图像(即包含我们要跟踪的对象的整个图片)根据我们上面得到的Hist来判断要处理的图像中的每一个像素是否属于我们跟踪对象或者说属于我们跟踪对象的概率有多大。   例如,我们要处理的图像中有一个点为红色,那么他就会去看我们跟踪对象的直方图Hist,发现直方图中的红色对应的属于跟踪对象的可能性(y轴的值)为255,则就会直接赋值为255(纯白色);如果要处理的图像中有一个点为棕色,然后去看直方图,发现发现直方图中的棕色对应的属于跟踪对象的可能性(y轴的值)为0,就会直接赋值为0(黑色),由此构成我们的直方图反投影图。   所以我们最后得到的图像就是一个与原图同样大小的灰度图像。例如我跟踪了下面绿色部分的物体,得到绿色部分的彩色直方图后,其对应的直方图反投影图就如下所示:   7   8   越暗的地方说明属于跟踪部分的可能性越低,越亮的地方属于跟踪部分的可能性越高。 这里使用到的函数为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)
 

meanshift跟踪实现程序

上述讲述的都是关于meanshift(均值漂移)的跟踪方法(其实Camshift也是这个样子的原理)。现在有了一切的基础知识后,我们就可以进行物体跟踪的实现了,这里我们打算跟踪一个绿色的物体来看看,首先我们载入我们要处理的视频文件(或者直接用摄像头也行):  
import cv2
import numpy as np
cap=cv2.VideoCapture('1.mp4')
  然后我们设置下我们第一个起始框的位置和长宽(可以理解为上面均值漂移原理中起始圆的起始位置和圆的大小)  
#r为rows,c为columns,h为height,w为weight
r,h,c,w=(400,500,400,500)
#跟踪框设置
track_window=(c,r,w,h)
  然后我们读取第一帧,先将图像转换为HSV图,方便目标跟踪,然后通过掩膜操作来得到图像中的绿色部分的掩膜:  
ret,frame=cap.read()
hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
#绿色物体的HSV上下限范围为(35,43,46),(77,255,255)
mask=cv2.inRange(hsv,np.array((35,43,46)),np.array((77,255,255)))
  (不同颜色的hsv上下限可以点击这个网址)   接着我们使用cv2.calcHist函数来得到图像中的绿色部分,并计算这部分的直方图,我们只要收集第0通道:H的数据就好了,因为是H通道,其取值范围为0-180,所以需要181根柱子,H通道像素的取值为0-180(柱子数量不是与像素取值范围不能一一对应的话,柱与柱之间会有点压缩,即x轴方向上会产生柱子与柱子之间的融合):  
hist=cv2.calcHist([hsv],[0],mask,[181],[0,180])
cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)
  然后我们来设置均值漂移meanshift的一次活动的终止条件:   cv2.TERM_CRITERIA_EPS:代表一次均值漂移累计的移动次数,EPS表示epsilon,这里我们设置为10。 cv2.TERM_CRITERIA_COUNT:表示一次均值漂移移动的最小偏移像素,如果一次漂移的像素值低于这个值,就会终止这次活动,这里我们设置为1。  
term_crit=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
  这里可能有读者会有点疑惑为什么要加上这个终止条件,其实这是为了让视频处理(摄像头)变得流畅而设置的,不然如果不设置这个条件,对于每一帧传入的图像,meanshift都要找到当前图像中最好的地方(密度最大的地方),然后才会开始处理下一帧,这样图像看起来就会变得异常卡顿。我们程序也没必要每一帧都完完全全处在跟踪物体的最佳位置上,容许有些许的偏差,只要能极大部分跟踪到了物体就可以了。 然后我们在第一帧中得到了要跟踪的物体的颜色直方图后,我们开始处理图像中的后续帧:  
while 1:
    ret,frame=cap.read()
    if ret== True:
    	#将整个画面转为HSV型
        hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
        #计算对于hist而言的直方图反向投影,得到概率分布图像dst
        dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
        #让我们设置的跟踪框从初始位置开始根据概率分布密度进行移动
        #终止条件为之前设置的term_crit
        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)
        cv2.namedWindow('img',cv2.WINDOW_NORMAL)
        cv2.imshow('img',img)
        if cv2.waitKey(1)==ord('q'):
            break
    else:
        break
cap.release()
cv2.destroyAllWindows()
  然后我们就能够实现绿色物体的跟踪了,运行结果如下所示:   9   完整代码如下所示:  
import cv2
import numpy as np
cap=cv2.VideoCapture('1.mp4')
ret,frame=cap.read()
r,h,c,w=(400,500,400,500)
#跟踪框
track_window=(c,r,w,h)
#获得绿色的直方图
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])
cv2.normalize(hist,hist,0,255,cv2.NORM_MINMAX)
term_crit=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,10,1)
while 1:
    ret,frame=cap.read()
    if ret== True:
        hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
        dst=cv2.calcBackProject([hsv],[0],hist,[0,180],1)
        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)
        cv2.namedWindow('img',cv2.WINDOW_NORMAL)
        cv2.imshow('img',img)
        if cv2.waitKey(1)==ord('q'):
            break
    else:
        break
cap.release()
cv2.destroyAllWindows()
   

Camshift跟踪

但我们可以注意到的是,我们矩形框的大小是我们一开始就直接设置好的,在整个跟踪过程中其大小是不会改变的(不管我们的跟踪物体是否变小或者变大了) 为了解决这个问题,我们可以在meanshift的基础上,让他自适应跟踪物体的大小来调整矩形框的大小,这就是Camshift。CamShift算法的全称是"Continuously Adaptive Mean-SHIFT",称为连续自适应的meanshift算法,算法部分不变,只是能让他能够自我适应跟踪物体大小而已。 代码方面也和meanshift差不多,只要在while循环里改几行就可以了:  
#之前的全部不变
while 1 :
    ret,frame=cap.read()
    if ret == True:
        hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
        dst=cv2.calcBackProject(hsv,[0],hist,[0,180],1)
        #下面开始改变,方法改为Camshift
        ret,track_window=cv2.CamShift(dst,track_window,term_crit)
        #注意这里不是用的track_window而是ret!
        boxes=cv2.boxPoints(ret)
        #因为要划线,所以下标必须为整形,所以将点的类型转为int
        pts=np.int0(boxes)
        #开始划线
        img2=cv2.polylines(frame,[pts],True,(0,255,0),2)
        #之后全部不变
        cv2.imshow('img2',img2)
        if cv2.waitKey(1)==ord('q'):
            break
    else:
        break
    然后运行结果中的绿色矩形框就能够根据跟踪的对象而自适应改变框的大小了(注意:这个Camshift很容易就会检测出错)

需要注意的点

1.这里我们可以看到的是,根据程序的意思来看,我们要跟踪的物体必须要处在第一帧内,不然我们就不能得到对应的彩色直方图了,这个其实只是编者写代码的问题,读者可以根据需要自行修改。 2.我们要跟踪的对象也可以不是指定的颜色区间,其他特定的东西也没问题,只要能得到颜色直方图的都可以。 3.我们这里的初始矩形框的位置是默认设置到一个位置上的,这其实不是很好,因为如果周边没有属于跟踪目标的像素或者可能性比较低,导致直方图反投影图中那一块部分全黑的话,那个框就会在那自行鬼畜(因为不知道该往哪走了),这块是可以加工的,比如直接把矩形框扔到密度比较高的部分也是可以的。(即先检测一帧,得到第一帧中密度高的区域,然后再设置矩形框) 4.这种检测中物体移动速度不宜过快过快,不然矩形框可能跟不上。  

总结

(前一段日子在赶着写书,被催稿(吐)导致的本系列咕咕咕了,之后可能还会咕,但是还是会尽量快的更新,谢谢大家支持啦!)