部分 V
图像特征提取与描述
34 角点检测的 FAST 算法
目标
? 理解 FAST 算法的基础
? 使用 OpenCV 中的 FAST 算法相关函数进行角点检测
原理
我们前面学习了几个特征检测器,它们大多数效果都很好。但是从实时处理的角度来看,这些算法都不够快。一个最好例子就是 SLAM(同步定位与地图构建),移动机器人,它们的计算资源非常有限。
为了解决这个问题,Edward_Rosten 和 Tom_Drummond 在 2006 年提出里 FAST 算法。我们下面将会对此算法进行一个简单的介绍。你可以参考原始文献获得更多细节(本节中的所有图像都是曲子原始文章)。
34.1 使用 FAST 算法进行特征提取
1. 在图像中选取一个像素点 p,来判断它是不是关键点。I p 等于像素点 p的灰度值。
2. 选择适当的阈值 t。
3. 如下图所示在像素点 p 的周围选择 16 个像素点进行测试。
4. 如果在这 16 个像素点中存在 n 个连续像素点的灰度值都高于 I p + t,或者低于 I p ?t,那么像素点 p 就被认为是一个角点。如上图中的虚线所示,n 选取的值为 12。
5. 为了获得更快的效果,还采用了而外的加速办法。首先对候选点的周围每个 90 度的点:1,9,5,13 进行测试(先测试 1 和 19, 如果它们符合阈值要求再测试 5 和 13)。如果 p 是角点,那么这四个点中至少有 3 个要符合阈值要求。如果不是的话肯定不是角点,就放弃。对通过这步测试的点再继续进行测试(是否有 12 的点符合阈值要求)。这个检测器的效率很高,但是它有如下几条缺点:
? 当 n<12 时它不会丢弃很多候选点 (获得的候选点比较多)。
? 像素的选取不是最优的,因为它的效果取决与要解决的问题和角点的分布情况。
? 高速测试的结果被抛弃
? 检测到的很多特征点都是连在一起的。
前 3 个问题可以通过机器学习的方法解决,最后一个问题可以使用非最大值抑制的方法解决。
34.2 机器学习的角点检测器
1. 选择一组训练图片(最好是跟最后应用相关的图片)
2. 使用 FAST 算法找出每幅图像的特征点
3. 对每一个特征点,将其周围的 16 个像素存储构成一个向量。对所有图像都这样做构建一个特征向量 P
4. 每一个特征点的 16 像素点都属于下列三类中的一种。
5. 根据这些像素点的分类,特征向量 P 也被分为 3 个子集:P d ,P s ,P b
6. 定义一个新的布尔变量 K p ,如果 p 是角点就设置为 Ture,如果不是就设置为 False。
7. 使用 ID3 算法(决策树分类器)Use the ID3 algorithm (decision tree classifier) to query each subset using the variable K p for the knowledge about the true class. It selects the x which yields the most information about whether the candidate pixel is a corner, measured by the entropy of K p .
8. This is recursively applied to all the subsets until its entropy iszero.
9. 将构建好的决策树运用于其他图像的快速的检测。
34.3 非极大值抑制
使用极大值抑制的方法可以解决检测到的特征点相连的问题
1. 对所有检测到到特征点构建一个打分函数 V。V 就是像素点 p 与周围 16个像素点差值的绝对值之和。
2. 计算临近两个特征点的打分函数 V。
3. 忽略 V 值最低的特征点
34.4 总结
FAST 算法比其它角点检测算法都快。但是在噪声很高时不够稳定,这是由阈值决定的。
34.5 OpenCV 中 中 FAST 特征检测器
很其他特征点检测一样我们可以在 OpenCV 中直接使用 FAST 特征检测器。如果你愿意的话,你还可以设置阈值,是否进行非最大值抑制,要使用的邻域大小()等。
邻域设置为下列 3 中之一:cv2.FAST_FEATURE_DETECTOR_TYPE_5_8,cv2.FAST_FEATURE_DETECTOR_TYPE_7_12和 cv2.FAST_FEATURE_DETECTOR_TYPE_9_16。下面是使用 FAST 算法进行特征点检测的简单代码。
import numpy as np import cv2 from matplotlib import pyplot as plt img = cv2.imread(‘simple.jpg‘,0) # Initiate FAST object with default values fast = cv2.FastFeatureDetector() # find and draw the keypoints kp = fast.detect(img,None) img2 = cv2.drawKeypoints(img, kp, color=(255,0,0)) # Print all default params print "Threshold: ", fast.getInt(‘threshold‘) print "nonmaxSuppression: ", fast.getBool(‘nonmaxSuppression‘) print "neighborhood: ", fast.getInt(‘type‘) print "Total Keypoints with nonmaxSuppression: ", len(kp) cv2.imwrite(‘fast_true.png‘,img2) # Disable nonmaxSuppression fast.setBool(‘nonmaxSuppression‘,0) kp = fast.detect(img,None) print "Total Keypoints without nonmaxSuppression: ", len(kp) img3 = cv2.drawKeypoints(img, kp, color=(255,0,0)) cv2.imwrite(‘fast_false.png‘,img3)
结果如下。第一幅图是使用了非最大值抑制的结果,第二幅没有使用非最大值抑制。
35 BRIEF(Binary Robust Independent Elementary Features)
目标
? 我们学习 BRIEF 算法的基础
原理
我们知道 SIFT 算法使用的是 128 维的描述符。由于它是使用的浮点数,所以要使用 512 个字节。同样 SURF 算法最少使用 256 个字节(64 为维描述符)。创建一个包含上千个特征的向量需要消耗大量的内存,在嵌入式等资源有限的设备上这样是合适的。匹配时还会消耗更多的内存和时间。
但是在实际的匹配过程中如此多的维度是没有必要的。我们可以使用 PCA,LDA 等方法来进行降维。甚至可以使用 LSH(局部敏感哈希)将 SIFT 浮点数的描述符转换成二进制字符串。对这些字符串再使用汉明距离进行匹配。汉明距离的计算只需要进行 XOR 位运算以及位计数,这种计算很适合在现代的CPU 上进行。但我们还是要先找到描述符才能使用哈希,这不能解决最初的内存消耗问题。
BRIEF 应运而生。它不去计算描述符而是直接找到一个二进制字符串。这种算法使用的是已经平滑后的图像,它会按照一种特定的方式选取一组像素点对 n d (x,y),然后在这些像素点对之间进行灰度值对比。例如,第一个点对的灰度值分别为 p 和 q。如果 p 小于 q,结果就是 1,否则就是 0。就这样对 n d个点对进行对比得到一个 n d 维的二进制字符串。
n d 可以是 128,256,512。OpenCV 对这些都提供了支持,但在默认情况下是 256(OpenC 是使用字节表示它们的,所以这些值分别对应与 16,32,64)。当我们获得这些二进制字符串之后就可以使用汉明距离对它们进行匹配了。
非常重要的一点是:BRIEF 是一种特征描述符,它不提供查找特征的方法。所以我们不得不使用其他特征检测器,比如 SIFT 和 SURF 等。原始文献推荐使用 CenSurE 特征检测器,这种算法很快。而且 BRIEF 算法对 CenSurE关键点的描述效果要比 SURF 关键点的描述更好。
简单来说 BRIEF 是一种对特征点描述符计算和匹配的快速方法。这种算法可以实现很高的识别率,除非出现平面内的大旋转。
35.1 OpenCV 中的 BRIEF
下面的代码使用了 CenSurE 特征检测器和 BRIEF 描述符。(在 OpenCV中 CenSurE 检测器被叫做 STAR 检测器)。
import numpy as np import cv2 from matplotlib import pyplot as plt img = cv2.imread(‘simple.jpg‘,0) # Initiate STAR detector star = cv2.FeatureDetector_create("STAR") # Initiate BRIEF extractor brief = cv2.DescriptorExtractor_create("BRIEF") # find the keypoints with STAR kp = star.detect(img,None) # compute the descriptors with BRIEF kp, des = brief.compute(img, kp) print brief.getInt(‘bytes‘) print des.shape
函数 brief.getInt( ′ bytes ′ ) 会以字节格式给出 n d 的大小,默认值为 32。下面就是匹配了,我们会在其他章节中介绍。
36 ORB (Oriented FAST and Rotated BRIEF)
目标
? 我们要学习 ORB 算法的基础
原理
对于一个 OpenCV 的狂热爱好者来说 ORB 最重要的一点就是:它来自“OpenCV_Labs‘‘。这个算法是在 2011 年提出的。在计算开支,匹配效率以及更主要的是专利问题方面 ORB 算法是是 SIFT 和 SURF 算法的一个很好的替代品。SIFT 和 SURF 算法是有专利保护的,如果你要使用它们,就可能要花钱。但是 ORB 不需要!!!
ORB 基本是 FAST 关键点检测和 BRIEF 关键点描述器的结合体,并通过很多修改增强了性能。首先它使用 FAST 找到关键点,然后再使用 Harris角点检测对这些关键点进行排序找到其中的前 N 个点。它也使用金字塔从而产生尺度不变性特征。但是有一个问题,FAST 算法步计算方向。那旋转不变性怎样解决呢?作者进行了如下修改。
它使用灰度矩的算法计算出角点的方向。以角点到角点所在(小块)区域质心的方向为向量的方向。为了进一步提高旋转不变性,要计算以角点为中心半径为 r 的圆形区域的矩,再根据矩计算除方向。
对于描述符,ORB 使用的是 BRIEF 描述符。但是我们已经知道 BRIEF对与旋转是不稳定的。所以我们在生成特征前,要把关键点领域的这个 patch的坐标轴旋转到关键点的方向。
For any feature set of n binary tests at location (x i , y i ), define a 2×n matrix, S which contains the coordinates of these pixels. Then using the orientation of patch,θ, its rotation matrix is found and rotates the S to get steered(rotated) version S θ . ORB discretize the angle to increments of2π/30(12 degrees), and construct a lookup table of precomputed BRIEF patterns. As long as the keypoint orientation \theta is consistent across views, the correct set of points S θ will be used to compute its descriptor.
BRIEF has an important property that each bit feature has a large variance and a mean near 0.5. But once it is oriented along keypoint direction, it loses this property and become more distributed. High variance makes a feature more discriminative, since it responds differentially to inputs. Another desirable property is to have the tests uncorrelated, since then each test will contribute to the result. To resolve all these, ORB runs a greedy search among all possible binary tests to find the ones that have both high variance and means close to 0.5, as well as being uncorrelated. The result is calledrBRIEF.
For descriptor matching, multi-probe LSH which improves on the traditional LSH, is used. The paper says ORB is much faster than SURF and SIFT and ORB descriptor works better than SURF. ORB isa good choice in low-power devices for panorama stitching etc.
实验证明,BRIEF 算法的每一位的均值接近 0.5,并且方差很大。steered_BRIEF算法的每一位的均值比较分散(均值为 0.5,0.45,0.35... 等值的关键点数相当),这导致方差减小。数据的方差大的一个好处是:使得特征更容易分辨。为了对 steered_BRIEF 算法使得特征的方差减小的弥补和减小数据间的相关性,用一个学习算法(learning method)选择二进制测试的一个子集。在描述符匹配中使用了对传统 LSH 改善后的多探针 LSH。文章中说 ORB算法比 SURF 和 SIFT 算法快的多,ORB 描述符也比 SURF 好很多。ORB是低功耗设备的最佳选择。
36.1 OpenCV 中的 ORB 算法
和前面一样我们首先要使用函数 cv3.ORB() 或者 feature2d 通用接口创建一个 ORB 对象。它有几个可选参数。最有用的应该是 nfeature,默认值为 500,它表示了要保留特征的最大数目。scoreType 设置使用 Harris打分还是使用 FAST 打分对特征进行排序(默认是使用 Harris 打分)等。参数 WTA_K 决定了产生每个 oriented_BRIEF 描述符要使用的像素点的数目。默认值是 2,也就是一次选择两个点。在这种情况下进行匹配,要使用NORM_HAMMING 距离。如果 WTA_K 被设置成 3 或 4,那匹配距离就要设置为 NORM_HAMMING2。
下面是一个使用 ORB 的简单代码。
import numpy as np import cv2 from matplotlib import pyplot as plt img = cv2.imread(‘simple.jpg‘,0) # Initiate STAR detector orb = cv2.ORB() # find the keypoints with ORB kp = orb.detect(img,None) # compute the descriptors with ORB kp, des = orb.compute(img, kp) # draw only keypoints location,not size and orientation img2 = cv2.drawKeypoints(img,kp,color=(0,255,0), flags=0) plt.imshow(img2),plt.show()
练习
37 特征匹配
目标
? 我们将要学习在图像间进行特征匹配
? 使用 OpenCV 中的蛮力(Brute-Force)匹配和 FLANN 匹配
37.1 Brute-Force 匹配的基础
蛮力匹配器是很简单的。首先在第一幅图像中选取一个关键点然后依次与第二幅图像的每个关键点进行(描述符)距离测试,最后返回距离最近的关键点。
对于 BF 匹配器,我们首先要使用 cv2.BFMatcher() 创建一个 BFMatcher 对象。它有两个可选参数。第一个是 normType。它是用来指定要使用的距离测试类型。默认值为 cv2.Norm_L2。这很适合 SIFT 和 SURF 等(c2.NORM_L1 也可以)。对于使用二进制描述符的 ORB,BRIEF,BRISK算法等,要使用 cv2.NORM_HAMMING,这样就会返回两个测试对象之间的汉明距离。如果 ORB 算法的参数设置为 V TA_K==3 或 4,normType就应该设置成 cv2.NORM_HAMMING2。
第二个参数是布尔变量 crossCheck,默认值为 False。如果设置为True,匹配条件就会更加严格,只有到 A 中的第 i 个特征点与 B 中的第 j 个特征点距离最近,并且 B 中的第 j 个特征点到 A 中的第 i 个特征点也是最近(A 中没有其他点到 j 的距离更近)时才会返回最佳匹配(i,j)。也就是这两个特征点要互相匹配才行。这样就能提供统一的结果,这可以用来替代 D.Lowe在 SIFT 文章中提出的比值测试方法。
BFMatcher 对象具有两个方法,BFMatcher.match() 和 BFMatcher.knnMatch()。
第一个方法会返回最佳匹配。第二个方法为每个关键点返回 k 个最佳匹配(降序排列之后取前 k 个),其中 k 是由用户设定的。如果除了匹配之外还要做其他事情的话可能会用上(比如进行比值测试)。
就像使用 cv2.drawKeypoints() 绘制关键点一样,我们可以使用cv2.drawMatches() 来绘制匹配的点。它会将这两幅图像先水平排列,然后在最佳匹配的点之间绘制直线(从原图像到目标图像)。如果前面使用的是 BFMatcher.knnMatch(),现在我们可以使用函数 cv2.drawMatchsKnn为每个关键点和它的 k 个最佳匹配点绘制匹配线。如果 k 等于 2,就会为每个关键点绘制两条最佳匹配直线。如果我们要选择性绘制话就要给函数传入一个掩模。
让我们分别看一个 ORB 和一个 SURF 的例子吧。(使用不同距离计算方法)。
37.2 对 对 ORB 描述符进行蛮力匹配
现在我们看一个在两幅图像之间进行特征匹配的简单例子。在本例中我们有一个查询图像和一个目标图像。我们要使用特征匹配的方法在目标图像中寻找查询图像的位置。(这两幅图像分别是/sample/c/box.png,和/sample/c/box_in_scene.png)
我们使用 ORB 描述符来进行特征匹配。首先我们需要加载图像计算描述符。
import numpy as np import cv2 from matplotlib import pyplot as plt img1 = cv2.imread(‘box.png‘,0) # queryImage img2 = cv2.imread(‘box_in_scene.png‘,0) # trainImage # Initiate SIFT detector orb = cv2.ORB() # find the keypoints and descriptors with SIFT kp1, des1 = orb.detectAndCompute(img1,None) kp2, des2 = orb.detectAndCompute(img2,None)
下面我们要创建一个 BFMatcher 对象,并将距离计算设置为 cv2.NORM_HAMMING(因为我们使用的是 ORB),并将 crossCheck 设置为 True。然后使用 Matcher.match()方法获得两幅图像的最佳匹配。然后将匹配结果按特征点之间的距离进行降序排列,这样最佳匹配就会排在前面了。最后我们只将前 10 个匹配绘制出来(太多了看不清,如果愿意的话你可以多画几条)。
# create BFMatcher object bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) # Match descriptors. matches = bf.match(des1,des2) # Sort them in the order of their distance. matches = sorted(matches, key = lambda x:x.distance) # Draw first 10 matches. img3 = cv2.drawMatches(img1,kp1,img2,kp2,matches[:10], flags=2) plt.imshow(img3),plt.show()
下面就是我得到的结果。
37.3 匹配器对象是什么?
matches = bf.match(des1,des2) 返回值是一个 DMatch 对象列表。这个DMatch 对象具有下列属性:
? DMatch.distance - 描述符之间的距离。越小越好。
? DMatch.trainIdx - 目标图像中描述符的索引。
? DMatch.queryIdx - 查询图像中描述符的索引。
? DMatch.imgIdx - 目标图像的索引。
37.4 对 对 SIFT 描述符进行蛮力匹配和比值测试
现在我们使用 BFMatcher.knnMatch() 来获得 k 对最佳匹配。在本例中我们设置 k = 2,这样我们就可以使用 D.Lowe 文章中的比值测试了。
import numpy as np import cv2 from matplotlib import pyplot as plt img1 = cv2.imread(‘box.png‘,0) # queryImage img2 = cv2.imread(‘box_in_scene.png‘,0) # trainImage # Initiate SIFT detector sift = cv2.SIFT() # find the keypoints and descriptors with SIFT kp1, des1 = sift.detectAndCompute(img1,None) kp2, des2 = sift.detectAndCompute(img2,None) # BFMatcher with default params bf = cv2.BFMatcher() matches = bf.knnMatch(des1,des2, k=2) # Apply ratio test good = [] for m,n in matches: if m.distance < 0.75*n.distance: good.append([m]) # cv2.drawMatchesKnn expects list of lists as matches. img3 = cv2.drawMatchesKnn(img1,kp1,img2,kp2,good,flags=2) plt.imshow(img3),plt.show()
结果如下:
37.5 FLANN 匹配器
FLANN 是快速最近邻搜索包(Fast_Library_for_Approximate_Nearest_Neighbors)的简称。它是一个对大数据集和高维特征进行最近邻搜索的算法的集合,而且这些算法都已经被优化过了。在面对大数据集时它的效果要好于 BFMatcher。
我们来对第二个例子使用 FLANN 匹配看看它的效果。
使用 FLANN 匹配,我们需要传入两个字典作为参数。这两个用来确定要使用的算法和其他相关参数等。第一个是 IndexParams。各种不同算法的信息可以在 FLANN 文档中找到。这里我们总结一下,对于 SIFT 和 SURF 等,我们可以传入的参数是:
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
但使用 ORB 时,我们要传入的参数如下。注释掉的值是文献中推荐使用的,但是它们并不适合所有情况,其他值的效果可能会更好。
index_params= dict(algorithm = FLANN_INDEX_LSH, table_number = 6, # 12 key_size = 12, # 20 multi_probe_level = 1) #2
第二个字典是 SearchParams。用它来指定递归遍历的次数。值越高结果越准确,但是消耗的时间也越多。如果你想修改这个值,传入参数:
search p arams = dict(checks = 100)。
有了这些信息我们就可以开始了。
import numpy as np import cv2 from matplotlib import pyplot as plt img1 = cv2.imread(‘box.png‘,0) # queryImage img2 = cv2.imread(‘box_in_scene.png‘,0) # trainImage # Initiate SIFT detector sift = cv2.SIFT() # find the keypoints and descriptors with SIFT kp1, des1 = sift.detectAndCompute(img1,None) kp2, des2 = sift.detectAndCompute(img2,None) # FLANN parameters FLANN_INDEX_KDTREE = 0 index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) search_params = dict(checks=50) # or pass empty dictionary flann = cv2.FlannBasedMatcher(index_params,search_params) matches = flann.knnMatch(des1,des2,k=2) # Need to draw only good matches, so create a mask matchesMask = [[0,0] for i in xrange(len(matches))] # ratio test as per Lowe‘s paper for i,(m,n) in enumerate(matches): if m.distance < 0.7*n.distance: matchesMask[i]=[1,0] draw_params = dict(matchColor = (0,255,0), singlePointColor = (255,0,0), matchesMask = matchesMask, flags = 0) img3 = cv2.drawMatchesKnn(img1,kp1,img2,kp2,matches,None,**draw_params) plt.imshow(img3,),plt.show()
结果如下:
38 使用特征匹配和单应性查找对象
目标
? 联合使用特征提取和 calib3d 模块中的 findHomography 在复杂图像中查找已知对象。
38.1 基础
还记得上一节我们做了什么吗?我们使用一个查询图像,在其中找到一些特征点(关键点),我们又在另一幅图像中也找到了一些特征点,最后对这两幅图像之间的特征点进行匹配。简单来说就是:我们在一张杂乱的图像中找到了一个对象(的某些部分)的位置。这些信息足以帮助我们在目标图像中准确的找到(查询图像)对象。
为了达到这个目的我们可以使用 calib3d 模块中的 cv2.findHomography()函数。如果将这两幅图像中的特征点集传给这个函数,他就会找到这个对象的透视图变换。然后我们就可以使用函数 cv2.perspectiveTransform() 找到这个对象了。至少要 4 个正确的点才能找到这种变换。
我们已经知道在匹配过程可能会有一些错误,而这些错误会影响最终结果。为了解决这个问题,算法使用 RANSAC 和 LEAST_MEDIAN(可以通过参数来设定)。所以好的匹配提供的正确的估计被称为 inliers,剩下的被称为outliers。cv2.findHomography() 返回一个掩模,这个掩模确定了 inlier 和outlier 点。
让我们来搞定它吧!!!
38.2 代码
和通常一样我们先在图像中来找到 SIFT 特征点,然后再使用比值测试找到最佳匹配。
import numpy as np import cv2 from matplotlib import pyplot as plt MIN_MATCH_COUNT = 10 img1 = cv2.imread(‘box.png‘,0) # queryImage img2 = cv2.imread(‘box_in_scene.png‘,0) # trainImage # Initiate SIFT detector sift = cv2.SIFT() # find the keypoints and descriptors with SIFT kp1, des1 = sift.detectAndCompute(img1,None) kp2, des2 = sift.detectAndCompute(img2,None) FLANN_INDEX_KDTREE = 0 index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) search_params = dict(checks = 50) flann = cv2.FlannBasedMatcher(index_params, search_params) matches = flann.knnMatch(des1,des2,k=2) # store all the good matches as per Lowe‘s ratio test. good = [] for m,n in matches: if m.distance < 0.7*n.distance: good.append(m)
现在我们设置只有存在 10 个以上匹配时才去查找目标(MIN_MATCH_COUNT=10),
否则显示警告消息:“现在匹配不足!”
如果找到了足够的匹配,我们要提取两幅图像中匹配点的坐标。把它们传入到函数中计算透视变换。一旦我们找到 3x3 的变换矩阵,就可以使用它将查询图像的四个顶点(四个角)变换到目标图像中去了。然后再绘制出来。
if len(good)>MIN_MATCH_COUNT: src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2) dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2) M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC,5.0) matchesMask = mask.ravel().tolist() h,w = img1.shape pts = np.float32([ [0,0],[0,h-1],[w-1,h-1],[w-1,0] ]).reshape(-1,1,2) dst = cv2.perspectiveTransform(pts,M) img2 = cv2.polylines(img2,[np.int32(dst)],True,255,3, cv2.LINE_AA) else: print "Not enough matches are found - %d/%d" % (len(good),MIN_MATCH_COUNT) matchesMask = None # Finally we draw our inliers (if successfully found the object) or matching keypoints (if failed). draw_params = dict(matchColor = (0,255,0), # draw matches in green color singlePointColor = None, matchesMask = matchesMask, # draw only inliers flags = 2) img3 = cv2.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params) plt.imshow(img3, ‘gray‘),plt.show()
结果如下。复杂图像中被找到的目标图像被标记成白色。