SuperGluePretrainedNetwork 详细解读

目录结构展示了SuperGluePretrainedNetwork项目的简化版布局。这是一个关于使用SuperGlue算法进行图像配对的深度学习项目,主要包括预训练的模型和执行配对的脚本。

demo_superglue.py

demo_superglue.py脚本的主要作用是展示SuperGlue预训练网络在图像对上进行特征匹配的能力。通过接收实时摄像头输入、视频文件或图像目录作为输入,该脚本能够实时地检测和匹配图像中的特征点,并可视化匹配结果。它是一个交互式的演示,允许用户通过键盘控制来调整匹配参数,同时展示关键点和匹配过程。

总结脚本功能:

  1. 输入处理:脚本接受多种形式的输入,包括USB摄像头、IP摄像头、图像目录或视频文件,支持通过命令行参数指定。

  2. 参数配置:用户可以通过命令行参数自定义多个匹配相关的配置,如关键点检测阈值、非极大值抑制(NMS)半径、Sinkhorn算法迭代次数、匹配阈值等。这些参数影响算法检测和匹配特征点的行为。

  3. 特征匹配:使用SuperPoint模型检测关键点和描述符,并通过SuperGlue模型进行特征点匹配。如果输入为视频或图像序列,脚本将连续匹配帧之间的特征点。

  4. 可视化:实时展示关键点检测和匹配结果,匹配的关键点会以连线形式显示。用户可以选择是否显示关键点。

  5. 交互式控制:提供键盘快捷键,允许用户实时调整关键点和匹配阈值,选择当前帧作为参考帧,以及开启或关闭关键点的可视化等。

  6. 输出:如果指定了输出目录,匹配结果将以图像形式保存。这些图像包含了原始图像、检测到的关键点和匹配的连线。

使用场景:

这个脚本非常适用于演示和评估SuperGlue算法在不同场景和条件下的特征匹配性能。它可以用于计算机视觉研究、机器人导航、增强现实应用等领域,为开发者和研究人员提供了一个便捷的工具来理解和利用SuperGlue模型的强大功能。

#! /usr/bin/env python3
# 这是一个 Python 脚本,声明使用 Python 3 作为解释器。

# 导入所需的模块和库。pathlib 用于处理文件路径,
# argparse 用于解析命令行参数,cv2 是 OpenCV 库用于图像处理,
# matplotlib.cm 用于获取颜色映射,torch 是 PyTorch 深度学习库。
from pathlib import Path
import argparse
import cv2
import matplotlib.cm as cm
import torch

# 从本地 models 包中导入 Matching 类和一些实用函数。具体来说:
# Matching 是一个类,用于将 SuperPoint 和 SuperGlue 模型组合在一起进行图像匹配。
# AverageTimer 是一个用于计时和统计平均时间的实用程序类。
# VideoStreamer 是一个用于从各种源读取视频帧或图像的实用程序类。
# make_matching_plot_fast 是一个函数,用于快速生成显示匹配结果的图像。
# frame2tensor 是一个函数,用于将图像帧转换为 PyTorch 张量。

from models.matching import Matching
from models.utils import (AverageTimer, VideoStreamer,
                          make_matching_plot_fast, frame2tensor)
# 禁用 PyTorch 中的自动求导,因为这是一个推理(inference)过程,不需要计算梯度。
torch.set_grad_enabled(False)


