> ## 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.

# 手部模型标定

## 概述

本篇实践介绍如何通过引导姿势完成手部模型标定，生成适合操作者手型的 URDF。应用场景包括适配不同操作者的手型和建立个人手部追踪模型。

对应的 Python 示例为 [5.calibration.py](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/5.calibration.py)，C 示例为 [4\_user\_calibration.c](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/4_user_calibration.c)。 标定的适用场景、产物存储与版本要求见[标定与配置](/docs/wuji-glove/calibration)，本页说明集成流程和运行时行为。

<Warning>
  本文在实机上由真人佩戴完成了前四个捏合姿势，第五个姿势 `four_finger_bend_90` 超时中止，因此没有拿到最终的 URDF 产物。用户门、六姿势清单与各自的约束阈值、反馈结构、状态机、两条超时错误码、`skip_constraints` 的实际行为全部为实测结果，标定成功后的结果结构按接口契约说明。
</Warning>

## 前置条件

### 硬件

| 项目 | 要求 |
| - | - |
| Wuji Glove | 1 只 |
| 连接方式 | 以太网 |
| 佩戴 | **必须有人戴着手套**，全程约 2 到 5 分钟 |

标定只读传感器、只写 SDK 侧模型文件，不写固件。

### 软件

| 项目 | 要求 |
| - | - |
| 操作系统 | Ubuntu 22.04 LTS 及以上版本 |
| Wuji SDK | v2026.8.31 或更高版本 |
| Python | 3.10 及以上版本 |
| C 工具链 | CMake 3.16 及以上版本，以及解压后的 SDK 包 |

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install wuji-sdk
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    cmake -S examples/c/wuji_glove -B build/glove \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/glove --target 4_user_calibration
    ```
  </Tab>
</Tabs>

### 前序步骤

**切到具名 SDK 用户。** 默认用户不能标定，见下一节。用 `--dry-run` 先确认：

```bash theme={null}
python examples/python/wuji_glove/5.calibration.py --sn <serial number> --dry-run
```

实测输出：

```text theme={null}
current SDK user:
  user_id:      'u_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  display_name: alice
  description:  None
  is_default:   False

Calibration results are stored per SDK user and hand side.

Dry run complete: no device connection or calibration was performed.
```

`--dry-run` 不连设备也不标定，就为确认用户。

## 数据流

```mermaid theme={null}
stateDiagram-v2
    [*] --> CheckUser
    CheckUser --> Connect: 具名 SDK 用户
    CheckUser --> [*]: 默认用户
    Connect --> Calibrating: connect(sn=...)
    Calibrating --> PromptPose
    PromptPose --> WaitingStable: on_feedback
    WaitingStable --> Collecting: 姿势稳定
    Collecting --> PoseDone: 当前姿势完成
    PoseDone --> PromptPose: 还有下一姿势
    PoseDone --> SaveModel: 六个姿势完成
    SaveModel --> Ready: 生成用户对应手别的 URDF
    Ready --> [*]
    Collecting --> [*]: 超时或 abort
```

## 默认用户不能标定

在默认用户下 `--dry-run`，示例会在报错之前先给出引导：

```text theme={null}
You are on the default SDK user, which cannot run calibration.
Create and switch to a user profile first, e.g.:
    user = manager.create_user("user1")
    manager.switch_user(user["user_id"])
