OpenCV Recipes:从图像中提取特征

In this post, we are going to learn how to detect salient points, also knwon as keypoints, in an image.

我们为什么关心关键点?

图像内容分析是指理解图像内容的过程。让我们看看人类是如何做到的。我们的大脑是一台非常强大的机器,可以很快完成复杂的事情。当我们观察某些东西时,我们的大脑会根据该图像的感兴趣方面自动创建足迹。

就目前而言,一个感兴趣区域是该区域的独特之处。如果我们称一个点为兴趣点,那么它的邻域中不应该有另一个满足约束的点。

我们自动倾向于图像中的感兴趣区域,因为这是所有信息的所在。当我们构建对象识别系统时,我们需要检测这些兴趣区域以便为图像创建签名。这些兴趣区域以关键点为特征。这就是在许多现代计算机视觉系统中关键点检测至关重要的原因。

什么是关键点?

现在我们知道关键点指的是图像中感兴趣区域,让我们深入挖掘一下。关键点是什么?这些点在哪里?当我们感兴趣时,意味着该区域发生着变化。例如,角点很有趣,因为两个不同方向的强度发生急剧变化。每个角点都是两条边相交的独特点。如果你看一下前面的图像,你会发现感兴趣区域并不完全由感兴趣内容组成。

如果你看一下前面的对象,感兴趣区域的内部部分是不感兴趣的:

所以,如果我们要描述这个对象,就需要确保我们选择了兴趣点。那么,我们如何定义兴趣点?我们可以说任何不是非感兴趣的区域都可能是兴趣点吗?让我们考虑以下示例:

我们可以看到图像中沿着边缘有很多高频内容。但我们不能称整个边缘有趣。重要的是要理解感兴趣的并不一定是指颜色或强度,它可以是任何独特的东西。我们需要的是与领域隔离的唯一的点。边缘的点相对于它们的邻居并不唯一。既然知道我们在寻找什么,那么如何选择一个兴趣点呢?桌角怎么样?它对邻居来说是唯一的。现在可以选择这一点作为关键点之一。我们用一些这样的关键点来表征特定图像。使用这些关键点的组合来创建图像签名。我们希望此图像签名能够以最佳的方式来表征给定的图像。

检测角点

由于我们对角点感兴趣,那么让我们看看如何检测它们。 在计算机视觉中,有一种流行的角点检测技术,称为 Harris 角点检测器。基于灰度图像的偏导数构造 2×2 矩阵,然后分析所获得的特征值,我们使用它们来检测角点。 这是一种对实际算法的过度简化,但它涵盖了基本要点。如果你想了解相关的数学细节,可以查看 Harris 和 Stephens 的原始论文。角点是两个特征值都较大点。

让我们考虑下面的图像:

如果你在图像上允许 Harris 角点检测器,你会看到如下结果:

如你所见,所有黑点都对应于图像的角点。 你可能会注意到未检测到底部的角点。 其原因是角点不够锐利。 可以通过调整角点检测器的阈值以识别这些角点。 执行此操作的代码如下:

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
import cv2
import numpy as np

img = cv2.imread('images/box.png')

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)

# To detect only sharp corners
dst = cv2.cornerHarris(gray, blockSize=4, ksize=5, k=0.04)
# Result is dilated for marking the corners
dst = cv2.dilate(dst, None)

# Threadhold for an optimal value, it may vary depending on the image
img[dst > 0.01*dst.max()] = [0, 0, 0]
cv2.imshow('Harris Corners (only sharp)', img)

# to detect soft corners
dst = cv2.cornerHarris(gray, blockSize=14, ksize=5, k=0.04)
dst = cv2.dilate(dst, None)

img[dst > 0.01*dst.max()] = [0, 0, 0]
cv2.imshow('Harris Corners (also soft)', img)

cv2.waitKey()

Good Features To Track

Harris 角点检测器在很多情况下都表现良好,但在一些情况下表现不佳。在 Harris 和 Stephens 的原始论文发表六年后,Shi 和 Tomasi 提出了一个更好的角点检测器。如果将 Shi-Tomasi 角点检测器应用于图像,你将看到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cv2
import numpy as np

