閱讀翻譯Hugging Face Community Computer Vision Course之Feature Matching (特徵匹配)
關於
- 首次發表日期:2024-07-14
- 原文連結: https://huggingface.co/learn/computer-vision-course/en/unit1/feature-extraction/feature-matching
- 使用ChatGPT和KIMI機翻,人工潤色
- 完整的程式碼執行示例
如何將一幅影像中的檢測到的特徵與另一幅影像中的特徵進行匹配?特徵匹配涉及比較不同影像中的關鍵屬性以找到相似之處。特徵匹配在許多計算機視覺應用中非常有用,包括場景理解、影像拼接、物件跟蹤和模式識別。
Brute-Force Search (暴力搜尋)
想象你有一個巨大的拼圖盒子,你試圖找到一塊特定的拼圖來完成你的拼圖。這就類似於在影像中尋找匹配的特徵。沒有任何特殊策略,你決定逐一檢查每一塊拼圖,直到找到正確的那一塊。這種簡單直接的方法就是暴力搜尋(brute-force search)。暴力搜尋的優點在於它的簡單性。你不需要任何特殊的技巧,只需要耐心。然而,它可能非常耗時,尤其是當需要檢查的拼圖很多時。在特徵匹配的背景下,這種暴力搜尋方法類似於將一幅影像中的每個畫素與另一幅影像中的每個畫素進行比較,看它們是否匹配。這是窮盡的,並且可能需要很多時間,特別是對於大影像。
既然我們對如何找到暴力匹配(brute-force matches)有了直觀的理解,讓我們深入研究演算法。我們將使用我們在前一章中學到的描述符來找到兩張影像中匹配的特徵。
首先安裝並載入庫。
pip install opencv-python
import cv2
import numpy as np
Brute Force with SIFT (使用 SIFT 的暴力匹配)
讓我們從初始化SIFT檢測器開始。
sift = cv2.SIFT_create()
現在我們來下載一對影像。
import io
import requests
def download_image(url: str, filename: str = "") -> str:
filename = url.split("/")[-1] if len(filename) == 0 else filename
# Download
bytesio = io.BytesIO(requests.get(url).content)
# Save file
with open(filename, "wb") as outfile:
outfile.write(bytesio.getbuffer())
return filename
url_a = "https://github.com/kornia/data/raw/main/matching/kn_church-2.jpg"
url_b = "https://github.com/kornia/data/raw/main/matching/kn_church-8.jpg"
download_image(url_a)
download_image(url_b)
fname1 = "kn_church-2.jpg"
fname2 = "kn_church-8.jpg"
img1 = cv2.imread(fname1)
img2 = cv2.imread(fname2)
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
使用SIFT找到關鍵點和描述符。
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
使用 k 近鄰演算法找到匹配。
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
應用比率測試(ratio test)來對最佳匹配項進行閾值化處理。
good = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good.append([m])
繪製匹配項。
img3 = cv2.drawMatchesKnn(
img1, kp1, img2, kp2, good, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
plt.imshow(img3)
Brute Force with ORB (binary) descriptors (使用ORB(二進位制)描述符的暴力搜尋)
初始化ORB描述符。
orb = cv2.ORB_create()
找到關鍵點和描述符。
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
由於ORB是一個二進位制描述符,我們使用漢明距離來找到匹配項,漢明距離是衡量兩個等長字串之間差異的一種方法。
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
我們現在將找到匹配項。
matches = bf.match(des1, des2)
我們可以按照它們距離的順序進行排序,如下所示。
matches = sorted(matches, key=lambda x: x.distance)
繪製前n個匹配項。
n = 15
img3 = cv2.drawMatches(
img1,
kp1,
img2,
kp2,
matches[:n],
None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
plt.imshow(img3)
快速近似最近鄰庫(FLANN)
FLANN(Fast Library for Approximate Nearest Neighbors)在Muja和Lowe的論文《Fast Approximate Nearest Neighbors With Automatic Algorithm Configuration》中提出。為了說明FLANN的工作原理,我們繼續使用拼圖解決的例子。想象一個巨大的拼圖,幾百塊拼圖散落在四周。你的目標是根據拼圖塊的匹配程度將這些拼圖組織起來。與其隨機嘗試匹配拼圖,FLANN使用一些巧妙的技巧來快速找出最可能匹配的拼圖塊。它不是將每一塊拼圖與其他每一塊拼圖進行比較,而是透過找到大致相似的拼圖塊來簡化過程。這意味著即使它們不是完全匹配,FLANN也可以對哪些碎片可能很好地拼合在一起做出有根據的猜測。
FLANN的內部機制使用了稱為k-D樹的結構。可以把它看作是以一種特殊的方式組織拼圖塊。FLANN不是檢查每一塊拼圖與其他所有的拼圖,而是將它們安排在一個類似樹的結構中,這使得找到匹配項更快。
在k-D樹的每個節點中,FLANN將具有相似特徵的拼圖塊放在一起。這就像將形狀或顏色相似的拼圖塊分到一堆。這樣,當你尋找匹配時,可以快速檢查最有可能具有相似拼圖塊的那一堆。假設你在尋找一塊“天空”拼圖,FLANN會引導你到k-D樹中分類為天空顏色的拼圖塊所在的位置,而不是搜尋所有的拼圖塊。
FLANN還會根據拼圖塊的特徵調整其策略。如果拼圖有很多顏色,它會重點關注顏色特徵。或者,如果拼圖具有複雜的形狀,它會關注這些形狀。透過在尋找匹配特徵時平衡速度和準確性,FLANN大大提高了查詢時間。
首先,我們建立一個字典來指定我們將要使用的演算法,對於SIFT或SURF,它看起來像下面這樣。
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
對於FLANN,我們將使用論文中的引數。
FLANN_INDEX_LSH = 6
index_params = dict(
algorithm=FLANN_INDEX_LSH, table_number=12, key_size=20, multi_probe_level=2
)
我們還建立了一個字典來指定要訪問的最大葉子節點數量,如下所示。
search_params = dict(checks=50)
初始化SIFT檢測器
sift = cv2.SIFT_create()
使用SIFT找到關鍵點和描述符。
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
我們現在將定義FLANN引數。在這裡,trees
是你想要劃分的箱子數量。
FLANN_INDEX_KDTREE = 1
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)
我們將只繪製好的匹配項,所以建立一個掩碼。
matchesMask = [[0, 0] for i in range(len(matches))]
我們可以執行一個比率測試來確定好的匹配項。
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=cv2.DrawMatchesFlags_DEFAULT,
)
img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matches, None, **draw_params)
Local Feature Matching with Transformers (LoFTR)
LoFTR是由Sun等人在《LoFTR: Detector-Free Local Feature Matching with Transformers》中提出的。LoFTR不使用特徵檢測器,而是採用基於學習的方法來進行特徵匹配。
讓我們保持簡單,並再次使用我們的拼圖示例。LoFTR 並不是簡單地逐畫素比較影像,而是尋找每幅影像中的特定關鍵點或特徵。這就像識別每塊拼圖的角落和邊緣。就像一個非常擅長拼圖的人可能會關注獨特的標記一樣,LoFTR 識別影像中的這些獨特的點。這些可能是顯著的地標或結構。
正如我們已經學到的,匹配演算法需要處理旋轉或縮放的變化。如果一個特徵被旋轉或縮放,LoFTR 仍然能夠識別它。這就像拼圖時拼圖塊可能被翻轉或調整一樣。LoFTR 在匹配特徵時,會分配一個相似度分數來表示特徵對齊的好壞。分數越高,匹配程度越高。這就像是給一個拼圖塊與另一個拼圖塊的契合度打分。
LoFTR 對某些變換具有不變性,這意味著它可以處理光照、角度或視角的變化。這在處理可能在不同條件下拍攝的影像時至關重要。LoFTR 穩健地匹配特徵的能力使其在像影像拼接這樣的任務中非常有價值,在影像拼接任務中,你透過識別和連線共同特徵將多張影像無縫地組合在一起。
我們可以使用 Kornia 來利用LoFTR在兩張影像中找到匹配的特徵。
pip install kornia
pip install kornia-rs
pip install kornia_moons
pip install opencv-python --upgrade
匯入必要的庫。
import cv2
import kornia as K
import kornia.feature as KF
import matplotlib.pyplot as plt
import numpy as np
import torch
from kornia_moons.viz import draw_LAF_matches
載入並調整影像大小。
from kornia.feature import LoFTR
img1 = K.io.load_image(fname1, K.io.ImageLoadType.RGB32)[None, ...]
img2 = K.io.load_image(fname2, K.io.ImageLoadType.RGB32)[None, ...]
img1 = K.geometry.resize(img1, (512, 512), antialias=True)
img2 = K.geometry.resize(img2, (512, 512), antialias=True)
指明影像是“室內”還是“室外”影像。
matcher = LoFTR(pretrained="outdoor")
LoFTR僅適用於灰度影像,因此需要將影像轉換為灰度。
input_dict = {
"image0": K.color.rgb_to_grayscale(img1),
"image1": K.color.rgb_to_grayscale(img2),
}
讓我們執行推理。
with torch.inference_mode():
correspondences = matcher(input_dict)
使用隨機樣本一致性法(RANSAC)來清理匹配。這有助於處理資料中的噪聲或異常值。
mkpts0 = correspondences["keypoints0"].cpu().numpy()
mkpts1 = correspondences["keypoints1"].cpu().numpy()
Fm, inliers = cv2.findFundamentalMat(mkpts0, mkpts1, cv2.USAC_MAGSAC, 0.5, 0.999, 100000)
inliers = inliers > 0
最後,我們可以視覺化匹配結果。
draw_LAF_matches(
KF.laf_from_center_scale_ori(
torch.from_numpy(mkpts0).view(1, -1, 2),
torch.ones(mkpts0.shape[0]).view(1, -1, 1, 1),
torch.ones(mkpts0.shape[0]).view(1, -1, 1),
),
KF.laf_from_center_scale_ori(
torch.from_numpy(mkpts1).view(1, -1, 2),
torch.ones(mkpts1.shape[0]).view(1, -1, 1, 1),
torch.ones(mkpts1.shape[0]).view(1, -1, 1),
),
torch.arange(mkpts0.shape[0]).view(-1, 1).repeat(1, 2),
K.tensor_to_image(img1),
K.tensor_to_image(img2),
inliers,
draw_dict={
"inlier_color": (0.1, 1, 0.1, 0.5),
"tentative_color": None,
"feature_color": (0.2, 0.2, 1, 0.5),
"vertical": False,
},
)
最佳匹配以綠色顯示,而不太確定的匹配則以藍色顯示。
資源和進一步閱讀
- FLANN Github
- Image Matching Using SIFT, SURF, BRIEF and ORB: Performance Comparison for Distorted Images
- ORB (Oriented FAST and Rotated BRIEF) tutorial
- Kornia tutorial on Image Matching
- LoFTR Github
- OpenCV Github
- OpenCV Feature Matching Tutorial
- OpenGlue: Open Source Graph Neural Net Based Pipeline for Image Matching