社区计算机视觉课程文档
特征匹配
并获得增强的文档体验
开始
特征匹配
我们如何将从一张图像中检测到的特征与另一张图像中的特征进行匹配?特征匹配涉及比较不同图像中的关键属性以找到相似之处。特征匹配在许多计算机视觉应用中都很有用,包括场景理解、图像拼接、物体跟踪和模式识别。
暴力搜索
想象一下,你有一大箱拼图碎片,你正在尝试找到一块特定的碎片来放入你的拼图中。这类似于在图像中搜索匹配的特征。你没有使用任何特殊的策略,而是决定逐个检查每块碎片,直到找到合适的碎片。这种直接的方法就是暴力搜索。暴力搜索的优点是它的简单性。你不需要任何特殊的技巧——只需要耐心。然而,它可能很耗时,特别是当有很多碎片需要检查时。在特征匹配的背景下,这种暴力方法类似于将一张图像中的每个像素与另一张图像中的每个像素进行比较,以查看它们是否匹配。它是详尽的,并且可能需要大量时间,特别是对于大型图像。
现在我们对如何找到暴力匹配有了一个直观的了解,让我们深入研究算法。我们将使用我们在上一章中学到的描述符来查找两张图像中的匹配特征。
首先安装并加载库。
!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 在《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)
对于 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 参数。这里,trees 是您想要的 bin 的数量。
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)
使用 Transformers 的局部特征匹配 (LoFTR)
LoFTR 是 Sun 等人在《LoFTR: Detector-Free Local Feature Matching with Transformers》中提出的。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(Oriented FAST and Rotated BRIEF)教程
- Kornia 图像匹配教程
- LoFTR Github
- OpenCV Github
- OpenCV 特征匹配教程
- OpenGlue:基于开源图神经网络的图像匹配管道