OpenCV实战(32)——使用SVM和定向梯度直方图执行目标检测

2023-09-14 07:57:02

0. 前言

本节中,我们将介绍机器学习方法支持向量机 (Support Vector Machine, SVM),它可以根据训练数据得到准确的二分类分类器,它已被广泛用于解决许多计算机视觉问题。该分类器可以通过使用数学公式表达,该公式可以用于在高维空间中查看数据集的几何结构。此外,我们还将介绍一种新的图像表示,该表示通常与 SVM 结合使用以获取鲁棒性目标检测器。

1. HOG 图像特征

物体图像主要以其形状和内容为特征,通常可以由定向梯度直方图 (Histogram of Oriented Gradients, HOG) 表示,这种表示方法基于图像梯度构建的直方图。因为我们对物体形状和纹理感兴趣,所以需要分析梯度方向的分布。此外,为了考虑这些梯度的空间分布,在将图像划分为多个区域计算多个直方图。
因此,构建 HOG 表示的第一步是计算图像的梯度。然后将图像细分为小单元格(例如,8×8 像素),并为这些单元格构建梯度方向直方图。因此,必须将可能的方向范围划分为多个区间。大多数情况下,只考虑梯度方向而不考虑它们的符号(称为无符号梯度),在这种情况下,可能的方向范围是 0180 度,此时,一个 9-bin 直方图会将可能的方向划分为 920 度的区间。单元格中的每个梯度向量都累积在对应于该梯度大小的 bin
然后将单元格分组为块,由一定数量的单元格组成一个块,这些块可以相互重叠(即它们可以共享单元格)。例如,块由 2×2 个单元格组成,如果块步长为 1 个单元格,每个单元格(除了最后一个单元格)将被 2 个块共享。相反,如果块步长为 2 个单元格,则块不会发生重叠现象。
一个块包含一定数量的单元格直方图(例如,块由 2×2 个单元格组成时,包含 4 个单元格直方图)。将直方图连接在一起可以形成一个长向量(例如,当单元格数为 4、直方图 bin 数为 9 时,将生成一个长度为 36 的向量)。为了这种图像表示具有鲁棒性,对该向量进行归一化(例如,每个元素除以向量的大小)。最后,将与图像的所有块相关联的向量(按行顺序)连接在一起得到一个更长的向量(例如,在 64×64 图像中,当块中单元格大小为 8x8,步长为 1 时,可以得到 7x7=49 个块,因此最终向量为 49×36=1764 维),这个长向量就是图像的 HOG 表示。
图像的 HOG 表示是一个高维向量,可以使用该向量表征图像,用于执行图像分类任务,为了实现这一目标,我们需要使用可以处理高维向量的机器学习方法。

2. 交通标志分类

在本节中,我们构建交通标志标志分类器,用于说明图像的 HOG 表示,并利用这种表示方法进行图像分类。

2.1 SVM 模型

(1) 首先,获取用于训练的样本,使用的正样本如下所示:

正样本

(2) 负样本集如下所示:

负样本

(3) 接下来,使用 SVM 区分这两个类别,在 OpenCV 中可以借助 cv::svm 类实现。为了构建鲁棒性分类器,使用 HOG 来表示图像实例。更准确地说,我们使用由 2×2 单元组成的 8×8 块,块步长为 1 单元:

    cv::HOGDescriptor hog(cv::Size((image.cols / 16) * 16, (image.rows / 16) * 16), // 窗口大小
                    cv::Size(16, 16),    // 块尺寸
                    cv::Size(16, 16),    // 块步长
                    cv::Size(4, 4),      // 单元格尺寸
                    9);                  // bins 数量

(4) 使用 9-bin 直方图和尺寸为 64×64 样本,将得到大小为 8100HOG 向量(由 225 个块组成)。为每个样本计算 HOG 描述符,然后将它们组合为一个矩阵,矩阵中的每一行都表示一个 HOG

    // 计算第一个描述符
    std::vector<float> desc;
    hogDesc.compute(positives[0], desc);
    std::cout << "Positive sample size: " << positives[0].rows << "x" << positives[0].cols << std::endl;
    std::cout << "HOG descriptor size: " << desc.size() << std::endl;
    // 样本描述符矩阵 
    int featureSize = desc.size();
    int numberOfSamples = positives.size() + negatives.size();
    // 创建包含样本HOG的矩阵 
    cv::Mat samples(numberOfSamples, featureSize, CV_32FC1);
    // 用第一个描述符填充第一行
    for (int i = 0; i < featureSize; i++)
        samples.ptr<float>(0)[i] = desc[i];
    // 正样本的计算描述符
    for (int j = 1; j < positives.size(); j++) {
        hogDesc.compute(positives[j], desc);
        // 用当前描述符填充下一行
        for (int i = 0; i < featureSize; i++)
            samples.ptr<float>(j)[i] = desc[i];
    }
    // 计算负样本的描述符
    for (int j = 0; j < negatives.size(); j++) {
        hogDesc.compute(negatives[j], desc);
        // 用当前描述符填充下一行
        for (int i = 0; i < featureSize; i++)
            samples.ptr<float>(j + positives.size())[i] = desc[i];
    }

