ABEJA Tech Blog

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

ROS2を使ってシミュレーション環境にてロボットアームをVLAで動かす (on Mac)

こちらはABEJA アドベントカレンダー 2025の8日目の記事です!

ABEJAでデータサイエンティストをしている岩城です。 最近はロボティクス関連のキャッチアップを行っています。

ロボットをやるにあたってROS2は避けては通れぬということで、最近ROS2初心者に入門しました。 今回は、ROS2を使ってロボットをVLAで動かす方法について、自身のキャッチアップも兼ねて書いていこうと思います。

シミュレーション環境で実施したのですが、筆者が手軽に動かせるPCはMacのみだったため、さまざまな制約と戦いながらの検証になりました。

この記事は、気軽にMacを使ってシミュレーション環境上でROS2 + VLAを試してみたいという方向けです。

ROS2とは

ROSはRobot Operating Systemの略で、その名の通りロボットを操作するためのオープンソースツールです。 ROSの次世代版がROS2になります。

ROS1の公式サポートは2025年5月に終了したようで、現在はROS2がロボット開発のメインストリームとなっています。

ROS/ROS2に関しては、以下の記事がわかりやすいです。

tech-blog.abeja.asia

ROS2のノードやPublish/Subscribeについては、以下の記事が非常にわかりやすいです。

www.valinux.co.jp

VLAとは

VLAはVision Language Actionの略で、従来のVLM (Vision Language Model) に Action機構を追加したモデルです。 VLMで言語と画像や動画などの視覚情報をマルチモーダルに処理し、Action機構でその状況に応じた行動を出力することができます。 これにより、自然言語による指示と視覚情報からロボットが行動することができるようになります。

現在はOpenVLAやPhysical Intelligenceの  \pi シリーズ、NVIDIAのIsaac GR00T N1といった基盤モデルが登場しています。

今回は、その中でも比較的軽量なHuggingFaceのSmolVLAを使いたいと思います。

VLAに関しては以下の記事がわかりやすいです。

tech-blog.abeja.asia

シミュレーション環境

MacではコンテナからGPUを使うことができないため、コンテナ技術は使わずMacOS上にpixiというパッケージマネージャで仮想環境を作ります。 ROS環境をコンテナ上で作成し、VLAはMacOS上でserveして、ポートを繋いでやり取りすることも可能ですが、今回は簡単のためこの方法を取ります。

pixiはcondaのコア開発者らが開発した比較的モダンなツールです。condaとpypiの両方からパッケージをイストールすることができます。robostack-stagingチャネルからROS2パッケージを、pypiからMujocoをインストールして使うなどすることができます。

ROSでよく使われるシミュレーション環境としては、まずRviz2が挙げられます。物理演算を必要とする場合はGazeboが使われることが多いと思います。今回も当初はGazeboを想定していたのですが、MacOSに非対応だったため (Gazebo classicは対応していますが) 、Mujocoを使うこととしました。MujocoはROS2と相性がいいわけではないので、ここは苦労した点でした。

ハードとソフトの要件は以下です。

PCMacBook Pro 2023
OSmacOS 15.6.1
チップApple M3
メモリ16GB
pixi0.59.0

実装

コードは以下に置いてあります。

github.com

ディレクトリ構成

.
├── pixi.lock
├── pixi.toml
├── README.md
└── ros2_ws
    └── src
        ├── lerobot_mujoco/
        └── lerobot_vla/

パッケージの情報は pixi.toml に記述されます。 中身は以下のようになっています。

[workspace]
authors = ["FumitakaIwaki <iwaki2323a@gmail.com>"]
channels = ["robostack-staging", "conda-forge"]
name = "ros2-so101"
platforms = ["osx-arm64"]
version = "0.1.0"

[activation]
scripts = ["ros2_ws/install/setup.sh"]

[tasks]
build = { cmd = "colcon build --symlink-install", cwd = "ros2_ws" }
launch-mujoco = { cmd = "pixi run ros2 launch lerobot_mujoco so101_mujoco.launch.py", cwd = "ros2_ws" }
launch-vla = { cmd = "pixi run ros2 launch lerobot_vla smolvla.launch.py", cwd = "ros2_ws" }

