一、到底发生了什么

测试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广播端主动限制了输出频率。

解决方法是:

  1. 在Axis Studio里寻找独立的广播帧率设置。
  2. 将BVH广播输出帧率设置成96Hz。
  3. 如果BVH固定只能约60Hz,改用支持96Hz的中间数据广播、原始数据广播或其他官方接口。
  4. 如果控制系统只需要约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。

这时解决方案是:

  1. 不再通过MocapApi逐关节读取。
  2. 直接接收Axis输出的一整个BVH二进制帧。
  3. 一次解析整帧骨骼数据。
  4. 避免每帧几十次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 }
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