(5) 在以上代码中,我们创建了描述符矩阵,接下来,我们继续创建第二个矩阵以表示与每个样本相关联的标签,为正样本分配标签 1,而负样本分配标签 -1

    // 创建标签
    cv::Mat labels(numberOfSamples, 1, CV_32SC1);
    // 正样本标签
    labels.rowRange(0, positives.size()) = 1.0;   
    // 负样本标签
    labels.rowRange(positives.size(), numberOfSamples) = -1.0; 

(6) 构建用于训练的 SVM 分类器,设定 SVM 的类型和要使用的核:

    // 创建 SVM 分类器
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC);
    svm->setKernel(cv::ml::SVM::LINEAR);

(7) 将标记样本输入到分类器中,并调用 train 方法训练模型:

    // 准备训练数据
    cv::Ptr<cv::ml::TrainData> trainingData =
        cv::ml::TrainData::create(samples, cv::ml::SampleTypes::ROW_SAMPLE, labels);
    // 训练 SVM
    svm->train(trainingData);

(8) 训练完成后,可以将分类器用于预测新图像所属的类别:

    cv::Mat queries(4, featureSize, CV_32FC1);
    // 用查询描述符填充行
    hogDesc.compute(cv::imread("11.png", cv::IMREAD_GRAYSCALE), desc);
    for (int i = 0; i < featureSize; i++)
        queries.ptr<float>(0)[i] = desc[i];
    hogDesc.compute(cv::imread("12.png", cv::IMREAD_GRAYSCALE), desc);
    for (int i = 0; i < featureSize; i++)
        queries.ptr<float>(1)[i] = desc[i];
    hogDesc.compute(cv::imread("n12.jpg", cv::IMREAD_GRAYSCALE), desc);
    for (int i = 0; i < featureSize; i++)
        queries.ptr<float>(2)[i] = desc[i];
    hogDesc.compute(cv::imread("n13.jpg", cv::IMREAD_GRAYSCALE), desc);
    for (int i = 0; i < featureSize; i++)
        queries.ptr<float>(3)[i] = desc[i];
    cv::Mat predictions;
    // 测试SVM分类器
    svm->predict(queries, predictions);

    for (int i = 0; i < 4; i++)
        std::cout << "query: " << i << ": " << ((predictions.at<float>(i) < 0.0)? "Negative" : "Positive") << std::endl;

2.2 SVM 原理

在交通标志识别中,每个图像实例都由 8100HOG 空间中的一个点表示。显然我们不可能可视化如此高维的空间,但 SVM 的思想是在高维空间中确定边界,利用边界可以区分属于不同类别的样本。更具体地说,这个边界实际上只是一个简单的超平面,可以在 2D 空间中解释这种思想,2D 空间中每个样本都可以表示为一个 2D 点。在这种情况下,超平面是一条简单的线:

SVM 2D 超平面

这是十分简单的例子,但从概念上讲,在 2 维空间或 8100 维空间中 SVM 的工作方式都是一样的。上图演示了如何使用一条简单的线将属于两个不同类别的点分开,可以看出,空间中还存在许多不同的线可以将不同类别分开。因此,我们需要确定究竟选择哪条线?在这个问题之前,我们必须考虑到,用来构建分类器的样本只是所有可能样本的极小的一部分,而我们希望分类器不仅能够正确分离提供的样本集,而且还能够对没有见过的其他样本做出最佳决策,这通常称为分类器的泛化能力。理想情况下,分类超平面应该无偏的位于两个类之间。更正式地说,SVM 应当将超平面设置在使边界周围的边距最大化的位置,边距定义为分离超平面和正样本集中最近点之间的最小距离加上超平面和最近的负样本之间的距离,最近的点(定义边界点)称为支持向量 (support vectors)。SVM 定义了一个优化函数,用于识别这些支持向量。但是并不总可以仅使用直线分割不同类别,如果样本点的分布如下:

