尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLOv8 Pose ONNX 模型部署实战:推理、后处理与避坑指南

YOLOv8 Pose ONNX 模型部署实战:推理、后处理与避坑指南 简介这份压缩包提供了一套基于C#与Onnx Runtime的YOLOv8姿态识别完整实现内置训练好的模型文件解压即可运行适合需要在Windows桌面端快速集成人体关键点检测的开发者。包内共272个文件包含cs源码、onnx模型、依赖dll及配置文件其中模型权重与C#工程代码均已配好无需额外下载即可执行推理也便于对照阅读网络结构与预处理流程。资源整体约235.08MB覆盖面较全既有核心运行库也有辅助资源能够支撑从环境搭建到姿态检测结果输出的完整链路。目前已有537人学习使用对于刚接触YOLOv8或想参考C#部署方案的人员是一个可直接上手的样例工程可用于二次开发或学习关键点检测的实现细节。1. 拿到 Onnx Yolov8 Pose.rar先搞清楚你手里到底是个什么东西拿到一个名为 Onnx Yolov8 Pose.rar 的压缩包多半是两种情况要么是姿态识别项目里别人移交的模型包要么是毕业设计、算法竞赛里绕不开的一步。这个包的核心价值在于它把训练好的 YOLOv8 Pose 模型导出成了 ONNX 格式让你不依赖 PyTorch 那套训练环境可以直接用 onnxruntime 在普通电脑、RK3588 这类边缘设备上跑姿态识别推理。对工程师来说它意味着两件事第一模型本身是一个“黑匣子”你只需要保证输入图片和输出解析正确第二真正的落地工作几乎全在环境搭建和后处理上而这两件事都可以逐步复现。读这个包时我建议你先建立一个预期ONNX 只是模型文件姿态识别能力不是解压即得的现成 API。你需要补上预处理、推理、关键点后处理三段代码整条链路才算通。这篇文章的读者是准备做人体姿态估计落地、或者正在为 yolov8 训练自己的数据集做准备的从业者我会把从“解压”到“输出骨架”再到“优化迁移”的整个过程拆开讲。2. 把 ONNX 跑起来解压、最小环境与第一次骨架输出2.1 解压后先盘点三类文件各是什么角色大多数 Onnx Yolov8 Pose.rar 压缩包在解压后会看到三个常见成员一个 .onnx 模型文件可能叫 yolov8n-pose.onnx 或者 model.onnx一个 demo/infer 推理脚本还有一个 README 或 requirements.txt。你最先要做的不是急着运行脚本而是先列目录把所有文件的名字和大小过一遍。这一步能帮你判断模型有没有带动态尺寸、是不是 int8 量化后的版本以及推理脚本用的是哪个版本的 API。我自己处理这类包的习惯是先把 .onnx 单独复制到一个 working 目录再用 Python 把它读一遍打印出输入输出信息。这一步不花时间却能避免后面所有“shape 对不上”的排查。常见做法是用 onnxruntime 的 InferenceSession 来读取它是目前最通用的 onnx 运行时比直接解析 onnx 文件更容易读懂网络结构。如果你想快速知道 .onnx 怎么运行最简单的答案就是交给 onnxruntime 去 load 它然后给它喂一张符合预期的张量。2.2 在 ubuntu20.04 CPU 环境搭建 onnxruntime 最小运行环境我之前在 ubuntu20.04 上搭建过不少 yolov8 环境CPU 版本完全够用来跑这个 onnx 推理因为 onnxruntime 本身对 CPU 做了很多优化。用 venv 隔离环境避免把系统 Python 弄乱cd ~/pose_work python3 -m venv .venv source .venv/bin/activate pip install onnxruntime opencv-python numpy这段命令创建虚拟环境并安装三个最小依赖onnxruntime 负责加载和推理opencv-python 负责读图和画骨架numpy 负责张量操作。如果你的机器有 NVIDIA 显卡想用 GPU 跑可以把第一行 pip install 换成 onnxruntime-gpu但要注意版本要和 CUDA/cuDNN 配套否则运行时会报错说找不到对应动态库。GTX 1660 Ti 跑 yolov8 的 onnx 推理用 onnxruntime-gpu 能比 CPU 快十倍左右但我建议第一遍先用 CPU 跑通逻辑再去做性能优化。如果包里有 requirements.txt就直接用pip install -r requirements.txt代替上面第二行。注意不要盲目升级包版本尤其 onnxruntime 大版本不同时算子的实现可能有细微差异可能导致同样的模型在 1.16 上正常、在 1.19 上输出 NaN——这种换版本就翻车的情况后面会专门讲。2.3 用 Python 跑通第一次推理从图片到 56×8400 张量把 .onnx 模型放到工作目录后写下面这个脚本只需十几行就能完成加载和一次前向推理import cv2 import numpy as np import onnxruntime as ort # 创建推理会话显式指定 CPU 执行提供程序 sess ort.InferenceSession( yolov8n-pose.onnx, providers[CPUExecutionProvider] ) # 打印输入输出信息这一步能避免后面的 shape 地狱 in_meta sess.get_inputs()[0] out_meta sess.get_outputs() print(input:, in_meta.name, in_meta.shape) print(output:, [(o.name, o.shape) for o in out_meta]) # 读取图片YOLOv8 的预处理是简单的 RGB 归一化到 0~1 img_bgr cv2.imread(demo.jpg) img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) orig_h, orig_w img_rgb.shape[:2] # 缩放到模型输入尺寸默认 640x640 input_w, input_h 640, 640 img_resized cv2.resize(img_rgb, (input_w, input_h), interpolationcv2.INTER_LINEAR) # 转成 NCHW 且数值范围 0~1 的 float32 张量 img_tensor img_resized.astype(np.float32) / 255.0 img_tensor np.transpose(img_tensor, (2, 0, 1))[None, ...] # 推理直接获取所有输出 outputs sess.run(None, {in_meta.name: img_tensor}) print(输出张量个数:, len(outputs), 每个 shape:, [o.shape for o in outputs])这段代码里有两个关键参数值得解释。providers 显式声明用 CPU避免本机装了 CUDA 环境后自动加载 GPU provider 然后报错输入尺寸写成 640x640 有两个前提一是导出时是固定尺寸二是后面后处理要把缩放比例记下来。如果打印出来是 [1, 3, -1, -1]说明是动态尺寸那就要保持宽高比做 letterbox不能直接 resize 成 640否则人会被拉变形关键点也会错。预处理本身没有减均值、没有除标准差YOLOv8 官方训练时就是这么做的如果你加了 ImageNet 的 mean/std关键点很容易乱飘。至于 outputs 的 shape绝大多数 YOLOv8 Pose 导出结果是单个输出 [1, 56, 8400]。其中 56 表示 4 个框坐标加 1 个置信度加 51 个关键点坐标17 个点每个点有 x、y、visibility 三个值8400 是 80x80、40x40、20x20 三个尺度特征图上的候选框总数。如果你打印出来不是这个 shape说明导出方式不同后面解析要根据实际 shape 调整。2.4 后处理可视化把 17 个关键点连成骨架并映射回原图拿到 56×8400 的原始输出后需要做四件事按行拆候选框和关键点、按置信度过滤、做非极大值抑制 NMS、把坐标从 640 缩放回原图尺寸。这里用 cv2.dnn.NMSBoxes不需要自己写 NMS省去很多麻烦。# 取第一个输出并转置成 [8400, 56] feat outputs[0][0].T # shape (8400, 56) # 拆分前4是 cx, cy, w, h第5是置信度后面51是 17*3 boxes feat[:, :4] conf feat[:, 4] kpts feat[:, 5:].reshape(-1, 17, 3) # 最后一维是 x, y, visibility # 1. 置信度过滤 mask conf 0.5 boxes, conf, kpts boxes[mask], conf[mask], kpts[mask] if len(boxes) 0: print(没有检测到人体) # 2. 将 640 网络坐标映射回原图 scale_x orig_w / input_w scale_y orig_h / input_h boxes[:, [0, 2]] boxes[:, [0, 2]] * scale_x boxes[:, [1, 3]] boxes[:, [1, 3]] * scale_y kpts[:, :, 0] kpts[:, :, 0] * scale_x kpts[:, :, 1] kpts[:, :, 1] * scale_y # 3. NMS输入需要是 xywh 格式 keep cv2.dnn.NMSBoxes( boxes.tolist(), conf.tolist(), score_threshold0.5, nms_threshold0.5 )这里 NMS 的 score_threshold 会和前面的置信度过滤重复作用但前者已经过滤过一次这里再填一次是为了防止某些框的分数刚好卡在边缘。画骨架时COCO 数据集的 17 个关键点顺序是固定的骨架连线也有一套官方定义。通常不需要重新下载骨骼图直接用下面这个 pair 列表就能画出常见的人体骨架skeleton [ [0, 1], [0, 2], [1, 3], [2, 4], # 鼻子到眼睛、耳朵 [5, 6], [5, 7], [7, 9], [6, 8], [8, 10], # 肩膀、手肘、手腕 [5, 11], [6, 12], [11, 12], # 躯干 [11, 13], [12, 14], [13, 15], [14, 16] # 腿和脚 ] for i in keep.flatten(): box boxes[i].astype(int) points kpts[i].astype(int) cv2.rectangle(img_bgr, (box[0], box[1]), (box[0] box[2], box[1] box[3]), (0, 255, 0), 2) for p in points: cv2.circle(img_bgr, (p[0], p[1]), 3, (0, 0, 255), -1) for a, b in skeleton: if points[a][2] 0.3 and points[b][2] 0.3: cv2.line(img_bgr, (points[a][0], points[a][1]), (points[b][0], points[b][1]), (255, 0, 0), 1) cv2.imwrite(result.jpg, img_bgr)这段代码里的 skeleton 列表就是姿态识别里 pose search 骨架阶段要用的结构后续的行为识别项目也常基于这些骨架坐标做动作分类。visibility 低于阈值的点直接放弃连线能避免“凭空长出一只手”的假骨架。阈值 0.3 是我常用的经验值如果画面里有大量遮挡可以降到 0.1但要接受噪声变多。3. Yolov8 Pose 的模型结构、输出头与三个必调参数3.1 一张 yolov8 网络结构图Pose head 和 Detect head 差在哪要调好这个 onnx 模型先要建立一版 yolov8 网络结构图在心里。YOLOv8 是一个 anchor-free 的检测架构主干网络用 C2f 模块提取特征颈部做多尺度融合最后输出头是解耦的一个分支做分类一个分支做边框回归。Pose 版本在回归分支之外再接了一个关键点头输出每个候选框对应的 17 个关键点坐标和可见性这就是它和纯 Detect 模型的本质差别。理解这个结构后你会明白为什么很多人用 yolov8 做姿态识别时总在想改进 head常见做法是在关键点头前加一层轻量注意力或坐标注意力来增强小目标关键点的响应。我提 yolov8 协调注意力机制是因为这是社区里一个很热门的改进方向。把坐标注意力模块插入到 neck 融合之后对遮挡、小尺寸人体的关键点定位有一定提升。但如果你用的是别人导出的 onnx就只能通过这些参数去调而不能直接改网络结构想真正改进网络得回到 pytorch 训练流程里去。这正好引到下一个问题onnxruntime 和 onnx 的区别。3.2 onnxruntime 和 onnx 的区别你的模型是文件它是运行时经常有人问 onnxruntime 和 onnx 的区别概念简短回答onnx 是模型的一种中间表示格式是一个文件onnxruntime 是加载这个文件并执行推理的引擎。你可以把 onnx 看作相机拍出来的 raw 照片onnxruntime 就是能解析这张 raw 的软件。为什么需要这个中间层因为训练框架 PyTorch 和部署平台 NVIDIA、RK3588、手机 NPU 之间的算子实现并不统一onnx 提供了一套相对稳定的算子定义让模型可以被不同运行时加载。这就是为什么你手里的 rar 包会以 .onnx 作为交付物而不是 .pt 权重因为对方希望你跨平台部署。在使用上onnxruntime 提供两个关键能力一是运行前对计算图做图优化包括常量折叠、算子融合二是通过 provider 机制选择 CPU、CUDA、TensorRT 等后端。所以同样一个 onnx 文件CPU 上可能是几百毫秒TensorRT 上可能是十几毫秒。你在跑通后想提速优先从 provider 和优化选项入手而不是去改模型本身。3.3 三个必调参数置信度阈值、IoU 阈值和输入尺寸把模型跑通之后真正决定效果的是三个参数我一般按重要性排序置信度阈值 conf_thres、NMS 的 IoU 阈值 iou_thres、输入尺寸。参数默认经验值影响调参方向conf_thres0.5过滤低质量候选框漏检多就降到 0.25误检多就提到 0.7iou_thres0.5抑制重叠框人群拥挤场景提到 0.7单人场景用 0.5input_size640精度与速度的权衡小目标/远距离提到 1280实时视频用 640 或 480置信度阈值是最直观的旋钮。在姿态识别里如果置信度太低背景里的伪人体框会产生大量随机关键点如果太高侧身、遮挡的人体会被直接过滤造成漏检。我通常先跑一批测试图统计“没画框”和“画错框”的比例再决定往哪个方向调。IoU 阈值影响多人场景人挨着人时默认 0.5 容易把相邻框合并导致关键点串到旁边人身上遇到这种情况我会把 nms_threshold 提到 0.6 或 0.7。输入尺寸则是性能敏感项很多嵌入式部署方案宁可使用 480x480 的输入也要保住帧率代价是远距离小目标基本丢弃。3.4 从 pytorch 训练好的模型转 onnx导出命令、opset 与输出名检查如果这个 rar 包里的模型是别人导出的调用关系就到此为止。但多数从业者迟早会面临“用自己的 .pt 训练完再转 onnx”这一步这里给出最可靠的导出思路。常见做法是直接用 Ultralytics 的导出接口yolo export modelyolov8n-pose.pt formatonnx dynamicTrue opset12 simplifyTrue上面命令做了三件关键事把权重导出为 onnx打开 dynamic 让长宽维度变成 -1便于在部署端动态决定输入尺寸simplify 会调用 onnx-simplifier 尝试融合一些冗余算子。其中 opset12 是保守选择它和大多数嵌入式推理框架的兼容性最好opset 越高可能会用到更新的算子比如一些 Resize 的坐标变换模式在旧框架里没有实现。导出后一定要用 2.3 节里的打印脚本检查输出名和 shape如果输出名是 output0 且 shape 是 [1, 56, 8400]说明结构符合预期如果多出一个输出说明关键点头被拆分成了独立分支后处理要做相应调整。检查完还有一个习惯我特别推荐用同一张测试图在 PyTorch 里跑一次、在 onnxruntime 里跑一次比较两次输出张量的均方误差。误差在 1e-3 以内说明转换没问题误差大了先查预处理和输入尺寸不要急着调阈值因为阈值调不回来数值上的偏差。4. 部署避坑这个 rar 里的模型换台机器就出问题五个排查思路4.1 模型输出维度对不上先看输出名字和 shape别急着改代码现象拿到包后跑脚本报错 index out of range或者 outputs[0].shape 是 [1, 52, 8400]、[1, 4, 8400] 之类和网上教程里的 56 对不上。原因YOLOv8 Pose 的关键点输出维度与关键点数量强相关输出通道是 4 1 3×KK17 时是 56。但有些仓库改成了 14 个点去掉脸部的部分点有些在导出时把关键点头和检测头拆成两个输出 tensor于是你拿到的是两个数组而不是一个。这是模型定义差异不是 bug。解决第一步永远打印 sess.get_outputs() 的信息。看到两个输出时不要慌用 onnx 自带的 shape 推理工具把每个输出的 shape 打印出来按实际 shape 去拆。如果第二个输出 shape 是 [1, 17, 8400] 或 [1, 8400, 17]说明关键点是独立输出的那就把 boxes/kpts 分开解析而不是强行拼成一个 56 通道的矩阵。提示检查 onnx 图结构最稳妥的命令是python -m onnx.shape_inference --input model.onnx --output model_shaped.onnx它能生成带有完整 shape 信息的新模型便于你数清楚每一个中间输出。4.2 CPU 推理慢到没法用int8 量化、固定输入尺寸和线程数才是后悔药现象一张 720p 图片在 CPU 上推理耗时 400 到 800 毫秒视频基本卡成幻灯片。原因绝大多数 .onnx 是 FP32 权重CPU 上跑卷积很吃力如果导出时开了 dynamic shape每次推理还要重新分配内存只会更慢。很多人在笔记本上第一次跑 yolov8 pose 都会被这个数字吓到我见过有人直接判定“模型有问题”其实只是没有做部署优化。解决从三个层面下手。一是使用 onnxruntime 的 int8 动态量化一般能获得 2 到 3 倍加速代码很简单from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( yolov8n-pose.onnx, yolov8n-pose-int8.onnx, weight_typeQuantType.QInt8 )二是把动态输入固定成 640x640避免运行时动态内存分配三是给 InferenceSession 设置线程数和图优化等级import onnxruntime as ort sess_options ort.SessionOptions() sess_options.intra_op_num_threads 8 sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession( yolov8n-pose-int8.onnx, sess_options, providers[CPUExecutionProvider] )这三招组合下来在普通 x86 CPU 上跑到 50 毫秒以内是常见的。如果还不够那就说明这个模型压根不是为 CPU 准备的应该考虑边缘 NPU 或 GPU。4.3 关键点乱飘坐标映射和预处理的“玄学”现象检测框位置基本准确但关键点像乱跳尤其是手、脚这些远端节点视频里看起来在快速抖动。原因多半是后处理坐标没有按实际缩放比例换算。很多人的代码把网络输出的坐标直接当作原图坐标画而忘记除以 640 再乘以原图宽高另一个常见原因是用了 letterbox 加灰边但前后处理不对称。YOLOv8 的预处理很朴素不需要减均值但如果你在转 onnx 之前在训练代码里加了自定义的归一化比如除以 255 之后再减 0.5导出后的模型就学会了这套分布部署时也必须原样复刻只除以 255 反而会出现整体偏移。解决把预处理和后处理写在同一个 helper 函数里记录输入尺寸、原图尺寸、letterbox 的 pad 值。对照 PyTorch 的结果一张一张调试。笔者的建议是忘掉“看着差不多就行”的心态直接打印坐标比较 np.allclose偏差在像素级就说明链路对了。4.4 板端部署不兼容RK3588 上转 rknn、转 ncnn 的流程预检现象模型在 PC 上完全正常但拿到 RK3588 或海思平台后加载失败或精度断崖式下跌。原因onnx 是通用格式不代表所有平台都能跑。RK3588 这类边缘设备一般要转成 rknn 格式使用 RKNN Toolkit 完成模型转换与量化手机端则常用 onnx 转 ncnn很多人会借助网上的 onnx2ncnn 在线转换服务但转换前必须检查算子支持表。YOLOv8 Pose 里常用的 Resize、Split、Transpose 算子大部分 NPU 支持但部分旧版本导出风格会生成大量动态 shape 相关算子直接导致转换失败。解决转板端格式之前先在 PC 上用 onnx-simplifier 做一遍图优化目标是让计算图规整减少 Transpose 和 Reshape 的随意组合pip install onnx-simplifier python -m onnxsim yolov8n-pose.onnx yolov8n-pose-sim.onnx简化后再转 rknn 或 ncnn成功率会高很多。如果在转换时报 unsupported op去算子支持表里查常见的坑是 Resize 的 coordinate_transformation_mode 设置为 half_pixel部分平台只支持 asymmetric这时候要回导出端改 opset 或手工修改 onnx 节点。hi3516cv610 这类海思平台的流程类似先看硬件支持算子再做 NPU 格式转换最后在板端跑精度对比。别指望“用在线转换网站无脑转”转换只是开始真正的坑在前后处理和算子对齐上。4.5 训练自己的数据集后导出失败从 labelme 标注到 YOLO 格式的关键点坑现象用 ultralytics 训练自己的姿态数据集后导出 onnx推理出来的关键点位置是乱的或者 loss 不收敛。原因yolov8 预训练权重下载很方便但换成自己的数据集后数据格式要求 images/ 目录放图片labels/ 目录放同名 txttxt 每一行是class x1 y1 x2 y2 x1 y1 v1 x2 y2 v2 ...注意关键点坐标是相对图片宽度和高度的归一化浮点数。labelme 标注导出的通常是像素坐标必须除以图片尺寸。不少人在这里把 x 坐标除以高度或者忘了加可见性标记 v0 表示该点不在图内导致训练时模型一直在学错误目标。处理数据集用于 yolov8 训练时最容易被忽略的就是这个归一化。解决写一个清洗脚本统一做四件事把像素坐标归一化、补上 visibility 标记、按训练需要的 17 点顺序重新排列、剔除超出图像范围的点。训练前先用 yolo 自带的验证画图功能看一批标注叠加图如果发现骨骼线画错了位置说明标注转换错了这时候不要急着训练要把数据修到完全符合预期再开始。yolov8 画损失函数曲线图只是事后指标真正的问题在数据进入 dataloader 之前就定性了。5. 进阶把骨架坐标变成动作判定并给模型一个可量化的验证指标5.1 用关键点坐标做跌倒判定的最小逻辑当你能稳定输出 17 个点下一步通常是让这些坐标产生业务价值。我做过一个跌倒检测的小功能核心逻辑是判断髋关节高度和躯干倾角。先用 11 和 12 号点也就是左右髋的中点和 5、6 号点也就是左右肩计算躯干向量再比较连续几帧的髋部中心 y 坐标下降速率from math import atan2 hip_center (pts[11] pts[12]) / 2 shoulder_center (pts[5] pts[6]) / 2 trunk_angle abs(atan2(hip_center[1] - shoulder_center[1], hip_center[0] - shoulder_center[0])) if trunk_angle 1.2 and hip_center[1] prev_hip_y: fall_candidate 1这种简单规则适合做演示和初筛真正上线要结合时序滤波避免把“弯腰捡东西”误判成跌倒。5.2 用 OKS 替代肉眼验证模型精度验证模型好坏不要光靠肉眼。我习惯用 OKS也就是 Object Keypoint Similarity作为量化指标它根据关键点位置偏差和每个点的标注方差计算相似度分数越接近 1 越准。取一批标注好的测试集对每个预测关键点计算 OKS 后画分布图能直接看到模型在哪些关键点上系统性偏弱通常是手肘、手腕。这比“看着挺准”更能说服人也方便你在 head 改进、训练轮数、输入尺寸之间做横向对比。5.3 把推理封装成 HTTP 服务最后建议把推理脚本封装成一个小服务用 FastAPI 暴露 HTTP 接口前后端就能直接调用同一份推理逻辑from fastapi import FastAPI, UploadFile import numpy as np import cv2 app FastAPI() app.post(/pose) async def pose(file: UploadFile): data np.frombuffer(await file.read(), np.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) result run_inference(img) return {keypoints: result[kpts].tolist(), boxes: result[boxes].tolist()}这个接口把 run_inference 抽出去单独维护坐标换算都在函数内完成前端拿到的是已经映射回原图的关键点坐标。我自己的习惯是每拿到一个 onnx 模型先做一次“三位一体”检查——打印输出、固定输入尺寸、写一个标准坐标换算函数。这三件事做完后面部署到任何平台都只是换 provider 的问题。希望这篇文章能帮你在姿态识别这条路上少走几步弯路把时间留给真正影响效果的业务逻辑。本文还有配套的精品资源点击获取
返回列表