OpenCV实战(30)——OpenCV与机器学习的碰撞

OpenCV实战(30)——OpenCV与机器学习的碰撞

    • 0. 前言
    • 1. 机器学习简介
    • 2. 基于局部二值模式的最近邻人脸识别
    • 3. 图像表示与人脸识别
    • 4. 完整代码
    • 小结
    • 系列链接

0. 前言

随着人工智能的发展,许多机器学习算法开始用于解决机器视觉问题。机器学习是一个广泛的研究领域,包含许多重要的概念,本节我们将介绍一些主要的机器学习技术,并介绍如何使用 OpenCV 在计算机视觉系统中应用这些技术。

1. 机器学习简介

机器学习的核心是开发可以自行学习如何对数据输入进行处理的计算机系统。机器学习系统无需明确的显式编程,而是根据数据样本自动训练和学习,一旦系统成功完成训练,则训练后的系统可以对新的没有见过的数据输出正确的结果。
机器学习可以用于解决多种类型的问题,但在本节,我们重点是分类问题。通常,为了构建一个可以识别特定类别实例的分类器,必须使用大量带标签的样本来训练分类器。在二分类问题中,样本数据集由表示要学习的类实例的正样本和由不属于感兴趣类实例的负样本组成。从样本数据中,系统将学习能够预测输入实例正确类别的决策函数。
在计算机视觉中,数据样本可以是图像或视频片段。因此,首先需要以一种统一的方式描述每个图像的内容,一种简单的表示是将图像缩放至固定大小,将缩放后的像素的逐行连接形成一个向量,然后将其用作机器学习算法的训练样本。本节中我们将学习不同的图像表示方法,并构建一个经典的人脸识别模型。

2. 基于局部二值模式的最近邻人脸识别

我们首先介绍最近邻分类 (nearest neighbor classification) 以及局部二值模式 (Local Binary Pattern, LBP) 特征,局部二值模式是一种流行的图像表示方法,以独特的方式对图像的纹理图案和轮廓进行编码。
我们将使用以上两种技术解决人脸识别问题。人脸识别一个非常具有挑战性的问题,在过去的 20 年中一直是流行的研究对象,本节中,我们将介绍在 OpenCV 中实现的人脸识别解决方案。
OpenCV 库提供了许多通用 cv::face::FaceRecognizer 类的子类实现的人脸识别方法。在本节中,我们将学习 cv::face::LBPHFaceRecognizer 类,它是一种基于简单但通常有效的分类方法,即最近邻分类器。此外,它使用的图像表示是根据 LBP 特征构建的,这是一种流行的表征图像模式的方式。

(1) 为了创建 cv::face::LBPHFaceRecognizer 类的实例,调用其静态 create 方法:

    cv::Ptr<cv::face::FaceRecognizer> recognizer =
        cv::face::LBPHFaceRecognizer::create(1, // LBP 模式半径
                                        8,      // 要考虑的邻居像素数
                                        8, 8,   // 单元格尺寸
                                        200.);  // 到最近邻居的最小距离

