触觉数据
# 触觉矩阵
sub = glove.tactile().subscribe()
frame = await sub.recv_async()
print(f"最大压力值: {max(frame.data):.2f}")
# 分区数据
sub = glove.tactile_zones().subscribe()
zones = await sub.recv_async()
print(f"手掌: {zones.palm}, 拇指: {zones.thumb}")接触检测与接触残差数据流(TactileBinary、TactileResidual)依赖训练好的接触模型。首次使用前,先在 Wuji Studio 完成 触觉接触标定,模型按 SDK 用户与手套序列号自动加载。
触觉矩阵布局
触觉数据来自压阻阵列的 24×31 压力矩阵,共 744 个位置,其中 526 个为有效触点。TactileFrame、TactileBinary、TactileResidual 均采用该布局,按 24×31 row-major 排列。访问第 row 行第 col 列:data[row * 31 + col](row 为 0–23,col 为 0–30)。
下图标注了行、列索引在手套上对应的物理触点位置(左:列索引,右:行索引),可据此把矩阵索引(或导出点云中的点顺序)对应到手套的具体部位。

布局变更
固件 v0.11.0 起,触觉矩阵从 24×32(768 值)调整为 24×31(744 值),移除 24×32 布局中的无效列 26。其余列相对顺序不变:列 0–25 索引保持原样,列 27–31 左移为列 26–30。

