0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[PyBullet] 6軸ロボットアームを動かす Part4 (干渉物が存在する環境下にて物体把持)

Last updated at Posted at 2025-08-08

はじめに

PyBullet (Python上で動く物理シミュレータ) を使用して,ロボットアームを可視化して,動かしたい.
前記事では,PyBullet (Python上で動く物理シミュレータ) で干渉物が存在する環境において,グリッパー付き3軸ロボットアームの経路生成および物体把持を実装した.
(https://qiita.com/haruhiro1020/items/d26009d50be190d97368)

本記事では,前記事の続きで,6軸ロボットアームにグリッパーを追加して,干渉物が存在する環境下にて,物体を把持する(下図はPyBullet上のグリッパー付き6軸ロボットアームである).赤枠がグリッパー,緑枠が把持したい物体である.把持したい物体は,机の上に置いた.今回は机を干渉物として考えて,机に干渉しないように,物体把持を実装する.
6DoF_Gripper_RRT.png

本記事では,ロボットが自動で把持物体の位置まで干渉しないように移動し,物体を把持するまでを実装する.そのため,把持した物体を他の場所へ移動させることは実装していない.実際に動かしている動画は下図の通りである.
PyBulletと6軸ロボットアーム(RRT)_グリッパ付き_把持_見せるよう.gif

本記事で実装すること

・6軸ロボットアームにグリッパーを追加
・干渉物が存在する環境下での経路生成 + 物体把持

本記事では実装できないこと (将来実装したい内容)

・カメラによる物体の位置把握 + 干渉物が存在する環境下での経路生成 + 物体把持

動作環境

・macOS Sequoia (バージョン15.5)
・Python3 (3.13.3)
・PyBullet (3.2.5) (物理シミュレータ)
・Numpy (2.3.0) (数値計算用ライブラリ)

PyBullet のインストール方法

PyBullet のインストール方法については前記事にて,説明したため,割愛する.
(前記事 https://qiita.com/haruhiro1020/items/ecc5215234350b663770)

6軸ロボットアームの定義

本記事で動かしたい,6軸ロボットアームについて説明する.
本記事では,下図のような6軸ロボットアームを考えている.
$\theta_{1},\theta_{4},\theta_{6}$はZ軸方向, $\theta_{2},\theta_{3},\theta_{5}$ はY軸方向へ回転する.
6DoF_URDF1.drawio.png

上図のパラメータ $l_{1}$ ~ $l_{6}$, $m_{1}$ ~ $m_{6}$ の値は下表として,考える(値は適当である).

パラメータ名 概要
$l_{1}$ リンク$1$の長さ 1.0 [m]
$l_{2}$ リンク$2$の長さ 1.0 [m]
$l_{3}$ リンク$3$の長さ 1.0 [m]
$l_{4}$ リンク$4$の長さ 0.1 [m]
$l_{5}$ リンク$5$の長さ 0.1 [m]
$l_{6}$ リンク$6$の長さ 0.1 [m]
$m_{1}$ リンク$1$の質量 1.0 [kg]
$m_{2}$ リンク$2$の質量 1.0 [kg]
$m_{3}$ リンク$3$の質量 1.0 [kg]
$m_{4}$ リンク$4$の質量 0.1 [kg]
$m_{5}$ リンク$5$の質量 0.1 [kg]
$m_{6}$ リンク$6$の質量 0.1 [kg]

PyBulletで,上図のロボットを使用するために,URDF (Unified Robot Description Format)を作成する必要がある.

グリッパーの定義

本記事では,下図のようなパラレルグリッパーをロボットアームに追加する.グリッパーは3軸ロボットアームに追加したのと大方同じである.座標系および形状を少し変更した.
Gripper_6DoF_Open.drawio.png

上図はグリッパーが開いた時の画像である.下図はグリッパーを閉じた時の画像である.2本の指の間に把持物体を置いた状態で,グリッパーを閉じることで,物体把持を実施する.
Gripper_6DoF_Close.drawio.png

パラレルグリッパーを採用した理由は下記の通りである.
・URDFの作成および制御が容易
・立方体および直方体の場合,接触する面積が大きくなるため,摩擦力が大きくなり滑りにくい

人の手である "多指グリッパー" も存在するが,URDFを作成するのが困難であるため,採用しない.

把持対象物に関して

本記事では,把持対象物を立方体とする.
また,把持までの流れは以下の通りとなる.

1:始点から終点まで,干渉しない経路を探索する
2:干渉しない始点から終点までの経路を移動する
3:物体把持

ここで,重要なことは上記の「1」の時に,ロボットと把持対象物がぶつかってしまう可能性があること.ぶつかってしまうと,把持対象物が指定した位置から大きくずれた位置に動いてしまう.
上記を考慮して,把持対象物は経路探索時に,ぶつかっても動かないように拘束させる.PyBulletでは,拘束させる(動かさないようにする)ことが可能である.

把持対象物のURDF作成

把持対象物(立方体)のUDEFを作成する.立方体を原点$(x, y, z) = (0, 0, 0)$に設定して,Python側で立方体の位置を変更していく.

grasp_object.urdf
<?xml version="1.0"?>
<robot name="grasp_cube">
    <link name="cube_link">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <box size="0.1 0.1 0.1"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <box size="0.1 0.1 0.1"/>
            </geometry>
        </collision>

        <!-- 慣性 -->
        <inertial>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <mass value="0.1"/>
            <inertia ixx="0.01" iyy="0.01" izz="0.01" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>
</robot>

経路生成の環境に関して

本記事では,先頭で説明した環境より干渉しない経路を生成する.
机を配置して,机の上に把持対象物が存在する環境としている.今回は,机と把持対象物は別URDFファイルを作成して,対応している.

環境(机)のURDF作成

環境(机)のURDFを作成する.机の色は,ネットで「木の色 カラーコード」と検索した値を使用している.机の天板の中心位置を$(x, y, z) = (0, 0, 1)$に設定して,Python側で机の位置を変更していく.

environment_6dof.urdf
<?xml version="1.0" ?>
<robot name="table">


    <!-- 色を先に定義 (共通化したいから) ↓ -->
    <material name="wood">
        <color rgba="0.871 0.722 0.529 1"/>
    </material>
    <!-- 色を先に定義 (共通化したいから) ↑ -->


    <link name="base">
        <!-- 見た目,干渉判定,慣性はなし -->
    </link>


    <!-- テーブルの作成 ↓ -->
    <!-- テーブルの天板を動かさないようのジョイント -->
    <joint name="joint_table_top" type="fixed">

        <parent link="base"/>
        <child link="table_top"/>
        <origin xyz="0 0 1" rpy="0 0 0"/>

    </joint>


    <!-- テーブルの天板 -->
    <link name="table_top">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 0"/>
            <geometry>
                <box size="0.5 1 0.01"/>
            </geometry>
            <material name="wood"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 0"/>
            <geometry>
                <box size="0.5 1 0.01"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0 0 0"/>
            <mass value="1.0"/>
            <inertia ixx="0.083" iyy="0.021" izz="0.104" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- テーブルの天板とテーブルの足を固定化する関節 -->
    <joint name="joint_table_leg1" type="fixed">

        <parent link="table_top"/>
        <child link="table_leg1"/>
        <origin xyz="0.2 0.45 0" rpy="0 0 0"/>

    </joint>


    <!-- テーブルの足1 (+x, +y方向) -->
    <link name="table_leg1">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
            <material name="wood"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0 0 -0.5"/>
            <mass value="2.0"/>
            <inertia ixx="0.1683" iyy="0.1683" izz="0.0033" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- テーブルの天板とテーブルの足を固定化する関節 -->
    <joint name="joint_table_leg2" type="fixed">

        <parent link="table_top"/>
        <child link="table_leg2"/>
        <origin xyz="-0.2 0.45 0" rpy="0 0 0"/>

    </joint>


    <!-- テーブルの足2 (-x, +y方向) -->
    <link name="table_leg2">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
            <material name="wood"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0 0 -0.5"/>
            <mass value="2.0"/>
            <inertia ixx="0.1683" iyy="0.1683" izz="0.0033" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- テーブルの天板とテーブルの足を固定化する関節 -->
    <joint name="joint_table_leg3" type="fixed">

        <parent link="table_top"/>
        <child link="table_leg3"/>
        <origin xyz="-0.2 -0.45 0" rpy="0 0 0"/>

    </joint>


    <!-- テーブルの足3 (-x, -y方向) -->
    <link name="table_leg3">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
            <material name="wood"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0 0 -0.5"/>
            <mass value="2.0"/>
            <inertia ixx="0.1683" iyy="0.1683" izz="0.0033" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- テーブルの天板とテーブルの足を固定化する関節 -->
    <joint name="joint_table_leg4" type="fixed">

        <parent link="table_top"/>
        <child link="table_leg4"/>
        <origin xyz="0.2 -0.45 0" rpy="0 0 0"/>

    </joint>


    <!-- テーブルの足4 (+x, -y方向) -->
    <link name="table_leg4">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
            <material name="wood"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 -0.5"/>
            <geometry>
                <box size="0.1 0.1 1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0 0 -0.5"/>
            <mass value="2.0"/>
            <inertia ixx="0.1683" iyy="0.1683" izz="0.0033" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>
    <!-- テーブルの作成 ↑ -->


</robot>

グリッパー付き6軸ロボットアームのURDF作成

グリッパー付き6軸ロボットアームのURDFを作成する.グリッパーなし6軸ロボットアームのURDFは前記事と同じであるため,割愛する.(前記事 https://qiita.com/haruhiro1020/items/f8c807257a498522f5be)

robot_6dof_hand.urdf
<?xml version="1.0"?>
<robot name="robot_6dof_hand">

    <!-- 色を先に定義 (共通化したいから) ↓ -->
    <material name="black">
        <color rgba="0 0 0 1"/>
    </material>
    <!-- 色を先に定義 (共通化したいから) ↑ -->


    <!-- ベースリンク -->
    <link name="base_link">
        <!-- 見た目,干渉判定,慣性なし -->
    </link>


    <!-- ベースリンクとリンク1間の関節 -->
    <joint name="joint1" type="revolute">

        <parent link="base_link"/>
        <child link="link1"/>
        <origin xyz="0 0 0" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク1 -->
    <link name="link1">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.5"/>
            <!-- 質量は適当 -->
            <mass value="1.0"/>
            <inertia ixx="0.0866667" iyy="0.0866667" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク1とリンク2間の関節 -->
    <joint name="joint2" type="revolute">

        <parent link="link1"/>
        <child link="link2"/>
        <origin xyz="0 0 1.0" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク2 -->
    <link name="link2">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.5"/>
            <!-- 質量は適当 -->
            <mass value="1.0"/>
            <inertia ixx="0.0866667" iyy="0.0866667" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク2とリンク3間の関節 -->
    <joint name="joint3" type="revolute">

        <parent link="link2"/>
        <child link="link3"/>
        <origin xyz="0 0 1.0" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク3 -->
    <link name="link3">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.5"/>
            <geometry>
                <box size="0.2 0.2 1.0"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.5"/>
            <!-- 質量は適当 -->
            <mass value="1.0"/>
            <inertia ixx="0.0866667" iyy="0.0866667" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク3とリンク4間の関節 -->
    <joint name="joint4" type="revolute">

        <parent link="link3"/>
        <child link="link4"/>
        <origin xyz="0 0 1.0" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク4 -->
    <link name="link4">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.05"/>
            <!-- 質量は適当 -->
            <mass value="0.1"/>
            <inertia ixx="0.0004167" iyy="0.0004167" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク4とリンク5間の関節 -->
    <joint name="joint5" type="revolute">

        <parent link="link4"/>
        <child link="link5"/>
        <origin xyz="0 0 0.1" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク5 -->
    <link name="link5">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.05"/>
            <!-- 質量は適当 -->
            <mass value="0.1"/>
            <inertia ixx="0.0004167" iyy="0.0004167" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク5とリンク6間の関節 -->
    <joint name="joint6" type="revolute">

        <parent link="link5"/>
        <child link="link6"/>
        <origin xyz="0 0 0.1" rpy="0 0 0"/>
        <axis xyz="0 0 1"/>
        <limit lower="-3.14" upper="3.14" effort="100" velocity="5.0"/>

    </joint>


    <!-- リンク6 -->
    <link name="link6">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.05"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.05"/>
            <!-- 質量は適当 -->
            <mass value="0.1"/>
            <inertia ixx="0.0004167" iyy="0.0004167" izz="0.0066667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- リンク6とグリッパー間の関節 -->
    <joint name="gripper_joint" type="fixed">

        <parent link="link6"/>
        <child link="gripper_base_link"/>
        <origin xyz="0 0 0.1" rpy="0 0 0"/>

    </joint>


    <!-- グリッパーのベース部分 -->
    <!-- グリッパーはT字型とする -->
    <!-- T字型の縦棒作成 -->
    <link name="gripper_base_link">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 0.1" rpy="0 0 0"/>
            <geometry>
                <box size="0.1 0.1 0.2"/>
            </geometry>
            <material name="black"/>
        </visual>

        <!-- 干渉判定 -->
        <visual>
            <origin xyz="0 0 0.1" rpy="0 0 0"/>
            <geometry>
                <box size="0.1 0.1 0.2"/>
            </geometry>
            <material name="black"/>
        </visual>

        <!-- 慣性 -->
        <inertial>
            <mass value="0.05"/>
            <origin xyz="0 0 0.1" rpy="0 0 0"/>
            <inertia ixx="0.000208" iyy="0.000208" izz="0.000083" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- グリッパーとフィンガー間の関節 -->
    <joint name="finger_joint" type="fixed">

        <parent link="gripper_base_link"/>
        <child link="gripper_finger_link"/>
        <origin xyz="0 0 0.2" rpy="0 0 0"/>

    </joint>


    <!-- T字型の横棒作成 -->
    <link name="gripper_finger_link">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
            <material name="black"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <geometry>
                <box size="0.2 0.2 0.1"/>
            </geometry>
        </collision>

        <!-- 慣性 -->
        <inertial>
            <mass value="0.1"/>
            <origin xyz="0 0 0" rpy="0 0 0"/>
            <inertia ixx="0.000417" iyy="0.000417" izz="0.000667" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- フィンガーと右フィンガー間の関節 -->
    <joint name="right_finger_joint" type="prismatic">

        <parent link="gripper_finger_link"/>
        <child link="right_finger_link"/>
        <origin xyz="0 -0.095 0.05" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
        <limit lower="0.0" upper="0.090" effort="100.0" velocity="1.0"/>

    </joint>


    <!-- 右フィンガー -->
    <link name="right_finger_link">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.15"/>
            <geometry>
                <box size="0.2 0.01 0.3"/>
            </geometry>
            <material name="black"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.15"/>
            <geometry>
                <box size="0.2 0.01 0.3"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.15"/>
            <mass value="0.015"/>
            <inertia ixx="0.000113" iyy="0.000163" izz="0.000051" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- ハンドベースと左フィンガー間の関節 -->
    <joint name="left_finger_joint" type="prismatic">

        <parent link="gripper_finger_link"/>
        <child link="left_finger_link"/>
        <origin xyz="0 0.095 0.05" rpy="0 0 0"/>
        <axis xyz="0 1 0"/>
        <limit lower="-0.090" upper="0.0" effort="100.0" velocity="1.0"/>

    </joint>


    <!-- 左グリッパー -->
    <link name="left_finger_link">

        <!-- 見た目 -->
        <visual>
            <origin xyz="0.0 0.0 0.15"/>
            <geometry>
                <box size="0.2 0.01 0.3"/>
            </geometry>
            <material name="black"/>
        </visual>

        <!-- 干渉判定 -->
        <collision>
            <origin xyz="0.0 0.0 0.15"/>
            <geometry>
                <box size="0.2 0.01 0.3"/>
            </geometry>
        </collision>

        <!-- 慣性行列 -->
        <inertial>
            <origin xyz="0.0 0.0 0.15"/>
            <mass value="0.015"/>
            <inertia ixx="0.000113" iyy="0.000163" izz="0.000051" ixy="0.0" ixz="0.0" iyz="0.0"/>
        </inertial>

    </link>


    <!-- フィンガーと先端 (エンドエフェクター) 間の関節 -->
    <joint name="ee_joint" type="fixed">

        <parent link="gripper_finger_link"/>
        <child link="ee_link"/>
        <origin xyz="0 0 0.3" rpy="0 0 0"/>

    </joint>


    <!-- グリッパー先端 (エンドエフェクタ) -->
    <link name="ee_link">
        <!-- 見た目,干渉判定,慣性はなし -->
    </link>


</robot>

経路生成手法であるRRT

本記事で使用する経路生成手法として,RRTを使用する.
RRTは以前に記事を作成したため,内容は割愛する.
(RRTに関する記事 https://qiita.com/haruhiro1020/items/6eaae645bd638c2b8897)
本記事では,RRTを使用して,干渉回避できる経路を作成する.

PyBulletの使用方法

Pythonの物理シミュレータであるPyBulletの使用方法について説明する.
下記リンクのPyBullet公式で調べながら,PyBulletを使用している.
https://pybullet.org/wordpress/
上記リンクを開くと,下図のようなサイトに飛ぶ.使用方法を調べるときは,下図の赤枠内の「PYBULLET QUICKSTART GUIDE」タグをクリックする.
PyBullet公式.png

「PYBULLET QUICKSTART GUIDE」タグをクリックすると,下図のようなドキュメントを見ることができる.基本的には,ドキュメントに記載されている関数の使用方法を見て,ソースコードを作成している.
PyBulletドキュメント.png
PyBulletの関数の引数や戻り値をもっと知りたいのでしたら,上記ドキュメントを見た方がわかりやすいです.

全体のソースコード

はじめに,本記事で使用する全体のソースコードについて説明する.
その後,重要な箇所を抜粋して別途解説をしていく.
ソースコードとして,下記の4ファイルを作成する.
・定数の定義 (constant.py)
・全体的なメイン処理 (main.py)
・PyBulletでロボットを動かす (pybullet_robot.py)
・経路生成手法であるRRT (pybullet_rrt.py)

各ファイルの中身を説明する.

定数の定義 (constant.py)

定数を定義する constant.py について説明する.

constant.py
# 複数ファイルで使用する定数の定義
from enum import Enum


# 次元数を定義
DIMENTION_NONE  = -1    # 未定義
DIMENTION_2D    =  2    # 2次元
DIMENTION_3D    =  3    # 3次元
DIMENTION_6D    =  6    # 6次元

# 重力
GRABITY_VALUE   = 9.81

# シード値の最小値と最大値
MIN_SEED        = 0
MAX_SEED        = 2 ** 32 - 1  # 4バイト (uint) の最大値

# 0割を防ぐための定数
EPSILON         = 1e-6

# ノードの最短ノード要素とコスト要素を定義
# RRT
RRT_NEAR_NODE_IDX       = -1    # RRTでの最短ノード要素

# ツリーの初期ノードの親ノード
INITIAL_NODE_NEAR_NODE  = -1    # 初期ノードに親ノードが存在しないから-1

# 補間方法の定義
class INTERPOLATION(Enum):
    """
    補間方法
    """
    NONE      = "none"      # 未定義
    JOINT     = "joint"     # 関節補間
    POSITION  = "pos"       # 位置補間

# RRTによる経路生成手法の定義
class PATHPLAN(Enum):
    """
    経路生成手法
    """
    RRT     = "rrt"     # RRTによる経路生成

# ロボットアームが保存されている URDF ファイル名
class ROBOTURDF(Enum):
    """
    URDFファイル名
    """
    # 2軸ロボットアーム
    DOF2 = "robot_2dof.urdf"            # ハンド(グリッパ)なし
    DOF2_HAND = "robot_2dof_hand.urdf"  # ハンド(グリッパ)付き

    # 3軸ロボットアーム
    DOF3 = "robot_3dof.urdf"            # ハンド(グリッパ)なし
    DOF3_HAND = "robot_3dof_hand.urdf"  # ハンド(グリッパ)付き

    # 6軸ロボットアーム
    DOF6 = "robot_6dof.urdf"            # ハンド(グリッパ)なし
    DOF6_HAND = "robot_6dof_hand.urdf"  # ハンド(グリッパ)付き

全体的なメイン処理 (main.py)

全体的なメイン処理を定義する main.py について説明する.
後ほど説明する pybullet_robot.py 内のクラスを実装するのがメインな処理である.

main.py
# Pybullet (Pythonでの3次元物理シミュレータ) によるロボットアームの可視化


# 標準ライブラリの読み込み
import numpy as np


# 自作モジュールの読み込み
from pybullet_robot import MainPyBulletRobot
from constant import *



N_ROBOT_AXIS = DIMENTION_6D     # ロボットの関節数
CONST_SEED = 1  # シード値 (常に同じ結果としたいから)
HAND_FLG   = True   # ハンドの装着有無


def main():
    """
    メイン処理
    """
    # 環境が保存されている URDF ファイル名・探索空間・初期位置
    if N_ROBOT_AXIS == DIMENTION_2D:    # 2軸ロボットアーム
        environment_urdf = "environment_2dof.urdf"
        interpolation = INTERPOLATION.POSITION.value    # 直交空間の探索
        start_pos = np.array([1.0, -1.0])

    elif N_ROBOT_AXIS == DIMENTION_3D:  # 3軸ロボットアーム
        environment_urdf = "environment_3dof.urdf"
        interpolation = INTERPOLATION.POSITION.value    # 直交空間の探索
        start_pos = np.array([1.0, -1.0, 1.0])

    elif N_ROBOT_AXIS == DIMENTION_6D:  # 6軸ロボットアーム
        environment_urdf = "environment_6dof.urdf"
        interpolation = INTERPOLATION.JOINT.value       # 関節空間の探索
        start_pos = np.array([1.0, -1.0, 1.0, 0, np.pi/2, 0]) # 位置(x, y, z)・姿勢(ロール, ピッチ, ヨー)

    else:
        raise ValueError(f"N_ROBOT_AXIS is abnormal. N_ROBOT_AXIS is {N_ROBOT_AXIS}")

    # 経路生成手法
    path_plan = PATHPLAN.RRT.value

    # 把持対象物が保存されている URDF ファイル名
    grasp_urdf = "grasp_object.urdf"

    # Pybulletを使用するインスタンス作成
    my_robot = MainPyBulletRobot(interpolation, N_ROBOT_AXIS, environment_urdf=environment_urdf, grasp_urdf=grasp_urdf, hand=HAND_FLG)

    # シード値の設定
    np.random.seed(CONST_SEED)

    if interpolation == INTERPOLATION.JOINT.value:
        # 関節空間の探索時は位置を関節角度に変換
        start_theta = my_robot.convert_pos_to_theta(start_pos)
        print(f"start_theta = {start_theta}")

        result = my_robot.run(start_theta, path_plan)
    else:
        result = my_robot.run(start_pos,   path_plan)

    print(f"result = {result}")


if __name__ == "__main__":
    # 本ファイルがメインで呼ばれた時の処理
    main()

6軸ロボットアームの初期位置は,RRTによる経路生成が可能となるように,調整した.
2, 3軸ロボットアームの環境URDFファイルは,以下記事で定義した.
(2軸ロボットアームの環境URDFファイル:https://qiita.com/haruhiro1020/items/43ddb8b3c4e379ab7983
3軸ロボットアームの環境URDFファイル:https://qiita.com/haruhiro1020/items/d26009d50be190d97368)

PyBulletでロボットを動かす (pybullet_robot.py)

PyBulletでロボットを動かす処理を定義する pybullet_robot.py について説明する.

pybullet_robot.py
# PyBulletで使用するロボットを記載


# ライブラリの読み込み
import pybullet as p    # PyBullet
import pybullet_data    # PyBulletで使用するデータ
import time             # 時間
import numpy as np      # 数値計算ライブラリ


# サードパーティの読み込み


# 自作モジュールの読み込み
from constant import *
from pybullet_rrt import RRTPyBullet



class BaseGripper:
    """
    グリッパーのベースクラス (抽象クラス)

    プロパティ
        _robot_id(): ロボットアームのID番号
        _n_joints(int): ロボットアームの関節数

    メソッド
        public
            run(): 実行 現在角度を関節に与える (オープン・クローズ以外で常に呼ぶこと)
            open(): グリッパーのオープン
            close(): グリッパーのクローズ
    """
    # 定数の定義
    _JOINT_CURRENT_VALUE_IDX = 0    # 関節の現在値の要素番号
    
    
    def __init__(self, robot_id, n_joint):
        """
        コンストラクタ

        パラメータ
            robot_id(p.loadURDFの戻り値): ロボットURDFを読み込んだ際のID
            n_joint(int): ロボットの関節数 (グリッパーは含むが,グリッパー先端は含まない)
        """
        # プロパティの初期化
        self._robot_id = robot_id
        self._n_joint  = n_joint
        
        
    def run(self):
        """
        実行 (毎時刻,本関数を呼ぶこと)
        """
        raise InterruptedError("run() is necessary override.")


class ParallelGripper(BaseGripper):
    """
    パラレルグリッパー (2本の指が並行に動く)

    メソッド
        public
            実行 (毎時刻,本関数を呼ぶこと)

        protected
            _get_move_direction(): グリッパーの移動方向を取得
            _get_gripper_right_left_idx(): グリッパーの右・左の関節番号
            _set_joint_values(): 関節角度[m]を設定
            _get_joint_values(): 関節角度[m]を取得
    """
    # 定数の定義
    _GRIPPER_RIGHT_IDX =  -2    # 右グリッパーの関節番号
    _GRIPPER_LEFT_IDX  =  -1    # 左グリッパーの関節番号
    
    _GRIPPER_MOVE_VAL  = 0.01   # グリッパーの1回あたりの移動量 [m]
    _GRIPPER_LATERAL_FRIC = 1.0 # グリッパーの摩擦係数
    
    _GRIPPER_CLOSE_VAL = 0.03   # グリッパーのクローズ時のフィンガー角度 [m]
    _GRIPPER_OPEN_VAL  = 0      # グリッパーのクローズ時のフィンガー角度 [m]
    
    
    def __init__(self, robot_id, n_joint):
        """
        コンストラクタ

        パラメータ
            robot_id(p.loadURDFの戻り値): ロボットURDFを読み込んだ際のID
            n_joint(int): ロボットの関節数 (グリッパーは含むが,グリッパー先端は含まない)
        """
        # 親クラスのコンストラクタ
        super().__init__(robot_id, n_joint)

        # ダイナミクスの変更
        self._chg_dynamics()
    
    
    def _chg_dynamics(self):
        """
        ダイナミクスの変更
        """
        # グリッパーの右・左の関節番号を取得
        gripper_right_left_idx = self._get_gripper_right_left_idx()

        for idx in gripper_right_left_idx:
            p.changeDynamics(self._robot_id,        # 把持対象物ID
                            idx,                    # 関節番号
                            lateralFriction=1.0)    # 床との摩擦係数
    
    
    def run(self, open=False, close=False):
        """
        実行 (毎時刻,本関数を呼ぶこと)

        パラメータ
            open(bool): グリッパーのオープンフラグ
            close(bool): グリッパーのクローズフラグ
        """
        # グリッパーの現在の関節角度[m]を取得
        joint_values = self._get_joint_values()

        # オープンとクローズが同時実行の時,安全の観点よりクローズよりもオープンを優先
        if open:
            # グリッパーのオープン時
            # 移動方向を取得して,設定したい関節角度[m]を計算
            direction = self._get_move_direction(open=open)
            joint_values = direction * self._GRIPPER_OPEN_VAL
        elif close:
            # グリッパーのクローズ時
            # グリッパーのクローズに関するキーボードが押下された時
            direction = self._get_move_direction(open=open)
            joint_values = direction * self._GRIPPER_CLOSE_VAL
        else:
            # 押下されていないため,何もしない
            pass

        # 関節角度の設定
        self._set_joint_values(joint_values)

    def _get_move_direction(self, open):
        """
        グリッパーの移動方向を取得

        パラメータ
            open(bool): True/False = オープン/クローズ

        戻り値
            numpy.ndarray: グリッパーの移動方向 (右関節・左関節の順番)
        """
        # グリッパーの右関節・左関節の移動方向
        move_direction = np.array([-1.0, 1.0])
        if not open:        # クローズ
            move_direction *= -1

        return move_direction

    def _get_gripper_right_left_idx(self):
        """
        グリッパーの右・左の関節番号

        戻り値
            list: グリッパーの右・左の関節番号 (右・左の順番にデータ保存)
        """
        right_left_idx = [self._n_joint + self._GRIPPER_RIGHT_IDX, self._n_joint + self._GRIPPER_LEFT_IDX]

        return right_left_idx

    def _set_joint_values(self, values):
        """
        関節角度[m]を設定

        パラメータ
            values(numpy.ndarray): 設定値
        """
        # パラメータのサイズを確認
        if values.shape[0] != DIMENTION_2D:
            # 異常
            raise ValueError(f"values'shape[0] is abnormal. values'shape[0] is {values.shape[0]}")

        # グリッパーの右・左の関節番号
        gripper_right_left_idx = self._get_gripper_right_left_idx()

        for idx, gripper_idx in enumerate(gripper_right_left_idx):
            p.setJointMotorControl2(
                bodyIndex=self._robot_id,
                jointIndex=gripper_idx,
                controlMode=p.POSITION_CONTROL,
                targetPosition=values[idx],
                positionGain=0.5,      # デフォルトよりやや高め
                velocityGain=1.0       # 高速応答(必要に応じて調整)
            )

    def _get_joint_values(self):
        """
        関節角度[m]を取得

        戻り値
            numpy.ndarray: 関節角度 (グリッパーの右関節,グリッパーの左関節)
        """
        # グリッパーの右・左の関節番号
        gripper_right_left_idx = self._get_gripper_right_left_idx()
        joint_values = []

        for gripper_idx in gripper_right_left_idx:
            # グリッパー関節の状態を取得
            joint_state = p.getJointState(bodyUniqueId=self._robot_id, jointIndex=gripper_idx)
            # 関節の値を保存
            joint_values.append(joint_state[self._JOINT_CURRENT_VALUE_IDX])

        return np.array(joint_values)


class PyBulletRobot:
    """
    PyBulletのロボットのベースクラス (抽象クラス)

    プロパティ
        _robot_id(): ロボットアームのID番号
        _n_joint(int): URFDファイル内のロボットの間接数
        _hand(RobotHand): ハンドクラス
        _interpolation(str): 探索空間 (直交空間/関節空間)

    メソッド
        public
            set_joint(): 関節角度の設定
            set_jump_joint(): 関節角度をジャンプ
            convert_pos_to_theta(): 位置から関節角度に変換
            run_gripper(): グリッパーの実行

        protected
            _chk_pos_dim(): 位置(姿勢も含む)の次元数確認
            _chk_thetas_dim(): 関節の次元数確認
    """
    # 定数の定義
    # 子クラスで必ず定義する必要がある ↓
    _DIM_JOINT = DIMENTION_NONE     # 関節の次元数
    _DIM_POSE  = DIMENTION_NONE     # 位置(姿勢も含む)の次元数
    # 子クラスで必ず定義する必要がある ↑
    
    _N_HAND_JOINT = 4               # ハンド用の関節数 (パラレルグリッパーだけに対応するため,4固定)
    _N_HAND_JOINT_NOT_FIXED = 2     # ハンド用の関節で固定関節以外
    
    _WEIGHT_JOINT = None            # 各軸への重み (RRTで使用
    
    _JOINT_LIMIT_LOW_IDX = 8        # 関節限界の下限値インデックス
    _JOINT_LIMIT_UP_IDX  = 9        # 関節限界の上限値インデックス
    
    
    def __init__(self, robot_id, interpolation, hand):
        """
        コンストラクタ

        パラメータ
            robot_id(int): ロボットID (loadURDF()の戻り値)
            interpolation(str): 探索方法 (関節空間/位置空間)
            hand(bool): ハンド装着の有無 True/False = あり/なし
        """
        # 引数の確認
        if not (interpolation == INTERPOLATION.JOINT.value or interpolation == INTERPOLATION.POSITION.value):
            # 異常
            raise ValueError(f"interpolation is abnormal. interpolation is {interpolation}")

        # プロパティの更新
        self._robot_id = robot_id
        self._interpolation = interpolation

        # urdf よりロボットの関節数を取得 (エンドエフェクタ用のデバッグ関節は不要なため -1)
        self._n_joints = p.getNumJoints(robot_id) - 1

        # ハンド装着有無
        if hand:        # ハンド装着
            if self._n_joints != (self._DIM_JOINT + self._N_HAND_JOINT):
                # 関節数が異常
                raise ValueError(f"self._n_joints is abnormal. {self._n_joints} is abnormal.")

            # 今回は,パラレルグリッパーのみに対応
            self._hand = ParallelGripper(self._robot_id, self._n_joints)

        else:           # ハンドなし
            if self._n_joints != self._DIM_JOINT:
                # 関節数が異常
                raise ValueError(f"self._n_joints is abnormal. {self._n_joints} is abnormal.")

            self._hand = None

        # URDFから関節限界を取得
        self._joint_limit()


    def _joint_limit(self):
        """
        関節限界
        """
        joint_limit = []

        # 全軸の関節限界
        for joint_index in range(self._DIM_JOINT):
            joint_info = p.getJointInfo(self._robot_id, joint_index)
            lower = joint_info[self._JOINT_LIMIT_LOW_IDX]
            upper = joint_info[self._JOINT_LIMIT_UP_IDX]
            joint_limit.append((lower, upper))

        # プロパティの更新
        self._joint_limit = np.array(joint_limit)


    # プロパティのゲッター ↓
    @property
    def joint_limit(self):
        """
        _joint_limitプロパティのゲッター
            最小関節限界:[:, 0]
            最大関節限界:[:, 1]
        """
        return self._joint_limit

    @property
    def weight_joint(self):
        """
        _WEIGHT_JOINT (各関節の重み) のゲッター
        """
        return self._WEIGHT_JOINT
    # プロパティのゲッター ↑


    def _chk_pos_dim(self, pos):
        """
        位置(姿勢も含む)の次元数確認

        パラメータ
            pos(numpy.ndarray): 位置 [m],姿勢 [rad]
        """
        # 引数の確認
        if len(pos) != self._DIM_POSE:
            # 異常
            raise ValueError(f"pos's shape is abnormal. pos'size is {len(pos)}")

    def _chk_thetas_dim(self, thetas):
        """
        関節の次元数確認

        パラメータ
            thetas(numpy.ndarray): 関節角度 [rad]
        """
        # 修正後 ↓
        if len(thetas) != self._DIM_JOINT:
            # 異常
            raise ValueError(f"theta's shape is abnormal. thetas'size is {len(thetas)}")
        # 修正後 ↑

        # 修正前 ↓
        # if self._hand is None:      # ハンドなし
        #     # 引数の確認
        #     if len(thetas) != self._DIM_JOINT:
        #         # 異常
        #         raise ValueError(f"theta's shape is abnormal. thetas'size is {len(thetas)}")

        # else:                       # ハンドあり
        #     # 引数の確認
        #     if len(thetas) != (self._DIM_JOINT + self._N_HAND_JOINT_NOT_FIXED):
        #         # 異常
        #         raise ValueError(f"theta's shape is abnormal. thetas'size is {len(thetas)}")
        # 修正前 ↑

    def set_joint(self, thetas):
        """
        関節角度の設定

        パラメータ
            thetas(numpy.ndarray): 関節角度 [rad]
        """
        # 引数の確認
        self._chk_thetas_dim(thetas)

        for i in range(len(thetas)):
            # 関節角度を設定
            p.setJointMotorControl2(
                bodyUniqueId=self._robot_id,    # IDの設定
                jointIndex=i,                   # 関節番号の設定
                controlMode=p.POSITION_CONTROL, # 位置制御
                targetPosition=thetas[i]        # 関節角度
            )

    def set_jump_joint(self, thetas):
        """
        関節角度をジャンプ

        パラメータ
            thetas(numpy.ndarray): 関節角度 [rad]
        """
        # 引数の確認
        self._chk_thetas_dim(thetas)

        for i in range(thetas.shape[0]):
            # 関節角度を設定
            p.resetJointState(
                bodyUniqueId=self._robot_id,    # IDの設定
                jointIndex=i,                   # 関節番号の設定
                targetValue=thetas[i]           # 関節角度
            )

    def convert_pos_to_theta(self, pos, force=False):
        """
        位置から関節角度に変換

        パラメータ
            pos(numpy.ndarray): 位置 / 関節角度
            force(bool): パラメータposを絶対に位置とみなす

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        raise InterruptedError("convert_pos_to_theta() is necessary override.")

    def run_gripper(self, open=False, close=False):
        """
        グリッパーの実行
        
        パラメータ
            open(bool): グリッパーのオープンフラグ
            close(bool): グリッパーのクローズフラグ
        """
        if self._hand is None:
            # ハンド非装着のため,処理終了
            return

        self._hand.run(open, close)


class PyBullet2DoFRobot(PyBulletRobot):
    """
    PyBulletの2軸ロボットクラス

    メソッド
        public
            convert_pos_to_theta(): 位置から関節角度に変換

        private
            __inverse_kinematics(): 逆運動学(位置から関節角度に変換)
    """
    # 定数の定義
    _Z_VALUE = 0.0      # 位置を3次元変換する時のZ値
    
    _DIM_JOINT = DIMENTION_2D     # 関節の次元数
    _DIM_POSE  = DIMENTION_2D     # 位置(姿勢も含む)の次元数
    
    
    def __init__(self, robot_id, interpolation, hand):
        """
        コンストラクタ

        パラメータ
            robot_id(int): ロボットID (loadURDF()の戻り値)
            interpolation(str): 探索方法 (関節空間/位置空間)
            hand(bool): ハンド装着の有無 True/False = あり/なし
        """
        # 親クラスのコンストラクを実行
        super().__init__(robot_id, interpolation, hand)


    def __inverse_kinematics(self, pos):
        """
        逆運動学(位置から関節角度に変換)

        パラメータ
            pos(numpy.ndarray): 位置 [m]

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        # 引数の確認
        self._chk_pos_dim(pos)

        # posは2次元データであるため,3次元データへ変換する
        # (PyBulletの逆運動学を実装するため)
        pos = np.append(pos, self._Z_VALUE)

        # エンドエフェクタのリンク要素はベースリンクを除いた要素番号となる
        thetas = p.calculateInverseKinematics(self._robot_id, self._n_joints, pos)
        thetas = np.array(thetas)

        return thetas


    def convert_pos_to_theta(self, pos, force=False):
        """
        位置から関節角度に変換

        パラメータ
            pos(numpy.ndarray): 位置 [m] / 関節角度 [rad]
            force(bool): パラメータposを絶対に位置とみなす

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        if force:       # posを位置とみなす
            # 逆運動学
            thetas = self.__inverse_kinematics(pos)

        else:           # プロパティ "_interpolation" より決定
            if self._interpolation == INTERPOLATION.POSITION.value:
                # 逆運動学
                thetas = self.__inverse_kinematics(pos)

            else:
                # pos が関節角度のため,そのまま返す
                thetas = np.copy(pos)

        if self._hand is not None:
            # グリッパー付きの場合は,グリッパー部分を削除
            thetas = thetas[:self._DIM_JOINT]

        return thetas


class PyBullet3DoFRobot(PyBulletRobot):
    """
    PyBulletの3軸ロボットクラス

    メソッド
        public
            convert_pos_to_theta(): 位置から関節角度に変換

        private
            __inverse_kinematics(): 逆運動学(位置から関節角度に変換)
    """
    # 定数の定義
    _DIM_JOINT = DIMENTION_3D     # 関節の次元数
    _DIM_POSE  = DIMENTION_3D     # 位置(姿勢も含む)の次元数
    
    
    def __init__(self, robot_id, interpolation, hand):
        """
        コンストラクタ

        パラメータ
            robot_id(int): ロボットID (loadURDF()の戻り値)
            interpolation(str): 探索方法 (関節空間/位置空間)
            hand(bool): ハンド装着の有無 True/False = あり/なし
        """
        # 親クラスのコンストラクを実行
        super().__init__(robot_id, interpolation, hand)


    def __inverse_kinematics(self, pos):
        """
        逆運動学(位置から関節角度に変換)

        パラメータ
            pos(numpy.ndarray): 位置 [m]

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        # 引数の確認
        self._chk_pos_dim(pos)

        # エンドエフェクタのリンク要素はベースリンクを除いた要素番号となる
        thetas = p.calculateInverseKinematics(self._robot_id, self._n_joints, pos)
        thetas = np.array(thetas)

        return thetas


    def convert_pos_to_theta(self, pos, force=False):
        """
        位置から関節角度に変換

        パラメータ
            pos(numpy.ndarray): 位置 [m] / 関節角度 [rad]
            force(bool): パラメータposを絶対に位置とみなす

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        if force:       # posを位置とみなす
            # 逆運動学
            thetas = self.__inverse_kinematics(pos)

        else:           # プロパティ "_interpolation" より決定
            if self._interpolation == INTERPOLATION.POSITION.value:
                # 逆運動学
                thetas = self.__inverse_kinematics(pos)

            else:
                # pos が関節角度のため,そのまま返す
                thetas = np.copy(pos)

        if self._hand is not None:
            # グリッパー付きの場合は,グリッパー部分を削除
            thetas = thetas[:self._DIM_JOINT]

        return thetas


class PyBullet6DoFRobot(PyBulletRobot):
    """
    PyBulletの6軸ロボットクラス

    メソッド
        public
            convert_pos_to_theta(): 位置から関節角度に変換

        private
            __inverse_kinematics(): 逆運動学(位置から関節角度に変換)
    """
    # 定数の定義
    _DIM_JOINT = DIMENTION_6D     # 関節の次元数
    _DIM_POSE  = DIMENTION_6D     # 位置(姿勢も含む)の次元数
    
    # _WEIGHT_JOINT = np.array([1, 1, 1, 1, 1, 1])  # 各軸への重み (RRTで使用)
    
    
    def __init__(self, robot_id, interpolation, hand):
        """
        コンストラクタ

        パラメータ
            robot_id(int): ロボットID (loadURDF()の戻り値)
            interpolation(str): 探索方法 (関節空間/位置空間)
            hand(bool): ハンド装着の有無 True/False = あり/なし
        """
        # 親クラスのコンストラクを実行
        super().__init__(robot_id, interpolation, hand)


    def __inverse_kinematics(self, pos):
        """
        逆運動学(位置から関節角度に変換)

        パラメータ
            pos(numpy.ndarray): 位置[m]・姿勢[rad]

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        # 引数の確認
        self._chk_pos_dim(pos)

        # パラメータ pos を位置と姿勢に分解
        position, orientation = pos[:DIMENTION_3D], pos[DIMENTION_3D:]
        # 姿勢を ロール・ピッチ・ヨー から クォータニオンへ変換
        # 逆運動学の解を算出するには,姿勢はクォータニオンでないといけない
        quaternion = p.getQuaternionFromEuler(orientation)

        # エンドエフェクタのリンク要素はベースリンクを除いた要素番号となる
        thetas = p.calculateInverseKinematics(self._robot_id, self._n_joints, targetPosition=position, targetOrientation=quaternion)
        thetas = np.array(thetas)

        if self._hand is not None:
            # グリッパー付きなら,グリッパー部分の角度は不要
            thetas = thetas[:self._DIM_JOINT]

        return thetas


    def convert_pos_to_theta(self, pos, force=False):
        """
        位置から関節角度に変換

        パラメータ
            pos(numpy.ndarray): 位置 [m] / 関節角度 [rad]
            force(bool): パラメータposを絶対に位置とみなす

        戻り値
            numpy.ndarray: 関節角度 [rad]
        """
        if force:       # posを位置とみなす
            # 逆運動学
            thetas = self.__inverse_kinematics(pos)

        else:           # プロパティ "_interpolation" より決定
            if self._interpolation == INTERPOLATION.POSITION.value:
                # 逆運動学
                thetas = self.__inverse_kinematics(pos)

            else:
                # pos が関節角度のため,そのまま返す
                thetas = np.copy(pos)

                if self._hand is not None:
                # グリッパー付きなら,グリッパー部分の角度は不要
                    thetas = thetas[:self._DIM_JOINT]

        return thetas


class MainPyBulletRobot:
    """
    PyBulletのメインクラス

    プロパティ
        _robot_id(): ロボットアームのID番号
        _environment_id(): 県境のID番号
        _interpolation(str): 探索空間 (直交空間/関節空間)
        _rrt(rrt.py内のクラス): 経路生成

    メソッド
        public

            メイン処理関連
                run(): 実行 (始点から終点まで,干渉しない経路を生成)

            運動学関連
                convert_pos_to_theta(): 位置から関節角度に変換 (クラス外で使う用)


        protected

            事前準備関連
                _init_robot(): ロボットの初期化
                _init_environment(): 環境の初期化

            メイン処理関連
                _set_path_plan(): 経路生成手法の設定
                _path_planning(): 始点から終点までの経路生成
                _post_path_planning(): 経路生成の後処理 (経路生成成功時だけ実装)

            干渉判定処理
                _is_line_interference(): 2点間の干渉判定
                _is_interference_start_end_pos(): 始点と終点が干渉判定していないかの確認
                _is_interference_pos(): 位置にジャンプして干渉判定
    """
    # 定数の定義
    _PLANE_URDF     = "plane.urdf"  # 地面に関する urdf ファイル
    
    _IDX_MIN_JOINT  = 8             # 関節の最小値が保存されている要素番号
    _IDX_MAX_JOINT  = 9             # 関節の最大値が保存されている要素番号
    
    _SIMULATION_SLEEP_TIME = 0.05   # シミュレーションの待機時間 [sec]
    
    _INTERFERENCE_MARGIN   = 0.15   # 干渉判定のマージン [m]
    
    _PATH_PLAN_TIME = 100   # 経路生成の最大時間 [sec]
    _N_MARGIN_MOVE  = 50    # 経路生成終了後の余白時間 [回]
    
    _ROBOT_BASE_POSITION      = [ 0,   0,   0]      # ロボットーのベース位置
    # 2軸ロボットアーム用データ
    _GRASP_OBJECT_POS_2DOF    = [ 1.8, 1.0, 0.05]   # 把持対象物の位置
    _GRASP_OBJECT_OFFSET_2DOF = [-0.4, 0]           # 把持対象物の位置のオフセット
    _ENVIRONMENT_POS_2DOF     = [ 0,   0,   0]      # 環境のベース位置
    # 3軸ロボットアーム用データ
    _GRASP_OBJECT_POS_3DOF    = [ 1.25,  0.4, 0.55] # 把持対象物の位置
    _GRASP_OBJECT_OFFSET_3DOF = [-0.15,  0,   0.15] # 把持対象物の位置のオフセット
    _ENVIRONMENT_POS_3DOF     = [ 1.5,  0,   -0.5]  # 環境のベース位置
    # 6軸ロボットアーム用データ
    _GRASP_OBJECT_POS_6DOF    = [ 1.5 ,  0.4,  1.05] # 把持対象物の位置
    _GRASP_OBJECT_OFFSET_6DOF = [ -0.2,  0,    0.2]    # 把持対象物の位置のオフセット
    _ENVIRONMENT_POS_6DOF     = [ 1.75,  0,    0]  # 環境のベース位置
    
    
    def __init__(self, interpolation, n_robot_joint, environment_urdf=None, grasp_urdf=None, hand=False):
        """
        コンストラクタ

        パラメータ
            interpolation(str): 補間方法 (関節空間/位置空間)
            n_robot_joint(int): ロボットアームの関節数(2, 3, 6だけ)
            environment_urdf(str): 環境のファイル名 (urdf)
            grasp_object(str): 把持対象物のファイル名 (urdf)
            hand(bool): ハンドの装着有無 True/False = 装着/未装着
        """
        # PyBulletの初期化
        p.connect(p.GUI)
        # パスの追加
        p.setAdditionalSearchPath(pybullet_data.getDataPath())
        # シミュレーションの初期化
        p.resetSimulation()
        # 重力の設定 (下(-z軸)方向の加速度)
        p.setGravity(0, 0, -GRABITY_VALUE)

        # ロボットの初期化
        self._init_robot(n_robot_joint, interpolation, hand)

        # 環境の初期化
        self._init_environment(environment_urdf, grasp_urdf)

        self._rrt = None


    # 事前準備メソッド ↓
    def _init_robot(self, n_robot_joint, interpolation, hand):
        """
        ロボットの初期化

        パラメータ
            n_robot_joint(int): ロボットアームの関節数(2, 3, 6だけ)
            interpolation(str): 探索方法 (関節空間/位置空間)
            hand(bool): ハンドの装着有無 True/False = 装着/未装着
        """
        if n_robot_joint == DIMENTION_2D:       # 2軸ロボットアーム
            if hand:    # ハンド装着
                robot_urdf = ROBOTURDF.DOF2_HAND.value
            else:       # ハンド未装着
                robot_urdf = ROBOTURDF.DOF2.value
            # 2軸ロボットアームのクラス
            robot_cls = PyBullet2DoFRobot

        elif n_robot_joint == DIMENTION_3D:     # 3軸ロボットアーム
            if hand:    # ハンド装着
                robot_urdf = ROBOTURDF.DOF3_HAND.value
            else:       # ハンド未装着
                robot_urdf = ROBOTURDF.DOF3.value
            robot_cls = PyBullet3DoFRobot

        elif n_robot_joint == DIMENTION_6D:     # 6軸ロボットアーム
            if hand:    # ハンド装着
                robot_urdf = ROBOTURDF.DOF6_HAND.value
            else:       # ハンド未装着
                robot_urdf = ROBOTURDF.DOF6.value
            robot_cls = PyBullet6DoFRobot

        else:   # 異常
            raise ValueError(f"n_robot_joint is abnormal. n_robot_joint is {n_robot_joint}.")

        # ロボットを読み込む.ベースリンクの原点は (x, y, z) = (0, 0, 0) として,ベースリンクは地面に固定
        self._robot_id = p.loadURDF(robot_urdf, basePosition=self._ROBOT_BASE_POSITION, useFixedBase=True)

        # ロボットクラスのインスタンス作成
        self._robot = robot_cls(self._robot_id, interpolation, hand)

        # プロパティの更新
        self._interpolation = interpolation
        self._n_robot_joint = n_robot_joint

    def _init_environment(self, environment_urdf, grasp_urdf):
        """
        環境の初期化
        
        パラメータ
            environment_urdf(str): 環境が保存されているファイル名
            grasp_urdf(str): 把持対象物が保存されているファイル名
        """
        # 地面を読み込む (pybulletが提供している "plane.urdf" を読み込む)
        p.loadURDF(self._PLANE_URDF)

        # 環境を読み込む
        self._environment_id = None
        if environment_urdf is not None:
            if self._n_robot_joint == DIMENTION_2D:     # 2軸ロボットアーム
                basePosition = self._ENVIRONMENT_POS_2DOF
            elif self._n_robot_joint == DIMENTION_3D:       # 3軸ロボットアーム
                basePosition = self._ENVIRONMENT_POS_3DOF
            else:       # 6軸ロボットアーム
                basePosition = self._ENVIRONMENT_POS_6DOF

            self._environment_id = p.loadURDF(environment_urdf, basePosition=basePosition, useFixedBase=True)

        # 把持対象物を読み込む
        self._grasp_id = None
        self._grasp_constraint_id = None

        if grasp_urdf is not None:
            if self._n_robot_joint == DIMENTION_2D:     # 2軸ロボットアーム
                grasp_center = self._GRASP_OBJECT_POS_2DOF
            elif self._n_robot_joint == DIMENTION_3D:   # 3軸ロボットアーム
                grasp_center = self._GRASP_OBJECT_POS_3DOF
            else:       # 6軸ロボットアーム
                grasp_center = self._GRASP_OBJECT_POS_6DOF

            self._grasp_id = p.loadURDF(grasp_urdf, basePosition=grasp_center)

            # 把持対象物に摩擦を付与する
            p.changeDynamics(self._grasp_id,        # 把持対象物ID
                            -1,                     # ベースに対して
                            lateralFriction=1.0,    # 床との摩擦係数
                            spinningFriction=1.0,   # 回転摩擦係数
                            rollingFriction=1)      # 転がり摩擦

            # 把持対象物の位置・姿勢を取得
            grasp_pos, grasp_ori = self._get_grasp_pos(offset=[0, 0, 0])
            # 把持対象物に拘束条件を付与
            self._grasp_constraint_id = self._set_constraint(self._grasp_id, grasp_pos, grasp_ori)
    # 事前準備メソッド ↑


    # メイン処理 ↓
    def run(self, start_pos, path_plan):
        """
        実行
            始点から終点まで,干渉しない経路を生成

        パラメータ
            start_pos(numpy.ndarray): 経路生成の始点
            path_plan(str): 経路生成手法
        
        戻り値
            result(bool): True/False = 経路生成に成功/失敗
        """
        # リアルタイムでのシミュレーション
        p.setRealTimeSimulation(1)

        # 把持対象物の位置を取得
        if self._n_robot_joint == DIMENTION_2D:     # 2軸ロボットアーム
            offset = self._GRASP_OBJECT_OFFSET_2DOF
            end_pos, _ = self._get_grasp_pos(offset, dim2=True)

        elif self._n_robot_joint == DIMENTION_3D:   # 3軸ロボットアーム
            offset = self._GRASP_OBJECT_OFFSET_3DOF
            end_pos, _ = self._get_grasp_pos(offset)

        else:       # 6軸ロボットアーム
            offset = self._GRASP_OBJECT_OFFSET_6DOF
            end_pos, end_ori = self._get_grasp_pos(offset)
            end_pos += end_ori
            # print(f"end_pos = {end_pos}")
            # 逆運動学(位置から関節角度へ変換) → 6軸ロボットアームは関節空間だけの対応だから
            end_pos = self._robot.convert_pos_to_theta(end_pos, force=True)

        end_pos = np.array(end_pos)

        # print(f"start_pos = {start_pos}")
        # print(f"end_theta = {end_pos}")

        # 始点と終点で干渉していないかの確認
        self._is_interference_start_end_pos(start_pos, end_pos)

        # 経路生成手法の設定
        self._set_path_plan(path_plan)

        # 経路生成の実装
        result = self._path_planning(start_pos, end_pos)

        if result:
            # 経路生成に成功
            self._post_path_planning(start_pos, end_pos)

        # ファイル保存
        self._rrt.save()

        # 経路生成後の余白時間
        # self._exec_margin_time(end_pos, np.array(offset))
        self._exec_margin_time()

        return result

    def _exec_margin_time(self):
        """
        経路生成後の余白時間の処理
        """
        # 把持対象物の位置を取得
        end_pos, end_ori = self._get_grasp_pos(offset=[0, 0, 0])
        if self._n_robot_joint == DIMENTION_2D:
            end_pos = np.array(end_pos)[:DIMENTION_2D]
            # 把持対象物への位置へ移動
            end_theta = self._robot.convert_pos_to_theta(end_pos)
        elif self._n_robot_joint == DIMENTION_3D:
            end_pos = np.array(end_pos)
            # 把持対象物への位置へ移動
            end_theta = self._robot.convert_pos_to_theta(end_pos)
        else:
            end_pos = np.array(end_pos + end_ori)
            # 把持対象物への位置へ移動
            end_theta = self._robot.convert_pos_to_theta(end_pos, force=True)
            print(f"end_pos = {end_pos}")
            print(f"end_theta = {end_theta}")

        # 経路生成後の余白時間 (即座にPyBulletが終了するのを防ぐための余白時間)
        for _ in range(self._N_MARGIN_MOVE):
            # 終点に移動
            self._robot.set_joint(end_theta)
            # グリッパーの実行
            self._robot.run_gripper(open=True)
            # 待機時間
            time.sleep(self._SIMULATION_SLEEP_TIME)

        # 把持物体の拘束を解除
        if self._grasp_constraint_id is not None:
            self._release_constraint(self._grasp_constraint_id)
            time.sleep(self._SIMULATION_SLEEP_TIME)
            self._grasp_constraint_id = None

        # 経路生成後の余白時間 (即座にPyBulletが終了するのを防ぐための余白時間)
        for _ in range(self._N_MARGIN_MOVE):
            # 終点に移動
            self._robot.set_joint(end_theta)
            # グリッパーの実行
            self._robot.run_gripper(close=True)
            # 待機時間
            time.sleep(self._SIMULATION_SLEEP_TIME)

    def _set_constraint(self, object_id, pos, ori):
        """
        拘束条件の設定

        パラメータ
            object_id(int): 拘束したい対象物ID
            pos(list): 拘束したい位置
            ori(list): 拘束したい姿勢

        戻り値
            int: 拘束条件ID
        """
        constraint_id = p.createConstraint(
                            object_id,      # 親番号(拘束したい対象物ID)
                            -1,             # 親リンクの要素番号("-1"はベース)
                            -1,             # 子番号("-1"はなし)
                            -1,             # 子リンクの要素番号("-1"はベース)
                            p.JOINT_FIXED,  # 関節タイプ(今回は固定"JOINT_FIXED")
                            [0, 0, 0],      # 関節軸
                            [0, 0, 0],      # 親の中心からの位置 
                            pos,            # 子の中心からの位置 (今回は子を設定していないから,ワールド座標系から見た関節位置)
                            parentFrameOrientation=ori,         # 親の中心からの姿勢
                            childFrameOrientation=[0, 0, 0, 1]) # 子の中心からの姿勢 (今回は,子を設定していないから,ワールド座標系から見た姿勢)

        return constraint_id

    def _release_constraint(self, constraint_id):
        """
        拘束条件の解除
        """
        if constraint_id is not None:
            # 拘束条件を解除
            p.removeConstraint(constraint_id)

    def _get_grasp_pos(self, offset=None, dim2=False):
        """
        把持対象物の位置を取得

        パラメータ
            offset(list): 把持対象物へのオフセット量 (x, y, z)
            dim2(bool): 2次元位置として取得するかどうか

        戻り値
            list: 把持対象物の位置 [m]
            list: 把持対象物の姿勢 (ロール・ピッチ・ヨー [rad])
        """
        if self._grasp_id is None:  # 把持対象物が存在しない
            raise ValueError("self._grasp_id is abnorma. please set grasp_urdf.")

        # 把持対象物の位置[m]・姿勢[rad]を取得
        grasp_pos, grasp_ori = p.getBasePositionAndOrientation(self._grasp_id)
        # 姿勢をクォータニオンからロール・ピッチ・ヨーへ変換
        roll, pitch, yaw = p.getEulerFromQuaternion(grasp_ori)
        # ピッチ角を90度回転させる → 把持対象物の正面(x方向)から把持したいから
        pitch += np.pi/2
        grasp_ori_rpy = [roll, pitch, yaw]

        if offset is not None:
            # オフセット量の考慮
            grasp_pos_offset = [pos + off for pos, off in zip(grasp_pos, offset)]

        if dim2:
            grasp_pos_offset = grasp_pos_offset[:DIMENTION_2D]

        return grasp_pos_offset, grasp_ori_rpy

    def _set_path_plan(self, path_plan):
        """
        経路生成手法の設定

        パラメータ
            path_plan(str): 経路生成手法名
        """
        if path_plan == PATHPLAN.RRT.value:
            # RRT
            self._rrt = RRTPyBullet()
        else:
            # 異常
            raise ValueError(f"path_plan is abnormal. path_plan is {path_plan}")

    def _path_planning(self, start_pos, end_pos):
        """
        始点から終点までの経路生成

        パラメータ
            start_pos(numpy.ndarray): 始点
            end_pos(numpy.ndarray): 終点

        戻り値
            result(bool): True/False = 経路生成に成功/失敗
        """
        result = False

        # 経路生成の準備
        self._rrt.preparation(start_pos, end_pos, self._interpolation)

        # 関節限界の設定
        self._set_joint_limit()

        start_time = time.time()

        # 始点から終点までの経路が生成するまでループ
        while True:
            now_time = time.time()
            if (now_time - start_time) >= self._PATH_PLAN_TIME:
                # タイムアウト
                break

            # 経路生成を1度実行
            new_node_pos, near_node_pos, near_node = self._rrt.expand_once(end_pos, self._robot.weight_joint)
            if self._is_line_interference(new_node_pos, near_node_pos):
                # 干渉あり
                continue

            print(f"new_node_pos = {new_node_pos}")
            # 干渉なしだから,ノード追加 + 経路生成の完了確認
            if not self._rrt.add_node_and_chk_goal(end_pos, new_node_pos, near_node):
                # 終点までの近傍ではない
                continue

            # 新規ノードと始点までの干渉確認
            if not self._is_line_interference(new_node_pos, end_pos):
                # 始点から終点までの経路生成に成功
                result = True
                break

        return result

    def _set_joint_limit(self):
        """
        関節限界を設定
        """
        if self._interpolation == INTERPOLATION.JOINT.value:    # 探索空間が関節空間
            # RRTの探索範囲をロボットの関節限界とする
            self._rrt.set_strict_pos(self._robot.joint_limit[:, 0], self._robot.joint_limit[:, 1])

    def _post_path_planning(self, start_pos, end_pos):
        """
        経路生成の後処理 (経路生成成功時だけ実装)

        パラメータ
            start_pos(numpy.ndarray): 経路生成の始点
            end_pos(numpy.ndarray): 経路生成の終点
        """
        # 経路生成の終了処理
        self._rrt.fin_planning(start_pos, end_pos)

        # 始点に移動
        theta = self._robot.convert_pos_to_theta(start_pos)
        self._robot.set_jump_joint(theta)
        # グリッパーの実行
        self._robot.run_gripper(open=True)

        # 始点から終点までの経路を移動
        for row_idx in range(self._rrt.pathes.shape[0]):
            next_theta = self._robot.convert_pos_to_theta(self._rrt.pathes[row_idx])
            self._robot.set_joint(next_theta)
            # グリッパーの実行
            self._robot.run_gripper(open=True)
            # 待機時間
            time.sleep(self._SIMULATION_SLEEP_TIME)
    # メイン処理 ↑


    # 干渉判定処理 ↓
    def _is_line_interference(self, pos1, pos2):
        """
        2点間の干渉判定

        パラメータ
            pos1(numpy.ndarray): 位置1
            pos2(numpy.ndarray): 位置2

        戻り値
            is_interference(bool): True/False = 干渉あり/干渉なし
        """
        is_interference = True

        # 2点の干渉判定
        if self._is_interference_pos(pos2):
            return is_interference
        if self._is_interference_pos(pos1):
            return is_interference

        # pos1からpos2へ移動
        theta = self._robot.convert_pos_to_theta(pos2)
        self._robot.set_joint(theta)
        # グリッパーの実行
        self._robot.run_gripper(open=True)

        # 待機時間
        time.sleep(self._SIMULATION_SLEEP_TIME)

        # ロボットと干渉物との干渉判定
        close_points = p.getClosestPoints(self._robot_id, self._environment_id, self._INTERFERENCE_MARGIN)
        if len(close_points) == 0:  # 干渉なし
            is_interference = False

        return is_interference

    def _is_interference_start_end_pos(self, start_pos, end_pos):
        """
        始点と終点が干渉判定していないかの確認

        パラメータ
            start_pos(numpy.ndarray): 経路生成の始点 (直交空間/関節空間)
            end_pos(numpy.ndarray): 経路生成の終点 (直交空間/関節空間)
        """
        # 始点の干渉判定
        if self._is_interference_pos(start_pos):
            raise ValueError("start_pos is interference. change start_pos.")

        # 終点の干渉判定
        if self._is_interference_pos(end_pos):
            raise ValueError("end_pos is interference. change end_pos.")

    def _is_interference_pos(self, pos):
        """
        位置にジャンプして干渉判定

        パラメータ
            pos(numpy.ndarray): 位置/関節
        
        戻り値
            is_interference(bool): True/False = 干渉あり/干渉なし
        """
        is_interference = True

        # 位置から関節角度に変換
        theta = self._robot.convert_pos_to_theta(pos)
        # 位置にジャンプ
        self._robot.set_jump_joint(theta)
        # グリッパーの実行
        self._robot.run_gripper(open=True)

        # 待機時間
        time.sleep(self._SIMULATION_SLEEP_TIME)

        # ロボットと干渉物との干渉判定
        close_points = p.getClosestPoints(self._robot_id, self._environment_id, self._INTERFERENCE_MARGIN)
        if len(close_points) == 0:  # 干渉なし
            is_interference = False

        return is_interference
    # 干渉判定処理 ↑


    # 運動学関連 ↓
    def convert_pos_to_theta(self, pos):
        """
        位置から関節角度に変換

        パラメータ
            pos(numpy.ndarray): 位置 / 関節角度

        戻り値
            thetas(numpy.ndarray): 関節角度
        """
        thetas = self._robot.convert_pos_to_theta(pos, force=True)

        return thetas
    # 運動学関連 ↑

6軸ロボットアームでは関節空間を探索するため,関節空間でも探索できるように修正した.
6軸ロボットアームのクラス(PyBullet6DoFRobot)を作成した.
上ソースコードの大方は前記事にて説明しているため,説明した内容は割愛する.
(前記事 https://qiita.com/haruhiro1020/items/d26009d50be190d97368)

経路生成手法であるRRT (pybullet_rrt.py)

経路生成手法であるRRTの処理を定義する pybullet_rrt.py について説明する.
RRT内ではツリーの作成を実施する.干渉判定はPyBulletで実施するため,RRT内には干渉判定を実装しない.

pybullet_rrt.py
# 経路生成手法であるRRT (Rapidly-exploring Random Tree) の実装 (PyBullet用)


# ライブラリの読み込み
import numpy as np
import os


# 自作モジュールの読み込み
from constant import *              # 定数



class Tree:
    """
    ツリークラス

    プロパティ
        _nodes(numpy.ndarray): ノード
        _near_node_idx(int): _nodes内の最短ノードを保存している列番号

    メソッド
        public
            nodes(): _nodesプロパティのゲッター
            reset(): データの初期化
            add_node(): ノードの追加
            get_near_node(): 最短距離のノードを取得
            get_near_node_list(): ノードと近傍ノードをリストで取得

        protected
            _chk_node_exist(): ノードが存在するかの確認
    """
    # 定数の定義


    def __init__(self, near_node_idx):
        """
        コンストラクタ
        """
        # プロパティの初期化
        self._nodes = []
        self._near_node_idx = near_node_idx

    @property
    def nodes(self):
        """
        _nodesプロパティのゲッター
        """
        return self._nodes

    def reset(self):
        """
        データの初期化
        """
        if len(self._nodes) != 0:
            # 何かしらのデータが保存
            del self._nodes
        self._nodes = []

    def add_node(self, node):
        """
        ノードの追加

        パラメータ
            node(numpy.ndarray): ノード
        """
        if len(self._nodes) == 0:       # 初回だけ実行
            self._nodes = node
        else:
            self._nodes = np.append(self._nodes, node, axis=0)

    def _chk_node_exist(self):
        """
        ノードが存在するかの確認
        """
        if len(self._nodes) == 0:
            # 存在しない
            raise ValueError("self._nodes is not exist")

    def get_near_node(self, pos, weight):
        """
        最短距離のノードを取得

        パラメータ
            pos(numpy.ndarray): 位置
            weight(numpy.ndarray): 各次元の重み

        戻り値
            min_dist_idx(int): 最短距離のノード番号
        """
        # ノードの存在確認
        self._chk_node_exist()

        # ノードから位置を取得
        nodes_pos = self._nodes[:, :self._near_node_idx]
        # 差分を計算
        difference = nodes_pos - pos

        # 修正 ↓
        # 差分に重みを考慮
        if weight is None:
            weight = 1
        difference = difference * weight
        # 修正 ↑

        # 距離を計算 (各ノードとの距離を算出するため,引数にaxis=1を与えた)
        distance = np.linalg.norm(difference, axis=1)
        # 最短距離ノードを取得
        min_dist_idx = np.argmin(distance)

        return min_dist_idx

    def get_near_node_list(self, pos, radius):
        """
        ノードと近傍ノードをリストで取得

        パラメータ
            pos(numpy.ndarray): ノード位置
            radius(float): 半径

        戻り値
            near_node_list(list): 近傍ノードリスト
        """
        # ノードの存在確認
        self._chk_node_exist()

        near_node_list = []
        # ツリー内全ノード位置を取得
        all_node_pos = self._nodes[:, :self._near_node_idx]
        # ノードとツリー内全ノードの差分を計算
        difference = all_node_pos - pos
        # 差分から距離(ユークリッド距離)を計算
        distance = np.linalg.norm(difference, axis=1)

        # 距離が一定以内のノードだけを保存
        near_node_list = [idx for idx, dist in enumerate(distance) if dist <= radius]

        return near_node_list


class RRTPyBullet:
    """
    RRTにロボットを追加したクラス

    プロパティ
        _name(str): 経路生成手法名
        _pathes(numpy.ndarray): 始点から終点までの経路 (PruningやShortcut済みの経路)
        _start_tree(Tree): 始点ツリー
        _strict_min_pos(numpy.ndarray): 探索の最小範囲
        _strict_max_pos(numpy.ndarray): 探索の最大範囲
        _moving_value(float): 1回あたりの移動量 [rad] or [m]
        _before_modify_pathes(numpy.ndarray): PruningやShortcutなどの修正前の経路
        _pruning_pathes(numpy.ndarray): Pruning後の経路
        _shortcut_pathes(numpy.ndarray): Shortcut後の経路
        _partial_shortcut_pathes(numpy.ndarray): Partial Shortcut後の経路

    メソッド
        public

            プロパティのゲッター関連
                name(): _nameプロパティのゲッター
                pathes(): _pathesプロパティのゲッター

            準備処理
                preparation(): 経路生成の準備
                set_strict_pos(): 探索範囲を設定

            メイン処理関連
                expand_once(): ランダム探索による新規ノードおよび最短ノードの取得 (干渉判定は実装しない)
                add_node_and_check_goal(): ノード追加 + 経路生成の完了確認
                fin_planning(): 経路生成の終了処理

            ファイル関連
                save(): 生成した経路をファイル保存

        protected

            メイン処理関連
                _add_node_start_tree(): 始点ツリーにノードを追加
                _chk_end_pos_dist(): 終点との距離が一定範囲内であるかの確認
                _calc_new_pos(): 最短ノードからランダムな値方向へ新しいノード(位置)を作成
                _get_random_pos(): ランダムな位置を取得

            準備処理関連
                _reset(): データの初期化
                _set_interpolation(): 経路生成したい探索空間の設定
                _strict_planning_pos(): 探索範囲を制限する

            ファイル関連
                _make_folder(): フォルダーの作成
                _reset_folder(): フォルダー内のファイルを全削除
                _get_path_plan_folders(): 経路生成結果を保存する複数のフォルダー名を取得
                _make_path_plan_folder(): 経路生成結果を保存するフォルダー作成
                _save_numpy_data_to_txt(): Numpyデータをテキストファイルに保存
    """
    # 定数の定義
    # ファイル名の定義
    # 各経路生成手法で絶対に定義するべきファイル名 ↓
    # _pathesプロパティを保存するファイル名
    _FILE_NAME_PATHES = f"{PATHPLAN.RRT.value}_pathes.csv"
    # _start_treeプロパティを保存するファイル名
    _FILE_NAME_START_TREE = f"{PATHPLAN.RRT.value}_start_tree.csv"
    # 各経路生成手法で絶対に定義するべきファイル名 ↑

    # ツリーの要素番号を定義
    _NODE_NEAR_NODE_IDX = RRT_NEAR_NODE_IDX     # ノード内の最短ノード要素

    # 探索に関する定義
    _MOVING_VALUE_JOINT = 0.1      # 1回の移動量 [rad] (ロボットの関節空間)
    _MOVING_VALUE_POS = 0.2         # 1回の移動量 [m] (ロボットの位置空間)
    _STRICT_PLANNING_ROB_JOINT = np.pi / 2  # 探索範囲の制限 [rad] (ロボットの関節空間)
    _STRICT_PLANNING_ROB_POS = 1.0  # 探索範囲の制限 [m] (ロボットの位置空間)

    _GOAL_SAMPLE_RATE = 0.1     # ランダムな値を取るときに,終点を選択する確率


    def __init__(self):
        """
        コンストラクタ
        """
        self._name   = PATHPLAN.RRT.value
        self._pathes = []
        self._start_tree    = Tree(self._NODE_NEAR_NODE_IDX)
        self._interpolation = INTERPOLATION.NONE.value
        self._moving_value  = self._MOVING_VALUE_JOINT
        self._dim = DIMENTION_NONE


    # プロパティのゲッターメソッド ↓
    @property
    def name(self):
        """
        _nameプロパティのゲッター
        """
        return self._name

    @property
    def pathes(self):
        """
        _pathesプロパティのゲッター
        """
        return self._pathes
    # プロパティのゲッターメソッド ↑


    # メイン処理メソッド ↓
    def expand_once(self, end_pos, weight=None):
        """
        ランダム探索による新規ノードおよび最短ノードの取得 (干渉判定は実装しない)

        パラメータ
            end_pos(list): 経路生成の終点
            weight(numpy.ndarray): 各次元の重み

        戻り値
            new_node_pos(numpy.ndarray): 新規ノード
            near_node_pos(numpy.ndarray): 最短ノード位置
            near_node(int): 最短ノード番号
        """
        # ランダムな値を取得
        random_pos = self._get_random_pos(end_pos)
        # ランダムな値と最短ノードを計算
        near_node  = self._start_tree.get_near_node(random_pos, weight)
        # 最短ノードの位置
        near_node_pos = self._start_tree.nodes[near_node, :self._NODE_NEAR_NODE_IDX]
        # 最短ノードからランダムな値方向へ新しいノード(位置)を作成
        new_node_pos  = self._calc_new_pos(random_pos, near_node_pos, weight)

        return new_node_pos, near_node_pos, near_node

    def add_node_and_chk_goal(self, end_pos, node_pos, near_node):
        """
        ノード追加 + 経路生成の完了確認

        パラメータ
            end_pos(numpy.ndarray): 経路生成の終点
            node_pos(numpy.ndarray): ノード位置
            near_node(int): 親ノード

        戻り値
            is_successful(bool): True/False = 経路生成の完了/未完了
        """
        # 処理結果
        is_successful = False

        # 始点ツリーにノードを追加
        self._add_node_start_tree(node_pos, near_node)

        # 終点との距離が一定範囲内であるかの確認
        if self._chk_end_pos_dist(node_pos, end_pos):
            # 一定範囲内のため,経路生成の完了
            is_successful = True

        return is_successful

    def _add_node_start_tree(self, pos, near_node):
        """
        始点ツリーにノードを追加

        パラメータ
            pos(numpy.ndarray): 位置
            near_node(int): 最短ノード
        """
        # _start_treeにノードを追加
        node = np.append(pos, near_node).reshape(1, -1)
        self._start_tree.add_node(node)

    def _chk_end_pos_dist(self, pos, end_pos):
        """
        終点との距離が一定範囲内であるかの確認

        パラメータ
            pos(numpy.ndarray): ノード位置
            end_pos(numpy.ndarray): 経路生成の終点

        戻り値
            is_near(bool): True / False = 一定範囲内である / でない
        """
        is_near = False
        # 距離を計算
        dist = np.linalg.norm(end_pos - pos)
        # 一定範囲内であるかの確認
        if dist <= self._moving_value:
            is_near = True

        return is_near

    def fin_planning(self, start_pos, end_pos):
        """
        経路生成の終了処理

        パラメータ
            start_pos(list): 経路生成の始点
            end_pos(list): 経路生成の終点
        """
        # 始点から終点までの経路に関係するノードを選択
        revers_path = end_pos.reshape(1, -1)
        near_node   = -1

        while True:
            # 終点から始点方向へノードを取得
            node = self._start_tree.nodes[near_node]
            pos  = node[:self._NODE_NEAR_NODE_IDX].reshape(1, -1)
            # 浮動小数型になっているので,整数型に型変換
            near_node   = int(node[self._NODE_NEAR_NODE_IDX])
            revers_path = np.append(revers_path, pos, axis=0)
            if near_node == INITIAL_NODE_NEAR_NODE:
                # 始点ノードまで取得できたため,処理終了
                break

        # 経路が終点からの順番になっているため,始点から終点とする
        self._pathes = revers_path[::-1]
        # ツリーに終点を追加 (要素番号を指定するため -1)
        self._add_node_start_tree(end_pos, self._start_tree.nodes.shape[0] - 1)

    def _calc_new_pos(self, random_pos, near_node_pos, weight):
        """
        最短ノードからランダムな値方向へ新しいノード(位置)を作成

        パラメータ
            random_pos(numpy.ndarray): ランダムな位置
            near_node_pos(numpy.ndarray): 最短ノード位置
            weight(numpy.ndarray): 各次元の重み

        戻り値
            new_pos(numpy.ndarray): 新しいノード
        """
        # 方向を計算
        direction = random_pos - near_node_pos

        # 修正後 ↓
        # 重みの計算
        if weight is None:
            weight = 1
        weighted_direction = direction * weight
        norm_direction = weighted_direction / (np.linalg.norm(weighted_direction) + EPSILON)
        # 修正後 ↑

        # 新しいノードを作成
        new_pos = near_node_pos + norm_direction * self._moving_value

        return new_pos

    def _get_random_pos(self, target_pos):
        """
        ランダムな位置を取得

        パラメータ
            target_pos(numpy.ndarray): 目標点

        戻り値
            random_pos(numpy.ndarray): ランダムな位置
        """
        # 乱数を取って,目標点を選択するかランダムを選択するか
        select_goal = np.random.rand()
        if select_goal < self._GOAL_SAMPLE_RATE:
            # 目標点を選択s
            random_pos = target_pos
        else:
            random_pos = np.random.uniform(self._strict_min_pos, self._strict_max_pos)

        return random_pos
    # メイン処理メソッド ↑


    # 準備処理関連メソッド ↓
    def preparation(self, start_pos, end_pos, interpolation):
        """
        経路生成の準備

        パラメータ
            start_pos(list): 経路生成の始点
            end_pos(list): 経路生成の終点
            interpolation(int): 補間方法 (関節空間/位置空間)
        """
        # データの初期化
        self._reset()

        # 始点と終点の次元数が一致しているかの確認
        start_pos_dim = np.size(start_pos)
        end_pos_dim   = np.size(end_pos)
        if start_pos_dim != end_pos_dim:
            # 次元数が異なるので異常
            raise ValueError(f"start_pos_dim and end_pos_dim are not matched. start_pos_dim is {start_pos_dim}, end_pos_dim is {end_pos_dim}")

        self._dim = start_pos_dim

        # 探索空間の設定
        self._set_interpolation(interpolation)

        # 始点ノードをツリーに追加
        self._add_node_start_tree(start_pos, INITIAL_NODE_NEAR_NODE)

        # 探索範囲を設定
        self._strict_planning_pos(start_pos, end_pos)

        # 結果を保存するフォルダ作成
        self._make_path_plan_folder(interpolation)

        # フォルダー内のファイルを全部削除
        self._reset_folder(interpolation)

    def set_strict_pos(self, min_pos, max_pos):
        """
        探索範囲の設定

        パラメータ
            min_pos(numpy.ndarray): 探索の最小範囲
            max_pos(numpy.ndarray): 探索の最大範囲
        """
        # パラメータの要素数確認
        if min_pos.shape[0] != max_pos.shape[0]:
            raise ValueError(f"min_pos'shape and max_pos'shape are not match. min_pos'shape is {min_pos.shape}. max_pos'shape is {max_pos.shape}")
        if min_pos.shape[0] != self._strict_min_pos.shape[0]:
            raise ValueError(f"min_pos'shape and _strict_min_pos'shape are not match. min_pos'shape is {min_pos.shape[0]}. _strict_min_pos'shape is {self._strict_min_pos.shape[0]}")

        # プロパティの更新
        self._strict_min_pos = min_pos
        self._strict_max_pos = max_pos

    def _reset(self):
        """
        データの初期化
        """
        self._start_tree.reset()

        if len(self._pathes) != 0:
            # 何かしらのデータが保存
            del self._pathes
        self._pathes = []

        self._interpolation = INTERPOLATION.NONE.value
        self._dim = DIMENTION_NONE

    def _set_interpolation(self, interpolation):
        """
        経路生成したい探索空間の設定

        パラメータ
            interpolation(int): 補間の種類 (関節補間/位置補間)
        """
        if interpolation == INTERPOLATION.POSITION.value:
            # 位置空間
            self._moving_value = self._MOVING_VALUE_POS
        elif interpolation == INTERPOLATION.JOINT.value:
            # 関節空間
            self._moving_value = self._MOVING_VALUE_JOINT
        else:
            # 異常値
            raise ValueError(f"interpolation is abnormal. interpolation is {interpolation}")

        # 補間種類の更新
        self._interpolation = interpolation

    def _strict_planning_pos(self, start_pos, end_pos):
        """
        探索範囲を制限する

        パラメータ
            start_pos(numpy.ndarray): 始点
            end_pos(numpy.ndarray): 終点
        """
        all_pos = np.array([start_pos, end_pos])
        # 各列の最大/最小値を取得
        min_pos = np.min(all_pos, axis=0)
        max_pos = np.max(all_pos, axis=0)

        if self._interpolation == INTERPOLATION.POSITION:
            # 位置空間の探索
            strict_planning_pos = self._STRICT_PLANNING_ROB_POS
        else:
            # 関節空間の探索
            strict_planning_pos = self._STRICT_PLANNING_ROB_JOINT

        self._strict_min_pos = min_pos - strict_planning_pos
        self._strict_max_pos = max_pos + strict_planning_pos
        # print(f"self._strict_min_pos = {self._strict_min_pos}")
        # print(f"self._strict_max_pos = {self._strict_max_pos}")
    # 準備処理関連メソッド ↑


    # ファイル関連メソッド ↓
    def _make_folder(self, folder_name):
        """
        フォルダーの作成

        パラメータ
            folder_name(str): 作成したいフォルダー名
        """
        # フォルダーが作成済みでもエラーを出力しないよう,exist_ok=Trueとした.
        os.makedirs(folder_name, exist_ok=True)

    def _reset_folder(self, interpolation):
        """
        フォルダー内のファイルを全削除

        パラメータ
            interpolation(str): 探索方法 (位置空間/関節空間)
        """
        # フォルダー名を取得
        folder_names = self._get_path_plan_folders(interpolation)
        for folder_name in folder_names:
            for entry in os.listdir(folder_name):
                full_path = os.path.join(folder_name, entry)
                if os.path.isfile(full_path) or os.path.islink(full_path):
                    os.remove(full_path)         # 通常ファイル・シンボリックリンクを削除

    def _get_path_plan_folders(self, interpolation):
        """
        経路生成結果を保存する複数のフォルダー名を取得

        パラメータ
            interpolation(str): 探索方法 (位置空間/関節空間)

        戻り値
            folder_names(list): 複数のフォルダー名
        """
        folder_names = [os.path.join(self._name, interpolation), ]

        return folder_names

    def _make_path_plan_folder(self, interpolation):
        """
        経路生成結果を保存するフォルダー作成

        パラメータ
            interpolation(str): 探索方法 (位置空間/関節空間)
        """
        # フォルダー名を取得
        folder_names = self._get_path_plan_folders(interpolation)
        for folder_name in folder_names:
            self._make_folder(folder_name)

    def save(self):
        """
        生成した経路をファイル保存
        """
        # 始点から終点までの修正済みの経路をファイル保存
        self._save_numpy_data_to_txt(self._pathes, self._FILE_NAME_PATHES)
        # 始点のツリーを保存
        self._save_numpy_data_to_txt(self._start_tree.nodes, self._FILE_NAME_START_TREE)

    def _save_numpy_data_to_txt(self, data, file_name):
        """
        Numpyデータをテキストファイルに保存

        パラメータ
            data(numpy.ndarray): ファイル保存したいデータ
            file_name(str): 保存したいファイル名
        """
        # 引数の確認
        if len(data) == 0:
            # データが存在しないため,処理終了
            return

        if not file_name:
            # ファイル名が存在しないため,処理終了
            return

        # ファイル名にフォルダ名を追加 (各経路生成手法で異なるフォルダにデータを保存)
        full_path = f"{self._name}/{self._interpolation}/{file_name}"

        np.savetxt(full_path, data)
    # ファイル関連メソッド ↑

ロボットアームの関節限界を超えて欲しくないから,新規で"set_strict_pos()"を作成して,上位から探索範囲を設定できるようにした.
RRT内で干渉判定を実施できないため,RRTクラスの実装側で干渉判定の結果に応じて,処理を実装できるようにpubic関数を用意した.
RRTクラスの実装側で,呼んでほしくない処理はprotected関数として,public関数を最小限とした.
今回は,RRTによる経路生成だが将来的にはRRTの拡張アルゴリズムを使用する予定である.
RRTに関しては,私の他記事内やコメントを記載したため,各関数についての説明は割愛する.

PyBulletでロボットを動かす

上記にて,ソースコードを説明した.
main.py ファイルを実施することによって,PyBullet上のロボットを動かしていく.
直交空間で探索した時のロボットの動画を下図に2つ載せる.

動画1:始点から終点まで干渉しない経路を生成するための探索
PyBulletと6軸ロボットアーム(RRT)_グリッパ付き_把持_経路探索.gif

動画2:探索後の始点から終点までの経路 + 物体把持
PyBulletと6軸ロボットアーム(RRT)_グリッパ付き_把持_物体把持.gif

把持したい物体まで,干渉しない経路を作成して,物体把持ができることを確認した.物体把持まで自動化できたため,次なる目標は下記の通りとなる.
目標1:カメラを付けて,把持したい物体の位置を取得
目標2:物体把持後に他の場所へ物体を移動 (複数本の経路を生成)
目標3:強化学習の実装

おわりに

本記事では,Pythonを使用して,下記内容を実装しました
・PyBullet で6軸ロボットアームにグリッパーを追加
・PyBullet で干渉物が存在する環境下で,6軸ロボットアームの経路生成と把持 (経路生成手法としてRRTを採用)

次記事では,下記内容を実装していきます.
・PyBullet で2軸ロボットアームにカメラを追加
・PyBullet で物体把持の経路生成手法を"RRT"から"RRT-Connect"や"RRT*"に変更
(RRT-Connectの記事 https://qiita.com/haruhiro1020/items/c09508eee2761ff9380a)
・Unity で2軸ロボットアームを可視化して,関節を動かす
・今までの記事をPythonからC++に変換

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?