if __name__ == '__main__':
    #创建一个 ArgumentParser 对象,用于解析命令行参数。
    # description 描述了该程序的功能,formatter_class 指定了如何格式化帮助信息。
    parser = argparse.ArgumentParser(
        description='SuperGlue demo',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    # 添加一个命令行参数 --input。
    # 它指定输入源,可以是 USB 摄像头 ID、IP 摄像头 URL、图像目录或视频文件路径。
    # 默认值为 '0',表示使用默认的 USB 摄像头。
    parser.add_argument(
        '--input', type=str, default='0',
        help='ID of a USB webcam, URL of an IP camera, '
             'or path to an image directory or movie file')
    # 添加一个命令行参数 --output_dir。它指定输出目录,如果为 None(默认值)则不输出任何帧。
    parser.add_argument(
        '--output_dir', type=str, default=None,
        help='Directory where to write output frames (If None, no output)')
    # 添加一个命令行参数 --image_glob。它指定图像文件扩展名类型,如果输入是图像目录。
    # 默认值为 ['*.png', '*.jpg', '*.jpeg'],表示将读取 PNG、JPG 和 JPEG 格式的图像文件。
    parser.add_argument(
        '--image_glob', type=str, nargs='+', default=['*.png', '*.jpg', '*.jpeg'],
        help='Glob if a directory of images is specified')
    # 添加一个命令行参数 --skip。它指定跳过的帧数或图像数,如果输入是视频或图像目录。默认值为 1,表示不跳过任何帧或图像。
    parser.add_argument(
        '--skip', type=int, default=1,
        help='Images to skip if input is a movie or directory')
    # 添加一个命令行参数 --max_length。它指定最大长度,如果输入是视频或图像目录。默认值为 1000000,表示读取所有帧或图像。
    parser.add_argument(
        '--max_length', type=int, default=1000000,
        help='Maximum length if input is a movie or directory')
    # 添加一个命令行参数 --resize。它用于在运行推理前调整输入图像的大小。
    # 如果提供两个数字,则调整到指定的宽高;如果提供一个数字,则调整最大维度;
    # 如果为 -1,则不调整大小。默认值为 [640, 480],表示将图像调整为 640x480 的分辨率。
    parser.add_argument(
        '--resize', type=int, nargs='+', default=[640, 480],
        help='Resize the input image before running inference. If two numbers, '
             'resize to the exact dimensions, if one number, resize the max '
             'dimension, if -1, do not resize')
    # 添加一个命令行参数 --superglue。
    # 它指定使用 SuperGlue 算法的室内或室外预训练权重。可选值为 'indoor' 和 'outdoor',默认值为 'indoor'。
    parser.add_argument(
        '--superglue', choices={'indoor', 'outdoor'}, default='indoor',
        help='SuperGlue weights')
    # 添加一个命令行参数 --max_keypoints。它指定保留的最大关键点数量。
    # 如果设置为 -1(默认值),则保留所有检测到的关键点。
    parser.add_argument(
        '--max_keypoints', type=int, default=-1,
        help='Maximum number of keypoints detected by Superpoint'
             ' (\'-1\' keeps all keypoints)')
    # 添加一个命令行参数 --keypoint_threshold。它指定 SuperPoint 关键点检测器的置信度阈值。默认值为 0.005。
    parser.add_argument(
        '--keypoint_threshold', type=float, default=0.005,
        help='SuperPoint keypoint detector confidence threshold')
    # 添加一个命令行参数 --nms_radius。它指定 SuperPoint 算法中非最大值抑制(NMS)的半径。默认值为 4。注释说明该值必须为正数。
    parser.add_argument(
        '--nms_radius', type=int, default=4,
        help='SuperPoint Non Maximum Suppression (NMS) radius'
        ' (Must be positive)')
    # 添加一个命令行参数 --sinkhorn_iterations。它指定 SuperGlue 算法中 Sinkhorn 迭代的次数。默认值为 20。
    parser.add_argument(
        '--sinkhorn_iterations', type=int, default=20,
        help='Number of Sinkhorn iterations performed by SuperGlue')
    # 添加一个命令行参数 --match_threshold。它指定 SuperGlue 算法中匹配的阈值。默认值为 0.2。
    parser.add_argument(
        '--match_threshold', type=float, default=0.2,
        help='SuperGlue match threshold')
    # 添加一个命令行参数 --show_keypoints。它是一个布尔标志,用于指定是否在结果图像中显示检测到的关键点。如果设置该标志,则显示关键点。
    parser.add_argument(
        '--show_keypoints', action='store_true',
        help='Show the detected keypoints')
    # 添加一个命令行参数 --no_display。
    # 它是一个布尔标志,用于指定是否不显示任何图像。如果设置该标志,则不显示任何图像,这在远程运行时可能会很有用。
    parser.add_argument(
        '--no_display', action='store_true',
        help='Do not display images to screen. Useful if running remotely')
    # 添加一个命令行参数 --force_cpu。
    # 它是一个布尔标志,用于指定是否强制使用 CPU 模式运行推理,而不使用 GPU。如果设置该标志,则强制使用 CPU 模式。
    parser.add_argument(
        '--force_cpu', action='store_true',
        help='Force pytorch to run in CPU mode.')

    opt = parser.parse_args()
    # 解析命令行参数并打印出解析后的结果。
    print(opt)
    # 对上面的处理 --resize 参数进行处理,确保其格式正确(因为--resize不能输错),并打印出将要执行的调整操作。
    if len(opt.resize) == 2 and opt.resize[1] == -1:
        opt.resize = opt.resize[0:1]
    if len(opt.resize) == 2:
        print('Will resize to {}x{} (WxH)'.format(
            opt.resize[0], opt.resize[1]))
    elif len(opt.resize) == 1 and opt.resize[0] > 0:
        print('Will resize max dimension to {}'.format(opt.resize[0]))
    elif len(opt.resize) == 1:
        print('Will not resize images')
    else:
        raise ValueError('Cannot specify more than two integers for --resize')
    # 根据 GPU 是否可用和是否强制使用 CPU 模式,确定运行推理的设备(CUDA 或 CPU)。然后打印出将在哪个设备上运行推理。
    device = 'cuda' if torch.cuda.is_available() and not opt.force_cpu else 'cpu'
    print('Running inference on device \"{}\"'.format(device))
    # 创建一个配置字典,其中包含了 SuperPoint 和 SuperGlue 算法的相关参数。这些参数的值来自于命令行参数。
    config = {
        'superpoint': {
            'nms_radius': opt.nms_radius,
            'keypoint_threshold': opt.keypoint_threshold,
            'max_keypoints': opt.max_keypoints
        },
        'superglue': {
            'weights': opt.superglue,
            'sinkhorn_iterations': opt.sinkhorn_iterations,
            'match_threshold': opt.match_threshold,
        }
    }
    # 实例化一个 Matching 对象,将其设置为评估模式,并移动到指定的设备上。同时定义了一个列表 keys,用于存储关键信息的键名。
    matching = Matching(config).eval().to(device)
    keys = ['keypoints', 'scores', 'descriptors']
    # 实例化一个 VideoStreamer 对象,用于从指定的输入源读取帧或图像。
    # 读取第一帧,并确保读取成功。如果读取失败,则打印一条错误消息,提示尝试使用不同的 --input 参数。
    vs = VideoStreamer(opt.input, opt.resize, opt.skip,
                       opt.image_glob, opt.max_length)
    frame, ret = vs.next_frame()
    assert ret, 'Error when reading the first frame (try different --input?)'
    # 将第一帧转换为张量,并使用 SuperPoint 模型进行处理。
    # 将结果存储在 last_data 字典中,同时添加一些其他必需的键值对。保存第一帧的图像和帧 ID。
    frame_tensor = frame2tensor(frame, device)
    last_data = matching.superpoint({'image': frame_tensor})
    last_data = {k+'0': last_data[k] for k in keys}
    last_data['image0'] = frame_tensor
    last_frame = frame
    last_image_id = 0
    # 如果指定了输出目录,则创建该目录(如果不存在),并打印一条消息,说明将输出写入该目录。
    if opt.output_dir is not None:
        print('==> Will write outputs to {}'.format(opt.output_dir))
        Path(opt.output_dir).mkdir(exist_ok=True)

    # 如果没有指定不显示图像,则创建一个名为 "SuperGlue matches" 的窗口,用于显示结果。
    # 调整窗口大小为 1280x480 像素。否则,打印一条消息说明将跳过可视化,不会显示任何 GUI。
    if not opt.no_display:
        cv2.namedWindow('SuperGlue matches', cv2.WINDOW_NORMAL)
        cv2.resizeWindow('SuperGlue matches', 640*2, 480)
    else:
        print('Skipping visualization, will not show a GUI.')

    # 打印键盘控制说明,解释每个按键对应的操作。
    print('==> Keyboard control:\n'
          '\tn: select the current frame as the anchor\n'
          '\te/r: increase/decrease the keypoint confidence threshold\n'
          '\td/f: increase/decrease the match filtering threshold\n'
          '\tk: toggle the visualization of keypoints\n'
          '\tq: quit')
    # 实例化一个 AverageTimer 对象,用于计时和统计每次迭代的耗时。
    timer = AverageTimer()
    # 进入主循环。读取下一帧,如果读取失败则退出循环并打印一条消息。更新计时器。计算当前帧和上一帧的 ID。
    while True:
        frame, ret = vs.next_frame()
        if not ret:
            print('Finished demo_superglue.py')
            break
        timer.update('data')
        stem0, stem1 = last_image_id, vs.i - 1
        # 这是主循环,会一直运行直到视频流结束。
        # vs.next_frame()从视频流中获取下一帧并将其赋值给frame。
        # ret是一个布尔值,表示帧是否成功获取。如果ret为False,意味着视频流已结束,因此会打印一条消息并跳出循环。
        # timer.update('data')用于更新数据处理所用时间的计时器。stem0和stem1用于命名输出文件。
        frame_tensor = frame2tensor(frame, device)
        # 这一行将当前视频帧frame转换为PyTorch的张量格式。
        # frame2tensor是一个自定义函数,它将图像帧数据转换为张量,以便输入到神经网络模型中进行处理。device是指定将张量放在CPU还是GPU上的参数。
        pred = matching({**last_data, 'image1': frame_tensor})
        # 这里调用了一个名为matching的函数,传入两个参数:
        # **last_data - 这是一个Python的解包操作,将last_data字典中的键值对解包作为单独的参数传入。
        # 'image1': frame_tensor - 将当前帧张量作为一个新的键值对添加到参数中。
        # matching函数执行了SuperGlue算法,输入是之前的数据last_data和当前帧frame_tensor。
        # 它的返回值pred是一个字典,包含了预测的关键点、匹配和匹配分数等。

        kpts0 = last_data['keypoints0'][0].cpu().numpy()
        kpts1 = pred['keypoints1'][0].cpu().numpy()
        matches = pred['matches0'][0].cpu().numpy()
        confidence = pred['matching_scores0'][0].cpu().numpy()
        # 这四行代码从last_data和pred字典中提取关键点、匹配和置信度分数,并将它们转换为NumPy数组形式。
        # kpts0是上一帧(last_data)中的关键点
        # kpts1是当前帧中预测的关键点
        # matches是当前帧中每个关键点与上一帧关键点的匹配索引,如果为-1则表示无匹配
        # confidence是每个匹配的置信度分数
        # 这些数据将用于后续的可视化和分析。最后的.cpu().numpy()是将PyTorch的张量数据转换为NumPy数组,以便进行后续的数值计算和操作。
        timer.update('forward')
        # 这一行更新一个计时器,用于记录模型的前向传播(即SuperGlue匹配算法)所用的时间,可用于性能分析和优化。

        valid = matches > -1
        mkpts0 = kpts0[valid]
        mkpts1 = kpts1[matches[valid]]
        color = cm.jet(confidence[valid])
        text = [
            'SuperGlue',
            'Keypoints: {}:{}'.format(len(kpts0), len(kpts1)),
            'Matches: {}'.format(len(mkpts0))
        ]
        k_thresh = matching.superpoint.config['keypoint_threshold']
        m_thresh = matching.superglue.config['match_threshold']
        small_text = [
            'Keypoint Threshold: {:.4f}'.format(k_thresh),
            'Match Threshold: {:.2f}'.format(m_thresh),
            'Image Pair: {:06}:{:06}'.format(stem0, stem1),
        ]
        # 这部分准备可视化所需的数据。valid是一个布尔掩码,选择只有有效匹配(匹配值大于-1)。
        # mkpts0和mkpts1分别是前一帧和当前帧中有效匹配的关键点。
        # color是一个颜色列表,用于可视化匹配,颜色基于匹配的置信度。
        # text是一个字符串列表,将在可视化中显示。
        # k_thresh和m_thresh是SuperGlue算法使用的关键点和匹配阈值。
        # small_text是一个字符串列表,将作为小字体显示在可视化中。
        out = make_matching_plot_fast(
            last_frame, frame, kpts0, kpts1, mkpts0, mkpts1, color, text,
            path=None, show_keypoints=opt.show_keypoints, small_text=small_text)
        # 该行调用make_matching_plot_fast函数创建前一帧和当前帧之间匹配的可视化图像。
        # 它将前一帧、当前帧、关键点、有效关键点、颜色、文本和其他参数作为输入,并返回可视化图像out。
        if not opt.no_display:
            cv2.imshow('SuperGlue matches', out)
            key = chr(cv2.waitKey(1) & 0xFF)
            if key == 'q':
                vs.cleanup()
                print('Exiting (via q) demo_superglue.py')
                break
            elif key == 'n':  # set the current frame as anchor
                last_data = {k+'0': pred[k+'1'] for k in keys}
                last_data['image0'] = frame_tensor
                last_frame = frame
                last_image_id = (vs.i - 1)
            elif key in ['e', 'r']:
                # Increase/decrease keypoint threshold by 10% each keypress.
                d = 0.1 * (-1 if key == 'e' else 1)
                matching.superpoint.config['keypoint_threshold'] = min(max(
                    0.0001, matching.superpoint.config['keypoint_threshold']*(1+d)), 1)
                print('\nChanged the keypoint threshold to {:.4f}'.format(
                    matching.superpoint.config['keypoint_threshold']))
            elif key in ['d', 'f']:
                # Increase/decrease match threshold by 0.05 each keypress.
                d = 0.05 * (-1 if key == 'd' else 1)
                matching.superglue.config['match_threshold'] = min(max(
                    0.05, matching.superglue.config['match_threshold']+d), .95)
                print('\nChanged the match threshold to {:.2f}'.format(
                    matching.superglue.config['match_threshold']))
            elif key == 'k':
                opt.show_keypoints = not opt.show_keypoints
        # 这部分代码处理可视化的显示和用户输入。
        # 如果opt.no_display为False,它会使用cv2.imshow显示可视化图像。
        # 然后使用cv2.waitKey(1)等待按键。如果按下'q'键,它会清理视频流并退出循环。
        # 如果按下'n'键,它会将当前帧设置为新的锚帧用于匹配。如果按下'e'或'r'键,它会将关键点阈值增加或减少10%。
        # 如果按下'd'或'f'键,它会将匹配阈值增加或减少0.05。如果按下'k'键,它会切换是否在可视化中显示关键点。

        timer.update('viz')
        timer.print()
        # 更新可视化步骤所用时间的计时器,并打印每个步骤(数据、前向传播、可视化)的计时。
        if opt.output_dir is not None:
            #stem = 'matches_{:06}_{:06}'.format(last_image_id, vs.i-1)
            stem = 'matches_{:06}_{:06}'.format(stem0, stem1)
            out_file = str(Path(opt.output_dir, stem + '.png'))
            print('\nWriting image to {}'.format(out_file))
            cv2.imwrite(out_file, out)
        # 如果opt.output_dir不为None,它会使用stem0和stem1值构造输出图像的文件名,并使用cv2.imwrite将可视化图像out写入该文件。
    cv2.destroyAllWindows()
    vs.cleanup()
    # 总的来说,这段代码演示了如何使用SuperGlue算法在视频流的帧之间进行特征匹配。
    # 它提供了匹配的可视化效果,并允许用户调整关键点和匹配阈值,以及将可视化保存为文件。

match_pairs.py

这段代码是用于评估SuperGlue算法在图像对匹配和位姿估计方面的性能。主要流程包括读取图像对,使用SuperGlue进行特征匹配,可选地进行位姿估计评估,以及可视化匹配结果。下面是对其主要目的和功能的详细解释:

主要目的和功能

  1. 图像对的处理:脚本从指定的文件读取图像对列表,这些图像对可能包含或不包含用于评估的地面真实位姿信息。

  2. 配置参数解析:通过命令行参数,用户可以自定义多个选项,包括输入输出路径、图像尺寸、SuperGlue配置参数(如匹配阈值、关键点数量限制)、是否进行结果可视化、是否评估位姿估计准确性等。

  3. 模型初始化:使用给定的配置参数(关键点检测阈值、NMS半径、SuperGlue的权重等),初始化SuperPointSuperGlue模型。

  4. 匹配和评估:对每对图像,执行以下步骤:

    • 读取并预处理图像(可选地调整大小)。
    • 使用SuperPoint提取关键点和描述符,SuperGlue进行关键点匹配。
    • 如果启用评估(--eval),基于地面真实位姿和内参,估计图像对的相对位姿,并计算位姿估计误差和匹配精度。
  5. 结果保存:匹配结果(关键点、匹配对、匹配得分等)被保存到.npz文件中。如果进行了评估,位姿估计误差和匹配精度也会被保存。

  6. 可视化:如果启用可视化(--viz),匹配结果会以图像形式保存,展示匹配的关键点对。如果还进行了评估,还会展示位姿估计的评估结果。

  7. 性能评估报告:如果进行了评估,脚本最后会输出一个性能评估报告,包括位姿估计误差的AUC(Area Under the Curve)值和匹配精度。

总结

这段代码提供了一个完整的流程,用于评估SuperGlue算法在特定数据集上处理图像对匹配和位姿估计的性能。它不仅能够处理图像对、执行特征匹配,还能根据地面真实数据评估算法的准确性,并通过图像可视化直观展示匹配效果。这对于算法开发者和研究者来说是一个非常有用的工具,可以帮助他们测试和改进SuperGlue算法在不同场景下的表现。

models

SuperGluePretrainedNetwork/models目录中,我们可以看到一个组织良好的模块结构,旨在提供图像特征匹配功能,主要利用SuperPointSuperGlue这两个深度学习模型。下面是对这个目录结构的详细分析,帮助你更好地理解每个组成部分的作用:

文件和目录概览

  • __init__.py:这是一个空文件,其存在标志着models目录被Python视为一个包(Package)。这使得你可以从这个目录(包)中导入模块,比如matching.pysuperglue.py

  • matching.py:这个文件定义了Matching类,它是整个功能的核心。Matching类封装了特征点提取(通过SuperPoint模型)和特征点匹配(通过SuperGlue模型)的流程。简单来说,就是它负责接收图像,然后利用下面介绍的两个模型来找出图像间的匹配点。

  • superglue.py:定义了SuperGlue模型的结构和功能。SuperGlue用于匹配两组特征点(通常由SuperPoint提取),输出匹配对和每对匹配的置信度。SuperGlue在特征点匹配领域表现出色,能够处理复杂的场景和变化。

  • superpoint.py:定义了SuperPoint模型的结构和功能。SuperPoint主要用于从图像中提取特征点及其描述符。这些特征点和描述符随后可以用于匹配,以找出两个图像间相同的点。

  • utils.py:提供了一些辅助功能,如图像读取、预处理、评估函数等。这些工具函数支持上述模型的运行和结果的评估。

  • weights/目录:包含预训练模型权重文件,分别是:

    • superglue_outdoor.pth:用于SuperGlue模型的预训练权重,针对室外场景优化。
    • superpoint_v1.pthSuperPoint模型的预训练权重。

总结

models目录提供了一套完整的工具,用于从图像中提取特征点(SuperPoint)、将这些特征点进行匹配(SuperGlue),并包含了必要的工具函数(utils.py)及预训练模型权重(weights/目录)以便直接使用。通过matching.py封装的Matching类,用户可以方便地实现图像匹配的端到端流程,无需深入了解每个模型的内部细节。对于初学者来说,理解每个文件的基本作用和如何协同工作是理解整个系统的关键。

__init__.py

    __init__.py:这个文件通常用来将一个目录标记为Python的包。它可以为空,也可以用来写一些初始化代码或者为包定义一个方便的导入接口。首先查看这个文件,虽然它可能不包含对模型理解关键的信息,但它是理解包结构的起点。

superpoint.py

这段代码定义了SuperPoint类,是一个用于计算机视觉任务的深度学习模型,主要用于关键点检测和特征描述。下面是对这段代码主要功能的解释:

1. 模型的目的和功能

  • SuperPoint模型的核心功能是从输入的图像中自动检测出关键点(interest points 或 corners),并为每个检测到的关键点生成一个描述符(descriptor)。关键点检测允许模型识别图像中的显著特征位置,而描述符为这些关键点提供了一种量化的表示,使得模型可以比较不同图像中关键点的相似度。

2. 模型结构和流程

  • 编码器网络:模型通过一系列卷积层(conv1aconv4b)处理输入图像,这部分构成了共享的特征提取器或编码器。
  • 关键点检测头:接着,使用额外的卷积层(convPaconvPb)从共享特征中预测每个像素是否为关键点以及其得分。这些得分经过非最大抑制(NMS)处理,以确保关键点的分布合理且不过于密集。
  • 描述符生成头:另一组卷积层(convDaconvDb)用于生成每个位置的特征描述符,描述符通过插值操作在关键点的精确位置被采样。
  • 预处理和后处理:模型包含了几个关键的预处理和后处理步骤,如simple_nms函数用于非最大抑制,remove_borders用于移除边界附近的关键点,top_k_keypoints用于选择得分最高的K个关键点。

3. 使用预训练权重

  • 通过加载superpoint_v1.pth中的预训练权重,SuperPoint模型可以直接用于关键点检测和描述符计算,而无需从头开始训练。这使得模型在实际应用中更加高效和实用。

4. 主要用途

  • SuperPoint模型的输出(关键点位置、得分和描述符)可以用于多种计算机视觉任务,如图像匹配、目标跟踪、三维重建等。描述符的匹配允许模型识别不同图像之间的相同特征,是许多视觉系统的基础。

总结

这段代码实现了SuperPoint模型,包含了从输入图像中检测关键点和生成描述符的完整流程,以及模型的加载和应用方法。通过预训练权重和详细的网络结构定义,它为进行高效的关键点检测和特征描述提供了强大的工具。

from pathlib import Path
# Path类属于pathlib模块,它提供了一种面向对象的方式来处理和构建文件系统路径。
# 相比于传统的文件路径操作,如使用字符串和os模块的函数,pathlib使得路径操作更加直观和易于理解。
# 可以用来创建新路径、修改路径、检查路径是否存在、列出目录内容等。
import torch
# torch是PyTorch的核心库,是一个广泛使用的开源机器学习库,尤其用于深度学习应用。
# 它提供了一个多维数组对象(称为张量),这个对象类似于NumPy的数组,但它还可以在GPU上运行以加速计算。
# 此外,torch还包括了自动微分功能,方便了神经网络训练中的梯度计算,以及一个全面的深度学习模型库。
from torch import nn
# nn是PyTorch中的一个子模块,全称为torch.nn。
# 它包含了构建神经网络所需的各种模块和类,例如各种层类型(全连接层、卷积层、池化层等)、激活函数、损失函数等。
# 这个模块的设计目的是为了简化和加速神经网络模型的开发过程。通过torch.nn,开发者可以以模块化的方式来构建复杂的网络结构。
def simple_nms(scores, nms_radius: int):
    """ 这个函数实现了一个简单版本的非最大抑制(Non-Maximum Suppression)算法。
    非最大抑制(NMS)是计算机视觉和图像处理中常用的一种技术,用于确保在目标检测等任务中,每个目标只被检测一次。
    这种方法通常在检测到多个相邻的候选目标时使用,它会抑制(即置零)非最大的局部峰值点,只保留最强的信号点。
    在这种情况下,simple_nms函数通过一种简易的方法实现了非最大抑制,用于降低关键点检测中的冗余。 """
    # 参数:
    #     - scores: 输入的分数图,通常为二维张量,表示图像中每个像素的得分。
    #     - nms_radius: NMS操作的半径,确定了需要抑制的邻域大小。
    #
    #     返回:
    #     - 经过NMS处理后的分数图,邻近的非最大点被抑制为零。
    assert(nms_radius >= 0)

    def max_pool(x):
        return torch.nn.functional.max_pool2d(
            x, kernel_size=nms_radius*2+1, stride=1, padding=nms_radius)

    zeros = torch.zeros_like(scores)
    max_mask = scores == max_pool(scores)
    for _ in range(2):
        supp_mask = max_pool(max_mask.float()) > 0
        supp_scores = torch.where(supp_mask, zeros, scores)
        new_max_mask = supp_scores == max_pool(supp_scores)
        max_mask = max_mask | (new_max_mask & (~supp_mask))
    return torch.where(max_mask, scores, zeros)


def remove_borders(keypoints, scores, border: int, height: int, width: int):
    # """ """
    # 移除靠近边界的关键点。
    #
    # 参数:
    # - keypoints: 关键点的坐标数组,形状为[N, 2],其中N是关键点的数量。
    # - scores: 与关键点对应的分数数组,形状为[N]。
    # - border: 边界宽度,函数将移除距离图像边界小于这个宽度的关键点。
    # - height: 图像的高度。
    # - width: 图像的宽度。
    #
    # 返回:
    # - 移除靠近边界后的关键点坐标和分数数组。
    # """ """
    mask_h = (keypoints[:, 0] >= border) & (keypoints[:, 0] < (height - border))
    mask_w = (keypoints[:, 1] >= border) & (keypoints[:, 1] < (width - border))
    mask = mask_h & mask_w
    return keypoints[mask], scores[mask]


def top_k_keypoints(keypoints, scores, k: int):
    # """
    #     从所有关键点中选出得分最高的前k个关键点。
    #
    #     参数:
    #     - keypoints: 关键点的坐标数组,形状通常为[N, 2],其中N是关键点的数量。
    #     - scores: 与关键点对应的得分数组,形状为[N]。
    #     - k: 指定想要选出的关键点的数量。
    #
    #     返回:
    #     - 根据得分筛选出的前k个关键点的坐标和得分。
    # """
    # 如果请求的关键点数量k不小于输入的关键点数量,则直接返回所有关键点和得分
    if k >= len(keypoints):
        return keypoints, scores
    scores, indices = torch.topk(scores, k, dim=0)
    return keypoints[indices], scores


def sample_descriptors(keypoints, descriptors, s: int = 8):
    # """
    # 在关键点的位置进行描述子的插值操作。
    #
    # 参数:
    # - keypoints: 关键点的坐标数组,形状通常为 [N, 2],N为关键点数量。
    # - descriptors: 描述子张量,形状为 [B, C, H, W],其中:
    #   B 是批次大小,
    #   C 是每个描述子的通道数,
    #   H 和 W 分别是描述子空间的高度和宽度。
    # - s: 描述子的空间分辨率,默认值为 8。
    #
    # 返回:
    # - 在关键点位置插值后的描述子张量。
    # """
    b, c, h, w = descriptors.shape
    keypoints = keypoints - s / 2 + 0.5
    keypoints /= torch.tensor([(w*s - s/2 - 0.5), (h*s - s/2 - 0.5)],
                              ).to(keypoints)[None]
    keypoints = keypoints*2 - 1  # normalize to (-1, 1)
    args = {'align_corners': True} if torch.__version__ >= '1.3' else {}
    descriptors = torch.nn.functional.grid_sample(
        descriptors, keypoints.view(b, 1, -1, 2), mode='bilinear', **args)
    descriptors = torch.nn.functional.normalize(
        descriptors.reshape(b, c, -1), p=2, dim=1)
    return descriptors


class SuperPoint(nn.Module):
    """SuperPoint Convolutional Detector and Descriptor

    SuperPoint: Self-Supervised Interest Point Detection and
    Description. Daniel DeTone, Tomasz Malisiewicz, and Andrew
    Rabinovich. In CVPRW, 2019. https://arxiv.org/abs/1712.07629

    """
    default_config = {
        'descriptor_dim': 256,
        'nms_radius': 4,
        'keypoint_threshold': 0.005,
        'max_keypoints': -1,
        'remove_borders': 4,
    }
    #default_config 字典定义了一些默认配置,包括描述子维度、非极大值抑制半径、关键点阈值、最大关键点数量和边界删除范围。

    def __init__(self, config):
        super().__init__()
        # 合并默认配置和用户配置
        self.config = {**self.default_config, **config}
        # 定义激活函数和池化层
        self.relu = nn.ReLU(inplace=True)
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        # 定义不同层的通道数
        c1, c2, c3, c4, c5 = 64, 64, 128, 128, 256
        # 定义编码器网络
        self.conv1a = nn.Conv2d(1, c1, kernel_size=3, stride=1, padding=1)
        self.conv1b = nn.Conv2d(c1, c1, kernel_size=3, stride=1, padding=1)
        self.conv2a = nn.Conv2d(c1, c2, kernel_size=3, stride=1, padding=1)
        self.conv2b = nn.Conv2d(c2, c2, kernel_size=3, stride=1, padding=1)
        self.conv3a = nn.Conv2d(c2, c3, kernel_size=3, stride=1, padding=1)
        self.conv3b = nn.Conv2d(c3, c3, kernel_size=3, stride=1, padding=1)
        self.conv4a = nn.Conv2d(c3, c4, kernel_size=3, stride=1, padding=1)
        self.conv4b = nn.Conv2d(c4, c4, kernel_size=3, stride=1, padding=1)
        # 定义关键点预测头
        self.convPa = nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)
        self.convPb = nn.Conv2d(c5, 65, kernel_size=1, stride=1, padding=0)

        self.convDa = nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)
        self.convDb = nn.Conv2d(
            c5, self.config['descriptor_dim'],
            kernel_size=1, stride=1, padding=0)
        # 加载预训练权重
        path = Path(__file__).parent / 'weights/superpoint_v1.pth'
        self.load_state_dict(torch.load(str(path)))
        # 检查最大关键点数配置,如果不合理则抛出异常。
        mk = self.config['max_keypoints']
        if mk == 0 or mk < -1:
            raise ValueError('\"max_keypoints\" must be positive or \"-1\"')

        print('Loaded SuperPoint model')

    def forward(self, data):
        # 计算图像的关键点、分数和描述子
        # 共享编码器
        x = self.relu(self.conv1a(data['image']))
        x = self.relu(self.conv1b(x))
        x = self.pool(x)
        x = self.relu(self.conv2a(x))
        x = self.relu(self.conv2b(x))
        x = self.pool(x)
        x = self.relu(self.conv3a(x))
        x = self.relu(self.conv3b(x))
        x = self.pool(x)
        x = self.relu(self.conv4a(x))
        x = self.relu(self.conv4b(x))

        # 计算密集关键点分数
        cPa = self.relu(self.convPa(x))
        scores = self.convPb(cPa)
        scores = torch.nn.functional.softmax(scores, 1)[:, :-1]
        b, _, h, w = scores.shape
        scores = scores.permute(0, 2, 3, 1).reshape(b, h, w, 8, 8)
        scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h*8, w*8)
        scores = simple_nms(scores, self.config['nms_radius'])

        # 提取关键点
        keypoints = [
            torch.nonzero(s > self.config['keypoint_threshold'])
            for s in scores]
        scores = [s[tuple(k.t())] for s, k in zip(scores, keypoints)]

        # 去除靠近图像边界的关键点
        keypoints, scores = list(zip(*[
            remove_borders(k, s, self.config['remove_borders'], h*8, w*8)
            for k, s in zip(keypoints, scores)]))

        # 保留分数最高的 k 个关键点
        if self.config['max_keypoints'] >= 0:
            keypoints, scores = list(zip(*[
                top_k_keypoints(k, s, self.config['max_keypoints'])
                for k, s in zip(keypoints, scores)]))

        # 将 (h, w) 转换为 (x, y)
        keypoints = [torch.flip(k, [1]).float() for k in keypoints]

        # Compute the dense descriptors
        cDa = self.relu(self.convDa(x))
        descriptors = self.convDb(cDa)
        descriptors = torch.nn.functional.normalize(descriptors, p=2, dim=1)

        # 计算密集描述子
        descriptors = [sample_descriptors(k[None], d[None], 8)[0]
                       for k, d in zip(keypoints, descriptors)]

        return {
            'keypoints': keypoints,
            'scores': scores,
            'descriptors': descriptors,
        }

