dwb local planner 1

dwb local plannerの実装を理解していきます。

1997年のDynamic Window Approach(DWA)という論文がProbabilistic Roboticsの著者達によって提案され、それはROS1で実装され、その後にROS2/nav2となり機能をpluginとして交換しやすくした実装をdwbと呼ぶようです。

local plannerですね。

理論の前にRVizでlocal plannerを表示させて、ヒョコヒョコというか、ピロピロというか、出てるやつを確認しておくと理解が早いと思います。
通常であれば/local_planです。RVizで[Add]で[By topic]で見つかります。おそらく濃い青い線で表示されます。

自分で考える

source code解析をする前に自分で作ることを考えるとします。

詳解確率ロボティクスのPythonをお手本として回転のみでgoalを目指す実装をします。
障害物回避は後でやります。

詳解確率ロボティクスのPythonはrobotの動作がわかりやすく拡張もやりやすいので利用させていただきます。

整理する

星はgoalを表します。
robot(中心)とgoal(中心)で線を引きrobotの向きとの差が1度未満であれば前進する仕様とします。

001
002
003

robot座標から見てgoalがy<-1(deg)であれば時計回りとします。

004
005

robot座標から見てgoalが1(deg)<yであれば反時計回りとします。

006
007

robot座標から見てgoalが(中心)とgoal(中心)で線を引きrobotの向きとの差が179度より大きいまたは-179度より小さい場合は時計回りの仕様とします。

008
009
010

output

goalに到着の条件を実装していないためgoalを目指し続けます。

実装

実装です。

dwb_local_planner_001.cpp

import matplotlib
# matplotlib.use('nbagg')
import matplotlib.animation as anm
import matplotlib.pyplot as plt
import math
from math import sin, cos, degrees
import matplotlib.patches as patches
import numpy as np
import random
import time

# ------------------------------------------------------------------------------

PI = 3.141592
DEGREE_1 = PI/180
DEGREE_179 = DEGREE_1*179
ANGULAR_VELOCITY_MAX = 0.1

class World:
    def __init__( self, time_span, time_interval ):
        self.objects = []
        self.time_span = time_span
        self.time_interval = time_interval
        self.file_name_counter = 0

    def append( self,obj ):
        self.objects.append(obj)

    def draw(self):
        fig = plt.figure(figsize=(8,8)) # fig = plt.figure(figsize=(4,4))
        ## make axes
        ax = fig.add_subplot(111)
        ax.set_aspect('equal')
        ax.set_xlim(-10,10)
        ax.set_ylim(-10,10)
        ax.set_xlabel("X",fontsize=10)
        ax.set_ylabel("Y",fontsize=10)

        elems = []

        tmp_1 = int( self.time_span/self.time_interval ) + 1
        tmp_2 = int( self.time_interval )
        self.ani = anm.FuncAnimation(fig, self.one_step, fargs=(elems, ax), frames=tmp_1, interval=tmp_2, repeat=False)
        plt.show()

    def one_step(self, i, elems, ax):
        while elems: elems.pop().remove()
        time_str = "t = %.2f[s]" % (self.time_interval*i)
        elems.append(ax.text(-4.4, 4.5, time_str, fontsize=10))
        ##
        elems.append(ax.text(-4.4, 4.5, time_str, fontsize=10))
        for obj in self.objects:
            obj.draw(ax, elems)
            if hasattr(obj, "one_step"): obj.one_step(self.time_interval)

# ------------------------------------------------------------------------------

class Map:
    def __init__(self):
        self.landmarks = []
        self.goal = []

    def append_landmark(self, landmark):
        landmark.id = len(self.landmarks)
        self.landmarks.append(landmark)

    def append_goal( self, goal ):
        self.goal.append( goal )

    def draw( self, ax, elems ):
        for goal in self.goal: goal.draw(ax, elems)

# ------------------------------------------------------------------------------

class Goal:
    def __init__( self, x, y ):
        self.pos = np.array([x, y]).T
        self.id = None

    def draw( self, ax, elems ):
        c = ax.scatter(self.pos[0], self.pos[1], s=100, marker="*", label="goal", color="red")
        elems.append(c)
        elems.append(ax.text(self.pos[0], self.pos[1], "id:" + str(self.id), fontsize=10))

# ------------------------------------------------------------------------------

## x : robot position x
## y : robot position y
## yaw : robot position yaw

class Robot:
    def __init__( self, pose, nu, omega, goal, color="black" ):
        self.pose = pose
        self.r = 0.2
        self.color = color
        self.poses = [pose]
        self.nu = nu
        self.omega = omega
        self.goal = goal
        self.angle = 0.0
        self.diff_angle = 0.0
        self.g_x_in_robot_flame = 0.0
        self.g_y_in_robot_flame = 0.0
        self.tmp_x = 0.0
        self.tmp_y = 0.0

    def draw(self, ax, elems):
        x, y, yaw = self.pose

        xn = x + self.r * math.cos(yaw)
        yn = y + self.r * math.sin(yaw)
        ##
        elems += ax.plot([x,xn], [y,yn], color=self.color)
        ## robot's orientation
        ax.plot([x,xn], [y,yn], color=self.color)
        c = patches.Circle(xy=(x, y), radius=self.r, fill=False, color=self.color)
        elems.append(ax.add_patch(c))
        elems.append(ax.text( 2, -2, f'g_x - x: {self.tmp_x:.3f}', size=15 ))
        elems.append(ax.text( 2, -3, f'g_y - y: {self.tmp_y:.3f}', size=15 ))
        elems.append(ax.text( 2, -4, f'g_x_in_robot_flame: {self.g_x_in_robot_flame:.3f}', size=15 ))
        elems.append(ax.text( 2, -5, f'g_y_in_robot_flame: {self.g_y_in_robot_flame:.3f}', size=15 ))
        elems.append(ax.text( 2, -6, f'angle: {degrees(self.angle):.3f}', size=15 ))
        elems.append(ax.text( 2, -7, f'yaw: {degrees(self.pose[2]):.3f}', size=15 ))
        elems.append(ax.text( 2, -8, f'diff_angle: {degrees(self.diff_angle):.3f}', size=15 ))
        elems.append(ax.text( 2, -9, f'omega: {self.omega:.3f}', size=15 ))

        self.poses.append(self.pose)
        elems += ax.plot([e[0] for e in self.poses], [e[1] for e in self.poses], linewidth=0.5, color="black")

    @classmethod
    def state_transition(cls, nu, omega, time, pose):
        t0 = pose[2]
        if math.fabs(omega) < 1e-10:
            return pose + np.array( [nu*math.cos(t0),
                                     nu*math.sin(t0),
                                     omega ] ) * time
        else:
            return pose + np.array( [nu/omega*(math.sin(t0 + omega*time) - math.sin(t0)),
                                     nu/omega*(-math.cos(t0 + omega*time) + math.cos(t0)),
                                     omega*time ] )

    def one_step(self, time_interval):
        # nu, omega = self.run()
        nu, omega = self.dwa()
        self.pose = self.state_transition( nu, omega, time_interval, self.pose )

    def run( self ):
        return self.nu, self.omega

    def go_stragiht( self ):
        return self.nu, self.omega

    def dwa( self ):
        ## robot pose in world flame
        x, y, yaw = self.pose
        ## 0(deg) <= yaw < 360(deg)
        if (2*PI) <= yaw:
            yaw = yaw - 2*PI
        if yaw < 0:
            yaw = yaw + 2*PI
        self.pose[2] = yaw
        ## goal in world flame
        goal = self.goal
        g_x = goal.pos[0]
        g_y = goal.pos[1]
        print(f'x: {g_x},y: {g_y}')
        ## gradient = slope
        angle = math.atan2( g_y - y, g_x - x )
        ## 0(deg) <= angle < 360(deg)
        if angle < 0:
            angle = angle + 2*PI
        self.angle = angle
        print(f'robot yaw: {yaw}')
        print(f'angle (rad): {angle}')
        print(f'angle (deg): {math.degrees(angle)}')
        print(f'robot yaw - angle difference: {math.degrees(yaw - angle)}')
        ## goal in robot flame
        tmp_x = g_x - x
        tmp_y = g_y - y
        g_x_in_robot_flame = tmp_x * cos(-yaw) - tmp_y * sin(-yaw)
        g_y_in_robot_flame = tmp_x * sin(-yaw) + tmp_y * cos(-yaw)
        ## for debug
        self.g_x_in_robot_flame = g_x_in_robot_flame
        self.g_y_in_robot_flame = g_y_in_robot_flame
        self.tmp_x = tmp_x
        self.tmp_y = tmp_y

        diff_angle = angle - yaw
        self.diff_angle = diff_angle

        ## the goal is on the left
        if 0 < g_y_in_robot_flame:
            if abs(diff_angle) < DEGREE_1:
                ## move forward
                change_omega = 0
            else:
                change_omega = abs(diff_angle) * 0.1

        ## the goal is on the right
        if g_y_in_robot_flame < 0:
            if DEGREE_179 < abs(diff_angle) and abs(diff_angle) < DEGREE_181:
                ## Force clockwise
                change_omega = ANGULAR_VELOCITY_MAX
            else:
                change_omega = - abs(diff_angle) * 0.1

        self.omega = change_omega
        return self.nu, self.omega

