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 |
metadata | SDK 或算法附加信息;应用不应假设一定包含某个可选键 |
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 点骨骼姿态输出示例(二)
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_side | Side.LEFT 或 Side.RIGHT |
coordinate_frame | 固定为 wrist_local |
names | 固定 25 个节点名称,顺序见下表 |
positions_local | 25 个节点在腕部局部坐标系中的 XYZ 位置,单位米 |
orientations_local_xyzw | 25 个节点相对腕部的单位四元数,分量顺序为 XYZW |
同一索引上的三个数组元素属于同一个节点:
for index, name in enumerate(pose.names):
position_m = pose.positions_local[index]
orientation_xyzw = pose.orientations_local_xyzw[index]固定节点顺序:
| 索引 | 节点名称 | 含义 |
|---|---|---|
| 0 | wrist | 手腕参考节点,也是局部坐标原点 |
| 1–4 | thumb_metacarpal 至 thumb_tip | 拇指掌骨、近节、远节和指尖 |
| 5–9 | index_metacarpal 至 index_tip | 食指掌骨、近节、中节、远节和指尖 |
| 10–14 | middle_metacarpal 至 middle_tip | 中指掌骨、近节、中节、远节和指尖 |
| 15–19 | ring_metacarpal 至 ring_tip | 无名指掌骨、近节、中节、远节和指尖 |
| 20–24 | little_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 点节点名称
坐标系约定
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)