superglue.py

        SuperGlue用于对来自两个图像的关键点描述符进行匹配。它依赖于SuperPoint提取的特征。了解这个文件将帮助你理解如何从两组关键点中找到匹配对。



from copy import deepcopy
from pathlib import Path
from typing import List, Tuple

import torch
from torch import nn


def MLP(channels: List[int], do_bn: bool = True) -> nn.Module:
    # 这个函数可以方便地创建一个多层感知器网络,每一层由一个全连接层(使用一维卷积实现)组成。
    # 中间层可以选择是否添加批归一化,并且中间层都会使用 ReLU 激活函数。
    n = len(channels)
    layers = []
    for i in range(1, n):
        layers.append(
            nn.Conv1d(channels[i - 1], channels[i], kernel_size=1, bias=True))
        if i < (n-1):
            if do_bn:
                layers.append(nn.BatchNorm1d(channels[i]))
            layers.append(nn.ReLU())
    return nn.Sequential(*layers)


def normalize_keypoints(kpts, image_shape):
    # 该函数 normalize_keypoints 是用来根据图像的尺寸来归一化关键点位置的。
    _, _, height, width = image_shape
    one = kpts.new_tensor(1)
    size = torch.stack([one*width, one*height])[None]
    center = size / 2
    scaling = size.max(1, keepdim=True).values * 0.7
    return (kpts - center[:, None, :]) / scaling[:, None, :]


