模型算法包手部算法包

Qnbot 手部算法包输出说明

了解普通机器人手关节命令与 Dynamic OpenXR 25 点骨骼姿态的输出类型、字段和坐标系约定。

最新文档

Qnbot 手部算法包输出说明

SDK 简介

qnbot-sdk 位于手套数据和手部算法包之间:

手套数据 -> qnbot-sdk -> 手部算法包 -> SDK 输出 topic -> 上层应用

SDK 负责把手套数据送入选定的算法包,并将算法结果转换为稳定的公开 Python 数据类型。上层应用通过订阅 topic 接收 Sample[T]:

  • Sample.sequence:当前 topic 的帧序号,可用于检测丢帧或乱序。
  • Sample.value:本帧算法结果。

算法输出分为两类:

使用场景SDK topic返回类型主要数据
普通机器人手重定向device.output()HandJointCommand目标手关节名称和关节角
Dynamic OpenXR 骨骼姿态device.skeleton().pose()HandSkeletonPose固定 25 个骨骼节点的位置和方向

接口提示:普通机器人手只通过 device.output() 返回关节角,不通过该接口返回机器人手的三维关节点坐标。25 点坐标输出是 Dynamic OpenXR 人手骨骼的专用能力。

普通机器人手输出

返回类型

普通机器人手算法包的输出类型是 HandJointCommand:

@dataclass(frozen=True)
class HandJointCommand:
    target: str
    joints: dict[str, float]
    timestamp: float
    source: str | None
    metadata: dict[str, object]
字段含义
target本帧结果所属的目标手标识,用于区分多个输出目标
joints关节名称到关节位置的映射,格式为 {joint_name: angle_rad}
timestamp算法输入/输出时间戳,单位秒
source结果来源标识;没有来源信息时可以为 None
metadataSDK 或算法附加信息;应用不应假设一定包含某个可选键

joints 中每一个值都是关节位置,单位为弧度 rad。它不是角度制,也不是电机编码器值、转速、力矩或电流。

示例结果:

HandJointCommand(
    target="primary",
    joints={
        "thumb_joint": 0.42,
        "index_joint": 0.86,
        "middle_joint": 0.74,
    },
    timestamp=123.456,
    source="retargeting",
    metadata={},
)

上面的关节名仅用于说明数据结构,不代表某一款真实机器人手的固定关节表。

关节名称和数量

关节名称和数量由当前选择的目标手机型算法包决定,不同机器人手之间不相同。SDK 返回的 joints 字典就是当前目标手的实际公开输出集合。

使用时应遵循以下规则:

  • 始终按关节名称取值,不要假设不同手型具有相同的数组下标。
  • 不要按 Python 字典遍历顺序推断机器人控制器的关节顺序。
  • 需要转换为控制器数组时,应由应用维护明确的 controller_joint_order,再按名称组装。
  • 某些手型包含机械耦合或从动关节。算法包可能同时返回主动关节和根据耦合关系计算出的从动关节;不要仅凭关节数量判断自由度。
  • 左右手是否使用不同关节名前缀由具体手型模型决定。应用应使用 SDK 返回的实际名称,不要自行拼接 left_ 或 right_。

安全提示:输出值已经经过算法包的关节限位和已启用的输出处理;应用仍应在发送给真实硬件前执行自身的控制安全检查。

最小订阅示例

下面只展示输出订阅的核心形式,SDK 的设备和目标配置由上层项目负责:

from qnbot_sdk import Sample
from qnbot_sdk.glove import HandJointCommand


def on_output(sample: Sample[HandJointCommand]) -> None:
    command = sample.value
    print("sequence:", sample.sequence)
    print("target:", command.target)
    for joint_name, angle_rad in command.joints.items():
        print(joint_name, angle_rad)


output_subscription = device.output().subscribe(on_output)

Dynamic OpenXR 输出

