ABEJA Tech Blog

中の人の興味のある情報を発信していきます

【ロボット動かす】LeRobotのプラグイン拡張でノットフィジカルなAIを実装する!【部屋が欲しい】

はじめに

この記事は、ABEJAアドベントカレンダー2025の9日目の記事です。

こんにちは。
株式会社ABEJAのシステム開発部でエンジニアをしている中島です。

最近、(私の周囲だけかもしれませんが)フィジカルAIという単語を耳にするようになりました。
本記事を執筆中にも隣を見れば、
国際ロボット展なるもので人型のロボットが踊ったり、大型のロボットが車を軽々振り回しているようで個人的にはワクワクしながら拝見させていただいております。
私もSO-101等を組み立てたりしたことはあるのですが、まだまだ浅学でロボット周りはキャッチアップをしなくてはと思っております。

そんな中で2か月ほど前にLeRobotの更新で自作ロボットをLeRobotのワークフローで学習可能なフレームワークが登場したことを知りました。
キャッチアップも含め、本記事ではこのフレームワークを使用してノットフィジカルなAIを実装していきます。

余談ですが、この間SO-101を動かしていたらディスプレイを強打して大変だったので、反省も含めノットフィジカルな実装をしていきます。

先にまとめ

  • プラグイン拡張では、任意のロボット/テレオペレーター/カメラをLeRobotのワークフローに統合できるようになりました。
  • キーボードの入力をCLIで受け取るテレオペレーションをLeRobotのワークフローで実装します。
    • 今回は実機と接続しませんが、プラグイン拡張によってロボットアームだけではなく平面移動ロボット含めた様々なロボットも学習できることがわかるかと思います。
    • 実装はこちらのリポジトリにまとめています。
  • condaではなくuvを使用します。
    • uvに関しては公式のドキュメントに記述が少ないよう感じたので、微力ながらuv利用者の参考となれば幸いです。

動作イメージ

プラグイン拡張とは

  • LeRobotが10月中旬に発表した新しい仕組みです。
  • これまでは公式が提供する範囲のTeleoperator(操作側)およびRobot(操作される側)のデバイスのみがサポートされていましたが、プラグイン拡張によりユーザーが任意のデバイス(Teleoperator, Robot, Camera)をLeRobotに統合できるようになります。
  • Pythonパッケージとしてプラグインを公開することで、様々なユーザーが新しいデバイスをLeRobotのワークフローで利用可能になります。
  • いくつかのプラグイン例(xArm)もGitHubで公開されています。
  • 私の理解では、今後様々なプラグインが登場することで自作ハードを含めた様々なデバイスのフィジカルAI活用に対するハードルが下がっていくのだと解釈しています。

詳細は公式のドキュメントを参考にしてください。

実装について

というわけでプラグイン拡張がなんとなく分かったところで実装を見て理解を深めましょう!

今回はロボットがなくても動作するように下記を作っていきます。

  • キーボード入力を受け取るTeleoperator
  • CLIで現在位置を表示するRobot

上記は平面移動のロボットをキーボードで操作するテレオペレーションのイメージ実装です。
今回の記事では行いませんが、Robotに実機と接続して操作する実装を加えれば、LeRobotのワークフローで平面移動ロボットの学習ができるようになる想定です。

基本的には公式のドキュメントをベースに実装していきます。
公式のままだとSO-101用の実装なので適宜修正しながら進めていきます。

今回実装するソースコードはこちらに配置しているので、適宜参考にしてください。
下記の環境で動作確認していますが、おそらく他の環境でも動作すると思います。

  • M1 Mac
  • Python 3.11
  • uv 0.9.5
  • lerobot 0.4.2

事前準備

これからの説明では宗教上の理由でcondaではなくuvを使用しています。
特別なこだわりがなければuvをインストールしましょう。
こちらの手順に従ってインストールしてください。

全体

実装前にTeleoperator/Robotのクラス実装について簡単に見ておきましょう。

Teleoperator/Robotのクラスで実装する関数/プロパティは下記です。