img = cv2.imread('images/box.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

corners = cv2.goodFeaturesToTrack(gray, maxCorners=7, qualityLevel=0.05,
minDistance=25)
corners = np.float32(corners)

for item in corners:
x, y = item[0]
cv2.circle(img, (x,y), 5, 255, -1)

cv2.imshow("Top 'k' features", img)
cv2.waitKey()

尺度不变特征变换(SIFT)

尽管角点特征很好,但它还不足以表征真正感兴趣的部分。当我们谈论图像内容分析时,我们希望图像签名对于诸如比例、旋转和照明之类的影响是不变的。

让我们考虑角点特征。如果你放大图像,角点可能不再是角点,如下所示:

在第二种情况下,检测器不会拾取这个角点。并且,由于它在原始图像中能够识别,因此第二张图像将与第一张图像不匹配。它们基本上是相同的图像,但基于角点特征的方法完全错过了它。这意味着角点检测器不是精确的尺度不变的。这就是为什么我们需要一种更好的方法来表征图像。

SIFT 是计算机视觉领域最流行的算法之一。具体细节请阅读 David Lowe 的原始论文。我们可以使用该算法提取关键点并构建相应的特征描述符。为了识别潜在的关键点,SIFT 通过对图像进行下采样并获取高斯差来建立金字塔。这意味着我们在每一层都运行高斯滤波器并作差来构建金字塔中的连续层级。为了获知当前点是否为关键点,它会查看邻居以及金字塔中相邻层级的相同位置的像素,如果是最大值,那么当前点将被选为关键点。这可以确保我们保持关键点的尺度不变。

现在我们知道 SIFT 如何实现尺度不变性,让我们看看它如何实现旋转不变性。一旦我们识别出关键点,就会为每个关键点分配一个方向。我们取每个关键点的邻域并计算梯度。这让我们获知该关键点的方向。如果我们有这方面的信息,我们就能够将此关键点与其他图像(即使已经旋转)的关键点进行匹配。由于我们知道方向,我们就可以在比较之前进行归一化。

一旦我们掌握了所有这些信息,我们如何量化它?我们需要将其转换为一组数字,以便我们可以对其进行某种匹配。为实现这一目标,只需在每个关键点周围取 16x16 邻域,并将其划分为 16 个大小为 4x4 的块。对于每个块,我们计算具有 8 bins 的方向直方图。因此,我们有长度为 8 的向量与每个块相关联,这意味着邻域由大小为 128(8x16)的向量表示。这就是最终关键点描述符。如果我们从图像中提取 N 个关键点,那么我们将有 N 个描述符,每个描述符的长度为 128。

考虑如下图片:

如果使用 SIFT 提取关键点位置,您将看到如下所示的内容,其中圆的大小表示关键点的强度,圆圈内的线表示方向:

在我们查看代码之前,重要的是要知道 SIFT 已获得专利,并且它不能免费用于商业用途。以下是执行此操作的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2
import numpy as np

input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)

# For version opencv < 3.0.0, use cv2.SIFT()
sift = cv2.xfeatures2d.SIFT_create()
keypoints = sift.detect(gray_image, None)

cv2.drawKeypoints(input_image, keypoints, input_image,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('SIFT features', input_image)
cv2.waitKey()

我们还可以计算描述符。 OpenCV 允许我们单独执行,或者我们可以在同一步骤进行计算和检测部分:

1
keypoints, descriptors = sift.detectAndCompute(gray_image, None)

加速鲁棒特征(SURF)

SIFT 很有用,但计算量很大,如果使用 SIFT,我们很难构建实时系统。 因此我们需要一个快速的但具有 SIFT 所有优势的系统。SIFT 使用高斯差来构建金字塔,这个过程很慢,为了克服这个问题,SURF 使用简单的盒式滤波器来逼近高斯滤波器。 好处是,其非常容易计算,且速度相当快。 有关 SURF 的大量文档,请访问此页面。 还可以参考原始论文。SURF 也是专利的,不能免费用于商业用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cv2
import numpy as np

input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)

# For version opencv < 3.0.0, use cv2.SURF()
surf = cv2.xfeatures2d.SURF_create()
# This threshold controls the number of keypoints
surf.setHessianThreshold(15000)

