Machine-Learning-Mastery-OpenCV-教程-三-

91 阅读50分钟

Machine Learning Mastery OpenCV 教程(三)

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

OpenCV 中的图像特征提取:关键点和描述向量

原文:machinelearningmastery.com/opencv_sift_surf_orb_keypoints/

上一篇文章 中,你学习了一些 OpenCV 中的基本特征提取算法。特征以分类像素的形式提取。这确实从图像中抽象出特征,因为你不需要考虑每个像素的不同颜色通道,而是考虑一个单一的值。在这篇文章中,你将学习一些其他特征提取算法,它们可以更简洁地告诉你关于图像的信息。

完成本教程后,你将了解:

  • 图像中的关键点是什么

  • 在 OpenCV 中用于提取关键点的常见算法有哪些

通过我的书籍 《OpenCV 中的机器学习》 启动你的项目。它提供了 自学教程有效的代码

让我们开始吧。

OpenCV 中的图像特征提取:关键点和描述向量

图片由 Silas Köhler 提供,部分权利保留。

概述

本文分为两个部分;它们是:

  • 使用 SIFT 和 SURF 进行 OpenCV 中的关键点检测

  • 使用 ORB 进行 OpenCV 中的关键点检测

先决条件

对于本教程,我们假设你已经熟悉:

使用 SIFT 和 SURF 进行 OpenCV 中的关键点检测

尺度不变特征变换(SIFT)和加速稳健特征(SURF)是用于检测和描述图像中局部特征的强大算法。它们被称为尺度不变和鲁棒,是因为与 Harris 角点检测相比,其结果在图像发生某些变化后仍然是可以预期的。

SIFT 算法对图像应用高斯模糊,并计算多个尺度下的差异。直观上,如果整个图像是单一的平面颜色,这种差异将为零。因此,这个算法被称为关键点检测,它识别图像中像素值变化最显著的地方,例如角点。

SIFT 算法为每个关键点推导出某些“方向”值,并输出表示方向值直方图的向量。

运行 SIFT 算法的速度比较慢,因此有一个加速版本,即 SURF。详细描述 SIFT 和 SURF 算法会比较冗长,但幸运的是,你不需要了解太多就可以在 OpenCV 中使用它。

让我们通过以下图像看一个例子:

与之前的帖子类似,SIFT 和 SURF 算法假设图像为灰度图像。这次,你需要首先创建一个检测器,并将其应用于图像:

import cv2

# Load the image and convery to grayscale
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Initialize SIFT and SURF detectors
sift = cv2.SIFT_create()
surf = cv2.xfeatures2d.SURF_create()

# Detect key points and compute descriptors
keypoints_sift, descriptors_sift = sift.detectAndCompute(img, None)
keypoints_surf, descriptors_surf = surf.detectAndCompute(img, None)

注意: 你可能会发现运行上述代码时在你的 OpenCV 安装中遇到困难。为了使其运行,你可能需要从头编译自己的 OpenCV 模块。这是因为 SIFT 和 SURF 已经申请了专利,所以 OpenCV 认为它们是“非自由”的。由于 SIFT 专利已过期(SURF 仍在有效期内),如果你下载一个更新版本的 OpenCV,你可能会发现 SIFT 运行正常。

SIFT 或 SURF 算法的输出是一个关键点列表和一个描述符的 numpy 数组。描述符数组是 Nx128,对于 N 个关键点,每个由长度为 128 的向量表示。每个关键点是一个具有多个属性的对象,例如方向角度。

默认情况下可以检测到许多关键点,因为这有助于关键点的最佳用途之一——寻找失真图像之间的关联。

为了减少输出中检测到的关键点数量,你可以在 SIFT 中设置更高的“对比度阈值”和更低的“边缘阈值”(默认值分别为 0.03 和 10),或者在 SURF 中增加“Hessian 阈值”(默认值为 100)。这些可以通过 sift.setContrastThreshold(0.03)sift.setEdgeThreshold(10)surf.setHessianThreshold(100) 进行调整。

要在图像上绘制关键点,你可以使用 cv2.drawKeypoints() 函数,并将所有关键点的列表应用于它。完整代码,使用仅 SIFT 算法并设置非常高的阈值以保留少量关键点,如下所示:

import cv2

# Load the image and convery to grayscale
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Initialize SIFT detector
sift = cv2.SIFT_create()
sift.setContrastThreshold(0.25)
sift.setEdgeThreshold(5)

# Detect key points and compute descriptors
keypoints, descriptors = sift.detectAndCompute(img, None)
for x in keypoints:
    print("({:.2f},{:.2f}) = size {:.2f} angle {:.2f}".format(x.pt[0], x.pt[1], x.size, x.angle))