[dependencies]
ros-humble-desktop = ">=0.10.0,<0.11"
ros-humble-joint-state-publisher-gui = ">=2.4.0,<3"
ros-humble-controller-manager = ">=2.51.0,<3"
ros-humble-ros2-controllers = ">=2.48.0,<3"
ros-humble-joint-state-broadcaster = ">=2.48.0,<3"
ros-humble-cv-bridge = ">=3.2.1,<4"
ros-humble-rqt-image-view = ">=1.2.0,<2"
colcon-common-extensions = ">=0.3.0,<0.4"
ffmpeg = ">=7.1.1,<8"

[pypi-dependencies]
mujoco = ">=3.3.7, <4"
lerobot = { path = "./lerobot", editable = true, extras = ["smolvla"]}

ROS2はコマンドが長くなりがちですが、pixiではよく使うコマンドをtaskとして登録することができます。

[dependencies] はcondaチャネルからインストールされたパッケージで、 [pypi-dependencies] はpypiからインストールされたパッケージです。pixiはcondaの依存関係を優先して解決し、その後にpypi関連を解決します。

[activation] を設定しておけば、毎回pixiコマンド実行時にオーバーレイを自動で有効化してくれます。

ROS2では、 ros_ws というようなワークスペースで開発することが主流になっています。 ros2_ws/src 配下で以下の2種類のノードを実装しています。

  • lerobot_mujoco : Mujocoを起動するノード
  • lerobot_vla : SmolVLAを起動するノード

それぞれPythonパッケージとして実装し、それをROS2で使うという流れになります。

Mujocoを起動するノード

ざっくりとした説明は以下です。

.
├── config/ # 設定
├── launch
│   └── so101_mujoco.launch.py # Mujocoノードを起動するコード
├── lerobot_mujoco
│   ├── mujoco_sim_launcher.py # Mujocoを起動するコード
│   └── mujoco_sim.py # Mujocoノード
├── meshes/ # Mujoco用のロボット定義
├── package.xml # ROS2パッケージ管理
├── resource/
├── setup.cfg
├── setup.py
├── test/
└── urdf/ # ロボットの定義

Mujocoノードを起動するコードとMujocoを起動するコードが分かれていますね。 理由としては、MacOSでMujocoを起動するためには mjpyhon コマンドを使う必要があったためです。

Mujocoノードは、 pixi run ros2 launch lerobot_mujoco so101_mujoco.launch.py という ros2 コマンドで実行する必要があるのですが、これでは python コマンドで Mujocoノードであるmujoco_sim.py が実行されてしまいます。 mujoco_sim.pymjpython で 実行する mujoco_sim_launcher.py を作成し、 so101_mujoco.launch.py は このファイルを実行するようにして ros2 launch コマンドからMujocoを起動できるようにしています。ROS2とMujocoの相性の悪さが出ていますね。

つまり、この実装はMujocoを無理やりROS2でラップしているということになります。

Mujocoノードは以下のPublisher/Subscriberを備えています。

Publisher

  • /joint_states : SO-101の関節情報
  • /top_camera : SO-101を上から写すカメラの画像
  • /side_camera : SO-101を横から写すカメラの画像
  • /wrist_camera : SO-101のグリッパーに追従して横から写すカメラの画像
  • /top_camera/camera_info : top_cameraの情報
  • /side_camera/camera_info : side_cameraの情報
  • /wrist_camera/camera_info : wrist_cameraの情報

Subscriber

  • /arm_cotroller/joint_trajectory : SO-101の関節の軌跡
  • /gripper_controller/joint_trajectory : SO-101のグリッパーの軌跡

Publisherで情報を配信し、Subscriberで購読した情報でMujoco上のSO-101を動かします。

SmolVLAを起動するノード

.
├── launch
│   └── smolvla.launch.py # SmolVLAノードを起動するコード
├── lerobot_vla
│   └── smolvla_node.py # SmolVLAノード
├── package.xml
├── resource/
├── setup.cfg
├── setup.py
└── test/

MujocoノードのPublisherによって配信されたtopicから情報を取得し、SmolVLAを用いて行動を生成します。

SmolVLAノードは以下のPublisher/Subscriberを備えています。

