☰
BlazePose机器人姿态识别与模仿:C++/Python实现及实时优化
2026/9/28 16:18:46 网站建设 项目流程

简介:本资源为基于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。机器人上一般用 Full,Lite 精度不够,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, size=256): # 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, axis=0) 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、presence,x 和 y 是归一化到 [0,1] 的,z 是相对深度,visibility 是「这个点是否在画面内且没被遮挡」的置信度。

参数上,size=256是 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_unique<Ort::Session>(env_, model_path.c_str(), opts); } std::vector<float> 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::vector<int64_t> shape = {1, 256, 256, 3}; std::vector<float> input_data(resized.ptr<float>(), resized.ptr<float>() + resized.total() * resized.channels()); auto input_tensor = Ort::Value::CreateTensor<float>( 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].GetTensorMutableData<float>(); return std::vector<float>(out, out + 195); } private: Ort::Env env_{ORT_LOGGING_LEVEL_WARNING, "blazepose"}; Ort::MemoryInfo memory_info_ = Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); std::unique_ptr<Ort::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_m=0.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_cutoff=1.0, beta=0.007, d_cutoff=1.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 里左肩是 5,BlazePose 里左肩是 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 个动作序列,看有没有关节超限或者自碰撞。真机上只做最后的微调,这样能省下大量调试时间。希望帮到你。

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

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

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

立即咨询