> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wuji.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# 离线解算

离线解算以 Wuji Glove 的 `emf_poses` 生成关节角、指尖位姿和骨架，以原始 IMU 帧生成融合姿态。[数据录制](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/data-recording.md)将输入帧保存为 MCAP 文件，[重放代码](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-practice/offline-replay.md#重放代码)从该文件读取并还原帧。还原后的帧可直接传给离线解算接口，无需连接设备。

## 解算步骤

### 创建离线处理管线

创建离线处理管线时，指定采集设备的序列号与手别。序列号支持 1–64 字节的 ASCII 字母、数字、`.`、`_` 和 `-`，不能仅为 `.` 或 `..`。`urdf_path` 可选，用于固定手部模型，便于重复处理同一批数据。

以下示例使用右手，实际处理时替换为录制数据对应的值。

> [tab] Python
>
> `hand_side` 使用 `"left"` 或 `"right"`，非法值抛出 `WujiException`。`urdf_path=None` 使用下方的模型查找规则。
>
> ```python theme={null}
> from wuji_sdk import WujiGlove
>
> pipeline = WujiGlove.offline_pipeline(
>     sn="WujiGlove-12345",
>     hand_side="right",
>     urdf_path=None,
> )
> ```

> [tab] C
>
> 使用同一 SDK 发行包中的头文件和动态库。处理前调用 `wuji_init`，结束时调用 `wuji_shutdown`。初始化失败的处理和关闭位置见 [C 接口参考：初始化与约定](https://docs.wuji.tech/docs/zh/wuji-sdk/latest/c-sdk-reference.md#初始化与约定)。以下片段放在返回 `int` 的应用函数中，并包含 `wuji_sdk.h`、`stdio.h` 和 `stdlib.h`。
>
> ```c theme={null}
> 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;
> }
> ```

### 复算关节角、指尖位姿和骨架

处理录制数据时，从 MCAP 文件的 `/<序列号>/emf_poses` 通道读取输入帧。解算接口使用的输入类型在 Python 中为 `EmfPoseArray`，在 C 中为 `WujiEmfPoseArray`。Python 的帧读取与还原方法见[重放代码](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-practice/offline-replay.md#重放代码)。

每帧包含 5 个 EMF 位姿。对同一帧依次请求关节角、指尖位姿与骨架，共享一次解算。下面的函数接收 1 帧并输出结果数量。

> [tab] Python
>
> ```python theme={null}
> 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)}")
> ```

> [tab] C
>
> 输出先零初始化。使用结果后释放其堆字段，计算失败时也要释放此前已成功生成的结果。
>
> ```c theme={null}
> 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;
> }
> ```

| 结果 | 内容 |
| - | - |
| [HandJointAngles](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-data-reference/hand-tracking.md#handjointangles) | 21 DoF 手部关节角 |
| [FingertipPoses](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-data-reference/hand-tracking.md#fingertipposes) | 5 指指尖位姿 |
| [HandSkeleton](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-data-reference/hand-tracking.md#handskeleton) | 21 个 MediaPipe 关键点骨架 |

### 复算 IMU 姿态

处理录制数据时，从 MCAP 文件的 `/<序列号>/imu_raw/palm` 通道读取输入帧。解算接口使用的输入类型在 Python 中为原始 `ImuData`，在 C 中为原始 `WujiImuData`。Python 的帧读取与还原方法见[重放代码](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-practice/offline-replay.md#重放代码)。

将原始 IMU 帧传给对应的融合入口，获得姿态。6 路入口分别对应 `palm`、`thumb`、`index`、`middle`、`ring` 和 `pinky`，各自保持融合状态，均不依赖标定 URDF。Python 方法名使用 `imu_data_<部位>()`，C 函数名使用 `wuji_glove_offline_compute_imu_data_<部位>`。

> [tab] Python
>
> `ImuData.raw()` 将 `orientation_covariance[0]` 设为 `-1.0`，表示未融合输入。
>
> ```python theme={null}
> 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}")
> ```

> [tab] C
>
> 原始 `WujiImuData` 的 `orientation_covariance[0]` 设为 `-1.0`。融合结果该字段为 `0.0`，输出全部内联，无需释放。
>
> ```c theme={null}
> 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;
> }
> ```

输入和输出使用同一数据类型。融合后的 `orientation_covariance[0]` 为 `0.0`。

| 结果 | 内容 |
| - | - |
| [ImuData](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-data-reference/imu.md#imudata)（C：`WujiImuData`） | `orientation` 为融合后的朝向四元数，保留原始角速度和线加速度 |

## 示例

以下程序使用合成的右手 EMF 帧和手背 IMU 样本，依次创建管线、计算手部结果与 IMU 姿态，最后释放资源。运行时无需连接设备。

真实数据可使用 Wuji Glove 录制的 MCAP 文件，文件中需包含 `emf_poses` 和 `imu_raw/palm` 两条通道。按[重放代码](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-practice/offline-replay.md#重放代码)从文件还原原始帧并逐帧传入管线。序列号可从通道名核对，手别和采集时使用的 URDF 需在录制时另行记录。

> [tab] Python
>
> 保存为 `offline_pipeline.py`，运行 `python offline_pipeline.py`。
>
> ```python theme={null}
> from wuji_sdk import (
>     EmfPose, EmfPoseArray, FrameHeader, ImuData,
>     Pose, Quaternion, Vector3F64, WujiGlove,
> )
>
> # 创建离线管线。真实重放时使用采集设备的序列号和手别。
> pipeline = WujiGlove.offline_pipeline(sn="WujiGlove-12345", hand_side="right")
>
> # 构造一帧包含 5 指位姿的 EMF 输入。
> 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)
>     ],
> )
>
> # 同一帧依次计算关节角、指尖位姿和骨架。
> 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)}")
>
> # 构造未融合的手背 IMU 输入并计算姿态。
> 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}")
> ```

> [tab] C
>
> 将下面的 C 代码保存为 `offline_pipeline.c`。将同一 SDK 发行包的 C SDK 解压到文件所在目录，并将包根目录命名为 `sdk`，其下应包含 `include/` 和 `lib/`。
>
> ```c theme={null}
> #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};
>
>     // 创建离线管线。真实重放时使用采集设备的序列号和手别。
>     if (!check(wuji_glove_offline_create(
>             "WujiGlove-12345", WUJI_HANDEDNESS_RIGHT, NULL, &pipeline))) goto cleanup;
>
>     // 构造一帧包含 5 指位姿的 EMF 输入。
>     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,
>     };
>
>     // 同一帧依次计算关节角、指尖位姿和骨架。
>     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);
>
>     // 构造未融合的手背 IMU 输入并计算姿态。
>     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:
>     // 释放已生成的输出和离线管线。
>     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;
> }
> ```
>
> 下面的命令使用 C SDK 的头文件和动态库编译 `offline_pipeline.c`，然后运行生成的程序。
>
> ```bash theme={null}
> 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
> ```

两种程序均应输出 `Fingers: 5, tips: 5, skeleton joints: 21`，以及包含 4 个分量的 `Orientation`。姿态数值随 IMU 输入变化，手部结果数值还受手部模型影响。默认 SDK 用户使用内置模型时，日志中出现相应警告是预期行为。这一步确认解算接口可运行，真实录制数据的读取、逐帧重放和在线结果对比见 [SDK 使用实践 · 离线数据处理](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-practice/offline-replay.md)。

## 注意事项

### 选择并核对手部模型

手部结果依赖标定模型。两种语言按相同规则选择 URDF：

1. **显式路径**：传入 `urdf_path` 时直接使用该文件，读取失败时报错，不回退
2. **托管模型**：未指定路径时，优先使用当前 SDK 用户的 `calibration.hand_model_path` 指向的自定义文件，前提是它位于 SDK 托管目录之外。否则使用该用户对应手别的 `left_hand.urdf` 或 `right_hand.urdf`。两者二选一，不互为回退
3. **内置模型**：选中的托管模型文件缺失、无法读取，或路径指向另一手别时，使用内置默认 URDF 并记录警告

> [!WARNING]
>
> 默认 SDK 用户跳过托管模型，未显式指定路径时使用内置模型。要匹配采集时的手部模型，先切换到采集时所用的具名用户，或指定对应 URDF。详见[标定](https://docs.wuji.tech/docs/zh/wuji-glove/latest/sdk-data-reference/calibration.md)。内置模型可能降低手部结果的准确性，IMU 融合不受 URDF 影响。

创建管线后即可解算，无需再查询。托管模型不可用时可能回退到内置 URDF。需要确认实际使用的模型时，读取 `urdf_source` 和 `urdf_source_path`。序列号和手别也可从管线读回。

> [tab] Python
>
> `urdf_source` 为 `"override"`、`"calibration_file"` 或 `"builtin_default"`。内置模型的 `urdf_source_path` 为 `None`。
>
> ```python theme={null}
> print(pipeline.sn)
> print(pipeline.hand_side)
> print(pipeline.urdf_source)
> print(pipeline.urdf_source_path)
> ```

> [tab] C
>
> 来源为 `WUJI_OFFLINE_URDF_SOURCE_OVERRIDE`、`WUJI_OFFLINE_URDF_SOURCE_CALIBRATION_FILE` 或 `WUJI_OFFLINE_URDF_SOURCE_BUILTIN_DEFAULT`。序列号和文件路径支持两段式查询，`needed` 含结尾 NUL。文件路径缓冲区为空或不足时返回 `WUJI_STATUS_ERR_BUFFER_TOO_SMALL`。内置模型没有文件路径，长度查询返回 `WUJI_STATUS_OK`、`needed=1`，有空间时写入空字符串。以下函数为序列号预留 65 字节，并按实际长度分配路径缓冲区。创建管线后调用 `print_pipeline_info(pipeline)`。
>
> ```c theme={null}
> 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;
> }
> ```

### 按采集顺序处理帧

按采集顺序逐帧调用。同一管线保留解算状态，手部解算只缓存最近 1 帧。连续提交相同的手部帧不会再次求解，依次提交帧 A、帧 B、帧 A 时则会重新计算。

每个独立数据流使用独立管线，同一管线串行处理。从头重播或更换标定模型时创建新管线。

离线与在线流程复用相同解算器。对比结果时，保持输入帧、标定模型和处理顺序一致。

### 检查错误并释放资源

> [tab] Python
>
> 调用失败时抛出异常。结果对象由 Python 管理，无需手动释放。管线及其资源对象应在创建它们的线程中使用。重新处理一段录制时创建新的 `pipeline`。
>
> 非法 `hand_side` 抛出 `WujiException`。显式 URDF 不可读或解算失败时，通过异常获取原因。

> [tab] C
>
> 每次调用检查 `WujiStatus`，失败后立即读取 `wuji_last_error()`，再执行清理。完成所有计算后释放管线。不要在其他调用使用句柄时释放，也不要重复释放。
>
> ```c theme={null}
> wuji_glove_offline_free(pipeline);
> pipeline = NULL;
> wuji_shutdown();
> ```
>
> * 必填指针为空、非法手别、`poses_len` 不为 5 等参数错误返回 `WUJI_STATUS_ERR_INVALID_ARG`
> * 非法序列号、不可读的显式 URDF，以及内部解算失败返回 `WUJI_STATUS_ERR_INTERNAL`。内部错误后释放并重建管线
>
> 手部计算失败不会修改输出。复用已有结果前，调用对应的 `wuji_hand_joint_angles_free`、`wuji_fingertip_poses_free` 或 `wuji_hand_skeleton_free` 释放旧值。这些函数接受 `NULL`，也可以重复释放已清空的结构体。不要传入订阅回调中借用的数据。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.