1. 程式人生 > 實用技巧 >使用OpenCV進行影象全景拼接

使用OpenCV進行影象全景拼接

影象拼接是計算機視覺中最成功的應用之一。如今,很難找到不包含此功能的手機或影象處理API。在本文中,我們將討論如何使用Python和OpenCV進行影象拼接。也就是,給定兩張共享某些公共區域的影象,目標是“縫合”它們並建立一個全景影象場景。當然也可以是給定多張影象,但是總會轉換成兩張共享某些公共區域影象拼接的問題,因此本文以最簡單的形式進行介紹。

本文主要的知識點包含一下內容:

  • 關鍵點檢測

  • 區域性不變描述符(SIFT,SURF等)

  • 特徵匹配

  • 使用RANSAC進行單應性估計

  • 透視變換

我們需要拼接的兩張影象如下:

特徵檢測與提取

給定上述一對影象,我們希望將它們縫合以建立全景場景。重要的是要注意,兩個影象都需要有一些公共區域。當然,我們上面給出的兩張影象時比較理想的,有時候兩個影象雖然具有公共區域,但是同樣還可能存在縮放、旋轉、來自不同相機等因素的影響。但是無論哪種情況,我們都需要檢測影象中的特徵點。

關鍵點檢測

最初的並且可能是幼稚的方法是使用諸如Harris Corners之類的演算法來提取關鍵點。然後,我們可以嘗試基於某種相似性度量(例如歐幾里得距離)來匹配相應的關鍵點。眾所周知,角點具有一個不錯的特性:角點不變。這意味著,一旦檢測到角點,即使旋轉影象,該角點仍將存在。

但是,如果我們旋轉然後縮放影象怎麼辦?在這種情況下,我們會很困難,因為角點的大小不變。也就是說,如果我們放大影象,先前檢測到的角可能會變成一條線!

總而言之,我們需要旋轉和縮放不變的特徵。那就是更強大的方法(如SIFT,SURF和ORB)。

關鍵點和描述符

諸如SIFT和SURF之類的方法試圖解決角點檢測演算法的侷限性。通常,角點檢測器演算法使用固定大小的核心來檢測影象上的感興趣區域(角)。不難看出,當我們縮放影象時,該核心可能變得太小或太大。為了解決此限制,諸如SIFT之類的方法使用高斯差分(DoD)。想法是將DoD應用於同一影象的不同縮放版本。它還使用相鄰畫素資訊來查詢和完善關鍵點和相應的描述符。

首先,我們需要載入2個影象,一個查詢影象和一個訓練影象。最初,我們首先從兩者中提取關鍵點和描述符。通過使用OpenCV detectAndCompute()函式,我們可以一步完成它。請注意,為了使用detectAndCompute(),我們需要一個關鍵點檢測器和描述符物件的例項。它可以是ORB,SIFT或SURF等。此外,在將影象輸入給detectAndCompute()之前,我們將其轉換為灰度。

def detectAndDescribe(image, method=None):    """    Compute key points and feature descriptors using an specific method
"""
assert method is not None, "You need to define a feature detection method. Values are: 'sift', 'surf'"
# detect and extract features from the image if method == 'sift': descriptor = cv2.xfeatures2d.SIFT_create() elif method == 'surf': descriptor = cv2.xfeatures2d.SURF_create() elif method == 'brisk': descriptor = cv2.BRISK_create() elif method == 'orb': descriptor = cv2.ORB_create()
# get keypoints and descriptors (kps, features) = descriptor.detectAndCompute(image, None)
return (kps, features)

我們為兩個影象都設定了一組關鍵點和描述符。如果我們使用SIFT作為特徵提取器,它將為每個關鍵點返回一個128維特徵向量。如果選擇SURF,我們將獲得64維特徵向量。下圖顯示了使用SIFT,SURF,BRISK和ORB得到的結果。

使用ORB和漢明距離檢測關鍵點和描述符

使用SIFT檢測關鍵點和描述符

使用SURF檢測關鍵點和描述符

使用BRISK和漢明距離檢測關鍵點和描述符

特徵匹配

如我們所見,兩個影象都有大量特徵點。現在,我們想比較兩組特徵,並儘可能顯示更多相似性的特徵點對。使用OpenCV,特徵點匹配需要Matcher物件。在這裡,我們探索兩種方式:暴力匹配器(BruteForce)和KNN(k最近鄰)。

BruteForce(BF)Matcher的作用恰如其名。給定2組特徵(來自影象A和影象B),將A組的每個特徵與B組的所有特徵進行比較。預設情況下,BF Matcher計算兩點之間的歐式距離。因此,對於集合A中的每個特徵,它都會返回集合B中最接近的特徵。對於SIFT和SURF,OpenCV建議使用歐幾里得距離。對於ORB和BRISK等其他特徵提取器,建議使用漢明距離。我們要使用OpenCV建立BruteForce Matcher,一般情況下,我們只需要指定2個引數即可。第一個是距離度量。第二個是是否進行交叉檢測的布林引數。具體程式碼如下:

def createMatcher(method,crossCheck):    "Create and return a Matcher Object"
if method == 'sift' or method == 'surf': bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=crossCheck) elif method == 'orb' or method == 'brisk': bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=crossCheck) return bf