# ------------------------------------------------------------------------------

if __name__ == '__main__':
    ## 0.01 : very slow
    # world = World( 15, 0.1 )
    # world = World( 120, 0.1 )
    world = World( 200, 0.2 )
    # world = World( 120, 0.04 )
    # world = World( 120, 0.01 )

    m = Map()
    goal = Goal(4,8)
    m.append_goal(goal)
    world.append( m )

    robot = Robot( np.array([0, 0, 0]).T, nu=0.2, omega=0.0, goal=goal )
    # robot = Robot( np.array([0, 0, 1]).T, nu=0.2, omega=0.0, goal=goal )
    # robot = Robot( np.array([0, 0, 0]).T, nu=1.0, omega=1.0, goal=goal )

    world.append( robot )
    world.draw()

準備。

PI = 3.141592
DEGREE_1 = PI/180
DEGREE_179 = DEGREE_1*179
ANGULAR_VELOCITY_MAX = 0.1

debug.

        elems.append(ax.text( 2, -2, f'g_x - x: {self.tmp_x:.3f}', size=15 ))
        elems.append(ax.text( 2, -3, f'g_y - y: {self.tmp_y:.3f}', size=15 ))
        elems.append(ax.text( 2, -4, f'g_x_in_robot_flame: {self.g_x_in_robot_flame:.3f}', size=15 ))
        elems.append(ax.text( 2, -5, f'g_y_in_robot_flame: {self.g_y_in_robot_flame:.3f}', size=15 ))
        elems.append(ax.text( 2, -6, f'angle: {degrees(self.angle):.3f}', size=15 ))
        elems.append(ax.text( 2, -7, f'yaw: {degrees(self.pose[2]):.3f}', size=15 ))
        elems.append(ax.text( 2, -8, f'diff_angle: {degrees(self.diff_angle):.3f}', size=15 ))
        elems.append(ax.text( 2, -9, f'omega: {self.omega:.3f}', size=15 ))

robot(中心)とgoal(中心)で線を引きworld座標系でのその傾きを計算。変数名は良くはない。

        angle = math.atan2( g_y - y, g_x - x )

robot座標系でのgoalの位置。world座標系から計算。
回転の説明はこちら。

回転

        ## goal in robot flame
        tmp_x = g_x - x
        tmp_y = g_y - y
        g_x_in_robot_flame = tmp_x * cos(-yaw) - tmp_y * sin(-yaw)
        g_y_in_robot_flame = tmp_x * sin(-yaw) + tmp_y * cos(-yaw)

最初に図で説明した内容を実装。0.1は動作を見やすくするために適当な係数を付けているだけです。

        ## the goal is on the left
        if 0 < g_y_in_robot_flame:
            if abs(diff_angle) < DEGREE_1:
                ## move forward
                change_omega = 0
            else:
                change_omega = abs(diff_angle) * 0.1

        ## the goal is on the right
        if g_y_in_robot_flame < 0:
            if DEGREE_179 < abs(diff_angle):
                ## Force clockwise
                change_omega = ANGULAR_VELOCITY_MAX
            else:
                change_omega = - abs(diff_angle) * 0.1

広告

IT開発関連書とビジネス書が豊富な翔泳社の通販『SEshop』
さくらのレンタルサーバ
ムームードメイン
Oisix(おいしっくす)
らでぃっしゅぼーや
珈琲きゃろっと
エプソムソルト




«       »