特征匹配
我们如何将从一个图像中检测到的特征匹配到另一个图像?特征匹配涉及比较不同图像中的关键属性以找到相似之处。特征匹配在许多计算机视觉应用中很有用,包括场景理解、图像拼接、物体跟踪和模式识别。
暴力搜索
想象一下,你有一个巨大的拼图碎片盒,你正在尝试找到一块可以与你的拼图完美匹配的特定碎片。这类似于在图像中搜索匹配的特征。你决定不使用任何特殊策略,而是逐个检查每个碎片,直到找到合适的碎片为止。这种直接的方法是暴力搜索。暴力搜索的优点是简单。你不需要任何特殊的技巧——只需要耐心。但是,如果要检查的碎片很多,它可能很耗时。在特征匹配的背景下,这种暴力方法类似于将一个图像中的每个像素与另一个图像中的每个像素进行比较,以查看它们是否匹配。它很详尽,并且可能需要大量时间,尤其是对于大型图像而言。
现在我们已经对如何找到暴力匹配有了直观的了解,让我们深入了解算法。我们将使用我们在上一章中学到的描述符来查找两个图像中匹配的特征。
首先安装并加载库
!pip install opencv-python
import cv2
import numpy as np
SIFT 的暴力搜索
让我们从初始化 SIFT 检测器开始。
sift = cv2.SIFT_create()
使用 SIFT 查找关键点和描述符。
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
使用 k 最近邻查找匹配项。
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
应用比率测试来对最佳匹配项进行阈值处理。
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
)
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 个匹配项。
img3 = cv2.drawMatches(
img1,
kp1,
img2,
kp2,
matches[:n],
None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
快速近似最近邻库 (FLANN)
FLANN 在 具有自动算法配置的快速近似最近邻 中由 Muja 和 Lowe 提出。为了解释 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)
对于 ORB,我们将使用论文中的参数。
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 参数。在这里,树是你想要的箱子数量。
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)
使用转换器 (LoFTR) 的局部特征匹配
LoFTR 由 Sun 等人在 LoFTR:使用转换器进行无检测器局部特征匹配 中提出。LoFTR 不使用特征检测器,而是使用基于学习的方法进行特征匹配。
让我们保持简单,再次使用我们的拼图示例。LoFTR 不像简单地逐像素比较图像那样,而是寻找每个图像中的特定关键点或特征。这就像识别每个拼图碎片的角和边。就像一个非常擅长拼图的人可能会关注独特的标记一样,LoFTR 会识别一个图像中的这些独特点。这些可能是突出的关键地标或结构。正如我们已经学到的,匹配算法必须能够处理旋转或缩放的变化。如果特征被旋转或调整大小,LoFTR 仍然可以识别它。这就像解决拼图,其中碎片可能被翻转或调整。当 LoFTR 匹配特征时,它会分配一个相似度评分来指示特征对齐的程度。评分越高,匹配越好。这就像给一个拼图碎片与另一个拼图碎片匹配的程度打分一样。LoFTR 也对某些变换具有不变性,这意味着它可以处理光照、角度或透视的变化。这在处理可能在不同条件下拍摄的图像时至关重要。LoFTR 能够稳健地匹配特征,使其在图像拼接等任务中非常有用,在图像拼接中,通过识别和连接共同特征来无缝地组合多个图像。
我们可以使用Kornia来使用LoFTR查找两张图像中的匹配特征。
!pip install kornia kornia-rs kornia_moons 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(image1.jpg, K.io.ImageLoadType.RGB32)[None, ...]
img2 = K.io.load_image(image2.jpg, 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
- 使用SIFT、SURF、BRIEF和ORB进行图像匹配:失真图像的性能比较
- ORB(定向快速和旋转BRIEF)教程
- Kornia图像匹配教程
- LoFTR GitHub
- OpenCV GitHub
- OpenCV特征匹配教程
- OpenGlue:基于图神经网络的开源图像匹配管道