☰
TensorRT+C++部署SuperPoint+SuperGlue:低延迟特征匹配工程实践
2026/10/1 9:11:16 网站建设 项目流程

简介:面向需要把SuperPoint与SuperGlue算法部署到实际视觉系统中的C++开发者,这一压缩包提供基于TensorRT的完整落地思路与可运行工程。资源共50个文件,包含10个头文件与5个C++源文件,用于SuperPoint关键点检测和SuperGlue描述子匹配的推理实现;4个Python脚本负责将PyTorch模型转换成ONNX再生成TensorRT引擎;3个engine和3个onnx覆盖室内外模型权重,可直接调用;另含20张图片、配置yaml、README说明、CMakeLists工程文件和演示gif,包体约209MB。目前已有418人学习。项目安排上,先完成两个算法的C++迁移,再结合TensorRT做图优化、层融合、精度校准与引擎构建;针对实际部署中的性能瓶颈和精度损失均给出排查思路。读者可获得freiburg序列图像推理案例、室内外模型文件、转换脚本、头文件与C++源码,并掌握TensorRT的C++API使用、ONNX转换流程和模型加速技巧,适合具备C++与深度学习基础、希望提升推理速度的工程师。

1. 用 TensorRT 和 C++ 部署 SuperPoint+SuperGlue:先解决一个现实问题

做过视觉 SLAM 或者图像配准的同行应该都有体会:Python 里跑 SuperPoint+SuperGlue 的 Demo 很顺畅,torch 一加载、直接 forward,特征点哗哗往外冒。可一旦你想把它塞进巡检机器人、无人机或者工业检测的实时链路里,事情就开始变味了。模型推理占掉几百毫秒,显存被 torch 的运行时吃掉一大块,GPU 利用率还不高。用 TensorRT 加 C++ 把这个组合部署成生产级服务,是我这两年做视觉前端落地时被验证过的一条可靠路径:推理延迟砍掉一个数量级,显存占用压到几十 MB,整个链路可以被一个几十 KB 的可执行文件包住。这篇文章面向的读者很具体:手里有 SuperPoint+SuperGlue 的权重,想在 Jetson、工控机或数据中心 GPU 上把它做成稳定低延迟的 C++ 服务,并且对 TensorRT 的 engine 构建、动态 shape 和 C++ 端内存管理有真实诉求的人。

2. 先把网络和引擎对齐:SuperPoint、SuperGlue 在 TensorRT 里到底在算什么

2.1 SuperPoint 的编码器与两个输出头:C++ 侧看到的张量是什么样的

SuperPoint 的结构并不复杂,一个 VGG-like 的编码器(卷积加降采样)后面挂着两个头:关键点头和描述子头。关键点头输出的是半分辨率下的 heatmap,形状是1, 1, H/8, W/8,值越大代表这个位置越可能是角点;描述子头输出的是半分辨率下的 256 维描述子,形状是1, 256, H/8, W/8,之后会做 L2 归一化。C++ 侧拿到这两个输出后,要做的事情和 Python 侧完全一样:在 heatmap 上做非极大值抑制(NMS),取出 top-K 个关键点坐标,再按坐标把描述子从特征图里抽出来,按通道维排成K, 256的矩阵。

这一步值得在 TensorRT 的 engine 之外单独做,不要塞进网络图里。原因是 NMS 和坐标索引在 TensorRT 里要么没有原生算子,要么实现起来要拼一堆小算子,性能不一定比在 CUDA 核函数里手写更好。常见做法是让 TensorRT 只输出 heatmap 和描述子图,剩下的取出 top-K、坐标还原到原图分辨率、描述子 L2 归一化,都在 C++ 侧用一个简单的 CUDA kernel 或干脆 CPU 端循环完成。SuperPoint 输入图像一般会被缩放到640x480或480x360,即便在 CPU 端做 NMS 和抽取,开销也就在几毫秒量级,不会成为瓶颈。

描述子的 L2 归一化有个细节要留意。PyTorch 里的实现是在dim=1上做归一化,也就是说按通道维归一化的是每个位置上的 256 维向量;而如果你直接在 C++ 里按行归一化,数据布局不同会产生完全错误的结果。TensorRT 输出的布局默认是 NCHW,描述子张量的内存顺序是C, H, W连续排列的,所以你按空间位置取描述子时,必须按desc[ch * H * W + y * W + x]这样的内存偏移去索引,而不是按desc[y * W + x]去取。这个错误不报错,也不会产生显存越界,但取出来的描述子匹配率会低到让你怀疑模型没转换对。