class KeypointEncoder(nn.Module):
    # 在Python中,类可以包含多个方法(函数),并且 KeypointEncoder 类包含了两个方法:__init__ 和 forward。
    def __init__(self, feature_dim: int, layers: List[int]) -> None:
        super().__init__()
        self.encoder = MLP([3] + layers + [feature_dim])
        nn.init.constant_(self.encoder[-1].bias, 0.0)

    def forward(self, kpts, scores):
        inputs = [kpts.transpose(1, 2), scores.unsqueeze(1)]
        return self.encoder(torch.cat(inputs, dim=1))


def attention(query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> Tuple[torch.Tensor,torch.Tensor]:
    dim = query.shape[1]
    scores = torch.einsum('bdhn,bdhm->bhnm', query, key) / dim**.5
    prob = torch.nn.functional.softmax(scores, dim=-1)
    return torch.einsum('bhnm,bdhm->bdhn', prob, value), prob
# 这个函数执行一个常见的注意力机制计算过程。它接收查询(query)、键(key)和值(value)作为输入,并返回注意力权重后的值以及注意力概率。

class MultiHeadedAttention(nn.Module):
    # 这个类 MultiHeadedAttention 是一个PyTorch神经网络模块,
    # 用于实现多头注意力机制,这是一种在各种序列到序列模型(如Transformer模型)中广泛使用的技术,可以提高模型的表达能力。
    def __init__(self, num_heads: int, d_model: int):
        super().__init__()
        assert d_model % num_heads == 0
        self.dim = d_model // num_heads
        self.num_heads = num_heads
        self.merge = nn.Conv1d(d_model, d_model, kernel_size=1)
        self.proj = nn.ModuleList([deepcopy(self.merge) for _ in range(3)])

    def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> torch.Tensor:
        batch_dim = query.size(0)
        query, key, value = [l(x).view(batch_dim, self.dim, self.num_heads, -1)
                             for l, x in zip(self.proj, (query, key, value))]
        x, _ = attention(query, key, value)
        return self.merge(x.contiguous().view(batch_dim, self.dim*self.num_heads, -1))


class AttentionalPropagation(nn.Module):
    def __init__(self, feature_dim: int, num_heads: int):
        super().__init__()
        self.attn = MultiHeadedAttention(num_heads, feature_dim)
        self.mlp = MLP([feature_dim*2, feature_dim*2, feature_dim])
        nn.init.constant_(self.mlp[-1].bias, 0.0)

    def forward(self, x: torch.Tensor, source: torch.Tensor) -> torch.Tensor:
        message = self.attn(x, source, source)
        return self.mlp(torch.cat([x, message], dim=1))


class AttentionalGNN(nn.Module):
    def __init__(self, feature_dim: int, layer_names: List[str]) -> None:
        super().__init__()
        self.layers = nn.ModuleList([
            AttentionalPropagation(feature_dim, 4)
            for _ in range(len(layer_names))])
        self.names = layer_names

    def forward(self, desc0: torch.Tensor, desc1: torch.Tensor) -> Tuple[torch.Tensor,torch.Tensor]:
        for layer, name in zip(self.layers, self.names):
            if name == 'cross':
                src0, src1 = desc1, desc0
            else:  # if name == 'self':
                src0, src1 = desc0, desc1
            delta0, delta1 = layer(desc0, src0), layer(desc1, src1)
            desc0, desc1 = (desc0 + delta0), (desc1 + delta1)
        return desc0, desc1


def log_sinkhorn_iterations(Z: torch.Tensor, log_mu: torch.Tensor, log_nu: torch.Tensor, iters: int) -> torch.Tensor:
    """ Perform Sinkhorn Normalization in Log-space for stability"""
    u, v = torch.zeros_like(log_mu), torch.zeros_like(log_nu)
    for _ in range(iters):
        u = log_mu - torch.logsumexp(Z + v.unsqueeze(1), dim=2)
        v = log_nu - torch.logsumexp(Z + u.unsqueeze(2), dim=1)
    return Z + u.unsqueeze(2) + v.unsqueeze(1)


def log_optimal_transport(scores: torch.Tensor, alpha: torch.Tensor, iters: int) -> torch.Tensor:
    """ Perform Differentiable Optimal Transport in Log-space for stability"""
    b, m, n = scores.shape
    one = scores.new_tensor(1)
    ms, ns = (m*one).to(scores), (n*one).to(scores)

    bins0 = alpha.expand(b, m, 1)
    bins1 = alpha.expand(b, 1, n)
    alpha = alpha.expand(b, 1, 1)

    couplings = torch.cat([torch.cat([scores, bins0], -1),
                           torch.cat([bins1, alpha], -1)], 1)

    norm = - (ms + ns).log()
    log_mu = torch.cat([norm.expand(m), ns.log()[None] + norm])
    log_nu = torch.cat([norm.expand(n), ms.log()[None] + norm])
    log_mu, log_nu = log_mu[None].expand(b, -1), log_nu[None].expand(b, -1)

    Z = log_sinkhorn_iterations(couplings, log_mu, log_nu, iters)
    Z = Z - norm  # multiply probabilities by M+N
    return Z


def arange_like(x, dim: int):
    return x.new_ones(x.shape[dim]).cumsum(0) - 1  # traceable in 1.1


class SuperGlue(nn.Module):
    """SuperGlue特征匹配中间层

       SuperGlue是一种基于图神经网络的特征匹配算法,用于在两组关键点之间找到对应关系。
       该类实现了SuperGlue的前向传播过程,包括关键点编码、图神经网络处理和最优传输匹配。

       主要步骤:
       1. 关键点归一化
       2. 关键点MLP编码器
       3. 多层Transformer网络
       4. 最终MLP投影
       5. 计算匹配描述符距离
       6. 运行最优传输
       7. 获取得分高于阈值的匹配

       参数:
       - config: SuperGlue的配置字典,包含以下keys:
           - descriptor_dim: 描述符维度
           - weights: 预训练权重的类型,可选'indoor'或'outdoor'
           - keypoint_encoder: 关键点编码器的层次结构
           - GNN_layers: 图神经网络的层次结构
           - sinkhorn_iterations: Sinkhorn算法的迭代次数
           - match_threshold: 匹配得分阈值

       输入:
       - data: 一个字典,包含以下keys:
           - descriptors0: 第一组关键点的描述符
           - descriptors1: 第二组关键点的描述符
           - keypoints0: 第一组关键点的坐标
           - keypoints1: 第二组关键点的坐标
           - scores0: 第一组关键点的置信度得分
           - scores1: 第二组关键点的置信度得分
           - image0: 第一张图像
           - image1: 第二张图像

       输出:
       一个字典,包含以下keys:
       - matches0: 第一组关键点的匹配索引,未匹配的关键点索引为-1
       - matches1: 第二组关键点的匹配索引,未匹配的关键点索引为-1
       - matching_scores0: 第一组关键点的匹配得分
       - matching_scores1: 第二组关键点的匹配得分
       """
    default_config = {
        'descriptor_dim': 256,# 描述符维度
        'weights': 'indoor',# 预训练权重的类型
        'keypoint_encoder': [32, 64, 128, 256],# 关键点编码器的层次结构
        'GNN_layers': ['self', 'cross'] * 9,# 图神经网络的层次结构
        'sinkhorn_iterations': 100,# Sinkhorn算法的迭代次数
        'match_threshold': 0.2,# 匹配得分阈值
    }

    def __init__(self, config):
        super().__init__()
        self.config = {**self.default_config, **config}
        # 关键点编码器
        self.kenc = KeypointEncoder(
            self.config['descriptor_dim'], self.config['keypoint_encoder'])
        # 图神经网络
        self.gnn = AttentionalGNN(
            feature_dim=self.config['descriptor_dim'], layer_names=self.config['GNN_layers'])
        # 最终投影层
        self.final_proj = nn.Conv1d(
            self.config['descriptor_dim'], self.config['descriptor_dim'],
            kernel_size=1, bias=True)
        # 双向匹配得分权重
        bin_score = torch.nn.Parameter(torch.tensor(1.))
        self.register_parameter('bin_score', bin_score)

        # 加载预训练权重
        assert self.config['weights'] in ['indoor', 'outdoor']
        path = Path(__file__).parent
        path = path / 'weights/superglue_{}.pth'.format(self.config['weights'])
        self.load_state_dict(torch.load(str(path)))
        print('Loaded SuperGlue model (\"{}\" weights)'.format(
            self.config['weights']))

    def forward(self, data):
        """Run SuperGlue on a pair of keypoints and descriptors"""
        desc0, desc1 = data['descriptors0'], data['descriptors1']
        kpts0, kpts1 = data['keypoints0'], data['keypoints1']
        # 如果没有关键点,则返回空匹配结果
        if kpts0.shape[1] == 0 or kpts1.shape[1] == 0:  # no keypoints
            shape0, shape1 = kpts0.shape[:-1], kpts1.shape[:-1]
            return {
                'matches0': kpts0.new_full(shape0, -1, dtype=torch.int),
                'matches1': kpts1.new_full(shape1, -1, dtype=torch.int),
                'matching_scores0': kpts0.new_zeros(shape0),
                'matching_scores1': kpts1.new_zeros(shape1),
            }

        # Keypoint normalization.# 关键点归一化
        kpts0 = normalize_keypoints(kpts0, data['image0'].shape)
        kpts1 = normalize_keypoints(kpts1, data['image1'].shape)

        # Keypoint MLP encoder.# 关键点MLP编码器
        desc0 = desc0 + self.kenc(kpts0, data['scores0'])
        desc1 = desc1 + self.kenc(kpts1, data['scores1'])

        # Multi-layer Transformer network.# 多层Transformer网络
        desc0, desc1 = self.gnn(desc0, desc1)

        # Final MLP projection.# 最终MLP投影
        mdesc0, mdesc1 = self.final_proj(desc0), self.final_proj(desc1)

        # Compute matching descriptor distance.# 计算匹配描述符距离
        scores = torch.einsum('bdn,bdm->bnm', mdesc0, mdesc1)
        scores = scores / self.config['descriptor_dim']**.5

        # Run the optimal transport.# 运行最优传输算法
        scores = log_optimal_transport(
            scores, self.bin_score,
            iters=self.config['sinkhorn_iterations'])

        # Get the matches with score above "match_threshold".# 获取得分高于阈值的匹配
        max0, max1 = scores[:, :-1, :-1].max(2), scores[:, :-1, :-1].max(1)
        indices0, indices1 = max0.indices, max1.indices
        mutual0 = arange_like(indices0, 1)[None] == indices1.gather(1, indices0)
        mutual1 = arange_like(indices1, 1)[None] == indices0.gather(1, indices1)
        zero = scores.new_tensor(0)
        mscores0 = torch.where(mutual0, max0.values.exp(), zero)
        mscores1 = torch.where(mutual1, mscores0.gather(1, indices1), zero)
        valid0 = mutual0 & (mscores0 > self.config['match_threshold'])
        valid1 = mutual1 & valid0.gather(1, indices1)
        indices0 = torch.where(valid0, indices0, indices0.new_tensor(-1))
        indices1 = torch.where(valid1, indices1, indices1.new_tensor(-1))

        return {
            'matches0': indices0, # use -1 for invalid match
            'matches1': indices1, # use -1 for invalid match
            'matching_scores0': mscores0,
            'matching_scores1': mscores1,
        }

matching.py

        matching.py文件的主要目的是将SuperPoint和SuperGlue两个模块组合在一起,实现图像匹配的完整流程。Matching类的作用是将图像匹配的整个流程封装起来,使得可以通过一次前向传播完成从图像到匹配结果的计算。它在内部调用了SuperPoint和SuperGlue两个子模块,分别完成关键点检测和描述符匹配的任务。这样的设计使得代码结构清晰,易于理解和使用。

import torch

from .superpoint import SuperPoint
from .superglue import SuperGlue


class Matching(torch.nn.Module):
    """ 图像匹配前端(SuperPoint + SuperGlue) """
    def __init__(self, config={}):
        super().__init__()
        # 初始化SuperPoint模块
        self.superpoint = SuperPoint(config.get('superpoint', {}))
        # 初始化SuperGlue模块
        self.superglue = SuperGlue(config.get('superglue', {}))

    def forward(self, data):
        """ 运行SuperPoint(可选)和SuperGlue
        如果输入中存在['keypoints0', 'keypoints1'],则跳过SuperPoint
        参数:
          data: 字典,最少需要包含以下键: ['image0', 'image1']
        """
        pred = {}

        # 如果没有提供'keypoints0',则使用SuperPoint提取关键点、得分和描述符
        if 'keypoints0' not in data:
            pred0 = self.superpoint({'image': data['image0']})
            pred = {**pred, **{k+'0': v for k, v in pred0.items()}}
        
        # 如果没有提供'keypoints1',则使用SuperPoint提取关键点、得分和描述符
        if 'keypoints1' not in data:
            pred1 = self.superpoint({'image': data['image1']})
            pred = {**pred, **{k+'1': v for k, v in pred1.items()}}

        # 批处理所有特征
        # 我们应该有以下两种情况之一:
        # i) 每个批次只有一张图像,或者
        # ii) 批次中所有图像的局部特征数量相同
        data = {**data, **pred}

        # 将列表或元组类型的数据转换为PyTorch张量
        for k in data:
            if isinstance(data[k], (list, tuple)):
                data[k] = torch.stack(data[k])

        # 执行匹配
        pred = {**pred, **self.superglue(data)}

        return pred

utils.py

"utils"是"utilities"的缩写,翻译成汉语是“工具”或“实用程序”的意思。在编程和软件开发中,utils通常指的是一组提供常用功能和辅助操作的函数、类或模块集合。这些工具函数或类设计用来处理一些常见的、重复性的任务,如字符串操作、文件处理、数学计算等,以提高代码的复用性和减少重复编写代码的工作量。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/487558.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux下命令行方式导入Mysql数据库

一. 正常Mysql导入 直接登录mysql进行导入即可 mysql -u用户名 -p密码 要导入的数据库名 < 数据库名.sql mysql -u用户名 -p密码 db_doc < 20210616.sql二. Docker镜像方式Mysql导入 1. docker启动mysql docker exec -it mysal bash 2. 登录mysql mysql -uroot -p密…

软件设计师21--操作系统章节回顾

软件设计师21--操作系统章节回顾 章节重要内容考情分析 章节重要内容 考情分析

消息队列八股

RabbitMQ 确保消息不丢失 重复消费问题 延迟队列 消息堆积 高可用 很少使用 Kafka 如何保证消息不丢失 回调接口保证生产者发送到brocker消息不丢失 保证消息顺序性 高可用机制 数据清理机制 实现高性能的设计

公平锁和非公平锁,为什么要“非公平”?

公平锁和非公平锁&#xff0c;为什么要“非公平”&#xff1f; 主要讲一讲公平锁和非公平锁&#xff0c;以及为什么要“非公平”&#xff1f; 什么是公平和非公平 首先&#xff0c;我们来看下什么是公平锁和非公平锁&#xff0c;公平锁指的是按照线程请求的顺序&#xff0c;…

欧科云链OKLink:坎昆升级后,Layer2项目是否更具竞争力?

在坎昆升级激活之际&#xff0c;OKLink 上线以太坊坎昆升级 Dencun 专题页 &#x1f449; 从专业链上数据分析角度&#xff0c;带来一场充实且即时的 Layer2 数据盛宴。 在近日由 137Labs 发起&#xff0c;Cointime 主持的 Layer2 生态专场讨论中&#xff0c;OKLink 产品…

AISD智能安全配电装置--智能监测、远程监控

安科瑞薛瑶瑶18701709087 AISD100单相、AISD300三相智能安全配电装置是安科瑞专为低压配电侧开发的一款智能安全配电产品。主要针对低压配电系统人身触电、线路老化、短路、漏电等原因引起电气安全问题而设计。 产品主要应用于学校、加油站、医院、银行、疗养院、康复中心、敬…

补充--广义表学习

第一章 逻辑结构 &#xff08;1&#xff09;A()&#xff0c;A是一个空表&#xff0c;长度为0&#xff0c;深度为1。 &#xff08;2&#xff09;B(d,e)&#xff0c;B的元素全是原子&#xff0c;d和e&#xff0c;长度为2&#xff0c;深度为1。 &#xff08;3&#xff09;C(b,(c,…

什么事缓存击穿、缓存穿透、缓存雪崩?

缓存击穿&#xff1a;是指当某一个key的缓存过期时大并发量的请求同时访问此key&#xff0c;瞬间击穿缓存服务器直接访问数据库&#xff0c;让数据库处于负载的情况。 缓存穿透&#xff1a;是指缓存服务器中没有缓存数据&#xff0c;数据库中也没有符合条件的数据&#xff0c;…

2024 ccfcsp认证打卡 2023 09 02 坐标变换(其二)

202309-2 坐标变换&#xff08;其二&#xff09; 题解1题解2区别第一种算法&#xff08;使用ArrayList存储操作序列&#xff09;&#xff1a;数据结构&#xff1a;操作序列处理&#xff1a; 第二种算法&#xff08;使用两个数组存储累积结果&#xff09;&#xff1a;数据结构&a…

微服务day06 -- Elasticsearch的数据搜索功能。分别使用DSL和RestClient实现搜索

1.DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1.DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#xff09;来定义查询。常见的查询类型包括&#xff1a; 查询所有&#xff1a;查询出所有数据&#xff0c;一…

代码随想录算法训练营day57|647. 回文子串 、 516.最长回文子序列

目录 647. 回文子串 16.最长回文子序列 647. 回文子串 力扣题目链接(opens new window) 给定一个字符串&#xff0c;你的任务是计算这个字符串中有多少个回文子串。 具有不同开始位置或结束位置的子串&#xff0c;即使是由相同的字符组成&#xff0c;也会被视作不同的子串…

每天学点儿python(1)---print,input和注释

print函数 print语法格式 print(*objects, sep , end\n, filesys.stdout) sep参数默认为 一个空格 end&#xff08;输出末尾&#xff09;参数默认为 回车换行 file默认为 标准输出&#xff08;一般指屏幕&#xff09; 所以&#xff0c;如果想输出各个字段不用空格隔开&a…

人工智能三剑客NumPy、pandas、matplotlib和Jupyter四者之间的关系

NumPy 主要用途&#xff1a;NumPy&#xff08;Numerical Python的缩写&#xff09;主要用于处理大型多维数组和矩阵的科学计算。它提供了一个高性能的多维数组对象&#xff0c;以及用于数组操作的工具。与其他三者的联系&#xff1a;NumPy是pandas和matplotlib的基础库之一。许…

前端基础 Vue -组件化基础

1.全局组件 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><script src&…

如何搭建ERP帮助中心提升工作效率

现在是个信息化时代&#xff0c;ERP系统成了企业日常运营里必不可少的一部分。不过&#xff0c;随着ERP系统用得越来越多&#xff0c;大家在使用过程中碰到的问题变得多且复杂。为了解决这些麻烦&#xff0c;提高大家的工作效率&#xff0c;搭建ERP帮助中心其实是一个很有必要的…

FreeRTOS从代码层面进行原理分析(2 任务的启动)

FreeRTOS分析二—任务的启动 上一篇文章我们带着三个问题开始了对 FreeRTOS 代码的探究。 1. FreeRTOS 是如何建立任务的呢&#xff1f; 2. FreeRTOS 是调度和切换任务的呢&#xff1f; 3. FreeRTOS 是如何保证实时性呢&#xff1f; 并且在上一篇文章 FreeRTOS从代码层面进行…

学习刷题-13

3.23 hw机试【二叉树】 剑指offer32 剑指 offer32&#xff08;一、二、三&#xff09;_剑指offer 32-CSDN博客 从上到下打印二叉树I 一棵圣诞树记作根节点为 root 的二叉树&#xff0c;节点值为该位置装饰彩灯的颜色编号。请按照从 左 到 右 的顺序返回每一层彩灯编号。 输…

产品推荐 | 基于 Zynq UltraScale+ XCZU27DR的 FACE-RFSoC-C高性能自适应射频开发平台

一、产品概述 FACE-RFSOC-C自适应射频开发平台&#xff0c;是FACE系列新一代的产品。 平台搭载有16nm工艺的Zynq UltraScale™ RFSoC系列主器件。该器件集成数千兆采样RF数据转换器和ARM Cortex-A53处理子系统和UltraScale可编程逻辑&#xff0c;是一款单芯片自适应射频平台。…

电脑卸载软件怎么清理干净?电脑清理的5种方法

随着我们在电脑上安装和卸载各种软件&#xff0c;很多时候我们会发现&#xff0c;即使软件被卸载&#xff0c;其残留的文件和注册表项仍然存在于电脑中&#xff0c;这不仅占用了宝贵的磁盘空间&#xff0c;还可能影响电脑的性能。那么&#xff0c;如何确保在卸载软件时能够彻底…

FakeLocation报虚拟位置服务连接失败,请重启设备再试

虚拟位置服务连接失败&#xff0c;请重启设备再试 最近遇到一个手机软件报的bug“虚拟位置服务连接失败&#xff0c;请重启设备再试” 因为我的实体“虚拟机”已经root&#xff0c;按道理是不可能报这个错的 折腾了2天&#xff0c;终于解决了 原来是这样&#xff0c;安装最新…