第十七節、圖像描述符匹配算法、以及目標匹配


在前面的一些小節中,我們已經使用到的圖像描述符匹配相關的函數,在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中的位置。程序運行效果如下:

 

參考文章:

[1]SLAM入門之視覺里程計(1):特征點的匹配

[2]近似最近鄰搜索方法FLANN簡介

[3]OpenCV-Python Tutorials Feature Detection and Description

[4]Feature Matching + Homography to find Objects

[5]Homography Examples using OpenCV ( Python / C ++ )


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM