Skip to main content

Overview

WujiGlove.offline_pipeline() builds a solver pipeline that needs no physical device. Feed it EMF pose frames and it computes finger joint angles, fingertip poses, and a 21-joint skeleton. Feed it raw IMU frames and it runs orientation fusion. Three uses: rerun algorithms over a finished recording, develop and debug on a machine with no hardware, and re-solve the same data against a different hand model to see what changes. The most important measurement on this page: the offline pipeline and the online solver produce bit-identical output. Given the same EMF pose frames, offline joint angles differ from the device’s live output by exactly zero. Matching example: examples/python/wuji_glove/3.offline_pipeline.py. For the API and parameter semantics, see Offline Pipeline. This page covers only how to run it and what the measurements show.
The C SDK has no matching offline pipeline API. This page covers Python.

Prerequisites

Hardware

None. This is the only page in the series that needs no device.

Software

Before You Start

To replay real data, read Recording Data first. Running the example’s synthetic frames needs no preparation.

Data Flow

Channels the Pipeline Offers

The object returned by offline_pipeline() exposes these methods: Three read-only properties round it out: sn, hand_side, urdf_source, and urdf_source_path. Hand solving depends on the URDF. IMU fusion doesn’t. Swapping hand models changes only the first three methods.

Where the URDF Comes From

The sn you pass at construction decides where the SDK looks for a hand model. For a serial number that was never calibrated:
urdf_source takes two values here. builtin_default means the built-in generic hand model, with path as None. A calibrated device points at ~/.wuji/sdk/models/<serial number>_hand.urdf. To force a specific model, pass urdf_path:
A missing path raises immediately rather than falling back to the default model:
hand_side accepts only "left" and "right". Anything else raises ValueError: hand_side must be 'left' or 'right'.
When replaying, sn must name the glove that captured the data, not the one on your desk. The wrong serial number loads a different hand model, shifts every solver result, and reports no error.

Consistency With Online Results

This is the section worth reading.

Method

  1. Connect a real glove and use TopicRecorder to capture emf_poses, hand_joint_angles, and hand_skeleton together for 4 seconds
  2. Align the three streams by header.seq in the MCAP file, yielding 525 common frames
  3. Rebuild the recorded emf_poses into EmfPoseArray and feed them to the offline pipeline
  4. Subtract the offline results from the recorded online results, element by element

Results

Across the first 200 frames: Joint angles are exactly identical. The 7-nanometer difference in skeleton positions comes from MCAP’s JSON serialization round-tripping a float, not from the algorithm. Offline replay therefore reproduces online solving without loss. Tuning algorithms on recorded data, comparing hand models, and reproducing production issues all give trustworthy results.

Replay Code

Quaternion takes (x, y, z, w) in that order. When the MCAP stores orientation as an object, read it by field name rather than unpacking positionally.

IMU Fusion

The fuser carries state and advances frame by frame. The example feeds a single frame, which shows nothing, so here is how it evolves over a continuous run.

At Rest

Feeding angular_velocity=(0,0,0) and linear_acceleration=(0,0,9.8) for 399 frames: Orientation locks to the identity quaternion with no drift. When gravity aligns with the ideal Z axis, the fuser produces no correction.

Rotating

Switching to angular_velocity=(0, 0, 1.0) rad/s with timestamps 5 ms apart, 200 frames (1.0 second): One rad/s for one second should give 1 rad, or 57.30°, and the measurement reads 57.0°. That 0.3° gap exists because the first frame has no predecessor to compute a time delta against, so it doesn’t integrate. Only 199 frames integrate: 199/200 x 57.30° = 57.01°, matching the measurement. Two consequences. Timestamps must increase for real, because the fuser derives its integration step entirely from them. And the first frame always returns the initial orientation, so never judge anything by it.

Runtime Handling

Integration Steps

The pipeline needs no explicit shutdown. Let the object go out of scope.

Running It

Measured with no device connected. The example uses a demo serial number that doesn’t exist, so urdf_source reads builtin_default. The three frames raise curl from 0.00 to 0.70, simulating fingers closing. The thumb’s fifth angle rises from 0.629 to 1.087 accordingly, in the right direction. thumb_conf stays at 0.475 because confidence on synthetic data doesn’t vary with curl.

Adapting It

Compare Two Hand Models

Run the same data through two pipelines, one on the default model and one on a calibrated model, then compare joint angles frame by frame to quantify what calibration buys:

Don’t Build a New Pipeline Per Frame

hand_joint_angles() returns a channel object on each call, and calling it repeatedly in a loop is fine. IMU fusion state, however, lives on the pipeline, so a new pipeline resets the fuser. Run a whole batch through one pipeline instance.

Use Synthetic Data for Regression Tests

It runs without hardware, which suits CI. The example’s hand-written three frames extend naturally into fixed-input, fixed-expected-output assertions.