dwb local plannerの実装を理解していきます。
自分で考える
source code解析をする前に自分で作ることを考えるとします。
前回、詳解確率ロボティクスのPythonをお手本として回転のみでgoalを目指す実装をしました。
障害物回避を実装します。
詳解確率ロボティクスのPythonはrobotの動作がわかりやすく拡張もやりやすいので利用させていただきます。
整理する
遠くにある障害物は無視します。
通ろうとしていた経路の近くの衝突してしまう障害物に対して少し余裕を持って避ける動作を実装します。
output
障害物を回避します。
goalに到着の条件を実装していないためgoalを目指し続けます。
自分の大きさ、形状を考慮していません。次の記事からはnav2の解析をします。その後に自分の大きさを考慮した実装にします。
sensorでobstacleの位置を把握するところまで実装すればobstacleの位置をworld座標系からrobot座標系にするという変換を削除できます。そこまでやれる時間あるかなー。
実装
実装です。
dwb_local_planner_002.cpp
import matplotlib
# matplotlib.use('nbagg')
import matplotlib.animation as anm
import matplotlib.pyplot as plt
import math
from math import sin, cos, degrees, sqrt
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
COEFF = 0.2
COEFF_OB = 1.0
# ------------------------------------------------------------------------------
## x : robot position x
## y : robot position y
## yaw : robot position yaw
class Robot:
def __init__( self, pose, nu, omega, goal, obstacles, 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
self.obstacles = obstacles
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)
## atan2 return value: -PI < angle <= PI
# if (2*PI) <= angle:
# angle = angle - 2*PI
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
# ------------------------------------------------------------------------------
# obstacles in world flame
obstacles = self.obstacles
# ------------------------------------------------------------------------------
# determine omega towards the goal
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) * COEFF
## 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) * COEFF
# ------------------------------------------------------------------------------
# move around obstacle
# closest ranking
dist_ob = []
ob_in_robot_flame = []
for obstacle in obstacles:
# obstacles in robot flame
tmp_x = obstacle.pos[0] - x
tmp_y = obstacle.pos[1] - y
dist = sqrt( tmp_x * tmp_x + tmp_y * tmp_y )
ob_x_in_robot_flame = tmp_x * cos(-yaw) - tmp_y * sin(-yaw)
ob_y_in_robot_flame = tmp_x * sin(-yaw) + tmp_y * cos(-yaw)
dist_ob.append( (dist, ob_x_in_robot_flame, ob_y_in_robot_flame) )
# print( dist_ob )
dist_ob.sort(key=lambda x: x[0])
# print( dist_ob )
print(f'robot yaw: {yaw}')
print( f'dist: {dist_ob[0][0]}' )
print( f'ob_y_in_robot_flame: {dist_ob[0][2]}' )
# distance between robot and obstacle
if dist_ob[0][0] < 1.0:
if 0 < dist_ob[0][2]:
change_omega = - (1 / dist + 0.1) * COEFF_OB
if dist_ob[0][2] < 0:
change_omega = + (1 / dist + 0.1) * COEFF_OB
self.omega = change_omega
return self.nu, self.omega
# ------------------------------------------------------------------------------
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 = []
self.obstacles = []
def append_landmark(self, landmark):
landmark.id = len(self.landmarks)
self.landmarks.append(landmark)
def append_goal( self, goal ):
self.goal.append( goal )
def append_obstacle( self, obstacle ):
self.obstacles.append( obstacle )
def draw( self, ax, elems ):
for goal in self.goal: goal.draw(ax, elems)
for obstacle in self.obstacles: obstacle.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))
# ------------------------------------------------------------------------------
class Obstacle:
def __init__( self, x, y, r ):
self.pos = np.array([x, y]).T
self.id = None
self.radius = r
def draw( self, ax, elems ):
c = ax.scatter(self.pos[0], self.pos[1], s=100, marker="o", label="obstacle", color="black")
elems.append(c)
elems.append(ax.text(self.pos[0], self.pos[1], "id:" + str(self.id), fontsize=10))
c = patches.Circle(xy=(self.pos[0], self.pos[1]), radius=self.radius, fill=False, color="blue")
elems.append(ax.add_patch(c))
# ------------------------------------------------------------------------------
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 )
obstacle = Obstacle( 5, 6, 1 )
m.append_obstacle( obstacle )
obstacle = Obstacle( 3, 5, 1 )
m.append_obstacle( obstacle )
world.append( m )
robot = Robot( np.array([0, -3, 0]).T, nu=0.2, omega=0.0, goal=goal, obstacles=m.obstacles )
# 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()
まとめ
このくらいの複雑さからcontinuous integrationを組み込まないと検証できなくなる気がします。
入力と期待値の幅を用意することになる気がします。
それなりに厳しいobstacleの配置にしたけど、実機に搭載は怖くて出来ない。
広告
IT開発関連書とビジネス書が豊富な翔泳社の通販『SEshop』さくらのレンタルサーバ
ムームードメイン
Oisix(おいしっくす)
らでぃっしゅぼーや
珈琲きゃろっと
エプソムソルト