HOG 特征提取算法(实践篇)
Histograms of Oriented Gradients (HOG)
正如在 ORB 算法中看到的,我们可以使用图像中的关键点进行匹配,以检测图像中的对象。当想要检测具有许多一致的内部特性且不受背景影响的对象时,这些类型的算法非常有用。例如,这些算法在人脸检测中可以取得良好的效果,因为人脸有许多不受图像背景影响的一致的内部特征,例如眼睛、鼻子和嘴巴。然而,当试图进行更一般的对象识别时,例如图像中的行人检测时,这些类型的算法并不能很好地工作。原因是人们的内在特征不像脸那样一致,因为每个人的体型和风格都不同(见下图)。这意味着每个人都会有一套不同的内部特征,因此我们需要一些能够更全面地描述一个人的东西。
一种选择是尝试通过行人的轮廓来检测他们。通过图像的轮廓(边界)来检测物体是非常具有挑战性的,因为我们必须处理背景和前景之间的对比带来的困难。例如,假设想检测一个图像中的行人,她正走在一栋白色建筑前,穿着白色外套和黑色裤子。我们可以在下图中看到,由于图像的背景大多是白色,黑色裤子的对比度将非常高,但由于外套也是白色的,所以对比度将非常低。
在这种情况下,检测裤子的边缘是很容易的,但是检测外套的边缘是非常困难的。而这就是为什么需要 HOG 。即 定向梯度柱状图(Histograms of Oriented Gradients),它是由 Navneet Dalal 和 Bill Triggs 于 2005 年首次引入的。
Hog 算法的工作原理是创建图像中梯度方向分布的柱状图,然后以一种非常特殊的方式对其进行归一化。这种特殊的归一化使得Hog 能够有效地检测物体的边缘,即使在对比度很低的情况下也是如此。这些标准化的柱状图被放在一个特征向量(称为 HOG 描述符)中,可以用来训练机器学习算法,例如支持向量机(SVM),以根据图像中的边界(边)检测对象。由于它的巨大成功和可靠性,HOG 已成为计算机视觉中应用最广泛的目标检测算法之一。
在本次教程中,将要讲到的内容有:
- HOG 算法的工作原理
- 如何使用 OpenCV 创建一个 HOG 描述符
- 如何可视化 HOG 描述符
1. HOG 算法
顾名思义,HOG 算法基于从图像梯度方向创建直方图。HOG算法通过以下一系列步骤实现:
-
给定特定对象的图像,设置一个覆盖图像中整个对象的检测窗口(感兴趣区域)(见下图)。
-
计算检测窗口中每个像素的梯度大小和方向。
-
将检测窗口分成像素的连接单元格,所有单元格的大小相同(见下图)。单元的大小是一个自由参数,通常选择它来匹配要检测的特征的比例。例如,在一个 64 x 128 像素的检测窗口中,6 到 8 像素宽的方形单元适用于检测人体肢体。
-
为每个单元创建一个柱状图,首先将每个单元中所有像素的渐变方向分组为特定数量的方向(角度)箱,然后将每个角度箱中渐变的渐变幅度相加(见下图)。柱状图中的箱数是一个自由参数,通常设置为9个角箱。
-
将相邻单元分组成块(见下图)。每个块中的单元格数是一个自由参数,所有块的大小都必须相同。每个块之间的距离(称为跨距)是一个自由参数,但它通常设置为块大小的一半,在这种情况下,将得到重叠块(见动图)。经验表明,该算法能更好地处理重叠块。
-
使用每个块中包含的单元格来规范化该块中的单元格柱状图(见下图)。如果有重叠的块,这意味着大多数单元格将针对不同的块进行规格化(见动图)。因此,同一个单元可能有几个不同的归一化。
-
将所有块中的所有标准化柱状图收集到一个称为 HOG 描述符的特征向量中。
-
使用从包含同一对象的许多图像中得到的 HOG 描述符训练机器学习算法,例如使用 SVM,以检测图像中的这些对象。例如,可以使用来自许多行人图像的 HOG 描述符来训练 SVM 以检测图像中的行人。训练通过使用包含目标的正例和不包含目标的负例完成。
-
一旦对支持向量机进行了训练,就使用滑动窗口方法来尝试检测和定位图像中的对象。检测图像中的对象需要找到图像中与 SVM 学习到的 HOG 模式相似的部分。
为什么 HOG 算法有效
正如我们上面所了解的,HOG 通过在图像的局部区域中添加特定方向的梯度大小来创建柱状图,称为“cells”。通过这样做可以保证更强的梯度对它们各自的角度柱状图的大小贡献更大,同时最小化由噪声引起的弱梯度和随机定向梯度的影响。通过这种方式,柱状图告诉我们每个单元格的主要梯度方向。
处理相对性问题
现在考虑一个问题,由于局部照明的变化以及背景和前景之间的对比度,梯度方向的大小可以有很大的变化。
为了考虑背景-前景对比度的差异,HOG 算法尝试在局部检测边缘。为了做到这一点,它定义了称为块的单元格组,并使用该局部单元格组规范化柱状图。通过局部归一化,HOG 算法可以非常可靠地检测每个块中的边缘,这称为块归一化。
除了使用块规范化之外,HOG 算法还使用重叠块来提高其性能。通过使用重叠块,每个单元为最终的 HOG 描述符提供几个独立的组成部分,其中每个组成部分对应于一个针对不同块进行规范化的单元。这似乎是多余的,但是经验表明,通过对每个单元对不同的局部块进行多次规格化,HOG 算法的性能显著提高。
加载图像和导入资源
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]
# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')
# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)
# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
创建 HOG 描述符
我们将使用 OpenCV 的HOGDescriptor
类来创建 HOG 描述符。HOG 描述符的参数是使用HOGDescriptor()
函数设置的。 HOGDescriptor()
函数的参数及其默认值如下:
cv2.HOGDescriptor(win_size = (64, 128),
block_size = (16, 16),
block_stride = (8, 8),
cell_size = (8, 8),
nbins = 9,
win_sigma = DEFAULT_WIN_SIGMA,
threshold_L2hys = 0.2,
gamma_correction = true,
nlevels = DEFAULT_NLEVELS)
官方参数解释如下:
-
win_size – Size
Size of detection window in pixels (width, height). Defines the region of interest. Must be an integer multiple of cell size. -
block_size – Size
Block size in pixels (width, height). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get. -
block_stride – Size
Block stride in pixels (horizontal, vertical). It must be an integer multiple of cell size. Theblock_stride
defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longerblock_strides
makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well. -
cell_size – Size
Cell size in pixels (width, height). Determines the size fo your cell. The smaller the cell the finer detail you will get. -
nbins – int
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees. -
win_sigma – double
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms. -
threshold_L2hys – double
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in IJCV, 60(2):91-110, 2004. -
gamma_correction – bool
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm. -
nlevels – int
Maximum number of detection window increases.
我们可以看到,cv2.HOGDescriptor()
函数支持各种参数。 前几个参数(block_size,block_stride,cell_size
和nbins
)可能是最常用的参数。 其他参数一般可以保留其默认值,即可获得良好的结果。
在下面的代码中,我们将使用cv2.HOGDescriptor()
函数来设置单元格大小,块大小,块步幅以及HOG描述符直方图的 bin 数。 然后使用.compute(image)
方法计算给定image
的 HOG 描述符(特征向量)。
# Specify the parameters for our HOG descriptor
# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)
# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)
# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
num_cells_per_block[1] * cell_size[1])
# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]
# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1
# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1
# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)
# Number of gradient orientation bins
num_bins = 9
# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])
# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()
# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()
# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)
# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
HOG 描述符中的元素数量
HOG 描述符(特征向量)是由检测窗口中的所有块的所有单元的归一化直方图 concat 起来的长向量。 因此,HOG特征向量的大小将由检测窗口中的块总数乘以每个块的单元数乘以定向箱(bin)的数量来给出:
如果我们没有重叠块(即block_stride
等于block_size
的情况),则可以通过将检测窗口的大小除以块大小来容易地计算块的总数。 但是,在一般情况下,我们必须考虑到有重叠块的事实。 要查找一般情况下的块总数(即对于任何block_stride
和block_size
),我们可以使用下面给出的公式:
其中 Total$_x$,是沿检测窗口宽度的总块数, Total$_y$, 是沿检测窗口高度的块总数。 Total$_x$ 和 Total$_y$ 的公式考虑了重叠产生的额外块。 在计算 Total$_x$ 和 Total$_y$ 之后,我们可以通过乘法得到检测窗口中的块总数 Total$_x$ $\times$ Total$_y$。 上面的公式可以大大简化,因为block_size
,block_stride
和window_size
都是根据cell_size
定义的。 通过进行所有适当的替换和取消,上述公式简化为:
其中 cells$_x$ 是沿检测窗口宽度的单元格总数, cells$_y$ 是沿检测窗口高度的单元总数。 $N_x$ 是以cell_size
为单位的水平块步幅,$N_y$ 是以 cell_size
为单位的垂直块步幅。
下面让我们计算 HOG 特征向量的元素数量,并检查它是否与上面计算的 HOG 描述符的形状相匹配。
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)
# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)
# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins
# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
tot_bx, 'x',
tot_by, 'x',
num_cells_per_block[0], 'x',
num_cells_per_block[1], 'x',
num_bins, '=',
tot_els)
# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
可视化 HOG 描述符
我们可以通过将与每个细胞相关联的直方图绘制为矢量集合来可视化 HOG 描述符。 为此,我们将直方图中的每个 bin 绘制为单个向量,其大小由 bin 的高度给出,其方向由与其关联的角度 bin 给出。 由于任何给定的单元格可能有多个与之关联的直方图,因为存在重叠的块,我们将选择平均每个单元格的所有直方图,以便为每个单元格生成单个直方图。
如果在 Notebook 上运行,则可以单击灰度图像或 HOG 描述符图像上的任意位置以选择特定单元格。 单击任一图像后,将出现 * magenta * 矩形,显示您选择的单元格。 缩放窗口将显示所选单元格周围的HOG描述符的放大版本; 直方图将显示所选单元格的相应直方图。 交互式窗口底部还有按钮,允许其他功能,例如平移,并根据需要为您提供保存图形的选项。 主页按钮将图形返回到其默认值。
%matplotlib notebook
import copy
import matplotlib.patches as patches
# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]
# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
tot_by,
num_cells_per_block[0],
num_cells_per_block[1],
num_bins).transpose((1, 0, 2, 3, 4))
# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)
# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)
# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))
# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)
# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))
# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
for j in range(num_cells_per_block[1]):
ave_grad[i:tot_by + i,
j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]
hist_counter[i:tot_by + i,
j:tot_bx + j] += 1
# Calculate the average gradient for each cell
ave_grad /= hist_counter
# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]
# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)
# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))
# Set the counter to zero
counter = 0
# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
for j in range(ave_grad.shape[1]):
for k in range(ave_grad.shape[2]):
U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
V[counter] = ave_grad[i,j,k] * np.sin(deg[k])
X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)
counter = counter + 1
# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)
# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)
# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')
# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)
# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')
# Define function for interactive zoom
def onpress(event):
#Unless the left mouse button is pressed do nothing
if event.button != 1:
return
# Only accept clicks for subplots a and b
if event.inaxes in [a, b]:
# Get mouse click coordinates
x, y = event.xdata, event.ydata
# Select the cell closest to the mouse click coordinates
cell_num_x = np.uint32(x / cell_size[0])
cell_num_y = np.uint32(y / cell_size[1])
# Set the edge coordinates of the rectangle patch
edgex = x - (x % cell_size[0])
edgey = y - (y % cell_size[1])
# Create a rectangle patch that matches the the cell selected above
rect = patches.Rectangle((edgex, edgey),
cell_size[0], cell_size[1],
linewidth = 1,
edgecolor = 'magenta',
facecolor='none')
# A single patch can only be used in a single plot. Create copies
# of the patch to use in the other subplots
rect2 = copy.copy(rect)
rect3 = copy.copy(rect)
# Update all subplots
a.clear()
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)
a.add_patch(rect)
b.clear()
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')
b.add_patch(rect2)
c.clear()
c.set(title = 'Zoom Window')
c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
c.invert_yaxis()
c.set_aspect(aspect = 1)
c.set_facecolor('black')
c.add_patch(rect3)
d.clear()
d.set(title = 'Histogram of Gradients')
d.grid()
d.set_xlim(0, 180)
d.set_xticks(angle_axis)
d.set_xlabel('Angle')
d.bar(angle_axis,
ave_grad[cell_num_y, cell_num_x, :],
180 // num_bins,
align = 'center',
alpha = 0.5,
linewidth = 1.2,
edgecolor = 'k')
fig.canvas.draw()
# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
理解直方图
下面分析一下上图的几个静态截图,看看所选单元格的直方图是否有意义。 让我们开始查看三角形内部而不是边缘附近的单元格:
在这种情况下,由于三角形几乎都是相同的颜色,因此在所选单元格中不应存在任何主要梯度。 正如我们在缩放窗口和直方图中可以清楚地看到的那样,情况确实如此。 我们有很多渐变但没有一个明显地支配着另一个。
现在让我们看一下靠近水平边缘的单元格:
请记住,边缘是图像中强度突然变化的区域。 在这些情况下某个特定方向上具有高强度的梯度。 这正是我们在所选单元格的相应直方图和缩放窗口中看到的内容。 在缩放窗口中可以看到主导梯度向上,几乎在90度,因为这是强度急剧变化的方向。 因此,我们应该期望直方图中的90度区域比其他区域更强。 这实际上就是我们所看到的。
现在让我们看一下靠近垂直边缘的单元格:
在这种情况下,我们期望单元中的主导梯度是水平的,接近 180 度,因为这是强度急剧变化的方向。 因此,我们应该期望直方图中的 170 度区域比其他区域梯度影响更大。 这就是我们在直方图中看到的,但我们也看到单元中还有另一个主导梯度,即 10 度 bin 中的梯度。 这是因为 HOG 算法使用无符号梯度,这意味着 0 度和 180 度被认为是相同的。 因此,当创建直方图时,160 度和 180 度之间的角度与 10 度箱(bin)和 170 度箱(bin)成比例。 这导致在垂直边缘附近的单元中存在两个主要梯度而不是仅一个。
总结一下,让我们看一下靠近对角线边缘的单元格。
为了理解我们所看到的,让我们首先记住梯度由 x 部分(分量)和 y 部分(分量)组成,就像向量一样。因此,梯度的最终方向将由其分量的向量和给出。因此,在垂直边缘上,渐变是水平的,因为它们只有 x 分量,如上图所示。在水平边缘上,渐变是垂直的,因为它们只有 y 分量,正如上上图所示。因此,在对角线边缘,梯度也将是对角线,因为 * x * 和 * y * 分量都是非零的。由于图像中的对角线边缘接近 45 度,我们应该期望在 50 度箱(bin)中看到显著的梯度方向。而这实际上就是我们在直方图中看到的,但是,如上图所示,我们看到有两个主导梯度而不是一个。其原因在于,当创建直方图时,靠近区间边界的角度与相邻区间成比例地起作用。例如,角度为 40 度的梯度位于 30 度和 50 度箱的中间。因此,梯度的大小均匀地分成30度和50度的箱。这导致在对角线边缘附近的单元中存在两个主要梯度而不是仅一个。
后记
本文翻译整理自 Udacity 计算机视觉纳米学位练习,官方源码连接:
https://github.com/udacity/CVND_Exercises/blob/master/1_4_Feature_Vectors/3_1. HOG.ipynb