img_kp = cv2.drawKeypoints(img, keypoints, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow("Keypoints", img_kp)
cv2.waitKey(0)
cv2.destroyAllWindows()

创建的图像如下:

SIFT 算法检测到的关键点(放大查看)

原始照片由 Gleren Meneghin 提供,部分权利保留。

cv2.drawKeypoints() 函数不会修改你的原始图像,而是返回一个新图像。在上面的图片中,你可以看到关键点被绘制为与其“大小”成比例的圆圈,并有一个表示方向的描边。门上的“17”号以及邮件槽上都有关键点,但实际上还有更多。从上面的 for 循环中,你可以看到一些关键点重叠,因为发现了多个方向角度。

在图像上显示关键点时,你使用了返回的关键点对象。然而,如果你想进一步处理关键点,例如运行聚类算法,你可能会发现存储在 descriptors 中的特征向量很有用。但请注意,你仍然需要关键点列表中的信息,例如坐标,以匹配特征向量。

使用 OpenCV 中的 ORB 进行关键点检测

由于 SIFT 和 SURF 算法已经申请了专利,因此有开发无需许可的免费替代品的动力。这是 OpenCV 开发者自己开发的产品。

ORB 代表定向 FAST 和旋转 BRIEF。它是两个其他算法 FAST 和 BRIEF 的组合,并进行了修改以匹配 SIFT 和 SURF 的性能。你无需了解算法细节就可以使用它,输出结果也是一个关键点对象的列表,如下所示:

import cv2

# Load the image and convery to grayscale
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Initialize ORB detector
orb = cv2.ORB_create(30)

# Detect key points and compute descriptors
keypoints, descriptors = orb.detectAndCompute(img, None)
for x in keypoints:
    print("({:.2f},{:.2f}) = size {:.2f} angle {:.2f}".format(
            x.pt[0], x.pt[1], x.size, x.angle))

img_kp = cv2.drawKeypoints(img, keypoints, None,
                           flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow("Keypoints", img_kp)
cv2.waitKey(0)
cv2.destroyAllWindows()

在上面的操作中,你设置了 ORB 以在创建探测器时生成前 30 个关键点。默认情况下,这个数字是 500。

探测器返回的仍然是关键点列表和描述符的 numpy 数组(每个关键点的特征向量)。然而,现在每个关键点的描述符长度为 32,而不是 128。

生成的关键点如下:

ORB 算法检测的关键点

原始照片由 Gleren Meneghin 提供,部分权利保留。

你可以看到,关键点大致生成在相同的位置。结果并不完全相同,因为存在重叠的关键点(或偏移了非常小的距离),ORB 算法容易达到 30 的最大数量。此外,不同算法之间的大小不可比较。

想要开始使用 OpenCV 进行机器学习吗?

现在立即获取我的免费电子邮件速成课程(附样例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

进一步阅读

本节提供了更多资源,如果你想深入了解这个话题。

书籍

网站

总结

在本教程中,你学习了如何应用 OpenCV 的关键点检测算法,SIFT、SURF 和 ORB。

具体来说,你学到了:

  • 图像中的关键点是什么

  • 如何使用 OpenCV 函数查找关键点及其相关描述向量。

如果你有任何问题,请在下方留言。

使用 OpenCV 进行图像分类的随机森林

原文:machinelearningmastery.com/random-forest-for-image-classification-using-opencv/

随机森林算法是集成机器学习算法家族的一部分,是袋装决策树的一种流行变体。它也在 OpenCV 库中实现了。

在本教程中,您将学习如何应用 OpenCV 的随机森林算法进行图像分类,从相对简单的纸币数据集开始,然后在 OpenCV 的数字数据集上测试算法。

完成本教程后,您将了解:

  • 随机森林算法的几个最重要的特性。

  • 如何在 OpenCV 中使用随机森林算法进行图像分类。

用我的书《OpenCV 中的机器学习》Machine Learning in OpenCV 启动您的项目。它提供了自学教程可运行的代码

让我们开始吧。 

使用 OpenCV 进行图像分类的随机森林

照片由 Jeremy Bishop 提供,保留部分权利。

教程概述

本教程分为两个部分;它们是:

  • 随机森林工作原理的提醒

  • 将随机森林算法应用于图像分类

    • 纸币案例研究

    • 数字案例研究

随机森林工作原理的提醒

关于随机森林算法的主题已经在 Jason Brownlee 的这些教程中得到很好的解释[12],但让我们首先回顾一些最重要的点:

  • 随机森林是一种集成机器学习算法,称为袋装。它是袋装决策树的一种流行变体。

*** 决策树是一个分支模型,由一系列决策节点组成,每个决策节点根据决策规则对数据进行划分。训练决策树涉及贪心地选择最佳分裂点(即最佳划分输入空间的点),通过最小化成本函数来完成。

*** 决策树通过贪心方法构建其决策边界,使其容易受到高方差的影响。这意味着训练数据集中的小变化可能导致非常不同的树结构,从而影响模型预测。如果决策树没有被修剪,它还会倾向于捕捉训练数据中的噪声和异常值。这种对训练数据的敏感性使得决策树容易过拟合。

*** 集成决策树 通过结合来自多个决策树的预测来解决这种敏感性,每棵树都在通过替换抽样创建的训练数据的自助样本上进行训练。这种方法的局限性在于相同的贪婪方法训练每棵树,并且某些样本在训练期间可能被多次挑选,这使得树很可能共享相似(或相同)的分割点(因此,结果是相关的树)。

*** 随机森林算法通过在训练数据的随机子集上训练每棵树来减轻这种相关性,这些子集是通过无替换地随机抽样数据集创建的。这样,贪婪算法只能考虑固定的子集来创建每棵树的分割点,这迫使树之间有所不同。

*** 在分类问题中,森林中的每棵树都会产生一个预测输出,最终的类别标签是大多数树产生的输出。在回归问题中,最终的输出是所有树产生的输出的平均值。

**## 将随机森林算法应用于图像分类

纸币案例研究

我们将首先使用 这个教程 中使用的纸币数据集。

纸币数据集是一个相对简单的数据集,涉及预测给定纸币的真实性。数据集包含 1,372 行,每行代表一个特征向量,包括从纸币照片中提取的四个不同测量值,以及其对应的类别标签(真实或虚假)。

每个特征向量中的值对应于以下内容:

  1. 小波变换图像的方差(连续型)

  2. 小波变换图像的偏度(连续型)

  3. 小波变换图像的峰度(连续型)

  4. 图像的熵(连续型)

  5. 类别标签(整数)

数据集可以从 UCI 机器学习库 下载。

想要开始使用 OpenCV 进行机器学习吗?

现在参加我的免费电子邮件速成课程(包括示例代码)。

点击注册并同时获取课程的免费 PDF 电子书版本。

如同 Jason 的教程中所示,我们将加载数据集,将其字符串数字转换为浮点数,并将其划分为训练集和测试集:

Python

# Function to load the dataset
def load_csv(filename):
    file = open(filename, "rt")
    lines = reader(file)
    dataset = list(lines)
    return dataset

# Function to convert a string column to float
def str_column_to_float(dataset, column):
    for row in dataset:
        row[column] = float32(row[column].strip())

# Load the dataset from text file
data = load_csv('Data/data_banknote_authentication.txt')

# Convert the dataset string numbers to float
for i in range(len(data[0])):
    str_column_to_float(data, i)

# Convert list to array
data = array(data)

# Separate the dataset samples from the ground truth
samples = data[:, :4]
target = data[:, -1, newaxis].astype(int32)

# Split the data into training and testing sets
x_train, x_test, y_train, y_test = ms.train_test_split(samples, target, test_size=0.2, random_state=10)

OpenCV 库在 ml 模块中实现了 RTrees_create 函数,这将允许我们创建一个空的决策树:

Python

# Create an empty decision tree
rtree = ml.RTrees_create()

森林中的所有树木将使用相同的参数值进行训练,尽管是在不同的训练数据子集上。默认参数值可以自定义,但让我们首先使用默认实现。我们将在下一节中回到自定义这些参数值:

Python

# Train the decision tree
rtree.train(x_train, ml.ROW_SAMPLE, y_train)

# Predict the target labels of the testing data
_, y_pred = rtree.predict(x_test)

# Compute and print the achieved accuracy
accuracy = (sum(y_pred.astype(int32) == y_test) / y_test.size) * 100
print('Accuracy:', accuracy[0], '%')

Python

Accuracy: 96.72727272727273 %

我们已经使用默认实现的随机森林算法在钞票数据集上获得了约**96.73%**的高准确率。

完整的代码列表如下:

from csv import reader
from numpy import array, float32, int32, newaxis
from cv2 import ml
from sklearn import model_selection as ms

# Function to load the dataset
def load_csv(filename):
    file = open(filename, "rt")
    lines = reader(file)
    dataset = list(lines)
    return dataset

# Function to convert a string column to float
def str_column_to_float(dataset, column):
    for row in dataset:
        row[column] = float32(row[column].strip())

# Load the dataset from text file
data = load_csv('Data/data_banknote_authentication.txt')

# Convert the dataset string numbers to float
for i in range(len(data[0])):
    str_column_to_float(data, i)

# Convert list to array
data = array(data)

# Separate the dataset samples from the ground truth
samples = data[:, :4]
target = data[:, -1, newaxis].astype(int32)

# Split the data into training and testing sets
x_train, x_test, y_train, y_test = ms.train_test_split(samples, target, test_size=0.2, random_state=10)

# Create an empty decision tree
rtree = ml.RTrees_create()

# Train the decision tree
rtree.train(x_train, ml.ROW_SAMPLE, y_train)

# Predict the target labels of the testing data
_, y_pred = rtree.predict(x_test)

# Compute and print the achieved accuracy
accuracy = (sum(y_pred.astype(int32) == y_test) / y_test.size) * 100
print('Accuracy:', accuracy[0], '%')

数字案例研究

考虑将随机森林应用于 OpenCV 的数字数据集中的图像。

数字数据集仍然相对简单。然而,我们将使用 HOG 方法从其图像中提取的特征向量将具有比钞票数据集中的特征向量更高的维度(81 个特征)。因此,我们可以认为数字数据集比钞票数据集更具挑战性。

我们将首先调查随机森林算法的默认实现如何应对高维数据。

Python

from digits_dataset import split_images, split_data
from feature_extraction import hog_descriptors
from numpy import array, float32
from cv2 import ml

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8)

# Convert the image data into HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)
digits_test_hog = hog_descriptors(digits_test_imgs)

# Create an empty decision tree
rtree_digits = ml.RTrees_create()

# Predict the target labels of the testing data
_, digits_test_pred = rtree_digits.predict(digits_test_hog)

# Compute and print the achieved accuracy
accuracy_digits = (sum(digits_test_pred.astype(int) == digits_test_labels) / digits_test_labels.size) * 100
print('Accuracy:', accuracy_digits[0], '%')

Python

Accuracy: 81.0 %

我们发现默认实现返回的准确度为 81%。

从钞票数据集上获得的准确度下降可能表明,模型的默认实现可能无法学习我们现在处理的高维数据的复杂性。

让我们调查一下,通过更改以下内容是否可以提高准确度:

  • 训练算法的终止标准,它考虑了森林中的树木数量,以及模型的估计性能,通过袋外误差(OOB)来衡量。当前的终止标准可以通过getTermCriteria方法找到,并通过setTermCriteria方法设置。使用后者时,可以通过TERM_CRITERIA_MAX_ITER参数设置树的数量,而期望的准确度可以通过TERM_CRITERIA_EPS参数指定。

  • 森林中每棵树可以达到的最大深度。当前深度可以通过getMaxDepth方法找到,并通过setMaxDepth方法设置。如果先满足上述终止条件,可能无法达到指定的树深度。

在调整上述参数时,请记住,增加树的数量可以提高模型捕捉训练数据中更复杂细节的能力;这也会线性增加预测时间,并使模型更容易过拟合。因此,谨慎调整参数。

如果我们在创建空决策树后添加以下几行代码,我们可以找到树深度以及终止标准的默认值:

Python

print('Default tree depth:', rtree_digits.getMaxDepth())
print('Default termination criteria:', rtree_digits.getTermCriteria())

Python

Default tree depth: 5
Default termination criteria: (3, 50, 0.1)

以这种方式,我们可以看到,默认情况下,森林中的每棵树的深度(或层级数)等于 5,而树的数量和期望的准确度分别设置为 50 和 0.1。getTermCriteria方法返回的第一个值指的是考虑的终止标准的type,其中值为 3 表示基于TERM_CRITERIA_MAX_ITERTERM_CRITERIA_EPS的终止。

现在让我们尝试更改上述值,以研究它们对预测准确率的影响。代码列表如下:

Python

from digits_dataset import split_images, split_data
from feature_extraction import hog_descriptors
from numpy import array, float32
from cv2 import ml, TERM_CRITERIA_MAX_ITER, TERM_CRITERIA_EPS

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8)

# Convert the image data into HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)
digits_test_hog = hog_descriptors(digits_test_imgs)

# Create an empty decision tree
rtree_digits = ml.RTrees_create()

# Read the default parameter values
print('Default tree depth:', rtree_digits.getMaxDepth())
print('Default termination criteria:', rtree_digits.getTermCriteria())

# Change the default parameter values
rtree_digits.setMaxDepth(15)
rtree_digits.setTermCriteria((TERM_CRITERIA_MAX_ITER + TERM_CRITERIA_EPS, 100, 0.01))

# Train the decision tree
rtree_digits.train(digits_train_hog.astype(float32), ml.ROW_SAMPLE, digits_train_labels)

# Predict the target labels of the testing data
_, digits_test_pred = rtree_digits.predict(digits_test_hog)

# Compute and print the achieved accuracy
accuracy_digits = (sum(digits_test_pred.astype(int) == digits_test_labels) / digits_test_labels.size) * 100
print('Accuracy:', accuracy_digits[0], ‘%')

Python

Accuracy: 94.1 %

我们可能会看到,新设置的参数值将预测准确率提高到了 94.1%。

这些参数值在这里是随意设置的,以说明这个例子。然而,始终建议采取更系统的方法来调整模型的参数,并调查每个参数对性能的影响。

进一步阅读

本节提供了更多关于此主题的资源,如果你想更深入了解的话。

书籍

网站

总结

在本教程中,你学会了如何应用 OpenCV 的随机森林算法进行图像分类,从一个相对简单的钞票数据集开始,然后在 OpenCV 的数字数据集上测试该算法。

具体来说,你学到了:

  • 随机森林算法的一些最重要特征。

  • 如何在 OpenCV 中使用随机森林算法进行图像分类。

你有任何问题吗?

在下面的评论中提问,我会尽力回答。************

在 OpenCV 中运行神经网络模型

原文:machinelearningmastery.com/running-a-neural-network-model-in-opencv/

许多机器学习模型已经被开发出来,每种模型都有其优缺点。没有神经网络模型,这个目录是不完整的。在 OpenCV 中,你可以使用通过其他框架开发的神经网络模型。在这篇文章中,你将学习在 OpenCV 中应用神经网络的工作流程。具体来说,你将学习:

  • OpenCV 在其神经网络模型中可以使用的内容

  • 如何为 OpenCV 准备神经网络模型

启动你的项目,请参考我的书籍 《OpenCV 中的机器学习》。它提供了自学教程有效代码

让我们开始吧!

在 OpenCV 中运行神经网络模型

图片由 Nastya Dulhiier 提供。版权所有。

神经网络模型概述

神经网络的另一个名称是多层感知器。它的灵感来自于人脑的结构和功能。想象一下一个由互联节点组成的网络,每个节点对通过它的数据执行简单的计算。这些节点,或称为“感知器”,相互通信,根据接收到的信息调整它们的连接。这些感知器组织在一个有向图中,计算从输入到输出具有确定的顺序。它们的组织通常用顺序来描述。学习过程使网络能够识别模式并对未见过的数据进行预测。

在计算机视觉中,神经网络处理图像识别、对象检测和图像分割等任务。通常,在模型内部,会执行三种高级操作:

  1. 特征提取:网络接收图像作为输入。第一层然后分析像素,寻找基本特征,如边缘、曲线和纹理。这些特征就像积木一样,给予网络对图像内容的初步理解。

  2. 特征学习:更深的层次在这些特征的基础上构建,结合和转化它们,以发现更高层次、更复杂的模式。这可能涉及到识别形状或对象。

  3. 输出生成:最后,网络的最后几层使用学习到的模式来进行预测。根据任务的不同,它可以对图像进行分类(例如,猫与狗)或识别其包含的对象。

这些操作是通过学习获得的,而不是手工制作的。神经网络的强大在于其灵活性和适应性。通过微调神经元之间的连接并提供大量标记数据,我们可以训练它们以卓越的准确性解决复杂的视觉问题。但由于其灵活性和适应性,神经网络通常在内存和计算复杂性方面不是最有效的模型。

训练神经网络

由于模型的性质,训练一个通用的神经网络并不简单。OpenCV 中没有训练功能。因此,你必须使用其他框架训练模型并在 OpenCV 中加载它。你希望在这种情况下使用 OpenCV,因为你已经在使用 OpenCV 进行其他图像处理任务,不想给项目引入另一个依赖,或者因为 OpenCV 是一个更轻量的库。

例如,考虑经典的 MNIST 手写数字识别问题。为了简化,我们使用 Keras 和 TensorFlow 来构建和训练模型。数据集可以从 TensorFlow 获得。

Python

import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.datasets import mnist

# Load MNIST data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print(X_train.shape)
print(y_train.shape)

# Check visually
fig, ax = plt.subplots(4, 5, sharex=True, sharey=True)
idx = np.random.randint(len(X_train), size=4*5).reshape(4,5)
for i in range(4):
    for j in range(5):
        ax[i][j].imshow(X_train[idx[i][j]], cmap="gray")
plt.show()

这两个打印语句给出的结果是:

(60000, 28, 28)
(60000,)

你可以看到数据集以 28×28 的灰度格式提供数字。训练集有 60,000 个样本。你可以使用 matplotlib 展示一些随机样本,看到类似如下的图像:

这个数据集标记为 0 到 9,表示图像上的数字。你可以使用许多模型来解决这个分类问题。著名的 LeNet5 模型就是其中之一。让我们使用 Keras 语法创建一个:

Python

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Flatten

# LeNet5 model
model = Sequential([
    Conv2D(6, (5,5), input_shape=(28,28,1), padding="same", activation="tanh"),
    AveragePooling2D((2,2), strides=2),
    Conv2D(16, (5,5), activation="tanh"),
    AveragePooling2D((2,2), strides=2),
    Conv2D(120, (5,5), activation="tanh"),
    Flatten(),
    Dense(84, activation="tanh"),
    Dense(10, activation="softmax")
])
model.summary()

最后一行显示了神经网络架构如下:

Model: "sequential"
________________________________________________________________________________
 Layer (type)                            Output Shape           Param
                                                                 #
================================================================================
 conv2d (Conv2D)                         (None, 28, 28, 6)      156

 average_pooling2d (AveragePooling2D)    (None, 14, 14, 6)      0

 conv2d_1 (Conv2D)                       (None, 10, 10, 16)     2416

 average_pooling2d_1 (AveragePooling2D)  (None, 5, 5, 16)       0

 conv2d_2 (Conv2D)                       (None, 1, 1, 120)      48120

 flatten (Flatten)                       (None, 120)            0

 dense (Dense)                           (None, 84)             10164

 dense_1 (Dense)                         (None, 10)             850

================================================================================
Total params: 61706 (241.04 KB)
Trainable params: 61706 (241.04 KB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________

这个网络有三个卷积层,接着是两个全连接层。最终的全连接层输出一个 10 维向量,表示输入图像对应于 10 个数字中的一个的概率。

在 Keras 中训练这样的网络并不困难。

首先,你需要将输入从 28×28 图像像素重新格式化为 28×28×1 的张量,以便卷积层可以接受额外的维度。然后,标签应转换为一个独热向量,以匹配网络输出的格式。

然后,你可以通过提供超参数来启动训练:损失函数应该是交叉熵,因为这是一个多类分类问题。Adam 被用作优化器,因为它是常用的选择。在训练期间,你要观察预测准确率。训练应该很快。因此,决定运行 100 个周期,但如果你发现模型在验证集上的损失指标连续四个周期没有改进,就让它提前停止。

想要开始使用 OpenCV 进行机器学习吗?

立即获取我的免费电子邮件速成课程(包括示例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

代码如下:

Python

import numpy as np
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping

# Reshape data to shape of (n_sample, height, width, n_channel)
X_train = np.expand_dims(X_train, axis=3).astype('float32')
X_test = np.expand_dims(X_test, axis=3).astype('float32')
print(X_train.shape)

# One-hot encode the output
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
earlystopping = EarlyStopping(monitor="val_loss", patience=4, restore_best_weights=True)
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[earlystopping])

运行这个模型会打印出如下进度:

Epoch 1/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.1567 - accuracy: 0.9528 - val_loss: 0.0795 - val_accuracy: 0.9739
Epoch 2/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0683 - accuracy: 0.9794 - val_loss: 0.0677 - val_accuracy: 0.9791
Epoch 3/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0513 - accuracy: 0.9838 - val_loss: 0.0446 - val_accuracy: 0.9865
Epoch 4/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0416 - accuracy: 0.9869 - val_loss: 0.0438 - val_accuracy: 0.9863
Epoch 5/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0349 - accuracy: 0.9891 - val_loss: 0.0389 - val_accuracy: 0.9869
Epoch 6/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0300 - accuracy: 0.9903 - val_loss: 0.0435 - val_accuracy: 0.9864
Epoch 7/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0259 - accuracy: 0.9914 - val_loss: 0.0469 - val_accuracy: 0.9864
Epoch 8/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0254 - accuracy: 0.9918 - val_loss: 0.0375 - val_accuracy: 0.9891
Epoch 9/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0209 - accuracy: 0.9929 - val_loss: 0.0479 - val_accuracy: 0.9853
Epoch 10/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0178 - accuracy: 0.9942 - val_loss: 0.0396 - val_accuracy: 0.9882
Epoch 11/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0182 - accuracy: 0.9938 - val_loss: 0.0359 - val_accuracy: 0.9891
Epoch 12/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0150 - accuracy: 0.9952 - val_loss: 0.0445 - val_accuracy: 0.9876
Epoch 13/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0146 - accuracy: 0.9950 - val_loss: 0.0427 - val_accuracy: 0.9876
Epoch 14/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0141 - accuracy: 0.9954 - val_loss: 0.0453 - val_accuracy: 0.9871
Epoch 15/100
1875/1875 [==============================] - 7s 4ms/step - loss: 0.0147 - accuracy: 0.9951 - val_loss: 0.0404 - val_accuracy: 0.9890

由于早期停止规则,这次训练在第 15 个 epoch 时停止了。

一旦你完成模型训练,你可以将 Keras 模型保存为 HDF5 格式,这将包括模型结构和层权重:

Python

model.save("lenet5.h5")

构建模型的完整代码如下:

Python

#!/usr/bin/env python

import numpy as np
from tensorflow.keras.datasets import mnist
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Flatten
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import to_categorical

# Load MNIST data
(X_train, y_train), (X_test, y_test) = mnist.load_data()
print(X_train.shape)
print(y_train.shape)

# LeNet5 model
model = Sequential([
    Conv2D(6, (5,5), input_shape=(28,28,1), padding="same", activation="tanh"),
    AveragePooling2D((2,2), strides=2),
    Conv2D(16, (5,5), activation="tanh"),
    AveragePooling2D((2,2), strides=2),
    Conv2D(120, (5,5), activation="tanh"),
    Flatten(),
    Dense(84, activation="tanh"),
    Dense(10, activation="softmax")
])

# Reshape data to shape of (n_sample, height, width, n_channel)
X_train = np.expand_dims(X_train, axis=3).astype('float32')
X_test = np.expand_dims(X_test, axis=3).astype('float32')

# One-hot encode the output
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# Training
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
earlystopping = EarlyStopping(monitor="val_loss", patience=4, restore_best_weights=True)
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[earlystopping])

model.save("lenet5.h5")

将模型转换为 OpenCV 格式

OpenCV 在其 dnn 模块中支持神经网络。它可以处理由多个框架(包括 TensorFlow 1.x)保存的模型。但是,对于上述保存的 Keras 模型,最好先转换为 ONNX 格式。

用于将 Keras 模型(HDF5 格式)或通用 TensorFlow 模型(Protocol Buffer 格式)转换的工具是 Python 模块 tf2onnx。你可以使用以下命令在你的环境中安装它:

Shell

pip install tf2onnx

之后,你可以使用模块中的转换命令。例如,由于你将 Keras 模型保存为 HDF5 格式,你可以使用以下命令将其转换为 ONNX 格式:

Shell

python -m tf2onnx.convert --keras lenet5.h5 --output lenet5.onnx

然后,会创建一个文件 lenet5.onnx

要在 OpenCV 中使用它,你需要将模型作为网络对象加载到 OpenCV 中。如果是 TensorFlow Protocol Buffer 文件,可以使用函数 cv2.dnn.readNetFromTensorflow('frozen_graph.pb')。在这篇文章中,你使用的是 ONNX 文件。因此,它应该是 cv2.dnn.readNetFromONNX('model.onnx')

这个模型假设输入是一个“blob”,你应该使用以下方式调用模型:

net = cv2.dnn.readNetFromONNX('model.onnx')
blob = cv2.dnn.blobFromImage(numpyarray, scale, size, mean)
net.setInput(blob)
output = net.forward()

Blob 也是一个 numpy 数组,但进行了重新格式化以添加批次维度。

在 OpenCV 中使用模型只需几行代码。例如,我们再次从 TensorFlow 数据集中获取图像,并检查所有测试集样本以计算模型的准确性:

Python

import numpy as np
import cv2
from tensorflow.keras.datasets import mnist

# Load the frozen model in OpenCV
net = cv2.dnn.readNetFromONNX('lenet5.onnx')

# Prepare input image
(X_train, y_train), (X_test, y_test) = mnist.load_data()
correct = 0
wrong = 0
for i in range(len(X_test)):
    img = X_test[i]
    label = y_test[i]

    blob = cv2.dnn.blobFromImage(img, 1.0, (28, 28))

    # Run inference
    net.setInput(blob)
    output = net.forward()
    prediction = np.argmax(output)
    if prediction == label:
        correct += 1
    else:
        wrong += 1

print("count of test samples:", len(X_test))
print("accuracy:", (correct/(correct+wrong)))

在 OpenCV 中运行神经网络模型略有不同于在 TensorFlow 中运行模型,你需要将输入和输出分别处理为两个步骤。

在上面的代码中,你将输出转换为“blob”,没有进行缩放和移位,因为模型就是这样训练的。你设置单张图像的输入,输出将是一个 1×10 的数组。作为 softmax 输出,你使用 argmax 函数获取模型的预测。随后计算测试集上的平均准确性非常简单。上述代码打印:

count of test samples: 10000
accuracy: 0.9889

摘要

在这篇文章中,你学会了如何通过 dnn 模块在 OpenCV 中使用神经网络。具体来说,你学会了

  • 如何训练神经网络模型并将其转换为 ONNX 格式,以供 OpenCV 使用

  • 如何在 OpenCV 中加载和运行模型

使用 OpenCV 进行图像分类和检测的支持向量机

原文:machinelearningmastery.com/support-vector-machines-for-image-classification-and-detection-using-opencv/

之前的教程中,我们探讨了将支持向量机算法作为 OpenCV 库中最受欢迎的监督学习技术之一。

到目前为止,我们已经看到如何将支持向量机应用于我们生成的自定义数据集,该数据集由两个类的二维点组成。

在本教程中,你将学习如何将 OpenCV 的支持向量机算法应用于解决图像分类和检测问题。

完成本教程后,你将了解到:

  • 支持向量机的几个重要特征。

  • 如何将支持向量机应用于图像分类和检测问题。

通过我的书籍《OpenCV 中的机器学习》启动你的项目。它提供了自学教程实用代码

让我们开始吧。

使用 OpenCV 进行图像分类和检测的支持向量机

图片由Patrick Ryan提供,部分版权保留。

教程概述

本教程分为三个部分;它们是:

  • 支持向量机工作原理回顾

  • 将 SVM 算法应用于图像分类

  • 使用 SVM 算法进行图像检测

支持向量机工作原理回顾

之前的教程中,我们介绍了如何在 OpenCV 库中使用支持向量机(SVM)算法。到目前为止,我们已将其应用于我们生成的自定义数据集,该数据集由两个类的二维点组成。

我们已经看到,SVM 旨在通过计算一个决策边界来将数据点分为不同的类,该边界最大化到每个类的最近数据点(称为支持向量)的间隔。通过调整一个名为C的参数,可以放宽最大化间隔的约束,该参数控制最大化间隔和减少训练数据中的错误分类之间的权衡。

SVM 算法可能会使用不同的核函数,这取决于输入数据是否是线性可分的。在非线性可分的数据情况下,可能使用非线性核将数据转换到更高维空间,以便在其中线性可分。这类似于 SVM 在原始输入空间中找到一个非线性的决策边界。

将 SVM 算法应用于图像分类

我们将使用OpenCV 中的数字数据集来完成这个任务,尽管我们开发的代码也可以用于其他数据集。

想要开始使用 OpenCV 进行机器学习吗?

现在就参加我的免费电子邮件速成课程(附有示例代码)。

点击注册,同时获取课程的免费 PDF 电子书版本。

我们的第一步是加载 OpenCV 数字图像,将其分成多个包含手写数字 0 到 9 的子图像,并创建相应的真实标签,以便我们以后可以量化训练好的 SVM 分类器的准确性。对于这个具体的例子,我们将把 80% 的数据集图像分配给训练集,剩下的 20% 图像分配给测试集:

Python

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8)

我们的下一步是在 OpenCV 中创建一个使用 RBF 核的 SVM。正如我们在之前的教程中所做的那样,我们必须设置与 SVM 类型和核函数相关的几个参数值。我们还将包括终止标准,以停止 SVM 优化问题的迭代过程:

Python

# Create a new SVM
svm_digits = ml.SVM_create()

# Set the SVM kernel to RBF
svm_digits.setKernel(ml.SVM_RBF)
svm_digits.setType(ml.SVM_C_SVC)
svm_digits.setGamma(0.5)
svm_digits.setC(12)
svm_digits.setTermCriteria((TERM_CRITERIA_MAX_ITER + TERM_CRITERIA_EPS, 100, 1e-6))

我们将首先将每张图像转换为其 HOG 描述符,如本教程中所述,而不是直接在原始图像数据上训练和测试 SVM。HOG 技术旨在通过利用图像的局部形状和外观来获得更紧凑的表示。对 HOG 描述符进行分类器训练可以提高其区分不同类别的能力,同时减少数据处理的计算开销:

Python

# Converting the image data into HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)
digits_test_hog = hog_descriptors(digits_test_imgs)

我们可以最终对 HOG 描述符上的 SVM 进行训练,并继续预测测试数据的标签,基于此我们可以计算分类器的准确性:

Python

# Predict labels for the testing data
_, digits_test_pred = svm_digits.predict(digits_test_hog.astype(float32))

# Compute and print the achieved accuracy
accuracy_digits = (sum(digits_test_pred.astype(int) == digits_test_labels) / digits_test_labels.size) * 100
print('Accuracy:', accuracy_digits[0], '%')
Accuracy: 97.1 %

对于这个具体的例子,Cgamma 的值是经验性地设置的。然而,建议采用调优技术,如 网格搜索 算法,来研究是否有更好的超参数组合可以进一步提升分类器的准确性。

完整的代码清单如下:

Python

from cv2 import ml, TERM_CRITERIA_MAX_ITER, TERM_CRITERIA_EPS
from numpy import float32
from digits_dataset import split_images, split_data
from feature_extraction import hog_descriptors

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8)