交叉檢查布林引數表示這兩個特徵是否具有相互匹配才視為有效。換句話說,對於被認為有效的一對特徵(f1,f2),f1需要匹配f2,f2也必須匹配f1作為最接近的匹配。此過程可確保提供更強大的匹配功能集,這在原始SIFT論文中進行了描述。

但是,對於要考慮多個候選匹配的情況,可以使用基於KNN的匹配過程。KNN不會返回給定特徵的單個最佳匹配,而是返回k個最佳匹配。需要注意的是,k的值必須由使用者預先定義。如我們所料,KNN提供了更多的候選功能。但是,在進一步操作之前,我們需要確保所有這些匹配對都具有魯棒性。

比率測試

為了確保KNN返回的特徵具有很好的可比性,SIFT論文的作者提出了一種稱為比率測試的技術。一般情況下,我們遍歷KNN得到匹配對,之後再執行距離測試。對於每對特徵(f1,f2),如果f1和f2之間的距離在一定比例之內,則將其保留,否則將其丟棄。同樣,必須手動選擇比率值。

本質上,比率測試與BruteForce Matcher的交叉檢查選項具有相同的作用。兩者都確保一對檢測到的特徵確實足夠接近以至於被認為是相似的。下面2個圖顯示了BF和KNN Matcher在SIFT特徵上的匹配結果。我們選擇僅顯示100個匹配點以清晰顯示。

使用KNN和SIFT的定量測試進行功能匹配

在SIFT特徵上使用暴力匹配器進行特徵匹配

需要注意的是,即使做了多種篩選來保證匹配的正確性,也無法完全保證特徵點完全正確匹配。儘管如此,Matcher演算法仍將為我們提供兩幅影象中最佳(更相似)的特徵集。接下來,我們利用這些點來計算將兩個影象的匹配點拼接在一起的變換矩陣。

這種變換稱為單應矩陣。簡而言之,單應性是一個3x3矩陣,可用於許多應用中,例如相機姿態估計,透視校正和影象拼接。它將點從一個平面(影象)對映到另一平面。

估計單應性

隨機取樣一致性(RANSAC)是用於擬合線性模型的迭代演算法。與其他線性迴歸器不同,RANSAC被設計為對異常值具有魯棒性。

像線性迴歸這樣的模型使用最小二乘估計將最佳模型擬合到資料。但是,普通最小二乘法對異常值非常敏感。如果異常值數量很大,則可能會失敗。RANSAC通過僅使用資料中的一組資料估計引數來解決此問題。下圖顯示了線性迴歸和RANSAC之間的比較。需要注意資料集包含相當多的離群值。

我們可以看到線性迴歸模型很容易受到異常值的影響。那是因為它試圖減少平均誤差。因此,它傾向於支援使所有資料點到模型本身的總距離最小的模型。包括異常值。相反,RANSAC僅將模型擬合為被識別為點的點的子集。

這個特性對我們的用例非常重要。在這裡,我們將使用RANSAC來估計單應矩陣。事實證明,單應矩陣對我們傳遞給它的資料質量非常敏感。因此,重要的是要有一種演算法(RANSAC),該演算法可以從不屬於資料分佈的點中篩選出明顯屬於資料分佈的點。

估計了單應矩陣後,我們需要將其中一張影象變換到一個公共平面上。在這裡,我們將對其中一張影象應用透視變換。透視變換可以組合一個或多個操作,例如旋轉,縮放,平移或剪下。我們可以使用OpenCVwarpPerspective()函式。它以影象和單應矩陣作為輸入。

# Apply panorama correctionwidth = trainImg.shape[1] + queryImg.shape[1]height = trainImg.shape[0] + queryImg.shape[0]
result = cv2.warpPerspective(trainImg, H, (width, height))result[0:queryImg.shape[0], 0:queryImg.shape[1]] = queryImg
plt.figure(figsize=(20,10))plt.imshow(result)
plt.axis('off')plt.show()

生成的全景影象如下所示。如我們所見,結果中包含了兩個影象中的內容。另外,我們可以看到一些與照明條件和影象邊界邊緣效應有關的問題。理想情況下,我們可以執行一些處理技術來標準化亮度,例如直方圖匹配,這會使結果看起來更真實和自然一些。

import java.io.Serializable; www.javachenglei.comimport java.util.Arrays; import org.www.leguojizc.cn springframework.util.Assert; import org.springframework.util.StringUtils; public class SimpleKey implements Serializable { public static final SimpleKey EMPTY = new SimpleKey(new Object[0]); private final Object[] params; private final int hashCode; public SimpleKey(www.jintianxuesha.com Object... elements) { Assert.notNull(elements, www.tengyao3zc.cn"Elements must not be null"); this.params = new Object[elements.length]; System.arraycopy(elements, 0, this.params, 0, elements.length); this.hashCode = Arrays.deepHashCode(www.tyyleapp.comthis.params);

歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、感測器、自動駕駛、計算攝影、檢測、分割、識別、醫學影像、GAN、演算法競賽等微信群(以後會逐漸細分),請掃描下面微訊號加群,備註:”暱稱+學校/公司+研究方向“,例如:”張三+上海交大+視覺SLAM“。請按照格式備註,否則不予通過。新增成功後會根據研究方向邀請進入相關微信群。請勿在群內傳送廣告,否則會請出群,謝謝理解~