クラス 関数 / プロパティ 説明
共通 __init__ 初期化
共通 connect ロボットとの接続を行います。接続時にキャリブレーションを行うかどうかも指定可能です。例として、操作するロボットとUSBの接続処理が必要な場合こちらに記述します。
共通 is_connected ロボットが接続しているかどうかを取得するためのプロパティです。
共通 disconnect 接続しているロボットとの接続を切断します。
共通 calibrate ロボットのキャリブレーションを行います。
共通 is_calibrated ロボットのキャリブレーションが行われているかどうかを取得するためのプロパティです。
共通 configure ロボットの初期設定を行います。
Robot observation_features ロボットの観測情報の型を定義するためのプロパティです。
Robot action_features ロボットのアクションの型を定義するためのプロパティです。
Robot get_observation ロボットの観測情報を取得します。例として、ロボットのセンサー情報を取得します。
Robot send_action ロボットにアクションを送信します。例として、ロボットのモーターを回転させるアクションを行います。
Teleoperator action_features テレオペレーターのアクションの型を定義するためのプロパティです。
Teleoperator feedback_features テレオペレーターのフィードバックの型を定義するためのプロパティです。
Teleoperator get_action テレオペレーターのアクションを取得します。例として、キーボードの入力を受信します。
Teleoperator send_feedback テレオペレーターにフィードバックを送信します。現在は使用されていないようです。

パッケージ配置

皆様の興味があるのは多分実装だと思うので、パッケージ配置を先に行いましょう。
開発を行いながら動作確認を行ったりするため、Robot/Teleoperatorパッケージを配下のフォルダに作成します。
こちらのリポジトリを参考に、下記のようなフォルダ構成になるようファイルを作成します。

├── lerobot_robots: robotパッケージを作成するフォルダ
│   └── test
│       ├── lerobot_robot_test
│       │   ├── __init__.py
│       │   ├── config_test.py
│       │   └── test.py
│       └── pyproject.toml
├── lerobot_teleoperators: Teleoperatorパッケージを作成するフォルダ
│   └── test
│       ├── lerobot_teleoperator_test
│       │   ├── __init__.py
│       │   ├── config_test.py
│       │   └── test.py
│       └── pyproject.toml
└── pyproject.toml
  • pyproject.toml
  • lerobot_robots/test/pyproject.toml
  • lerobot_teleoperators/test/pyproject.toml
  • lerobot_robots/test/lerobot_robot_test/init.py
  • lerobot_teleoperators/test/lerobot_teleoperator_test/init.py

要点は下記です。

  • LeRobotでは特定のプレフィックスを持つパッケージを検出するので、命名規則を守ること
    • ロボット用パッケージ: lerobot_robot_*
    • テレオペレーター用パッケージ: lerobot_teleoperator_*
    • 余談ですが、上記の仕様のため、パッケージがeditableになっていると検出されないことがあるので注意してください。
  • LeRobotから簡単にアクセスできるように、__init__.pyでクラスを公開すること

その他、下記にも注意する必要がありますが、上記配置と今回の実装手順に従えばあまり気にする必要はありません。
次回以降の参考としていただけると幸いです。

  • デバイスの実装クラスはConfigのサフィックスを除いた名前とすること
    • Teleoperator/RobotのConfigのクラスをSomethingConfigクラスとしたなら、実装クラスはSomethingクラスとすること
  • LeRobotが自動的に検索する場所に実装クラスを配置すること
    • Configのクラスと同じフォルダ
    • デバイス名が付けられたサブモジュール内

ロボットの実装

それではRobotの実装を行っていきましょう。

Configクラスの作成

まずはRobotのConfigクラスを作成しましょう。
lerobot_robots/test/lerobot_robot_test/config_test.pyを記述します。
こちらのソースコードを参考にしてください。

from dataclasses import dataclass

from lerobot.robots import RobotConfig

@RobotConfig.register_subclass("test")  # コマンドのrobot.typeで指定する値
@dataclass
class TestRobotConfig(RobotConfig):
    robot_id: str = "test_robot_1"  # コマンドラインで指定可能な値。デフォルトを指定しているため、何も指定しないと"test_robot_1"を使用します。

Configクラスではコマンドライン使用時のロボットのtypeとパラメータを定義します。
例として、上記の定義をした場合にはテレオペレーションを行う際に下記のような指定が可能です。
この指定により、TestRobotConfigのロボットを参照してrobot_idtest_robot_1_1を使用することができます。

uv run lerobot-teleoperate
  --robot.type test
  --robot.robot_id test_robot_1_1
  ... # 他のパラメータも指定可能

Robotクラスの初期化 & 型定義

次にRobotクラスを作成しましょう。
lerobot_robots/test/lerobot_robot_test/test.pyを記述します。
こちらのソースコードを参考にしてください。

