AIGC标识 LightGlue特征匹配

  LightGlue是计算机视觉领域一个非常前沿的图像特征匹配算法,由瑞士苏黎世联邦理工学院(ETH Zurich)的计算机视觉与几何实验室(CVG)团队提出。它在2023年的计算机视觉顶级会议ICCV上发表,可以看作是著名的SuperGlue算法的“升级版”。它的核心突破在于将强大的Transformer架构与极致的运行效率结合了起来。

 

1. 核心特点

  • 自适应计算(Adaptive Computation)是最大亮点:LightGlue能根据图像匹配的难度“智能”地调整计算量。对于视角变化小、重叠度高的“简单”图像对,它会提前结束计算;只有面对复杂的图像时,才会动用全部网络层进行精细匹配。这种机制让其推理速度能比SuperGlue快4到10倍

  • 双重“瘦身”策略:它主要用两种方式来节省算力。一是动态提前停止(Adaptive Depth),当网络浅层已经匹配得很“自信”时,就停止后续计算;二是关键点修剪(Adaptive Width),对于那些网络判断为“无法匹配”的特征点(比如在另一张图中没有对应区域的点),会在计算中途将其剔除,不再参与后续运算

  • 灵活搭档:LightGlue本身是一个“匹配器”,负责建立特征点的对应关系。它需要搭配一个“检测器”来提取特征点。它和著名的SuperPoint是黄金搭档,同时也支持DISK、ALIKED,甚至经典的SIFT特征。

2. 下载地址

  github地址: https://github.com/fabio-sim/LightGlue-ONNX

  进去页面后,可以提前把onnx格式的模型下载下来(虽然原代码会自动下载与导出)。

image

image

  与其他特征匹配模型对比图:

image

3. 代码与运行效果

  这里直接给出修改后的,容易拿去即用的代码。

import numpy as np
import cv2
import onnxruntime as ort
import os

def preprocess(image, height=512, width=512):
    """严格复刻原版正常脚本的预处理:强制Resize,转RGB,只除以255,无Mean/Std"""
    if len(image.shape) == 2:
        image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
    elif len(image.shape) == 3 and image.shape[2] == 4:
        image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)

    # 转为RGB并强制缩放
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image = cv2.resize(image, (width, height))
    image = image.astype(np.float32) / 255.0

    if len(image.shape) == 2:
        image = np.stack([image, image, image], axis=2)

    # 转换为 (C, H, W) 并增加 Batch 维度
    image = np.transpose(image, (2, 0, 1))
    image = image[np.newaxis, :, :, :]  # (1, C, H, W)
    return image


def judge_similarity(img1, img2, session, threshold_count=100, threshold_conf=0.8, input_size=512,
                     save_path="output/result_matches.jpg"):
    # 1. 预处理
    tensor1 = preprocess(img1, input_size, input_size)
    tensor2 = preprocess(img2, input_size, input_size)
    input_tensor = np.concatenate([tensor1, tensor2], axis=0)

    # 2. 推理
    input_name = session.get_inputs()[0].name
    outputs = session.run(None, {input_name: input_tensor})

    keypoints = outputs[0]  # 形状通常为 (2, N, 2)
    matches = outputs[1]  # 形状为 (M, 3)
    mscores = outputs[2]  # 形状为 (N,) 与 keypoints 对应

    # 【核心修复】根据原版正常代码解析 matches 和 mscores
    if matches.shape[0] == 0:
        print("⚠️ 警告: 没有找到任何匹配点!")
        return {"原始总匹配数": 0, "高置信度匹配数": 0, "RANSAC 内点数": 0, "平均置信度": 0, "判定结果": "【不同类】"}

    kpt0_indices = matches[:, 1].astype(np.int64)
    kpt1_indices = matches[:, 2].astype(np.int64)
    score_indices = matches[:, 0].astype(np.int64)

    # 提取真实的匹配坐标
    mkpts0 = keypoints[0][kpt0_indices]
    mkpts1 = keypoints[1][kpt1_indices]

    # 提取真实的置信度
    match_scores = mscores[score_indices]

    # 3. 高置信度过滤
    high_conf_indices = np.where(match_scores > threshold_conf)[0]
    high_conf_src_pts = mkpts0[high_conf_indices].astype(np.float32)
    high_conf_dst_pts = mkpts1[high_conf_indices].astype(np.float32)

    high_conf_count = len(high_conf_indices)
    avg_conf = np.mean(match_scores)

    # 4. RANSAC 几何校验
    inlier_count = 0
    final_inliers_mask = None

    if high_conf_count >= 4:
        src_reshaped = high_conf_src_pts.reshape(-1, 1, 2)
        dst_reshaped = high_conf_dst_pts.reshape(-1, 1, 2)

        try:
            H, inliers = cv2.findHomography(src_reshaped, dst_reshaped, cv2.RANSAC, 5.0)
            if inliers is not None:
                final_inliers_mask = inliers.ravel() > 0
                inlier_count = int(np.sum(final_inliers_mask))
            else:
                final_inliers_mask = np.ones(high_conf_count, dtype=bool)
                inlier_count = high_conf_count
        except cv2.error:
            final_inliers_mask = np.ones(high_conf_count, dtype=bool)
            inlier_count = high_conf_count

    is_same_class = inlier_count >= threshold_count

    # 5. 可视化保存 (由于原版是强制 Resize,坐标就是缩放后的 512x512,需要再映射回原图)
    h1, w1 = img1.shape[:2]
    h2, w2 = img2.shape[:2]

    # 坐标缩放回原图大小
    scale_x1, scale_y1 = w1 / input_size, h1 / input_size
    scale_x2, scale_y2 = w2 / input_size, h2 / input_size

    vis_src_pts = (high_conf_src_pts * np.array([scale_x1, scale_y1])).astype(int)
    vis_dst_pts = (high_conf_dst_pts * np.array([scale_x2, scale_y2])).astype(int)

    # 拼接图像
    vis_img = np.zeros((max(h1, h2), w1 + w2, 3), dtype=np.uint8)
    vis_img[:h1, :w1] = img1
    vis_img[:h2, w1:w1 + w2] = img2

    for i in range(len(vis_src_pts)):
        color = (0, 255, 0) if final_inliers_mask[i] else (0, 0, 255)
        pt1 = tuple(vis_src_pts[i])
        pt2 = tuple(vis_dst_pts[i] + np.array([w1, 0]))
        cv2.line(vis_img, pt1, pt2, color, 1, cv2.LINE_AA)

    os.makedirs(os.path.dirname(save_path) if os.path.dirname(save_path) else ".", exist_ok=True)
    cv2.imwrite(save_path, vis_img)

    # 6. 结果汇总
    result = {
        "原始总匹配数": int(len(matches)),
        f"高置信度匹配数 (> {threshold_conf})": high_conf_count,
        "RANSAC 内点数": inlier_count,
        "平均置信度": round(float(avg_conf), 4),
        "判定结果": "【同一类】" if is_same_class else "【不同类】",
        "可视化保存路径": save_path
    }

    return result