# Create a new SVM
svm_digits = ml.SVM_create()

# Set the SVM kernel to RBF
svm_digits.setKernel(ml.SVM_RBF)
svm_digits.setType(ml.SVM_C_SVC)
svm_digits.setGamma(0.5)
svm_digits.setC(12)
svm_digits.setTermCriteria((TERM_CRITERIA_MAX_ITER + TERM_CRITERIA_EPS, 100, 1e-6))

# Converting the image data into HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)
digits_test_hog = hog_descriptors(digits_test_imgs)

# Train the SVM on the set of training data
svm_digits.train(digits_train_hog.astype(float32), ml.ROW_SAMPLE, digits_train_labels)

# Predict labels for the testing data
_, digits_test_pred = svm_digits.predict(digits_test_hog.astype(float32))

# Compute and print the achieved accuracy
accuracy_digits = (sum(digits_test_pred.astype(int) == digits_test_labels) / digits_test_labels.size) * 100
print('Accuracy:', accuracy_digits[0], '%')

使用 SVM 算法进行图像检测

可以将我们上面开发的图像分类思路扩展到图像检测中,后者指的是在图像中识别和定位感兴趣的对象。

我们可以通过在更大图像中的不同位置重复我们在前一部分开发的图像分类来实现这一点(我们将把这个更大的图像称为 测试图像)。

