第十八节、图像描述符匹配算法、以及目标匹配
在前面的一些小节中,我们已经使用到的图像描述符匹配相关的函数,在OpenCV中主要提供了暴力匹配、以及FLANN匹配函数库。
一 暴力匹配以及优化(交叉匹配、KNN匹配)
暴力匹配即两两匹配。该算法不涉及优化,假设从图片A中提取了$m$个特征描述符,从B图片提取了$n$个特征描述符。对于A中$m$个特征描述符的任意一个都需要和B中的$n$个特征描述符进行比较。每次比较都会给出一个距离值,然后将得到的距离进行排序,取距离最近的一个作为匹配点。这种方法简单粗暴,其结果也是显而易见的,通过前几节的演示案例,我们知道有大量的错误匹配,这就需要使用一些机制来过滤掉错误的匹配。比如我们对匹配点按照距离来排序,并指定一个距离阈值,过滤掉一些匹配距离较远的点。
OpenCV专门提供了一个BFMatcher对象来实现匹配,并且针对匹配误差做了一些优化:
cv2.BFMatcher_create([,normType[,crossCheck]])
参数说明:
- normType:它是用来指定要使用的距离测试类型。默认值为cv2.Norm_L2。这很适合SIFT和SURF等(c2.NORM_L1也可)。对于使用二进制描述符的ORB、BRIEF和BRISK算法等,要使用cv2.NORM_HAMMING,这样就会返回两个测试对象之间的汉明距离。如果ORB算法的参数设置为WTA_K==3或4,normType就应该设置成cv2.NORM_HAMMING2。
- crossCheck:针对暴力匹配,可以使用交叉匹配的方法来过滤错误的匹配。默认值为False。如果设置为True,匹配条件就会更加严格,只有到A中的第$i$个特征点与B中的第$j$个特征点距离最近,并且B中的第$j$个特征点到A中的第$i$个特征点也是最近时才会返回最佳匹配$(i,j)$,即这两个特征点要互相匹配才行。
BFMatcher对象有两个方法BFMatcher.match()和BFMatcher.knnMatch()。
- 第一个方法会返回最佳匹配,上面我们说过,这种匹配效果会出现不少误差匹配点。我们使用cv2.drawMatches()来绘制匹配的点,它会将两幅图像先水平排列,然后在最佳匹配的点之间绘制直线。
- 第二个方法为每个关键点返回$k$个最佳匹配,其中$k$是由用户设定的。我们使用函数cv2.drawMatchsKnn为每个关键点和它的$k$个最佳匹配点绘制匹配线。如果要选择性绘制就要给函数传入一个掩模。
注意:$k$近邻匹配,在匹配的时候选择$k$个和特征点最相似的点,如果这$k$个点之间的区别足够大,则选择最相似的那个点作为匹配点,通常选择$k = 2$,也就是最近邻匹配。对每个匹配返回两个最近邻的匹配,如果第一匹配和第二匹配距离比率足够大(向量距离足够远),则认为这是一个正确的匹配,比率的阈值通常在2左右。
具体可以参考C++版本程序,博客SLAM入门之视觉里程计(1):特征点的匹配,python程序如下:
# -*- coding: utf-8 -*- """ Created on Fri Sep 14 14:02:44 2018 @author: zy """ ''' 特征描述符匹配算法 暴力匹配,KNN匹配,FLANN匹配 ''' import cv2 import numpy as np def match_test(): ''' 暴力匹配 KNN最近邻匹配 ''' img1 = cv2.imread('./image/orb1.jpg',0) img2 = cv2.imread('./image/orb2.jpg',0) img2 = cv2.resize(img2,dsize=(450,300)) ''' 1.使用SIFT算法检测特征点、描述符 ''' sift = cv2.xfeatures2d.SIFT_create(100) kp1, des1 = sift.detectAndCompute(img1,None) kp2, des2 = sift.detectAndCompute(img2,None) #在图像上绘制关键点 img1 = cv2.drawKeypoints(image=img1,keypoints = kp1,outImage=img1,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) img2 = cv2.drawKeypoints(image=img2,keypoints = kp2,outImage=img2,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #显示图像 #cv2.imshow('sift_keypoints1',img1) #cv2.imshow('sift_keypoints2',img2) cv2.waitKey(20) ''' 2、匹配 ''' bf = cv2.BFMatcher() knnMatches = bf.knnMatch(des1,des2, k=2) print(type(knnMatches),len(knnMatches),knnMatches[0]) #获取img1中的第一个描述符在img2中最匹配的一个描述符 距离最小 dMatch0 = knnMatches[0][0] #获取img1中的第一个描述符在img2中次匹配的一个描述符 距离次之 dMatch1 = knnMatches[0][1] print('knnMatches',dMatch0.distance,dMatch0.queryIdx,dMatch0.trainIdx) print('knnMatches',dMatch1.distance,dMatch1.queryIdx,dMatch1.trainIdx) #将不满足的最近邻的匹配之间距离比率大于设定的阈值匹配剔除。 goodMatches = [] minRatio = 1/3 for m,n in knnMatches: if m.distance / n.distance < minRatio: goodMatches.append([m]) print(len(goodMatches)) sorted(goodMatches,key=lambda x:x[0].distance) #绘制最优匹配点 outImg = None outImg = cv2.drawMatchesKnn(img1,kp1,img2,kp2,goodMatches,outImg,flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT) cv2.imshow('matche',outImg) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': match_test()
运行结果如下:
在程序中我们指定获取100个特征点,并且指定knn的参数k=2,也就是说A图中的一个特征描述符会在B图中找到两个对应的特征描述符,一个是最佳匹配,距离最小,另一次次之,我们在程序中输出了一组匹配结果:
bf = cv2.BFMatcher() knnMatches = bf.knnMatch(des1,des2, k=2) print(type(knnMatches),len(knnMatches),knnMatches[0]) #获取img1中的第一个描述符在img2中最匹配的一个描述符 距离最小 dMatch0 = knnMatches[0][0] #获取img1中的第一个描述符在img2中次匹配的一个描述符 距离次之 dMatch1 = knnMatches[0][1] print('knnMatches',dMatch0.distance,dMatch0.queryIdx,dMatch0.trainIdx) print('knnMatches',dMatch1.distance,dMatch1.queryIdx,dMatch1.trainIdx)
可以看到dMatch0和dMatch1是DMatch类型,这个类型主要包括以下几个属性:
DMatch.distance
- Distance between descriptors. The lower, the better it is.DMatch.trainIdx
- Index of the descriptor in train descriptors;训练描述符就是我们程序中的img2的描述符;DMatch.queryIdx
- Index of the descriptor in query descriptors;测试描述符就是我们程序中的img1的描述符;DMatch.imgIdx
- Index of the train image.
然后我们遍历每一组匹配结果,我们设置最小比率为$\frac{1}{3}$,过滤掉匹配距离较为相近的。最后只剩下21组,匹配效果如上图所示。我们可以误匹配明显少了很多,基本看不到误匹配点。(实际上,比率设置为0.7,大概就可以过滤掉90%的误匹配点)
二 FLANN匹配
FLANN英文全称Fast Libary for Approximate Nearest Neighbors,FLANN是一个执行最近邻搜索的库,官方网站http://www.cs.ubc.ca/research/flann。它包含一组算法,这些算法针对大型数据集中的快速最近邻搜索和高维特征进行了优化,对于大型数据集,它比BFMatcher工作得更快。经验证、FLANN比其他的最近邻搜索软件快10倍。
在GitHub上可以找到FLANN,网址为https://github.com/mariusmuja/flann。根据作者的经验,基于FLANN的匹配非常准确、快速、使用起来也很方便。
# -*- coding: utf-8 -*- """ Created on Fri Sep 14 14:02:44 2018 @author: zy """ ''' 特征描述符匹配算法 暴力匹配,KNN匹配,FLANN匹配 ''' import cv2 import numpy as np def flann_test(): ''' FLANN匹配 ''' #加载图片 灰色 img1 = cv2.imread('./image/orb1.jpg') gray1 = cv2.cvtColor(img1,cv2.COLOR_BGR2GRAY) img2 = cv2.imread('./image/orb2.jpg') img2 = cv2.resize(img2,dsize=(450,300)) gray2 = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) queryImage = gray1.copy() trainImage = gray2.copy() #创建SIFT对象 sift = cv2.xfeatures2d.SIFT_create(100) #SIFT对象会使用DoG检测关键点,并且对每个关键点周围的区域计算特征向量。该函数返回关键点的信息和描述符 keypoints1,descriptor1 = sift.detectAndCompute(queryImage,None) keypoints2,descriptor2 = sift.detectAndCompute(trainImage,None) print('descriptor1:',descriptor1.shape,'descriptor2',descriptor2.shape) #在图像上绘制关键点 queryImage = cv2.drawKeypoints(image=queryImage,keypoints = keypoints1,outImage=queryImage,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) trainImage = cv2.drawKeypoints(image=trainImage,keypoints = keypoints2,outImage=trainImage,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #显示图像 #cv2.imshow('sift_keypoints1',queryImage) #cv2.imshow('sift_keypoints2',trainImage) #cv2.waitKey(20) #FLANN匹配 FLANN_INDEX_KDTREE = 0 indexParams = dict(algorithm = FLANN_INDEX_KDTREE,trees = 5) searchParams = dict(checks = 50) flann = cv2.FlannBasedMatcher(indexParams,searchParams) matches = flann.knnMatch(descriptor1,descriptor2,k=2) print(type(matches),len(matches),matches[0]) #获取queryImage中的第一个描述符在trainingImage中最匹配的一个描述符 距离最小 dMatch0 = matches[0][0] #获取queryImage中的第一个描述符在trainingImage中次匹配的一个描述符 距离次之 dMatch1 = matches[0][1] print('knnMatches',dMatch0.distance,dMatch0.queryIdx,dMatch0.trainIdx) print('knnMatches',dMatch1.distance,dMatch1.queryIdx,dMatch1.trainIdx) #设置mask,过滤匹配点 作用和上面那个一样 matchesMask = [[0,0] for i in range(len(matches))] minRatio = 1/3 for i,(m,n) in enumerate(matches): if m.distance/n.distance < minRatio: matchesMask[i] = [1,0] #只绘制最优匹配点 drawParams = dict(#singlePointColor=(255,0,0),matchColor=(0,255,0), matchesMask = matchesMask, flags = 0) resultImage = cv2.drawMatchesKnn(queryImage,keypoints1,trainImage,keypoints2,matches, None,**drawParams) cv2.imshow('matche',resultImage) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': flann_test()
其中FLANN匹配对象接收两个参数:indexParams和searchParams。这两个参数在python中以字典形式进行参数传递(在C++中以结构体形式进行参数传递),为了计算匹配,FALNN内部会决定如何处理索引和搜索对象。
flann = cv2.FlannBasedMatcher(indexParams,searchParams)
1、indexParams
对于像SIFT,SURF等算法,您可以传递以下内容:
indexParams = dict(algorithm = FLANN_INDEX_KDTREE,trees = 5)
使用ORB时,您可以传递以下内容:
indexParams= dict(algorithm = FLANN_INDEX_LSH, table_number = 6, # 12 key_size = 12, # 20 multi_probe_level = 1) #2
参数algorithm用来指定匹配所使用的算法,可以选择的有LinearIndex、KTreeIndex、KMeansIndex、CompositeIndex和AutotuneIndex,这里选择的是KTreeIndex(使用kd树实现最近邻搜索)。KTreeIndex配置索引很简单(只需要指定待处理核密度树的数量,最理想的数量在1~16之间),并且KTreeIndex非常灵活(kd-trees可被并行处理)。
2、searchParams
SearchParams它指定索引数倍遍历的次数。 值越高,精度越高,但也需要更多时间。 如果要更改该值,请传递:
searchParams = dict(checks = 50)
实际上、匹配效果很大程度上取决于输入。5 kd-trees和50 checks总能取得具有合理精度的结果,而且能够在很短的时间内完成匹配。
程序中我们通过matchesMask来设置绘制时需要显示的匹配线,由于我们设置$k=2$,对于A图中的一个特征描述符$a1$,对应在B图中两个特征描述符$b1,b2$,Mask有4中设置结果:
- [0,0]:屏蔽掉所有特征点连线;
- [1,0]:显示$a1$和$b1$的连线;
- [0,1]:显示$a1$和$b2$的连线;
- [1,1]:显示$a1$和$b1$的以及$a1$和$b2$的连线;
如果我们设置$k$为其它的数,那么上面对应的Mask也会改变,list的长度和$k$的长度一样。
三 FLANN的单应性匹配
上面我们已经介绍到,在图像queryImage中找到了一些特征点,在另一个图像trainImage找到了该图像中的特征点,我们发现它们之间的最佳匹配。我们可以利用这些匹配点来查找图像queryImage到图像trainImage的映射变换。为此,我们可以使用来自calib3d模块的函数,即cv2.findHomography()。如果我们从两个图像中传递几组匹配点(它需要至少四组正确的匹配点来找到转换),它将找到该对象的相应变换,即单应性矩阵。然后我们可以使用cv2.perspectiveTransform()来查找对象。
但是我们如何保证传入的匹配点都是正确的呢?在之前我们已经看到匹配时可能存在一些可能的错误,这可能会影响结果。为了解决这个问题,算法使用RANSAC或LEAST_MEDIAN(由标志决定)。cv2.findHomography()该函数返回一个$3\times{3}$的单应性变换矩阵和一个mask,该mask是ndarray类型,长度为匹配点的对数,每一个元素表示我们在计算单应性变换时是否使用到当前索引所对应的匹配点,换句话说如果当前索引处值为0,表示该匹配点是误匹配,我们抛弃它,如果是1,表示是正确匹配,可以用来计算单应性变换矩阵。
# -*- coding: utf-8 -*- """ Created on Sat Sep 15 13:22:10 2018 @author: zy """ ''' 单应性匹配 ''' import numpy as np import cv2 def flann_hom_test(): #加载图像 img1 = cv2.imread('./image/book1.jpg') #queryImage img2 = cv2.imread('./image/book2.jpg') #trainImage gray1 = cv2.cvtColor(img1,cv2.COLOR_BGR2GRAY) gray2 = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) #img2 = cv2.resize(img2,dsize=(450,600)) MIN_MATCH_COUNT = 10 ''' 1.使用SIFT算法检测特征点、描述符 ''' sift = cv2.xfeatures2d.SIFT_create(100) kp1, des1 = sift.detectAndCompute(gray1,None) kp2, des2 = sift.detectAndCompute(gray2,None) #在图像上绘制关键点 #img1 = cv2.drawKeypoints(image=img1,keypoints = kp1,outImage=img1,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #img2 = cv2.drawKeypoints(image=img2,keypoints = kp2,outImage=img2,color=(255,0,255),flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) #显示图像 #cv2.imshow('sift_keypoints1',img1) #cv2.imshow('sift_keypoints2',img2) #cv2.waitKey(20) ''' 2、FLANN匹配 ''' FLANN_INDEX_KDTREE = 0 indexParams = dict(algorithm = FLANN_INDEX_KDTREE,trees = 5) searchParams = dict(checks = 50) flann = cv2.FlannBasedMatcher(indexParams,searchParams) matches = flann.knnMatch(des1,des2,k=2) #将不满足的最近邻的匹配之间距离比率大于设定的阈值匹配剔除。 goodMatches = [] minRatio = 0.7 for m,n in matches: if m.distance / n.distance < minRatio: goodMatches.append(m) #注意 如果使用drawMatches 则不用写成List类型[m] ''' 3、单应性变换 ''' #确保至少有一定数目的良好匹配(理论上,计算单应性至少需要4对匹配点,实际上会使用10对以上的匹配点) if len(goodMatches) > MIN_MATCH_COUNT: #获取匹配点坐标 src_pts = np.float32([kp1[m.queryIdx].pt for m in goodMatches]).reshape(-1,2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in goodMatches]).reshape(-1,2) print('src_pts:',len(src_pts),src_pts[0]) print('dst_pts:',len(dst_pts),dst_pts[0]) #获取单应性:即一个平面到另一个平面的映射矩阵 M,mask = cv2.findHomography(src_pts,dst_pts,cv2.RANSAC,5.0) #print('M:',M,type(M)) #<class 'numpy.ndarray'> [3,3] matchesMask = mask.ravel().tolist() #用来配置匹配图,只绘制单应性图片中关键点的匹配线 #由于使用的是drawMatches绘制匹配线,这里list #每个元素也是一个标量,并不是一个list print('matchesMask:',len(matchesMask),matchesMask[0]) #计算原始图像img1中书的四个顶点的投影畸变,并在目标图像img2上绘制边框 h,w = img1.shape[:2] #源图片中书的的四个角点 pts = np.float32([[55,74],[695,45],[727,464],[102,548]]).reshape(-1,1,2) print('pts:',pts.shape) dst = cv2.perspectiveTransform(pts,M) print('dst:',dst.shape) #在img2上绘制边框 img2 = cv2.polylines(img2,[np.int32(dst)],True,(0,255,0),2,cv2.LINE_AA) else: print("Not enough matches are found - %d/%d" % (len(goodMatches),MIN_MATCH_COUNT)) matchesMask = None ''' 绘制显示效果 ''' 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,goodMatches,None,**draw_params) cv2.imshow('matche',img3) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': flann_hom_test()
程序中我们指定了至少需要10对良好匹配点,并且计算除了从图A中的书变换到图B中书的单应性矩阵,我们利用该矩阵就可以计算出图A中书在图B中的位置。程序运行效果如下:
四 代码下载
参考文章:
[3]OpenCV-Python Tutorials Feature Detection and Description