from typing import Any
from lerobot.robots import Robot
from .config_test import TestRobotConfig

class TestRobot(Robot):
    config_class = TestRobotConfig # Configクラスを指定します。
    name = "test_robot" # ロボットの名前を指定します。

    # 初期化を行います。
    def __init__(self, config: TestRobotConfig):
        super().__init__(config)
        self.id = config.robot_id
        self.x = 0
        self.y = 0

    # 観測情報の型を定義します。
    @property
    def observation_features(self) -> dict:
        return {
            "sensor1.pos": float,
            "sensor2.pos": float,
        }

    # アクションの型を定義します。
    @property
    def action_features(self) -> dict:
        return {
            "key.left": bool,
            "key.right": bool,
            "key.up": bool,
            "key.down": bool,
        }

RobotクラスはLeRobotのRobotクラスを継承して実装します。
また、config_classを指定してConfigクラスを指定します。
nameはロボットの名前を指定するのですが、自分が見た感じキャリブレーションファイルの保存先のパス名等に使用しているようです。

それぞれの関数を少し見ていきましょう。

__init__

こちらでは初期化を行います。
初期化時にConfigクラスを受け取るので、 ロボットに必要な情報があればConfigクラスのパラメータで設定できるようにしましょう。 今回のRobotクラスでは位置情報を保持するため、Configクラスのパラメータ以外にxyを初期化時に設定しています。
Robotクラスでは触れませんが、 super().__init__でキャリブレーションのロードも行っているので、キャリブレーションした結果は自然と設定されています。

observation_features

こちらではロボットの観測情報の型を定義します。
実際の動作時には、ロボットのセンサーで受信した値等をここで定義した型に合わせて返却します。

action_features

こちらではロボットのアクションの型を定義します。
実際の動作時には、Teleoperatorはここで定義した型に合わせてロボットのアクションを送信します。

接続処理

次に接続処理を行いましょう。

    # ロボットの接続処理を行います。
    def connect(self, calibrate: bool = True) -> None:
        pass

    # ロボットの切断処理を行います。
    def disconnect(self) -> None:
        pass

    # ロボットの接続状態を取得してboolを返却します。
    @property
    def is_connected(self) -> bool:
        return True

今回のRobotクラスではロボット等への接続を行いません。
そのため、こちらの関数では特に何も行いません。
実際の動作時には、ロボットとの接続を行うための処理をここに記述します。

下記は公式のSO-101の接続処理です。 こちらでは、モーターの接続とキャリブレーション、カメラの接続、設定を行っています。

    def connect(self, calibrate: bool = True) -> None:
        """
        We assume that at connection time, arm is in a rest position,
        and torque can be safely disabled to run calibration.
        """
        if self.is_connected:
            raise DeviceAlreadyConnectedError(f"{self} already connected")

        self.bus.connect()
        if not self.is_calibrated and calibrate:
            logger.info(
                "Mismatch between calibration values in the motor and the calibration file or no calibration file found"
            )
            self.calibrate()

        for cam in self.cameras.values():
            cam.connect()

        self.configure()
        logger.info(f"{self} connected.")

キャリブレーション & 設定

次にキャリブレーション処理と設定処理を行いましょう。

    # ロボットのキャリブレーションを行います。
    def calibrate(self) -> None:
        pass

    # ロボットのキャリブレーション状態を取得してboolを返却します。
    @property
    def is_calibrated(self) -> bool:
        return True

    # ロボットの設定を行います。
    def configure(self) -> None:
        pass

今回のRobotクラスでは特にキャリブレーションや設定を行いません。
そのため、こちらの関数では特に何も行いません。

下記は公式のSO-101の設定処理です。 こちらでは、モーターのトルクを無効化した上で、モーターのPID制御を行っています。
※ 接続のたびに書き込みが発生することについてはこちらを参照してください。

    def configure(self) -> None:
        with self.bus.torque_disabled():
            self.bus.configure_motors()
            for motor in self.bus.motors:
                self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value)
                # Set P_Coefficient to lower value to avoid shakiness (Default is 32)
                self.bus.write("P_Coefficient", motor, 16)
                # Set I_Coefficient and D_Coefficient to default value 0 and 32
                self.bus.write("I_Coefficient", motor, 0)
                self.bus.write("D_Coefficient", motor, 32)

アクションの送信

