0. 前言

机器学习基础一节中,我们介绍了机器学习的一些基本概念,并通过使用不同类别的样本来构建分类器。但这种方法训练分类器需要存储所有样本的表示,然后通过查看最近标记点(最近邻居)来预测新实例的标签。对于大多数机器学习方法,训练是一个迭代过程,在此过程中通过循环遍历样本来构建机器学习模型。通过使用更多的样本,得到的分类器性能会逐渐提高。当模型性能达到预设值或者当无法从当前训练数据集中获得更多改进时,学习过程将停止。本节中,我们将介绍一种遵循以上过程的机器学习算法,即级联分类器。

1. Haar 特征图像表示

在我们继续学习该分类器之前,首先将介绍 Haar 特征图像表示。我们已经知道,良好的图像表示是生成鲁棒性分类器的基本要素。
生成分类器的第一步是获取大量图像样本集合,这些样本包含要识别的对象类别的不同实例,样本的表示方式对利用样本构建的分类器的性能具有重要影响。像素级表示通常由于过于低级,而无法鲁棒地描述每一类对象的内在特征。相反,可以在多个尺度上描述图像中存在的独特图案的表示能够更好的表示图像特征。这就是 Haar 特征 (Haar features) 的基本思想,也称为类 Haar 特征 (Haar-like features),因为它们都源自 Haar 变换基函数 (Haar transform basis functions)。
Haar 特征定义了像素的小矩形区域,稍后通过简单的减法进行比较。通常可以使用三种不同的配置,即 2-矩形3-矩形4-矩形特征:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

这些特征可以是任意大小并应用于要表示的图像的任何区域。例如,下图中包括两个应用于人脸图像的 Haar 特征:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP
构建 Haar 表示包括选择任何给定类型、大小和位置的多个 Haar 特征,并将它们应用于图像。从选定的一组 Haar 特征中获得的一组特定值构成了图像表示。接下来,面临的挑战是确定要选择哪一组特征。事实上,为了区分不同对象,使用其中某些 Haar 特征可能比其他特征更有效。例如,在人脸图像样本中,在眼睛之间应用 3-矩形 Haar 特征(如上图所示)可能更有效。当然,由于存在数十万个可能的 Haar 特征,因此手动进行选择是十分困难的,我们需要使用机器学习方法来为给定的对象类别选择最相关的特征。

2. 基于级联 Haar 特征的二分类分类器

在本节中,我们将学习如何使用 OpenCV 构建增强的级联特征以生成二分类分类器。二分类分类器是一种可以从其他类别(例如,不包含人脸的图像)中识别出指定类(例如,人脸图像)实例的分类器,即分类任务中仅存在两个类别。在这种情况下,我们使用正样本(即人脸图像)和负样本(即非人脸图像)表示两种类别。本节使用的分类器由一系列简单的分类器组成,一次应用这些分类器。级联分类器的每一阶段都将根据为一小部分特征获得的值快速决定是否拒绝对象。在每个阶段通过做出更准确的决策来改进(或提升)前一个阶段的分类器性能,这种级联结构提升了分类器性能。这种方法的主要优点是级联的早期阶段由简单的检测组成,然后可以快速拒绝无关的类实例,使得级联分类器的计算更快,因为当通过扫描图像搜索一类对象时,大多数要测试的子窗口不属于我们感兴趣的类别。这样,只有少数窗口需要通过所有阶段才能决定被接受或拒绝。

(1) 为了训练增强级联分类器,OpenCV 提供用于执行这些操作的工具。安装 OpenCV 库时,会创建两个可执行程序并位于 bin 目录中:opencv_createsamplesopencv_traincascade

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

(2) 在训练分类器时,首先需要收集样本。正赝本由目标类实例的图像组成,在本节中,我们旨在训练一个分类器来识别交通标志,使用的正样本如下:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

(3) 使用的正样本列表在名为 sign.txt 的文本文件中指定,文件包含图像文件名和边界框坐标:

1.png 1 0 0 64 64
2.png 1 0 0 64 64
3.png 1 0 0 64 64
4.png 1 0 0 64 64
5.png 1 0 0 64 64
6.png 1 0 0 64 64
7.png 1 0 0 64 64
8.png 1 0 0 64 64
9.png 1 0 0 64 64

(4) 文件名后的第一个数字是图像中包含的正样本数,紧接着的两个值是包含正样本的边界框的左上角坐标,最后两个值是目标的宽度和高度。在本节中,我们已经在原始图像中提取了正样本,因此每个文件中有且仅有一个样本并且左上角坐标为 (0, 0)。接下来,通过运行提取器工具来创建正样本文件:

opencv_createsamples -info sign.txt -vec sign.vec -w 24 -h 24 -num 9

(5) 以上代码将创建 stop.vec 输出文件,其中包含输入文本文件中指定的所有正样本。需要注意的是,我们使样本大小 (24×24) 小于原始大小 (64×64),提取器工具将所有样本的大小调整为指定的大小。通常,Haar 特征与较小的模板能够更好的配合,但这必须根据具体情况进行验证。

(6) 负样本是不包含感兴趣类别实例的图像(在本节中,指不包含交通标志的图像)。除此之外,这些图像应该包含分类器期望获得的各类图像。这些负样本可以是任意大小,因为训练工具可以从这些图像中提取随机负样本:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

(7) 一旦正负样本准备完毕,就可以训练级联分类器了:

opencv_traincascade -data classifier -vec sign.vec -bg neg.txt -numPos 9 -numNeg 18 -numStages 20 -minHitRate 0.95 -maxFalseAlarmRate 0.5 -w 24 -h 24

需要注意的是,训练过程可能需要很长时间,在涉及数千个样本的复杂分类器中,训练过程甚至可能需要数天时间。运行训练程序时,级联分类器会在每次完成一个阶段的训练时打印出性能报告,给出分类器当前的命中率 (hit rate, HR),这是级联分类器当前接受的正样本的百分比(即,它们被正确识别为正样本,这种情况也称为真正例),我们希望该数字尽可能接近 1.0。报告还会提供当前的误报率 (false alarm rate, FAR),即被错误分类为正样本(也称为假正例)的已测试负样本的数量,我们希望该数字尽可能接近 0.0
在我们的分类器中训练只需几秒钟,生成的分类器的结构在训练阶段产生的 XML 文件中进行描述。训练完成后,就可以使用分类器了,我们可以向它输入任意样本,分类器将输出预测结果。

(8) 在本节中,我们用 24×24 的图像训练了级联分类器,但是,一般来说,我们需要找出任意尺寸图像的任意位置是否有类对象的实例。为了实现这一目标,我们需要扫描输入图像并提取样本大小的所有可能窗口。如果分类器足够准确,则只有包含目标对象的窗口才会返回正样本预测。但是,这仅在正样本具有合适大小时才有效,要在多个尺度上检测对象实例,必须通过构建图像金字塔,在每个级别将原始图像的大小进行缩放。通过这种方式,沿着金字塔向下,更大的目标示例最终将缩放至合适的大小。这是一个漫长的过程,但是 OpenCV 提供了实现此过程的类。首先,需要通过加载合适的 XML 文件来构建分类器:

    cv::CascadeClassifier cascade;
    if (!cascade.load("classifier/cascade.xml")) { 
        std::cout << "Error when loading the cascade classfier!" << std::endl; 
        return -1; 
    }

(9) 然后,使用输入图像调用检测方法:

    // 预测图片标签
    std::vector<cv::Rect> detections;
    cascade.detectMultiScale(inputImage,    // 输入图像
                            detections,     // 检测结果
                            1.1,            // 尺度缩放因子
                            1,              // 所需邻居检测数
                            0,              // 标志位
                            cv::Size(48, 48),       // 要检测的最小对象大小
                            cv::Size(200, 200));    // 要检测的最大对象大小
    std::cout << "detections= " << detections.size() << std::endl;
    for (int i = 0; i < detections.size(); i++)
        cv::rectangle(inputImage, detections[i], cv::Scalar(255, 255, 255), 2);
    cv::imshow("Sign detection", inputImage);