对于这个具体的例子,我们将创建一个由 OpenCV 数字数据集中随机选择的子图像拼接而成的 拼贴画,然后尝试检测感兴趣的数字出现情况。

首先创建测试图像。我们将通过从整个数据集中随机选择 25 个等间距的子图像,打乱它们的顺序,并将它们组合成一个100×100100\times 100像素的图像来实现:

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, _, digits_test_imgs, _ = split_data(20, sub_imgs, 0.8)

# Create an empty list to store the random numbers
rand_nums = []

# Seed the random number generator for repeatability
seed(10)

# Choose 25 random digits from the testing dataset
for i in range(0, digits_test_imgs.shape[0], int(digits_test_imgs.shape[0] / 25)):

    # Generate a random integer
    rand = randint(i, int(digits_test_imgs.shape[0] / 25) + i - 1)

    # Append it to the list
    rand_nums.append(rand)

# Shuffle the order of the generated random integers
shuffle(rand_nums)

# Read the image data corresponding to the random integers
rand_test_imgs = digits_test_imgs[rand_nums, :]

# Initialize an array to hold the test image
test_img = zeros((100, 100), dtype=uint8)

# Start a sub-image counter
img_count = 0

# Iterate over the test image
for i in range(0, test_img.shape[0], 20):
    for j in range(0, test_img.shape[1], 20):

        # Populate the test image with the chosen digits
        test_img[i:i + 20, j:j + 20] = rand_test_imgs[img_count].reshape(20, 20)

        # Increment the sub-image counter
        img_count += 1

# Display the test image
imshow(test_img, cmap='gray')
show()