样本点分分布

在这种情况下,简单的直线超平面无法实现合适的类别分离。 SVM 通过引入人工变量来解决这个问题,这些变量通过一些非线性变换将问题引入更高维空间。例如,在以上示例中,我们可以添加样本点到原点的距离作为附加变量,即计算每个点的 r = ( x 2 + y 2 ) r=\sqrt{(x^2+y^2)} r=(x2+y2) ,可以得到一个三维空间,为简单起见,我们在 ( r , x ) (r, x) (r,x) 平面上绘制点:

在极坐标上绘制样本点

此时,样本点集可以被一个简单的超平面分隔开,我们需要在这个新空间中找到支持向量。事实上,在 SVM 公式中,不必将所有点都引入新空间,只需要定义一种方法来测量该更高维空间中的点到超平面距离。因此,SVM 定义了核函数,用于在更高维空间中测量此距离,而无需显式计算该空间中的点坐标。这只是从数学上解释了为什么可以在高维空间中有效计算产生最大边距的支持向量。这也解释了为什么在使用 SVM 时,需要指定要使用的核。正是通过应用这些核,可以实现非线性分隔,以便在核空间中更好的分隔不同类别。
然而,由于使用 SVM,我们经常使用非常高维的特征(例如,本节使用的 HOG8100 维),因此样本可能可以使用简单的超平面进行分离,这就是我们不使用非线性核,而使用线性核 (cv::ml::SVM::LINEAR) 的原因,生成的分类器在计算上更简单,但是对于更具挑战性的分类问题,核仍然是一个非常有效的工具。OpenCV 提供了许多标准核(例如,径向基函数和 sigmoid 函数),用于将样本引入更高维的非线性空间,使不同类别可以通过超平面分离。SVM 有许多变体,最常见的变体是 C-SVM,它会对每个离群样本施加一个惩罚。
因为 SVM 具有严谨的数学基础,可以很好地处理非常高维的特征。事实上,当特征空间的维数大于样本数时,运行效果最好。与需要将所有样本点保存在内存中的最近邻居等方法相比,SVM 也是一种内存高效的算法,因为它们只需要存储支持向量。
定向梯度直方图和 SVM 组成了性能优异的分类器,这是由于 HOG 可以被视为一种强大的高维描述符,它捕获了对象样本的基本特征,HOG-SVM 分类器已成功应用于许多实际场景中,例如人物检测。

3. HOG 可视化

HOG 由组合在重叠块中的单元格构建而成,因此,很难可视化该描述符。然而,通常可以通过显示与每个单元格关联的直方图来表示。在这种情况下,与在常规条形图中对齐方向bin不同,可以使用星形绘制直方图,其中每条线都具有与其代表的 bin 相关联的方向,并且线的长度是与 bin 计数成正比,可以在图像上显示这些 HOG 表示,如下所示:

HOG 表示

每个单元格 HOG 表示可以由一个简单的函数生成,该函数接受一个指向直方图的迭代器。然后为每个 bin 绘制正确方向和长度的线:

// 在每个单元格上绘制一个HOG 
void drawHOG(std::vector<float>::const_iterator hog,    // HOG迭代器 
            int numberOfBins,                           // HOG中的bin数
            cv::Mat &image,                             // 单元格图像
            float scale=1.0) {                          // 长度乘数
    const float PI = 3.1415927;
    float binStep = PI / numberOfBins;
    float maxLength = image.rows;
    float cx = image.cols / 2.;
    float cy = image.rows / 2.;

    // 迭代每个bin
    for (int bin = 0; bin < numberOfBins; bin++) {
        // bin 方向
        float angle = bin*binStep;
        float dirX = cos(angle);
        float dirY = sin(angle);
        // 线长度与bin尺寸的比例
        float length = 0.5*maxLength* *(hog+bin);
        // 绘制线
        float x1 = cx - dirX * length * scale;
        float y1 = cy - dirY * length * scale;
        float x2 = cx + dirX * length * scale;
        float y2 = cy + dirY * length * scale;
        cv::line(image, cv::Point(x1, y1), cv::Point(x2, y2), CV_RGB(255, 255, 255), 1);
    }
}

然后,HOG 可视化函数为每个单元格调用以上函数:

// 在图像上绘制HOG
void drawHOGDescriptors(const cv::Mat &image,   // 输入图像
                        cv::Mat &hogImage,      // 生成的HOG图像
                        cv::Size cellSize,      // 每个单元格的大小
                        int nBins) {            // bins 数量

    // 块大小是图像大小
    cv::HOGDescriptor hog(cv::Size((image.cols / cellSize.width) * cellSize.width, 
                            (image.rows / cellSize.height) * cellSize.height),
        cv::Size((image.cols / cellSize.width) * cellSize.width,
                    (image.rows / cellSize.height) * cellSize.height),	
        cellSize,       // 块步长
        cellSize,       // 单元格大小
        nBins);         // bins 数量
    // 计算 HOG
    std::vector<float> descriptors;
    hog.compute(image, descriptors);
    float scale= 2.0 / *std::max_element(descriptors.begin(), descriptors.end());
    hogImage.create(image.rows, image.cols, CV_8U);
    std::vector<float>::const_iterator itDesc= descriptors.begin();
    for (int i = 0; i < image.rows / cellSize.height; i++) {
        for (int j = 0; j < image.cols / cellSize.width; j++) {
            // 绘制每个单元格
            hogImage(cv::Rect(j*cellSize.width, i*cellSize.height, cellSize.width, cellSize.height));
            cv::Mat roi= hogImage(cv::Rect(j*cellSize.width, i*cellSize.height,
                                    cellSize.width, cellSize.height));
            drawHOG(itDesc, nBins, roi, scale);
            itDesc += nBins;
        }
    }
}

此函数计算具有指定单元大小组成的 HOG 描述符,因此,这种表示忽略了在每个块上进行的归一化影响。

4. 人物检测

OpenCV 提供了一个基于 HOGSVM 的预训练人物检测器,SVM 分类器可用于通过在多个尺度上扫描图像上的窗口来检测完整图像中的实例,然后,构建分类器对图像执行检测:

    // 创建描述符
    std::vector<cv::Rect> peoples;
    cv::HOGDescriptor peopleHog;
    peopleHog.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector());
    // 检测人物
    peopleHog.detectMultiScale(myImage, // 输入图像
                peoples,                // 输出边界框列表
                0,                      // 阈值
                cv::Size(5, 5),         // 窗口步长
                cv::Size(40, 40),       // 填充图像
                1.1,                    // 缩放因子
                2);                     // 分组阈值,0表示不分组

以上代码中,窗口步幅用于定义单元格大小为 128×64 的模板如何在图像上移动(示例中,水平和垂直方向每次移动 4 个像素)。由于评估的窗口较少,较长的步幅可以使检测速度更快,但可能会错过一些人物实例。图像填充参数用于在图像的边界上添加像素,以便可以检测到图像边缘中的人物。SVM 分类器的标准阈值为 0 (正实例的标签值为 1,负实例的标签值为 -1)。如果想确定检测到的对象的确是一个人,那么可以提高这个阈值(这意味着我们需要高精度,但代价是会遗漏图像中的一些人),反之,如果想确定检测到所有人(即需要高召回率),那么可以降低阈值,但在这种情况下会出现更多的错误检测。获得的检测结果的示例如下所示:

检测结果

需要注意的是,当分类器应用于完整图像时,在连续位置应用的多个窗口通常会导致一个正样本进行多次检测。当两个或多个边界框在同一位置重叠时,最好的办法是只保留其中一个。使用函数 cv::groupRectangles 能够将相似位置的相似大小的矩形组合在一起(该函数由 detectMultiScale 自动调用)。事实上,在特定位置获得一组检测甚至可以被视为一个指标,以确认在该位置确实有一个正实例,因此 cv::groupRectangles 函数允许指定检测集群的最小大小,以接受为正样本,并丢弃孤立检测,这是 detectMultiScale 方法的最后一个参数。将该参数设置为 0 可以保留所有检测,得到的结果如下图所示:

所有检测结果

5. 完整代码

主函数文件 (trainSVM.cpp) 完整代码可以在 gitcode 中获取。

小结

定向梯度直方图 (Histogram of Oriented Gradients, HOG) 是基于图像梯度构建的直方图,是一种表示图像的有效方式。而支持向量机 (Support Vector Machine, SVM) 可以根据训练数据得到准确的二分类分类器,被广泛用于解决许多计算机视觉问题。HOGSVM 组成了性能优异的分类器,本节中我们利用 SVM-HOG 构建目标检测模型。

系列链接

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与机器学习的碰撞
OpenCV实战(31)——基于级联Haar特征的目标检测

更多推荐

数据仓库性能测试方法论与工具集

目录文章目录目录数据仓库v.s.传统数据库数据仓库性能测试案例性能指标测试方案测试场景测试数据集测试用例性能指标测试脚本工具基准环境准备硬件环境软件环境测试操作步骤Cloudwave执行步骤导入数据集TestCase1.执行13条标准SQL测试语句TestCase2.执行多表联合join拓展SQL1测试语句TestCa

