方向梯度直方图(简称HOG)是主要用于计算机视觉和机器学习以进行物体检测的描述符。但是,我们也可以使用HOG描述符来量化和表示 形状 和 纹理。
HOG特征首先由Dalal和Triggs在其CVPR 2005论文“人体检测的方向梯度直方图”中引入。在他们的工作中,Dalal和Triggs提出了HOG和一个5阶描述符来对静止图像中的人进行分类。
这5个阶段包括:
- 在描述之前归一化图像。
- 计算x 和 y 方向的梯度 。
- 获得单元格在空间和方向权重。
- 对归一化的重叠空间进行比较。
- 收集所有定向梯度直方图以形成最终特征向量。
HOG描述符的最重要参数是 方向 - orientations , pixels_per_cell 和 cells_per_block 。这三个参数(连同输入图像的大小)有效地控制了所得特征向量的维数。我们将在本文后面回顾这些参数及其含义。
在大多数实际应用中,HOG与线性SVM结合使用以执行目标检测。HOG被如此大量利用的原因是因为可以使用局部强度梯度的分布来表征局部物体外观和形状。实际上,这些是我们在Gradients课程中学到的完全类似的图像梯度,但是现在我们将采用这些图像梯度并将它们转换为强大的图像描述符。
我们将在本课程后面讨论将HOG和线性SVM组合成对象分类器所需的步骤。但是现在才明白HOG主要用作对象检测的描述符,后来这些描述符可以被输入到机器学习分类器中。
HOG在OpenCV和scikit-image中实现。OpenCV实现不如scikit-image实现灵活,因此我们将主要在本课程的其余部分中使用scikit-image实现。
目标:
在本课中,我们将详细讨论HOG描述子。
什么是用于描述的HOG描述子?
HOG描述符主要用于描述图像中对象的结构形状和外观,使其成为对象分类的优秀描述子。由于HOG可以捕获局部强度梯度和边缘方向,因此它也是获取良好纹理的描述子。
HOG描述子返回实值特征向量。该特征向量的维度取决于为上述方向,可选参数有:pixels_per_cell 和 cells_per_block。
HOG描述子如何工作?
HOG描述子算法的基石是可以通过图像的矩形区域内的强度梯度的分布来构建对象的外观:
实现该描述子需要将图像划分为称为单元的小连接区域,然后对于每个单元,计算每个单元内的像素的定向梯度的直方图。然后我们可以在多个单元格中累积这些直方图以形成我们的特征向量。
Dalal和Triggs还证明我们可以执行 块规范化 以提高性能。为了执行块归一化,我们采用重叠单元组,连接它们的直方图,计算标准化值,然后对比度标准化直方图。通过对多个重叠块进行归一化,得到的描述符对于照明和阴影的变化更加稳健。此外,执行这种类型的归一化意味着每个单元将在最终特征向量中多次表示,但由稍微不同的相邻单元组进行归一化。
现在,让我们回顾一下计算HOG描述符的每个步骤。
步骤1:在描述之前标准化图像。
此规范化步骤完全是可选的,但在某些情况下,此步骤可以提高HOG描述符的性能。我们可以考虑三种主要的规范化方法:
- 伽马/幂定律归一化: 在这种情况下,我们取输入图像中log(p)的每个像素p。然而,正如Dalal和Triggs所证明的那样,这种方法可能导致“过度修正”并且会伤害性能。
- 平方根归一化:这里,我们取输入图像中√(p) 的每个像素p。根据定义,平方根归一化压缩输入像素强度远小于伽马归一化。而且,正如Dalal和Triggs所证明的那样,平方根归一化实际上提高了准确性而不会降低性能。
- 方差归一化:稍微不太常用的归一化形式是方差归一化。在这里,我们计算输入图像的均值 μ 和标准差 σ 。通过从像素强度中减去平均值,所有像素以均值为中心,然后通过除以标准偏差归一化:p' = ( p - μ ) / σ 。Dalal和Triggs没有报告方差归一化的准确性; 然而,这是我喜欢表现的一种规范化形式,并认为值得尝试。
在大多数情况下,最好从没有规范化或平方根规范化开始。方差归一化也值得考虑,但在大多数情况下,它将以与平方根归一化类似的方式执行。
第2步:梯度计算
HOG描述子中的第一个实际步骤是计算x 和 y 方向上的图像梯度 。这些计算看似熟悉,因为我们已经在Gradients课程中对它们进行了验证。我们将应用卷积运算来获得梯度图像:
Gx = I * Dx 和 Gy = I * Dy I 输入图像在哪里,DX 是 x 方向, Dy 的滤波器,是 y 方向的滤波器。下面是计算 输入图像的 x 和 y 梯度的示例:
现在我们有了渐变图像,我们可以计算图像的最终梯度幅度表示:
我们可以在下面看到:
最后,输入图像中每个像素的渐变方向可以通过以下方式计算:
给定两者|G| 和 θ,我们现在可以计算定向梯度的直方图,其中直方图基于θ 和给定的添加到直方图的贡献 或 权重基于 |G|。
第3步:每个单元格中的加权投票
现在我们有了梯度幅度和方向表示,我们需要将图像分成单元格和块。
“单元”是由属于每个单元的像素数定义的矩形区域。例如,如果我们有一个 128 x 128的 图像并将pixel_per_cell定义 为 4 x 4,那么我们将有 32 x 32 = 1024个 单元格:
如果我们将pixels_per_cell定义 为 32 x 32, 我们将有 4 x 4 = 16个 总单元格:
如果我们将pixels_per_cell定义为 128 x 128,那么我们只有 1个 总单元格:
显然,这是一个夸张的例子; 我们可能永远不会对1 x 1 单元格表示感兴趣 。相反,这演示了我们如何根据每个单元的像素数将图像划分为单元格。
现在,对于图像中的每个单元,我们需要使用上面提到的梯度幅度 |G| 和方向来构建定向梯度的直方图 θ。但在构建此直方图之前,我们需要定义orientations 数量。orientations数量决定了直??方图中的区间数。渐变角度在 [0,180](无符号)或 [0,360](带符号)范围内。在一般情况下,最好使用unsigned梯度 [0,180] 并选择[9,12] 范围的orientations数量。但根据您的应用,使用无符号渐变的相较于有符号渐变可以提高精度。最后,每个像素对直方图贡献一定的权重 - |G|即给定像素处的梯度大小。
让我们通过查看分成16 x 16 像素单元格的示例图像来更清楚地说明这一点 :
然后对于每个细胞单元,我们将使用 每个直方图的9个方向(或区间)计算定向梯度的 直方图:
这是一个更具启发性的动画,我们可以直观地看到为每个单元格计算的不同直方图:
此时,我们可以收集并连接这些直方图中的每一个以形成我们的最终特征向量。但是,应用块归一化是有益的,我们将在下一节中进行讨论。
第4步:对比块的对比度归一化
为了解释光照和对比度的变化,我们可以在local归一化梯度值 。这需要将“单元”组合成更大的连接“block”。这些块通常是 重叠的,这意味着每个单元不止一次地对最终特征向量做出贡献。
同样,块的数量是矩形的; 但是,我们的单位不再是像素 - 它们就是细胞单元!Dalal和Triggs报告说,在大多数情况下,使用 2 x 2 或 3 x 3 cells_per_block可以获得合理的准确度。
下面是我们拍摄图像的输入区域,为每个单元格计算梯度直方图,然后将单元格局部分组为重叠块的示例 :
对于当前块中的每个单元,我们连接它们相应的梯度直方图,然后是L1或L2归一化整个连接的特征向量。同样,执行这种类型的归一化意味着每个单元将在最终特征向量中多次表示,但通过不同的值进行归一化。虽然这种多表示是多余的并且浪费空间,但它实际上提高了描述子的性能。
最后,在对所有块进行归一化后,我们将得到的直方图,连接它们,并将它们视为我们的最终特征向量。
HOG描述子在哪里实现?
HOG描述子在OpenCV和scikit-image库中实现。但是,OpenCV实现不是很灵活,主要面向Dalal和Triggs实现。scikit-image实现更加灵活,因此我们将主要使用整个课程中的scikit-image实现。
我如何使用HOG描述子?
以下是如何使用scikit-image计算HOG描述符的示例:
Extracting HOG Features
Python
from skimage import feature H = feature.hog(logo, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), transform_sqrt=True, block_norm="L1")
我们还可以看到生成的HOG图像:
Visualizing HOG features
Python
from skimage import exposure from skimage import feature import cv2 (H, hogImage) = feature.hog(logo, orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2), transform_sqrt=True, block_norm="L1", visualize=True) hogImage = exposure.rescale_intensity(hogImage, out_range=(0, 255)) hogImage = hogImage.astype("uint8") cv2.imshow("HOG Image", hogImage)
(2017-11-28)skimage的更新: 在 scikit - image == 0.12中 , normalize 参数已更新为 transform_sqrt 。该 transform_sqrt 执行完全一样的操作,只是用不同的名称。如果您使用的是较旧版本的 scikit - image(同样,在v0.12版本之前),那么您需要将transform_sqrt更改为 normalize 。在 scikit - image == 0.15 中,block_norm =“L1”的默认值 已被弃用并更改为 block_norm = “L2-Hys” 。因此,对于本课,我们将明确指定 block_norm = “L1” 。这样做可以避免在 我们不知道的情况下使用版本更新切换到 “L2-Hys”(并且会产生错误的汽车徽标识别结果)。您可以 在此处阅读有关L1和L2规范的内容。
(2019年1月6日)更新skimage: 该 visualise 参数已被弃用,变为 visualize 。
使用HOG描述子识别汽车徽标
在本课程的剩余部分中,我将演示如何使用“直方图梯度”描述子来描述汽车品牌的徽标。就像在 Haralick纹理 课程中一样,我们将利用一些机器学习来帮助我们进行分类(这在识别非常规的形状和对象时非常常见)。
同样,我将不会深入研究我们将在本课中使用的(非常少量的)机器学习 - 我们有完整的 图像分类 模块。如果在本课程结束时有几行代码感觉有点像“黑盒魔术”给你,那没关系,并且可以预料到。请记住,这些图像描述课程的重点是快速演示如何在自己的应用程序中使用它们。但在深入研究这个项目之前,让我们看一下我们的数据集。
数据集
我们的汽车标识数据集包括五个品牌的车辆: 奥迪, 福特, 本田, 斯巴鲁和 大众。
对于这些品牌中的每一个,我都 从Google 收集了五张 培训图片。这些图像是 我们将用于教授我们的机器学习算法的 示例图像,每个汽车徽标都是这样的。我们的培训数据集如下所示:
在收集了谷歌的图像后,我走出去,在当地的停车场漫步,拍下七张汽车标识照片。这些徽标将作为我们的 测试集 ,我们可以用它来评估分类器的性能。七个测试图像显示如下:
我们的目标
该项目的目标是:
- 从我们的训练集中提取HOG功能,以表征和量化每个汽车徽标。
- 训练机器学习分类器以区分每个汽车标志。
- 应用分类器来识别新的,看不见的汽车标识。
识别汽车标识
好吧,说够了。让我们开始编写这个例子。打开一个新文件,将其命名为 recogn_car_logos 。py ,让我们得到编码:
recognize_car_logos.py
Python
# import the necessary packages from sklearn.neighbors import KNeighborsClassifier from skimage import exposure from skimage import feature from imutils import paths import argparse import imutils import cv2 # construct the argument parse and parse command line arguments ap = argparse.ArgumentParser() ap.add_argument("-d", "--training", required=True, help="Path to the logos training dataset") ap.add_argument("-t", "--test", required=True, help="Path to the test dataset") args = vars(ap.parse_args()) # initialize the data matrix and labels print("[INFO] extracting features...") data = [] labels = []
此代码看起来应该与Haralick纹理示例中的代码非常相似 。解析我们的命令行参数,我们可以看到我们需要两个开关。第一个是 - 培训 ,这是示例汽车徽标驻留在磁盘上的路径。第二个开关是 - test ,我们将用于评估我们的汽车徽标分类器的测试图像目录的路径。我们还将初始化 数据 和 标签 ,两个列表分别包含我们训练集中每个图像的HOG功能和汽车品牌名称。让我们继续从我们的训练集中提取HOG功能:
recognize_car_logos.py
Python
# loop over the image paths in the training set for imagePath in paths.list_images(args["training"]): # extract the make of the car make = imagePath.split("/")[-2] # load the image, convert it to grayscale, and detect edges image = cv2.imread(imagePath) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) edged = imutils.auto_canny(gray) # find contours in the edge map, keeping only the largest one which # is presmumed to be the car logo cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = imutils.grab_contours(cnts) c = max(cnts, key=cv2.contourArea) # extract the logo of the car and resize it to a canonical width # and height (x, y, w, h) = cv2.boundingRect(c) logo = gray[y:y + h, x:x + w] logo = cv2.resize(logo, (200, 100)) # extract Histogram of Oriented Gradients from the logo H = feature.hog(logo, orientations=9, pixels_per_cell=(10, 10), cells_per_block=(2, 2), transform_sqrt=True, block_norm="L1") # update the data and labels data.append(H) labels.append(make)
在 第22行,我们开始循环遍历培训目录中的每个图像路径。示例图像路径如下所示: car_logos / audi / audi_01 .png
使用此图像路径,我们可以通过拆分路径并提取第二个子目录名称(在本例中为 audi)来提取第24行 的汽车品牌 。
从那里,我们将执行一些预处理并准备使用直方图梯度描述符描述的汽车徽标。我们需要做的就是从磁盘加载图像,将其转换为灰度,然后使用我们方便的 auto_canny 函数来检测品牌徽标中的边缘:
请注意我们如何能够找到汽车标志的轮廓。
无论何时我们 检测到轮廓,您都可以确保下一步(几乎总是) 找到轮廓的轮廓。实际上,这正是 第33-36行 所做的 - 提取边缘图中最大的轮廓,假设是汽车标识的轮廓。
然后,第40和41行采用最大轮廓区域,计算边界框,并提取ROI。
一定要注意 第42行, 因为它 非常重要。正如我在本课程前面提到的,对于您的图像具有不同的宽度和高度可以导致不同大小的 HOG特征向量 - 在几乎所有情况下,这不是您想要的预期行为!
可以这样想:让我们假设我从图像A中提取了大小为1,024-d的HOG特征向量。然后我从图像B中提取了一个具有不同维度的HOG特征向量(使用完全相同的HOG参数)(即宽度和高度)比图像A,留给我一个512-d大小的特征向量。
我将如何比较这两个特征向量?
简短的回答是,你做不到。
请记住,我们提取的特征向量应该用于表征和表示图像的可视内容。如果我们的特征向量不是相同的维度,则无法比较它们的相似性。如果我们无法比较我们的特征向量的相似性,我们无法比较我们的两个图像!
因此,在从图像数据集中提取HOG要素时,您需要定义 规范的已知大小 ,以便将每个图像调整大小。在许多情况下,这意味着您将丢弃图像的纵横比。通常,应该避免破坏图像的纵横比 - 但是在这种情况下我们很乐意这样做,因为它确保(1)我们的数据集中的每个图像以一致的方式描述,以及(2)每个特征向量具有相同的维度。当我们到达自定义对象检测器模块时,我们将更多地讨论这一点 。
无论如何,既然我们的标志被调整至一已知的,预先定义的 200×100 个像素,我们就可以利用应用HOG描述符 取向=9 , pixels_per_cell = (10 ,10 ) , cells_per_block = (2 ,2 ) ,和方 - 根规范化。这些参数是通过实验和检查分类器的准确性获得的 - 无论何时使用HOG描述符,您都应该期望这样做。运行实验并根据这些参数调整HOG参数是获得精确分类器的关键组成部分。
最后,给定HOG特征向量,然后我们 分别用特征向量和car make 更新我们的 数据 矩阵和 标签列表。
根据我们的数据和标签,我们现在可以训练我们的分类器:
recognize_car_logos.py
Python
# "train" the nearest neighbors classifier print("[INFO] training classifier...") model = KNeighborsClassifier(n_neighbors=1) model.fit(data, labels) print("[INFO] evaluating...")
为了识别和区分我们的五个汽车品牌之间的差异,我们将使用scikit- learnns KNeighborsClassifier。
k-最近邻分类器是一种“懒惰学习”算法,其中实际上没有“学习”。相反,k-最近邻(k-NN)训练阶段只接受一组特征向量和标签并存储它们 - 就是这样!然后,当需要对新特征向量进行分类时,它接受特征向量,计算到所有存储的特征向量的距离(通常使用欧几里德距离,但可以使用任何距离度量或相似度量),按距离对它们进行排序,并将前k个 “邻居” 返回 到输入要素向量。从那里,k个 邻居中的每一个都 投票决定他们认为分类的标签是什么。您可以在本课中阅读有关k-NN算法的更多信息 。
在我们的例子中,我们只是将HOG特征向量和标签传递给我们的k-NN算法,并要求它使用k = 1个 邻居向我们的查询要素报告最接近的徽标 。
让我们看看我们如何使用我们的k-NN分类器识别各种汽车标识:
recognize_car_logos.py
Python
# loop over the test dataset for (i, imagePath) in enumerate(paths.list_images(args["test"])): # load the test image, convert it to grayscale, and resize it to # the canonical size image = cv2.imread(imagePath) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) logo = cv2.resize(gray, (200, 100)) # extract Histogram of Oriented Gradients from the test image and # predict the make of the car (H, hogImage) = feature.hog(logo, orientations=9, pixels_per_cell=(10, 10), cells_per_block=(2, 2), transform_sqrt=True, block_norm="L1", visualize=True) pred = model.predict(H.reshape(1, -1))[0] # visualize the HOG image hogImage = exposure.rescale_intensity(hogImage, out_range=(0, 255)) hogImage = hogImage.astype("uint8") cv2.imshow("HOG Image #{}".format(i + 1), hogImage) # draw the prediction on the test image and display it cv2.putText(image, pred.title(), (10, 35), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 3) cv2.imshow("Test Image #{}".format(i + 1), image) cv2.waitKey(0)
第59行 开始循环遍布测试集中的图像。对于每个图像,我们将从磁盘加载它; 将其转换为灰度; 将其调整为已知的固定大小; 然后以与我们在训练阶段完成相同的方式从中提取HOG特征向量(第62-69行)。
第70行 然后调用我们的k-NN分类器,传递我们的HOG特征向量用于当前测试图像并询问分类器它认为徽标是什么。
我们还可以 在73-75行上可视化我们的直方图梯度图像 。这在调试HOG参数时尤其有用,以确保我们的图像内容得到充分量化。以下是我们测试汽车标识的HOG图像的一些示例:
注意HOG图像总是 200 x 100 像素,这是我们调整大小的测试图像的尺寸。我们还可以看到pixel_per_cell 和 orientationations 参数如何在 这里发挥作用,以及每个单元的主导方向,其中单元的大小由pixels_per_cell定义 。pixel_per_cell中的像素 越多,我们的表示就越粗糙。并且类似地,较小的pixels_per_cell值 将产生更细粒度的表示。可视化HOG图像是“查看”HOG描述符和参数集描述的内容的绝佳方式。
最后,我们采用分类结果,将其绘制在我们的测试图像上,并将其显示在78-81行的屏幕上 。
要试一下我们的汽车徽标分类器,只需打开终端并执行以下命令:
recognize_car_logos.py
shell
$ python recognize_car_logos.py --training car_logos --test test_images
在下面你会看到我们的输出分类:
在每种情况下,我们都能够使用HOG功能正确地对汽车品牌进行分类!
当然,这种方法只能起作用,因为我们的车标很紧。如果我们描述 了汽车的 整体形象,我们就不太可能正确地对品牌进行分类。但同样,当我们进入自定义对象检测器 模块时,我们可以解决这个问题 ,特别是 滑动窗口和图像金字塔。
与此同时,这个例子仍然能够演示如何使用Oriented Gradients描述符直方图和k-NN分类器来识别汽车的标识。这里的关键点是,如果您可以一致地检测并提取图像数据集的ROI,那么HOG描述符肯定应该在您要应用的图像描述符列表中,因为它非常强大并且能够获得良好的结果,尤其是在应用于与机器学习相结合。
使用HOG描述符时的建议:
HOG描述符非常强大; 但是,为方向数 , pixels_per_cell 和 cells_per_block选择正确的参数可能会很繁琐 ,尤其是在您开始使用对象分类时。
为起点,我倾向于使用 取向= 9 , pixels_per_cell = (4 ,4 ) ,和 cells_per_block = (2 ,2 ) ,然后再从那里。您的第一组参数不太可能产生最佳性能; 但是,重要的是从某处开始并获得基线 - 可以通过参数调整来改善结果。
将图像大小调整到合理的大小也很重要。如果输入区域为32 x 32 像素,则生成的维度为1,764-d。但是如果您的输入区域是 128 x 128 像素并且您再次使用上述参数,则您的特征向量将为34,596-d!通过使用大图像区域而不关注HOG参数,最终可以得到极大的特征向量。
我们将在本课程后期使用HOG描述符进行对象分类,所以如果你对如何正确调整参数有点困惑,请不要担心 - 这不是你最后一次看到这些描述符!
HOG优点和缺点
优点:
- 很强大的描述符。
- 非常出色的代表当地外观。
- 非常适用于表示没有表现出形式上的实质性变化的结构物体(即建筑物,走在街上的人,靠在墙上的自行车)。
- 对象分类非常准确。
缺点:
- 可能导致非常大的特征向量,导致大的存储成本和计算上昂贵的特征向量比较。
- 调整方向 , pixel_per_cell 和 cells_per_block 参数通常非常重要 。
- 不是最慢的计算描述符,但也不是最快的。
- 如果要描述的对象表现出实质的结构变化(即,对象的旋转/方向始终不同),那么HOG的标准香草实现将不会很好地执行。
(原文地址:https://gurus.pyimagesearch.com/lesson-sample-histogram-of-oriented-gradients-and-car-logo-recognition/#)