简介:这是一套基于OpenCV与ONNXRuntime部署YOLOX检测和ByteTrack跟踪的完整工程,适合有一定深度学习与视觉开发基础的读者,用来快速搭建目标检测加多目标跟踪流程。资源共53个文件,约2.76MB,包含14个Python源码、12个C++源码、10个头文件以及3个说明文档;其中py、cpp与h文件分别覆盖模型加载、预处理、推理和后处理,txt与md则提供环境配置与使用说明。目前已有110人学习,内容按Python与C++双版本组织,并附带ONNX模型,方便对比两种语言在相同任务上的实现差异。通过学习可以掌握ONNXRuntime推理流程、YOLOX检测结果解析、ByteTrack的IoU匹配与卡尔曼滤波跟踪思路,也可作为在嵌入式或实时系统上进行视觉任务开发时的参考工程。
1. 目标跟踪不是目标检测的下一步:为什么 YOLOX+ByteTrack 值得装进你的工程
很多做图像处理项目的人走到一定阶段都会卡在同一步:检测已经能出框了,但视频里的目标没有 ID,框在相邻帧之间对不上号。YOLOX 在检测精度和实时性之间平衡得很好,ByteTrack 又不像 DeepSORT 那样依赖单独的 ReID 特征网络,两个模型一前一后,几乎是最容易落地到 C++/Python 工程里的跟踪组合。这份资源把 ONNXRuntime 推理、YOLOX 检测、ByteTrack 跟踪、C++ 和 Python 两套源码打包在一起,你不用再从模型导出、依赖拼装、参数调优开始折腾。适合要做视频监控、车流统计或者把算法搬上边缘设备的工程师。接下来按环境、模型、推理、避坑、移植验证的顺序,把每个关键位置讲透。
2. 环境与依赖:ONNXRuntime 为什么是比 DNN 模块更好用的推理后端
2.1 为什么不用 OpenCV DNN 而要单独引入 ONNXRuntime
很多人会问:OpenCV 自己不是有 dnn 模块能读 ONNX 吗,为什么这个项目还要单独把 onnxruntime 打成依赖。我早期在 Windows 上用 OpenCV DNN 跑 YOLOX 时踩过一次很深的坑:模型加载成功了,但输出节点解析出来全是空,后来才发现是 dnn 模块对某个自定义算子的解释和 ONNXRuntime 不一致。从此我养成了一个习惯,只要模型是 ONNX 并且对速度有要求,就优先用 ONNXRuntime 做推理,OpenCV 只负责视频读写、图像预处理和绘制。这个包也是同样的分工:OpenCV 做图像侧,ONNXRuntime 做模型侧。
选择 ONNXRuntime 还有一层原因:它在 CPU、GPU、嵌入式设备上都能用同一套接口。Python 里改一行 providers 参数就能切换到不同的执行提供程序,C++ 里对应的就是 OrtSessionOptionsAppendExecutionProvider 这类调用。模型文件不变,代码结构不变,对需要跨平台交付的项目来说,维护成本会低很多。另一个实际好处是 ONNXRuntime 对动态输入形状的模型支持得比 OpenCV DNN 好,而 OpenCV 的 dnn 模块处理动态 shape 时经常要靠环境去猜,这也是很多项目最终放弃 DNN 模块的原因。
2.2 压缩包内容与目录结构
解压之后,重点看这几个东西。
| 目录/文件 | 作用 | 使用方式 |
|---|---|---|
| code-3/ | 主代码,Python 与 C++ 各放一边 | 按 README 指引进入对应目录 |
| eigen-3.3.9.zip | Eigen 3.3.9 头文件库 | 解压后加入 C++ include 路径 |
| onnxruntime/ | 推理库的 include、lib 与动态库 | C++ 链接;Python 侧按需安装 |
| README.md | 运行步骤、参数说明 | 先通读一遍 |
| requirements.txt | Python 依赖清单 | 用 pip 安装 |
这个包把依赖直接放在压缩包里的做法,对内网开发很友好。我见过不少项目 README 写“自行安装 OpenCV”,结果同事装出来的版本五花八门,代码里用的函数在新版本里已经改名了。作者把 Eigen、ONNXRuntime 都锁好版本带进来,实际是在帮你固定一套可复现的环境。所以拿到包之后,第一件事不是去下载最新版依赖,而是先按包内的版本跑通,再考虑升级。
我一般会把 onnxruntime/ 和 eigen-3.3.9.zip 解压到工程的 third_party 目录,而不是散落在桌面或下载文件夹里。C++ 工程对相对路径很敏感,你很难保证换一台机器后绝对路径还存在。把依赖收进工程目录,是让项目能跟着代码走的起码习惯。
2.3 Python 侧安装与版本冲突处理
Python 侧安装原本不应该有太多花样,但实际翻车率不低。最常见的问题是机器上已经装过 opencv-python 或 onnxruntime,版本冲突后 import 时报错。建议的安装流程:
python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate pip install -r requirements.txt python -c "import onnxruntime, cv2; print(onnxruntime.__version__, cv2.__version__)"requirements.txt 一般会把你要用的 numpy、opencv-python、onnxruntime 版本写在里面。有一个容易被忽略的细节:requirements.txt 是为 Python 3.8 到 3.10 准备的,如果你现在用的是 Python 3.12 或更新版本,onnxruntime 的 wheel 可能不匹配。遇到这种情况不要硬装,改用 conda 建一个 3.9 的环境最省事。
如果是为了 GPU 推理,把 onnxruntime 换成 onnxruntime-gpu,但两者不能同时存在于同一个环境。我的经验是先执行pip uninstall onnxruntime onnxruntime-gpu -y,再装你需要的那个,避免遇到 ImportError 之外的诡异崩溃。
2.4 C++ 侧依赖与 CMake 配置
C++ 侧会相对繁琐一点。先说 Eigen:这个包带了 eigen-3.3.9.zip 老版本,是因为部分 C++ 依赖在构建时引用了特定版本的 Eigen 头文件。新版本不保证 ABI 兼容,所以我建议严格使用包内版本。
cmake_minimum_required(VERSION 3.15) project(yolox_bytetrack CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(EIGEN3_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/third_party/eigen-3.3.9") set(ORT_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/third_party/onnxruntime/include") set(ORT_LIB_DIR "${PROJECT_SOURCE_DIR}/third_party/onnxruntime/lib") include_directories(${EIGEN3_INCLUDE_DIR} ${ORT_INCLUDE_DIR}) link_directories(${ORT_LIB_DIR}) find_package(OpenCV REQUIRED) add_executable(yolox_bytetrack src/main.cpp) target_link_libraries(yolox_bytetrack PRIVATE ${OpenCV_LIBS} onnxruntime)这段 CMake 的逻辑是:OpenCV 用 find_package 自动寻找,ONNXRuntime 和 Eigen 手动指定路径。原因在于 ONNXRuntime 官方没有提供好用的 CMake config 文件,手动指定反而更可控。编译之前记得把 onnxruntime.dll 复制到最终 exe 旁边,否则运行时会在加载阶段直接报“找不到 onnxruntime.dll”。Windows 下还会要求安装 Microsoft Visual C++ 2015-2022 Redistributable (x64),否则程序可能在启动瞬间秒退,这个在出现 0xc000007b 错误码时尤其明显。
2.5 先跑通最小检测再上跟踪
我拿到任何目标跟踪包,都不会直接跑完整 demo。先做一次纯检测验证,确认模型、依赖、后处理链路是通的,再叠加 ByteTrack,否则出现问题时你很难定位是检测的错还是跟踪的错。常见入口命令类似:
python demo.py --source test.mp4 --output out.mp4如果 README 提供了纯检测脚本,就优先跑那个。验证标准很简单:画面上应该能看到多个不同类别目标被框住,框的位置和真实物体基本贴合,不会有大量错位或空白帧。只要这一步通了,环境的大坑基本就排掉了。
3. YOLOX 的 ONNX 模型解析:输入输出、letterbox 与坐标还原
3.1 模型输入输出参数
先看模型,再写代码。用 onnxruntime 跑一个最简单的推理之前,建议先打印一下模型的输入输出结构,因为你从网上下载的 ONNX 可能是不同版本导出的,输入输出命名不一定一样。用一个小脚本可以省很多事:
import onnxruntime as ort sess = ort.InferenceSession('yolox.onnx', providers=['CPUExecutionProvider']) for inp in sess.get_inputs(): print('input:', inp.name, inp.shape, inp.type) for out in sess.get_outputs(): print('output:', out.name, out.shape, out.type)从常见导出来看,YOLOX 输入是 [1,3,640,640],顺序是 NCHW,RGB 图像,像素值归一化到 0 到 1。输出通常合并成一个张量,shape 是 (1, 8400, 85)。8400 这个数字来自三个检测层:640 输入下 stride 8 的特征图是 80×80=6400,stride 16 是 40×40=1600,stride 32 是 20×20=400,三层加起来正好 8400。每个候选框有 85 维:cx、cy、w、h、objectness、80 个类别的置信度。
这里要注意一个版本差异:有些导出脚本会在模型里把 sigmoid 一起做掉,有些不会。如果你在后处理里又做了一次 sigmoid,分数会变得很奇怪;如果不做,又可能得到负值。我的习惯是先不管公式,直接打印几帧输出统计值,看看数值范围是 0 到 1 还是负的,再决定要不要加 sigmoid。这个判断比看文档可靠。
3.2 letterbox 预处理
YOLOX 训练时用的是等比例缩放加灰边填充,推理时也必须保持一致。直接把宽屏视频压缩到 640×640,会破坏宽高比,特别是小目标,性能下降很可观。letterbox 的代码看起来短,但后处理时要用到它记录的两个东西:缩放系数和填充偏移。
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): h, w = img.shape[:2] r = min(new_shape[0] / h, new_shape[1] / w) new_w, new_h = int(round(w * r)), int(round(h * r)) img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) dw = (new_shape[1] - new_w) / 2 dh = (new_shape[0] - new_h) / 2 top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img, r, (left, top)这段代码返回三个东西:填充后的图、缩放比例 r、左上角填充偏移 (left, top)。后续做坐标还原时要同时用到它们。注意 top 和 left 这里是相对填充后图像坐标系的偏移,不是除以 r 之后的偏移。很多人在这一步直接把 (left, top) 减掉,实际上先减偏移再除 r 才对。
值得多说一句的是 0.1 这个数。它是从 YOLO 系列代码里一直保留的习惯,作用是避免浮点除不净时多出一个像素的边。如果不用它,某些奇数尺寸的帧会导致 box 偏移一到两像素,肉眼不易察觉,但跟踪时 IoU 计算会被放大影响。
3.3 后处理与 NMS
模型输出是候选框,不是最终结果,这一点要反复提醒。后处理主要做四件事:解码坐标、算置信度、按阈值过滤、NMS 去重。YOLOX 输出里的坐标是 cx、cy、w、h,对应输入图像坐标空间。还原代码大致如下:
def postprocess(preds, ratio, pad_left, pad_top, conf_thre=0.3, nms_thre=0.45): preds = preds[0] # (8400, 85) boxes = preds[:, :4].copy() scores = preds[:, 4] * preds[:, 5:].max(axis=1) class_ids = np.argmax(preds[:, 5:], axis=1) keep = scores > conf_thre boxes, scores, class_ids = boxes[keep], scores[keep], class_ids[keep] x1 = (boxes[:, 0] - boxes[:, 2] / 2 - pad_left) / ratio y1 = (boxes[:, 1] - boxes[:, 3] / 2 - pad_top) / ratio x2 = (boxes[:, 0] + boxes[:, 2] / 2 - pad_left) / ratio y2 = (boxes[:, 1] + boxes[:, 3] / 2 - pad_top) / ratio boxes = np.stack([x1, y1, x2, y2], axis=1) nms_idx = cv2.dnn.NMSBoxes( boxes.tolist(), scores.tolist(), conf_thre, nms_thre) if len(nms_idx) == 0: return [], [], [] nms_idx = np.array(nms_idx).reshape(-1).tolist() return boxes[nms_idx], scores[nms_idx], class_ids[nms_idx]置信度是 objectness 乘以类别最大分数,这一步不能省。有的实现会把 objectness 忽略,只用类别分数,这样会把大量背景框放进来,跟踪器的“低分框匹配”逻辑会被噪声彻底带偏。
cv2.dnn.NMSBoxes 在 OpenCV 4.5 之后返回类型不太稳定,有的是 list,有的是 tuple,还有的会返回空。我一般会先包一层np.array(nms_idx).reshape(-1).tolist(),避免在下一行做索引时报错。
3.4 session 复用与推理循环
ONNXRuntime 的 InferenceSession 创建时会加载模型、分配内存,这个操作很重。一个常见错误是把 session 写在视频循环里,一帧创建一个 session,帧率直接掉到个位数。正确做法是循环外只创建一次,循环内只调用 run。
sess = ort.InferenceSession('yolox.onnx', providers=['CPUExecutionProvider']) while cap.isOpened(): ret, frame = cap.read() if not ret: break img, r, (left, top) = letterbox(frame) blob = img[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 outputs = sess.run(None, {sess.get_inputs()[0].name: blob})[0] dets = postprocess(outputs, r, left, top)这里img[:, :, ::-1]是把 BGR 转成 RGB 的常见做法;transpose(2,0,1)把 HWC 转成 CHW。除以 255 的目的是匹配训练时的归一化。如果模型导出时已经把归一化算子放进去了,这里再除一遍就会出现所有 score 都偏低的现象。怎么判断?还是那句,先打印输出值范围。
4. 把检测器接上 ByteTrack:tracker 的调用方式与参数调优
4.1 ByteTrack 为什么不需要 ReID
到了跟踪这一段,很多新手会先想到 DeepSORT,然后被 ReID 特征网络劝退。ByteTrack 的设计思路是反过来的:它不区分“这是个什么人”,只关心“这个框和上一帧的哪个框最像”。这里的“像”用一个简单的 IoU 或中心点距离就能衡量,再配上卡尔曼滤波来预测下一帧位置。
ByteTrack 的关键创新是保留低分框。传统做法里置信度低于阈值的框会被直接丢掉,但这些低分框往往就是被遮挡或模糊的目标。ByteTrack 把它们收集起来,在第一轮没有匹配上的轨迹里继续做匹配,从而保住被短暂遮挡的目标 ID。这个策略在行人遮挡严重的监控场景里效果相当明显,而且实现成本很低。
4.2 在视频循环里集成 tracker
开始写集成代码。要注意 tracker 的实例化位置:它应该在循环外创建一次,不能每帧 new。否则卡尔曼滤波的累积状态全被重置,ID 会每帧从头编号。
tracker = BYTETracker(args, frame_rate=30) while cap.isOpened(): ret, frame = cap.read() if not ret: break img, r, (pad_left, pad_top) = letterbox(frame) blob = img[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 outputs = sess.run(None, {sess.get_inputs()[0].name: blob})[0] dets = postprocess(outputs, r, pad_left, pad_top) online_tlwhs, online_ids = tracker.update(dets, frame.shape[:2]) for tlwh, track_id in zip(online_tlwhs, online_ids): x, y, w, h = [int(v) for v in tlwh] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"ID:{track_id}", (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)tracker.update(dets, frame.shape[:2]) 的输入格式很关键。ByteTrack 原版实现里接收的 dets 通常是 [x1, y1, x2, y2, score] 的二维数组,但因为不同 fork 的接口有差异,有的接收 tlwh 格式。你打开项目里的 byte_tracker.py 看一眼 update 函数的 parse 部分就知道该传什么。拿不准的时候,先用固定视频跑一次,看报错信息里的尺寸提示,比猜接口靠谱。
我要提醒:检测框坐标混用是跟踪里最常见的隐性 bug。如果后处理返回的是 xyxy,tracker 内部按 tlwh 解析,矩形就会完全错误。为了统一,常用做法是在调用 update 之前统一转成项目要求的格式,并在转换处打印几帧验证。
4.3 ByteTrack 参数调优
byte_tracker 里有一批和效果直接相关的参数。以 ByteTrack 原版默认值为基准:
| 参数 | 常见默认值 | 调整方向 |
|---|---|---|
| track_thresh | 0.5 | 高分框/低分框的分界,调低让更多目标参与匹配 |
| match_thresh | 0.8 | 匹配阈值,调低匹配更宽松,ID 切换也会更频繁 |
| frame_rate | 30 | 需要和输入视频帧率一致 |
| max_time_lost | 30 | 轨迹丢失帧数,调大让长时间被遮挡的目标不消失 |
实际调的时候,我建议一次只动一个参数。跟踪效果很难量化,同时动两个参数,你无法判断是谁导致的 ID 跳变。先调 track_thresh,观察低分框第二阶段匹配是否把遮挡后的目标找了回来;再调 match_thresh,观察是否出现同一目标两个 ID。
还有一个会被忽略的地方:frame_rate 不是随意填的。ByteTrack 用 frame_rate 计算一个轨迹在丢失多少帧后被判定结束。如果你视频是 25 帧,参数填 30,max_time_lost=30 实际表示约一秒而不是你想要的一秒多一点。确定视频帧率最准的办法是用 OpenCV 读cap.get(cv2.CAP_PROP_FPS),不要靠眼睛猜。
4.4 保存结果与可视化
写到视频时,注意输出尺寸和 fourcc 编码。常见组合是 mp4v 加 .mp4,这个组合跨平台支持相对好。出现保存文件 0KB 或打不开时,先检查这两项:
fourcc = cv2.VideoWriter_fourcc(*'mp4v') writer = cv2.VideoWriter('out.mp4', fourcc, fps, (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))))fps 用cap.get(cv2.CAP_PROP_FPS),不要直接写死 30,除非你验证过。尺寸要和写入帧完全一致,否则 writer 会写不进任何内容。另一个坑是最后忘了writer.release(),导致文件没有正常收尾。把这行放在循环外面,用 try/finally 包住,养成习惯。
可视化时 OpenCV 的 rectangle 和 putText 只接受 int 坐标。如果你把后处理里的 float 坐标直接传进去,Python 会抛 TypeError 之类的报错。所以代码里我把 tlwh 四个值都用 int() 转了一遍,这个小动作能省很多运行时的意外中断。
5. 避坑排查:从编译失败到跟踪跳 ID 的五个实际问题
5.1 Eigen 头文件缺失或版本被顶掉
现象:C++ 编译时找不到eigen3/Eigen/Core,或者在解开 Eigen 后运行出现矩阵相关的断言失败。
原因:包内自带的是 eigen-3.3.9.zip,但有些系统或之前的工程里已经装过更高版本的 Eigen,include 搜索顺序把新版本排到了前面,或者 zip 根本没有解压。
解决:把 eigen-3.3.9 解压后统一放到 third_party/eigen-3.3.9,在 CMakeLists 中手动指定,并放在其他 include 路径之前。如果编译机之前安装过 Eigen,直接在 CMakeLists.txt 里用 include_directories 的 BEFORE 参数强制优先使用包内版本:
include_directories(BEFORE "${PROJECT_SOURCE_DIR}/third_party/eigen-3.3.9")这样做的好处是,同一份代码在不同机器上构建时,使用的 Eigen 完全一致,不会再出现“我本机能跑,到服务器就崩”的经典问题。
5.2 导入 onnxruntime 直接报 ModuleNotFoundError
现象:运行 Python demo 时提示ModuleNotFoundError: No module named 'onnxruntime';或者能导入但在 InferenceSession 创建时崩掉。
原因:暂时未激活虚拟环境,或者当前 Python 版本过新,pip 装了不匹配的 wheel;还有可能是同一环境里同时存在 onnxruntime 和 onnxruntime-gpu,导入顺序导致冲突。
解决:回到项目目录激活虚拟环境,然后统一重装:先pip uninstall onnxruntime onnxruntime-gpu -y,再按 requirements.txt 安装。如果 Python 是 3.11 及以上,优先用 conda 建一个 3.9 的环境,onnxruntime 对这个版本支持最稳。我见过的大多数翻车案例,都发生在过新的 Python 版本上。
5.3 检测框整体偏移
现象:检测框能框住目标,但框整体偏左上或偏右下,且偏移量随着目标位置变化。
原因:letterbox 的填充偏移在坐标还原时被忽略或写反。常见写法是只除 r 不剪偏移;或者减偏移用的坐标是 (dw, dh) 而不是实际填充值。
解决:用一张固定尺寸的图像做验证,在图上手动画一条参考线,跑完检测后看框的偏移方向。如果偏左上,说明忘了减填充偏移;如果偏右下,多半是符号写反。还原公式固定为:
x1 = (cx - w / 2 - pad_left) / ratio y1 = (cy - h / 2 - pad_top) / ratio把 pad_left 和 pad_top 都打印出来,对照 letterbox 里的 left、top 值是否一致。这个方法比盯着画面猜快得多。
5.4 目标 ID 频繁跳变
现象:同一行人走过画面,ID 从 1 跳到 3 又跳回 1;或者一个目标身上同时出现两个框。
原因:track_thresh 过低导致背景被当成目标;match_thresh 设置不当导致匹配过松;还有一些情况是 tracker 被错误地创建在循环里。
解决:先检查 tracker 实例化位置,确保它在循环之外。再把 track_thresh 从 0.5 上调到 0.6,过滤掉模糊背景框;如果目标在遮挡后 ID 容易丢,那就把 max_time_lost 调大到 60 试试。要判断是哪个参数造成的,最佳方式是固定同一段测试视频,记录每次调整前后的 ID 数,而不是肉眼盯着画面。
5.5 C++ 版运行时秒退或找不到模型
现象:编译成功,但双击 exe 闪退;或者控制台提示找不到 yolox.onnx / onnxruntime.dll。
原因:onnxruntime.dll 没有复制到 exe 目录;模型路径是相对路径,exe 所在目录和工程目录不一致;也有可能是缺少 Microsoft Visual C++ 2015-2022 Redistributable。
解决:在 CMake 里加一段自动复制 dll 的脚本,或者手动把 onnxruntime.dll 放到输出目录。模型路径用绝对路径先验证一次流程,确认能跑之后再改回相对路径。如果闪退时错误码是 0xc000007b,先确认编译配置是 x64,且所有依赖库都是 x64 版本,这个组合最容易在 Debug 模式下编译、Release 模式下运行时踩到。
6. 用逐帧日志验证 C++ 移植结果
Python 版跑通只是第一步。如果你最终要交付的是 C++ 服务,最担心的是移植后检测框对不上。我的验证办法是把 C++ 和 Python 放在同一段视频上跑,然后逐帧打印跟踪结果。字段格式保持一致:
printf("%d,%d,%.2f,%.2f,%.2f,%.2f\n", frame_id, track_id, x1, y1, w, h);两边各跑一遍,输出到两个文件。比对时不用逐像素对齐,重点看三类数据:每帧检测框数量是否一致;高置信度目标的 track_id 是否稳定;同一目标在两个输出里的坐标差是否在可接受范围内。允许 1 到 2 像素的偏差,那属于两次后处理浮点计算的正常误差;超过 5 像素就需要回去检查 letterbox 的还原逻辑。
可以用一个小脚本做对比,把按 frame_id 聚合后的结果拉出来,手动检查即可。常见做法是打印 100 帧里的 ID 总数和平均框数;如果在某个场景下 C++ 少了几个框,优先查 NMS 的返回类型解析,因为 OpenCV 在不同版本里 NMSBoxes 的返回值结构有过变动。C++ 侧我一般会写:
std::vector<int> indices; cv::dnn::NMSBoxes(boxes, scores, conf_thre, nms_thre, indices);这一步在两边的表现经常不一致,值得最优先排查。
性能方面,如果 CPU 推理总是不够快,先确认两个点:一是 ONNX 是否固定了 640×640 输入,动态维度会让 ONNXRuntime 每帧重新做内存规划;二是会话选项里是否开启了线程数配置。C++ 里设置方式是这样:
Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, model_path.c_str(), session_options);SetIntraOpNumThreads 控制单次推理内部线程数,一般和部署机器的物理核数一致,并不是设得越大越快;线程过多时线程切换开销反而会拖慢推理。ORT_ENABLE_ALL 让 ONNXRuntime 做完整的图优化,这个选项对定长输入的模型收益明显。
从那以后我每次做 ONNX 工程化时都强制走一遍固定流程:先固定输入尺寸,再打印输入输出形状,再用一张测试图验证 letterbox 映射,确认准确后才接 ByteTrack。这套习惯帮我省掉的调试时间,比任何工具都多。希望也能帮到你。
本文还有配套的精品资源,点击获取