TactileFrame
标定后的触觉矩阵数据。帧率:120 FPS。
| 字段 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | 帧头 |
data | List[float] | 744 个值,其中 526 个为有效触点(压力值 0.0~1.0),其余为无效触点,固定为 -1.0 |
data 为 744 个值的数组,按 24×31 row-major 排列。访问第 row 行第 col 列:data[row * 31 + col]。无效位置固定为 -1.0,可据此与有效触点的零压力 0.0 区分。
{
"header": {
"seq": 42,
"timestamp_us": 1709876543210,
"frame_id": ""
},
"data": [-1.0, 0.12, 0.45, 0.78, -1.0, 0.33, ...] // 共 744 个值,526 个有效,无效点为 -1.0
}TactileZones
按手指区域聚合的触觉数据。每个区域是一块矩形,按 row-major 排列。矩形内属于该区域的位置给出压力值,其余位置固定为 -1.0。
| 字段 | 类型 | 数组长度 | 有效触点 | 说明 |
|---|---|---|---|---|
header | FrameHeader | — | — | 帧头 |
palm | List[float] | 308 | 288 | 手掌区域,14 × 22 |
thumb | List[float] | 50 | 41 | 拇指区域,10 × 5 |
index | List[float] | 45 | 43 | 食指区域,9 × 5 |
middle | List[float] | 60 | 58 | 中指区域,10 × 6 |
ring | List[float] | 54 | 52 | 无名指区域,9 × 6 |
pinky | List[float] | 55 | 44 | 小指区域,11 × 5 |
六个区域的有效触点合计 526,与 TactileFrame 同掩码。数组长度是矩形的格数,按上表取值即可避免越界。
{
"header": {
"seq": 42,
"timestamp_us": 1709876543210,
"frame_id": ""
},
"palm": [0.0, 0.12, 0.45, ...], // 308 个值,其中 288 个有效触点
// 以下各指的数组长度与有效触点数见上表
"thumb": [0.0, 0.85, 0.92, ...],
"index": [0.0, 0.67, 0.23, ...],
"middle": [0.0, 0.34, ...],
"ring": [0.0, 0.11, ...],
"pinky": [0.0, 0.05, ...]
}PointField
点云字段描述。
| 字段 | 类型 | 说明 |
|---|---|---|
name | str | 字段名称 |
offset | int | 字节偏移 |
type | int | 数据的数值类型编码,固定为 7(float32) |
PointCloud
触觉点云数据:每帧给出手套 526 个有效触点的 3D 位置和压力。位置通过线性混合蒙皮(LBS)从手部骨架解出,坐标系 l_wrist / r_wrist。每点除 x / y / z 还带 pressure(0.0–1.0 归一化压力),可按压力着色做 3D 可视化。
glove.tactile_point_cloud() 的空间位置依赖 EMF 链路解算的手部姿态,输出率随 EMF 链路的 降频因数 同步变化。
字节布局沿用 ROS sensor_msgs/PointCloud2 的自描述格式:data 是扁平字节流,point_stride 是每点字节数,fields 描述每点内部的字段(名字、字节偏移、类型)。当前实现 4 个字段,每点 16 字节,data 共 point_count × 16 字节,按点顺序排布。新增字段时只需在 fields 追加一项,PointCloud 自身的消息定义不变。
┌─────── point 0 (16 bytes) ────┐┌─────── point 1 (16 bytes) ────┐
data = │ x0 (4B) y0 (4B) z0 (4B) p0 4B ││ x1 (4B) y1 (4B) z1 (4B) p1 4B │ ...
└───────────────────────────────┘└───────────────────────────────┘
offset 0 offset 16 offset 32按字节切回每个点:
start = i × point_stride
点 i 的 x = float32(data[start + 0 : start + 4])
点 i 的 y = float32(data[start + 4 : start + 8])
点 i 的 z = float32(data[start + 8 : start + 12])
点 i 的 pressure = float32(data[start + 12 : start + 16])| 字段 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | 帧头 |
frame_id | str | 坐标系 |
point_stride | int | 每个点占用的字节数 |
fields | List[PointField] | 每个点内部字段的描述列表 |
data | List[int] | 扁平字节流,长度 = point_count × point_stride |
| 方法 | 返回 | 说明 |
|---|---|---|
point_count() | int | 获取点数量 |
{
"header": {
"seq": 42,
"timestamp_us": 1709876543210,
"frame_id": "l_wrist"
},
"frame_id": "l_wrist",
"point_stride": 16,
"fields": [
{ "name": "x", "offset": 0, "type": 7 },
{ "name": "y", "offset": 4, "type": 7 },
{ "name": "z", "offset": 8, "type": 7 },
{ "name": "pressure", "offset": 12, "type": 7 }
],
"data": [0, 0, 128, 63, ...] // 原始字节流,每点 16 字节(4 字段 × 4 字节)
}拿到 PointCloud 后按字段布局解包:
import struct
# 当前布局:x, y, z, pressure 都是 float32,每点 16 字节
raw = bytes(cloud.data)
for i in range(cloud.point_count()):
x, y, z, pressure = struct.unpack_from('<ffff', raw, i * cloud.point_stride)TactileBinary
二值接触检测结果,基于触觉数据推断,帧率:120 FPS,与 glove.tactile() 同频发布。
| 字段 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | 帧头 |
data | List[float] | 744 个接触状态值:1.0 接触,0.0 未接触,-1.0 无效触点 |
data 按 24×31 row-major 排列,与 TactileFrame 同形、同掩码(526 个有效触点)。访问第 row 行第 col 列:data[row * 31 + col]。现有触觉网格可视化无需改动即可复用。
接触判定依赖一个训练好的接触模型,模型经 Wuji Studio 触觉接触标定 或 SDK 标定接口 glove.calibrate_tactile() 训练,并按当前 SDK 用户与手套序列号自动加载。未加载模型时,数据流仍以 120 FPS 发布,但所有有效触点恒为 0.0(无接触),无效触点为 -1.0,不会出现 1.0。
从旧版本升级后请重新标定一次:此前训练的模型不会迁移到新的模型目录,SDK 不再加载它,表现与未标定一致。
训练接触模型
接触模型训练一次,按当前 SDK 用户与手套序列号保存(目录规则见 触觉数据路径),订阅时 SDK 自动加载,无需配置路径。训练入口有两个:
- Wuji Studio:在 Wuji Studio 触觉接触标定 中按图形界面引导完成。
- SDK 标定接口:
await glove.calibrate_tactile()(或阻塞版glove.calibrate_tactile_blocking())按提示录制几组动作,SDK 自动完成训练与安装。每组录制结束可以保留、重录或停止,传入on_pose_prompt回调即可交互控制,不传则全程自动执行。完整用法见示例7.tactile_calibration.py(Python)与4_tactile_calibration.c(C,接口说明见 C 接口参考)。
跨机器迁移模型
推荐用 导出与导入数据 整体迁移。手动迁移时必须满足两点:
- 拷贝全部 3 个模型文件:
contact.safetensors、contact.npz与contact.json(位置见 标定产物保存位置)。contact.json是模型的提交标记,缺它模型不会被加载。 - 放入
manager.tactile_model_paths(user_id, sn)返回的paths["dir"]目录。
切换 SDK 用户或重新训练模型后,SDK 会自动热重载。
from wuji_sdk import SdkManager
# 查询该用户与手套对应的模型目录(经 Wuji Studio 使用时由 Studio 写入)
manager = SdkManager.instance()
user_id = manager.current_user()["user_id"]
paths = manager.tactile_model_paths(user_id, "WujiGlove-12345")
print(paths["dir"]) # 把 contact.safetensors、contact.npz 与 contact.json 放到这里
sub = glove.tactile_binary().subscribe()
frame = await sub.recv_async()
contacts = sum(1 for v in frame.data if v == 1.0)
print(f"接触触点数: {contacts}"){
"header": {
"seq": 42,
"timestamp_us": 1709876543210,
"frame_id": ""
},
"data": [-1.0, 0.0, 1.0, 1.0, -1.0, 0.0, ...] // 共 744 个值,526 个有效
}TactileResidual
每像素有符号接触残差,基于触觉数据推断,帧率:120 FPS,与 glove.tactile() 同频发布。
| 字段 | 类型 | 说明 |
|---|---|---|
header | FrameHeader | 帧头 |
data | List[float] | 744 个残差值。正值 = 比基线压得重(接触),约 0 = 无接触,负值 = 比基线更轻,-1.0 表示无效触点 |
data 按 24×31 row-major 排列,与 TactileFrame 同形、同掩码(526 个有效触点)。访问第 row 行第 col 列:data[row * 31 + col]。
残差信号不平滑、不归一化、不二值化,业务侧自行定阈值与后处理。和 TactileBinary 共享同一接触模型,同时订阅两者只跑一次模型推断。
残差解算依赖一个训练好的接触模型,模型经 Wuji Studio 触觉接触标定 或 SDK 标定接口 glove.calibrate_tactile() 训练,并按当前 SDK 用户与手套序列号自动加载。未加载模型时,数据流仍以 120 FPS 发布,但所有有效触点恒接近静止零点,无法反映真实压力变化。
从旧版本升级后请重新标定一次:此前训练的模型不会迁移到新的模型目录,SDK 不再加载它,表现与未标定一致。
tactile_residual 与 tactile_binary 共享同一个模型目录,同样按当前 SDK 用户与手套序列号自动加载,模型的训练入口与迁移方式见 TactileBinary。切换 SDK 用户或重新训练模型后,SDK 会自动热重载。
from wuji_sdk import SdkManager
# 查询该用户与手套对应的模型目录(经 Wuji Studio 使用时由 Studio 写入)
manager = SdkManager.instance()
user_id = manager.current_user()["user_id"]
paths = manager.tactile_model_paths(user_id, "WujiGlove-12345")
print(paths["dir"]) # 把 contact.safetensors、contact.npz 与 contact.json 放到这里
sub = glove.tactile_residual().subscribe()
frame = await sub.recv_async()
valid = [v for v in frame.data if v != -1.0]
print(f"残差范围: [{min(valid):.2f}, {max(valid):.2f}]"){
"header": {
"seq": 42,
"timestamp_us": 1709876543210,
"frame_id": ""
},
"data": [-1.0, 0.05, 1.23, 0.87, -1.0, -0.02, ...] // 共 744 个值,526 个有效
}