次にアクションの送信を行いましょう。

    # アクションを送信します。
    def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
        left = action.get("key.left", False)
        right = action.get("key.right", False)
        up = action.get("key.up", False)
        down = action.get("key.down", False)

        # ロボットの位置を更新
        if right:
            self.x += 1
        if left:
            self.x -= 1
        if up:
            self.y += 1
        if down:
            self.y -= 1

        # ロボットの位置を表示
        print(f"TestRobot: x={self.x}, y={self.y} : ")

        return action

今回のRobotクラスでは実際にロボットにアクションを送信するわけではありませんが、 Teleoperatorから受信したアクションによってX軸方向とY軸方向に移動したこととしましょう。
受信したアクションによって、クラス変数のxyを更新します。
最後に更新した位置を表示します。

実際の動作時には、受信したアクションによってロボットを動かす処理をここに記述します。
例としてSO-101では受信したアクション(関節角度)に基づいてモーターを制御しています。

また、こちらの関数ではアクションを返却しますが、データセット作成時には返却した値を使用するのではなく引数の方の値を使用するようです。
データセット作成を見据えている方は留意いただければ幸いです。

観測情報の取得

次に観測情報の取得を行いましょう。

    # 観測情報を取得します。
    def get_observation(self) -> dict:
        return {
            "sensor1.pos": self.x,
            "sensor2.pos": self.y,
        }

今回のRobotクラスではTeleoperatorからのアクションによって移動したことにするため、こちらの関数では位置情報を返却します。
実際の動作時には、センサーで受信した値等をここで定義した型に合わせて返却します。

ここまででRobotクラスの実装は終了です。
お疲れ様でした。

Teleoperatorクラスの実装

次にTeleoperatorクラスの実装を行っていきましょう。

基本はRobotクラスの実装と同じです。 こちらこちらのソースコードを適宜参考にしてください。

Configクラスの作成

まずは先ほどと同様にConfigクラスを作成します。

import random
import string
from dataclasses import dataclass, field

from lerobot.teleoperators.config import TeleoperatorConfig


@TeleoperatorConfig.register_subclass("test") # コマンドのteleop.typeで指定する値
@dataclass
class TestTeleoperatorConfig(TeleoperatorConfig):
    teleoperator_id: str # コマンドラインで指定可能な値。デフォルトを指定していないため、何も指定しないとエラーになります。

    teleoperator_name: str = field(
        default_factory=lambda: "".join(random.choices(string.ascii_letters + string.digits, k=8))
    )  # コマンドラインで指定可能な値。デフォルトを指定しているため、何も指定しないとランダムな名前を使用します。

基本的にRobotと同じなので少し変更してみましょう。

teleoperator_idはデフォルト値を設定しないこととします。
こちらによって、コマンドラインで何も指定しないとエラーになります。
引数から必須の情報を受け取る必要がある場合は、必須パラメータとして定義しましょう。

teleoperator_nameは先ほどと同じくデフォルト値を設定しているため、指定しないでも大丈夫です。
しかし今回はdefault_factoryを使ってランダムな名前を生成するようにしています。
固定値でない値を生成する場合は、こちらのように設定しましょう。

Teleoperatorクラスの初期化 & 型定義

次にTeleoperatorクラスを作成しましょう。
lerobot_teleoperators/test/lerobot_teleoperator_test/test.pyを記述します。
こちらのソースコードを参考にしてください。

from typing import Any

import pygame
from lerobot.teleoperators.teleoperator import Teleoperator

from .config_test import TestTeleoperatorConfig


class TestTeleoperator(Teleoperator):
    config_class = TestTeleoperatorConfig
    name = "test_teleoperator"


    def __init__(self, config: TestTeleoperatorConfig):
        super().__init__(config)
        self.id = config.teleoperator_id
        self.name = config.teleoperator_name

        # pygame関連の初期化
        self.screen = None
        self.font = None
        self.clock = None
        self._is_connected = False

        # キー状態の管理
        self.key_states = {
            "left": False,
            "right": False,
            "up": False,
            "down": False,
        }

        # ウインドウ設定
        self.window_width = 400
        self.window_height = 300
        self.bg_color = (30, 30, 30)
        self.text_color = (255, 255, 255)
        self.active_color = (100, 200, 100)

    # テレオペレーターのアクションの型を定義
    @property
    def action_features(self) -> dict:
        return {
            "key.left": bool,
            "key.right": bool,
            "key.up": bool,
            "key.down": bool,
        }

    # テレオペレーターのフィードバックの型を定義
    @property
    def feedback_features(self) -> dict:
        return {}

