EMF & Hand Tracking
EMF Poses
sub = glove.emf_poses().subscribe()
poses = await sub.recv_async()
for pose in poses.poses:
pos = pose.pose.position
print(f"Position: [{pos[0]:+.3f}, {pos[1]:+.3f}, {pos[2]:+.3f}], confidence: {pose.confidence:.2f}")EmfPose
Single-finger EMF pose.
| Field | Type | Description |
|---|---|---|
pose | Pose | Pose (position + orientation) |
confidence | float | Positioning confidence (0.0~1.0, higher is more reliable. Values below 0.9 can be treated as unreliable) |
EmfPoseArray
5-finger EMF pose array. Finger order: thumb, index, middle, ring, pinky.
| Field | Type | Description |
|---|---|---|
header | FrameHeader | Frame header |
poses | List[EmfPose] | 5-finger pose list |
{
"header": {
"seq": 100,
"timestamp_us": 1709876543210,
"frame_id": "l_wrist"
},
"poses": [
{
"pose": {
"position": [0.041, 0.066, 0.022],
"orientation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
}
},
"confidence": 0.95
}
]
}Hand Tracking Products
# Joint angles
sub = glove.hand_joint_angles().subscribe()
angles = await sub.recv_async()
for i, finger in enumerate(angles.fingers):
print(f"Finger {i}: angles={finger.angles}, confidence={finger.confidence:.2f}")
# Hand skeleton
sub = glove.hand_skeleton().subscribe()
skeleton = await sub.recv_async()
for joint in skeleton.joints:
print(f"{joint.name}: position={joint.pose.position}")FingertipPose
Single-finger fingertip pose.
| Field | Type | Description |
|---|---|---|
pose | Pose | Pose |
confidence | float | Confidence |
FingertipPoses
5-finger fingertip poses.
| Field | Type | Description |
|---|---|---|
header | FrameHeader | Frame header |
poses | List[FingertipPose] | 5-finger fingertip poses |
{
"header": {
"seq": 100,
"timestamp_us": 1709876543210,
"frame_id": "l_wrist"
},
"poses": [
{
"pose": {
"position": [0.041, 0.066, 0.022],
"orientation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
}
},
"confidence": 0.95
},
... // 5 fingers total
]
}FingerJointAngles
Single-finger joint angles.
| Field | Type | Description |
|---|---|---|
angles | List[float] | Fixed array of 5 joint-angle slots (radians), accessed by index. The thumb uses all 5 values. The other four fingers use only the first 4, with the 5th value fixed at 0 as padding |
confidence | float | IK solver confidence |
HandJointAngles
Full-hand 21 DoF joint angles, where 21 DoF refers to the degrees of freedom of the hand kinematic model used by the hand tracking algorithm.
Returned per finger. The thumb maps to CMC3 + MCP1 + IP1, so all 5 slots of angles are used. The other four fingers map to MCP2 + PIP1 + DIP1, so only the first 4 slots are used, with the 5th slot as padding. Total 21 DoF for the whole hand. To keep the data structure uniform, the SDK returns a fixed-length 5 angles array per finger, so fingers contains 5 × 5 slots in total.
| Field | Type | Description |
|---|---|---|
header | FrameHeader | Frame header |
fingers | List[FingerJointAngles] | 5-finger joint angles in thumb, index, middle, ring, pinky order |
{
"header": {
"seq": 100,
"timestamp_us": 1709876543210,
"frame_id": ""
},
"fingers": [
{
"angles": [0.12, 0.34, 0.56, 0.78, 0.91],
"confidence": 0.92
},
{
"angles": [0.11, 0.22, 0.33, 0.44, 0.0],
"confidence": 0.88
},
... // 5 fingers total. For non-thumb fingers, the 5th value is 0 padding
]
}SkeletonJoint
Hand skeleton keypoint.
| Field | Type | Description |
|---|---|---|
name | str | Keypoint name (such as index_finger_mcp) |
pose | Pose | Pose |
confidence | float | Confidence |
HandSkeleton
21-keypoint hand skeleton in the MediaPipe topology.
| Field | Type | Description |
|---|---|---|
header | FrameHeader | Frame header |
joints | List[SkeletonJoint] | 21 keypoints |
{
"header": {
"seq": 100,
"timestamp_us": 1709876543210,
"frame_id": "l_wrist"
},
"joints": [
{
"name": "wrist",
"pose": {
"position": [0.0, 0.0, 0.0],
"orientation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
}
},
"confidence": 0.95
},
... // 21 MediaPipe keypoints total
]
}Output Rate Tuning
emf_poses and its derived streams publish at the EMF input rate (120 Hz) by default. To cut IK/FK compute, call glove.emf_poses_rate_divider() to set divider N — the output rate becomes input rate / N.
Calling
glove.emf_poses_rate_divider().set(4) # 120 Hz → 30 Hz
n = glove.emf_poses_rate_divider().get()Values and Default
| Value | Behavior |
|---|---|
1 (default) | No decimation. Output rate equals input rate |
≥ 2 integer | Output rate = input rate / N, not restricted to divisors of 120 (for example, N=5 yields 24 Hz) |
< 1, non-numeric, or unset | Treated as 1. No error, no stream interruption |
The change takes effect within 10 input frames (≤ ~83 ms at 120 Hz).
Affected Streams
| Follows divider | Unaffected |
|---|---|
emf_poses | imu_* (IMU streams) |
tip_poses | tf, tf_static |
hand_joint_angles | tactile, tactile_zones |
hand_skeleton | tactile_binary, tactile_residual |
tactile_point_cloud | — |
EMF solving still runs per input frame. Only the publish step decimates, so tracking continuity stays intact.
Interaction with Calibration
When you call glove.calibrate() or calibrate_blocking(), the SDK temporarily forces the divider to 1 for the capture phase and restores the prior value when capture ends — including error and cancellation paths. Pre-setting N=4 won't stretch the calibration runtime by 4×. See Calibration.