01、背景知识
1.1●Haar级联简介
Haar级联是基于Haar特征的级联分类器。那么级联分类器是什么?级联分类器就是把弱分类器(性能受限的分类器)串联成强分类器的过程。建立一个实时系统的前提是,需要保证分类器足够简单。但简单分类器不能达到足够精确,如果想要更精确,运行速度就会变慢且变成计算密集型。在机器学习中,速度和精确度两者之间的取舍是很常见的。为了解决这一问题,可以串联一群弱分类器,形成一个统一的强分类器。弱分类器不需要很精确,而串联后形成的强分类器具有高精确度。
Haar分类器又称为Viola-Jones识别器,是Viola和Jones分别于2001年、2004年提出和改进的。Haar分类器由Haar特征提取、离散强分类器、强分类级联器组成。其核心思想是提取人脸的Haar特征,使用积分图像对特征进行快速计算,然后挑选出少量关键特征,送入由强分类器组成的级联分类器进行迭代训练。
1.2●问题描述
本例使用Haar级联来检测视频中的脸部。Paul Viola和Michael Jones在2001年的标志性研究论文中首先提出了这种目标检测方法。论文描述了一种可用于检测任何物体的、有效的机器学习技术,使用了简单分类器的增强级联。这种级联可以用于构建高精度执行的整体分类器,因为它规避了构建高精度执行的单步分类器的过程,构建这样一个健壮的单步分类器是一个计算密集型的过程。
2.1●Mean Shift简介
Mean Shift(均值偏移,也叫均值漂移或均值平移)的概念最早是由Fukunaga等人于1975年在The estimation of the gradient of a density function with application in pattern recognition这篇关于概率密度梯度函数的论文中提出来的,其最初含义正如其名,就是偏移的均值向量。但随着Mean Shift理论的发展,其含义也发生了变化。Mean Shift在聚类、图像平滑、图像切割和跟踪方面得到了较广泛的应用。
Mean Shift算法是一个迭代的步骤,首先计算出当前点的偏移均值,然后开始移动该点直到偏移均值,接着作为新的起始点继续移动,这样迭代下去,直到满足条件时结束。
Mean Shift的一个问题是不允许随着时间的推移而改变目标的大小。一旦画出一个边框,无论对象距离摄像机有多近或多远,这个边框都会保持不变,这就是需要使用CAMShift的原因,因为它可以使边框适应对象的大小。
2.2●CAMShift算法简介
CAMShift算法的思想是对视频序列的所有图像帧做Mean Shift运算,下一帧Mean Shift算法的搜索窗口初始值是上一帧的结果(即搜索窗口的中心位置和窗口大小),这样来进行迭代。Mean Shift算法是针对单张图片寻找最优的迭代结果,对应的CAMShift算法则是处理视频序列,并对每一帧图片都调用Mean Shift来寻找最优迭代结果。由于CAMShift的处理单位是一个视频序列,因此保证其可以不断地调整窗口大小,当目标的大小变化时,CAMShift算法就可以自适应地调整目标区域,保证持续跟踪。
3.1●光流法
目前,光流(optical flow)法是运动图像分析的重要方法之一,它是由James J. Gibson于20世纪40年代首先提出的。它是像素的运动瞬时速度,即空间中的运动物体在观察平面上的像素运动的瞬时速度。光流利用图像序列中像素在时间域上的变化与相邻帧之间的相关性,找到当前帧跟上一帧之间存在的对应关系,从而计算出相邻帧之间的物体运动信息。
在计算机视觉中,Lucas–Kanade算法是一种两帧差分的光流估计算法,它是由Bruce D. Lucas和Takeo Kanade提出的。这个算法是目前最常见、最流行的。它计算两帧在时间在t~t + δt每个像素位置的移动。由于它基于图像信号的泰勒级数,这种方法称为差分,也就是对空间和时间坐标使用偏导数。Lucas-Kanade算法广泛用于图像对齐、光流法、目标追踪、图像拼接和人脸检测等课题中。
3.2●角点检测
角点检测(corner detection)是计算机视觉系统中用来获得图像特征的一种方法,也称为特征点检测。常用的角点检测算法有Harris和Shi-Tomasi,本例中用的就是Shi-Tomasi角点检测算法。
角点通常被定义为两条边的交点。例如,三角形有三个角,矩形有四个角,这些点就是角点,也叫作矩形、三角形的特征。上面所说的是严格意义上的角点,但是从广义来说,角点指的是拥有特定特征的图像点,这些特征点在图像中有具体的坐标,并具有某些数学特征(比如局部最大或最小的灰度)。
02.实现代码
2.1、使用Haar级联进行人脸检测
import cv2 import numpy as np face_cascade = cv2.CascadeClassifier( 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture('face.mp4') scaling_factor = 0.5 while True: _, frame = cap.read() frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) face_rects = face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in face_rects: cv2.rectangle(frame, (x, y), (x + w, y + h ), (0, 255, 0), 2) cv2.imshow('Output',frame) c = cv2.waitKey(1) if c == 27: break cap.release() cv2.destroyAllwindows()
2.2使用CAMShift算法进行人脸追踪
import cv2 import numpy as np cap = cv2.VideoCapture('face.mp4') _, frame = cap.read() hsv_roi = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv_roi, np.array((0.,60., 32.)), np.array((180., 255., 255.))) x0, y0, x1, y1 = 200, 100, 300, 400 track_widow = (x0, y0, x1, y1) roi = frame[y0:y0+y1, x0:x0+x1] hist = cv2.calcHist([hsv_roi], [0], mask, [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: _, frame = cap.read() scaling_factor = 0.5 frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) dst = cv2.calcBackProject([hsv], [0], hist, [0,180], 1) ret, track_widow = cv2.CamShift(dst, track_widow, term_crit) pts = cv2.boxPoints(ret) pts = np.int0(pts) img = cv2.polylines(frame, [pts], True, (0,255,0), 2) cv2.imshow('Output', img) key = cv2.waitKey(5) if key == 27: break cap.release() cv2.destroyAllWindows()
2.3使用光流法进行人脸追踪
import cv2 import numpy as np feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7) lk_params = dict(winSize=(15,15), maxLevel=2, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) cap = cv2.VideoCapture('face.mp4') _, frame = cap.read() scaling_factor = 0.5 frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) p0 = cv2.goodFeaturesToTrack(gray, mask=None, **feature_params) mask = np.zeros_like(frame) while True: _, frame = cap.read() frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) p1, st, err = cv2.calcOpticalFlowPyrLK(gray, frame_gray, p0, None, **lk_params) good_new = p1[st == 1] good_old = p0[st == 1] for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() cv2.line(mask, (a, b),(c, d),(0, 150, 0), 1) cv2.circle(frame, (a, b), 3, (0, 255, 0), -1) gray = frame_gray.copy() p0 = good_new.reshape(-1, 1, 2) img = cv2.add(frame, mask) cv2.imshow("Output", img) k = cv2.waitKey(30) if k == 27: break cap.release() cv2.destroyAllWindows()
03.运行结果
3.1使用Haar级联进行人脸检测
3.2使用CAMShift算法进行人脸追踪
3.3 使用光流法进行人脸追踪
04、代码分析
4.1使用Haar级联进行人脸检测
在本程序中,首先加载与人脸检测相对应的Haar级联文件,这个文件是OpenCV自带的一个级联分类器,可以在OpenCV的data文件中找到它。该文件中还包含很多分类器,可以用来检测眼睛、身体、微笑等,感兴趣的读者可以自己动手尝试一下。
接下来加载视频文件并定义比例因子的大小,使输出窗口的大小较为舒适。当文件被成功加载之后,捕获当前帧,使用比例因子调整它的大小,并将其转换为灰度图。然后在灰度图像上使用detectMultiScale()函数采取人脸检测,detectMultiScale()函数的作用:对图像进行多尺度多目标检测,从而筛选出符合我们需求的目标。其次遍历检测到的人脸并在面部周围画一个矩形代表我们成功检测到人脸,就像是常用的手机相机一般。最后使用imshow()函数将其输出出来。
在程序的输出中可以看到,程序正确地检测出了人脸,并在其周围画出了一个矩形。
4.2使用CAMShift算法进行人脸追踪
在本程序中,首先加载视频文件,并捕获当前帧。把该帧转换为HSV图,限定颜色的取值范围,得到掩膜mask。接着,设置初始窗口的大小和位置,并定义要跟踪的区域roi。程序第13~14行使用calcHist()函数计算图像的直方图并将其进行归一化处理,以便使计算机能够更好地理解这些数据。归一化技术是数据预处理的方式之一,这里不做详细介绍。
第15行设置跟踪器的终止条件,迭代10次或至少移动1次。在循环中,捕获当前帧,调整它的大小并将其转换为HSV图。使用calcBackProject()方法计算该HSV图的反向投影,反向投影的作用是在输入图像中查找特定图像最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。接着,调用CamShift()方法在反向投影中寻找目标区域并且回。
程序第25~28行将目标区域在图像上绘制出来并显示输出结果。
最后,如果用户按下Esc键,那么就退出循环。退出循环后使用release()函数释放视频捕捉对象,并使用destroyAllWindows()函数关闭所有窗口。
在程序的输出中会选中移动的目标区域,并实时跟踪目标的移动。
4.3 使用光流法进行人脸追踪
在本程序中,首先设置了角点(特征点)检测的参数,如最大角点、质量等级、最小距离和区块大小,这些用来计算良好的特征以便进行跟踪。接着设置光流场的参数,如窗口大小、最大等级和终止标准,其中最大等级为使用图像金字塔(图像金字塔是以多个分辨率表示图像的一种有效且简单的概念,它是分辨率逐层降低的、以金字塔形状排列的、图像集合)的层数。其次加载视频,获取到视频的第一帧,调整第一帧的大小并转换为灰度图。goodFeaturesToTrack()函数是寻找好的角点。接着创建一个掩膜mask,以便后面绘制角点的光流轨迹。
在循环中使用方法calcOpticalFlowPyrLK()计算光流,该方法通过金字塔光流方法Lucas-Kanade计算特征集的光流,获取角点的新位置。接着选取好的角点,筛选出旧的角点对应的新角点,并且绘制角点的轨迹。最后,更新当前帧和当前角点的位置,并显示输出到屏幕。copy()函数创建当前帧的一个副本,cv2中的add()方法将两幅图片进行叠加。
在程序的输出中,可以在屏幕上看到显示的角点,并且随着画面的移动,角点也会随之移动。