☰
ONNXRuntime部署UFLD-v2车道线检测:从Python到C++全流程
2026/10/3 2:46:40 网站建设 项目流程

简介:这是一份基于ONNXRuntime部署Ultra-Fast-Lane-Detection-v2的车道线检测工程,面向自动驾驶、智能交通领域的开发者,提供C++与Python双语言推理源码,适合需要快速落地实时车道线识别功能的算法工程师或嵌入式开发者。资源包共23个文件,压缩体积4.35MB,包含2个Python脚本、2个C++源文件、18张测试图片及1份说明文档,图片可用于效果验证,源码对应不同语言调用方式,文档则介绍部署流程与模型转换要点。目前已有548人浏览学习,兼具实用性与参考价值。通过这份资源,读者可掌握从ONNX模型加载、图像预处理到推理后处理的完整链路,并对比C++与Python在推理性能上的差异;同时,项目展示了如何利用ONNXRuntime的Session接口高效运行车道线检测模型,为后续在嵌入式平台或服务端扩展提供了可复用的代码范式。

1. 先弄清三件事:ONNXRuntime 与 UFLD-v2 的部署边界

跑车道线模型的部署,最常见的情况是:PyTorch 里训练和验证都正常,指标也好看,一旦要放进 C++ 工程或者换到不带 torch 的机器上,就卡住了。Ultra-Fast-Lane-Detection-v2 是典型的行分类式车道线检测模型,它不像分割模型那样输出整张 mask,而是输出一组概率分布;这让它的 ONNX 导出和后处理都跟直觉不太一样。ONNXRuntime 在这里的价值不是加速,而是把 PyTorch 的模型扒掉训练框架依赖,变成一个 C++ 和 Python 都能读的推理产物。这套方案解决的核心问题是:同一个 ONNX 文件,怎么在 Python 里快速验证、在 C++ 里稳定集成,以及推理前后那几步处理为什么不能乱改。适合手里有训练好的模型、正打算往嵌入式设备或服务端迁的工程师。

2. 模型输出与预处理:ONNX 黑匣子里装着什么

2.1 行分类与关键点:UFLD-v2 并不是在画线

UFLD-v2 的做法,是把车道线检测拆成「每一行上找车道线的 x 位置」。拿到一张图后,模型先在高度方向预设若干行锚点(row anchor),每一行上再划分成固定数量的网格(grid);模型输出的不是连续的 x 坐标,而是每个网格的概率。后处理时对概率分布求最大下标,再把下标换算成像素坐标。

这个设计对部署非常友好:输出张量形状固定,不会出现分割 mask 那种 H×W 的大块内存;同时它天然把检测结果行行对齐,方便做透视变换和拟合。但代价是,后处理必须知道 row anchor 的具体数值和 grid 数量,否则画出来的线会整段错位。v2 相对 v1 主要改了训练损失和辅助分支,推理输出结构基本沿用了「位置概率 + 存在性」的组合。

2.2 输入张量规格:从 BGR 到 NCHW 的归一化细节

部署 ONNX 时,最容易踩的第一个坑就是输入约定。OpenCV 读图出来是 HWC 排列、BGR 通道,而 ONNX 模型期望的是 NCHW 排列、RGB 通道。转换顺序必须是:先 BGR 转 RGB,再 HWC 转 CHW,最后扩出 batch 维。归一化方面,UFLD-v2 常见训练配置是直接除以 255,并没有用 ImageNet 的 mean/std,但不同导出脚本也可能带归一化层。

import cv2 import numpy as np img = cv2.imread("test.jpg") img = cv2.resize(img, (800, 288)) # 宽 800,高 288,注意顺序 img = img[:, :, ::-1] # BGR -> RGB img = img.transpose(2, 0, 1) # HWC -> CHW img = np.ascontiguousarray(img, dtype=np.float32) img = img / 255.0 # 与训练预处理保持一致 img = img[None, ...] # 增加 batch 维 -> (1,3,288,800)

这段代码里,cv2.resize的宽高顺序与模型输入 shape 相反,写成(800, 288)而不是(288, 800)。ascontiguousarray保证内存连续,避免 ONNXRuntime 在读取时因为 stride 不连续而告警或复制。归一化方式一定要以导出模型时的预处理为准:如果导出脚本里写的是减均值除方差,这里就不能只做255缩放,否则推理结果与训练时分布不一致,表现会骤降。