keypoints, descriptors = surf.detectAndCompute(gray_image, None)

cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0),
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('SURF features', input_image)
cv2.waitKey()

加速分割测试特征(FAST)

尽管 SURF 比 SIFT 快,但它对于实时系统来说还是不够快,特别是当存在资源限制时。在移动设备上构建实时应用程序时,是无法使用 SURF 进行实时计算的。我们需要真正快速且计算成本低的特征。 因此,Rosten 和 Drummond 提出了 FAST。 顾名思义,它真的很快!

他们只是进行了快速的测试,来确定当前点是否是潜在的关键点,而没有进行昂贵代价的计算。需要注意的是 FAST 只用于关键点的检测。一旦检测到关键点,需要使用 SIFT 或 SURF 来计算描述符。 请考虑以下图像:

如果我们在此图像上运行 FAST 关键点检测器,您将看到如下内容:

如果我们抑制不重要的关键点,它将如下所示:

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
import cv2
import numpy as np

input_image = cv2.imread('images/tool.png')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)

# Version under opencv 3.0.0 cv2.FastFeatureDetector()
fast = cv2.FastFeatureDetector_create()

# Detect keypoints
keypoints = fast.detect(gray_image, None)
print("Number of keypoints with non max suppression:", len(keypoints))

# Draw keypoints on top of the input image
img_keypoints_with_nonmax=input_image.copy()
cv2.drawKeypoints(input_image, keypoints, img_keypoints_with_nonmax,
color=(0,255,0), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('FAST keypoints - with non max suppression',
img_keypoints_with_nonmax)

# Disable nonmaxSuppression
fast.setNonmaxSuppression(False)
# Detect keypoints again
keypoints = fast.detect(gray_image, None)
print("Total Keypoints without nonmaxSuppression:", len(keypoints))

# Draw keypoints on top of the input image
img_keypoints_without_nonmax=input_image.copy()
cv2.drawKeypoints(input_image, keypoints, img_keypoints_without_nonmax,
color=(0,255,0), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('FAST keypoints - without non max suppression',
img_keypoints_without_nonmax)

cv2.waitKey()

Binary Robust Independent Elementary Features (BRIEF)

即使我们有 FAST 检测关键点,我们仍然要使用 SIFT 或 SURF 来计算描述符。因此,我们还需要一种快速计算描述符的方法,BRIEF 由此被引入。 BRIEF 是一种用于提取特征描述符的方法。 它无法自行检测关键点,因此我们需要将其与关键点检测器结合使用。

请考虑以下图像:

BRIEF 获取输入的关键点列表并输出更新列表。 如果在此图片上运行 BRIEF,你将看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2
import numpy as np

input_image = cv2.imread('images/house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)

# Initiate FAST detector
fast = cv2.FastFeatureDetector_create()

# Initiate BRIEF extractor, before opencv 3.0.0 use
# cv2.DescriptorExtractor_create("BRIEF")
brief = cv2.xfeatures2d.BriefDescriptorExtractor_create()

# find the keypoints with STAR
keypoints = fast.detect(gray_image, None)

# compute the descriptors with BRIEF
keypoints, descriptors = brief.compute(gray_image, keypoints)
cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0))
cv2.imshow('BRIEF keypoints', input_image)
cv2.waitKey()

Oriented FAST and Rotated BRIEF(ORB)

现在,让我们来将现有技术相结合来获得一个最佳组合。该算法由 OpenCV Labs 提出,它快速、强大且开源!SIFT 和 SURF 算法都获得专利,不能用于商业目的;这就是为什么 ORB 好的原因。 在图像上运行 ORB 关键点提取器你会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2
import numpy as np

input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)

# Initiate ORB object, before opencv 3.0.0 use cv2.ORB()
orb = cv2.ORB_create()

# find the keypoints with ORB
keypoints = orb.detect(gray_image, None)

# compute the descriptors with ORB
keypoints, descriptors = orb.compute(gray_image, keypoints)

# draw only the location of the keypoints without size or orientation
cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0))

cv2.imshow('ORB keypoints', input_image)

cv2.waitKey()
GreatX wechat
关注我的公众号,推送优质文章。