Day9 unitree_G1人形机器人BVH/MocapApi实际输出少于Axis
一、到底发生了什么
测试30秒时,我们得到:
Axis姿态索引范围:
125854 ~ 128738
理论索引数量:
128738 - 125854 + 1 = 2885
30秒内2885帧大约是:
2885 ÷ 30 ≈ 96Hz
但是MocapApi实际交给程序:
source_received=1864
实际速率大约:
1864 ÷ 30 ≈ 62Hz
所以看到:
Axis姿态索引:约96Hz
程序实际取得:约62Hz
中间索引缺少:
2885 - 1864 = 1021
刚好等于:
source_missing_indices=1021
这不是ZMQ丢帧,因为它发生在ZMQ发送之前。
二、有两个主要可能原因
原因一:Axis内部96Hz,但BVH广播主动降采样
可能的数据流是:
传感器/Axis内部解算:96Hz
↓
Axis BVH广播输出:约60Hz
↓
MocapApi:约60Hz
例如Axis内部产生:
100
101
102
103
104
但BVH广播只输出:
100
102
104
程序看到姿态编号跳跃,但实际上Axis根本没有把中间帧通过BVH接口发出来。
如果是这种情况:
- 我们的程序没有丢;
- MocapApi没有丢;
- ZMQ没有丢;
- 是Axis广播端主动限制了输出频率。
解决方法是:
- 在Axis Studio里寻找独立的广播帧率设置。
- 将BVH广播输出帧率设置成96Hz。
- 如果BVH固定只能约60Hz,改用支持96Hz的中间数据广播、原始数据广播或其他官方接口。
- 如果控制系统只需要约60Hz,则把程序参数改成真实输入频率:
-InputFps 60 -OutputFps 60
不能把实际62Hz的输入假装成96Hz,否则速度和插值时间基准会不准确。
原因二:我们的客户端读取一帧太慢
日志中有两个关键数据:
poll_block_ms≈0.03~0.05ms
loop_gap_ms≈15ms
它们分别表示:
poll_block_ms
从MocapApi取一个事件用了多久
结果只有约:
0.04ms
说明:
PollApplicationNextEvent本身很快
loop_gap_ms
两次进入pollFrame之间间隔多久
结果约:
15ms
换算:
1000 ÷ 15 ≈ 66Hz
这和实际约62~65Hz非常接近。
这说明旧客户端的主要时间不是花在“取事件”,而是花在事件取出后的骨骼读取。
三、旧代码为什么可能只能跑到约62Hz
旧实现每收到一个 AvatarUpdated,都会做这些事情:
取得Avatar全部关节列表
↓
逐个关节查询JointTag
↓
根据Tag建立关节映射
↓
读取23个关节位置
↓
读取23个关节旋转
↓
坐标转换
↓
生成NoitomFrame
其中很多信息是静态的。
例如:
LeftArm对应哪个jointHandle
RightLeg对应哪个jointHandle
Head对应哪个jointHandle
在同一个Avatar运行期间通常不会每帧变化。
但是旧程序每帧都重新查一次,相当于:
每拍一张照片,都重新数一遍演员有多少只手、多少条腿,再确认每个关节叫什么。
这些重复SDK调用可能让单帧读取占用约15ms。
而96Hz要求每帧总处理时间小于:
1000 ÷ 96 ≈ 10.42ms
如果读取一帧需要15ms,客户端理论上最多只能处理:
1000 ÷ 15 ≈ 66帧/秒
这与实测非常吻合。
四、我们已经怎么修改了
我们已经把静态关节映射改成缓存。
修改前
每一帧:
获取全部关节
重新查询所有Tag
重新建立映射
读取姿态
修改后
Avatar第一次出现时:
获取全部关节
查询Tag
建立关节映射
缓存jointHandle
后续每一帧:
直接使用缓存的jointHandle
读取位置和旋转
类似:
第一次:
Head → handle 10
LeftArm → handle 25
RightLeg → handle 38
把结果记下来
后面:
直接访问handle 10、25、38
不再重新查名称和Tag
缓存结构大致是:
AvatarHandle
↓
[
HipsHandle,
SpineHandle,
HeadHandle,
LeftArmHandle,
RightArmHandle,
...
]
这样去掉了每帧重复执行的:
GetAvatarJoints- 多次
GetJointTag - 每帧重新构建
map - 每帧重新匹配fallback关节
五、我们还增加了什么诊断数据
为了不继续猜测,增加了:
avg_avatar_read_ms
max_avatar_read_ms
avg_avatar_read_ms
表示平均读取一整套Avatar骨骼需要多少毫秒。
max_avatar_read_ms
表示测试期间最慢的一帧读取用了多少毫秒。
这样重新测试后,就能区分原因。
六、优化后结果应该怎么判断
情况A:输入提升到接近96Hz
例如:
source_recv_rate=94~96Hz
avg_avatar_read_ms明显低于10ms
source_missing_indices明显减少
结论:
主要原因是旧客户端每帧重复解析关节句柄,读取速度不足;缓存优化有效。
情况B:读取很快,但输入仍然约62Hz
例如:
avg_avatar_read_ms=2ms
source_recv_rate=62Hz
结论:
客户端已经有足够性能,但Axis只通过当前BVH/MocapApi链路输出约62Hz。
这时应该去Axis设置或换广播接口,继续改客户端没有意义。
情况C:读取仍然约15ms
例如:
avg_avatar_read_ms=14~16ms
source_recv_rate=62Hz
结论:
即使缓存关节句柄,MocapApi逐关节读取位置和旋转仍然太慢。
每帧仍要调用大约:
23次位置读取
+
23次旋转读取
如果SDK这些调用开销较大,仍然无法达到96Hz。
这时解决方案是:
- 不再通过MocapApi逐关节读取。
- 直接接收Axis输出的一整个BVH二进制帧。
- 一次解析整帧骨骼数据。
- 避免每帧几十次SDK函数调用。
七、现在应该执行什么测试
我们已经完成关节句柄缓存并重新编译。
需要运行一次优化后的测试:
cd "C:\Users\RX01296\Desktop\全身控制7_28\gmr-master_PN - 副本\cpp_xsens_g1"
执行:
.\run_zmq_mujoco.ps1 -MotionInput noitom -NoitomTransport tcp -NoitomIp 127.0.0.1 -NoitomPort 7003 -HumanHeight 1.70 -InputFps 96 -OutputFps 96 -TestDurationSeconds 30 2>&1 | Tee-Object -FilePath ".\fourth_live_test.log"
自动结束后查看:
Select-String -LiteralPath ".\fourth_live_test.log" -Pattern "avg_avatar_read_ms" |
Select-Object -Last 10 |
ForEach-Object { $_.Line }
再查看:
Select-String -LiteralPath ".\fourth_live_test.log" -Pattern "\[Metrics\]" |
Select-Object -Last 10 |
ForEach-Object { $_.Line }
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)