阅读翻译Hugging Face Community Computer Vision Course之Feature Matching (特征匹配)
阅读翻译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