こちらも基本はRobotと同じなので少し変更してみましょう。
Teleoperatorクラスではキーボードの入力を受信するためにpygameを使用します。
※ キーボード取得のライブラリは他にもありますが、OS間の互換性やOSのアクセシビリティの許可が必要になったりするので今回はpygameを使用します。
__init__ではRobotで行った初期化以外にpygameのための変数を定義しています。

action_featuresはキー状態を取得するための型を定義しています。
feedback_featuresは使用しないため、空の辞書を返却しています。

接続処理

次に接続処理を行いましょう。

    # テレオペレーターの接続処理を行います。
    def connect(self, calibrate: bool = True) -> None:
        pygame.init()
        self.screen = pygame.display.set_mode((self.window_width, self.window_height))
        pygame.display.set_caption("Test Teleoperator - Arrow Keys")
        self.font = pygame.font.Font(None, 36)
        self.clock = pygame.time.Clock()
        self._is_connected = True

    # テレオペレーターの切断処理を行います。
    def disconnect(self) -> None:
        if self._is_connected:
            pygame.quit()
            self._is_connected = False
            self.screen = None
            self.font = None
            self.clock = None

    # テレオペレーターの接続状態を取得してboolを返却します。
    @property
    def is_connected(self) -> bool:
        return self._is_connected

今回はpygameを使用してキーボードの入力を受信するため、connectではpygameの初期化を行っています。
pygameの初期化が完了したら、接続状態_is_connectedTrueにしています。

disconnectでは、接続状態がTrueの場合にpygameの終了処理を行っています。
pygameの終了処理が完了したら、接続状態をFalseにしています。

is_connectedでは、上記で設定した接続状態を返却しています。

キャリブレーション & 設定

    # テレオペレーターのキャリブレーションを行います。
    def calibrate(self) -> None:
        pass

    # テレオペレーターのキャリブレーション状態を取得してboolを返却します。
    @property
    def is_calibrated(self) -> bool:
        return True

    # テレオペレーターの設定を行います。
    def configure(self) -> None:
        pass

今回もキャリブレーション、設定を行いません。
そのため、説明をスキップします。

アクションの取得

次にアクションの取得を行いましょう。

    # テレオペレーターのアクションを取得します。
    def get_action(self) -> dict[str, Any]:
        if not self.is_connected:
            raise ConnectionError(f"{self} is not connected.")

        # pygameイベントを処理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self._is_connected = False
                return {
                    "key.left": False,
                    "key.right": False,
                    "key.up": False,
                    "key.down": False,
                }

        # 現在のキー状態を取得
        keys = pygame.key.get_pressed()
        self.key_states["left"] = keys[pygame.K_LEFT]
        self.key_states["right"] = keys[pygame.K_RIGHT]
        self.key_states["up"] = keys[pygame.K_UP]
        self.key_states["down"] = keys[pygame.K_DOWN]

        # 画面を描画
        self._render_display()

        return {
            "key.left": self.key_states["left"],
            "key.right": self.key_states["right"],
            "key.up": self.key_states["up"],
            "key.down": self.key_states["down"],
        }

    def _render_display(self) -> None:
        """画面に現在のキー状態を表示"""
        self.screen.fill(self.bg_color)

        # タイトル
        title = self.font.render("Arrow Keys", True, self.text_color)
        title_x = self.window_width // 2 - title.get_width() // 2
        self.screen.blit(title, (title_x, 20))

        # 画面を4分割した各領域の中心
        left_center_x = self.window_width // 4
        right_center_x = self.window_width * 3 // 4
        top_center_y = self.window_height // 4
        bottom_center_y = self.window_height * 3 // 4

        # 上キー(上半分の中心)
        up_color = self.active_color if self.key_states["up"] else self.text_color
        up_text = self.font.render("UP", True, up_color)
        up_x = self.window_width // 2 - up_text.get_width() // 2
        up_y = top_center_y - up_text.get_height() // 2
        self.screen.blit(up_text, (up_x, up_y))

        # 下キー(下半分の中心)
        down_color = self.active_color if self.key_states["down"] else self.text_color
        down_text = self.font.render("DOWN", True, down_color)
        down_x = self.window_width // 2 - down_text.get_width() // 2
        down_y = bottom_center_y - down_text.get_height() // 2
        self.screen.blit(down_text, (down_x, down_y))

        # 左キー(左半分の中心)
        left_color = self.active_color if self.key_states["left"] else self.text_color
        left_text = self.font.render("LEFT", True, left_color)
        left_x = left_center_x - left_text.get_width() // 2
        left_y = self.window_height // 2 - left_text.get_height() // 2
        self.screen.blit(left_text, (left_x, left_y))

        # 右キー(右半分の中心)
        right_color = self.active_color if self.key_states["right"] else self.text_color
        right_text = self.font.render("RIGHT", True, right_color)
        right_x = right_center_x - right_text.get_width() // 2
        right_y = self.window_height // 2 - right_text.get_height() // 2
        self.screen.blit(right_text, (right_x, right_y))

        pygame.display.flip()
        self.clock.tick(60)  # 60 FPS

