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

2023-08-31 07:53:07

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 的人物图片:

人物图片

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

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

输入图像如下图所示:

输入图像

识别器不仅会返回预测的标签,还会返回相应的置信度分数。在 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));
}

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

原始图像
最后将得到一张 LBP 图像,可以将其显示为灰度图像:

LBP 图像

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 <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/face.hpp>

// 计算灰度图像点局部二值特征
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)——视频对象追踪

更多推荐

<git>如何快速上手并高效协同

git是什么?Git是一种分布式版本控制系统,用于跟踪计算机文件的变化和协调多个人之间的工作。它最初由LinusTorvalds于2005年创建,旨在管理Linux内核的开发。Git可以在本地计算机上存储完整的版本历史记录,并允许用户在不同的分支上进行开发和合并。它还提供了许多工具和命令,用于管理代码库、协作开发、解决

ClickHouse与Elasticsearch比较总结

目录背景分布式架构存储架构写入链路设计Elasticsearch再谈Schemaless查询架构计算引擎数据扫描再谈高并发性能测试日志分析场景access_log(数据量197921836)trace_log(数据量569816761)官方Ontime测试集用户画像场景(数据量262933269)二级索引点查场景(数据

分享从零开始学习网络设备配置--任务3.4 利用单臂路由实现部门间网络互访

任务描述某公司的管理员对部门划分了VLAN后,发现两个部门之间无法通信,但有时两个部门的员工需要进行通信,管理员现要通过简单的方法来实现此功能。划分VLAN之后,VLAN之间是不能通信的,使用路由器的单臂路由功能可以解决这个问题。任务要求(1)利用单臂路由实现部门间网络互访,网络拓扑图如图。(2)在交换机SWA上划分V

Canal实现Mysql数据同步至Redis、Elasticsearch

文章目录1.Canal简介1.1MySQL主备复制原理1.2canal工作原理2.开启MySQLBinlog3.安装Canal3.1下载Canal3.2修改配置文件3.3启动和关闭4.SpringCloud集成Canal4.1Canal数据结构![在这里插入图片描述](https://img-blog.csdnimg.

CGI与FastCGI的区别在哪里,FastCGI的应用场景讲解

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。🎉欢迎👍点赞✍评论⭐收藏文章目录1.CGI和FastCGI1.1CGI1.2FastCGI1.3对比2.Fas

MySQL的高级SQL语句

目录一、高级SQL语句1、select查询表中一个或多个字段的数据2、distinct不显示重复的数据记录3、where有条件查询4、and与or且与或5、in显示在某个范围值内的字段的信息6、between显示两个值范围内的数据记录7、orderby对字段进行排序8、groupby对字段进行分组汇总9、having用

【操作系统】聊聊磁盘IO是如何工作的

磁盘机械磁盘主要是由盘片和读写磁头组成。数据存储在盘片的的环状磁道上,读写数据前需要移动磁头,先找到对应的磁道,然后才可以访问数据。如果数据都在同一磁道上,不需要在进行切换磁道,这就是连续IO,可以获得更好的性能。而随机IO性能就比较差。固态磁盘固态磁盘不需要寻找磁道,所以随机IO和连续IO性能都不错。连续IO的性能其

【Linux】自制shell

本期我们利用之前学过的知识,写一个shell命令行程序目录一、初始代码二、使用户输入的ls指令带有颜色分类三、解决cd指令后用户所在路径不变化问题3.1chdir函数四、关于环境变量的问题一、初始代码#include<stdio.h>#include<unistd.h>#include<stdlib.h>#includ

模块化开发_php中使用redis

redis介绍和安装redis数据库,支持数据持久化,常用与分布式锁,支持事务,持久化,非关心型数据库区别:关系型数据库:硬盘,安全,结构简单,易于理解,浪费空间非关系型数据库:内存,断电丢失数据,读写速度快,内存的速度是硬盘的100倍redis:用于缓存压力,提升网站访问速度三种类型:持久化(将数据保存到硬盘中,再开

02. Springboot集成Flyway

目录1、前言2、什么是Flyway?3、为什么要使用Flyway?4、简单示例4.1、创建SpringBoot工程4.2、添加Flyway依赖4.3、Springboot添加Flyway配置4.4、创建执行SQL脚本4.5、启动测试4.6、Flyway版本管理5、SQL脚本文件命名规则6、使用注意事项1、前言在现代应用

kafka介绍

1.kafka概述消息中间件对比特性ActiveMQRabbitMQRocketMQKafka开发语言javaerlangjavascala单机吞吐量万级万级10万级100万级时效性msusmsms级以内可用性高(主从)高(主从)非常高(分布式)非常高(分布式)功能特性成熟的产品、较全的文档、各种协议支持好并发能力强、

热文推荐