YOLOv8-pose手部关键点模型从PyTorch到ONNX的完整转换指南在计算机视觉领域姿态估计模型的应用越来越广泛特别是手部关键点检测在虚拟现实、手势识别和人机交互等场景中发挥着重要作用。YOLOv8-pose作为Ultralytics推出的最新姿态估计模型以其优异的性能和易用性受到开发者青睐。本文将深入探讨如何将训练好的YOLOv8-pose手部关键点检测模型从PyTorch格式转换为ONNX格式并提供完整的前后处理代码实现。1. 环境准备与模型训练验证在开始模型转换之前确保您已经完成了以下准备工作Python环境推荐使用Python 3.8或更高版本PyTorch安装建议安装2.0.0及以上版本Ultralytics库通过pip安装最新版YOLOv8ONNX运行时安装onnxruntime库用于后续推理验证pip install torch2.0.1 ultralytics onnxruntime验证您的YOLOv8-pose模型是否训练成功from ultralytics import YOLO # 加载训练好的模型 model YOLO(path/to/your/trained_model.pt) # 测试模型推理 results model(test_image.jpg) results[0].show() # 显示检测结果如果模型能够正确输出手部关键点检测结果说明模型训练成功可以继续后续的转换工作。2. PyTorch到ONNX的模型导出YOLOv8提供了简单的导出接口但需要注意几个关键参数from ultralytics import YOLO # 加载训练好的模型 model YOLO(path/to/your/trained_model.pt) # 导出为ONNX格式 model.export( formatonnx, imgsz640, # 与训练时相同的输入尺寸 opset17, # ONNX算子集版本 simplifyTrue, # 简化模型 dynamicFalse, # 固定输入输出维度 halfFalse # 是否使用FP16 )导出过程中需要特别注意以下几点输入尺寸一致性确保imgsz参数与训练时使用的输入尺寸一致OPset版本YOLOv8-pose推荐使用opset 17或更高版本动态维度除非有特殊需求否则建议保持dynamicFalse以获得更好的性能导出完成后您将得到一个.onnx文件可以使用Netron等工具可视化模型结构验证输入输出节点是否符合预期。3. ONNX模型的前处理实现ONNX模型的前处理需要与训练时保持一致以下是完整的前处理代码实现import cv2 import numpy as np def preprocess_warpAffine(image, dst_width640, dst_height640): 图像预处理保持长宽比的缩放和填充 :param image: 输入图像(BGR格式) :param dst_width: 目标宽度 :param dst_height: 目标高度 :return: 预处理后的图像和逆变换矩阵 # 计算缩放比例 scale min(dst_width / image.shape[1], dst_height / image.shape[0]) # 计算填充偏移量 ox (dst_width - scale * image.shape[1]) / 2 oy (dst_height - scale * image.shape[0]) / 2 # 构建仿射变换矩阵 M np.array([ [scale, 0, ox], [0, scale, oy] ], dtypenp.float32) # 执行仿射变换 img_pre cv2.warpAffine( image, M, (dst_width, dst_height), flagscv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue(114, 114, 114) # YOLO风格的填充值 ) # 计算逆变换矩阵(用于后处理中将坐标映射回原图) IM cv2.invertAffineTransform(M) # 图像归一化并转换通道顺序 img_pre (img_pre[...,::-1] / 255.0).astype(np.float32) # BGR-RGB, 归一化 img_pre img_pre.transpose(2, 0, 1)[None] # HWC-CHW并增加batch维度 return img_pre, IM4. ONNX模型的后处理实现后处理是姿态估计模型部署中的关键环节需要正确处理模型输出并还原到原始图像坐标空间def postprocess(pred, IMNone, conf_thres0.25, iou_thres0.45): 后处理函数解析模型输出执行NMS还原坐标到原图空间 :param pred: 模型输出(1,8400,47) :param IM: 前处理的逆变换矩阵 :param conf_thres: 置信度阈值 :param iou_thres: NMS的IOU阈值 :return: 检测结果列表每个元素为[left, top, right, bottom, conf, *keypoints] boxes [] # 筛选置信度高于阈值的预测框 for img_id, box_id in zip(*np.where(pred[...,4] conf_thres)): item pred[img_id, box_id] cx, cy, w, h, conf item[:5] # 计算边界框坐标 left cx - w * 0.5 top cy - h * 0.5 right cx w * 0.5 bottom cy h * 0.5 # 处理关键点(21个关键点每个点有x,y坐标) keypoints item[5:].reshape(-1, 2) # 将关键点坐标映射回原图空间 if IM is not None: keypoints[:, 0] keypoints[:, 0] * IM[0][0] IM[0][2] keypoints[:, 1] keypoints[:, 1] * IM[1][1] IM[1][2] boxes.append([left, top, right, bottom, conf, *keypoints.reshape(-1).tolist()]) # 如果没有检测到目标返回空列表 if not boxes: return [] # 将边界框坐标映射回原图空间 boxes np.array(boxes) if IM is not None: lr boxes[:,[0, 2]] # left和right坐标 tb boxes[:,[1, 3]] # top和bottom坐标 boxes[:,[0,2]] IM[0][0] * lr IM[0][2] boxes[:,[1,3]] IM[1][1] * tb IM[1][2] # 按置信度降序排序 boxes sorted(boxes.tolist(), keylambda x: x[4], reverseTrue) # 执行NMS return NMS(boxes, iou_thres) def NMS(boxes, iou_thres): 非极大值抑制实现 :param boxes: 检测框列表 :param iou_thres: IOU阈值 :return: 保留的检测框列表 remove_flags [False] * len(boxes) keep_boxes [] for i, ibox in enumerate(boxes): if remove_flags[i]: continue keep_boxes.append(ibox) for j in range(i 1, len(boxes)): if remove_flags[j]: continue jbox boxes[j] if iou(ibox, jbox) iou_thres: remove_flags[j] True return keep_boxes def iou(box1, box2): 计算两个边界框的IOU # 计算box1的面积 area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) # 计算box2的面积 area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) # 计算交集区域 left max(box1[0], box2[0]) top max(box1[1], box2[1]) right min(box1[2], box2[2]) bottom min(box1[3], box2[3]) # 计算交集面积 inter max(0, right - left) * max(0, bottom - top) union area1 area2 - inter return inter / union if union 0 else 05. 完整推理流程与可视化将前处理、推理和后处理整合成完整的流程并添加可视化功能import onnxruntime import time class HandPoseEstimator: def __init__(self, model_path, providers[CPUExecutionProvider]): 初始化手部姿态估计器 :param model_path: ONNX模型路径 :param providers: 执行提供者(CPU/CUDA) self.session onnxruntime.InferenceSession(model_path, providersproviders) self.input_name self.session.get_inputs()[0].name self.output_name self.session.get_outputs()[0].name # 手部关键点连接关系(21个关键点) self.skeleton [ [1,2],[2,3],[3,4],[4,5], # 拇指 [1,6],[6,7],[7,8],[8,9], # 食指 [6,10],[10,11],[11,12],[12,13], # 中指 [10,14],[14,15],[15,16],[16,17], # 无名指 [14,18],[18,19],[19,20],[20,21], # 小指 [1,18] # 手掌连接 ] # 关键点和骨骼颜色 self.kpt_color [(0,255,0) for _ in range(21)] # 绿色关键点 self.limb_color [(0,0,255) for _ in range(len(self.skeleton))] # 红色骨骼 def inference(self, image, conf_thres0.25): 执行推理并可视化结果 :param image: 输入图像(BGR格式) :param conf_thres: 置信度阈值 :return: 带标注的结果图像 # 记录推理时间 start_time time.time() # 前处理 img_pre, IM preprocess_warpAffine(image) # ONNX推理 pred self.session.run( [self.output_name], {self.input_name: img_pre.astype(np.float32)} )[0] # 转置维度: (1,47,8400) - (1,8400,47) pred np.transpose(pred, (0, 2, 1)) # 后处理 boxes postprocess(pred, IM, conf_thresconf_thres) # 可视化结果 for box in boxes: left, top, right, bottom map(int, box[:4]) conf box[4] # 绘制边界框 cv2.rectangle(image, (left, top), (right, bottom), (255,0,0), 2) # 绘制置信度文本 label fHand {conf:.2f} (w, h), _ cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1) cv2.rectangle(image, (left, top - 20), (left w, top), (255,0,0), -1) cv2.putText(image, label, (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 1) # 获取关键点(21个点每个点有x,y坐标) keypoints np.array(box[5:]).reshape(-1, 2) # 绘制关键点 for i, (x, y) in enumerate(keypoints): if x 0 and y 0: # 有效关键点 cv2.circle(image, (int(x), int(y)), 5, self.kpt_color[i], -1) # 绘制骨骼连接 for i, (start, end) in enumerate(self.skeleton): start_idx, end_idx start-1, end-1 # 转换为0-based索引 x1, y1 keypoints[start_idx] x2, y2 keypoints[end_idx] # 确保两个关键点都有效 if x1 0 and y1 0 and x2 0 and y2 0: cv2.line(image, (int(x1), int(y1)), (int(x2), int(y2)), self.limb_color[i], 2) # 计算并显示推理时间 infer_time (time.time() - start_time) * 1000 cv2.putText(image, fInfer: {infer_time:.1f}ms, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2) return image # 使用示例 if __name__ __main__: # 初始化估计器 estimator HandPoseEstimator(handpose.onnx) # 读取测试图像 image cv2.imread(test_hand.jpg) # 执行推理 result estimator.inference(image) # 显示结果 cv2.imshow(Hand Pose Estimation, result) cv2.waitKey(0) cv2.destroyAllWindows()6. 性能优化与常见问题解决在实际部署中可能会遇到各种性能问题和兼容性问题以下是一些优化建议和常见问题的解决方案性能优化技巧量化模型将FP32模型量化为INT8可以显著提升推理速度model.export(formatonnx, int8True) # 导出时尝试量化使用TensorRT加速将ONNX模型进一步转换为TensorRT引擎# 需要安装tensorrt和onnx-tensorrt import tensorrt as trt多线程处理对于视频流处理可以使用多线程分离前处理和推理常见问题与解决方案问题现象可能原因解决方案导出失败Opset版本不兼容尝试使用opset 17或更高版本推理结果异常前处理不一致检查归一化方式和图像尺寸是否与训练一致关键点位置偏移后处理坐标转换错误验证逆变换矩阵的计算是否正确性能低下未使用硬件加速检查是否启用了CUDA或TensorRT关键点数据格式差异处理YOLOv8-pose的不同数据集可能使用不同的关键点格式x,y格式只有关键点坐标x,y,v格式坐标加可见性标志(v0不可见v1可见v2遮挡)如果您的模型使用x,y格式而后处理代码是为x,y,v格式设计的需要调整后处理逻辑# 修改后处理中的关键点提取部分 if original_format xyv: keypoints item[5:].reshape(-1, 3)[:, :2] # 只取x,y else: # xy格式 keypoints item[5:].reshape(-1, 2)7. 实际应用扩展基于ONNX格式的手部关键点模型可以在多种平台上部署应用移动端应用通过ONNX Runtime移动版在iOS/Android上运行嵌入式设备部署到边缘计算设备如Jetson系列Web应用使用ONNX.js在浏览器中运行跨平台应用任何支持ONNX运行时的平台对于实时视频处理可以扩展为以下模式def process_video(input_path, output_path, estimator): cap cv2.VideoCapture(input_path) fps cap.get(cv2.CAP_PROP_FPS) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 创建VideoWriter fourcc cv2.VideoWriter_fourcc(*mp4v) out cv2.VideoWriter(output_path, fourcc, fps, (width, height)) frame_count 0 start_time time.time() while cap.isOpened(): ret, frame cap.read() if not ret: break # 执行推理 result estimator.inference(frame) # 写入输出视频 out.write(result) # 显示处理进度 frame_count 1 if frame_count % 10 0: elapsed time.time() - start_time print(fProcessed {frame_count} frames, FPS: {frame_count/elapsed:.2f}) # 实时显示(可选) cv2.imshow(Processing, result) if cv2.waitKey(1) 0xFF ord(q): break cap.release() out.release() cv2.destroyAllWindows()通过以上完整的流程和代码实现开发者可以顺利地将训练好的YOLOv8-pose手部关键点模型从PyTorch导出到ONNX格式并在各种平台上高效部署应用。