Skip to content
  • Home
  • About the Blog
  • About the Author
  • Sitemap

Abdur Rosyid's Blog

Just a few notes on mechanical engineering and robotics

2D Navigation of a Mobile Robot in ROS

July 21, 2021 by Abdur Rosyid

Navigation can be 2D or 3D. For a ground mobile robot, the navigation is typically 2D since the mobile robot does not have a degree of freedom in the vertical direction. Even for other types of mobile robots such as unmanned aerial vehicles (UAVs), the navigation can be simplified to 2D. This is performed by decoupling the motion control into 2D horizontal motion and 1D vertical motion (i.e. altitude control). See the following example of 2D navigation applied to a UAV: https://www.wilselby.com/research/ros-integration/2d-mapping-navigation/

There are five things required to navigate a mobile robot:

  1. Map
  2. Robot localization
  3. Sensing the environment
  4. Motion planning
  5. User Interface
  6. Motion control

The first four things above are handled by a navigation package, whereas the last thing (i.e. the motion control) should be handled by the robot controller.

Here we are going to create a 2D navigation package for a mobile robot. The package has a structure like this:

my_navigation_package:
|--launch
  |--amcl.launch.xml
  |--my_navigation_launch.launch
|--maps
  |--my_map.pgm
  |--my_map.yaml
|--param
  |--move_base_params.yaml
  |--dwa_planner_params.yaml
  |--costmap_common_params.yaml
  |--global_costmap_params.yaml
  |--local_costmap_params.yaml
|--rviz
  |--my_navigation_rviz.rviz
|--src
  |--my_navigation_client.cpp
|--script
  |--my_navigation_client.py
|--package.xml
|--CMakeLists.txt

All the launch files described below should be combined into a single launch file called “my_navigation_launch.launch” as shown in the file structure above. This is the launch file which should be launched to load the navigation configuration, after the robot (or the robot simulator) is launched. This launch file performs several tasks including:

  • running the map_server and loading the map
  • running AMCL node (used to localize the robot)
  • running move_base node (used to navigate the robot from the current pose to the goal pose).

A similar navigation package can be found here: https://github.com/ROBOTIS-GIT/turtlebot3/tree/master/turtlebot3_navigation

Map

There are two cases in the navigation of a mobile robot with regard to map:

  • The map is available
  • The map is not available

The availability of map represents an apriori knowledge about the environment. A map can be drawn manually if the environment is know in advance. If the map is not available, simultaneous localization and mapping (SLAM) is typically performed to create the map while the robot is exploring the environment. A simple way to perform SLAM is by teleoperating the robot and observing the obstacles by using the robot exteroceptive sensors while creating the map using a SLAM algorithm. Similar to navigation, a map can be either a 2D map or a 3D map.

In ROS, a map should be represented by two files: a pgm file and a YAML file. The pgm file is the visual representation of the environment, which can be presented as an occupancy grid map with gray scale ranging from 0 (black) to 255 (white), but typically to be classified into binary scale: either occupied (black) or free (white). The YAML file contains some information about the map. A YAML file called “my_map.yaml” corresponding to a map called “my_map.pgm” looks like this:

image: my_map.pgm
resolution: 0.050000
origin: [-10.000000, -10.000000, 0.000000]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.196

The resolution is given in meters per pixel. In the example above, it means that 1 pixel equals 0.05 meters. In other words, 100 pixel equals 5 meters, i.e. 1 pixel equals 5/100 meters = 5 cm. The origin described above means that the lower left of the map is at x = -10 meters, y = -10 meters. Negate means whether black and white colors are inverted. If negate is 0, it means black remains black and white remains white. The threshold to consider a grayscale occupied is 0.65 whereas the threshold to consider a grayscale free is 0.196. This threshold is in the range of 0 to 1.

To load a map, run the “map_server” node and load the map. It can be performed by using a launch file like this:

1
2
3
4
5
6
<launch>
  <!-- Map server -->
  <arg name="map_file" default="$(find my_navigation_package)/maps/my_map.yaml"/>
  <node name="map_server" pkg="map_server" type="map_server" args="$(arg map_file)">
  </node>
</launch>

Robot localization

There are several methods to localize the robot, including:

  • Kalman Filter (KF), including EKF and UKF.
  • Particle Filter (PF).

A well-known PF-based localization algorithm is Monte Carlo Localization (MCL). The improved version of MCL is the Adaptive Monte Carlo Localization (AMCL). The AMCL node can be easily launched by including it in a launch file like this:

1
2
3
<launch>
  <include file="$(find my_navigation_package)/launch/amcl.launch.xml"/>
</launch>

where the “amcl.launch.xml” looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<launch>
  <!-- if true, AMCL receives map topic instead of service call. -->
  <arg name="use_map_topic" default="false"/>
  <!-- topic name for the sensor values from the distance sensor. -->
  <arg name="scan_topic" default="scan"/>
  <!-- used as the initial x-coordinate value of the Gaussian distribution in initial pose
estimation.-->
  <arg name="initial_pose_x" default="0.0"/>
  <!-- used as the initial y-coordinate value of the Gaussian distribution in the initial pose
estimation.-->
  <arg name="initial_pose_y" default="0.0"/>
  <!-- used as the initial yaw coordinate value of the Gaussian distribution in the initial pose
estimation. -->
  <arg name="initial_pose_a" default="0.0"/>
  
  <!-- execute the amcl node by referring to the parameter settings below. -->
  <node pkg="amcl" type="amcl" name="amcl">
    <!-- filter related parameters -->
    <!-- min number of particles allowed -->
    <param name="min_particles" value="500"/>
    <!-- max number of particles allowed (the higher the better; set based on PC performance) -->
    <param name="max_particles" value="3000"/>
    <!-- max error between the actual distribution and the estimated distribution -->
    <param name="kld_err" value="0.02"/>
    <!-- translational motion required for filter update (meter) -->
    <param name="update_min_d" value="0.2"/>
    <!-- rotational motion required for filter update (radian) -->
    <param name="update_min_a" value="0.2"/>
    <!-- resampling interval -->
    <param name="resample_interval" value="1"/>
    <!-- conversion allowed time (by sec) -->
    <param name="transform_tolerance" value="0.5"/>
    <!-- index drop rate(slow average weight filter), deactivated if 0.0 -->
    <param name="recovery_alpha_slow" value="0.0"/>
    <!-- index drop rate(fast average weight filter), deactivated if 0.0 -->
    <param name="recovery_alpha_fast" value="0.0"/>
    <!-- refer to above initial_pose_x -->
    <param name="initial_pose_x" value="$(arg initial_pose_x)"/>
    <!-- refer to above initial_pose_y -->
    <param name="initial_pose_y" value="$(arg initial_pose_y)"/>
    <!-- refer to above initial_pose_a -->
    <param name="initial_pose_a" value="$(arg initial_pose_a)"/>
    <!-- max period to visually displaying scan and path info -->
    <!-- example: 10Hz = 0.1sec, deactivated if -1.0 -->
    <param name="gui_publish_rate" value="50.0"/>
    <!-- same as the explanation for use_map_topic -->
    <param name="use_map_topic" value="$(arg use_map_topic)"/>
    <!--distance sensor parameter -->
    <!-- change the sensor topic name -->
    <remap from="scan" to="$(arg scan_topic)"/>
    <!-- max distance of laser sensing distance (meter) -->
    <param name="laser_max_range" value="3.5"/>
    <!-- max number of laser beams used during filter update -->
    <param name="laser_max_beams" value="180"/>
    <!-- z_hit mixed weight of sensor model (mixture weight) -->
    <param name="laser_z_hit" value="0.5"/>
    <!-- z_short mixed weight of sensor model (mixture weight) -->
    <param name="laser_z_short" value="0.05"/>
    <!-- z_max mixed weight of sensor model (mixture weight) -->
    <param name="laser_z_max" value="0.05"/>
    <!-- x_rand mixed weight of sensor model (mixture weight) -->
    <param name="laser_z_rand" value="0.5"/>
    <!-- standard deviation of Gaussian model using z_hit of sensor -->
    <param name="laser_sigma_hit" value="0.2"/>
    <!-- index drop rate parameter for z_short of sensor -->
    <param name="laser_lambda_short" value="0.1"/>
    <!-- max distance and obstacle for likelihood_field method sensor -->
    <param name="laser_likelihood_max_dist" value="2.0"/>
    <!-- sensor type(select likelihood_field or beam) -->
    <param name="laser_model_type" value="likelihood_field"/>
    <!-- parameter related to odometry -->
    <!-- robot driving methods. "diff" or "omni" can be selected -->
    <param name="odom_model_type"value="diff"/>
    <!-- estimated rotational motion noise of the odometry during rotational motion -->
    <param name="odom_alpha1" value="0.1"/>
    <!-- estimated rotational motion noise of the odometry during translation motion -->
    <param name="odom_alpha2" value="0.1"/>
    <!-- estimated translation motion noise of the odometry during translation motion -->
    <param name="odom_alpha3" value="0.1"/>
    <!-- estimated translation motion noise of the odometry during rotational motion -->
    <param name="odom_alpha4" value="0.1"/>
    <!-- odometry frame -->
    <param name="odom_frame_id" value="odom"/>
    <!-- robot base frame -->
    <param name="base_frame_id" value="base_footprint"/>
  </node>