结果测试图像如下所示:

图像检测的测试图像

接下来,我们将像前一节那样训练一个新创建的 SVM。然而,鉴于我们现在处理的是检测问题,真实标签不应对应图像中的数字,而应区分训练集中正样本和负样本。

比如说,我们有兴趣检测测试图像中两个0数字的出现。因此,数据集中训练部分中的0图像被视为正样本,并通过类标签 1 区分。所有其他属于剩余数字的图像被视为负样本,并通过类标签 0 区分。

一旦生成了真实标签,我们可以开始在训练数据集上创建和训练 SVM:

Python

# Generate labels for the positive and negative samples
digits_train_labels = ones((digits_train_imgs.shape[0], 1), dtype=int)
digits_train_labels[int(digits_train_labels.shape[0] / 10):digits_train_labels.shape[0], :] = 0

# Create a new SVM
svm_digits = ml.SVM_create()

# Set the SVM kernel to RBF
svm_digits.setKernel(ml.SVM_RBF)
svm_digits.setType(ml.SVM_C_SVC)
svm_digits.setGamma(0.5)
svm_digits.setC(12)
svm_digits.setTermCriteria((TERM_CRITERIA_MAX_ITER + TERM_CRITERIA_EPS, 100, 1e-6))

# Convert the training images to HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)

# Train the SVM on the set of training data
svm_digits.train(digits_train_hog, ml.ROW_SAMPLE, digits_train_labels)

我们将要添加到上面代码列表中的最终代码执行以下操作:

  1. 按预定义的步幅遍历测试图像。

  2. 从测试图像中裁剪出与特征数字的子图像(即,20 ×\times 20 像素)等大小的图像块,并在每次迭代时进行处理。

  3. 提取每个图像块的 HOG 描述符。

  4. 将 HOG 描述符输入到训练好的 SVM 中,以获得标签预测。

  5. 每当检测到时,存储图像块坐标。

  6. 在原始测试图像上为每个检测绘制边界框。

Python

# Create an empty list to store the matching patch coordinates
positive_patches = []

# Define the stride to shift with
stride = 5

# Iterate over the test image
for i in range(0, test_img.shape[0] - 20 + stride, stride):
    for j in range(0, test_img.shape[1] - 20 + stride, stride):

        # Crop a patch from the test image
        patch = test_img[i:i + 20, j:j + 20].reshape(1, 400)

        # Convert the image patch into HOG descriptors
        patch_hog = hog_descriptors(patch)

        # Predict the target label of the image patch
        _, patch_pred = svm_digits.predict(patch_hog.astype(float32))

        # If a match is found, store its coordinate values
        if patch_pred == 1:
            positive_patches.append((i, j))

# Convert the list to an array
positive_patches = array(positive_patches)

# Iterate over the match coordinates and draw their bounding box
for i in range(positive_patches.shape[0]):

    rectangle(test_img, (positive_patches[i, 1], positive_patches[i, 0]),
              (positive_patches[i, 1] + 20, positive_patches[i, 0] + 20), 255, 1)

# Display the test image
imshow(test_img, cmap='gray')
show()

完整的代码列表如下:

Python

from cv2 import ml, TERM_CRITERIA_MAX_ITER, TERM_CRITERIA_EPS, rectangle
from numpy import float32, zeros, ones, uint8, array
from matplotlib.pyplot import imshow, show
from digits_dataset import split_images, split_data
from feature_extraction import hog_descriptors
from random import randint, seed, shuffle

# Load the digits image
img, sub_imgs = split_images('Images/digits.png', 20)

# Obtain training and testing datasets from the digits image
digits_train_imgs, _, digits_test_imgs, _ = split_data(20, sub_imgs, 0.8)

# Create an empty list to store the random numbers
rand_nums = []

# Seed the random number generator for repeatability
seed(10)

# Choose 25 random digits from the testing dataset
for i in range(0, digits_test_imgs.shape[0], int(digits_test_imgs.shape[0] / 25)):

    # Generate a random integer
    rand = randint(i, int(digits_test_imgs.shape[0] / 25) + i - 1)

    # Append it to the list
    rand_nums.append(rand)

# Shuffle the order of the generated random integers
shuffle(rand_nums)

# Read the image data corresponding to the random integers
rand_test_imgs = digits_test_imgs[rand_nums, :]

# Initialize an array to hold the test image
test_img = zeros((100, 100), dtype=uint8)

# Start a sub-image counter
img_count = 0

# Iterate over the test image
for i in range(0, test_img.shape[0], 20):
    for j in range(0, test_img.shape[1], 20):

        # Populate the test image with the chosen digits
        test_img[i:i + 20, j:j + 20] = rand_test_imgs[img_count].reshape(20, 20)

        # Increment the sub-image counter
        img_count += 1

# Display the test image
imshow(test_img, cmap='gray')
show()

# Generate labels for the positive and negative samples
digits_train_labels = ones((digits_train_imgs.shape[0], 1), dtype=int)
digits_train_labels[int(digits_train_labels.shape[0] / 10):digits_train_labels.shape[0], :] = 0

# Create a new SVM
svm_digits = ml.SVM_create()

# Set the SVM kernel to RBF
svm_digits.setKernel(ml.SVM_RBF)
svm_digits.setType(ml.SVM_C_SVC)
svm_digits.setGamma(0.5)
svm_digits.setC(12)
svm_digits.setTermCriteria((TERM_CRITERIA_MAX_ITER + TERM_CRITERIA_EPS, 100, 1e-6))

# Convert the training images to HOG descriptors
digits_train_hog = hog_descriptors(digits_train_imgs)

# Train the SVM on the set of training data
svm_digits.train(digits_train_hog, ml.ROW_SAMPLE, digits_train_labels)

# Create an empty list to store the matching patch coordinates
positive_patches = []

# Define the stride to shift with
stride = 5

# Iterate over the test image
for i in range(0, test_img.shape[0] - 20 + stride, stride):
    for j in range(0, test_img.shape[1] - 20 + stride, stride):

        # Crop a patch from the test image
        patch = test_img[i:i + 20, j:j + 20].reshape(1, 400)

        # Convert the image patch into HOG descriptors
        patch_hog = hog_descriptors(patch)

        # Predict the target label of the image patch
        _, patch_pred = svm_digits.predict(patch_hog.astype(float32))

        # If a match is found, store its coordinate values
        if patch_pred == 1:
            positive_patches.append((i, j))

# Convert the list to an array
positive_patches = array(positive_patches)

# Iterate over the match coordinates and draw their bounding box
for i in range(positive_patches.shape[0]):

    rectangle(test_img, (positive_patches[i, 1], positive_patches[i, 0]),
              (positive_patches[i, 1] + 20, positive_patches[i, 0] + 20), 255, 1)

# Display the test image
imshow(test_img, cmap='gray')
show()

结果图像显示我们成功检测到了测试图像中出现的两个0数字:

检测0数字的两个出现

我们考虑了一个简单的示例,但相同的思想可以轻松地适应更具挑战性的实际问题。如果你打算将上述代码适应更具挑战性的问题:

  • 记住,感兴趣的对象可能会在图像中以不同的大小出现,因此可能需要进行多尺度检测任务。

  • 在生成正负样本来训练你的 SVM 时,避免遇到类别不平衡问题。本教程中考虑的示例是变化非常小的图像(我们仅限于 10 个数字,没有尺度、光照、背景等方面的变化),任何数据集不平衡似乎对检测结果几乎没有影响。然而,现实中的挑战通常不会如此简单,类别之间的不平衡分布可能会导致性能较差。

进一步阅读

如果你想深入了解,本节提供了更多资源。

书籍

网站

总结

在本教程中,你学习了如何应用 OpenCV 的支持向量机算法来解决图像分类和检测问题。

具体来说,你学习了:

  • 支持向量机的一些重要特征。

  • 如何将支持向量机应用于图像分类和检测问题。

你有任何问题吗?

在下面的评论中提问,我会尽力回答。

OpenCV 中的支持向量机

原文:machinelearningmastery.com/support-vector-machines-in-opencv/

支持向量机算法是最受欢迎的监督学习技术之一,并且它在 OpenCV 库中得到了实现。

本教程将介绍开始使用 OpenCV 中支持向量机所需的技能,我们将使用自定义数据集生成。在随后的教程中,我们将应用这些技能于图像分类和检测的具体应用。

在本教程中,你将学习如何在自定义二维数据集上应用 OpenCV 的支持向量机算法。

完成本教程后,你将了解:

  • 支持向量机的一些最重要的特征。

  • 如何在 OpenCV 中使用支持向量机算法处理自定义数据集。

通过我的书《OpenCV 中的机器学习》 启动你的项目。它提供了自学教程可运行的代码

让我们开始吧。

OpenCV 中的支持向量机

图片由Lance Asper提供,版权所有。

教程概述

本教程分为两部分,它们是:

  • 支持向量机工作原理的提醒

  • 在 OpenCV 中发现 SVM 算法

支持向量机工作原理的提醒

支持向量机(SVM)算法在Jason Brownlee 的这个教程中已经解释得很好,不过我们先从复习他教程中的一些最重要的点开始:

  • 为了简单起见,我们假设有两个独立的类别 0 和 1。一个超平面可以将这两个类别中的数据点分开,决策边界将输入空间分割以根据类别区分数据点。这个超平面的维度取决于输入数据点的维度。

*** 如果给定一个新观察到的数据点,我们可以通过计算它位于超平面哪一侧来找出它所属的类别。

*** 一个边际是决策边界与最近数据点之间的距离。它是通过仅考虑属于不同类别的最近数据点来确定的。它是这些最近数据点到决策边界的垂直距离。

*** 最大的边际与最近数据点的距离特征化了最佳决策边界。这些最近的数据点被称为支持向量

*** 如果类别之间不能完全分开,因为它们可能分布得使得一些数据点在空间中混杂,那么需要放宽最大化边界的约束。通过引入一个称为 C 的可调参数,可以放宽边界约束。

