BlazePose机器人姿态识别与模仿:C++/Python实现及实时优化

发布时间:2026/9/28 16:19:09
BlazePose机器人姿态识别与模仿:C++/Python实现及实时优化 简介本资源为基于C与Python实现的BlazePose人体姿势识别与模仿算法源码包面向计算机视觉、机器人控制方向的高校学生与开发者尤其适合作为本科生毕业设计参考。仓库按功能划分为五部分BlazePose_train_test负责模型复现与训练测试BlazePose_pc与BlazePose_app分别提供PC端和基于TNN的移动端姿态识别BlazePose_unity与BlazePose_robot则实现虚拟机器人和真实机器人的姿态模仿形成从识别到驱动的完整链路。压缩包共约2000个文件以cc、h、cu、mm、cuh、metal等C与跨平台源码为主辅以xml、java、cmake、py等配置与脚本文件整体约234MB工程结构清晰。目前已有178人学习下载。读者可据此掌握BlazePose关键点检测、TNN推理部署及机器人动作映射的完整实现思路并借助现成工程快速搭建实验环境、对照调试与二次开发。1. 从一段 C 推理代码说起BlazePose 在机器人上到底能干什么很多人第一次听到「机器人人体姿势识别与模仿」脑子里浮现的是人形机器人跟着人跳舞。真上手才发现难点根本不在「跳舞」而在从摄像头画面里稳定地拿到 33 个骨骼关键点再把这 33 个点映射成机器人能执行的关节角度。BlazePose 就是干前一半活的它是 Google 提出的轻量级人体姿态估计模型输入一张 RGB 图输出 33 个关键点的归一化坐标和可见性置信度模型小、速度快适合塞进机器人本体的算力盒子里跑。这个标题里的「C 和 Python 实现」不是随便凑的。常见做法是Python 侧做模型加载、预处理、可视化调试C 侧做实时推理和与机器人控制器的对接两边通过 ONNX Runtime 或 TensorRT 共享同一个模型文件。适合谁看做机器人导航、工业机器人二次开发、四足/人形机器人模仿学习的工程师以及想把姿态估计从「跑个 demo」推进到「闭环控制」的人。下面按「模型怎么跑通 → 关键点怎么用 → 机器人怎么模仿 → 坑在哪」这条线拆开讲。2. BlazePose 的模型结构与 C/Python 双端推理链路2.1 为什么 BlazePose 适合机器人而不是 HRNet机器人场景对姿态模型的要求和服务器端完全不同。HRNet 精度高但参数量大、推理慢塞进 Jetson 或 RK3588 这类边缘盒子帧率直接掉到个位数控制回路根本闭不上。BlazePose 走的是「检测器 关键点回归」两阶段先用 BlazeFace 把人框出来再在裁剪区域内回归 33 个关键点。这个设计的好处是输入分辨率可以压到 256×256单帧推理在边缘设备上能到 30 FPS 以上。33 个关键点的定义比 COCO 的 17 点细多了手掌、脚掌、面部轮廓的点。对机器人模仿来说手掌和脚掌的点很关键——抓取动作要看手腕和指尖行走模仿要看脚踝和脚尖。这也是选 BlazePose 而不是 MoveNet 的原因之一MoveNet 只有 17 点手部信息缺失。模型有三个变体Lite、Full、Heavy。机器人上一般用 FullLite 精度不够Heavy 在边缘设备上跑不动。输入张量是[1, 256, 256, 3]输出是[1, 195]其中 33×5 是关键点信息x、y、z、visibility、presence剩下的是置信度。2.2 Python 侧用 ONNX Runtime 跑通最小推理先把模型跑通别急着接机器人。Python 侧的作用是快速验证模型输出对不对以及做可视化调试。import cv2 import numpy as np import onnxruntime as ort # 加载 BlazePose ONNX 模型providers 按优先级排列 session ort.InferenceSession( blazepose_full.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider] ) def preprocess(frame, size256): # BlazePose 输入是 RGB归一化到 [0,1]不需要 ImageNet 均值方差 img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img cv2.resize(img, (size, size)) img img.astype(np.float32) / 255.0 # 增加 batch 维度变成 [1,256,256,3] return np.expand_dims(img, axis0) def infer(frame): inp preprocess(frame) # 输入名和输出名用 session.get_inputs() 查别硬编码 outputs session.run(None, {input: inp}) # outputs[0] 形状 [1,195]reshape 成 [33,5] landmarks outputs[0].reshape(33, 5) return landmarks cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break lm infer(frame) # visibility 低于 0.5 的点直接丢弃别拿去算角度 valid lm[lm[:, 3] 0.5] print(f有效关键点数量: {len(valid)}) cv2.imshow(pose, frame) if cv2.waitKey(1) 0xFF 27: break这段代码的逻辑说明preprocess里没有做均值方差归一化是因为 BlazePose 训练时就是简单的/255加了反而掉精度这是很多人第一次跑翻车的地方。session.run的第一个参数传None表示取所有输出第二个参数是输入字典键名必须和模型里的输入名一致用session.get_inputs()[0].name打印出来确认。landmarks的 5 列分别是 x、y、z、visibility、presencex 和 y 是归一化到 [0,1] 的z 是相对深度visibility 是「这个点是否在画面内且没被遮挡」的置信度。参数上size256是 Full 变体的标准输入改成 128 会掉精度改成 512 帧率腰斩。providers的顺序决定了优先用哪个后端有 GPU 就把 CUDA 放前面没有就只留 CPU。2.3 C 侧把推理塞进控制循环Python 跑通后真正上机器人要用 C因为控制循环对延迟敏感Python 的 GIL 和解释开销在 30 FPS 以上会拖后腿。C 侧用 ONNX Runtime 的 C API核心流程和 Python 一样但内存管理要自己来。#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp class BlazePoseRunner { public: BlazePoseRunner(const std::string model_path) { Ort::SessionOptions opts; // 设置线程数边缘设备上别超过物理核心数 opts.SetIntraOpNumThreads(4); opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); session_ std::make_uniqueOrt::Session(env_, model_path.c_str(), opts); } std::vectorfloat infer(const cv::Mat frame) { cv::Mat rgb, resized; cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); cv::resize(rgb, resized, cv::Size(256, 256)); resized.convertTo(resized, CV_32F, 1.0 / 255.0); // 构造输入张量NCHW 还是 NHWC 要看模型导出时的设置 std::vectorint64_t shape {1, 256, 256, 3}; std::vectorfloat input_data(resized.ptrfloat(), resized.ptrfloat() resized.total() * resized.channels()); auto input_tensor Ort::Value::CreateTensorfloat( memory_info_, input_data.data(), input_data.size(), shape.data(), shape.size()); const char* input_names[] {input}; const char* output_names[] {output}; auto outputs session_-Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); float* out outputs[0].GetTensorMutableDatafloat(); return std::vectorfloat(out, out 195); } private: Ort::Env env_{ORT_LOGGING_LEVEL_WARNING, blazepose}; Ort::MemoryInfo memory_info_ Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); std::unique_ptrOrt::Session session_; };逻辑说明SetIntraOpNumThreads(4)在 Jetson 上设成 4 比较稳设成 8 反而因为线程调度开销掉帧。CreateTensor这里用的是 CPU 内存如果要走 GPU得用Ort::MemoryInfo::CreateCuda并把数据拷到显存这一步是 C 侧最容易出 access violation 的地方——input_data是局部变量Run返回后如果还有异步操作引用它就会崩。参数上shape的顺序必须和模型导出时一致PyTorch 导出默认是 NCHW但有些转换脚本会转成 NHWC搞错了输出全是乱的。3. 从 33 个关键点到机器人关节角坐标映射与模仿策略3.1 关键点坐标系转换别直接拿归一化坐标去算角度BlazePose 输出的 x、y 是相对输入图像的归一化坐标z 是以髋部中心为原点的相对深度。直接拿这三个值算关节角度结果会随人离摄像头的远近剧烈变化。正确做法是先做「图像坐标 → 世界坐标」的转换。常见做法是用一个已知尺寸的标定物比如棋盘格标定相机内参然后用人体髋部宽度作为尺度参考。假设成年人髋部宽度约 0.35 米从关键点里取左右髋的距离像素就能算出像素到米的换算系数。深度方向用 z 值乘以同一个系数近似。def to_world_coords(landmarks, hip_width_m0.35): # 取左右髋关键点索引 23 和 24 left_hip landmarks[23] right_hip landmarks[24] pixel_dist np.linalg.norm(left_hip[:2] - right_hip[:2]) if pixel_dist 1e-6: return None scale hip_width_m / pixel_dist # 以髋部中点为原点 origin (left_hip[:3] right_hip[:3]) / 2 world (landmarks[:, :3] - origin) * scale return world参数说明hip_width_m这个值因人而异成年人 0.30 到 0.40 之间用固定值会有误差更稳的做法是让用户站直后手动输入身高按身高比例估算。pixel_dist太小说明人离摄像头太远或者关键点检测失败直接返回 None 跳过这一帧别硬算。3.2 关节角度计算用向量夹角而不是坐标差拿到世界坐标后算关节角度要用向量夹角。以肘关节为例取肩、肘、腕三个点算上臂向量和前臂向量的夹角。def angle_between(a, b, c): # a、b、c 是三个关节的世界坐标b 是中间关节 v1 a - b v2 c - b cos_theta np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-8) # 裁剪到 [-1,1] 防止浮点误差导致 arccos 出 nan cos_theta np.clip(cos_theta, -1.0, 1.0) return np.degrees(np.arccos(cos_theta)) # 左臂肩 11肘 13腕 15 elbow_angle angle_between(world[11], world[13], world[15])逻辑说明1e-8是防止除零np.clip是防止浮点误差让arccos返回 nan这两个细节不加跑久了必出玄学 bug。角度算出来是 0 到 180 度机器人关节一般有正负范围需要根据机器人 URDF 里的关节限位做映射。3.3 模仿策略直接映射还是重定向拿到人体关节角度后怎么让机器人模仿有两条路。一条是直接映射人体肘关节弯 90 度机器人肘关节也弯 90 度。这条路简单但要求机器人和人体比例接近否则动作会看起来很怪。另一条是运动重定向把人体骨架的末端位置映射到机器人末端用逆运动学解关节角。这条路复杂但适应性强人形机器人、四足机器人、机械臂都能用。常见做法是混合大关节肩、髋用角度映射末端手腕、脚踝用位置重定向。这样既保证了动作的自然度又保证了末端执行的精度。重定向需要机器人的 URDF 模型和逆运动学求解器ROS2 生态里用 MoveIt 或者 Pinocchio 都能做。4. 实时性优化从 15 FPS 到 30 FPS 的四个手段4.1 输入分辨率与模型变体的取舍Full 变体在 256×256 输入下Jetson Orin 上单帧推理约 20ms加上预处理和后处理整体 30 FPS 左右。如果掉到 15 FPS先查是不是用了 Heavy 变体或者输入被意外放大到 512。用trtexec或者 ONNX Runtime 的 profiling 看各阶段耗时别凭感觉猜。4.2 跳帧推理与关键点插值姿态估计不需要每帧都跑。常见做法是每两帧推理一次中间帧用线性插值补关键点。这样推理负载减半对慢速动作的模仿几乎看不出差别。插值用简单的线性插值就行别上样条样条在关键点抖动时会过冲。def interpolate_landmarks(lm_prev, lm_next, alpha): # alpha 是 0 到 1 之间的插值系数 return lm_prev * (1 - alpha) lm_next * alpha参数说明alpha根据两帧之间的时间差算如果推理间隔固定直接用 0.5。关键点 visibility 低于阈值的点不参与插值保持上一帧的值。4.3 关键点平滑一欧元滤波BlazePose 的输出在帧间会有抖动直接拿去控制机器人关节会抖得像帕金森。一欧元滤波One Euro Filter是姿态估计里常用的平滑方法比卡尔曼滤波参数少、调起来快。class OneEuroFilter: def __init__(self, min_cutoff1.0, beta0.007, d_cutoff1.0): self.min_cutoff min_cutoff self.beta beta self.d_cutoff d_cutoff self.x_prev None self.dx_prev 0.0 def __call__(self, x, dt): if self.x_prev is None: self.x_prev x return x dx (x - self.x_prev) / dt # 根据速度动态调整截止频率慢速时平滑强快速时跟随快 cutoff self.min_cutoff self.beta * abs(dx) alpha 1.0 / (1.0 (1.0 / (2 * np.pi * cutoff)) / dt) x_hat alpha * x (1 - alpha) * self.x_prev self.x_prev x_hat return x_hat参数说明min_cutoff越小越平滑但延迟越大1.0 是常用起点。beta控制速度自适应强度0.007 适合人体动作。dt是帧间隔用实际时间戳算别用固定值。4.4 多线程流水线把采集、推理、控制拆成三个线程用队列传递数据。采集线程只管抓帧推理线程只管跑模型控制线程只管发指令。这样任何一个环节卡顿不会阻塞其他环节。队列长度设成 2 到 3太长会引入延迟太短会丢帧。5. 避坑与排查五个真实踩过的坑5.1 关键点索引对不上动作全乱现象机器人模仿的动作和人的动作完全对不上比如人抬左手机器人抬右手。原因BlazePose 的 33 点索引和 COCO 的 17 点索引不一样很多人拿 COCO 的索引表去查 BlazePose 的输出。比如 COCO 里左肩是 5BlazePose 里左肩是 11。解决把 BlazePose 的 33 点索引表打印出来贴在显示器边上写代码时对着查。左右对称的点索引差 1比如左肩 11 右肩 12左肘 13 右肘 14记住这个规律能减少一半错误。5.2 visibility 阈值设太低遮挡点参与计算现象人转身时被遮挡的那侧手臂角度突然跳到 180 度机器人猛地甩臂。原因visibility 阈值设成了 0.3被遮挡的点 visibility 在 0.3 到 0.5 之间被当成有效点参与了角度计算但坐标是模型猜的误差很大。解决阈值提到 0.5 以上被遮挡的点直接跳过用上一帧的角度保持。如果连续多帧都低于阈值让机器人回到安全姿态别硬跟。5.3 C 侧张量内存生命周期错误现象程序跑几分钟后随机崩溃报 access violation c0000005。原因Ort::Value::CreateTensor用的是外部内存指针如果这个指针指向的内存被释放了而 ONNX Runtime 还在异步引用它就会崩。解决要么用Ort::Value::CreateTensor的 allocator 版本让 ONNX Runtime 自己管内存要么确保输入数据在Run返回前一直有效。别把局部std::vector的data()传进去就完事。5.4 坐标系搞混深度方向反了现象人往前走机器人往后退。原因BlazePose 的 z 值是相对深度越靠近摄像头 z 越小负值但有些机器人坐标系里前方是正 z。直接拿 BlazePose 的 z 去控制方向就反了。解决在坐标转换那一步统一坐标系把 BlazePose 的 z 取反或者乘一个方向系数。这个系数写在配置文件里换机器人时改配置不改代码。5.5 帧率不稳导致滤波参数失效现象一欧元滤波在帧率稳定时效果好帧率一波动就出现延迟忽大忽小。原因滤波器的dt用了固定值 1/30实际帧率在 20 到 35 之间波动dt不准导致截止频率算错。解决用std::chrono或者 Python 的time.time()取实际帧间隔传给滤波器。帧率波动太大时先查采集线程是不是被其他进程抢了 CPU。6. 进阶用重定向把 BlazePose 接到 MuJoCo 仿真里验证真机调试成本高摔一次机器人可能几千块就没了。我一般先在 MuJoCo 里把重定向算法验证通过再上真机。MuJoCo 里加载人形机器人的 MJCF 模型把 BlazePose 的关键点映射成 MuJoCo 的 mocap 点用逆运动学驱动关节。import mujoco import numpy as np model mujoco.MjModel.from_xml_path(humanoid.xml) data mujoco.MjData(model) # 把 BlazePose 的 33 点映射到 MuJoCo 的 mocap 点 # 这里只映射主要关节面部和手部细节先忽略 keypoint_to_mocap { 11: 0, # 左肩 12: 1, # 右肩 13: 2, # 左肘 14: 3, # 右肘 23: 4, # 左髋 24: 5, # 右髋 } def update_mocap(world_landmarks): for kp_idx, mocap_idx in keypoint_to_mocap.items(): if world_landmarks[kp_idx][3] 0.5: data.mocap_pos[mocap_idx] world_landmarks[kp_idx][:3] mujoco.mj_step(model, data)逻辑说明mocap_pos是 MuJoCo 里的虚拟点不参与动力学只作为逆运动学的目标。mj_step每步会求解逆运动学让机器人关节跟随 mocap 点。参数上world_landmarks要先做坐标转换把 BlazePose 的坐标系对齐到 MuJoCo 的世界坐标系否则机器人会朝奇怪的方向动。验证方法在 MuJoCo 里录一段人做动作的视频对比机器人关节角度和人体关节角度的曲线两条曲线重合度到 80% 以上再上真机。重合度不够就调重定向的权重大关节权重高末端权重低。我自己的习惯是每次改完重定向参数先在仿真里跑 100 个动作序列看有没有关节超限或者自碰撞。真机上只做最后的微调这样能省下大量调试时间。希望帮到你。本文还有配套的精品资源点击获取