or run 4.user.py to manage profiles, then re-run this example.
```

真跑的话，SDK 抛出带错误码的异常：

```text theme={null}
Error: Execute error [calibrate] (0x4108): Default SDK user cannot run calibrate.
Create a user profile first — e.g. SdkManager.create_user("user1") then
switch_user() — and calibrate again; calibration results are stored per user profile
```

记住 **0x4108**，它专指这一种情况。

## 六个引导姿势

`step_total` 实测为 6，顺序固定：

| 步骤 | `step_name` | 操作 | 判据 |
| - | - | - | - |
| 0 | `pinch_index` | 拇指尖碰食指尖 | 两指尖距离 ≤ 0.05 m |
| 1 | `pinch_middle` | 拇指尖碰中指尖 | 同上 |
| 2 | `pinch_ring` | 拇指尖碰无名指尖 | 同上 |
| 3 | `pinch_pinky` | 拇指尖碰小指尖 | 同上 |
| 4 | `four_finger_bend_90` | 四指弯曲约 90 度 | 四指朝向三轴角，见下表 |
| 5 | `flat_open` | 五指伸直，手掌摊平，手腕不动 | 四指朝向三轴角，见下表 |

真人佩戴实测跑到 `step 4/6`，前四个捏合姿势全部通过。

每个姿势之间要**真的把手动过去**，状态机靠检测运动来切步，见下文 `waiting_movement`。

### 判据分两类

前四个捏合姿势只看**一个**指标：拇指与对应手指的指尖距离，上限 0.05 m。做起来直观，捏上就过。

后两个姿势换成**四根手指朝向的三轴角约束**，roll、pitch、yaw 都要落进区间：

| 姿势 | roll | pitch | yaw |
| - | - | - | - |
| `four_finger_bend_90` | −135° \~ −45° | −38° \~ +50° | −35° \~ +40° |
| `flat_open` | −40° \~ +35° | −30° \~ +40° | −30° \~ +50° |

约束作用在**四指**上（不含拇指），所以拇指位置不受此约束。

**第五步是整个流程的实际难点。** 真人实测在这一步耗尽了 120 秒，收到的提示统计：`rotate to decrease roll` 43 次、`rotate to increase yaw` 36 次、`rotate to decrease yaw` 15 次。

`decrease roll` 的意思是 roll 要更负，也就是**手指要弯得更深**。凭感觉弯到四五十度远远不够，区间中心在 −90° 附近，得弯到指根到指尖接近直角。握紧成拳又会让 yaw 跑偏，四指要并排、各自弯曲，不要攥。

`flat_open` 的区间宽得多，自然摊平就能过。

<Info>
  源码里定义了 8 个姿势，另两个是 `thumb_to_index_dip` 与 `index_to_thumb_emf`。默认流程实测只走其中 6 个，`step_total` 恒为 6。
</Info>

## 反馈结构实测

`on_feedback` 回调收到的是一个 dict。实测第一步的完整内容：

```json theme={null}
{
  "operation": "calibrate",
  "step_index": 0,
  "step_total": 6,
  "step_name": "pinch_index",
  "state": "waiting_stable",
  "progress": 0.0,
  "hold_elapsed": 0.0,
  "hold_target": 1.0,
  "variance_ok": true,
  "constraints_ok": false,
  "variance": 9.308185333622987e-09,
  "variance_target": 0.00019999999494757503,
  "metrics": [
    {
      "label": "Fingertip distance",
      "finger": "thumb",
      "finger_b": "index",
      "value": 0.0758685,
      "max": 0.05,
      "error": 0.025868502,
      "unit": "m",
      "hint": "move the two fingertips closer"
    }
  ],
  "hints": ["move the two fingertips closer"]
}
```

字段分三组：

| 组 | 字段 | 用途 |
| - | - | - |
| 进度 | `step_index`、`step_total`、`step_name`、`state`、`progress` | 画进度条 |
| 保持判据 | `hold_elapsed`、`hold_target`、`variance`、`variance_target`、`variance_ok` | 姿势够不够稳 |
| 约束判据 | `constraints_ok`、`metrics`、`hints` | 姿势摆得对不对 |

`metrics` 里每条都自带 `label`、`unit`、当前 `value`、允许上限 `max`、超出量 `error` 和一句人话 `hint`。直接拿去渲染 UI，不用自己写文案。

### 两条判据是独立的

这一点从实测数据看得很清楚。手套静置在桌上、无人佩戴时：

| 判据 | 实测 | 结论 |
| - | - | - |
| 方差 | `9.31e-9`，目标 `2.0e-4` | `variance_ok = true`，静置当然极稳 |
| 约束 | 拇指与食指指尖距离 `0.0759 m`，上限 `0.05 m` | `constraints_ok = false`，没捏合 |

**稳不等于对。** 两条都满足才会进入采集。

## 状态机实测

跳过约束检查跑一轮，捕获到的状态转移：

```text theme={null}
[   0.4s] step 0/6 pinch_index     state=waiting_stable    progress=0.239
[   1.1s] step 0/6 pinch_index     state=collecting        progress=0.0
[   5.1s] step 0/6 pinch_index     state=done              progress=1.0
[   5.1s] step 1/6 pinch_middle    state=waiting_movement  progress=0.0
```

| `state` | 含义 |
| - | - |
| `waiting_stable` | 等姿势稳定下来，`hold_target` 实测为 1.0 秒 |
| `collecting` | 正在采集 |
| `done` | 本步采完 |
| `waiting_movement` | 等待手部移动到下一个姿势 |

真人佩戴跑同一条流程，四个捏合姿势依次走完 `waiting_movement` → `waiting_stable` → `collecting` → `done`，与上面的转移序列一致。

流程里的几个时间常量：

| 常量 | 取值 |
| - | - |
| 姿势保持多久才开始采集 | 1.0 秒 |
| 每个姿势采集多久 | 1.0 秒 |
| 单步超时 | 120 秒 |
| 反馈回调间隔 | 0.3 秒 |

`waiting_movement` 是卡住不动的手套过不去的那一关。上一步 `done` 之后，状态机检测到真实运动后，才认为下一个姿势开始。静置手套永远停在这里，直到超时。

## 两条超时是分开的

| 超时 | 触发条件 | 表现 |
| - | - | - |
| 单步超时 | 某个姿势 120 秒没完成 | `WujiException: Execute error [calibrate] (0x4101): Pose 'pinch_middle' timed out after 120s` |
| 整体超时 | 超过 `timeout_s`（默认 900 秒） | `WujiException: calibrate timed out` |

单步的 120 秒是固定的，`timeout_s` 调的是整体上限。实测把 `timeout_s` 设成 25 秒时，先撞上的是整体超时，抛 `WujiException`。设成 200 秒时，先撞上的是 `pinch_middle` 的单步 120 秒，抛带 **0x4101** 的 `WujiException`。

两种超时均抛出 `WujiException`，通过错误消息区分整体超时与单步超时。

## skip\_constraints 的影响

这个选项名字看着无害，实测下来它把姿势正确性检查整个关掉了。

**静置在桌上、无人佩戴的手套，在 `skip_constraints=True` 下顺利通过了第一个姿势的全部三个阶段。** 采集到的是一份完全错误的「拇指碰食指」数据。

因为 `variance_ok` 只管稳不稳，静置最稳。约束一关，就没有任何东西能判断这个姿势是不是真的摆对了。

所以：

* 调试流程、验证接入、跑自动化冒烟测试时可以用
* **生成真正要用的手模时绝对不要用**，它会安静地产出一份垃圾模型，而且不报任何错

## 集成步骤

完整示例：[Python 手部模型标定](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/5.calibration.py) 和 [C 手部模型标定](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/4_user_calibration.c)。

以下为接入片段。Python 导入与 manager 初始化、C 初始化与连接错误分支、回调定义均沿用 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/5.calibration.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/4_user_calibration.c)。C 每次调用应检查返回状态，失败时按 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/5.calibration.py) 和 [C 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/c/wuji_glove/4_user_calibration.c) 释放已有资源并退出。

### 1. 确认具名用户

标定使用当前 SDK 用户。默认用户会被拒绝，先切换到具名用户。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    manager = SdkManager.instance()
    if manager.current_user()["is_default"]:
        raise RuntimeError("先切换到具名用户")
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    WujiUserInfo current = {0};
    wuji_current_user(&current);
    bool is_default = current.is_default;
    wuji_user_info_free(&current);
    if (is_default) { wuji_shutdown(); return 2; }
    ```
  </Tab>