Publisher

  • /arm_cotroller/joint_trajectory
  • /gripper_controller/joint_trajectory

Subscriber

  • /joint_state
  • /top_camera
  • /side_camera
  • wrist_camera

まずノードの起動時にSmolVLAを読み込みます。初回はモデルのダウンロードが入ります。

起動後は、Subscriberから得られた画像と関節情報をlerobotのデータ形式に変換し、タスク指示と共にSmolVLAに入力して出力を得ます。 この出力には各関節をどう動かすかの値が入っています。 得られた出力をMujocoで使える形式に変換しPublishします。

このノードは lerobot の機能をインポートして実装しています。

lerobot/examples/tutorial/smolvla/using_smolvla_example.py at main · huggingface/lerobot · GitHub

上記LeRobotリポジトリの using_smolvla_example.py を参考に、ここから必要な機能を探していき、LeRobotのソースコードとにらめっこしながら実装しました。

動作検証

上で共有したリポジトリのREADME.mdに従っていただければ再現できるかと思います。

手順

  1. pixiのインストール

     brew install pixi
    
  2. リポジトリのクローン

     git clone https://github.com/FumitakaIwaki/ros2-so101-mujoco.git
    
  3. lerobotをクローン

     git clone https://github.com/huggingface/lerobot.git
    
  4. lerobotの pyproject.toml から rerun-sdk を削除

     ...
     "draccus==0.10.0", # TODO: Remove ==
     "gymnasium>=1.1.1,<2.0.0",
     # "rerun-sdk>=0.24.0,<0.27.0", <-- ここをコメントアウト
     ...
    
  5. pixi install

     pixi install
    
  6. ビルド

     pixi run build 
    
  7. MUJOCOノードの起動

     pixi run launch-mujoco
    
  8. 別のターミナルを起動しSmolVLAノードを起動

     pixi run launch-vla
    

手順4で rerun-sdk をコメントアウトしているのは、rerun-sdknumpy 2.x.x を必要とするのですが、 ros2パッケージの一部が numpy 1.x.x に依存しており、コンフリクトを起こしてしまうためです。

検証画面

実際に動いている様子は以下です。

アーム以外に何もシミュレーション環境においていないので、タスク指示は “move to the middle position” としてみました。 あまり理解してはくれませんでした。

動作している様子

SmolVLAに入力されている画像情報は以下です。 画像はrqtを使って表示することができます。

pixi run rqt

カメラ情報

細かい画角の設定などは Mujocoノードを実装している mujoco_sim.py 内で行っています。

この画角は、筆者がオフィスでSO-101の模倣学習を実施した時の設定に準拠しています。

最後に

今回は、MacBookのみでROS2 + Mujoco + VLAをやってみました!
色々な制約と闘った変遷をここでまとめておきます。より良い方法がある場合はぜひご教示いただきたいです!

  • 手軽にMacBookで、ROS2で実機を動かすことを想定したシミュレーション環境を作りたい
    • Ubuntuコンテナ + ROS2 + Gazebo
  • VLAでロボットを動かしたい
    • MacはコンテナからGPUを使うことができない
    • Ubuntuコンテナ MacOS + pixi + ROS2 + Gazebo
  • MacだとGazebo動かない
    • MacOS + pixi + ROS2 + Gazebo Mujoco
  • Mujocoを起動するノードをROS2からlaunchできない
    • Mujocoノードを mjpython コマンドで実行するスクリプトを作成し、それをROS2でlaunch

紆余曲折しながら実現したのですが、やり切った後に果たしてこの営みには意味があったのか、という気持ちになりました。

ROS2のキャッチアップはある程度できたのでそこは良かったです。次は、ここで得たROS2の知見をもとに Isaac ROS を使ってみたいと思います。

余談ですが、VLAノードの実装中にランダムなアクションを生成するモックを作ったのですが、それによる挙動と今回のSmolVLAによる挙動が酷似していました。 うまく推論できていない時はランダムウォークなんだなぁと思いました。

     
p.s. 実機でもROS2経由でSO-101とカメラを接続し、VLAで動かすことができました (※本記事で紹介したリポジトリでは動きません)

We are Hiring!

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

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

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

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

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