</launch>

Please notice that the AMCL node requires the determination of the initial pose of the robot.

Another ROS package commonly used for robot localization is robot_localization. This package is capable to fuse data from several sensors such as odometry, IMU, and GPS to improve the localization. The pose estimation is performed by using EKF and UKF.

The robot pose is published by the “robot_state_publisher” node. Hence, it is necessary to run the “robot_state_publisher” node if it is not running yet. This can be done by using a launch file like this:

1
2
3
4
5
<launch>
  <node pkg="robot_state_publisher" type="robot_state_publisher" name="robot_state_publisher" output="screen">
    <param name="publish_frequency" type="double" value="50.0" />
  </node>
</launch>

Motion planning

A very common ROS package used to navigate a mobile robot in ROS is move_base. It consists of a global planner and a local planner. The global planner is based on the global costmap, whereas the local planner is based on the local costmap. Both the costmaps are presented as occupancy grid map in grayscale, ranging from 0 to 255:

  • 000: Free area where robot can move freely
  • 001~127: Areas of low collision probability
  • 128~252: Areas of high collision probability
  • 253~254: Collision area
  • 255: Occupied area where robot can not move

The global costmap is indicated by grayscale representation around the obstacles on the loaded map, whereas the local costmap is indicated by grayscale representation around the robot body.

The “move_base” node can be conveniently launched by using a launch file like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<launch>
  <!-- move_base -->
  <arg name="cmd_vel_topic" default="/cmd_vel" />
  <arg name="odom_topic" default="odom" />
  <node pkg="move_base" type="move_base" respawn="false" name="move_base" output="screen">
    <param name="base_local_planner" value="dwa_local_planner/DWAPlannerROS" />
    <rosparam file="$(find my_navigation_package)/param/move_base_params.yaml" command="load" />
    <rosparam file="$(find my_navigation_package)/param/dwa_local_planner_params.yaml" command="load" />
    <rosparam file="$(find my_navigation_package)/param/costmap_common_params.yaml" command="load" ns="global_costmap" />
    <rosparam file="$(find my_navigation_package)/param/costmap_common_params.yaml" command="load" ns="local_costmap" />
    <rosparam file="$(find my_navigation_package)/param/global_costmap_params.yaml" command="load" />
    <rosparam file="$(find my_navigation_package)/param/local_costmap_params.yaml" command="load" />
   <remap from="cmd_vel" to="$(arg cmd_vel_topic)"/>
   <remap from="odom" to="$(arg odom_topic)"/>
  </node>
