User Guide

Hand Retargeting

An XHand on an xArm7 replaying a captured hand motionHand-object retargeting across three datasets
An XHand on an xArm7 replaying a captured hand motion.Hand-object retargeting across three datasets.

robokit.helpers.hand_retargeting maps ordered hand points to robot states. Online retargeting solves one frame at a time. Offline retargeting solves a whole trajectory. Complete programs are in src/robokit/examples/hand_retargeting.

How Retargeting Works

flowchart LR POINTS[Ordered target points] --> ONLINE[HandRetargeterOnline] POINTS --> OFFLINE[HandRetargeterOffline] ONLINE --> ONLINE_SOLVE["solve()"] OFFLINE --> SOLVE["solve()"] OFFLINE --> CONTACT["solve_with_contact()"] ONLINE_SOLVE --> STATE[Base transform and joints] SOLVE --> STATE CONTACT --> STATE

Presets

Each robot preset is a module with one hand spec and a config per workflow.

python
from robokit.helpers.hand_retargeting.presets import shadow

spec = shadow.spec
online_config = shadow.online
online_wrist_config = shadow.online_wrist
offline_config = shadow.offline

Order points as spec.target_names. The MANO layout is not assumed.

WorkflowNumPy inputOutput
Online(K, 3) or (B, K, 3)(B, 7 + Q)
Offline(T, K, 3) or (B, T, K, 3)(T, 7 + Q) or (B, T, 7 + Q)

B is batch size, T frame count, K is len(spec.target_names), and Q actuated joints. Each output row is the base transform (x, y, z, qw, qx, qy, qz) followed by the joints.

Online Retargeting

A preset's costs are tasks in config.terms; change one by name:

python
from copy import deepcopy

config = deepcopy(shadow.online)
config.task("smoothness").weight = 0.1
config.task("rest").weight = 0.01

Offline configs work the same way. contact_config() and refine_config() derive the contact and refinement problems from a tracking config.

Online retargeting follows __init__ → warmup → solve → reset, one frame per solve; the caller owns the loop. Each frame after the first is smoothed against the previous solution.

python
from robokit.helpers.hand_retargeting import HandRetargeterOnline

retargeter = HandRetargeterOnline(
    config=shadow.online,
    robot=robot,
    spec=shadow.spec,
    device=device,
)
retargeter.warmup(batch_size=1)

for points in stream:
    qpos = retargeter.solve_numpy(target_points=points)

retargeter.reset()

See 01_online.py, 05_online_batch.py, and 03_batch.py.

Wrist Tracking

Pass an online_wrist preset to HandRetargeterOnline to track the wrist pose and finger joints together. Floating hands take world points and return the base transform in the world frame:

python
from robokit.helpers.hand_retargeting import HandRetargeterOnline

retargeter = HandRetargeterOnline(shadow.online_wrist, robot, shadow.spec, device=device)
retargeter.warmup(batch_size=1)
for points in stream:
    qpos = retargeter.solve_numpy(points)
retargeter.reset()

Wrist presets set reference_origin="wrist"; the default is "world". Inputs and outputs stay in world coordinates either way, including init_state and compute_costs(state). Fixed-base arm rigs take points and states in the robot base frame.

Offline Retargeting

solve_numpy() warms up for its input shape. The Warp API needs warmup(batch_size=B, num_frames=T) before solve().

python
from robokit.helpers.hand_retargeting import HandRetargeterOffline

retargeter = HandRetargeterOffline(
    config=shadow.offline,
    robot=robot,
    spec=shadow.spec,
    device=device,
)
qpos = retargeter.solve_numpy(
    target_points=points,
    root_quat_wxyz=root_quat_wxyz,
)

Pass root_quat_wxyz when a task reads the root pose; the default root_rotation task does. Split long trajectories into chunks yourself.

See 02_offline.py and 04_arm.py.

Contact and Collision

Contact solves take one contact point and mask per spec.contact_target_names entry. Wrap a tracking config with contact_config(). Use refine_config() to keep the result near init_state.

python
from robokit.helpers.hand_retargeting import contact_config

retargeter = HandRetargeterOffline(
    config=contact_config(shadow.offline),
    robot=robot,
    spec=shadow.spec,
    scene=scene,
    device=device,
)
qpos = retargeter.solve_with_contact_numpy(
    target_points=points,
    contact_points=contact_points,
    contact_mask=contact_mask,
    init_state=init_state,
    local_contact_points=local_contact_points,
)

Pass init_state to start from a trajectory; the refinement keeps the result near it.

Pass a WarpScene for scene collision; config.task("collision").weight sets its cost. Self-collision uses the robot's collision spheres and the skip pairs from Robot.load(self_collision_ignore_path=...).

See 06_contact.py. 07_batch_contact.py retargets every right-hand DexYCB capture and writes one NPZ per capture: uv run src/robokit/examples/hand_retargeting/07_batch_contact.py --output-dir PATH.