2.3 ONNX 导出参数:固定 Shape 还是动态 Shape

导出 ONNX 看似一条命令,但有两个参数会直接影响后续部署难度:opset_version和dynamic_axes。对于 UFLD-v2 这种行分类模型,建议固定输入输出 shape 导出,也就是dynamic_axes设为None。固定 shape 后,ONNXRuntime 可以做更多图优化,C++ 端的内存分配也简单,不需要每次跟着 shape 变化重新规划。

import torch model = load_model_from_checkpoint("model.pth") model.eval() dummy = torch.zeros(1, 3, 288, 800, dtype=torch.float32) torch.onnx.export( model, dummy, "model.onnx", input_names=["input"], output_names=["loc", "exist"], # 以实际模型输出头为准 opset_version=11, do_constant_folding=True, dynamic_axes=None # 静态 shape 导出 )

opset_version=11是兼容性比较好的选择,ONNXRuntime 从 1.4 开始就能稳定跑 opset 11 的模型。输出名这里写成loc和exist,但不同分支的导出脚本可能带embed之类的额外输出,务必用 Netron 打开 onnx 确认。do_constant_folding=True会把 BN 层和常量算子折叠掉,减小模型体量,也让 ONNXRuntime 加载更快。

2.4 ONNX 与 ONNXRuntime 的区别:导出和推理是两回事

很多人在这一步把概念混在一起:ONNX 只是模型的一种中间表示格式,描述计算图和权重,它本身不会执行推理;ONNXRuntime 是读取这个格式的推理引擎,负责在 CPU、GPU 或 NPU 上把图跑起来。你可以把 ONNX 类比成一份「源码工程」,ONNXRuntime 是编译器加运行时。工程文件写得再标准,没有编译器也跑不出结果。

因此部署路径是:PyTorch 导出 ONNX,再用 ONNXRuntime 加载并推理。如果中间某一步报算子不支持,问题几乎都出在导出阶段,而不是推理阶段。排查时先看 ONNX 是否通过了onnx.checker.check_model,再看 ONNXRuntime 加载时的 warning,这两步能筛选掉八成问题。

3. Python 源码部署:从 ONNX 加载到画出车道线

3.1 环境准备:虚拟环境与 onnxruntime 安装

Python 端不用装 torch,只要一个干净的推理环境。建议用 venv 隔离,不要往系统 Python 里直接塞包,尤其是机器上同时存在多个项目时,依赖冲突会让人浪费大量时间。onnxruntime 用 pip 安装即可,CPU 版和 GPU 版的包名不一样。

python3 -m venv .venv source .venv/bin/activate pip install onnxruntime opencv-python numpy

如果需要 GPU 加速,把onnxruntime换成onnxruntime-gpu,注意两个包不能同时安装,否则 import 时会被覆盖。Windows 上如果之前没有装过 Visual C++ Redistributable,onnxruntime 的 DLL 可能加载失败,装一遍对应版本的 vc_redist.x64.exe 就能解决。VS Code 里直接选.venv作为解释器,避免命令行和编辑器用的不是同一个环境。

3.2 最小推理脚本:加载模型、预处理、拿输出

模型加载和推理本身代码很少,难点在于搞清楚 session 的输入输出是谁。写脚本时先不急着接后处理,把每个输出的名字和 shape 打出来,这一步能省下后面大量猜测时间。

import onnxruntime as ort import numpy as np import cv2 sess = ort.InferenceSession( "model.onnx", providers=["CPUExecutionProvider"] ) for inp in sess.get_inputs(): print("input:", inp.name, inp.shape) for out in sess.get_outputs(): print("output:", out.name, out.shape)

打印结果会显示类似output: loc [1, 4, 56, 200],意思是 4 条车道线、56 个 row anchor、200 个网格。这个 56 和 200 不是写死的,而是模型自带的结构参数。如果输出里还有 2 维张量,比如[1, 4],那基本就是每条车道线的存在性概率。拿到这些信息后,再写正式的推理函数。

import onnxruntime as ort import numpy as np import cv2 class LaneDetector: def __init__(self, onnx_path): self.sess = ort.InferenceSession( onnx_path, providers=["CPUExecutionProvider"]) self.input_name = self.sess.get_inputs()[0].name def preprocess(self, bgr, dst_w=800, dst_h=288): img = cv2.resize(bgr, (dst_w, dst_h)) img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 return img[None, ...] def infer(self, bgr): tensor = self.preprocess(bgr) outputs = self.sess.run(None, {self.input_name: tensor}) return outputs

self.sess.run(None, ...)表示返回所有输出,顺序与get_outputs()一致。这里不写死输出名,后续接后处理时再按 shape 判断哪个是 loc、哪个是 exist,这样即使换了一个输出头顺序不同的模型,也只需要改判断逻辑而不动推理框架。

3.3 后处理:把 4 个头的输出解析成可视化的车道线

拿到原始输出后,第一步是从四维张量里取 batch 为 0 的数据,第二步对 loc 在最后一个维度做 argmax,同时保留最大概率值用于过滤低置信度位置。exist 则直接过 sigmoid 或阈值。

def postprocess(outputs, img_w=800, img_h=288, conf_thresh=0.3, exist_thresh=0.5): loc = None exist = None for out in outputs: if out.ndim == 4: loc = out[0] # (num_lanes, num_rows, grid) elif out.ndim == 2: exist = np.squeeze(out) # (num_lanes,) num_lanes, num_rows, grid = loc.shape grid_idx = np.argmax(loc, axis=-1) # (num_lanes, num_rows) max_prob = np.max(loc, axis=-1) # 每个位置的置信度 lanes = [] for l in range(num_lanes): if exist is not None and exist[l] < exist_thresh: continue pts = [] for r in range(num_rows): if max_prob[l, r] < conf_thresh: continue x = grid_idx[l, r] * img_w / grid y = row_anchors[r] * img_h pts.append((float(x), float(y))) lanes.append(pts) return lanes

grid_idx乘img_w / grid是把网格下标换算成像素 x 坐标,这个换算里的img_w必须是模型输入宽度,不是原图宽度;原图缩放的问题后面统一处理。row_anchors是一个长度等于num_rows的数组,内容是 0 到 1 归一化的 y 坐标,需要从训练配置里拷贝出来。不同数据集、不同训练配置生成的 row_anchor 不一样,不要从别的项目里复用,否则 y 坐标全部错位。

3.4 参数调整:confidence 阈值、row anchor 对齐与可视化

conf_thresh和exist_thresh是部署时最值得调的两个参数。conf 阈值控制单行点的保留,值太大会断线,值太小会把护栏和路沿的误检画进来;exist 阈值控制整条车道线是否显示,在 CULane 这类 4 类固定场景里可以放低到 0.3,在 Tusimple 上可以稍高。调参时把两个阈值放到配置文件里改,尽量别在代码里硬编码。

可视化时,直接把后处理得到的像素坐标画在经resize后的图上会偏小。建议先画在模型输入尺寸的图上,再用cv2.resize放大到原图,或者把所有坐标在画图前统一乘上缩放系数。另一个常见做法是把相邻 row 点用cv2.polylines连起来,但离散点更适合用cv2.line逐段连,避免曲线在断点处出现折线。最后把存在性低于阈值的整条线剔除,再输出去重后的结果。

4. C++ 源码部署:用 ONNXRuntime 动态库封装 LaneDetector

4.1 CMake 工程结构与 ONNXRuntime 动态库链接

C++ 端的目标不是把 Python 代码翻译一遍,而是封装成一个可复用的推理类。工程上推荐把模型加载、预处理、推理、后处理拆成四个模块,编译成静态库或直接塞进现有项目。ONNXRuntime 以动态库形式提供,需要拿到onnxruntime.h头文件和对应的libonnxruntime.so(Linux)或onnxruntime.dll(Windows)。

cmake_minimum_required(VERSION 3.16) project(lane_detector LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(ONNXRUNTIME_DIR /opt/onnxruntime) # 改成实际解压路径 find_package(OpenCV REQUIRED) add_library(onnxruntime SHARED IMPORTED) set_target_properties(onnxruntime PROPERTIES IMPORTED_LOCATION ${ONNXRUNTIME_DIR}/lib/libonnxruntime.so INTERFACE_INCLUDE_DIRECTORIES ${ONNXRUNTIME_DIR}/include ) add_executable(lane_demo src/main.cpp src/lane_detector.cpp) target_link_libraries(lane_demo PRIVATE onnxruntime ${OpenCV_LIBS})

IMPORTED_LOCATION指向动态库实际路径,不能只写-lonnxruntime让链接器自己找,因为 ONNXRuntime 的安装目录往往不在系统默认搜索路径里。源文件建议放在src/下,include/下放lane_detector.h,这样后续加入 TensorRT 或 CUDA Execution Provider 时不用改工程结构。Windows 上要把onnxruntime.dll拷贝到可执行文件旁边,或者把它的目录加入 PATH,否则运行时报找不到 DLL。

4.2 Ort::Session 封装与输入输出读取

ONNXRuntime 的 C++ API 整体上是 RAII 风格,Ort::Session必须在Ort::Env存活期间使用,所以推荐用unique_ptr持有二者,避免析构顺序出错。创建会话时显式设置优化级别,默认值有时候不会开启所有图优化。

#include <onnxruntime_cxx_api.h> #include <memory> #include <string> class LaneDetector { public: explicit LaneDetector(const std::string& model_path) { env_ = std::make_unique<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, "lane"); Ort::SessionOptions opts; opts.SetGraphOptimizationLevel( Ort::GraphOptimizationLevel::ORT_ENABLE_ALL); opts.SetIntraOpNumThreads(4); session_ = std::make_unique<Ort::Session>(*env_, model_path.c_str(), opts); Ort::AllocatorWithDefaultOptions allocator; input_name_ = session_->GetInputNameAllocated(0, allocator).get(); output_names_.push_back(session_->GetOutputNameAllocated(0, allocator).get()); if (session_->GetOutputCount() > 1) { output_names_.push_back(session_->GetOutputNameAllocated(1, allocator).get()); } } private: std::unique_ptr<Ort::Env> env_; std::unique_ptr<Ort::Session> session_; std::string input_name_; std::vector<std::string> output_names_; };

GetInputNameAllocated返回的是Ort::AllocatedStringPtr,如果直接把它传给Run,指针生命周期可能提前结束。常见做法是拷贝到std::string再转成const char*,或者在每次Run前临时取指针。SetIntraOpNumThreads(4)在推理线程内部控制算子并行度,四核机器上设 4 通常够用,设太高反而因为线程切换拖慢单次延迟。

4.3 后处理与车道线解析:把概率图还原成坐标

推理时最关键的是把cv::Mat的数据拷进std::vector<float>,再构造成Ort::Value。注意 ONNXRuntime 会按 tensor shape 读取内存,必须保证数据在内存里是紧密排列的,不能直接拿cv::Mat的data指针去构造多维 tensor。

std::vector<float> input_tensor_data(1 * 3 * H * W); // 假设 src 是已经 resize 到 W x H 的 BGR 图 int idx = 0; for (int c = 2; c >= 0; --c) { // BGR -> RGB,channel 反转 for (int i = 0; i < H; ++i) { for (int j = 0; j < W; ++j) { input_tensor_data[idx++] = src.at<cv::Vec3b>(i, j)[c] / 255.0f; } } } std::array<int64_t, 4> input_shape{1, 3, H, W}; Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>( memory_info, input_tensor_data.data(), input_tensor_data.size(), input_shape.data(), input_shape.size()); const char* input_names[] = {input_name_.c_str()}; std::vector<const char*> out_names; for (auto& n : output_names_) out_names.push_back(n.c_str()); auto output_tensors = session_->Run(Ort::RunOptions{nullptr}, input_names, &input_tensor, 1, out_names.data(), out_names.size());

三层循环里先遍历 channel 再遍历高和宽,这和 NCHW 布局一致。CreateTensor不会拷贝数据,它只是用指针和 shape 信息构造视图,因此input_tensor_data的生命周期必须覆盖session_->Run调用,不能提前析构。拿到output_tensors后,用GetTensorTypeAndShapeInfo().GetShape()推断维度,再按 row anchor 还原坐标。

const float* loc_data = output_tensors[0].GetTensorData<float>(); auto shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape(); // shape: [num_lanes, num_rows, grid] int num_lanes = static_cast<int>(shape[0]); int num_rows = static_cast<int>(shape[1]); int grid = static_cast<int>(shape[2]); std::vector<std::vector<cv::Point2f>> lanes(num_lanes); for (int l = 0; l < num_lanes; ++l) { float exist_score = 1.0f; if (output_tensors.size() > 1) { exist_score = output_tensors[1].GetTensorData<float>()[l]; } if (exist_score < 0.5f) continue; for (int r = 0; r < num_rows; ++r) { const float* row_ptr = loc_data + (l * num_rows + r) * grid; int best = static_cast<int>(std::max_element(row_ptr, row_ptr + grid) - row_ptr); float prob = row_ptr[best]; if (prob < 0.3f) continue; float x = best * W / static_cast<float>(grid); float y = row_anchors[r] * H; lanes[l].emplace_back(x, y); } }

exist_score如果存在性概率低于阈值,直接跳过整条线,减少后续拟合的无效计算。std::max_element返回指针,用指针差值得到网格下标。这里W和H用的是模型输入的 800 和 288,假如要在地图坐标或原图画线,最后再做一次仿射缩放,不要在循环里反复算。

4.4 在 VS Code 里调试:launch.json 与 C++ 调试技巧

C++ 部署最容易翻车的地方不是算法,而是环境。VS Code 里调试时需要配置两个文件:tasks.json负责构建,launch.json负责启动调试器。launch.json里要指定program指向编译出的可执行文件,并把工作目录设成可执行文件所在目录,否则相对路径下的model.onnx和测试图片全都找不到。

{ "version": "0.2.0", "configurations": [ { "name": "lane_debug", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/build/lane_demo", "args": [], "cwd": "${workspaceFolder}", "environment": [ { "name": "LD_LIBRARY_PATH", "value": "/opt/onnxruntime/lib" } ] } ] }

LD_LIBRARY_PATH里要带上 onnxruntime 动态库的目录,否则启动时直接报找不到libonnxruntime.so。在Run之前,可以在输入 tensor 构造处打断点,检查input_tensor_data前几个浮点值是不是和 Python 预处理结果一致;如果第一个值就是 255,说明cv::Mat的at<Vec3b>访问方式和 channel 顺序写错了。

5. 部署避坑记录:Python 与 C++ 双端最容易翻车的 5 个场景

5.1 C++ 直接崩溃:access violation 与 Visual C++ Redistributable

现象:程序在session_->Run调用瞬间崩溃,错误码是0xC0000005 access violation,调试器里看不出具体哪一行是根因。

原因:最常见的是 onnxruntime 动态库本身依赖的 Visual C++ 运行库缺失或版本过旧,尤其是从别处拷贝的onnxruntime.dll与当前系统运行库不匹配;另一个常见原因是input_tensor_data的std::vector被优化器提前析构,CreateTensor指针失效,Run 时访问到野指针。

解决:先安装对应版本的 Visual C++ Redistributable,确认系统里存在vcruntime140.dll和msvcp140.dll;再检查input_tensor_data的生命周期,确保它定义在Run调用所在的函数作用域内,不要放在条件分支里。如果这两步都没问题,把ORT_LOGGING_LEVEL_WARNING临时改成VERBOSE,看崩溃前最后一条日志在哪。

5.2 Python 导出成功但加载报 UnsupportedOperator

现象:torch.onnx.export正常完成,onnx.checker.check_model也通过,但 ONNXRuntime 加载模型时提示No Op registered for ...。

原因:导出时opset_version设得过高,或者模型里有自定义算子没注册到 ONNX。很多训练代码用到了较新的 PyTorch 算子,导出到高 opset 后 ONNXRuntime 的算子库还没有完整覆盖。

解决:把opset_version降回 11,同时加上do_constant_folding=True,让一部分复杂算子折叠成常量。如果仍然报错,找到报错里提到的算子名,检查是不是aten::前缀——这类算子多半是训练代码里调了某些不常用的 PyTorch 函数,需要把对应逻辑改成 ONNX 支持的等价操作后重新导出。

5.3 部署效果和 PyTorch 完全对不上:归一化不一致

现象:同一张测试图,PyTorch 里检测结果正常,ONNXRuntime 推理后车道线全都画在错误位置,或者置信度明显偏低。

原因:预处理链路不一致。PyTorch 训练时是BGR->RGB -> /255,部署代码里做成了RGB->BGR -> /255;或者训练代码里用了 mean/std 归一化,部署代码只做了255缩放。这类问题不会报错,只会让结果悄无声息地变差,属于最消耗时间的排查场景。

解决:把训练仓库里的预处理函数完整拷贝到部署端,不要凭记忆重写。输出端用同一张图在 PyTorch 脚本和 ONNXRuntime 脚本里各跑一遍,打印输入 tensor 的前 10 个浮点值,比对是否完全一致;有一位数不同,问题就在这一环。

5.4 车道线整体偏移贴边:row_anchor 用错

现象:能画出车道线,但线条整体向下偏移,或者左侧车道线跑到图像边界外。

原因:row_anchor 数组和当前模型不是同一套。UFLD 系列的 row anchor 是训练时根据数据集统计出来的固定 y 坐标,不同数据集、不同版本的训练脚本生成的值不完全相同。如果从别的项目拷了一个长度相近的 row_anchor,长度可能对得上,但具体坐标对不上,画出来就整体错位。

解决:检查 row_anchor 的来源。最稳妥的做法是从训练仓库的配置文件或 checkpoint 里读出原始数组,放进部署代码的常量区;再用模型输出的num_rows验证数组长度是否一致。用 Netron 查看 ONNX 里是否有对应的 constant 节点,如果导出时已经把它固化在模型里,直接读取该 constant 作为 row_anchor 最保险。

5.5 输入尺寸不是对齐单位导致 INTERNAL_ERROR

现象:ONNXRuntime 报INTERNAL_ERROR,或者 GPU 推理时输出 shape 和预期不符。

原因:部分 Execution Provider(尤其是 TensorRT)对输入宽高有对齐要求,常见是 8 或 16 的倍数。UFLD-v2 常规输入是 800x288,已经对齐,但如果为了提速把分辨率改成640x360这类非对齐值,CPU 上能跑,GPU 上就会触发错误。

解决:导出时保持静态 shape;必须做动态分辨率时,先把宽高向上取整到 16 的倍数,推理后再把后处理坐标按实际输入尺寸裁剪。模型输入尺寸改动后,row_anchor 的 y 坐标也需要同步重算,否则纵向位置全部失真。

6. 从 CPU 到 GPU:推理加速的最后一公里与验证流程

CPU 上的 ONNXRuntime 跑 UFLD-v2 通常已经能到几十毫秒量级,但如果要接实时视频流,或者目标平台是 Jetson 这类带 TensorRT 的设备,就该加 Execution Provider 了。配置方式和 CPU 差不多,Python 端直接把providers列表改成["CUDAExecutionProvider", "CPUExecutionProvider"],C++ 端在Ort::SessionOptions里追加AppendExecutionProvider_CUDA或AppendExecutionProvider_Tensorrt。需要注意 provider 列表的顺序:GPU 排在前面,CPU 作为回退,否则模型会安静地跑在 CPU 上。

加速之后必须做一致性验证。我的做法是固定一张测试图,先把 CPU EP 的输出完整保存成 npy 文件,再切换到 GPU EP 跑一遍,对比两张输出张量的最大绝对误差。常见情况是多数位置完全一致,个别像素有1e-6量级的浮点差异,这属于正常范围;如果差异到了1e-2以上,多半是模型里存在对浮点精度敏感的算子,需要把计算精度统一到 FP32。TensorRT EP 还有一层坑:首次推理会触发 engine 构建,耗时可能长达几十秒,部署时要把预热步骤放到初始化阶段,不要让它出现在线上第一帧。

UFLD-v2 这类行分类模型本身不算重,实际延迟大头往往在预处理和后处理。C++ 工程里cv::resize和BGR->RGB的通道拷贝经常吃掉和推理差不多的时间。如果已经上了 GPU EP,建议把resize和normalize也搬到 CUDA 上,或者直接用cv::cuda::resize,否则 GPU 省下的时间又会被 CPU 端补回去,测出来的帧率提升远小于预期。最后一个习惯是:每换一次 EP、每改一次输入分辨率,就把第 3 章的 shape 打印脚本重跑一遍,确认输出维度没有变化;这套验证流程看着繁琐,但能省下在错误方向上反复调试的时间。希望帮到你。

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

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

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

立即咨询