アクションの取得はキーボードの入力を受信するための処理を行っています。
少し処理が煩雑になってしまったので、関数を分けて解説します。

get_action

こちらでは大まかに下記処理を行っています。

  1. 接続状態を確認し、接続していない場合はエラーを返却
  2. pygameのイベントを処理し、ウインドウを閉じるイベントが発生した場合は接続状態をFalseにして、キー状態をFalseにして返却
  3. 現在のキー状態を取得し、key_statesに保存
  4. 画面にキー状態を表示
  5. 取得したキー状態を返却

プラグイン拡張ではこちらの関数で返却したキー状態をRobotクラスのsend_actionに送信します。
そのため、LeRobotのコマンドを使用する場合にはTeleoperatorクラスで定義するアクションの型定義とRobotクラスで定義するアクションの型定義が一致している必要があります。
自分でPythonスクリプトを作成する場合には、processorで変換処理を行ってTeleoperatorクラスで取得したアクションをRobotクラスで使用することが可能です。

_render_display

こちらでは画面にキー状態を表示するための処理を行っています。
画面描画を行っているだけで今回の実装にはそこまで関わらないため説明をスキップします。

フィードバックの送信

次にフィードバックの送信を行いましょう。

    # テレオペレーターのフィードバックを送信します。
    def send_feedback(self, feedback: dict[str, Any]) -> None:
        pass

フィードバックの送信はサポートしていないため実装しません。
本来的にはRobotクラスからTeleoperatorクラスにフィードバックを送信することができるようになっています。

ここまででTeleoperatorクラスの実装は終了です。
お疲れ様でした!

動作確認

では実際に動作確認を行ってみましょう。
まずはパッケージをインストールしましょう。

uv sync

既にuv syncを行っている場合は下記を実行してパッケージを更新しましょう。

uv sync --reinstall-package lerobot-robot-test --reinstall-package lerobot-teleoperator-test

ここまでで動作確認の準備ができたのでテレオペレーションを行いましょう。

uv run lerobot-teleoperate
--robot.type test
--teleop.type test
--teleop.teleoperator_id test_teleoperator_1
--robot.robot_id test_robot_1

※ 最初の起動には少し時間がかかるのでお待ちください。

少し待つと下記のようなウインドウを表示します。

Teleoperator画面

ウインドウをクリックしてからキーボードの矢印キーを押下しましょう。
矢印キーに合わせてCLIのロボットの位置が更新していきます。

動作イメージ

ここまでで動作確認は終了です。
お疲れ様でした!

まとめ

今回は簡単な実装でしたが、プラグイン拡張を使うことでLeRobotのコマンドに自分のソースコードを組み込むことができました。
LeRobotのワークフローをそのまま使用して実装できるのが嬉しいところです。 次の段階としては、こちらを利用してデータセットを作成していくのが面白いかもしれません。

また、ここまで読まれた方はプラグイン拡張のTeleoperator/Robotの実装方法が完全に理解していただいたかと思います。
皆様の自作ロボットでの活用はもちろんのこと、様々な実装で本記事が少しでも役立つことを祈ります。
もしも何か実装されましたら、きっと見つけに行くので、どこか(Twitter(新: X), 等)でアウトプットしていただけると嬉しいです!

We Are Hiring!

ABEJAは、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!) careers.abejainc.com

特に下記ポジションの募集を強化しています!ぜひ御覧ください!

トランスフォーメーション領域:データサイエンティスト

トランスフォーメーション領域:データサイエンティスト(ミドル)

トランスフォーメーション領域:データサイエンティスト(シニア)