lerobot设备标定
- lerobot
- 7天前
- 43热度
- 0评论
why calibrate
先来看看标定后的数据
{
"shoulder_pan": { #肩部旋转关节
"id": 1,
"drive_mode": 0,
"homing_offset": -1620,
"range_min": 1142,
"range_max": 2931
},
"shoulder_lift": { #肩部升降关节
"id": 2,
"drive_mode": 0,
"homing_offset": 2025,
"range_min": 844,
"range_max": 3053
},
"elbow_flex": { #肘部弯曲关节
"id": 3,
"drive_mode": 0,
"homing_offset": -1208,
"range_min": 963,
"range_max": 3078
},
"wrist_flex": { #腕部弯曲关节
"id": 4,
"drive_mode": 0,
"homing_offset": 2021,
"range_min": 884,
"range_max": 3222
},
"wrist_roll": { #腕部旋转关节
"id": 5,
"drive_mode": 0,
"homing_offset": -777,
"range_min": 142,
"range_max": 3961
},
"gripper": { #夹爪关节
"id": 6,
"drive_mode": 0,
"homing_offset": 909,
"range_min": 1978,
"range_max": 3522
}
}
上面数据是每个舵机的相关参数,一共有6个舵机,每个舵机都有一个id来标识,可以对应实物来看实际是从下到上。
- id: 电机的唯一标识符,用于在总线通信时精准定位和控制特定电机。
- drive_mode: 电机的驱动模式,取值为 0 表示特定的驱动模式,不同驱动模式会影响电机的运动特性与控制方式。
- homing_offset:归位偏移量,指电机从物理零点位置到校准零点位置的偏移量。此参数能保证电机在每次启动时都能回到准确的零点位置,从而提升运动精度。
- range_min 和 range_max:电机运动范围的最小值和最大值,以数值形式呈现。这两个参数限定了电机的运动边界,避免因超出范围而导致硬件损坏或者运动异常。range_min 和 range_max:电机运动范围的最小值和最大值,以数值形式呈现。这两个参数限定了电机的运动边界,避免因超出范围而导致硬件损坏或者运动异常。
上面的参数信息在代码中lerobot/src/lerobot/robots/koch_follower/koch_follower.py 中回进行配置。
从上面的配置信息可知,之所以要标定,就是要获取电机的一些物理特性,主要是几个方面:明确电机物理运动范围、归一化电机位置、确保机器人运行精度等。
明确电机范围
机器的电机要有特定的物理运动范围,因为发送要是超过这个范围,回造成硬件损坏。使用calibrate操作可以记录每个电机的最小和最大位置限制,保证后续发送的指令都在安全范围内。
lerobot/src/lerobot/robots/so101_follower/so101_follower.py
def calibrate(self) -> None:
# ...已有代码...
print(
"Move all joints sequentially through their entire ranges "
"of motion.\nRecording positions. Press ENTER to stop..."
)
range_mins, range_maxes = self.bus.record_ranges_of_motion()
self.calibration = {}
for motor, m in self.bus.motors.items():
self.calibration[motor] = MotorCalibration(
id=m.id,
drive_mode=0,
homing_offset=homing_offsets[motor],
range_min=range_mins[motor],
range_max=range_maxes[motor],
)
# ...已有代码...
归一化电机位置
不同电机的原始位置值可能都离散的任意值,这些依赖于具体的电机型号,不具备通用性。calibrate 操作能将这些原始位置值归一化为有意义的连续值,像百分比、角度等,方便后续处理和控制。
lerobot/src/lerobot/motors/motors_bus.py
def _normalize(self, id_, val, min_, max_, drive_mode):
motor = self._id_to_name(id_)
if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100:
norm = (((val - min_) / (max_ - min_)) * 200) - 100
normalized_values[id_] = -norm if drive_mode else norm
# ...已有代码...
确保机器运行精度
lerobot/src/lerobot/robots/so100_leader/so100_leader.py 中的 calibrate
def calibrate(self) -> None:
# ...已有代码...
input(f"Move {self} to the middle of its range of motion and press ENTER....")
homing_offsets = self.bus.set_half_turn_homings()
# ...已有代码...
通过校准,可以确定电机的归位偏移量(homing offset),这有助于提高机器人运动的精度和重复性。在实际应用中,准确的位置控制对机器人完成任务至关重要。
最后经过校准后的数据,后续无需在重复校准,校准保存的路径一般在~/.cache/huggingface/lerobot/calibration,实际运行过程中,回进行加载。
保存
def _save_calibration(self, fpath: Path | None = None) -> None:
draccus.dump(self.calibration, f, indent=4)
加载
def _load_calibration(self, fpath: Path | None = None) -> None:
self.calibration = draccus.load(dict[str, MotorCalibration], f)
校准流程
python -m lerobot.calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=R12252801
上面是执行命令示例,接下来个跟踪一下流程。Python 解释器接收到 python -m lerobot.calibrate 命令后,会将 lerobot.calibrate 当作一个可执行模块来处理。按照 Python 模块查找机制,会在 sys.path 所包含的路径里寻找 lerobot/calibrate.py 文件,对应的文件路径为lerobot/src/lerobot/calibrate.py。
@draccus.wrap()
def calibrate(cfg: CalibrateConfig):
init_logging()
logging.info(pformat(asdict(cfg)))
if isinstance(cfg.device, RobotConfig):
device = make_robot_from_config(cfg.device)
elif isinstance(cfg.device, TeleoperatorConfig):
device = make_teleoperator_from_config(cfg.device)
device.connect(calibrate=False)
device.calibrate()
device.disconnect()
if __name__ == "__main__":
calibrate()
调用calibrate含税,从calibrate可以看到主要的步骤为:
- 参数解析与配置初始化
- 创建设备实例
- 连接设备
- 设备校准
- 设备断开连接
参数解析与配置初始化
calibrate函数被@draccus.wrap()装饰,draccus是一个用于解析命令行参数并创建配置对象的库。
@dataclass
class CalibrateConfig:
teleop: TeleoperatorConfig | None = None
robot: RobotConfig | None = None
def __post_init__(self):
if bool(self.teleop) == bool(self.robot):
raise ValueError("Choose either a teleop or a robot.")
self.device = self.robot if self.robot else self.teleop
draccus会把命令行参数--robot.type=so101_follower --robot.port=/dev/ttyACM0 --robot.id=R12252801解析成CalibrateConfig类的实例。robot属性会被初始化为RobotConfig类型的对象,如果是--teleop.type=so101_leader 将会初始化为TeleoperatorConfig类型的对象。RobotConfig对象在config.py中定义的。
@dataclass(kw_only=True)
class RobotConfig(draccus.ChoiceRegistry, abc.ABC):
# Allows to distinguish between different robots of the same type
id: str | None = None
# Directory to store calibration file
calibration_dir: Path | None = None
def __post_init__(self):
if hasattr(self, "cameras") and self.cameras:
for _, config in self.cameras.items():
for attr in ["width", "height", "fps"]:
if getattr(config, attr) is None:
raise ValueError(
f"Specifying '{attr}' is required for the camera to be used in a robot"
)
@property
def type(self) -> str:
return self.get_choice_name(self.__class__)
其会根据命令行参数或配置文件中的类型字段,动态选择并创建对应的配置子类实例。
如当前的参数有type,port,id分别设置为so101_follower、/dev/ttyACM0 和 R12252801。
创建设备实例
根据cfg.device类型,调用对应工厂含税创建设备实例。
if isinstance(cfg.device, RobotConfig):
device = make_robot_from_config(cfg.device)
elif isinstance(cfg.device, TeleoperatorConfig):
device = make_teleoperator_from_config(cfg.device)
由于 cfg.device 是 RobotConfig 类型,所以会调用 make_robot_from_config(cfg.device)。该函数可能定义在 /home/laumy/lerobot/src/lerobot/robots/init.py 或者相关的工厂模块中,依据 cfg.device.type 的值(即 so101_follower),会创建 SO101Follower 类的实例。
连接设备
调用 device.connect(calibrate=False) 方法连接设备,lerobot/src/lerobot/robots/so101_follower/so101_follower.py 文件中,其 connect 方法可能如下:
def connect(self, calibrate: bool = True) -> None:
if self.is_connected:
raise DeviceAlreadyConnectedError(f"{self} already connected")
# 连接串口总线
self.bus = DynamixelBus(port=self.config.port, baudrate=1000000)
self.bus.connect()
if not self.is_calibrated and calibrate:
self.calibrate()
# 连接摄像头
for cam in self.cameras.values():
cam.connect()
self.configure()
logger.info(f"{self} connected.")
因为传入的 calibrate=False,所以不会触发自动校准。在连接过程中,会先连接串口总线,再连接摄像头,最后进行设备配置。
标定动作
调用 device.calibrate() 方法对设备进行校准,SO101Follower 类的 calibrate 方法可能如下:
def calibrate(self) -> None:
logger.info(f"\nRunning calibration of {self}")
# 禁用扭矩
self.bus.disable_torque()
# 设置电机工作模式为位置模式
for motor in self.bus.motors:
self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value)
# 手动操作提示
input(f"Move {self} to the middle of its range of motion and press ENTER....")
# 设置半圈归零偏移量
homing_offsets = self.bus.set_half_turn_homings()
print(
"Move all joints sequentially through their entire ranges "
"of motion.\nRecording positions. Press ENTER to stop..."
)
# 记录关节运动范围
range_mins, range_maxes = self.bus.record_ranges_of_motion()
self.calibration = {}
for motor, m in self.bus.motors.items():
self.calibration[motor] = MotorCalibration(
id=m.id,
drive_mode=0,
homing_offset=homing_offsets[motor],
range_min=range_mins[motor],
range_max=range_maxes[motor],
)
# 将校准数据写入电机
self.bus.write_calibration(self.calibration)
# 保存校准数据
self._save_calibration()
校准过程包含禁用扭矩、设置电机工作模式、手动操作提示、记录关节运动范围、保存校准数据等步骤。
设备断开连接
调用 device.disconnect() 方法断开设备连接,SO101Follower 类的 disconnect 方法如下:
def disconnect(self):
if not self.is_connected:
raise DeviceNotConnectedError(f"{self} is not connected.")
# 断开串口总线连接
self.bus.disconnect(self.config.disable_torque_on_disconnect)
# 断开摄像头连接
for cam in self.cameras.values():
cam.disconnect()
logger.info(f"{self} disconnected.")