OpenCV Tutorials 25 - 级联分类器

2,784 阅读7分钟

级联分类器

本小节我们主要学习:

  1. Haar级联检测对象工作机制
  2. 应用基于Haar特征的级联分类器检测面部眼睛基础
  3. 使用 cv::CascadeClassifier 类来检测视频流中的物体,会用到以下函数:
    • cv::CascadeClassifier::load:载入一个.xml分类器文件(包括了Haar,LBP分类器)
    • cv::CascadeClassifier::detectMultiScale:执行检测

一、概念

使用基于 Haar 特征的级联分类器进行对象检测是 Paul Viola 和 Michael Jones 在其 2001 年的论文“Rapid Object Detection using a Boosted Cascade of Simple Features”中提出的一种有效的对象检测方法。它是一种基于机器学习的方法,其中 级联函数是从大量正负样本图像中训练出来的。 然后它用于检测其他图像中的对象。 在这里,我们将使用人脸检测。 最初,该算法需要大量的正图像(人脸图像)和负图像(没有人脸的图像)来训练分类器。 然后我们需要从中提取特征。 为此,使用了下图中显示的 Haar 特征。 它们就像我们的卷积核。 每个特征都是通过从黑色矩形下的像素总和中减去白色矩形下的像素总和而获得的单个值。

download.jpg

现在,每个内核的所有可能大小和位置都用于计算大量特征。 (想象一下它需要多少计算量?即使是 24x24 的窗口也会产生超过 160000 个特征)。对于每个特征计算,我们需要找到白色和黑色矩形下的像素之和。为了解决这个问题,他们引入了积分图像。无论您的图像多大,它都会将给定像素的计算减少为仅涉及四个像素的操作。不错,不是吗?它使事情变得超级快。

但在我们计算的所有这些特征中,大部分都是无关紧要的。例如,考虑下图。顶行显示了两个很好的功能。选择的第一个特征似乎侧重于眼睛区域通常比鼻子和脸颊区域更暗的属性。选择的第二个特征依赖于眼睛比鼻梁更暗的属性。但同样的窗口适用于脸颊或任何其他地方是无关紧要的。那么我们如何从 160000+ 个特征中选出最好的特征呢?它是由 Adaboost 实现的。

download.png

为此,我们在所有训练图像上应用每一个特征。对于每个特征,它会找到将人脸分类为正面和负面的最佳阈值。显然,会有错误或错误分类。我们选择具有最小错误率的特征,这意味着它们是最准确地分类人脸和非人脸图像的特征。 (过程没有这么简单,一开始给每张图片同等的权重。每次分类后,错误分类的图片的权重都会增加。然后做同样的过程。计算新的错误率。还有新的权重。过程一直持续到达到所需的准确性或错误率或找到所需数量的特征)。

最终的分类器是这些弱分类器的加权和。之所以称为弱分类器,是因为它单独无法对图像进行分类,但与其他分类器一起形成了一个强分类器。该论文称,即使是 200 个特征,也能以 95% 的准确率进行检测。他们的最终设置有大约 6000 个功能。 (想象一下从 160000+ 个特征减少到 6000 个特征。这是一个很大的收获)。

所以现在你拍一张照片。获取每个 24x24 窗口。对其应用 6000 个特征。看看是不是人脸。哇..是不是有点低效和耗时?是的。作者对此有一个很好的解决方案。

在一幅图像中,大部分图像是非人脸区域。所以最好有一个简单的方法来检查窗口是否不是人脸区域。如果不是,请一次性丢弃,不要再次处理。相反,请关注可能存在面孔的区域。这样,我们会花更多时间检查可能的面部区域。

为此,他们引入了分类器级联的概念。不是在一个窗口上应用所有 6000 个特征,而是将这些特征分组到分类器的不同阶段并一个一个地应用。 (通常前几个阶段包含的特征要少得多)。如果窗口在第一阶段失败,则丢弃它。我们不考虑它的其余功能。如果通过,则应用第二阶段的功能并继续该过程。通过所有阶段的窗口是人脸区域。计划如何!

作者的检测器有 38 个阶段的 6000 多个特征,前五个阶段有 1、10、25、25 和 50 个特征。 (上图中的两个特征实际上是从 Adaboost 中获得的最好的两个特征)。根据作者的说法,每个子窗口平均评估 6000 多个特征中的 10 个。

所以这是对 Viola-Jones 人脸检测如何工作的简单直观的解释。阅读本文了解更多详细信息或查看附加资源部分中的参考资料。

二、OpenCV中的Haar-cascade检测

OpenCV 提供了一种训练方法(参见 Cascade Classifier Training)或预训练模型,可以使用 cv::CascadeClassifier::load 方法读取。 预训练模型位于 OpenCV 安装的数据文件夹中,或者可以在此处找到。

以下代码示例将使用预训练的 Haar 级联模型来检测图像中的人脸和眼睛。 首先,创建一个 cv::CascadeClassifier 并使用 cv::CascadeClassifier::load 方法加载必要的 XML 文件。 之后,使用 cv::CascadeClassifier::detectMultiScale 方法完成检测,该方法返回检测到的人脸或眼睛的边界矩形。

  • 用法如下:
  1. cv.CascadeClassifier.load( filename ) -> retval
  2. cv.CascadeClassifier.detectMultiScale:
    • cv.CascadeClassifier.detectMultiScale( image[, scaleFactor[, minNeighbors[, flags[, minSize[, maxSize]]]]] ) -> objects
    • cv.CascadeClassifier.detectMultiScale2( image[, scaleFactor[, minNeighbors[, flags[, minSize[, maxSize]]]]] ) -> objects, numDetections
    • cv.CascadeClassifier.detectMultiScale3( image[, scaleFactor[, minNeighbors[, flags[, minSize[, maxSize[, outputRejectLevels]]]]]] ) -> objects, rejectLevels, levelWeights