</launch>

As seen above, there are some YAML files for the “move_base” node which should be written:

  • move_base_params.yaml
  • dwa_local_planner_params.yaml
  • costmap_common_params.yaml
  • global_costmap_params.yaml
  • local_costmap_params.yaml

The “move_base_params.yaml” looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# choosing whether to stop the costmap node when move_base is inactive
shutdown_costmaps: false
# cycle of control iteration (in Hz) that orders the speed command to the robot base
controller_frequency: 3.0
# maximum time (in seconds) that the controller will listen for control information before the space-clearing operation is performed
controller_patience: 1.0
# repetition cycle of global plan (in Hz)
planner_frequency: 2.0
# maximum amount of time (in seconds) to wait for an available plan before the space-clearing operation is performed
planner_patience: 1.0
# time (in sec) allowed to allow the robot to move back and forth before executing the recovery behavior.
oscillation_timeout: 10.0
# oscillation_timeout is initialized if you move the distance below the distance (in meter) that the robot should move so that it does not move back and forth.
oscillation_distance: 0.2
# Obstacles farther away from fixed distance are deleted on the map during costmap initialization of the restore operation
conservative_reset_dist: 0.1

The “dwa_local_planner_params.yaml” looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
DWAPlannerROS:
  # Robot Parameters
  max_vel_x: 0.18 # max velocity for x axis(meter/sec)
  min_vel_x:-0.18 # min velocity for x axis (meter/sec)
  max_vel_y: 0.0 # Not used. applies to omni directional robots only
  min_vel_y: 0.0 # Not used. applies to omni directional robots only
  max_trans_vel: 0.18 # max translational velocity(meter/sec)
  min_trans_vel: 0.05 # min translational velocity (meter/sec), negative value for reverse
  # trans_stopped_vel: 0.01 # translation stop velocity(meter/sec)
  max_rot_vel: 1.8 # max rotational velocity(radian/sec)
  min_rot_vel: 0.7 # min rotational velocity (radian/sec)
  # rot_stopped_vel: 0.01 # rotation stop velocity (radian/sec)
  acc_lim_x: 2.0 # limit for x axis acceleration(meter/sec^2)
  acc_lim_y: 0.0 # limit for y axis acceleration (meter/sec^2)
  acc_lim_theta: 2.0 # theta axis angular acceleration limit (radian/sec^2)
 
  # Target point error tolerance
  yaw_goal_tolerance: 0.15 # yaw axis target point error tolerance (radian)
  xy_goal_tolerance: 0.05 # x, y distance Target point error tolerance (meter)
 
  # Forward Simulation Parameter
  sim_time: 3.5 # forward simulation trajectory time
  vx_samples: 20 # number of sample in x axis velocity space
  vy_samples: 0 # number of sample in y axis velocity space
  vtheta_samples: 40 # number of sample in theta axis velocity space
 
  # Trajectory scoring parameter (trajectory evaluation)
  # Score calculation used for the trajectory evaluation cost function is as follows.
  # cost =
  # path_distance_bias * (distance to path from the endpoint of the trajectory in meters)
  # + goal_distance_bias * (distance to local goal from the endpoint of the trajectory in meters)
  # + occdist_scale * (maximum obstacle cost along the trajectory in obstacle cost (0-254))
  path_distance_bias: 32.0 # weight value of the controller that follows the given path
  goal_distance_bias: 24.0 # weight value for the goal pose and control velocity
  occdist_scale: 0.04 # weight value for the obstacle avoidance
  forward_point_distance: 0.325 # distance between the robot and additional scoring point (meter)
  stop_time_buffer: 0.2 # time required for the robot to stop before collision (sec)
  scaling_speed: 0.25 # scaling Speed (meter/sec)
  max_scaling_factor: 0.2 # max scaling factor
 
  # Oscillation motion prevention paramter
  # distance the robot must move before the oscillation flag is reset
  oscillation_reset_dist: 0.05
 
  # Debugging
  publish_traj_pc: true # debugging setting for the movement trajectory
  publish_cost_grid_pc: true # debugging setting for costmap
  global_frame_id: odom # ID setting for global frame