(10) 返回 cv::Rect 实例的向量,要可视化检测结果,只需在输入图像上绘制这些矩形:

    for (int i = 0; i < detections.size(); i++)
        cv::rectangle(inputImage, detections[i], cv::Scalar(255, 255, 255), 2);
    cv::imshow("Sign detection", inputImage);

使用分类器在图像上进行测试,可以得到以下结果:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

3. 级联分类器算法流程

在上一小节中,我们介绍了如何使用正负样本构建 OpenCV 级联分类器,接下来,我们将介绍训练级联分类器的基本步骤。我们介绍的级联分类器使用了 Haar 特征进行训练,但是,我们也可以使用其他图像特征构建级联分类器。
级联分类器背后包含两个核心思想。第一个是可以通过将几个性能较弱的分类器(即基于简单特征的分类器)组合在一起来构建得到性能较强的分类器;其次,在机器视觉中,负样本出现的概率比正样本更频繁,因此可以通过划分阶段进行有效的分类。早期阶段迅速拒绝明显的负实例,后期阶段对更复杂的样本做出更精细的决策。基于以上思想,我们继续介绍提升级联学习算法 (boosted cascade learning algorithm),我们所使用的算法基于 boosting 的变体算法 AdaBoost
在本节中,我们使用 Haar 特征来构建弱分类器。当应用一个 Haar 特征(给定类型、大小和位置)时,将获得一个值。然后通过找到最能根据该特征值对负实例和正实例进行分类的阈值来获得一个简单的分类器。为了找到最佳阈值,我们可以使用一些正样本和负样本 (opencv_traincascade 使用的正样本和负样本的数量由 -numPos-numNeg 参数指定)。由于有大量可能的 Haar 特征,检查所有特征并选择能够对样本集进行分类的最佳特征,显然,这个基础的分类器可能出错,即出现错误分类的情况,因此我们需要构建数个这种分类器,这些分类器是迭代添加的,每次搜索新的 Haar 特征时都会给出最佳分类。但是,由于在每次迭代中,我们都希望关注当前被错误分类的样本,因此通过对错误分类的样本赋予更高的权重来衡量分类性能。因此,最终将获得一组简单的分类器,然后根据这些弱分类器的加权和构建强分类器(即,性能更好的分类器被赋予更高的权重)。使用这种方法,可以通过组合数百个简单的特征来获得具有良好性能的强分类器。
然而,训练早期我们不希望由大量弱分类器组成强分类器。相反,我们需要找到仅使用少量 Haar 特征的简单分类器,以便快速拒绝明显的负样本,同时保留所有正样本。在经典形式中,AdaBoost 旨在通过计算假阴性(即被错误分类为负样本的正样本)和假阳性(即错误分类为正样本的负样本)的数量来最小化总分类误差。我们的目标是将绝大多数甚至全部正样本正确分类,同时最小化误报率,可以通过修改 AdaBoost 以便在预测真正例时给予更高的奖励。因此,在训练级联分类器的每个阶段,必须设置两个标准——最小命中率和最大误报率;在 opencv_traincascade 中,使用 -minHitRate (默认值为 0.995) 和 -maxFalseAlarmRate (默认值为 0.5) 参数指定。在各个阶段添加 Haar 特征,直到满足这两个性能标准。必须将最小命中率设置得较高,以确保正实例能够进入下一阶段;需要注意的是,如果某个正实例在某个阶段被拒绝,则无法恢复被拒绝的正实例。因此,为了便于生成低复杂度的分类器,应将最大误报率设置得较高。否则,将需要较多的 Haar 特征才能满足性能标准,这与早期训练弱分类器的思想相矛盾。
因此,一个好的级联分类器在早期应使用较少特征的组成,随着级联的增加,每个阶段的特征数量随之增加。在 opencv_traincascade 中,每个阶段的最大特征数使用 -maxWeakCount (默认值为 100) 参数设置,阶段数使用 -numStages (默认值为 20) 参数设置。
当开始新阶段的训练时,必须收集新的负样本,可以从提供的负样本图像中提取。难点是找到通过所有前期阶段的负样本(即那些被错误分类为正样本的样本)。训练的阶段越多,收集这些负样本就越困难,这就是需要为分类器提供大量负样本图像的原因,从难以分类的图像块中提取样本(因为它们类似于正样本)。此外,如果在给定阶段,在不添加任何新特征的情况下满足了两个性能标准,那么级联训练将在停止,这意味着我们可以按原样使用该模型,或者通过提供更多复杂样品。反之,如果无法达到性能标准,则训练也会停止,在这种情况下,我们应该使用更简单的性能标准尝试新的训练过程。
通过由 n 个阶段组成的级联分类器,可以很容易地证明分类器的全局性能至少会优于 minHitRatemaxFalseAlarmRate,这是由于每个阶段都建立在先前级联阶段的结果之上。例如,我们考虑 opencv_traincascade 的默认值,期望分类器具有 0.99 5 20 0.995^{20} 0.99520 的准确率(命中率)和 0. 5 20 0.5^{20} 0.520 的误报率。这意味着 90% 的正样本能被正确识别,0.001% 的负样本将被错误地归类为正样本。需要注意的是,当进行级联时,一部分正样本将丢失,我们必须提供比每个阶段使用的指定样本数量更多的正样本。在以上示例中,我们将 numPos 设置为可用正样本数量的 90%
一个重要的问题是应该使用多少样本进行训练?这需要根据具体应用判断,但显然,正样本集必须足够大才能涵盖大部分类实例。负样本图片也应该是相关的,通常的经验法则是令 numNeg = 2 * numPos
本节中,我们已经介绍了如何使用 Haar 特征构建级联分类器。此类特征也可以使用其他特征来构建,例如 LBP 特征或定向梯度直方图等,在 opencv_traincascade 程序中可以使用 -featureType 参数选择不同的特征类型。
OpenCV 库包含了许多预训练的级联分类器,可以使用这些分类器来检测人脸、面部特征等,我们可以在源目录的数据目录中找到这些 XML 文件。