from __future__ import print_function
import cv2 as cv
import argparse
def detectAndDisplay(frame):
    frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    # 直方图均衡,增强对比度
    frame_gray = cv.equalizeHist(frame_gray)
    #-- 检测人脸
    faces = face_cascade.detectMultiScale(frame_gray)
    # 圈出人脸
    for (x,y,w,h) in faces:
        center = (x + w//2, y + h//2)
        # 绘制椭圆
        frame = cv.ellipse(frame, center, (w//2, h//2), 0, 0, 360, (255, 0, 255), 4)
        faceROI = frame_gray[y:y+h,x:x+w]
        #-- 在检测出的每张脸上都绘制出眼睛
        eyes = eyes_cascade.detectMultiScale(faceROI)
        for (x2,y2,w2,h2) in eyes:
            eye_center = (x + x2 + w2//2, y + y2 + h2//2)
            radius = int(round((w2 + h2)*0.25))
            frame = cv.circle(frame, eye_center, radius, (255, 0, 0 ), 4)
    cv.imshow('Capture - Face detection', frame)
parser = argparse.ArgumentParser(description='Code for Cascade Classifier tutorial.')
# 首先应该载入data,将data文件夹下载后放在同目录下
parser.add_argument('--face_cascade', help='Path to face cascade.', default='data/haarcascades/haarcascade_frontalface_alt.xml')
parser.add_argument('--eyes_cascade', help='Path to eyes cascade.', default='data/haarcascades/haarcascade_eye_tree_eyeglasses.xml')
parser.add_argument('--camera', help='Camera divide number.', type=int, default=0)
args = parser.parse_args(args = [])

face_cascade_name = args.face_cascade
eyes_cascade_name = args.eyes_cascade
face_cascade = cv.CascadeClassifier()
eyes_cascade = cv.CascadeClassifier()

#-- 1.载入级联器
if not face_cascade.load(cv.samples.findFile(face_cascade_name)):
    print('--(!)Error loading face cascade')
    exit(0)
if not eyes_cascade.load(cv.samples.findFile(eyes_cascade_name)):
    print('--(!)Error loading eyes cascade')
    exit(0)
camera_device = args.camera
#-- 2. 读取视频流
cap = cv.VideoCapture(camera_device)
if not cap.isOpened:
    print('--(!)Error opening video capture')
    exit(0)
while True:
    ret, frame = cap.read()
    if frame is None:
        print('--(!) No captured frame -- Break!')
        break
    detectAndDisplay(frame)
    if cv.waitKey(10) == 27:
        break
cap.release()
cv.destroyAllWindows()

这是运行上述代码并使用内置网络摄像头的视频流作为输入的结果:

download.png

可以看出,检测仍存在一定的误差,把耳朵识别为脸部了

这是使用文件 lbpcascade_frontalface.xml(LBP 训练)进行人脸检测的结果。 对于眼睛,我们继续使用教程中使用的文件(haarcascade_eye_tree_eyeglasses.xml)。

def detectAndDisplay(frame):
    frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    # 直方图均衡,增强对比度
    frame_gray = cv.equalizeHist(frame_gray)
    #-- 检测人脸
    faces = face_cascade.detectMultiScale(frame_gray)
    # 圈出人脸
    for (x,y,w,h) in faces:
        center = (x + w//2, y + h//2)
        # 绘制椭圆
        frame = cv.ellipse(frame, center, (w//2, h//2), 0, 0, 360, (0, 255, 0), 4)
        faceROI = frame_gray[y:y+h,x:x+w]
        #-- 在检测出的每张脸上都绘制出眼睛
        eyes = eyes_cascade.detectMultiScale(faceROI)
        for (x2,y2,w2,h2) in eyes:
            eye_center = (x + x2 + w2//2, y + y2 + h2//2)
            radius = int(round((w2 + h2)*0.25))
            frame = cv.circle(frame, eye_center, radius, (0, 0, 255 ), 4)
    cv.imshow('Capture - Face detection', frame)


parser = argparse.ArgumentParser(description='Code for Cascade Classifier tutorial.')
# 修改面部检测的文件
parser.add_argument('--face_cascade', help='Path to face cascade.', default='data/lbpcascades/lbpcascade_frontalface.xml')
parser.add_argument('--eyes_cascade', help='Path to eyes cascade.', default='data/haarcascades/haarcascade_eye_tree_eyeglasses.xml')
parser.add_argument('--camera', help='Camera divide number.', type=int, default=0)
args = parser.parse_args(args = [])

face_cascade_name = args.face_cascade
eyes_cascade_name = args.eyes_cascade
face_cascade = cv.CascadeClassifier()
eyes_cascade = cv.CascadeClassifier()

#-- 1.载入级联器
if not face_cascade.load(cv.samples.findFile(face_cascade_name)):
    print('--(!)Error loading face cascade')
    exit(0)
if not eyes_cascade.load(cv.samples.findFile(eyes_cascade_name)):
    print('--(!)Error loading eyes cascade')
    exit(0)
camera_device = args.camera
#-- 2. 读取视频流
cap = cv.VideoCapture(camera_device)
if not cap.isOpened:
    print('--(!)Error opening video capture')
    exit(0)
while True:
    ret, frame = cap.read()
    if frame is None:
        print('--(!) No captured frame -- Break!')
        break
    detectAndDisplay(frame)
    if cv.waitKey(10) == 27:
        break
cap.release()
cv.destroyAllWindows()

download.jpg