Transformer使用指导
一、Transformer概述
Transformer(值变换器)是配置驱动映射中的数据转换单元,位于mapping rule的transforms链上,对单个值做转换:类型转换、切片索引、数值缩放、图像编解码、机器人夹爪换算等。
Transformer只在device_to_r2c(上行:将设备数据转换到Observations)和r2c_to_device(下行:将Actions数据转换到设备指令)两个映射段里生效。
三条使用路径如表1所示。
| 场景 | 做法 | 章节 |
|---|---|---|
| 用现成能力 | 在内置清单里找名字,写进YAML。 | |
| 单次覆盖(进程内) | build_transformer_registry(overrides=...) 传实例。 | |
| 需要分发/复用 | 打包成wheel,用entry_point注册。 |
二、快速上手
通过以下示例帮助您快速上手。
YAML写法
机器人配置YAML里,transforms写在一条mapping rule内部:
device_to_r2c:
mappings:
# 腕部相机:ndarray → JPEG bytes(Observations.images.* 必须是 bytes)
- target: "images.color.wrist"
source: "wrist"
transforms:
- ndarray_to_jpeg: {quality: 85}
# 末端位姿:numpy → list
- target_key: "joint_states.position"
source_path: "eef_pose"
transforms: array_to_list
r2c_to_device:
mappings:
# 模型输出 7D → 前 6 维给 eef_pose,第 7 维给夹爪
- target: "eef_pose"
source: "joint_states.position"
transforms:
- slice: {start: 0, end: 6} 完整可运行样例详见在CloudRobo平台下载的R2C SDK包中的config/robot_so101_lerobot_config.yaml。
程序化写法
from r2c_sdk.core.config_mapper import ConfigDrivenMapper
mapper = ConfigDrivenMapper.from_rule_mappings([
{"source": "eef_pose", "target": "joint_states.position",
"transforms": ["array_to_list"]},
])
mapper.map({"eef_pose": [0.1, 0.2, 0.3, 0.0, 0.0, 0.0]}) 查看当前有哪些transformer
from r2c_sdk.core.transformers import TransformerRegistry
print(TransformerRegistry.available_transformers()) # 内置 + 第三方,按字母序
print(TransformerRegistry.lookup("ndarray_to_jpeg")) # 拿到实例,或 None 三、配置语法详解
如下详细说明各配置语法。
transform与transforms
| 写法 | 含义 |
|---|---|
| transform: <条目> | 单数,等价于只有一个元素的链。 |
| transforms: [<条目>, ...] | 复数,按顺序串联。 |
两者互斥,同时出现直接报错:
ValueError: Use 'transforms' or 'transform' (singular) — not both.
transforms: [] 和 transforms:(值为 null)都会报错,不需要转换就删掉这个key。
单个条目的四种写法
| 序号 | 写法 | 示例 | 传给transform() 的config |
|---|---|---|---|
| 1 | 裸名字符串 | to_float | None |
| 2 | 名字:参数 简写 | "crop:bottom_left" | 字符串 "bottom_left" |
| 3 | 单键映射 | {ndarray_to_jpeg: {quality: 85}} | 映射 {"quality": 85} |
| 4 | 单键映射(标量) | {subtract: 1.0} | 1.0 |
transforms: to_float # 写法 1
transforms: "crop:bottom_left" # 写法 2(注意整个条目要加引号)
transforms: [{"ndarray_to_jpeg": {"quality": 85}}] # 写法 3
transforms: [{"subtract": 1.0}] # 写法 4 写法2的config永远是字符串,其适配 crop(区域名)、decode_image(bgr/rgb/gray)这类字符串参数;但对ndarray_to_jpeg这种要dict的会失败:"ndarray_to_jpeg:85" → Invalid config for transformer 'ndarray_to_jpeg': ... got str。
不支持name/config双键写法
下面这种写法(在部分旧文档里出现过)会直接报错:
transforms:
- name: my_scale # ✗ 非法
config:
factor: 0.001 正确写法是将名字作为唯一key,config作为它的值:
transforms:
- my_scale: {factor: 0.001} # ✓ 流水线:链式组合
列表即流水线,前一个的输出是后一个的输入:
transforms:
- "crop:center" # ndarray 裁剪
- {resize: [224, 224]} # 缩放
- {bgr_to_rgb: null} # 通道序(无参数用 null 占位)
- {ndarray_to_jpeg: {quality: 90}} 链中任何一步抛出异常,整条rule失败,异常向上抛给调用方。
程序化注入overrides
build_transformer_registry(overrides=...) 接受实例(注意与entry_point注册类不同),可用于测试或临时替换:
from r2c_sdk.core.config_mapper import ConfigDrivenMapper
from r2c_sdk.core.transformers import build_transformer_registry
registry = build_transformer_registry(overrides={"my_double": MyDouble()})
mapper = ConfigDrivenMapper.from_rule_mappings(rules, transformers=registry) context参数
transform(value, config, context) 的第三个参数是整个源 payload,供transformer按名称反查索引(subtract / euler_modulo 的names模式用它)。
mapper.map({"pose": [0.1, 7.0], "pose_names": ["roll", "yaw"]}) 四、内置Transformer清单
当前版本共63个内置transformer。权威清单以TransformerRegistry.available_transformers()为准(版本升级会新增)。
类型与结构
| 名称 | 说明 | 配置 |
|---|---|---|
| identity | 原样返回。 | 无配置。 |
| to_float / to_int / to_str | 标量类型转换。 | 无配置。 |
| to_bool | 支持 "true"/"yes"/"on"/"1" 等字符串。 | 无配置。 |
| to_list | 可迭代对象转换为list(拒绝str/bytes)。 | 无配置。 |
| list_wrapper | 整体包成[value](与 to_list 展平语义相反)。 | 无配置。 |
序列/数值
| 名称 | 说明 | 配置 |
|---|---|---|
| slice | value[start:end]转换为新list。 | {start: N, end: M}(至少一个)。 |
| index | 取第N个元素。 | {index: N} |
| subtract | 逐元素相减。 | 标量/等长list/,参考subtract的三种模式。 |
| euler_modulo | 指定位置取模(欧拉角回绕)。 | {modulus, indices} 或 {modulus, names, names_path} |
| array_to_list | ndarray转换为list(依赖 .tolist())。 | 无配置。 |
| list_to_ndarray | 序列 转换为float32 ndarray。 | 无配置。 |
| crop | ndarray裁剪。 | 参考crop的三种模式。 |
| resize | ndarray缩放。 | [height, width] |
| normalize | 归一化/缩放。 | {type: scale, range, source_min, source_max} |
| format | HWC与CHW布局转换(双向转换)(仅3D)。 | "HWC" / "CHW" |
| transpose | 任意轴序转置。 | [2, 0, 1]这类整数列表 |
- subtract的三种模式
transforms: [{"subtract": 1.0}] # 标量:所有元素 -1.0 transforms: [{"subtract": [0.0, 0.0, 1.0]}] # 等长列表:逐元素相减 transforms: # 定向模式:只改指定位置 - subtract: {values: [1.0], indices: [2]} transforms: # 定向模式(按名,需 payload 带名字表) - subtract: {values: [1.0], names: [yaw], names_path: pose_names}indices 与 names 互斥;names 模式要求 context 中存在 names_path 指向的名字列表,否则抛错。
- crop的三种模式
transforms: "crop:bottom_left" # 预设区域名 transforms: [{"crop": {"region": "center"}}] # 映射形式 transforms: [{"crop": {"box": [100, 300, 50, 400]}}] # 像素框 [y1, y2, x1, x2]9个预设:top_left、top_right、bottom_left、bottom_right、left_half、right_half、top_half、bottom_half、center。box越界或y1 >= y2会报错。
图像编解码
| 名称 | 方向 | 配置 |
|---|---|---|
| ndarray_to_png | ndarray转换为bytes。 | 无配置。 |
| ndarray_to_jpeg | ndarray转换为bytes。 | {quality: 0-100},默认95。 |
| ndarray_to_webp | ndarray转换为bytes。 | {quality: 0-100},默认80。 |
| ndarray_to_bytes | ndarray转换为裸内存bytes。 | 无配置。 |
| bytes_to_ndarray | 裸 bytes转换为ndarray。 | {dtype: "float32", shape: [...]}必填dtype。 |
| png_to_ndarray | bytes转换为ndarray。 | 无配置。 |
| jpeg_to_ndarray | bytes转换为ndarray。 | 无配置。 |
| webp_to_ndarray | bytes转换为ndarray。 | 无配置。 |
| decode_image | bytes转换为ndarray(自动识别格式)。 | "bgr" / "rgb" / "gray" |
| bgr_to_rgb / rgb_to_bgr | 通道序互换。 | 无配置。 |
| jpeg_to_base64 | bytes/ndarray转换为base64字符串。 | 无配置。 |
WebP体积通常比同画质JPEG小30~40%,编码耗时高约10~15倍,适合带宽敏感场景。
ROS消息
| 名称 | 说明 |
|---|---|
| ros_image_to_ndarray / ros_image_to_png / ros_image_to_jpeg / ros_image_to_webp | sensor_msgs/Image转换为各种目标。 |
| ros_compressed_image_to_ndarray / _png / _jpeg / _webp | sensor_msgs/CompressedImage转换为各种目标。 |
| ros_compress_image_to_*(4 个) | 上一行的别名,兼容compress拼写。 |
| ros_message_to_mapping | 任意ROS消息转换为递归dict。 |
| list_to_ros_float64_multi_array | list转换为std_msgs/Float64MultiArray。 |
| scalar_to_ros_float64 | 标量转换为std_msgs/Float64。 |
| list_to_ros_joint_state | list转换为sensor_msgs/JointState({names, velocity, effort}可选)。 |
| list_to_ros_move_request | 6维位姿转换为jaka_msgs/srv/Move.Request。 |
| select_joints_by_name | 按名字筛选/重排关节数组,如下所示: select_joints_by_name 输入必须是含name(或 names)列表的Mapping,其余等长列表会同步筛选重排: transforms:
- select_joints_by_name:
names: ["joint_1", "joint_3", "joint_5"] 请求的名字在源消息中找不到时直接报错(并打印requested / available / missing三行对照),不会静默补0。 |
ros_image_to_* 支持 rgb8、bgr8、rgba8、bgra8、mono8、8uc1、mono16、16uc1编码。
ROS 消息及机器人夹爪中涉及ROS消息类型的transformer(ros_*、list_to_ros_*、scalar_to_ros_float64、scalar_to_step_motor_gripper*)在调用时才import对应的ROS消息包,因此必须在装有对应ROS运行时的机器上运行;纯Python环境调用会抛ImportError。
机器人夹爪
| 名称 | 适用 | 方向 |
|---|---|---|
| parallel_gripper_fingers_to_stroke | 平行夹爪 | 两指 [left, right] 转换为行程(求和)。 |
| parallel_gripper_stroke_to_fingers | 平行夹爪 | 行程 转换为[S/2, S/2]。 |
| scalar_to_step_motor_gripper | 步进电机夹爪 | 标量 转换为 step_motor/Motor 开合指令(按阈值)。 |
| scalar_to_step_motor_gripper_debounce | 同上 | 连续N次越阈才发指令(去抖动)。 |
| so101_gripper_value_to_joint / so101_gripper_joint_to_value | SO101 | 模型值 ↔ 关节弧度(双向转换)。 |
| extract_gripper_states | 青龙 | 关节列表 转换为 左右夹爪二值状态。 |
| qinglong_gripper_real_transform / qinglong_gripper_visual_transform | 青龙 | 夹爪值 转换为 真实 / 视觉关节指令。 |
| moz1_gripper_value_to_joints / moz1_extract_gripper_states | Moz1 | 夹爪值 ↔ 8 关节(双向转换)。 |
| jaka_gripper_value_to_joints / jaka_extract_gripper_state | Jaka | 夹爪值 ↔ 2 关节(双向转换)。 |
| r1_gripper_value_to_joint / r1_gripper_joint_to_value | 星海图 R1 | 夹爪值 ↔ 2 关节(双向转换)。 |
scalar_to_step_motor_gripper配置项:open_threshold(80.0)、close_threshold(20.0)、id、speed、mode、angle、state、sub_divide。值落在两个阈值之间时返回None(不产生指令)。
五、常用配置
相机图像上行(ndarray)
- target: "images.color.wrist"
source_path: "wrist"
transforms:
- "crop:center"
- {resize: [224, 224]}
- {ndarray_to_jpeg: {quality: 85}} ROS2图像上行
- target: "images.color.front"
source_path: "camera.image"
transforms:
- {ros_image_to_jpeg: {quality: 85}} # 一步到位,无需中间 ndarray 模型输出拆分
- target: "eef_pose"
source: "joint_states.position"
transforms: [{slice: {start: 0, end: 6}}]
- target: "gripper_position"
source: "joint_states.position"
source_index: 6 夹爪双向换算(上行提取状态、下行下发指令)
device_to_r2c:
mappings:
- target: "gripper_stroke"
source: "joint_states.position"
transforms: [parallel_gripper_fingers_to_stroke]
r2c_to_device:
mappings:
- target: "finger_targets"
source: "gripper_stroke"
transforms: [parallel_gripper_stroke_to_fingers] 六、校验、调试与排错
参考以下说明进行校验、调试及处理常见报错。
启动即校验(fail fast)
构造ConfigDrivenMapper时(服务启动阶段)会对每条 rule 的每个transform调用validate_config并检查名字是否存在。配置错误在启动期暴露,不会拖到运行期。
| 错误原文 | 原因分析 | 处理方法 |
|---|---|---|
| Unknown transformer 'x' in rule <src> → <tgt> | 名字拼错,或第三方包未安装/未生效。 | 用available_transformers()核对;检查第三方包,可参考注意事项。 |
| Invalid config for transformer 'x' in rule ...: <原因> | config不合法。 | 根据报错原因修改YAML配置文件。 |
| Each transform mapping must include exactly one transform name | 用了name + config双键写法。 | 修改为{转换器名字:config}。 |
| Use 'transforms' or 'transform' (singular) — not both. | 两个key同时出现。 | 任意删掉一个key。 |
| 'transforms' must not be an empty list. | 空列表/ null。 | 删掉该key。 |
| targets under 'images.*' require an image-encoding transform | images.* 目标没配编码转换。 | 添加ndarray_to_jpeg等。 |
运行期才会出现的错误:Unknown mapper transform: 'x',以及各transformer自己的类型/形状校验异常。
开启日志
import logging
logging.getLogger("r2c_sdk.core.transformers").setLevel(logging.DEBUG) entry_point的冲突与加载失败都以WARNING级别打在这个logger上。
快速定位某个值经过了什么
临时在rule上加identity收敛范围,或写单元测试直接调mapper:
mapper = ConfigDrivenMapper.from_rule_mappings([...]) print(mapper.map(payload))
七、基于entry_point扩展Transformer
当内置能力不够,或希望把转换逻辑独立打包复用时,用Python标准entry_point机制注册第三方transformer,无需修改SDK源码。
加载扩展包的时序图
加载扩展包的时序图如图2所示。
两个关键点:
- 扫描只发生一次(_scanned标志),时机是r2c_sdk.core.transformers首次被import。装完包必须重启进程才能生效。
- 实例被缓存复用(参考注意事项)。
加载转换器的优先级
同名时内置永远胜出,并记录日志:
WARNING r2c_sdk.core.transformers: Transformer 'identity' from entry_point is ignored because a builtin with the same name exists.
第三方名字请添加厂商/项目前缀,例如my_gripper_stroke_to_fingers。
实操:以examples/ur5e_transformer为示例
该示例把内置的平行夹爪“手指 ↔ 行程”(双向转换)换算在SDK之外重新实现了一遍(并换成项目自己的比例系数),是标准的第三方transformer包。
examples/ur5e_transformer/ ├── pyproject.toml # entry_point 声明 ├── my_parallel_gripper_transformers.py # transformer 实现 └── README.md # 安装说明
第 1 步:实现类
my_parallel_gripper_transformers.py:
from dataclasses import dataclass
from typing import Any, Mapping
from r2c_sdk.core.interfaces import IValueTransformer
@dataclass(frozen=True)
class MyGripperFingersToStrokeTransformer(IValueTransformer):
"""两手指位置 -> 行程。"""
def transform(self, value: Any, config: Any = None, context: Any = None) -> Any:
left, right = value
return (float(left) + float(right)) * 1250.0
@dataclass(frozen=True)
class MyGripperStrokeToFingersTransformer(IValueTransformer):
"""行程 -> 两手指位置(逆变换)。"""
def transform(self, value: Any, config: Any = None, context: Any = None) -> Any:
return [float(value) / 2500.0, float(value) / 2500.0] 第 2 步:声明entry_point
pyproject.toml:
[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "my-parallel-gripper-r2c-transformers" version = "0.1.0" requires-python = ">=3.10" dependencies = ["hw-r2c-sdk"] [tool.setuptools] py-modules = ["my_parallel_gripper_transformers"] [project.entry-points."r2c_sdk.transformers"] my_gripper_fingers_to_stroke = "my_parallel_gripper_transformers:MyGripperFingersToStrokeTransformer" my_gripper_stroke_to_fingers = "my_parallel_gripper_transformers:MyGripperStrokeToFingersTransformer"
要点:
- group名固定为r2c_sdk.transformers。
- 等号左边是用户写在YAML里的名字。
- 等号右边是 模块:类,必须是类,不是实例。
- 值用引号包起来(含 : 的TOML值)。
第 3 步:安装
pip install -e . # 开发期(示例目录 README 用的是 pip install --no-build-isolation -e .)
若构建后端在当前环境不可用(受限网络 / 内网源),添加 --no-build-isolation 并预装 setuptools,即示例 README 的做法。uv 用户可直接 uv pip install -e .。
第 4 步:在YAML中使用
device_to_r2c:
mappings:
- target: "gripper_stroke"
source: "joint_states.position" # [left, right]
transform: my_gripper_fingers_to_stroke
r2c_to_device:
mappings:
- target: "gripper_fingers"
source: "gripper_stroke"
transform: my_gripper_stroke_to_fingers 第 5 步:验证
from r2c_sdk.core.transformers import TransformerRegistry, build_transformer_registry
print([n for n in TransformerRegistry.available_transformers() if n.startswith("my_")])
reg = build_transformer_registry()
print(reg["my_gripper_fingers_to_stroke"].transform([0.02, 0.04])) # 75.0
print(reg["my_gripper_stroke_to_fingers"].transform(75.0)) # [0.03, 0.03] 接口契约
class IValueTransformer(ABC):
@abstractmethod
def transform(self, value: Any, config: Any = None, context: Any = None) -> Any: ...
@staticmethod
def validate_config(config: Any) -> None:
"""在加载期校验配置,非法则抛 ValueError。默认空实现。""" - 必须实现transform,签名固定三参数(context 可忽略)。
- validate_config可选覆盖;覆盖了就会在启动期被调用,可在那里做早失败检查。
- transform返回None是合法语义:该rule本帧不产生值(如夹爪去抖动、阈值中间态)。
- 推荐 @dataclass(frozen=True) 写无状态实现;需要内部状态(计数器等)时用普通类。
注意事项
| 注意事项 | 说明 | 规避方法 |
|---|---|---|
| 注册的是类 | SDK调ep.load() 后执行cls(),无参实例化。 | __init__ 不能有必填参数;需要参数请在transform()里读config。 |
| 实例被缓存复用 | 同一名字全进程共用一个实例(_entry_cache),多个mapper共享。 | 有状态transformer 要按需重置状态,或改用纯函数式实现。 |
| 扫描只做一次 | import时扫描,装包后当前进程不感知。 | 重启进程;重装后仍不生效先确认装进了运行时的那个虚拟环境。 |
| 同名内置优先 | 第三方被静默忽略(WARNING日志)。 | 名字加前缀;检查日志关键字 is ignored because a builtin。 |
| 加载失败被跳过 | ep.load() 抛错只记 WARNING,名字不出现在注册表里。 | 排查时看日志Failed to load transformer entry_point,或表现为启动期 Unknown transformer。 |
| 非子类被跳过 | 不是IValueTransformer子类只记WARNING。 | 继承IValueTransformer并实现transform。 |
| 装了没用 | 打包时模块没被包含(py-modules/packages漏配)。 | python -c "import 你的模块" 先确认可导入。 |
打包发布
python -m build # 产出 wheel + sdist pip install dist/xxx-0.1.0-py3-none-any.whl
发布到PyPI或内网源后,用户只需pip install <包名> 即可在YAML里按名引用。运行时不需要--extra-index之外的特殊配置,entry_point元数据随wheel一起安装。
