IFCファイルをSDFファイルに変換してシミュレーション環境（Gazebo）に読み込んで、その中でマニピュレーター付きロボットを動かしてみた

# 1. はじめに
前回の記事[[1]](#参考記事やサイト)ではROS2パッケージを自作し、マニピュレーターを搭載したロボットによる塗装面をrviz2上で疑似的に表現することに成功しました。そしてその記事の中で「Kimらの論文[[2]](#参考記事やサイト)を参考に、BIM(Building Information Modeling)データをROS2のシミュレーションに取り込めるようにするツールの再現にも取り組みたいと考えています。」と述べましたので、それに取り組んでみたのが、この記事の内容になります。

なお、この記事のシミュレーション環境は前回の記事[[1]](#参考記事やサイト)で構築した内容となりますの。このことを前提に記事を記載していきます（つまり細かいことは省きます）。

# 2. 実行環境
IFCからGazeboで読み込めるSDFファイルに変換したPCの環境：
- CPU: CORE i5 8th Gen
- メモリ: 8GB
- OS: Windows 11 22H2

シミュレーション環境：
- CPU: CORE i7 7th Gen
- メモリ: 32GB
- GPU: GeForce RTX 2070
- OS: Ubuntu22.04(WSL2ではなくPCに直接インストール)
- ROS2: Humble
- Nvidia Driver: 535 
- 対象ロボット: Neobotix社 MPO-700(マニピュレーター搭載）[[1]](#参考記事やサイト)で構築した環境

# 3. IFCファイルからSDFファイルへの変換
## 3.1 リポジトリのクローンと環境構築
BIMデータを取り込むとしましたが、今回の対象とするのは形状を表すデータであるIFCファイルです。そしてIFCファイルからSDFファイルへの変換については、以下のリポジトリをWindows 11 22H2の環境にクローンして用いました。
 
https://github.com/ISBE-TUe/ifc_to_sdf

なお、このリポジトリを利用するには
- IfcConvert
- ifcOpenShell-python
をダウンロードして構築しなくてはいけないのですが、リンクをクリックしてもアクセスできませんでしたので、IfcConvertは以下のリンクの"Pre-built packages"のWindows 64bitのexeファイルをダウンロードして適当なところに保存しました。

https://docs.ifcopenshell.org/ifcconvert/installation.html

またifcOpenShell-pythonは以下のリンクの"PyPl"の項目で紹介されている以下のコマンドでインストールしました。
```:terminal
pip install ifcopenshell
```
https://docs.ifcopenshell.org/ifcopenshell-python/installation.html

## 3.2 IFCからSDFの変換
IFCからSDFの変換にはクローンしてきたifc_to_sdf.pyをWindows PCで実行しますが、ifc_to_sdf.pyの'your path'の部分を変更する必要があります。

```python:ifc_to_sdf.py
# BIM model's path
path_bim = 'your path'
# the path where you want to set the sdf folder
path_sdf = 'your path'
# the path for ifcConverter
path_converter = 'your path'
```
- BIM model's pathのところには変換したいifcファイルの保存先を記載します。

- the path where you want to set the sdf folderのところには、変換したファイルをどこに保存したいかを追記します。

- the path for ifcConverterには前章でダウンロードしてきたIfcConvertのexeファイル（IfcConvert.exe）の保存先を指定しました。

ifc_to_sdf.pyを実行すると、出力先として指定されたファイル内にlaunchファイル、modelsファイル、worldsファイルが保存されます。

## 3.3 変換対象のIFCファイル
この記事ではまずクローンしてきたリポジトリでデフォルトで用意されている"ifcUnit.ifc"をSDFファイルへの変換対象にしたいと思います。
どんな形状なのか[Open IFC Viewer](https://openifcviewer.com/)で確認してみましたところ、以下のような形状でした。

![スクリーンショット 2024-11-17 092821.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581920/82f30c18-252d-7389-32e7-4d1344925c99.png)


# 4. Gazeboでのシミュレーションを行う際の準備
## 4.1 IFCから変換されたファイルの処理
modelsのファイルに保存されている各ファイルはすべてコピーして、ros2_mmo500\src\neo_simulation2\modelsの中にそのままペーストします。

次にworldsファイルに保存されたifcからsdfに変換されたworldファイル（私の環境ではIfcSite_3Xeu9seOf84B0KzOCCiNEE.world）をコピーしてros2_mmo500\src\neo_simulation2\worldsの中にそのままペーストします。

## 4.2 launchファイルの作成
次にros2_mmo500\src\neo_simulation2\launchファイルの中にあるsimulation.launch.pyをコピーして、simulation_ifc.launch.pyとして保存します。そして以下の箇所を修正しました。

- MY_NEO_ENVIRONMENT:先ほどros2_mmo500\src\neo_simulation2\worldsに保存したworldファイルの名前に修正します。
- spawn_entity:ここを変更しなくてもシミュレーションは実行できるのですが、ロボットの位置が良くないので"spawn_entity"の"arguments"の"robot_description"ところを「'-x', "1.0", '-y', "1.0", '-z', "0.0"」としました。デフォルトではx,y,zがいずれも0.0です。

```python:simulation_ifc.launch.py
# Neobotix GmbH
# Author: Pradheep Padmanabhan

import launch
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, ExecuteProcess, OpaqueFunction
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import ThisLaunchFileDir, LaunchConfiguration, Command
from launch_ros.actions import Node
import os
from pathlib import Path
import xacro
from launch.launch_context import LaunchContext

MY_NEO_ROBOT = os.environ.get('MY_ROBOT', "mpo_700")
MY_NEO_ENVIRONMENT = os.environ.get('MAP_NAME', "IfcSite_3Xeu9seOf84B0KzOCCiNEE")

def execution_stage(context: LaunchContext):
    default_world_path = os.path.join(get_package_share_directory('neo_simulation2'), 'worlds', MY_NEO_ENVIRONMENT + '.world')

    use_sim_time = LaunchConfiguration('use_sim_time', default='true')
    robot_dir = LaunchConfiguration(
        'robot_dir',
        default=os.path.join(get_package_share_directory('neo_simulation2'),
            'robots/'+MY_NEO_ROBOT,
            MY_NEO_ROBOT+'.urdf'))

    declare_use_sim_time_cmd = DeclareLaunchArgument(
        'use_sim_time',
        default_value='false',
        description='Use simulation (Gazebo) clock if true')
    use_robot_state_pub = LaunchConfiguration('use_robot_state_pub')

    urdf = os.path.join(
        get_package_share_directory('neo_simulation2'),
        'robots/'+MY_NEO_ROBOT+'/', MY_NEO_ROBOT+'.urdf.xacro')
    
    # ToDo add an launch argument for arm
    arm = "ur10" # elite or "ur10"
    xacro_file = Command([
            "xacro", " ", urdf, " ", arm, ':=',
            "true", " "])

    doc = xacro.parse(xacro_file.perform(context)) 
    xacro.process_doc(doc) 

    spawn_entity = Node(
        package='gazebo_ros',
        executable='spawn_entity.py',
        arguments=['-entity', MY_NEO_ROBOT, '-topic', "robot_description",'-x', "1.0", '-y', "1.0", '-z', "0.0"],
        output='screen')

    start_robot_state_publisher_cmd = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        name='robot_state_publisher',
        output='screen',
        parameters=[{'use_sim_time': use_sim_time, 'robot_description': doc.toxml()}],
    )

    teleop =  Node(package='teleop_twist_keyboard',executable="teleop_twist_keyboard",
    output='screen',
    prefix = 'xterm -e',
    name='teleop')

    gazebo = IncludeLaunchDescription(
            PythonLaunchDescriptionSource(
                os.path.join(get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')
            ),
            launch_arguments={
                'world': default_world_path,
                'verbose': 'true',
            }.items()
        )
    joint_state_broadcaster_spawner = Node(
        package="controller_manager",
        executable="spawner",
        parameters=[{'use_sim_time': use_sim_time}],
        arguments=["joint_state_broadcaster", "--controller-manager", "/controller_manager"],
    )

    initial_joint_controller_spawner_stopped = Node(
        package="controller_manager",
        executable="spawner",
        arguments=["arm_controller", "-c", "/controller_manager"],
    )

    return [spawn_entity,
        start_robot_state_publisher_cmd,
        teleop, 
        gazebo,
        initial_joint_controller_spawner_stopped,
        joint_state_broadcaster_spawner]


def generate_launch_description():
    opq_function = OpaqueFunction(function=execution_stage)
    return LaunchDescription([opq_function])
```

## 4.3 ビルド
以下のコマンドでビルドしました。
```:terminal
cd ~/ros2_mmo500
rosdep update
rosdep install --from-paths src --ignore-src -r -y
source /opt/ros/humble/setup.bash
colcon build
```

# 5. シミュレーション結果
ビルド完了後、以下のコマンドでシミュレーション環境(Gazebo)を起動しました。
```:terminal(1)
source ~/ros2_mmo500/install/setup.bash
ros2 launch neo_simulation2 simulation_ifc.launch.py
```

以下が起動したときの画像です。SDFに変換されたユニットとNeobotix社 MPO-700(マニピュレーター搭載）がシミュレーション空間にあることが確認できます。
![Screenshot from 2024-11-17 08-44-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581920/e0a0306a-cccd-0f29-4116-1300fc0f7849.png)

次にこのユニットの周りを遠隔操作でロボットを動かしたときの動画が以下になります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/2qKOZddfu_g?si=FcI0fsrhNP4MBFfe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 6. まとめ
今回はBIM(Building Information Modeling)データの中でも形状を表すデータであるIFCファイルをROS2のシミュレーションに取り込むことに成功しました。ただ簡単なユニットの変換でしたので、建物などより複雑な形状の変換が可能なのかは不明です。またこの環境でロボット走行用地図を作成して自律走行するということもできていません。今後はこの課題に取り組むとともにKimらの論文[[2]](#参考記事やサイト)を参考に、BIM(Building Information Modeling)データに入っている工程をロボットでの塗装シミュレーションに取り込めるようにすることの再現にも取り組みたいと考えています。

# 参考記事やサイト
[1][マニピュレーター付きロボットで疑似的に壁を塗装してみた](https://qiita.com/bebebefu/items/9ce4f4a57b246e063fc9)
[2][Development of BIM-integrated construction robot task planning and simulation system](https://www.sciencedirect.com/science/article/pii/S0926580521001710)