if __name__ == '__main__':
    IMAGE_PATH_1 = r"test_images\001.png"
    IMAGE_PATH_2 = r"test_images\002.png"
    ONNX_MODEL_PATH = r"weights\raco_aliked_lightglue_pipeline_k512.onnx"

    # 判定阈值
    COUNT_THRESHOLD = 100
    CONF_THRESHOLD = 0.8

    image1 = cv2.imread(IMAGE_PATH_1)
    image2 = cv2.imread(IMAGE_PATH_2)

    if image1 is None or image2 is None:
        print("❌ 错误:无法读取图像,请检查图片路径。")
    elif not os.path.exists(ONNX_MODEL_PATH):
        print("❌ 错误:找不到 ONNX 模型文件,请检查模型路径。")
    else:
        print("🚀 正在加载 ONNX 模型...")
        session = ort.InferenceSession(ONNX_MODEL_PATH, providers=['CPUExecutionProvider'])
        print("✅ 模型加载成功!")

        print("🔍 正在执行特征匹配、RANSAC校验与相似度判断...")
        result = judge_similarity(
            image1, image2, session,
            threshold_count=COUNT_THRESHOLD,
            threshold_conf=CONF_THRESHOLD,
            save_path="output/final_judgment.jpg"
        )

        print("\n" + "=" * 40)
        print("📊 终极判定结果:")
        for key, value in result.items():
            print(f"   - {key}: {value}")
        print("=" * 40)

  效果及打印如下:

image

image

image

image

image

image

  前面说了,如果不想下载,可以自动下载并且导出模型:

from pathlib import Path

from lightglue_dynamo.cli import export, infer
from lightglue_dynamo.config import Extractor, InferenceDevice


extractor_type = Extractor.superpoint  # Extractor.disk
model_path = Path(f"weights/{extractor_type}_lightglue_pipeline.onnx")  #导出的目录

export(extractor_type=extractor_type, output=model_path, batch_size=0, height=0, width=0, num_keypoints=1024)

  要用到项目下的lightglue_dynamo:

image

3. 应用场景

  • 同类图片相似度对比
  • 三维重建(比如把一堆照片变成3D模型)

  • 视觉SLAM(机器人/无人机定位导航)

  • 图像拼接(全景图、卫星图拼接)

  • 视觉定位(手机AR、相册搜索)

 

  小结:总的来说,LightGlue用“自适应”的思路,在精度和速度之间找到了一个很好的平衡点,简单图片简单处理,复杂图片认真处理,在CPU下测试速度也是较快的。如果你正在做图像匹配相关的工作,它绝对值得一试。最后把自己下载的两个模型(raco_aliked_lightglue_pipeline_k512.onnx与raco_aliked_lightglue_pipeline_k1024.onnx)也放到网盘了,最后,本文借助AI完成(仅供参考)。

  通过网盘分享的文件:light_glue_onnx
  链接: https://pan.baidu.com/s/17ACPjJx-145RVE6NJVvJQA 提取码: ckr7

 

 

  若存在不足或错误之处,欢迎评论与指正!

posted @ 2026-08-30 10:32  wancy  阅读(37)  评论(0)    收藏  举报