</Tabs>

### 2. 连接并启动标定

连接目标序列号后，选择可取消的异步 API。正式标定保持 `skip_constraints=False`，为 Ctrl+C 注册取消处理。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    glove = manager.connect(sn=serial_number, device_name="my_glove")
    options = dict(skip_constraints=False, timeout_s=900.0,
                   on_feedback=on_feedback)
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    WujiGloveCalibrationOptions options;
    wuji_glove_calibration_options_default(&options);
    options.skip_constraints = false;
    options.timeout_s = 900.0;
    WujiGloveCalibrationResult result = {0};
    /* run_async_calibration 是 [Python 示例](https://github.com/wuji-technology/wuji-sdk/blob/main/examples/python/wuji_glove/5.calibration.py) 中的辅助函数：
     * start → try_finish 轮询 → 收到退出信号时 cancel → session_free */
    WujiStatus st = run_async_calibration(dev, &options, display, &result);
    /* st 非 OK 时报告错误并清理设备，不读取 result */
    ```
  </Tab>
</Tabs>

### 3. 读取结果并释放资源

将上一步的 Python 标定调用包在 `try/finally` 中，取消或异常时也断开设备。C 辅助函数先释放标定会话，成功后读取并释放结果，所有退出分支都清理设备。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    try:
        result = await glove.calibrate(**options)
        print(result)
    finally:
        manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    wuji_glove_calibration_result_free(&result);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

## 运行与输出

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    # 先确认用户
    python examples/python/wuji_glove/5.calibration.py --sn <serial number> --dry-run
    # 终端引导模式
    python examples/python/wuji_glove/5.calibration.py --sn <serial number>
    # 只看回调与结果流，适合对接自己的 UI
    python examples/python/wuji_glove/5.calibration.py --sn <serial number> --mode api
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/glove/4_user_calibration
    ./build/glove/4_user_calibration --blocking
    ```
  </Tab>
</Tabs>

`--mode terminal`（默认）会给一屏姿势提示，`--mode api` 只打印回调和结果流。

实测无人佩戴、跳过约束时的状态流：