全链路压测演进之迭代式压测

文章导读1.背景原因2.压测流程改进分析3.迭代式压测流程压测服务的稳定性压测环境的稳定性压测时效压测人员贴合业务4.全流程压测模式演进5.压测模式对比6.迭代式压测反馈效果7.总结1.背景原因!!做系统服务压测都是比较耗时耗人力的,特别是在生产环境上做压测,压测的时间都是在晚上23点后,甚至在凌晨1-4点,每次投入的

第一章:最新版零基础学习 PYTHON 教程(第五节 - Python 中的关键字和示例)

Python中的关键字是保留字,不能用作变量名、函数名或任何其他标识符。Python中的关键字列表关键词描述关键词描述关键词描述and它是一个逻辑运算符False表示将导致不为真的表达式。nonlocal它是一个非局部变量as它用于创建别名finally它的使用有例外not它是一个逻辑运算符assert它用于调试for

C++项目实战——基于多设计模式下的同步&异步日志系统-⑧-日志落地类设计

文章目录专栏导读抽象基类StdoutSink类设计FileSink类设计RollBySizeSink类设计日志落地工厂类设计日志落地类整理日志落地拓展测试RollByTimeSink类设计测试代码测试完整代码专栏导读🌸作者简介:花想云,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内

算法通关村-----图的基本算法

图的实现方式邻接矩阵定义邻接矩阵是一个二维数组,其中的元素表示图中节点之间的关系。通常,如果节点i与节点j之间有边(无向图)或者从节点i到节点j有边(有向图),则矩阵中的元素值为1或者表示边的权重值。如果没有边相连,则元素值为0或者一个特定的标记(通常表示无穷大)。优点适用于稠密图,即节点之间有很多边的情况,因为它不会

AI实战营第二期 第七节 《语义分割与MMSegmentation》——笔记8

文章目录摘要主要特性案例什么是语义分割应用:无人驾驶汽车应用:人像分割应用:智能遥感应用:医疗影像分析三种分割的区别语义分割的基本思路按颜色分割逐像素份分类全卷积网络FullyConvolutionalNetwork2015存在问题基于多层级特征的上采样UNet20115PSPNet2016DeepLab系列空洞卷积解

crAPI靶场学习记录

靶场搭建[靶场下载地址](我fork了一份)docker安装,笔者是用的wsl+docker.[lab0:**初始账户**]1.注册一个账户,邮箱为[API@qq.com],密码为Admin@1231.登陆后访问对应IP的8025端口,接收邮件获取车辆信息。[lab1:**访问其它用户车辆的详细信息**]登录后首先找到

PyTorch实战-实现神经网络图像分类基础Tensor最全操作详解(二)

前言PyTorch可以说是三大主流框架中最适合初学者学习的了,相较于其他主流框架,PyTorch的简单易用性使其成为初学者们的首选。这样我想要强调的一点是,框架可以类比为编程语言,仅为我们实现项目效果的工具,也就是我们造车使用的轮子,我们重点需要的是理解如何使用Torch去实现功能而不要过度在意轮子是要怎么做出来的,那

背包问题学习笔记-01背包

背景背包问题是动态规划问题中的一个大类,学习背包问题对于掌握动态规划十分重要。背包问题也很容易成为程序员算法面试中的一个槛,但其实背包问题已经被研究,讲解的比较成熟了,在这些丰富的讲解资料的基础之上,大家理解背包问题的难度也被大大减弱了。本篇笔记主要参考了AcWing上的题目列表以及讲解视频,原因有二:1)上面截图中相

神经网络:基本概念、模型与技术

神经网络:基本概念、模型与技术引言在近年来,人工智能领域取得了显著的进步,其中最引人注目的成就之一就是神经网络。神经网络是一种模拟人脑工作机制的算法,以其出色的学习和识别能力,对各个领域的实际问题产生了深远影响。在本篇博客中,我们将深入探讨神经网络的基本概念、模型和技术。神经网络的基本概念神经网络是一种模拟人脑神经元网

决策树(中):数据挖掘十大算法之一

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️🐴作者:秋无之地🐴简介:CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作,主要擅长领域有:爬虫、后端、大数据开发、数据分析等。🐴欢迎小伙伴们点赞👍🏻、收藏⭐️、留言💬、关注🤝,关注必回关上一篇文章已经跟大家介绍过

热文推荐