*** 参数 C 的值控制边界约束可以被违反的程度,值为 0 意味着完全不允许违反。增加 C 的目的是在最大化边界和减少误分类之间达到更好的折衷。

*** 此外,SVM 使用核函数来计算输入数据点之间的相似性(或距离)度量。在最简单的情况下,当输入数据是线性可分且可以通过线性超平面分离时,核函数实现了一个点积操作。

*** 如果数据点一开始不是线性可分的,核技巧 就会派上用场,其中核函数执行的操作旨在将数据转换到更高维的空间,使其变得线性可分。这类似于 SVM 在原始输入空间中找到一个非线性决策边界。

**## 在 OpenCV 中发现 SVM 算法

首先,我们考虑将 SVM 应用于一个简单的线性可分数据集,这样我们可以在继续更复杂的任务之前,直观地了解前面提到的几个概念。

为此,我们将生成一个包含 100 个数据点(由 n_samples 指定)的数据集,这些数据点均匀地分成 2 个高斯簇(由 centers 指定),标准差设置为 1.5(由 cluster_std 指定)。为了能够复制结果,我们还将定义一个 random_state 的值,我们将其设置为 15:

Python

# Generate a dataset of 2D data points and their ground truth labels
x, y_true = make_blobs(n_samples=100, centers=2, cluster_std=1.5, random_state=15)

# Plot the dataset
scatter(x[:, 0], x[:, 1], c=y_true)
show()

上面的代码应生成以下数据点的图。你可能会注意到,我们将颜色值设置为实际标签,以便区分属于两个不同类别的数据点:

线性可分的数据点属于两个不同的类别

下一步是将数据集分成训练集和测试集,其中前者用于训练 SVM,后者用于测试:

Python

# Split the data into training and testing sets
x_train, x_test, y_train, y_test = ms.train_test_split(x, y_true, test_size=0.2, random_state=10)

# Plot the training and testing datasets
fig, (ax1, ax2) = subplots(1, 2)
ax1.scatter(x_train[:, 0], x_train[:, 1], c=y_train)
ax1.set_title('Training data')
ax2.scatter(x_test[:, 0], x_test[:, 1], c=y_test)
ax2.set_title('Testing data')
show()

将数据点分为训练集和测试集

从上面的训练数据图像中,我们可以看到这两个类别明显可分,并且应该能够通过一个线性超平面轻松分开。因此,让我们继续创建和训练一个在 OpenCV 中使用线性核的 SVM,以找到这两个类别之间的最佳决策边界:

Python

# Create a new SVM
svm = ml.SVM_create()

# Set the SVM kernel to linear
svm.setKernel(ml.SVM_LINEAR)

# Train the SVM on the set of training data
svm.train(x_train.astype(float32), ml.ROW_SAMPLE, y_train)

在这里,请注意 OpenCV 中 SVM 的 train 方法需要输入数据为 32 位浮点类型。

我们可以继续使用训练好的 SVM 对测试数据预测标签,并通过将预测与相应的真实标签进行比较来计算分类器的准确性:

Python

# Predict the target labels of the testing data
_, y_pred = svm.predict(x_test.astype(float32))