Dynamic OpenXR 同时提供关节角和 25 点骨骼姿态。两种输出来自同一个算法包,但属于两个不同的 SDK topic,不能把两者的索引直接互相对应。

  • Dynamic OpenXR 25 点骨骼姿态输出示例(一)Dynamic OpenXR 25 点骨骼姿态输出示例(一)
  • Dynamic OpenXR 25 点骨骼姿态输出示例(二)Dynamic OpenXR 25 点骨骼姿态输出示例(二)

25 点骨骼姿态

调用:

skeleton = device.skeleton()
skeleton.pose().subscribe(on_skeleton_pose)

返回类型是 HandSkeletonPose:

@dataclass(frozen=True)
class HandSkeletonPose:
    timestamp_sec: float
    hand_side: Side
    coordinate_frame: str
    names: tuple[str, ...]
    positions_local: tuple[tuple[float, float, float], ...]
    orientations_local_xyzw: tuple[tuple[float, float, float, float], ...]
字段含义
timestamp_sec本帧骨骼姿态时间戳,单位秒
hand_sideSide.LEFT 或 Side.RIGHT
coordinate_frame固定为 wrist_local
names固定 25 个节点名称,顺序见下表
positions_local25 个节点在腕部局部坐标系中的 XYZ 位置,单位米
orientations_local_xyzw25 个节点相对腕部的单位四元数,分量顺序为 XYZW

同一索引上的三个数组元素属于同一个节点:

for index, name in enumerate(pose.names):
    position_m = pose.positions_local[index]
    orientation_xyzw = pose.orientations_local_xyzw[index]

固定节点顺序:

索引节点名称含义
0wrist手腕参考节点,也是局部坐标原点
1–4thumb_metacarpal 至 thumb_tip拇指掌骨、近节、远节和指尖
5–9index_metacarpal 至 index_tip食指掌骨、近节、中节、远节和指尖
10–14middle_metacarpal 至 middle_tip中指掌骨、近节、中节、远节和指尖
15–19ring_metacarpal 至 ring_tip无名指掌骨、近节、中节、远节和指尖
20–24little_metacarpal 至 little_tip小指掌骨、近节、中节、远节和指尖

完整名称顺序:

wrist
thumb_metacarpal, thumb_proximal, thumb_distal, thumb_tip
index_metacarpal, index_proximal, index_intermediate, index_distal, index_tip
middle_metacarpal, middle_proximal, middle_intermediate, middle_distal, middle_tip
ring_metacarpal, ring_proximal, ring_intermediate, ring_distal, ring_tip
little_metacarpal, little_proximal, little_intermediate, little_distal, little_tip

手部节点示意图:Dynamic OpenXR 固定 25 点节点名称手部节点示意图:Dynamic OpenXR 固定 25 点节点名称

坐标系约定

wrist_local 是随手腕运动的局部右手笛卡尔坐标系:

  • 所有位置单位都是米,分量顺序是 (x, y, z)。
  • 所有四元数分量顺序都是 (x, y, z, w)。
  • wrist 位置固定为 (0, 0, 0)。
  • wrist 姿态固定为 (0, 0, 0, 1)。
  • 其他节点的位置都相对同一个 wrist frame 表达,不是相对各自父节点的位置增量。
  • 该接口不提供 world pose。

坐标构造为:

p_local = R_wrist^T * (p_node - p_wrist)
R_local = R_wrist^T * R_node

坐标系提示:如果应用需要机器人、相机、ROS 或世界坐标,应维护显式的腕部到目标坐标系刚体变换。不要通过交换 XYZ、左右手取反或读取算法包内部 URDF 猜测坐标关系。

最小订阅示例

from qnbot_sdk import Sample
from qnbot_sdk.glove import HandSkeletonPose


def on_pose(sample: Sample[HandSkeletonPose]) -> None:
    pose = sample.value
    for index, name in enumerate(pose.names):
        print(
            name,
            pose.positions_local[index],
            pose.orientations_local_xyzw[index],
        )


skeleton = device.skeleton()
pose_subscription = skeleton.pose().subscribe(on_pose)