Offline Solving
Integration and Development
Wuji Glove offline solving turns emf_poses into joint angles, fingertip poses, and a hand skeleton, and raw IMU frames into fused orientation. Data Recording saves input frames to an MCAP file. The replay code reads that file and rebuilds the frames. Pass the rebuilt frames directly to the offline solving API without connecting to a device.
Solving Steps
Create the Pipeline
Create an offline processing pipeline with the capture device’s serial number and hand side. The serial number accepts 1–64 bytes of ASCII letters, digits, ., _, and -, excluding . and .. alone. Use the optional urdf_path to pin a hand model when processing the same data again.
The examples use a right hand. Replace these values with those of the recording.
Python
C
hand_side accepts "left" or "right". An invalid value raises WujiException. Set urdf_path=None to use the model lookup rules below.
from wuji_sdk import WujiGlove
pipeline = WujiGlove.offline_pipeline(
sn="WujiGlove-12345",
hand_side="right",
urdf_path=None,
)Use the header and shared library from the same SDK release. Call wuji_init before processing and wuji_shutdown afterward. For initialization error handling and where to call shutdown, see C API Reference: Initialization and conventions. Place this snippet in an application function returning int, with wuji_sdk.h, stdio.h, and stdlib.h included.
if (wuji_init(NULL) != WUJI_STATUS_OK) {
fprintf(stderr, "Init failed: %s\n", wuji_last_error());
return 1;
}
WujiGloveOfflinePipeline *pipeline = NULL;
WujiStatus status = wuji_glove_offline_create(
"WujiGlove-12345", WUJI_HANDEDNESS_RIGHT, NULL, &pipeline);
if (status != WUJI_STATUS_OK) {
fprintf(stderr, "Create failed: %s\n", wuji_last_error());
wuji_shutdown();
return 1;
} Recompute Joint Angles, Fingertip Poses, and Skeleton
When processing recorded data, read input frames from the /<serial_number>/emf_poses channel in the MCAP file. The offline API takes EmfPoseArray in Python or WujiEmfPoseArray in C. See the Python replay code for MCAP reading and frame reconstruction.
Each frame contains five EMF poses. Request joint angles, fingertip poses, and the skeleton from the same frame to share one solve. These functions process one frame and print the result counts.
Python
C
def compute_hand_frame(pipeline, frame):
angles = pipeline.hand_joint_angles().compute(frame)
tips = pipeline.tip_poses().compute(frame)
skeleton = pipeline.hand_skeleton().compute(frame)
print(f"Fingers: {len(angles.fingers)}, tips: {len(tips.poses)}, "
f"skeleton joints: {len(skeleton.joints)}")Zero-initialize the outputs. Free their heap fields after use, including earlier successful results if a later computation fails.
int compute_hand_frame(WujiGloveOfflinePipeline *pipeline, const WujiEmfPoseArray *frame) {
WujiHandJointAngles angles = {0};
WujiFingertipPoses tips = {0};
WujiHandSkeleton skeleton = {0};
WujiStatus status = wuji_glove_offline_compute_hand_joint_angles(pipeline, frame, &angles);
if (status == WUJI_STATUS_OK)
status = wuji_glove_offline_compute_tip_poses(pipeline, frame, &tips);
if (status == WUJI_STATUS_OK)
status = wuji_glove_offline_compute_hand_skeleton(pipeline, frame, &skeleton);
if (status == WUJI_STATUS_OK)
printf("Fingers: %zu, tips: %zu, skeleton joints: %zu\n",
angles.fingers_len, tips.poses_len, skeleton.joints_len);
else
fprintf(stderr, "Compute failed: %s\n", wuji_last_error());
wuji_hand_joint_angles_free(&angles);
wuji_fingertip_poses_free(&tips);
wuji_hand_skeleton_free(&skeleton);
return status == WUJI_STATUS_OK ? 0 : 1;
} | Output | Content |
|---|---|
| HandJointAngles | 21 DoF hand joint angles |
| FingertipPoses | Five fingertip poses |
| HandSkeleton | 21 MediaPipe keypoints |
Recompute IMU Orientation
When processing recorded data, read input frames from the /<serial_number>/imu_raw/palm channel in the MCAP file. The offline API takes raw ImuData in Python or raw WujiImuData in C. See the Python replay code for MCAP reading and frame reconstruction.
Pass each raw IMU frame to the matching fusion entry point to compute orientation. The six entry points correspond to palm, thumb, index, middle, ring, and pinky. Each keeps a separate fusion state and doesn’t depend on the calibration URDF. Python methods follow imu_data_<location>(), and C functions follow wuji_glove_offline_compute_imu_data_<location>.
Python
C
ImuData.raw() sets orientation_covariance[0] to -1.0 to mark an unfused input.
def compute_palm_imu(pipeline, raw):
fused = pipeline.imu_data_palm().compute(raw)
q = fused.orientation
print(f"Orientation: {q.x}, {q.y}, {q.z}, {q.w}")Set the raw WujiImuData field orientation_covariance[0] to -1.0. The fused output sets it to 0.0. All output fields are inline, with nothing to free.
int compute_palm_imu(WujiGloveOfflinePipeline *pipeline, const WujiImuData *raw) {
WujiImuData fused = {0};
WujiStatus status = wuji_glove_offline_compute_imu_data_palm(pipeline, raw, &fused);
if (status != WUJI_STATUS_OK) {
fprintf(stderr, "IMU compute failed: %s\n", wuji_last_error());
return 1;
}
printf("Orientation: %g, %g, %g, %g\n", fused.orientation.x, fused.orientation.y,
fused.orientation.z, fused.orientation.w);
return 0;
} Input and output use the same data type. The fused output has orientation_covariance[0] set to 0.0.
| Output | Content |
|---|---|
ImuData (WujiImuData in C) | orientation contains the fused quaternion alongside the original angular velocity and linear acceleration |
Example
These programs use a synthetic right-hand EMF frame and a raw palm IMU sample. They create a pipeline, compute hand outputs and IMU orientation, and release resources. No device connection is needed.
For real data, use an MCAP file recorded from a Wuji Glove with both emf_poses and imu_raw/palm channels. Follow the replay code to rebuild the raw frames and submit them one by one. Check the serial number against the channel name. Record the hand side and URDF used during capture separately.
Python
C
Save the program as offline_pipeline.py, then run python offline_pipeline.py.
from wuji_sdk import (
EmfPose, EmfPoseArray, FrameHeader, ImuData,
Pose, Quaternion, Vector3F64, WujiGlove,
)
# Create the pipeline. For replay, use the capture device's serial number and hand side.
pipeline = WujiGlove.offline_pipeline(sn="WujiGlove-12345", hand_side="right")
# Construct one EMF frame with five finger poses.
emf = EmfPoseArray(
header=FrameHeader(seq=1, timestamp_us=1000000, frame_id="r_hand_emf_tx"),
poses=[
EmfPose(
pose=Pose(position=[x, 0.0, 0.0],
orientation=Quaternion(0.0, 0.0, 0.0, 1.0)),
confidence=1.0,
)
for x in (0.00, 0.03, 0.07, 0.11, 0.15)
],
)
# Compute joint angles, fingertip poses, and skeleton from the same frame.
angles = pipeline.hand_joint_angles().compute(emf)
tips = pipeline.tip_poses().compute(emf)
skeleton = pipeline.hand_skeleton().compute(emf)
print(f"Fingers: {len(angles.fingers)}, tips: {len(tips.poses)}, "
f"skeleton joints: {len(skeleton.joints)}")
# Construct an unfused palm IMU sample and compute its orientation.
raw_imu = ImuData.raw(
header=FrameHeader(seq=1, timestamp_us=1000000, frame_id="imu_raw/palm"),
angular_velocity=Vector3F64(0.1, 0.2, 0.3),
linear_acceleration=Vector3F64(0.0, 0.0, 9.8),
)
fused = pipeline.imu_data_palm().compute(raw_imu)
q = fused.orientation
print(f"Orientation: {q.x}, {q.y}, {q.z}, {q.w}")Save the C code below as offline_pipeline.c. Put the extracted C SDK package root from the same release in the program’s directory and name it sdk. It should contain include/ and lib/.
#include "wuji_sdk.h"
#include <stdio.h>
static int check(WujiStatus status) {
if (status == WUJI_STATUS_OK) return 1;
fprintf(stderr, "SDK error: %s\n", wuji_last_error());
return 0;
}
int main(void) {
if (!check(wuji_init(NULL))) return 1;
int result = 1;
WujiGloveOfflinePipeline *pipeline = NULL;
WujiHandJointAngles angles = {0};
WujiFingertipPoses tips = {0};
WujiHandSkeleton skeleton = {0};
// Create the pipeline. For replay, use the capture device's serial number and hand side.
if (!check(wuji_glove_offline_create(
"WujiGlove-12345", WUJI_HANDEDNESS_RIGHT, NULL, &pipeline))) goto cleanup;
// Construct one EMF frame with five finger poses.
WujiEmfPose poses[5] = {0};
const float x[5] = {0.00f, 0.03f, 0.07f, 0.11f, 0.15f};
for (size_t i = 0; i < 5; ++i) {
poses[i].pose.position[0] = x[i];
poses[i].pose.orientation.w = 1.0f;
poses[i].confidence = 1.0f;
}
WujiEmfPoseArray emf = {
.header = {.seq = 1, .timestamp_us = 1000000, .frame_id = "r_hand_emf_tx"},
.poses = poses,
.poses_len = 5,
};
// Compute joint angles, fingertip poses, and skeleton from the same frame.
if (!check(wuji_glove_offline_compute_hand_joint_angles(pipeline, &emf, &angles)))
goto cleanup;
if (!check(wuji_glove_offline_compute_tip_poses(pipeline, &emf, &tips))) goto cleanup;
if (!check(wuji_glove_offline_compute_hand_skeleton(pipeline, &emf, &skeleton)))
goto cleanup;
printf("Fingers: %zu, tips: %zu, skeleton joints: %zu\n",
angles.fingers_len, tips.poses_len, skeleton.joints_len);
// Construct an unfused palm IMU sample and compute its orientation.
WujiImuData raw_imu = {
.header = {.seq = 1, .timestamp_us = 1000000, .frame_id = "imu_raw/palm"},
.orientation = {.w = 1.0},
.orientation_covariance = {-1.0},
.angular_velocity = {.x = 0.1, .y = 0.2, .z = 0.3},
.linear_acceleration = {.z = 9.8},
};
WujiImuData fused = {0};
if (!check(wuji_glove_offline_compute_imu_data_palm(pipeline, &raw_imu, &fused)))
goto cleanup;
printf("Orientation: %g, %g, %g, %g\n", fused.orientation.x,
fused.orientation.y, fused.orientation.z, fused.orientation.w);
result = 0;
cleanup:
// Free the outputs and the pipeline.
wuji_hand_joint_angles_free(&angles);
wuji_fingertip_poses_free(&tips);
wuji_hand_skeleton_free(&skeleton);
wuji_glove_offline_free(pipeline);
wuji_shutdown();
return result;
}The command below compiles offline_pipeline.c against the C SDK header and shared library, then runs the program.
SDK="$PWD/sdk"
cc offline_pipeline.c -I "$SDK/include" -L "$SDK/lib" \
-Wl,-rpath,"$SDK/lib" -lwuji_sdk_c -o offline_pipeline &&
./offline_pipeline Both programs should print Fingers: 5, tips: 5, skeleton joints: 21 and an Orientation with four components. IMU input determines the orientation values. Hand output values also depend on the hand model. A warning about the built-in model is expected for the default SDK user. This step confirms that the solving API runs. For MCAP reading, frame-by-frame replay, and a measured comparison with online solving, see SDK Usage Practices: Replaying Data Offline.
Usage Notes
Select and Verify the Hand Model
Hand outputs depend on the calibration model. Both languages select the URDF using the same rules:
- Explicit Path: use
urdf_pathwhen supplied. A read failure returns an error, without a fallback - Managed Model: without an explicit path, use the current SDK user’s custom
calibration.hand_model_pathif it points outside the SDK-managed directory. Otherwise, use that user’sleft_hand.urdforright_hand.urdf. These are alternatives, not fallbacks for each other - Built-In Model: if the selected managed file is missing, unreadable, or named for the other hand, use the built-in default URDF and log a warning
The default SDK user skips managed models and uses the built-in model unless you specify a path. To match the model used during capture, switch to that named user or specify its URDF. See Calibration. The built-in model can reduce hand-output accuracy. URDF selection doesn’t affect IMU fusion.
You can solve frames immediately after creating the pipeline. No query is required. The SDK may fall back to the built-in URDF if a managed model is unavailable. To confirm the model it selected, read urdf_source and urdf_source_path. You can also read back the serial number and hand side.
Python
C
urdf_source is "override", "calibration_file", or "builtin_default". The built-in model’s urdf_source_path is None.
print(pipeline.sn)
print(pipeline.hand_side)
print(pipeline.urdf_source)
print(pipeline.urdf_source_path)The source is WUJI_OFFLINE_URDF_SOURCE_OVERRIDE, WUJI_OFFLINE_URDF_SOURCE_CALIBRATION_FILE, or WUJI_OFFLINE_URDF_SOURCE_BUILTIN_DEFAULT. Serial numbers and file paths support a two-call size query, with needed including NUL. A missing or short file-path buffer returns WUJI_STATUS_ERR_BUFFER_TOO_SMALL. For the built-in model, the size query returns WUJI_STATUS_OK with needed=1, and writes an empty string when space is available. This function reserves 65 bytes for the serial number and allocates the path buffer from its required size. Call print_pipeline_info(pipeline) after creating the pipeline.
int print_pipeline_info(const WujiGloveOfflinePipeline *pipeline) {
char sn[65];
WujiHandedness hand_side;
WujiOfflineUrdfSource source;
WujiStatus status = wuji_glove_offline_serial_number(pipeline, sn, sizeof(sn), NULL);
if (status == WUJI_STATUS_OK)
status = wuji_glove_offline_handedness(pipeline, &hand_side);
if (status == WUJI_STATUS_OK)
status = wuji_glove_offline_urdf_source(pipeline, &source);
if (status != WUJI_STATUS_OK) {
fprintf(stderr, "Query failed: %s\n", wuji_last_error());
return 1;
}
size_t needed = 0;
status = wuji_glove_offline_urdf_source_path(pipeline, NULL, 0, &needed);
if (status != WUJI_STATUS_OK && status != WUJI_STATUS_ERR_BUFFER_TOO_SMALL) {
fprintf(stderr, "Path query failed: %s\n", wuji_last_error());
return 1;
}
char *path = malloc(needed);
if (path == NULL) return 1;
status = wuji_glove_offline_urdf_source_path(pipeline, path, needed, NULL);
if (status == WUJI_STATUS_OK)
printf("SN: %s, hand: %s, URDF source: %d, path: %s\n", sn,
hand_side == WUJI_HANDEDNESS_LEFT ? "left" : "right", (int)source, path);
else
fprintf(stderr, "Path query failed: %s\n", wuji_last_error());
free(path);
return status == WUJI_STATUS_OK ? 0 : 1;
} Process Frames in Capture Order
Submit frames in capture order. Each pipeline retains solver state and caches only the most recent hand frame for hand computations. Consecutive calls with the same hand frame reuse its result. Submitting frames A, B, and A in that order recomputes A.
Use a separate pipeline for each independent stream, and process one pipeline serially. Create a new pipeline to restart playback or change the calibration model.
Offline and online processing share the same solvers. Use the same input frames, calibration model, and processing order when comparing results.
Check Errors and Release Resources
Python
C
Failed calls raise exceptions. Python manages result objects, so no manual release is needed. Use the pipeline and its resource objects on the thread that created them. Create a new pipeline for another replay.
An invalid hand_side raises WujiException. An unreadable explicit URDF or a failed computation reports its cause through an exception.
Check WujiStatus after each call. On failure, read wuji_last_error() before cleanup. Free the pipeline after all computations finish. Don’t free the handle while another call uses it, and don’t free it twice.
wuji_glove_offline_free(pipeline);
pipeline = NULL;
wuji_shutdown();- NULL required pointers, an invalid hand side, or a pose count other than five return
WUJI_STATUS_ERR_INVALID_ARG - An invalid serial number, an unreadable explicit URDF, or an internal solver failure returns
WUJI_STATUS_ERR_INTERNAL. Free and recreate the pipeline after an internal error
Failed hand computations leave outputs unchanged. Before reusing a result, release its old value with the matching wuji_hand_joint_angles_free, wuji_fingertip_poses_free, or wuji_hand_skeleton_free. These functions accept NULL and can safely free a cleared structure again. Don’t pass borrowed subscription callback data.
Documentation