2.2 SuperGlue 的 GNN 与最优传输:Sinkhorn 迭代放在哪一层

SuperGlue 的核心是两个关键点集之间的匹配。它先把两幅图的描述子经过一个编码器映射成节点特征,然后在图神经网络里做 self-attention 和 cross-attention 的消息传递,最后用一个可学习的最优传输层输出一个得分矩阵,矩阵的维度是M+1, N+1,多出来的那一行一列是 dustbin,代表「没有匹配」的置信度。

这套东西转换到 TensorRT 时有几个关键决策点。attention 层里的 multi-head 结构、线性投影和 LayerNorm,ONNX 导出时基本都能映射到 TensorRT 的算子;真正麻烦的是 Sinkhorn 迭代。Sinkhorn 本质上是一连串的逐行逐列归一化,在 PyTorch 里用一个for循环迭代 20 次左右。如果你把这个循环原样导出到 ONNX,TensorRT 会把每一次迭代展开成一串显式算子,图会变得非常大,而且可能触发某些算子融合失败,构建速度明显变慢,推理延迟也不一定理想。

我一般会把 Sinkhorn 迭代次数从部署时的 20 次下调到 5 到 7 次。SuperGlue 的作者在训练时用的就是逐步减少迭代次数的策略,推理阶段 5 次迭代和 20 次迭代的匹配质量差距在肉眼和常见指标上几乎看不出来,但延迟能省下一大截。至于得分矩阵的后续处理——用阈值过滤低分匹配、按 dustbin 行和列判断 unmatched,这些放在 C++ 侧做,和 SuperPoint 的 NMS 一样,不要塞进 TensorRT。C++ 端处理一个M+1, N+1的矩阵,遍历一遍的代价在几万关键点规模下是微秒级的。

2.3 为什么不用 Python 直接部署:延迟之外还有显存和线程安全

很多人问的一个问题是:Python 有 torch 或者 onnxruntime,为什么要绕一大圈去做 TensorRT C++ 部署?直接原因有三个。第一是延迟,Python 侧每次推理有解释器开销和 torch 的运行时调度开销,单次推理多出几十毫秒在视觉 SLAM 这种循环里是致命的;第二是显存,torch 的 CUDA context 启动就占掉几百 MB,TensorRT 的 engine 本身显存占用可以压到几十 MB 级别;第三是线程安全,Python 的 GIL 和 torch 的多线程推理在真实服务里会让你吃尽苦头,而 C++ 侧一个 context 一个线程,行为可预期得多。

在 Jetson 这类嵌入式设备上,还有一个额外理由:TensorRT 是 NVIDIA 官方在 Jetson 平台上的第一公民,DeepStream 和 VPI 等周边组件都围绕它工作。如果后续想把特征匹配的结果接进目标跟踪或者重定位链路,TensorRT 的 engine 可以直接被其它模块复用,而不用维护一个常驻的 Python 进程。

对比项Python + PyTorchPython + ONNX RuntimeC++ + TensorRT
单次推理延迟(典型 640x480)30~80 ms15~40 ms3~8 ms
显存占用500 MB 以上200 MB 左右50~150 MB
多线程并发GIL 限制需小心配置每 context 独立
嵌入式平台支持受 PyTorch 版本限制一般原生支持

3. 从 pt 到 engine:把 PyTorch 权重转成 TensorRT 能吃的格式

3.1 ONNX 导出:opset 版本和动态轴设在哪

TensorRT 不直接吃.pt权重,这是第一个要迈过去的坎。常见路径是 PyTorch → ONNX → TensorRT engine,这个流程里 ONNX 导出的质量直接决定后面能不能顺利构建 engine。以 SuperPoint 为例,我一般会用下面的脚本导出:

import torch import torch.onnx from superpoint import SuperPoint model = SuperPoint() model.load_state_dict(torch.load("superpoint_v1.pth", map_location="cpu")["model"]) model.eval() dummy_input = torch.randn(1, 3, 480, 640) torch.onnx.export( model, dummy_input, "superpoint.onnx", opset_version=17, input_names=["input"], output_names=["scores", "descriptors"], dynamic_axes={ "input": {2: "height", 3: "width"}, "scores": {2: "height", 3: "width"}, "descriptors": {2: "height", 3: "width"}, }, do_constant_folding=True, verbose=False, )