```text theme={null}
[   0.4s] step 0/6 pinch_index     state=waiting_stable    progress=0.239
[   1.1s] step 0/6 pinch_index     state=collecting        progress=0.0
[   5.1s] step 0/6 pinch_index     state=done              progress=1.0
[   5.1s] step 1/6 pinch_middle    state=waiting_movement  progress=0.0
标定结束: WujiException: Execute error [calibrate] (0x4101): Pose 'pinch_middle' timed out after 120s
```

以上为实机运行结果，设备是右手 Wuji Glove，当前为具名 SDK 用户。手套静置无人佩戴，所以流程停在第二个姿势的运动检测上。

标定成功时返回 `handedness` 与 `calibrated_urdf` 路径，模型落到 `~/.wuji/sdk/models/` 下。实测标定超时退出后该目录**没有新增文件**，未完成的标定不会留下半成品。

## 注意事项

| 分支点 | 表现 | 处理 |
| - | - | - |
| 当前是默认 SDK 用户 | `Execute error [calibrate] (0x4108)` | 先切到具名用户 |
| 姿势摆得不对 | `constraints_ok=false`，`hints` 给出指引 | 把 `hints` 显示给用户 |
| 第五步反复 `decrease roll` | 四指弯曲深度不够 | 弯到接近直角，区间中心在 −90° 附近 |
| 第五步反复 `increase/decrease yaw` | 四指朝侧面偏了，或攥成了拳 | 四指并排各自弯曲，不要攥紧 |
| 姿势没端稳 | `variance_ok=false` | 保持不动，等 `hold_elapsed` 走到 `hold_target` |
| 某姿势超过 120 秒 | `0x4101`，指名是哪个姿势 | 重跑，或检查手套佩戴是否正常 |
| 超过 `timeout_s` | `WujiException` | 调大 `timeout_s`，默认 900 秒 |
| 用 `skip_constraints` | 姿势正确性不再校验 | 只用于调试 |
| 标定中途 `Ctrl+C` | 阻塞式会推迟到调用返回，异步式可即时取消 | 需要可取消就用 `calibrate()` |
| 标定未完成 | 不落任何文件 | 实测超时退出后 `~/.wuji/sdk/models/` 无新增 |

阻塞式与异步式的取消语义差别写在文档字符串里：

```text theme={null}
Run Wuji Glove hand model calibration from synchronous Python code.

Blocks the calling thread until calibration finishes or times out;
Ctrl+C is deferred until the call returns. Use ``calibrate`` from
asyncio code when responsive cancellation is needed.
```

要让用户能随时中断，用 `await glove.calibrate(...)`，别用 `calibrate_blocking`。

## 扩展用法

### 标定之前确认是哪个用户

产物按 SDK 用户进行管理，选错用户会覆盖对应用户的模型，而且不会报错。标定脚本开头打一行：

```python theme={null}
print(f"标定将写入用户: {SdkManager.instance().current_user()['display_name']}")
```

### 换手套不用重标

模型按**用户加手别**存，不绑定序列号。同一用户换一只同侧手套，加载的还是这份模型。换到另一只手则要重标。

### 把角度数值显示出来，别只显示 hints

`hints` 只有方向（`rotate to decrease roll`），没有量。真人实测卡在第五步 120 秒，就是因为不知道还差多少。把 `metrics` 里的当前值和允许区间一起渲染出来，调整一次就能到位：

```text theme={null}
姿势✗ 稳✓  Roll=-52.3° 需在[-135,-45] | Pitch=8.1° 需在[-38,50] | Yaw=41.0° 需在[-35,40] ✗
```

做标定向导时这是最值得花的一点力气。

### 把 hints 直接给用户看

`metrics` 里的 `hint` 是现成的人话（比如 `move the two fingertips closer`），`label` 和 `unit` 也齐了。做 UI 时照着渲染就行，别自己另写一套判断逻辑。

### 标定完验证效果

拿标定前录的一段数据，用离线管线分别跑默认手模和新手模，逐帧比关节角，就能量化标定带来的变化。方法见[离线数据处理](/docs/wuji-glove/sdk-practice/offline-replay)。

### 别用 skip\_constraints 生成正式手模

再说一遍：静置的手套在这个选项下能通过姿势检查。它只适合调试流程。

## 相关内容

* [SDK 本地用户管理](/docs/wuji-sdk/sdk-practice/user-management/)：切到具名用户，这是标定的前置条件
* [设置自定义手部模型](/docs/wuji-glove/sdk-practice/custom-hand-model)：换手模对求解结果影响的量化数据
* [离线数据处理](/docs/wuji-glove/sdk-practice/offline-replay)：用录好的数据对比两套手模
* [触觉标定](/docs/wuji-glove/sdk-practice/tactile-calibration)：另一条标定链路，产出接触模型


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