4. 使用 Haar 级联检测器进行人脸检测

OpenCV 中已经提供了预训练的人脸检测模型,我们要做的就是使用适当的 XML 文件创建 cv::CascadeClassifier 类的实例:

    cv::CascadeClassifier faceCascade;
    if (!faceCascade.load("haarcascade_frontalface_default.xml")) {
        std::cout << "Error when loading the face cascade classfier!" << std::endl;
        return -1;
    }

然后,要检测具有 Haar 特征的人脸:

   faceCascade.detectMultiScale(picture,   // 输入图像
                detections,                 // 检测结果
                1.1,                        // 尺度缩放因子
                3,                          // 所需邻居检测数
                0,                          // 标记位
                cv::Size(48, 48),           // 要检测的最小对象大小
                cv::Size(200, 200));        // 要检测的最大对象大小
    std::cout << "detections= " << detections.size() << std::endl;

也可以使用相同的过程检测人物眼睛:

OpenCV实战(31)——基于级联Haar特征的目标检测-LMLPHP

5. 完整代码

完整代码 detectObjects.cpp 如下所示:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/objdetect.hpp>

int main() {
    // 打开正样本
    std::vector<cv::Mat> referenceImages;
    referenceImages.push_back(cv::imread("1.png"));
    referenceImages.push_back(cv::imread("2.png"));
    referenceImages.push_back(cv::imread("3.png"));
    referenceImages.push_back(cv::imread("4.png"));
    referenceImages.push_back(cv::imread("5.png"));
    referenceImages.push_back(cv::imread("6.png"));
    referenceImages.push_back(cv::imread("7.png"));
    referenceImages.push_back(cv::imread("8.png"));
    referenceImages.push_back(cv::imread("9.png"));
    // 组合图像
    cv::Mat positveImages(2 * referenceImages[0].rows, 4 * referenceImages[0].cols, CV_8UC3);
    for (int i = 0; i < 2; i++)
        for (int j = 0; j < 4; j++) {

            referenceImages[i * 2 + j].copyTo(positveImages(cv::Rect(j*referenceImages[i * 2 + j].cols, i*referenceImages[i * 2 + j].rows, referenceImages[i * 2 + j].cols, referenceImages[i * 2 + j].rows)));
        }
    cv::imshow("Positive samples", positveImages);
    cv::Mat negative = cv::imread("n1.jpg");
    cv::resize(negative, negative, cv::Size(), 0.33, 0.33);
    cv::imshow("One negative sample", negative);
    cv::Mat inputImage = cv::imread("sign.png");
    cv::resize(inputImage, inputImage, cv::Size(), 0.5, 0.5);
    cv::CascadeClassifier cascade;
    if (!cascade.load("classifier/cascade.xml")) { 
        std::cout << "Error when loading the cascade classfier!" << std::endl; 
        return -1; 
    }

    // 预测图片标签
    std::vector<cv::Rect> detections;
    cascade.detectMultiScale(inputImage,    // 输入图像
                            detections,     // 检测结果
                            1.1,            // 尺度缩放因子
                            1,              // 所需邻居检测数
                            0,              // 标志位
                            cv::Size(48, 48),       // 要检测的最小对象大小
                            cv::Size(200, 200));    // 要检测的最大对象大小
    std::cout << "detections= " << detections.size() << std::endl;
    for (int i = 0; i < detections.size(); i++)
        cv::rectangle(inputImage, detections[i], cv::Scalar(255, 255, 255), 2);
    cv::imshow("Sign detection", inputImage);

    // 人脸检测
    cv::Mat picture = cv::imread("girl.png");
    cv::CascadeClassifier faceCascade;
    if (!faceCascade.load("haarcascade_frontalface_default.xml")) {
        std::cout << "Error when loading the face cascade classfier!" << std::endl;
        return -1;
    }

    faceCascade.detectMultiScale(picture,   // 输入图像
                detections,                 // 检测结果
                1.1,                        // 尺度缩放因子
                3,                          // 所需邻居检测数
                0,                          // 标记位
                cv::Size(48, 48),           // 要检测的最小对象大小
                cv::Size(200, 200));        // 要检测的最大对象大小
    std::cout << "detections= " << detections.size() << std::endl;
    // 绘制检测到的对象边界框
    for (int i = 0; i < detections.size(); i++)
        cv::rectangle(picture, detections[i], cv::Scalar(255, 255, 255), 2);

    // 检测眼睛
    cv::CascadeClassifier eyeCascade;
    if (!eyeCascade.load("haarcascade_eye.xml")) {
        std::cout << "Error when loading the eye cascade classfier!" << std::endl;
        return -1;
    }

    eyeCascade.detectMultiScale(picture,    // 输入图像
                detections,                 // 检测结果
                1.1,                        // 尺度缩放因子
                3,                          // 所需邻居检测数
                0,                          // 标记位
                cv::Size(24, 24),           // 要检测的最小对象大小
                cv::Size(36, 36));          // 要检测的最大对象大小

    std::cout << "detections= " << detections.size() << std::endl;
    // 绘制检测到的对象边界框
    for (int i = 0; i < detections.size(); i++)
        cv::rectangle(picture, detections[i], cv::Scalar(0, 0, 0), 2);
    cv::imshow("Detection results", picture);
    cv::waitKey();
    return 0;
}

小结

Haar 特征能够在多个尺度上描述图像中存在的独特图案的表示,可以更好的表示图像特征,级联分类器通过将几个性能较弱的分类器组合在一起来构建得到性能较强的分类器,本节介绍了使用 cv::CascadeClassifier 函数实现级联 Haar 特征执行目标检测。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理
OpenCV实战(27)——追踪视频中的特征点
OpenCV实战(28)——光流估计
OpenCV实战(29)——视频对象追踪
OpenCV实战(30)——OpenCV与机器学习的碰撞

09-13 04:29