The “costmap_common_params.yaml” looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Indicate the object as an obstacle when the distance between the robot and obstacle is within this range.
obstacle_range: 2.5
# sensor value that exceeds this range will be indicated as a freespace
raytrace_range: 3.5
# external dimension of the robot is provided as polygons in several points
footprint: [[-0.110, -0.090], [-0.110, 0.090], [0.041, 0.090], [0.041, -0.090]]
# radius of the inflation area to prevent collision with obstacles
inflation_radius: 0.15
# scaling variable used in costmap calculation. Calculation formula is as follows.
# exp(-1.0 * cost_scaling_factor *(distance_from_obstacle - inscribed_radius)) *(254 - 1)
cost_scaling_factor: 0.5
# select costmap to use between voxel(voxel-grid) and costmap(costmap_2d)
map_type: costmap
# tolerance of relative coordinate conversion time between tf
transform_tolerance: 0.2
# specify which sensor to use
observation_sources: scan
# set data type and topic, marking status, minimum obstacle for the laser scan
scan: {data_type: LaserScan, topic: scan, marking: true, clearing: true}

The “global_costmap_params.yaml” looks like this:

1
2
3
4
5
6
7
global_costmap:
  global_frame: /map # set map frame
  robot_base_frame: /base_footprint # set robot's base frame
  update_frequency: 2.0 # update frequency
  publish_frequency: 0.1 # publish frequency
  static_map: true # setting whether or not to use given map
  transform_tolerance: 1.0 # transform tolerance time

The “local_costmap_params.yaml” looks like this:

1
2
3
4
5
6
7
8
9
10
11
local_costmap:
  global_frame: /odom # set map frame
  robot_base_frame: /base_footprint # set robot's base frame
  update_frequency: 2.0 # update frequency
  publish_frequency: 0.5 # publish frequency
  static_map: false # setting whether or not to use given map
  rolling_window: true # local map window setting
  width: 3.5 # area of local map window
  height: 3.5 # height of local map window
  resolution: 0.05 # resolution of local map window (meter/cell)
  transform_tolerance: 1.0 # transform tolerance time

User Interface

A user can provide inputs for navigation in two ways:

  • Using RViz where a user can initialize the pose of the robot for the AMCL node and determine a goal pose of the robot for the move_base node.
  • Using client node. This can be written in C++ or Python. By using such a client node, one can programmatically determine a goal pose of the robot. Below we can see simple client nodes written in C++ and Python to move the robot 2 meters forward.

A client node written in C++ looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <ros/ros.h>
#include <move_base_msgs/MoveBaseAction.h>
#include <actionlib/client/simple_action_client.h>
 
typedef actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> MoveBaseClient;
 
int main(int argc, char** argv){
  ros::init(argc, argv, "simple_navigation_goals");
 
  //tell the action client that we want to spin a thread by default
  MoveBaseClient ac("move_base", true);
 
  //wait for the action server to come up
  while(!ac.waitForServer(ros::Duration(5.0))){
    ROS_INFO("Waiting for the move_base action server to come up");
  }
 
  move_base_msgs::MoveBaseGoal goal;
 
  //we'll send a goal to the robot to move 2 meters in the X+ direction of the map
  goal.target_pose.header.frame_id = "map";
  goal.target_pose.header.stamp = ros::Time::now();
 
  goal.target_pose.pose.position.x = 2.0;
  goal.target_pose.pose.orientation.w = 1.0;
 
  ROS_INFO("Sending goal");
  ac.sendGoal(goal);
 
  ac.waitForResult();
 
  if(ac.getState() == actionlib::SimpleClientGoalState::SUCCEEDED)
    ROS_INFO("Hooray, the base moved 2 meters forward");
  else
    ROS_INFO("The base failed to move forward 2 meters for some reason");
 
  return 0;
}

