社区计算机视觉课程文档

特征匹配

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

特征匹配

我们如何将一幅图像中检测到的特征与另一幅图像进行匹配?特征匹配涉及比较不同图像中的关键属性以发现相似之处。特征匹配在许多计算机视觉应用中都非常有用,包括场景理解、图像拼接、目标跟踪和模式识别。

暴力搜索

想象一下你有一个巨大的拼图盒,你正在尝试找到一个适合你的拼图的特定拼图块。这类似于在图像中搜索匹配的特征。你决定逐个检查每个拼图块,直到找到正确的,而不是使用任何特殊的策略。这种直接的方法就是暴力搜索。暴力搜索的优点是它的简单性。你不需要任何特殊的技巧——只需要耐心。然而,它可能很耗时,特别是当有许多拼图块需要检查时。在特征匹配的背景下,这种暴力方法类似于将一幅图像中的每个像素与另一幅图像中的每个像素进行比较,以查看它们是否匹配。它是穷举的,可能需要大量时间,特别是对于大型图像。

既然我们对暴力匹配的查找方式有了直观的了解,接下来让我们深入了解这些算法。我们将使用前一章中学习到的描述符来查找两幅图像中的匹配特征。

首先安装并加载库。

!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
)
SIFT

使用 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 参数。在这里,`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)

FLANN

基于 Transformer 的局部特征匹配 (LoFTR)

LoFTR 由 Sun 等人在《LoFTR:基于 Transformer 的无检测器局部特征匹配》中提出。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,
    },
)

最佳匹配以绿色显示,而不太确定的匹配以蓝色显示。

LoFTR

资源和进一步阅读

< > 在 GitHub 上更新