这里有两个参数要特别注意。dynamic_axes里把高和宽设成了动态轴,这直接对应 TensorRT profile 里的 min/opt/max 配置。SuperPoint 在推理时图像分辨率可能会有变化,如果你把分辨率写死成480x640,后续换一个输入尺寸就必须重新构建 engine,很不划算。opset_version我会选 17 或更高,较低版本对grid_sample、scatter这类算子的支持不完整,导出时不会报错,但转 TensorRT 时会出现不支持的节点。

导出完成后,先用 onnxruntime 或onnx.checker验证一遍。有一个很隐蔽的问题:ONNX 导出时把torch.max或argmax这类带索引的算子展开成了多个基础算子,某些展开方式会让 TensorRT 的图优化器无法融合,导致构建变慢。如果遇到这种情况,可以在导出时用torch.onnx.export的operator_export_type参数切换导出模式,或者干脆在模型代码里把max替换成amax这类 ONNX 原生支持的算子。

3.2 用 trtexec 构建 engine:三个 profile 参数怎么填

拿到 ONNX 之后,构建 engine 有两种方式:直接用 NVIDIA 自带的trtexec命令行工具,或者在 C++ 代码里调用OnnxParser。我建议第一次构建用 trtexec,因为构建信息一目了然;确认参数没问题后再把构建逻辑写进代码里。以 SuperPoint 为例,一条典型命令是这样:

trtexec \ --onnx=superpoint.onnx \ --saveEngine=superpoint.engine \ --minShapes=input:1x3x360x480 \ --optShapes=input:1x3x480x640 \ --maxShapes=input:1x3x720x1280 \ --fp16 \ --workspace=2048

minShapes、optShapes、maxShapes三个参数定义了这个 engine 能接受的输入尺寸范围,TensorRT 会分别为三个档位做图优化。optShapes设置为实际部署中最常见的分辨率,minShapes和maxShapes是防边界情况用的,范围设得越大,TensorRT 生成的 kernel 越多,engine 文件也越大。一般部署场景里min = 实际最小值、opt = 实际主要值、max = 实际最大值就够了,不要把范围设置到模型完全不支持的尺寸。

--fp16有个坑:它只开启 FP16 计算,不改变输入输出张量的数据类型。输入仍然是 FP32,TensorRT 在内部把能安全转换的层切换成 FP16,精度敏感的层保持 FP32。SuperGlue 里的 LayerNorm 和 Sinkhorn 归一化对精度比较敏感,如果强制全精度转换,匹配质量会下降。TensorRT 从 8.x 开始有自动精度选择机制,你只需要启用--fp16并允许 TensorRT 自主决定哪些层用 FP16,而不是用--precision=FP16去强制所有层。

构建完成后一定要检查日志里的Total Host Memory、Total Device Memory和每个 layer 的推理时间。如果某个 layer 的时间异常长,比如 Sinkhorn 展开后的某个归一化层花了 1 毫秒以上,说明这个 layer 没有被有效融合,回到 ONNX 导出那一步去改算子可能是更高效的路径。

3.3 量化精度档位:FP16 和 INT8 的取舍

TensorRT 支持 INT8 量化,理论上能把延迟再砍一半,但 SuperPoint 和 SuperGlue 是典型的「尾部精度敏感」模型,INT8 量化后特征点的重复性和描述子的区分度都会有可感知的下降。我的经验是:在 Jetson 系列(Orin 及以后)上用 FP16 完全够用;在更老的嵌入式 GPU 上如果确实需要 INT8,务必准备一个真实分布的数据集做校准,而不是随机生成几万张图草草校准。

校准集的选择直接影响 INT8 效果。校准数据要覆盖模型在部署时可能见到的所有场景——室内、室外、弱纹理、过曝——每类至少几百张。calibration cache 文件要保留好,换 TensorRT 版本后如果 engine 需要重建,cache 还在就能省去重新校准的耗时。还要注意,校准数据不参与梯度计算,但要走一遍完整的前向推理,所以校准耗时等于数据量乘以单次推理时间,几百张图在 GPU 上几分钟内跑完,不算门槛。

4. C++ 部署:把 engine 接进 SuperPoint+SuperGlue 的处理链

4.1 engine 加载与输入输出绑定:搞清楚指针和生命周期

engine 构建完成的产物是一个二进制文件,C++ 侧的工作从这个文件开始。加载和推理的核心代码大致如下:

#include <NvInfer.h> #include <fstream> #include <vector> std::vector<char> loadEngineFile(const std::string& path) { std::ifstream file(path, std::ios::binary | std::ios::ate); size_t size = file.tellg(); file.seekg(0, std::ios::beg); std::vector<char> buffer(size); file.read(buffer.data(), size); return buffer; } auto readFile = loadEngineFile("superpoint.engine"); auto runtime = std::unique_ptr<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger)); auto engine = std::unique_ptr<nvinfer1::ICudaEngine>( runtime->deserializeCudaEngine(readFile.data(), readFile.size(), nullptr)); auto context = std::unique_ptr<nvinfer1::IExecutionContext>( engine->createExecutionContext()); int inputIndex = engine->getBindingIndex("input"); int scoreIndex = engine->getBindingIndex("scores"); int descIndex = engine->getBindingIndex("descriptors"); // 动态 shape 必须显式设置 context->setInputShape(inputIndex, nvinfer1::Dims4(1, 3, 480, 640)); context->setTensorAddress(inputIndex, inputDevicePtr); context->setTensorAddress(scoreIndex, scoreDevicePtr); context->setTensorAddress(descIndex, descDevicePtr);

setInputShape是动态 shape 场景下最容易漏的一步。如果你跳过它,推理会直接失败或产生错误结果,报错信息还不太直观。setTensorAddress绑定的是 device 指针,C++ 侧需要你自己用cudaMalloc分配好输入输出的显存并把指针递进去。

生命周期管理是这里最常见的坑。engine和context都是智能指针管理,但runtime的生命周期必须长过 engine 和 context,否则在程序退出时会出现 cuda 相关的崩溃。另外,如果你在服务里想多线程并发推理,一个好的模式是:一个engine创建多个context,每个线程持有一个独立的context,而不是每线程持有一个独立的 engine。context比engine轻量得多,且共享引擎的权重参数,显存占用更低。

4.2 预处理:归一化、BGR/RGB 和内存布局

SuperPoint 在 PyTorch 里的预处理通常是:读图 → resize → 转 RGB → 归一化到[0, 1]→ 转 CHW → 加 batch 维。C++ 侧做同样的事情,但要注意三个常被忽略的细节。

第一是颜色通道顺序。OpenCV 读进来是 BGR,PyTorch 训练时用的是 RGB,如果直接把 OpenCV 的图喂给 TensorRT,特征点质量会显著下降。常见的做法是在 CUDA kernel 里顺手做 BGR→RGB 的转换,不要先在 CPU 上做一次cvtColor再拷贝到显存,那样多一次 PCIe 拷贝。第二是归一化参数,PyTorch 里是标准归一化(x / 255.0),没有 mean 和 std,但有些人的自定义训练脚本里用了mean=[0.485, 0.456, 0.406],这两者的预处理必须和训练时完全一致,否则 model 拿到的是分布漂移的输入,输出没有任何意义。第三是 HWC 到 CHW 的转换,TensorRT 默认要求 NCHW 布局,OpenCV 的Mat是 HWC 布局,这个转换在 CUDA kernel 里一起做掉:

__global__ void preprocessKernel( const uint8_t* src, float* dst, int width, int height) { int idx = blockIdx.x * blockDim.x + threadIdx.x; int total = width * height; if (idx >= total) return; int x = idx % width; int y = idx / width; int srcIdx = y * width + x; int dstIdx = x * height + y; // CHW 布局下的偏移 // BGR -> RGB,并归一化到 [0, 1] dst[0 * width * height + y * width + x] = src[srcIdx * 3 + 2] / 255.0f; dst[1 * width * height + y * width + x] = src[srcIdx * 3 + 1] / 255.0f; dst[2 * width * height + y * width + x] = src[srcIdx * 3 + 0] / 255.0f; }

这个 kernel 是完整可用、能直接编译的版本。dst的空间是按(C, H, W)分配的,dst[0 * H * W + y * W + x]对应通道 0 的第(y, x)个像素,索引写错会得到一张完全混乱的图,而且不一定报错。把预处理直接塞进显存操作,能免掉 CPU↔GPU 之间的多次拷贝,在图像尺寸大的时候省下的延迟不容忽视。

4.3 后处理与匹配 pipeline:把两个模型串成一条流水线

SuperPoint 推理输出原始张量后,C++ 侧要把它们接进 SuperGlue,再把匹配结果输出到上层模块。完整流程拆成下面几步:

// 1. 取出 top-K 关键点 std::vector<cv::KeyPoint> keypoints; extractTopK(scoresHost, width, height, k, confidence_threshold, keypoints); // 2. 按坐标抽取描述子,L2 归一化 std::vector<float> descs(k * 256); collectDescriptors(descHost, keypoints, descs.data(), width, height); normalizeDescriptors(descs.data(), k); // 3. 构造 SuperGlue 的输入 float* glueInputHost = new float[2 * k * 256]; memcpy(glueInputHost, descs.data(), k * 256 * sizeof(float)); // 把第二幅图的描述子接到后面 // 4. 推理 SuperGlue,得到 M+1 行 N+1 列的得分矩阵 std::vector<float> scoresMatrix((M + 1) * (N + 1)); // 5. 阈值过滤 + dustbin 处理 std::vector<cv::DMatch> matches; filterMatches(scoresMatrix.data(), M, N, threshold, matches);

关键点的置信度阈值是一个需要调的参数。SuperPoint 默认置信度阈值在 0.015 左右,但在不同的光照和纹理条件下,这个值可能需要调整。如果匹配点太少,先别调 SuperGlue 的参数,试着把 SuperPoint 的 top-K 从 1024 提高到 2048 往往立竿见影。top-K 越大,SuperGlue 的 attention 计算量平方级增长,所以这个值要在匹配数量和延迟之间找平衡。

SuperGlue 的得分矩阵处理有个细节:矩阵的行代表第一幅图的关键点,列代表第二幅图的关键点,(M, N)位置的 score 同时满足两个条件才算是有效匹配——该位置的 score 大于阈值,且它同时是这一行的最大值、也是这一列的最大值。这个「双向最大值」检查不能省,否则会得到大量一对多的错误匹配。

5. 部署避坑:5 个让 engine 白构建的常见翻车点

5.1 TensorRT 10.x 在老 GPU 上报「sm_xx not supported」

现象:在 GTX 1070 或者更老的 Maxwell 架构 GPU 上用 TensorRT 10.x 加载 engine,直接报错说支持的 SM 版本不匹配,或者构建时找不到对应的 CUDA capability。

原因:TensorRT 10.x 的官方预编译包默认不再包含 Pascal 及更老架构的支持。GTX 1070 是 6.1 计算能力,而新版本 TensorRT 的某些 kernel 是为 7.5 及以上的 Turing 架构编译的。这属于版本兼容问题,不是代码 bug。

解决:先确认自己 GPU 的计算能力,查到之后去 NVIDIA 官网查 TensorRT 的 release note,看该版本最低支持到哪个 SM。如果是 Pascal 架构的老卡,锁到 TensorRT 8.5 LTS 或 8.6 版本,同时在构建命令里显式指定--deviceType=GPU和对应的 SM 参数,不要盲目追新。在 Jetson 设备上同理,JetPack 自带的 TensorRT 版本和 GPU 算力是匹配过的,自己重装新版 TensorRT 经常踩进这个坑里。

5.2 ONNX 导出成功,但 trtexec 报「Unsupported Operator」

现象:torch.onnx.export一路顺利,onnxruntime 也能跑通,一进 trtexec 就报某层不支持的算子,常见于torch.nn.functional.grid_sample或某些scatter、index_put操作。

原因:PyTorch 的 ONNX 导出器对部分算子采用的是「分解」策略——把一个高层算子拆成多个 ONNX 基础算子。这些基础算子互相组合时,可能超出 TensorRT 的算子支持范围。尤其在动态 shape 场景下,某些降级用的算子组合无法被解析。

解决:查看报错日志里具体是哪一层不兼容,回到 PyTorch 代码里把那一段改写。SuperPoint 的 NMS 如果在模型内部实现,大概率就是罪魁祸首——把它从模型里摘掉,放在 C++ 侧做。另一个备选方案是升级 ONNX 的 opset 版本,新版本里 TensorRT 的 parser 支持度更高;再不行就查 TensorRT 版本的 operator support 文档(在官方安装包 doc 目录里有)。

5.3 动态 shape 下 engine 构建成功,但推理时显存暴涨

现象:固定 shape 构建的 engine 完全正常,改成动态 shape 构建后,推理过程中nvidia-smi里显存占用持续攀升,或者偶尔触发 cuda OOM。

原因:minShapes和maxShapes范围跨度太大,TensorRT 在构建时按最大尺寸分配了中间缓冲。比如 max 设成720x1280,而实际推理一直在480x640,中间 activation 的缓冲区远大于实际需要。如果启用了多个 context,累积下来的显存消耗会非常可观。

解决:收紧 maxShapes 到实际业务场景的最大值。如果业务里有 4K 图的需求,但高频场景是 1080p,我一般会准备两个 engine:一个 maxShapes 覆盖到 4K 用于低频高精度场景,一个 maxShapes 限制在 1080p 用于实时链路。如果你同时用多个 context,给 context 里setInputShape传入实际输入尺寸,TensorRT 会按实际尺寸分配部分缓冲,但 actovation 缓冲的分配策略还是要看构建时的 profile 设置。

5.4 FP16 开启后匹配质量肉眼可见地变差

现象:不开--fp16时,两幅图的匹配结果很干净;开 FP16 后,匹配数量明显减少,或者出现一批明显错误的跨区域匹配。

原因:SuperGlue 里有 LayerNorm 和 Softmax,这两类操作在 FP16 下的动态范围不够,导致中间结果截断误差被后续的 attention 放大。TensorRT 的--fp16默认是「尽可能用 FP16」,但它在精度收益不明确时不会自动回退到 FP32。

解决:用--precision和--layerPrecisions参数做层级别的精度控制。优先把 LayerNorm、Softmax、Sinkhorn 相关的层锁定为 FP32,其余卷积层保持 FP16。另一个更实用的办法是直接不用成对比较肉眼效果,用指标说话:把匹配结果的 AUC 或正确率数值跑出来,低于阈值就继续调,高于阈值就收工。精度调优是层层逼近的过程,不要指望一把到位。

5.5 第二次推理开始结果错乱或程序崩溃

现象:第一次推理完全正常,第二次开始输出数据不对,或者在退出时抛 cuda error。

原因:最典型的原因为显存指针复用错误。输入输出缓冲区在第一次推理后被覆盖,或者 buffer 被释放后 engine 内部还有指针引用着它。另一个原因是 event 同步缺失——异步推理时 CUDA stream 没有同步,下一次推理的写入把上一次没算完的数据覆盖了。

解决:确认输入输出 buffer 的生命周期长于所有推理操作,并在每次推理结束后调用cudaStreamSynchronize。多线程场景下,每个线程要有独立的cudaStream和独立的IExecutionContext,绝对不要在不同线程间共享 context。程序退出时的崩溃,检查runtime、engine、context的析构顺序,runtime最后析构,确保 device 资源全部释放后再销毁 runtime。

6. 让部署再进一步:engine 复用、异步流水线和一个验证指标

上面讲的是把流程跑通,这一节说三个让它变得实用的经验。

第一个是 engine 的复用策略。engine 文件从二进制反序列化后,如果你在进程里创建多个 context,它们会共享权重。这在做多路视频流时非常关键:一路视频流一个 context,每路共享同一份模型权重,显存只涨 activation 部分不涨权重部分。常见做法是启动时加载一次 engine,之后每次新连接只创建 context,这样可以显著降低单路显存占用。

第二个是 CUDA stream 的使用。把输入图像的预处理、SuperPoint 推理、关键点后处理、SuperGlue 推理放到两条 stream 里流水线化。第一条 stream 做预处理和 SuperPoint 推理,第二条 stream 等 SuperPoint 的 event 信号后再启动 SuperGlue 推理。关键点是不要让 CPU 端在两次推理之间停下来等待拷贝完成——把同步点安排得越少,GPU 利用率就越高。如果图像分辨率不大,CPU 端的后处理也可以并行做,不必等 GPU 全部结束。

第三个是验证方法。不要用肉眼判断部署结果。我一般会拿 HPatches 序列里的几百对图像跑一遍,计算一个简单指标:匹配点中几何一致的比率。做法是用基础矩阵做 RANSAC 校验,内点比例高于 60% 就认为这个部署精度是可接受的。这个指标不依赖 ground truth,任何场景都能立刻算出来,比肉眼数匹配点靠谱得多。

每个新模型的部署我都会保留三个东西:导出 ONNX 时的导出日志、trtexec 的构建日志、第一次推理时的性能日志。这三份日志是排障的后悔药。TensorRT 的版本升级、算子行为变化都是黑匣子,没有日志对照,每翻一次车都要从头排查。

希望这些踩坑经验能帮你把 SuperPoint 和 SuperGlue 的 TensorRT 部署这条路走得更顺。整个方向是值得投入的,把视觉前端的延迟从几十毫秒压到几毫秒之后,上层能做的事情会比现在多得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询