If the robot is intended to move 2 meters forward with respect to the robot’s frame, let’s say base_link frame, you just need to change the reference frame of the motion:

1
  goal.target_pose.header.frame_id = "base_link";

A client node written in Python looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python
# license removed for brevity
 
import rospy
import actionlib
from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
 
def movebase_client():
 
   # Create an action client called "move_base" with action definition file "MoveBaseAction"
    client = actionlib.SimpleActionClient('move_base',MoveBaseAction)
   # Waits until the action server has started up and started listening for goals.
    client.wait_for_server()
 
   # Creates a new goal with the MoveBaseGoal constructor
    goal = MoveBaseGoal()
    goal.target_pose.header.frame_id = "map"
    goal.target_pose.header.stamp = rospy.Time.now()
   # Move 2 meters in the X+ direction of the map
    goal.target_pose.pose.position.x = 2.0
   # No rotation of the mobile base frame w.r.t. map frame
    goal.target_pose.pose.orientation.w = 1.0
 
   # Sends the goal to the action server.
    client.send_goal(goal)
   # Waits for the server to finish performing the action.
    wait = client.wait_for_result()
   # If the result doesn't arrive, assume the Server is not available
    if not wait:
        rospy.logerr("Action server not available!")
        rospy.signal_shutdown("Action server not available!")
    else:
    # Result of executing the action
        return client.get_result()  
 
# If the python node is executed as main process (sourced directly)
if __name__ == '__main__':
    try:
       # Initializes a rospy node to let the SimpleActionClient publish and subscribe
        rospy.init_node('movebase_client_py')
        result = movebase_client()
        if result:
            rospy.loginfo("Goal execution done!")
    except rospy.ROSInterruptException:
        rospy.loginfo("Navigation test finished.")

If the robot is intended to move 2 meters forward with respect to the robot’s frame, let’s say base_link frame, you just need to change the reference frame of the motion:

1
    goal.target_pose.header.frame_id = "base_link"

Motion control

Motion control of the mobile robot is handled by the robot controller, such as ros_control and friends. Since the “move_base” node publishes the velocity command (/cmd_vel topic) which is a geometry_msgs/Twists message (containing the linear and angular velocities), this velocity command is fed to the robot controller, such as the “diff_drive_controller” which is one of the controller in ros_controller.

Official documentation

  • http://wiki.ros.org/navigation/Tutorials
  • http://wiki.ros.org/navigation/Tutorials/RobotSetup

Post navigation

Previous Post:

How to Use MoveIt

Next Post:

Fusing Wheel Odometry and IMU Data Using robot_localization in ROS

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Categories

  • STEM 101
  • Robotics
  • Kinematics
  • Dynamics
  • Control
  • Robot Operating System (ROS)
  • Robot Operating System (ROS2)
  • Software Development
  • Mechanics of Materials
  • Finite Element Analysis
  • Fluid Mechanics
  • Thermodynamics

Recent Posts

  • Pull Request on Github
  • Basics of Git and Github
  • Conda vs Docker
  • A Conda Cheat Sheet
  • Installing NVIDIA GPU Driver on Ubuntu

Archives

  • June 2025
  • July 2021
  • June 2021
  • March 2021
  • September 2020
  • April 2020
  • January 2015
  • April 2014
  • March 2014
  • March 2012
  • February 2012
  • June 2011
  • March 2008
© 2026 Abdur Rosyid's Blog | WordPress Theme by Superbthemes