圖像配準:基于 OpenCV 的高效實現(xiàn)
在這篇文章中,我將對圖像配準進行一個簡單概述,展示一個最小的 OpenCV 實現(xiàn),并展示一個可以使配準過程更加高效的簡單技巧。
什么是圖像配準
圖像配準被定義為將不同成像設(shè)備或傳感器在不同時間和角度拍攝的兩幅或多幅圖像,或來自同一場景的兩幅或多幅圖像疊加起來,以幾何方式對齊圖像以進行分析的過程(Zitová 和 Flusser,2003 年)。
百度百科給出的解釋
圖像配準:圖像配準(Image registration)就是將不同時間、不同傳感器(成像設(shè)備)或不同條件下(天候、照度、攝像位置和角度等)獲取的兩幅或多幅圖像進行匹配、疊加的過程,它已經(jīng)被廣泛地應(yīng)用于遙感數(shù)據(jù)分析、計算機視覺、圖像處理等領(lǐng)域。
醫(yī)學科學、遙感和計算機視覺都使用圖像配準。
有兩種主要方法:
- 經(jīng)典計算機視覺方法(使用 OpenCV)——我們將在本文中關(guān)注的內(nèi)容
- 基于深度學習的方法
雖然后者可以更好地工作,但它可能需要一些“域”適應(yīng)(在你的數(shù)據(jù)上微調(diào)神經(jīng)網(wǎng)絡(luò))并且可能計算量太大。
使用 OpenCV 進行圖像配準
基于特征的方法:由單應(yīng)變換關(guān)聯(lián)的圖像對
此操作試圖發(fā)現(xiàn)兩張照片之間的匹配區(qū)域并在空間上對齊它們以最大限度地減少錯誤。
我們的目標是找到一個單應(yīng)性矩陣 H,它告訴我們需要如何修改其中一張圖像,使其與另一張圖像完美對齊。
第 1 步:關(guān)鍵點檢測
關(guān)鍵點定義了圖像中一個獨特的小區(qū)域(角、邊緣、圖案)。關(guān)鍵點檢測器的一個重要方面是找到的區(qū)域應(yīng)該對圖像變換(例如定位、比例和亮度)具有魯棒性,因為這些區(qū)域很可能出現(xiàn)在我們試圖對齊的兩個圖像中。有許多執(zhí)行關(guān)鍵點檢測的算法,例如 SIFT、ORB、AKAZE、SURF 等。
第 2 步:特征匹配
現(xiàn)在我們必須匹配來自兩個圖像的關(guān)鍵點,這些關(guān)鍵點實際上對應(yīng)于同一點。
第 3 步:單應(yīng)性
單應(yīng)性通常由一個 3x3 矩陣表示,它描述了應(yīng)該應(yīng)用于一個圖像以與另一個圖像對齊的幾何變換。
第 4 步:圖像變形
找到單應(yīng)性矩陣后,我們可以用它來對齊圖像。下面是該過程的代碼:
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
img1 = cv.imread('image1.jpg', cv.IMREAD_GRAYSCALE) # referenceImage
img2 = cv.imread('image2.jpg', cv.IMREAD_GRAYSCALE) # sensedImage
# Initiate SIFT detector
sift_detector = cv.SIFT_create()
# Find the keypoints and descriptors with SIFT
kp1, des1 = sift_detector.detectAndCompute(img1, None)
kp2, des2 = sift_detector.detectAndCompute(img2, None)
# BFMatcher with default params
bf = cv.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Filter out poor matches
good_matches = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good_matches.append(m)
matches = good_matches
points1 = np.zeros((len(matches), 2), dtype=np.float32)
points2 = np.zeros((len(matches), 2), dtype=np.float32)
for i, match in enumerate(matches):
points1[i, :] = kp1[match.queryIdx].pt
points2[i, :] = kp2[match.trainIdx].pt
# Find homography
H, mask = cv2.findHomography(points1, points2, cv2.RANSAC)
# Warp image 1 to align with image 2
img1Reg = cv2.warpPerspective(img1, H, (img2.shape[1], img2.shape[0]))
cv.imwrite('aligned_img1.jpg', img1Reg)The problem is that this matrix H is found via a compute-intensive optimization process.
高效的圖像配準
無論您為每個步驟選擇的參數(shù)如何,對執(zhí)行時間影響最大的是圖像的分辨率。您可以大幅調(diào)整它們的大小,但如果您需要對齊的圖像具有原始分辨率,會發(fā)生什么情況?
幸運的是,有辦法解決這個問題。事實證明,您可以計算低分辨率圖像的變換,然后調(diào)整此變換以適用于全分辨率圖像。
詳細步驟:
- 調(diào)整圖像大小
- 在低分辨率圖像上計算矩陣 H
- 變換矩陣 H 使其適用于全分辨率圖像
- 將新矩陣應(yīng)用于原始圖像。
第 3 步可能是這里最不明顯的部分,所以讓我們看看它是如何工作的:
我們想要調(diào)整在低分辨率圖像上計算的變換以適用于高分辨率圖像。因此,我們希望高分辨率圖像中的每個像素執(zhí)行以下操作:
幸運的是,所有這些步驟都只是矩陣乘法,我們可以將所有這些步驟組合在一個單一的轉(zhuǎn)換中。
設(shè) H 為您計算出的變換。您可以將 H 乘以另一個單應(yīng)性 A,得到 AH = H',其中 H' 是進行兩種變換的單應(yīng)性,相當于先應(yīng)用 H,然后應(yīng)用 A。
下面是詳細代碼:
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
img1 = cv.imread('image1.jpg', cv.IMREAD_GRAYSCALE) # referenceImage
img2 = cv.imread('image2.jpg', cv.IMREAD_GRAYSCALE) # sensedImage
# Resize the image by a factor of 8 on each side. If your images are
# very high-resolution, you can try to resize even more, but if they are
# already small you should set this to something less agressive.
resize_factor = 1.0/8.0
img1_rs = cv.resize(img1, (0,0), fx=resize_factor, fy=resize_factor)
img2_rs = cv.resize(img2, (0,0), fx=resize_factor, fy=resize_factor)
# Initiate SIFT detector
sift_detector = cv.SIFT_create()
# Find the keypoints and descriptors with SIFT on the lower resolution images
kp1, des1 = sift_detector.detectAndCompute(img1_rs, None)
kp2, des2 = sift_detector.detectAndCompute(img2_rs, None)
# BFMatcher with default params
bf = cv.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Filter out poor matches
good_matches = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good_matches.append(m)
matches = good_matches
points1 = np.zeros((len(matches), 2), dtype=np.float32)
points2 = np.zeros((len(matches), 2), dtype=np.float32)
for i, match in enumerate(matches):
points1[i, :] = kp1[match.queryIdx].pt
points2[i, :] = kp2[match.trainIdx].pt
# Find homography
H, mask = cv2.findHomography(points1, points2, cv2.RANSAC)
# Get low-res and high-res sizes
low_height, low_width = img1_rs.shape
height, width = img1.shape
low_size = np.float32([[0, 0], [0, low_height], [low_width, low_height], [low_width, 0]])
high_size = np.float32([[0, 0], [0, height], [width, height], [width, 0]])
# Compute scaling transformations
scale_up = cv.getPerspectiveTransform(low_size, high_size)
scale_down = cv.getPerspectiveTransform(high_size, low_size)
# Combine the transformations. Remember that the order of the transformation
# is reversed when doing matrix multiplication
# so this is actualy scale_down -> H -> scale_up
h_and_scale_up = np.matmul(scale_up, H)
scale_down_h_scale_up = np.matmul(h_and_scale_up, scale_down)
# Warp image 1 to align with image 2
img1Reg = cv2.warpPerspective(
img1,
scale_down_h_scale_up,
(img2.shape[1], img2.shape[0])
)
cv.imwrite('aligned_img1.jpg', img1Reg)