(2) cv::face::LBPHFaceRecognizer 类的前两个参数用于描述要使用的 LBP 特征,然后向识别器提供输入参考人脸图像。输入参考图像需要提供两个向量:一个包含人脸图像,另一个包含相关的标签,标签是整数值,用于标识特定的人物。通过向识别器输入要识别的人的不同图像来训练识别器,输入图像越具有代表性,识别正确人物的机会就越大。在示例中,我们仅提供两个参考人物的两张图像,train 方法是要调用的方法:

    // 参考图像矢量及其标签
    std::vector<cv::Mat> referenceImages;
    std::vector<int> labels;
    // 打开参考图像
    referenceImages.push_back(cv::imread("p0_1.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(0); // person 0
    referenceImages.push_back(cv::imread("p0_2.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(0); // person 0
    referenceImages.push_back(cv::imread("p1_1.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(1); // person 1
    referenceImages.push_back(cv::imread("p1_2.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(1); // person 1
    // 通过计算 LBPH 来训练分类器
    recognizer->train(referenceImages, labels);

(3) 使用的图片如下图所示,第一行是编号为 0 的人物图片,第二行是编号为 1 的人物图片:

OpenCV实战(30)——OpenCV与机器学习的碰撞_第1张图片

(4) 参考图像的质量也很重要。此外,我们可以对其执行标准化,即将主要面部特征置于标准化位置。例如,鼻尖位于图像的中间,两只眼睛水平对齐在特定的图像位置,可以使用这种方法自动标准化面部图像的面部特征检测方法。通过提供一个输入图像,模型会预测人脸图像对应的标签:

    // 预测图像标签
    recognizer->predict(inputImage,     // 人脸图像
                        predictedLabel, // 图像的预测标签
                        confidence);    // 预测的置信度

输入图像如下图所示:

OpenCV实战(30)——OpenCV与机器学习的碰撞_第2张图片

识别器不仅会返回预测的标签,还会返回相应的置信度分数。在 cv::face::LBPHFaceRecognizer 类中,置信度用于衡量所识别人脸和原模型的差距,该值越低,识别器对其预测的置信度就越高。

3. 图像表示与人脸识别

为了理解本节中介绍的人脸识别方法,接下来,我们将解释它的两个主要组成部分:图像表示和分类方法。
cv::face::LBPHFaceRecognizer 算法利用了 LBP 特性,这是一种描述图像中存在的图像模式的方式。它是一种局部表示,通过对邻域中发现的图像强度模式进行编码,将每个像素转换为二进制表示。为了实现这个目标,需要应用以下规则;将局部像素与其选定的每个相邻像素进行比较,如果其值大于其邻居的值,则将相应位置的值置为 0,否则将其置为 1。最常见的情况下,需要将每个像素与其 8 个直接相邻像素进行比较,从而生成 8 位模式。例如,假设我们有以下局部模式:
[ 87 98 17 21 26 89 19 24 90 ] \left[ \begin{array}{ccc} 87&98&17\\ 21&26&89\\ 19&24&90\\\end{array}\right] 872119982624178990
应用上述规则会生成以下二进制值:
[ 1 1 0 0 1 0 0 1 ] \left[ \begin{array}{ccc} 1&1&0\\ 0&&1\\ 0&0&1\\\end{array}\right] 10010011
取左上角像素作为初始位置并顺时针移动,中心像素被 11011000 的二进制序列替换。通过循环图像的所有像素以生成所有像素相应的 LBP 字节,可以生成完整的 8LBP 图像:

// 计算灰度图像点局部二值特征
void lbp(const cv::Mat &image, cv::Mat &result) {
    assert(image.channels() == 1);      // 输入图像必须为灰度图像
    result.create(image.size(), CV_8U); // 内存分配
    for (int j = 1; j<image.rows - 1; j++) {                    // 循环所有行 (除了第一行和最后一行)
        const uchar* previous = image.ptr<const uchar>(j - 1);  // 上一行
        const uchar* current = image.ptr<const uchar>(j);       // 当前行
        const uchar* next = image.ptr<const uchar>(j + 1);      // 下一行
        uchar* output = result.ptr<uchar>(j);                   // 输出行
        for (int i = 1; i<image.cols - 1; i++) {
            // 局部二值特征
            *output = previous[i - 1] > current[i] ? 1 : 0;
            *output |= previous[i] > current[i] ? 2 : 0;
            *output |= previous[i + 1] > current[i] ? 4 : 0;
            *output |= current[i - 1] > current[i] ? 8 : 0;
            *output |= current[i + 1] > current[i] ? 16 : 0;
            *output |= next[i - 1] > current[i] ? 32 : 0;
            *output |= next[i] > current[i] ? 64 : 0;
            *output |= next[i + 1] > current[i] ? 128 : 0;
            output++;   // 下一像素
        }
    }
    // 将未处理像素置为零
    result.row(0).setTo(cv::Scalar(0));
    result.row(result.rows - 1).setTo(cv::Scalar(0));
    result.col(0).setTo(cv::Scalar(0));
    result.col(result.cols - 1).setTo(cv::Scalar(0));
}

循环体将每个像素与其八个相邻像素进行比较,并分配位值:

OpenCV实战(30)——OpenCV与机器学习的碰撞_第3张图片
最后将得到一张 LBP 图像,可以将其显示为灰度图像:

OpenCV实战(30)——OpenCV与机器学习的碰撞_第4张图片

cv::face::LBPHFaceRecognizer 类中,create 方法的前两个参数通过大小(即以像素为单位的半径)和维度(即沿圆的像素数,可能应用插值)指定要考虑的邻域。生成 LBP 图像后,将图像划分为网格。网格的大小通过 create 方法的第三个参数指定。
对于结果网格中的每个块,构建 LBP 值的直方图。通过将所有这些直方图的 bin 计数连接成一长向量,获得了全局图像表示。使用 8×8 网格,计算出的 256bin 直方图集,形成一个 16384 维向量。
cv::face::LBPHFaceRecognizer 类的 train 方法为提供的每个参考图像生成一个长向量。然后,每个人脸图像都可以看作是高维空间中的一个点。当使用 predict 方法将新图像传递给识别器时,会找到与该图像最近的参考点。因此,与该点相关联的标签是预测标签,置信度值是计算出的距离。通常还会存在另一种情况;如果输入点的最近邻居离它太远,那么这可能意味着这个点实际上不属于任何参考类。我们可以通过 cv::face::LBPHFaceRecognizer 类的 create 方法的第四个参数指定究竟多远的距离才会被视为异常值。
通过将不同的类绘制在表示空间生成不同的点云时,可以观察到此方法的有效性。该方法的另一个优势在于隐式地处理多个类,因为它只是从最近的邻居中得到预测的类别。其缺点在于较高的计算成本,在可能由海量样本点组成的庞大空间中找到最近邻居可能大量时间,且存储所有这些样本点的空间成本也很高。

4. 完整代码

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

#include 
#include 
#include 
#include 
#include 

// 计算灰度图像点局部二值特征
void lbp(const cv::Mat &image, cv::Mat &result) {
    assert(image.channels() == 1);      // 输入图像必须为灰度图像
    result.create(image.size(), CV_8U); // 内存分配
    for (int j = 1; j<image.rows - 1; j++) {                    // 循环所有行 (除了第一行和最后一行)
        const uchar* previous = image.ptr<const uchar>(j - 1);  // 上一行
        const uchar* current = image.ptr<const uchar>(j);       // 当前行
        const uchar* next = image.ptr<const uchar>(j + 1);      // 下一行
        uchar* output = result.ptr<uchar>(j);                   // 输出行
        for (int i = 1; i<image.cols - 1; i++) {
            // 局部二值特征
            *output = previous[i - 1] > current[i] ? 1 : 0;
            *output |= previous[i] > current[i] ? 2 : 0;
            *output |= previous[i + 1] > current[i] ? 4 : 0;
            *output |= current[i - 1] > current[i] ? 8 : 0;
            *output |= current[i + 1] > current[i] ? 16 : 0;
            *output |= next[i - 1] > current[i] ? 32 : 0;
            *output |= next[i] > current[i] ? 64 : 0;
            *output |= next[i + 1] > current[i] ? 128 : 0;
            output++;   // 下一像素
        }
    }
    // 将未处理像素置为零
    result.row(0).setTo(cv::Scalar(0));
    result.row(result.rows - 1).setTo(cv::Scalar(0));
    result.col(0).setTo(cv::Scalar(0));
    result.col(result.cols - 1).setTo(cv::Scalar(0));
}

int main(){
    cv::Mat image = imread("test_img.png", cv::IMREAD_GRAYSCALE);
    cv::imshow("Original image", image);
    cv::Mat lbpImage;
    lbp(image, lbpImage);
    cv::imshow("LBP image", lbpImage);
    cv::Ptr<cv::face::FaceRecognizer> recognizer =
        cv::face::LBPHFaceRecognizer::create(1, // LBP 模式半径
                                        8,      // 要考虑的邻居像素数
                                        8, 8,   // 单元格尺寸
                                        200.);  // 到最近邻居的最小距离
    // 参考图像矢量及其标签
    std::vector<cv::Mat> referenceImages;
    std::vector<int> labels;
    // 打开参考图像
    referenceImages.push_back(cv::imread("p0_1.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(0); // person 0
    referenceImages.push_back(cv::imread("p0_2.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(0); // person 0
    referenceImages.push_back(cv::imread("p1_1.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(1); // person 1
    referenceImages.push_back(cv::imread("p1_2.png", cv::IMREAD_GRAYSCALE));
    labels.push_back(1); // person 1
    // 4 个正样本
    cv::Mat faceImages(2 * referenceImages[0].rows, 2 * referenceImages[0].cols, CV_8U);
    for (int i = 0; i < 2; i++)
        for (int j = 0; j < 2; j++) { 
            referenceImages[i * 2 + j].copyTo(faceImages(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::resize(faceImages, faceImages, cv::Size(), 0.5, 0.5);
    cv::imshow("Reference faces", faceImages);
    // 通过计算 LBPH 来训练分类器
    recognizer->train(referenceImages, labels);
    int predictedLabel = -1;
    double confidence = 0.0;
    // 提取人脸图像
    cv::Mat inputImage;
    cv::resize(image(cv::Rect(300, 75, 150, 150)), inputImage, cv::Size(256, 256));
    cv::imshow("Input image", inputImage);
    // 预测图像标签
    recognizer->predict(inputImage,     // 人脸图像
                        predictedLabel, // 图像的预测标签
                        confidence);    // 预测的置信度
    std::cout << "Image label= " << predictedLabel << " (" << confidence << ")" << std::endl;
    cv::waitKey();
}

小结

机器学习是人工智能的子集,它为计算机以及其它具有计算能力的系统提供自动预测或决策的能力,诸如虚拟助理、车牌识别系统、智能推荐系统等机器学习应用程序给我们的日常生活带来了便捷的体验。本节中,我们介绍了如何在 OpenCV 计算机视觉应用程序中机器学习算法,并以人脸识别为例体验了人工智能的强大之处。

系列链接

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,机器学习,人工智能)