# Compute and print the achieved accuracy
accuracy = (sum(y_pred[:, 0].astype(int) == y_test) / y_test.size) * 100
print('Accuracy:', accuracy, ‘%')

Python

Accuracy: 100.0 %

预期地,所有测试数据点都被正确分类。让我们还可视化 SVM 算法在训练期间计算的决策边界,以更好地理解它是如何得出这一分类结果的。

与此同时,迄今为止的代码清单如下:

Python

from sklearn.datasets import make_blobs
from sklearn import model_selection as ms
from numpy import float32
from matplotlib.pyplot import scatter, show, subplots

# Generate a dataset of 2D data points and their ground truth labels
x, y_true = make_blobs(n_samples=100, centers=2, cluster_std=1.5, random_state=15)

# Plot the dataset
scatter(x[:, 0], x[:, 1], c=y_true)
show()

# Split the data into training and testing sets
x_train, x_test, y_train, y_test = ms.train_test_split(x, y_true, test_size=0.2, random_state=10)

# Plot the training and testing datasets
fig, (ax1, ax2) = subplots(1, 2)
ax1.scatter(x_train[:, 0], x_train[:, 1], c=y_train)
ax1.set_title('Training data')
ax2.scatter(x_test[:, 0], x_test[:, 1], c=y_test)
ax2.set_title('Testing data')
show()

# Create a new SVM
svm = ml.SVM_create()

# Set the SVM kernel to linear
svm.setKernel(ml.SVM_LINEAR)

# Train the SVM on the set of training data
svm.train(x_train.astype(float32), ml.ROW_SAMPLE, y_train)

# Predict the target labels of the testing data
_, y_pred = svm.predict(x_test.astype(float32))

# Compute and print the achieved accuracy
accuracy = (sum(y_pred[:, 0].astype(int) == y_test) / y_test.size) * 100
print('Accuracy:', accuracy, '%')

为了可视化决策边界,我们将创建许多二维点,构成一个矩形网格,该网格跨越用于测试的数据点占据的空间:

x_bound, y_bound = meshgrid(arange(x_test[:, 0].min() - 1, x_test[:, 0].max() + 1, 0.05),
                            arange(x_test[:, 1].min() - 1, x_test[:, 1].max() + 1, 0.05))

接下来,我们将把构成矩形网格的数据点的 x 和 y 坐标组织成一个两列数组,并传递给 predict 方法,为每一个数据点生成一个类标签:

Python

bound_points = column_stack((x_bound.reshape(-1, 1), y_bound.reshape(-1, 1))).astype(float32)
_, bound_pred = svm.predict(bound_points)

最后,我们可以通过轮廓图来可视化它们,覆盖用于测试的数据点,以确认 SVM 算法计算的决策边界确实是线性的:

Python

contourf(x_bound, y_bound, bound_pred.reshape(x_bound.shape), cmap=cm.coolwarm)
scatter(x_test[:, 0], x_test[:, 1], c=y_test)
show()

SVM 计算的线性决策边界

我们还可以从上图确认,在第一节中提到的,测试数据点已根据它们所在决策边界的一侧被分配了一个类标签。

此外,我们还可以突出显示被识别为支持向量并在决策边界确定中发挥关键作用的训练数据点:

Python

support_vect = svm.getUncompressedSupportVectors()

scatter(x[:, 0], x[:, 1], c=y_true)
scatter(support_vect[:, 0], support_vect[:, 1], c='red')
show()

突出显示的支持向量为红色

生成决策边界并可视化支持向量的完整代码清单如下:

Python

from numpy import float32, meshgrid, arange, column_stack
from matplotlib.pyplot import scatter, show, contourf, cm

x_bound, y_bound = meshgrid(arange(x_test[:, 0].min() - 1, x_test[:, 0].max() + 1, 0.05),
                            arange(x_test[:, 1].min() - 1, x_test[:, 1].max() + 1, 0.05))

bound_points = column_stack((x_bound.reshape(-1, 1), y_bound.reshape(-1, 1))).astype(float32)
_, bound_pred = svm.predict(bound_points)

# Plot the testing set
contourf(x_bound, y_bound, bound_pred.reshape(x_bound.shape), cmap=cm.coolwarm)
scatter(x_test[:, 0], x_test[:, 1], c=y_test)
show()

support_vect = svm.getUncompressedSupportVectors()

scatter(x[:, 0], x[:, 1], c=y_true)
scatter(support_vect[:, 0], support_vect[:, 1], c='red')
show()

到目前为止,我们考虑了最简单的情况,即有两个可以明确区分的类。但是,如何区分空间中混合在一起的数据点所属的不太明显可分离的类别,比如以下情况:

Python

# Generate a dataset of 2D data points and their ground truth labels
x, y_true = make_blobs(n_samples=100, centers=2, cluster_std=8, random_state=15)

属于两个不同类别的非线性可分数据点

将非线性可分数据分割为训练集和测试集

在这种情况下,我们可能希望根据两个类别彼此重叠的程度探索不同的选项,例如 (1) 通过增加 C 参数的值放宽线性核的边界约束,以在最大化边界和减少误分类之间取得更好的折衷,或者 (2) 使用能够产生非线性决策边界的不同核函数,如径向基函数(RBF)。

在此过程中,我们需要设置 SVM 和正在使用的核函数的几个属性的值:

  • SVM_C_SVC:称为C-支持向量分类,此类 SVM 允许对具有不完全分离的类别进行 n 类分类(n \geq 2)(即非线性可分)。使用 setType 方法设定。

  • C:处理非线性可分类时异常值的惩罚倍数。使用 setC 方法设定。

  • Gamma:决定了 RBF 核函数的半径。较小的 gamma 值导致更宽的半径,可以捕捉远离彼此的数据点的相似性,但可能导致过拟合。较大的 gamma 值导致较窄的半径,只能捕捉附近数据点的相似性,可能导致欠拟合。使用 setGamma 方法设定。

在这里,Cgamma 的值被随意设定,但您可以进行进一步的测试,以探索不同数值如何影响最终预测准确性。前述两个选项均使用以下代码达到 85%的预测准确性,但是通过不同的决策边界实现此准确性:

  • 使用放宽边界约束的线性核函数:

Python

svm.setKernel(ml.SVM_LINEAR)
svm.setType(ml.SVM_C_SVC)
svm.setC(10)

使用放宽边界约束的线性核函数计算的决策边界

  • 使用 RBF 核函数:

Python

svm.setKernel(ml.SVM_RBF)
svm.setType(ml.SVM_C_SVC)
svm.setC(10)
svm.setGamma(0.1)

使用 RBF 核函数计算的决策边界

SVM 参数的选择通常取决于任务和手头的数据,并需要进一步测试以进行相应的调整。

进一步阅读

如果您想深入了解这个主题,本节提供了更多资源。

书籍

网站

总结

在本教程中,你学习了如何在自定义的二维数据集上应用 OpenCV 的支持向量机算法。

具体来说,你学到了:

  • 支持向量机算法的几个最重要的特性。

  • 如何在 OpenCV 中对自定义数据集使用支持向量机算法。

你有任何问题吗?

在下面的评论中提出你的问题,我会尽力回答。****************

在 OpenCV 中训练 Haar 级联目标检测器

原文:machinelearningmastery.com/training-a-haar-cascade-object-detector-in-opencv/

在 OpenCV 中使用 Haar 级联分类器很简单。你只需要提供一个 XML 文件中的训练模型即可创建分类器。然而,从零开始训练并不那么直接。在本教程中,你将看到训练应该是怎样的。特别是,你将学习:

  • 在 OpenCV 中训练 Haar 级联分类器的工具有哪些

  • 如何准备训练数据

  • 如何进行训练

通过我的书籍 《OpenCV 中的机器学习》 快速启动你的项目。它提供了自学教程有效的代码

让我们开始吧!

在 OpenCV 中训练 Haar 级联目标检测器

图片由 Adrià Crehuet Cano 提供。保留部分权利。

概述

本文分为五部分,内容包括:

  • OpenCV 中训练 Cascade 分类器的问题

  • 环境设置

  • Cascade 分类器训练概述

  • 准备训练数据

  • 训练 Haar Cascade 分类器

OpenCV 中训练 Cascade 分类器的问题

OpenCV 已经存在多年,并有许多版本。在写作时,OpenCV 5 正在开发中,推荐的版本是 OpenCV 4,准确来说是 4.8.0。

在 OpenCV 3 和 OpenCV 4 之间进行了大量清理。最显著的是大量代码被重写。变化是显著的,并且许多函数也发生了变化。这包括训练 Haar 级联分类器的工具。

Cascade 分类器不是一个简单的模型像 SVM 那样容易训练。它是一个使用 AdaBoost 的集成模型。因此,训练涉及多个步骤。OpenCV 3 有一个命令行工具来帮助进行这种训练,但在 OpenCV 4 中,该工具已被破坏,修复尚未提供。

因此,只能使用 OpenCV 3 训练 Haar 级联分类器。幸运的是,训练后你可以丢弃它,并在将模型保存到 XML 文件后恢复到 OpenCV 4。这就是你将在本文中做的事情。

你不能在 Python 中同时拥有 OpenCV 3 和 OpenCV 4。因此,建议为训练创建一个单独的环境。在 Python 中,你可以使用venv模块创建虚拟环境,这实际上是创建一个单独的已安装模块集合。另一种选择是使用 Anaconda 或 Pyenv,它们在相同的理念下有不同的架构。在上述所有选择中,Anaconda 环境被认为是最简单的。

想要开始使用 OpenCV 进行机器学习吗?

现在就来参加我的免费电子邮件速成课程(附带示例代码)。

点击注册,并获得课程的免费 PDF 电子书版本。

环境设置

如果你使用 Anaconda,会更简单,你可以使用以下命令创建并使用一个新环境,并将其命名为“cvtrain”:

conda create -n cvtrain python 'opencv>=3,<4'
conda activate cvtrain

如果你发现命令opencv_traincascade可用,那么你就准备好了:

$ opencv_traincascade
Usage: opencv_traincascade
  -data <cascade_dir_name>
  -vec <vec_file_name>
  -bg <background_file_name>
  [-numPos <number_of_positive_samples = 2000>]
  [-numNeg <number_of_negative_samples = 1000>]
  [-numStages <number_of_stages = 20>]
  [-precalcValBufSize <precalculated_vals_buffer_size_in_Mb = 1024>]
  [-precalcIdxBufSize <precalculated_idxs_buffer_size_in_Mb = 1024>]
  [-baseFormatSave]
  [-numThreads <max_number_of_threads = 16>]
  [-acceptanceRatioBreakValue <value> = -1>]
--cascadeParams--
  [-stageType <BOOST(default)>]
  [-featureType <{HAAR(default), LBP, HOG}>]
  [-w <sampleWidth = 24>]
  [-h <sampleHeight = 24>]
--boostParams--
  [-bt <{DAB, RAB, LB, GAB(default)}>]
  [-minHitRate <min_hit_rate> = 0.995>]
  [-maxFalseAlarmRate <max_false_alarm_rate = 0.5>]
  [-weightTrimRate <weight_trim_rate = 0.95>]
  [-maxDepth <max_depth_of_weak_tree = 1>]
  [-maxWeakCount <max_weak_tree_count = 100>]
--haarFeatureParams--
  [-mode <BASIC(default) | CORE | ALL
--lbpFeatureParams--
--HOGFeatureParams--

如果你使用pyenvvenv,则需要更多步骤。首先,创建一个环境并安装 OpenCV(你应该注意到与 Anaconda 生态系统中包的名称不同):

# create an environment and install opencv 3
pyenv virtualenv 3.11 cvtrain
pyenv activate cvtrain
pip install 'opencv-python>=3,<4'

这允许你使用 OpenCV 运行 Python 程序,但你没有用于训练的命令行工具。要获取这些工具,你需要按照以下步骤从源代码编译它们:

  1. 下载 OpenCV 源代码并切换到 3.4 分支

    # download OpenCV source code and switch to 3.4 branch
    git clone https://github.com/opencv/opencv
    cd opencv
    git checkout 3.4
    cd ..
    
  2. 创建与仓库目录分开的构建目录:

    mkdir build
    cd build
    
  3. 使用cmake工具准备构建目录,并参考 OpenCV 仓库:

    cmake ../opencv
    
  4. 运行make进行编译(你可能需要先在系统中安装开发者库)

    make
    ls bin
    
  5. 所需的工具将位于bin/目录中,如上面的最后一条命令所示

所需的命令行工具是opencv_traincascadeopencv_createsamples。本文其余部分假设你已经有了这些工具。

级联分类器训练概述

你将使用 OpenCV 工具训练一个级联分类器。该分类器是一个使用 AdaBoost 的集成模型。简单来说,多个较小的模型被创建,其中每个模型在分类上较弱。结合起来,它成为一个强大的分类器,具有良好的精确度和召回率。

每个弱分类器都是一个二元分类器。要训练它们,你需要一些正样本和负样本。负样本很简单:你提供一些随机图片给 OpenCV,让 OpenCV 选择一个矩形区域(最好这些图片中没有目标物体)。然而,正样本则作为图像和包含物体的边界框提供。

一旦提供了这些数据集,OpenCV 将从中提取 Haar 特征,并使用它们来训练多个分类器。Haar 特征是通过将正样本或负样本划分为矩形区域得到的。如何进行划分涉及到一些随机性。因此,OpenCV 需要时间来找到最好的方式来推导用于分类任务的 Haar 特征。

在 OpenCV 中,你只需提供以 OpenCV 可以读取的格式(如 JPEG 或 PNG)的图像文件中的训练数据。对于负样本,它只需要一个包含文件名的纯文本文件。对于正样本,则需要一个“信息文件”,这是一个包含文件名、图像中物体数量以及相应边界框的纯文本文件。

训练用的正样本应为二进制格式。OpenCV 提供了一个工具opencv_createsamples,可以从“信息文件”生成二进制格式。然后将这些正样本与负样本一起提供给另一个工具opencv_traincascade,以进行训练并生成 XML 格式的模型输出。这是你可以加载到 OpenCV Haar 级联分类器中的 XML 文件。

准备训练数据

让我们考虑创建一个猫脸检测器。要训练这样的检测器,你首先需要数据集。一种可能性是位于以下位置的 Oxford-IIIT Pet Dataset:

这是一个 800MB 的数据集,在计算机视觉数据集的标准下算是一个小数据集。图像以 Pascal VOC 格式进行标注。简而言之,每张图像都有一个对应的 XML 文件,格式如下:

XHTML

<?xml version="1.0"?>
<annotation>
  <folder>OXIIIT</folder>
  <filename>Abyssinian_100.jpg</filename>
  <source>
    <database>OXFORD-IIIT Pet Dataset</database>
    <annotation>OXIIIT</annotation>
    <image>flickr</image>
  </source>
  <size>
    <width>394</width>
    <height>500</height>
    <depth>3</depth>
  </size>
  <segmented>0</segmented>
  <object>
    <name>cat</name>
    <pose>Frontal</pose>
    <truncated>0</truncated>
    <occluded>0</occluded>
    <bndbox>
      <xmin>151</xmin>
      <ymin>71</ymin>
      <xmax>335</xmax>
      <ymax>267</ymax>
    </bndbox>
    <difficult>0</difficult>
  </object>
</annotation>

XML 文件告诉你它所指的是哪个图像文件(如上例中的Abyssinian_100.jpg),以及它包含什么对象,边界框在<bndbox></bndbox>标签之间。

要从 XML 文件中提取边界框,你可以使用以下函数:

import xml.etree.ElementTree as ET

def read_voc_xml(xmlfile: str) -> dict:
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text,
             "objects": []}
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)

    return boxes

上述函数返回的字典示例如下:

{'filename': 'yorkshire_terrier_160.jpg',
'objects': [{'name': 'dog', 'xmax': 290, 'xmin': 97, 'ymax': 245, 'ymin': 18}]}

有了这些,就可以轻松创建用于训练的数据集:在 Oxford-IIT Pet 数据集中,照片要么是猫,要么是狗。你可以将所有的狗照片作为负样本。然后,所有的猫照片将是带有适当边界框集的正样本。

OpenCV 期望的正样本“信息文件”是一个文本文件,每行的格式如下:

filename N x0 y0 w0 h0 x1 y1 w1 h1 ...

文件名后的数字是该图像中边界框的数量。每个边界框都是一个正样本。后面的内容是边界框。每个框由其左上角的像素坐标和框的宽度和高度指定。为了获得 Haar 级联分类器的最佳结果,边界框应与模型预期的长宽比一致。

假设你下载的宠物数据集位于dataset/目录中,你应该会看到文件按如下方式组织:

