SDK 数据参考

触觉数据

# 触觉矩阵
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}")

接触检测与接触残差数据流(TactileBinaryTactileResidual)依赖训练好的接触模型。首次使用前,先在 Wuji Studio 完成 触觉接触标定,模型按 SDK 用户与手套序列号自动加载。

触觉矩阵布局

触觉数据来自压阻阵列的 24×31 压力矩阵,共 744 个位置,其中 526 个为有效触点。TactileFrameTactileBinaryTactileResidual 均采用该布局,按 24×31 row-major 排列。访问第 row 行第 col 列:data[row * 31 + col](row 为 0–23,col 为 0–30)。

下图标注了行、列索引在手套上对应的物理触点位置(左:列索引,右:行索引),可据此把矩阵索引(或导出点云中的点顺序)对应到手套的具体部位。

触觉矩阵列索引(col,左)与行索引(row,右)在手套上的物理位置

布局变更

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

触觉矩阵布局变更对比:左为 24×32(固件 v0.10.1 及更早),红框标出无效列 26,右为 24×31(固件 v0.11.0 起)

TactileFrame

标定后的触觉矩阵数据。帧率:120 FPS

字段类型说明
headerFrameHeader帧头
dataList[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

字段类型数组长度有效触点说明
headerFrameHeader帧头
palmList[float]308288手掌区域,14 × 22
thumbList[float]5041拇指区域,10 × 5
indexList[float]4543食指区域,9 × 5
middleList[float]6058中指区域,10 × 6
ringList[float]5452无名指区域,9 × 6
pinkyList[float]5544小指区域,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

点云字段描述。

字段类型说明
namestr字段名称
offsetint字节偏移
typeint数据的数值类型编码,固定为 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 字节,datapoint_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])
字段类型说明
headerFrameHeader帧头
frame_idstr坐标系
point_strideint每个点占用的字节数
fieldsList[PointField]每个点内部字段的描述列表
dataList[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() 同频发布。

字段类型说明
headerFrameHeader帧头
dataList[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.safetensorscontact.npzcontact.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() 同频发布。

字段类型说明
headerFrameHeader帧头
dataList[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_residualtactile_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 个有效
}