在之前的教程中,我们探讨了支持向量机(SVM)算法作为OpenCV库中最受欢迎的监督式机器学习技术之一。
到目前为止,我们已经看到了如何将支持向量机应用于我们生成的一个自定义数据集,该数据集由两个类别的二维点组成。
在本教程中,您将学习如何将OpenCV的支持向量机算法应用于解决图像分类和检测问题。
完成本教程后,您将了解:
- 支持向量机的几个最重要的特征。
- 如何将支持向量机应用于图像分类和检测问题。
通过我的书《OpenCV 机器学习》启动您的项目。它提供了带有可用代码的自学教程。
让我们开始吧。

使用 OpenCV 进行图像分类和检测的支持向量机
照片由Patrick Ryan拍摄,保留部分权利。
教程概述
本教程分为三个部分;它们是:
- 支持向量机工作原理回顾
- 将SVM算法应用于图像分类
- 使用SVM算法进行图像检测
支持向量机工作原理回顾
在之前的教程中,我们介绍了在OpenCV库中使用支持向量机(SVM)算法。到目前为止,我们已将其应用于我们生成的一个自定义数据集,该数据集由两个类别的二维点组成。
我们已经看到,SVM通过计算一个决策边界来将数据点分离到不同的类别,该边界最大化到每个类中最近数据点的间隔,这些最近数据点称为支持向量。通过调整称为C的参数,可以放宽最大化间隔的约束,该参数控制最大化间隔与减少训练数据上的误分类之间的权衡。
SVM算法可能使用不同的核函数,这取决于输入数据是否是线性可分的。对于非线性可分数据,可以使用非线性核将数据转换为更高维的空间,使其线性可分。这类似于SVM在原始输入空间中找到非线性决策边界。
将SVM算法应用于图像分类
我们将在此任务中使用OpenCV中的数字数据集,尽管我们开发的代码也可以用于其他数据集。
想开始学习 OpenCV 机器学习吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
我们的第一步是加载OpenCV数字图像,将其分成包含0到9手写数字的许多子图像,并创建相应的真实标签,以便稍后量化训练的SVM分类器的准确性。对于这个特定的例子,我们将分配80%的数据集图像用于训练集,剩余20%的图像用于测试集。
1 2 3 4 5 |
# 加载数字图像 img, sub_imgs = split_images('Images/digits.png', 20) # 从数字图像获取训练和测试数据集 digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8) |
下一步是创建一个使用RBF核的OpenCV SVM。正如我们在之前的教程中所做的那样,我们必须设置与SVM类型和核函数相关的几个参数值。我们还将包括终止条件,以停止SVM优化问题的迭代过程。
1 2 3 4 5 6 7 8 9 |
# 创建一个新的SVM svm_digits = ml.SVM_create() # 将SVM核设置为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)) |
与其在原始图像数据上训练和测试SVM,不如先将每个图像转换为其HOG描述符,如本教程中所述。HOG技术通过利用图像的局部形状和外观,旨在更紧凑地表示图像。在HOG描述符上训练分类器可能会增加其区分不同类别的判别力,同时降低处理数据的计算成本。
1 2 3 |
# 将图像数据转换为HOG描述符 digits_train_hog = hog_descriptors(digits_train_imgs) digits_test_hog = hog_descriptors(digits_test_imgs) |
最后,我们可以对HOG描述符进行SVM训练,并根据测试数据预测标签,然后计算分类器的准确性。
1 2 3 4 5 6 |
# 预测测试数据的标签 _, digits_test_pred = svm_digits.predict(digits_test_hog.astype(float32)) # 计算并打印获得的准确度 accuracy_digits = (sum(digits_test_pred.astype(int) == digits_test_labels) / digits_test_labels.size) * 100 print('Accuracy:', accuracy_digits[0], '%') |
1 |
准确率:97.1% |
对于这个特定的例子,C和gamma的值是凭经验设置的。然而,建议采用像网格搜索算法这样的调优技术来研究是否存在更好的超参数组合来进一步提高分类器的准确性。
完整的代码清单如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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 # 加载数字图像 img, sub_imgs = split_images('Images/digits.png', 20) # 从数字图像获取训练和测试数据集 digits_train_imgs, digits_train_labels, digits_test_imgs, digits_test_labels = split_data(20, sub_imgs, 0.8) # 创建一个新的SVM svm_digits = ml.SVM_create() # 将SVM核设置为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描述符 digits_train_hog = hog_descriptors(digits_train_imgs) digits_test_hog = hog_descriptors(digits_test_imgs) # 使用训练数据集的集合训练SVM svm_digits.train(digits_train_hog.astype(float32), ml.ROW_SAMPLE, digits_train_labels) # 预测测试数据的标签 _, digits_test_pred = svm_digits.predict(digits_test_hog.astype(float32)) # 计算并打印获得的准确度 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\times 100$ 像素的图像。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# 加载数字图像 img, sub_imgs = split_images('Images/digits.png', 20) # 从数字图像获取训练和测试数据集 digits_train_imgs, _, digits_test_imgs, _ = split_data(20, sub_imgs, 0.8) # 创建一个空列表来存储随机数 rand_nums = [] # 播种随机数生成器以保证可重复性 seed(10) # 从测试数据集中选择25个随机数字 for i in range(0, digits_test_imgs.shape[0], int(digits_test_imgs.shape[0] / 25)): # 生成一个随机整数 rand = randint(i, int(digits_test_imgs.shape[0] / 25) + i - 1) # 将其添加到列表中 rand_nums.append(rand) # 打乱生成的随机整数的顺序 shuffle(rand_nums) # 读取与随机整数对应的图像数据 rand_test_imgs = digits_test_imgs[rand_nums, :] # 初始化一个数组来保存测试图像 test_img = zeros((100, 100), dtype=uint8) # 启动一个子图像计数器 img_count = 0 # 遍历测试图像 for i in range(0, test_img.shape[0], 20): for j in range(0, test_img.shape[1], 20): # 用选定的数字填充测试图像 test_img[i:i + 20, j:j + 20] = rand_test_imgs[img_count].reshape(20, 20) # 递增子图像计数器 img_count += 1 # 显示测试图像 imshow(test_img, cmap='gray') show() |
生成的测试图像如下所示:
接下来,我们将创建一个新SVM并像上一节那样进行训练。然而,由于我们现在正在处理一个检测问题,因此真实标签不应对应于图像中的数字;相反,它们应该区分训练集中的正样本和负样本。
例如,假设我们有兴趣在测试图像中检测数字0的两个实例。因此,在数据集的训练部分中包含0的图像被视为正样本,并用类别标签1进行区分。属于其他数字的所有其他图像被视为负样本,并用类别标签0进行区分。
一旦我们生成了真实标签,就可以继续在训练数据集上创建并训练SVM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 为正负样本生成标签 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 # 创建一个新的SVM svm_digits = ml.SVM_create() # 将SVM核设置为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描述符 digits_train_hog = hog_descriptors(digits_train_imgs) # 使用训练数据集的集合训练SVM svm_digits.train(digits_train_hog, ml.ROW_SAMPLE, digits_train_labels) |
我们将添加到上述代码列表中的最后一部分代码执行以下操作:
- 以预定义的步幅遍历测试图像。
- 在每次迭代时,从测试图像中裁剪一个大小与包含数字的子图像(即20 x 20像素)相同的图像块。
- 提取每个图像块的HOG描述符。
- 将HOG描述符输入训练好的SVM以获得标签预测。
- 在找到检测结果时存储图像块坐标。
- 在原始测试图像上绘制每个检测到的边界框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# 创建一个空列表来存储匹配的图像块坐标 positive_patches = [] # 定义要移动的步幅 stride = 5 # 遍历测试图像 for i in range(0, test_img.shape[0] - 20 + stride, stride): for j in range(0, test_img.shape[1] - 20 + stride, stride): # 从测试图像中裁剪一个图像块 patch = test_img[i:i + 20, j:j + 20].reshape(1, 400) # 将图像块转换为HOG描述符 patch_hog = hog_descriptors(patch) # 预测图像块的目标标签 _, patch_pred = svm_digits.predict(patch_hog.astype(float32)) # 如果找到匹配项,则存储其坐标值 if patch_pred == 1: positive_patches.append((i, j)) # 将列表转换为数组 positive_patches = array(positive_patches) # 遍历匹配坐标并绘制其边界框 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) # 显示测试图像 imshow(test_img, cmap='gray') show() |
完整的代码清单如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
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 # 加载数字图像 img, sub_imgs = split_images('Images/digits.png', 20) # 从数字图像获取训练和测试数据集 digits_train_imgs, _, digits_test_imgs, _ = split_data(20, sub_imgs, 0.8) # 创建一个空列表来存储随机数 rand_nums = [] # 播种随机数生成器以保证可重复性 seed(10) # 从测试数据集中选择25个随机数字 for i in range(0, digits_test_imgs.shape[0], int(digits_test_imgs.shape[0] / 25)): # 生成一个随机整数 rand = randint(i, int(digits_test_imgs.shape[0] / 25) + i - 1) # 将其添加到列表中 rand_nums.append(rand) # 打乱生成的随机整数的顺序 shuffle(rand_nums) # 读取与随机整数对应的图像数据 rand_test_imgs = digits_test_imgs[rand_nums, :] # 初始化一个数组来保存测试图像 test_img = zeros((100, 100), dtype=uint8) # 启动一个子图像计数器 img_count = 0 # 遍历测试图像 for i in range(0, test_img.shape[0], 20): for j in range(0, test_img.shape[1], 20): # 用选定的数字填充测试图像 test_img[i:i + 20, j:j + 20] = rand_test_imgs[img_count].reshape(20, 20) # 递增子图像计数器 img_count += 1 # 显示测试图像 imshow(test_img, cmap='gray') show() # 为正负样本生成标签 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 # 创建一个新的SVM svm_digits = ml.SVM_create() # 将SVM核设置为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描述符 digits_train_hog = hog_descriptors(digits_train_imgs) # 使用训练数据集的集合训练SVM svm_digits.train(digits_train_hog, ml.ROW_SAMPLE, digits_train_labels) # 创建一个空列表来存储匹配的图像块坐标 positive_patches = [] # 定义要移动的步幅 stride = 5 # 遍历测试图像 for i in range(0, test_img.shape[0] - 20 + stride, stride): for j in range(0, test_img.shape[1] - 20 + stride, stride): # 从测试图像中裁剪一个图像块 patch = test_img[i:i + 20, j:j + 20].reshape(1, 400) # 将图像块转换为HOG描述符 patch_hog = hog_descriptors(patch) # 预测图像块的目标标签 _, patch_pred = svm_digits.predict(patch_hog.astype(float32)) # 如果找到匹配项,则存储其坐标值 if patch_pred == 1: positive_patches.append((i, j)) # 将列表转换为数组 positive_patches = array(positive_patches) # 遍历匹配坐标并绘制其边界框 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) # 显示测试图像 imshow(test_img, cmap='gray') show() |
结果图像显示我们已成功检测到测试图像中的两个0数字实例。
我们考虑了一个简单的例子,但同样的思想可以轻松地改编以解决更具挑战性的现实生活问题。如果您计划将上述代码改编到更具挑战性的问题中:
- 请记住,感兴趣的对象可能以各种大小出现在图像中,因此您可能需要执行多尺度检测任务。
- 在生成用于训练SVM的正负样本时,不要陷入类别不平衡问题。我们在本教程中考虑的例子是变化非常小的图像(我们只限制在10个数字,没有尺度、光照、背景等变化),任何数据集的不平衡似乎对检测结果的影响都很小。然而,现实生活中的挑战通常不像这样简单,类别之间的不平衡分布可能导致性能下降。
进一步阅读
如果您想深入了解此主题,本节提供了更多资源。
书籍
- OpenCV 机器学习, 2017.
- 使用 Python 精通 OpenCV 4, 2019.
网站
总结
在本教程中,您学习了如何将OpenCV的支持向量机算法应用于解决图像分类和检测问题。
具体来说,你学到了:
- 支持向量机的几个最重要的特征。
- 如何将支持向量机应用于图像分类和检测问题。
你有什么问题吗?
在下面的评论中提出您的问题,我将尽力回答。
暂无评论。