dataset
|-- annotations
|   |-- README
|   |-- list.txt
|   |-- test.txt
|   |-- trainval.txt
|   |-- trimaps
|   |   |-- Abyssinian_1.png
|   |   |-- Abyssinian_10.png
|   |   ...
|   |   |-- yorkshire_terrier_98.png
|   |   `-- yorkshire_terrier_99.png
|   `-- xmls
|       |-- Abyssinian_1.xml
|       |-- Abyssinian_10.xml
|       ...
|       |-- yorkshire_terrier_189.xml
|       `-- yorkshire_terrier_190.xml
`-- images
    |-- Abyssinian_1.jpg
    |-- Abyssinian_10.jpg
    ...
    |-- yorkshire_terrier_98.jpg
    `-- yorkshire_terrier_99.jpg

有了这些,使用以下程序可以轻松创建正样本的“信息文件”和负样本文件列表:

import pathlib
import xml.etree.ElementTree as ET

import numpy as np

def read_voc_xml(xmlfile: str) -> dict:
    """read the Pascal VOC XML and return (filename, object name, bounding box)
    where bounding box is a vector of (xmin, ymin, xmax, ymax). The pixel
    coordinates are 1-based.
    """
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text,
             "objects": []
            }
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)

    return boxes

# Read Pascal VOC and write data
base_path = pathlib.Path("dataset")
img_src = base_path / "images"
ann_src = base_path / "annotations" / "xmls"

negative = []
positive = []
for xmlfile in ann_src.glob("*.xml"):
    # load xml
    ann = read_voc_xml(str(xmlfile))
    if ann['objects'][0]['name'] == 'dog':
        # negative sample (dog)
        negative.append(str(img_src / ann['filename']))
    else:
        # positive sample (cats)
        bbox = []
        for obj in ann['objects']:
            x = obj['xmin']
            y = obj['ymin']
            w = obj['xmax'] - obj['xmin']
            h = obj['ymax'] - obj['ymin']
            bbox.append(f"{x} {y} {w} {h}")
        line = f"{str(img_src/ann['filename'])} {len(bbox)} {' '.join(bbox)}"
        positive.append(line)

# write the output to `negative.dat` and `postiive.dat`
with open("negative.dat", "w") as fp:
    fp.write("\n".join(negative))

with open("positive.dat", "w") as fp:
    fp.write("\n".join(positive))

该程序扫描数据集中的所有 XML 文件,然后提取每张猫照片中的边界框。列表negative将保存狗照片的路径。列表positive将保存猫照片的路径以及上述格式的边界框,每行作为一个字符串。在循环结束后,这两个列表会被写入磁盘,分别作为negative.datpositive.dat文件。

negative.dat的内容很简单。positive.dat的内容如下:

dataset/images/Siamese_102.jpg 1 154 92 194 176
dataset/images/Bengal_152.jpg 1 84 8 187 201
dataset/images/Abyssinian_195.jpg 1 8 6 109 115
dataset/images/Russian_Blue_135.jpg 1 228 90 103 117
dataset/images/Persian_122.jpg 1 60 16 230 228

在运行训练之前的步骤是将positive.dat转换为二进制格式。这可以通过以下命令行完成:

opencv_createsamples -info positive.dat -vec positive.vec -w 30 -h 30

该命令应在与positive.dat相同的目录中运行,以便可以找到数据集图像。此命令的输出将是positive.vec,也称为“vec 文件”。在执行此操作时,你需要使用-w-h参数指定窗口的宽度和高度。这是为了将边界框裁剪的图像调整为此像素大小,然后写入 vec 文件。这还应与运行训练时指定的窗口大小匹配。

训练 Haar 级联分类器

训练分类器需要时间。它分多个阶段完成。每个阶段都要写入中间文件,所有阶段完成后,你将获得保存在 XML 文件中的训练模型。OpenCV 期望将所有这些生成的文件存储在一个目录中。

运行训练过程确实很简单。假设创建一个新的目录cat_detect来存储生成的文件。目录创建完成后,可以使用命令行工具opencv_traincascade运行训练:

Shell

# need to create the data dir first
mkdir cat_detect
# then run the training
opencv_traincascade -data cat_detect -vec positive.vec -bg negative.dat -numPos 900 -numNeg 2000 -numStages 10 -w 30 -h 30

注意使用positive.vec作为正样本和negative.dat作为负样本。还要注意,-w-h参数与之前在opencv_createsamples命令中使用的相同。其他命令行参数解释如下:

  • -data <dirname>:存储训练分类器的目录。该目录应已存在

  • -vec <filename>:正样本的 vec 文件

  • -bg <filename>:负样本列表,也称为“背景”图像

  • -numPos <N>:每个阶段训练中使用的正样本数量

  • -numNeg <N>:每个阶段训练中使用的负样本数量

  • -numStages <N>:要训练的级联阶段数量

  • -w <width>-h <height>:对象的像素大小。必须与使用opencv_createsamples工具创建训练样本时的大小相同

  • -minHitRate <rate>:每个阶段所需的最小真实正例率。训练一个阶段不会终止,直到达到此要求。

  • -maxFalseAlarmRate <rate>:每个阶段的最大假正例率。训练一个阶段不会终止,直到达到此要求。

  • -maxDepth <N>:弱分类器的最大深度

  • -maxWeakCount <N>:每个阶段的弱分类器最大数量

这些参数并非全部必需。但你应该尝试不同的组合,看看是否能训练出更好的检测器。

在训练期间,你会看到以下屏幕:

$ opencv_traincascade -data cat_detect -vec positive.vec -bg negative.dat -numPos 900 -numNeg 2000 -numStages 10 -w 30 -h 30
PARAMETERS:
cascadeDirName: cat_detect
vecFileName: positive.vec
bgFileName: negative.dat
numPos: 900
numNeg: 2000
numStages: 10
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HAAR
sampleWidth: 30
sampleHeight: 30
boostType: GAB
minHitRate: 0.995
maxFalseAlarmRate: 0.5
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
Number of unique features given windowSize [30,30] : 394725

===== TRAINING 0-stage =====
<BEGIN
POS count : consumed   900 : 900
NEG count : acceptanceRatio    2000 : 1
Precalculation time: 3
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2|        1|        1|
+----+---------+---------+
|   3|        1|        1|
+----+---------+---------+
|   4|        1|   0.8925|
+----+---------+---------+
|   5| 0.998889|   0.7785|
...
|  19| 0.995556|    0.503|
+----+---------+---------+
|  20| 0.995556|    0.492|
+----+---------+---------+
END>
...
Training until now has taken 0 days 2 hours 55 minutes 44 seconds.

===== TRAINING 9-stage =====
<BEGIN
POS count : consumed   900 : 948
NEG count : acceptanceRatio    2000 : 0.00723552
Precalculation time: 4
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2|        1|        1|
+----+---------+---------+
|   3|        1|        1|
+----+---------+---------+
|   4|        1|        1|
+----+---------+---------+
|   5| 0.997778|   0.9895|
...
|  50| 0.995556|   0.5795|
+----+---------+---------+
|  51| 0.995556|   0.4895|
+----+---------+---------+
END>
Training until now has taken 0 days 3 hours 25 minutes 12 seconds.

你应该注意到训练运行的NN个阶段编号为 0 到N1N-1。某些阶段可能需要更长时间进行训练。开始时,会显示训练参数以明确其正在做什么。然后在每个阶段中,会逐行打印一个表格。表格显示三列:特征数量N、命中率HR(真实正例率)和误报率FA(假正例率)。

在阶段 0 之前,您应该看到打印的 minHitRate 为 0.995 和 maxFalseAlarmRate 为 0.5。因此,每个阶段将找到许多 Haar 特征,直到分类器能够保持命中率在 0.995 以上,同时虚警率低于 0.5。理想情况下,您希望命中率为 1,虚警率为 0。由于 Haar cascade 是一个集成方法,如果大多数结果正确,则会得到正确的预测。大致上,您可以认为具有 nn 个阶段、命中率为 pp 和虚警率为 qq 的分类器,其总体命中率为 pnp^n,总体虚警率为 qnq^n。在上述设置中,n=10n=10p>0.995p>0.995q<0.5q<0.5。因此,总体虚警率将低于 0.1%,总体命中率高于 95%。

这个训练命令在现代计算机上完成需要超过 3 小时。输出将命名为 cascade.xml,保存在输出目录下。您可以使用以下示例代码检查结果:

import cv2

image = 'dataset/images/Abyssinian_88.jpg'
model = 'cat_detect/cascade.xml'

classifier = cv2.CascadeClassifier(model)
img = cv2.imread(image)

# Convert the image to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Perform object detection
objects = classifier.detectMultiScale(gray,
                                      scaleFactor=1.1, minNeighbors=5,
                                      minSize=(30, 30))

# Draw rectangles around detected objects
for (x, y, w, h) in objects:
    cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)

# Display the result
cv2.imshow('Object Detection', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

结果将取决于您的模型训练得有多好,也取决于您传递给 detectMultiScale() 的参数。有关如何设置这些参数,请参见上一篇文章。

上述代码在数据集中运行检测器,您可能会看到如下结果:

使用训练好的 Haar cascade 对象检测器的示例输出

您会看到一些误报,但猫的脸已被检测到。改善质量的方法有多种。例如,您使用的训练数据集没有使用方形边界框,而您在训练和检测中使用了方形形状。调整数据集可能会有所改善。同样,您在训练命令行中使用的其他参数也会影响结果。然而,您应该意识到,Haar cascade 检测器非常快速,但使用的阶段越多,速度会变得越慢。

进一步阅读

本节提供了更多相关资源,供您深入了解该主题。

书籍

网站

总结

在这篇文章中,您学习了如何在 OpenCV 中训练 Haar cascade 对象检测器。具体来说,您学习了:

  • 如何为 Haar cascade 训练准备数据

  • 如何在命令行中运行训练过程

  • 如何使用 OpenCV 3.x 训练检测器,并在 OpenCV 4.x 中使用训练好的模型