class MapTest(object): """docstring for MapTest""" def __init__(self): self.map_s = MapStorage(debug=True) # Create default marker for display of swarm / debug point_marker = Marker() # point_marker.type = Marker.ARROW point_marker.type = Marker.SPHERE point_marker.action = Marker.ADD # point_marker.scale.x = 2.5 # point_marker.scale.y = 0.35 # point_marker.scale.z = 0.35 point_marker.scale.x = 0.2 point_marker.scale.y = 0.2 point_marker.scale.z = 0.2 point_marker.color.r = 1.0 point_marker.color.g = 0.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 self.mark_p = MarkerPlacer("rviz_map_test", "map", 1000, point_marker) self.x = 0.0 self.y = 0.0 self.start_x = self.map_s.min_x_pos+0.05 self.start_y = self.map_s.min_y_pos+0.05 self.end_x = self.map_s.max_x_pos self.end_y = self.map_s.max_y_pos self.delta_dist = self.map_s.resolution def run(self): self.x = self.start_x self.y = self.start_y while self.x < self.end_x: while self.y < self.end_y: # Check if spot is occupied if self.map_s.xy_pos_is_occupied(self.x, self.y): # If spot is occupied place particle p = Point(self.x, self.y, 0.0) o = Quaternion() self.mark_p.place_marker([p], [o]) # Update new y self.y += 1.0*self.delta_dist # Sleep a tiny bit time.sleep(0.0001) self.y = self.start_y self.x += 5.0*self.delta_dist
def __init__(self): rospy.init_node("real_pose_broadcaster") # Enable us to get odom and the pose from within odom self.odom = Odometry() self.linear = Point(0.0, 0.0, 0.0) self.quat = Quaternion(0.0, 0.0, 0.0, 0.0) rospy.Subscriber("base_pose_ground_truth", Odometry, self.get_odom) # Want to broadcast a transform relating the position of the robot to the map frame self.br = tf.TransformBroadcaster() # Want to put a marker for the real pose self.mark_placer = MarkerPlacer("rviz_real_robot_pose", "map") self.mark_placer.set_scale(1.25, 0.25, 0.25) # Also want to broadcast the real pose of the robot self.pose_pub = rospy.Publisher("/real_pose", PoseStamped, queue_size=10) # Update this every 10 seconds or so self.rate = rospy.Rate(10) while not rospy.is_shutdown(): self.br.sendTransform(translation=(self.linear.x, self.linear.y, self.linear.z), rotation=(self.quat.x, self.quat.y, self.quat.z, self.quat.w), time=rospy.Time.now(), child="real_robot_pose", parent="map") self.mark_placer.place_marker([self.linear], [self.quat]) pose = PoseStamped() pose.header.frame_id = "/map" pose.header.stamp = rospy.Time.now() pose.pose.position = self.linear pose.pose.orientation = self.quat self.pose_pub.publish(pose) self.rate.sleep()
def __init__(self): self.map_s = MapStorage(debug=True) # Create default marker for display of swarm / debug point_marker = Marker() # point_marker.type = Marker.ARROW point_marker.type = Marker.SPHERE point_marker.action = Marker.ADD # point_marker.scale.x = 2.5 # point_marker.scale.y = 0.35 # point_marker.scale.z = 0.35 point_marker.scale.x = 0.2 point_marker.scale.y = 0.2 point_marker.scale.z = 0.2 point_marker.color.r = 1.0 point_marker.color.g = 0.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 self.mark_p = MarkerPlacer("rviz_map_test", "map", 1000, point_marker) self.x = 0.0 self.y = 0.0 self.start_x = self.map_s.min_x_pos+0.05 self.start_y = self.map_s.min_y_pos+0.05 self.end_x = self.map_s.max_x_pos self.end_y = self.map_s.max_y_pos self.delta_dist = self.map_s.resolution
def __init__(self): rospy.init_node("drifting_pose_broadcaster") self.odom = Odometry() self.br = tf.TransformBroadcaster() self.rate = rospy.Rate(10) self.linear = Point(0.0, 0.0, 0.0) self.quat = Quaternion(0.0, 0.0, 0.0, 0.0) self.yaw = 0.0 self.odom_initialised = False rospy.Subscriber("base_pose_ground_truth", Odometry, self.get_real_odom) rospy.Subscriber("odom", Odometry, self.get_drift_odom) self.mark_placer = MarkerPlacer("rviz_drifting_robot_pose", "map") self.mark_placer.set_color(0.674509804, 0.337254902, 0.0, 1.0) while not rospy.is_shutdown(): self.br.sendTransform(translation=(self.linear.x, self.linear.y, self.linear.z), rotation=(self.quat.x, self.quat.y, self.quat.z, self.quat.w), time=rospy.Time.now(), child="drifting_robot_pose", parent="map") self.mark_placer.place_marker([self.linear], [self.quat]) self.rate.sleep()
map.downsample_map_by_half() map.expand_map() map.expand_map() map.expand_map() t.print_delta() def_marker = Marker() def_marker.type = Marker.SPHERE def_marker.action = Marker.ADD def_marker.scale.x = 0.10 def_marker.scale.y = 0.10 def_marker.scale.z = 0.10 def_marker.color.r = 0.0 def_marker.color.g = 1.0 def_marker.color.b = 0.0 def_marker.color.a = 1.0 markers = MarkerPlacer("astar_markers", "map", 1000, def_marker) # Original map # Heuristic: rank = dist(n, goal) + cost # cost = current[1].cost + X # Map: 2x down, 2x expand # start_pos = (-61.9, -15.46) # goal_pos = (-16.49, -34.79) # 3.37 s, X = 1 # goal_pos = (40.0, 17.71) # 94.08 s, X = 1 # goal_pos = (-16.49, -34.79) # 0.52 s, X = 0.2 # goal_pos = (40.0, 17.71) # 964.05 s, X = 0.2 # Heuristic: rank = dist(n, goal) # cost = current[1].cost + X # Map: 2x down, 2x expand # start_pos = (-61.9, -15.46)
def __init__(self, start_pos, target_list=[]): """ Various parameters we use to make decisions on """ # The start position we want to hit before we follow targets self.start_pos = start_pos # List of targets to follow self.target_list = target_list # nav_queue is used to contain a list of lists of coordinates to follow self.nav_queue = list() # The distance required of closeness to current coordinate for assuming it to be reached self.coord_dist_cutoff = 0.6 # What the current intermediate goal is self.curr_target = (0.0, 0.0) # Gains for calculating linear and angular speed # ref: A Stable Target-Tracking Control for Unicycle Mobile Robots Lee, S. et al. # Linear velocity depends only on K1, angular depends on K1 and K2 self.K1_default = 1.5 self.K2_default = -5.0 self.K1_slow = 0.5 self.K2_slow = -5.0 self.K1 = self.K1_default self.K2 = self.K2_default # Set up a markerarray to show the planned path to take self.planned_markers = MarkerPlacer("rviz_planned_path", "map", 4000) # Make the marker size a bit bigger so that they are easier to see self.planned_markers.set_scale(0.12, 0.12, 0.12) # Set them to be spheres self.planned_markers.set_type(Marker.SPHERE) # Pick a random colour since we might have multiple planned paths and want # to differentiate between them self.planned_markers.random_color() # Set up a markerarray to show the actual path taken self.taken_markers = MarkerPlacer("rviz_taken_path", "map", 4000) # Make the marker size a bit bigger so that they are easier to see self.taken_markers.set_scale(0.12, 0.12, 0.12) # Set them to be spheres self.taken_markers.set_type(Marker.SPHERE) # Pick a random colour since we might want to follow multiple paths and # want to differentiate between them self.taken_markers.random_color() # Set up a markerarray to place markers when the goal is reached self.goal_markers = MarkerPlacer("rviz_goals", "map", 40) # Make the marker size a bit bigger so that they are easier to see self.goal_markers.set_scale(0.10, 0.10, 1.35) # Set them to be spheres self.goal_markers.set_type(Marker.CYLINDER) # Set colour to pink self.goal_markers.set_color(1.0, 0.0, 1.0, 1.0) # Add a marker for the assumed pose that the navigator is acting on self.pose_marker = MarkerPlacer("/rviz_nav_pose", "/map") self.pose_marker.set_color(1.0, 1.0, 0.0, 1.0) self.pose_marker.set_scale(1.25, 0.25, 0.25) # Set up a container for current pose of robot. This pose is the fusion of # data received from the particle filter and odometry updates. # We store yaw in a separate variable since this will reduce the number of # transformations is required from going quat -> yaw -> quat since we will # use the yaw directly for navigation purposes self.pose = Pose() self.yaw = 0.0 # Subscribe to the real pose of the robot in order to drop markers along the followed path self.real_pose = PoseStamped() rospy.Subscriber("/real_pose", PoseStamped, self.get_real_pose) # Set up a subscriber to particle filter pose self.particle_pose = PoseStamped() rospy.Subscriber("/particle_pose", PoseStamped, self.get_particle_pose) # At this point we kind-of want to wait until we have a particle update # before we start throwing odometry updates at the uninitialised position. rospy.wait_for_message("/particle_pose", Odometry) # Set up a subscriber for (noisy) odometry updates # First wait for an Odometry message in order to get a time stamp which # will be used to update the robots position from the odometry data rospy.wait_for_message("/odom", Odometry) self.last_odom_time = rospy.get_time() # Then create a container for the odometry and subscribe to the topic. self.odom = Odometry() rospy.Subscriber("/odom", Odometry, self.get_odom) # Initialize the map that will be used for calculating paths and overall navigation self.m_s = None self.initialize_map() # Find path from robot position to start position. For now we're doing # this in the navigator, but should perhaps be outsources to an external # module later since it can take a very long time. self.xy_path = list() self.xy_pos_path = list() self.get_path_to_start() # Add the start pos since the resolution of the planned path is not perfect self.xy_pos_path.append(start_pos) # Place markers for the current path to show that we know where we are going (hopefully!) self.mark_current_path() # At this point we got: Robot pose (fusion of particle + odom) and a list # of coordinates we want to follow towards a goal. This should be enough # navigate! # Set up a publisher for sending velocity commands to the robot self.vel = Twist() self.vel_publisher = rospy.Publisher("/cmd_vel", Twist, queue_size=10) # Pop off the first item in list as current goal self.curr_target = self.xy_pos_path.pop(0)
class Navigator(object): """docstring for Navigator""" def __init__(self, start_pos, target_list=[]): """ Various parameters we use to make decisions on """ # The start position we want to hit before we follow targets self.start_pos = start_pos # List of targets to follow self.target_list = target_list # nav_queue is used to contain a list of lists of coordinates to follow self.nav_queue = list() # The distance required of closeness to current coordinate for assuming it to be reached self.coord_dist_cutoff = 0.6 # What the current intermediate goal is self.curr_target = (0.0, 0.0) # Gains for calculating linear and angular speed # ref: A Stable Target-Tracking Control for Unicycle Mobile Robots Lee, S. et al. # Linear velocity depends only on K1, angular depends on K1 and K2 self.K1_default = 1.5 self.K2_default = -5.0 self.K1_slow = 0.5 self.K2_slow = -5.0 self.K1 = self.K1_default self.K2 = self.K2_default # Set up a markerarray to show the planned path to take self.planned_markers = MarkerPlacer("rviz_planned_path", "map", 4000) # Make the marker size a bit bigger so that they are easier to see self.planned_markers.set_scale(0.12, 0.12, 0.12) # Set them to be spheres self.planned_markers.set_type(Marker.SPHERE) # Pick a random colour since we might have multiple planned paths and want # to differentiate between them self.planned_markers.random_color() # Set up a markerarray to show the actual path taken self.taken_markers = MarkerPlacer("rviz_taken_path", "map", 4000) # Make the marker size a bit bigger so that they are easier to see self.taken_markers.set_scale(0.12, 0.12, 0.12) # Set them to be spheres self.taken_markers.set_type(Marker.SPHERE) # Pick a random colour since we might want to follow multiple paths and # want to differentiate between them self.taken_markers.random_color() # Set up a markerarray to place markers when the goal is reached self.goal_markers = MarkerPlacer("rviz_goals", "map", 40) # Make the marker size a bit bigger so that they are easier to see self.goal_markers.set_scale(0.10, 0.10, 1.35) # Set them to be spheres self.goal_markers.set_type(Marker.CYLINDER) # Set colour to pink self.goal_markers.set_color(1.0, 0.0, 1.0, 1.0) # Add a marker for the assumed pose that the navigator is acting on self.pose_marker = MarkerPlacer("/rviz_nav_pose", "/map") self.pose_marker.set_color(1.0, 1.0, 0.0, 1.0) self.pose_marker.set_scale(1.25, 0.25, 0.25) # Set up a container for current pose of robot. This pose is the fusion of # data received from the particle filter and odometry updates. # We store yaw in a separate variable since this will reduce the number of # transformations is required from going quat -> yaw -> quat since we will # use the yaw directly for navigation purposes self.pose = Pose() self.yaw = 0.0 # Subscribe to the real pose of the robot in order to drop markers along the followed path self.real_pose = PoseStamped() rospy.Subscriber("/real_pose", PoseStamped, self.get_real_pose) # Set up a subscriber to particle filter pose self.particle_pose = PoseStamped() rospy.Subscriber("/particle_pose", PoseStamped, self.get_particle_pose) # At this point we kind-of want to wait until we have a particle update # before we start throwing odometry updates at the uninitialised position. rospy.wait_for_message("/particle_pose", Odometry) # Set up a subscriber for (noisy) odometry updates # First wait for an Odometry message in order to get a time stamp which # will be used to update the robots position from the odometry data rospy.wait_for_message("/odom", Odometry) self.last_odom_time = rospy.get_time() # Then create a container for the odometry and subscribe to the topic. self.odom = Odometry() rospy.Subscriber("/odom", Odometry, self.get_odom) # Initialize the map that will be used for calculating paths and overall navigation self.m_s = None self.initialize_map() # Find path from robot position to start position. For now we're doing # this in the navigator, but should perhaps be outsources to an external # module later since it can take a very long time. self.xy_path = list() self.xy_pos_path = list() self.get_path_to_start() # Add the start pos since the resolution of the planned path is not perfect self.xy_pos_path.append(start_pos) # Place markers for the current path to show that we know where we are going (hopefully!) self.mark_current_path() # At this point we got: Robot pose (fusion of particle + odom) and a list # of coordinates we want to follow towards a goal. This should be enough # navigate! # Set up a publisher for sending velocity commands to the robot self.vel = Twist() self.vel_publisher = rospy.Publisher("/cmd_vel", Twist, queue_size=10) # Pop off the first item in list as current goal self.curr_target = self.xy_pos_path.pop(0) def run(self): # Here we need to act on the information previously received regarding # robot pose and list of coordinates to follow. # Using these parameters we have to figure out: # 1) When we are close enough to a coordinate to knock it off the list # 1.1) Is the target the goal? If so -> What to do? # 2) What the next coordinate is # 3) The angle towards the next coordinate # 4) The angular and linear speed we need to set in order to hit the coordinate # Find closeness to current coordinate curr_dist = dist((self.pose.position.x, self.pose.position.y), self.curr_target) if curr_dist < self.coord_dist_cutoff: # Drop off a marker to show that we have been here self.taken_markers.place_marker([copy.deepcopy(self.real_pose.pose.position)], [copy.deepcopy(self.real_pose.pose.orientation)]) # Check if we have arrived at the last point in the list (the ultimate goal) if len(self.xy_pos_path) == 0: if curr_dist < 0.2: # Poop goal marker self.goal_markers.place_marker([self.real_pose.pose.position], [self.real_pose.pose.orientation]) # Check if there are more targets to go to if len(self.target_list) > 0: # Change the colour for the planned path and path taken to differentiate # it from the previous path self.taken_markers.random_color() self.planned_markers.random_color() # Find a new target and path self.xy_path = self.get_next_path() # Convert the path to xy_pos format self.convert_current_path() # Mark it self.mark_current_path() # if no more targets go to sleep else: rospy.sleep() # If we have more coordinates to visit pop off the next one! else: self.curr_target = self.xy_pos_path.pop(0) curr_dist = dist((self.pose.position.x, self.pose.position.y), self.curr_target) # Adjust the speed depending on how close we are to the current goal if len(self.xy_pos_path) < 5: self.K1 = self.K1_slow self.K2 = self.K2_slow else: self.K1 = self.K1_default self.K2 = self.K2_default # Figure out the angle we want to travel in to reach the current target # Assumption: This is given by atan2 between the target and the robot? # This makes sense as it would transpose the origin to the centre of the # robot, leaving the calculation to a "virtual" coordinate system around # the robot. theta = math.atan2(self.curr_target[1] - self.pose.position.y, self.curr_target[0] - self.pose.position.x) # Find the difference in theta that should go towards zero when the # correct heading is reached d_theta = theta - self.yaw d_theta = self.normalize(d_theta) # Find a new set of linear and angular speeds to publish lin_speed = self.new_speed(curr_dist, d_theta) ang_speed = self.new_angular_speed(d_theta) self.vel.linear.x = lin_speed self.vel.angular.z = ang_speed self.vel_publisher.publish(self.vel) def new_speed(self, radius, target_theta): """ ref: A Stable Target-Tracking Control for Unicycle Mobile Robots Lee, S. et al. """ return self.K1 * radius * math.cos(target_theta) def new_angular_speed(self, target_theta): """ ref: A Stable Target-Tracking Control for Unicycle Mobile Robots Lee, S. et al. """ factor_1 = (-self.K1) * math.sin(target_theta) * math.cos(target_theta) factor_2 = self.K2 * target_theta return factor_1 - factor_2 def get_real_pose(self, pose): self.real_pose = pose def get_particle_pose(self, particle_pose): prev_particle_pose = self.particle_pose self.particle_pose = particle_pose # Set up a check to see if the new pose is out of whack - Might happen on # random occasions. If it is we do not want to update the assumed pose but # rather continue using the estimated pose from previous particle filter + # odometry updates # TODO(geir): Implement the check somehow if True: self.pose.position = self.particle_pose.pose.position self.pose.orientation = self.particle_pose.pose.orientation pitch, roll, yaw = tf.transformations.euler_from_quaternion([self.pose.orientation.x, self.pose.orientation.y, self.pose.orientation.z, self.pose.orientation.w]) self.yaw = yaw def get_odom(self, odom): self.odom = odom # Update current pose with odom data! # First get the current time and time since last update time = rospy.get_time() d_time = time-self.last_odom_time self.last_odom_time = time # Get the linear and angular velocities lin_vel = self.odom.twist.twist.linear.x ang_vel = self.odom.twist.twist.angular.z # Calculate change in yaw, x and y d_theta = ang_vel * d_time d_x = (lin_vel * math.cos(self.yaw))*d_time d_y = (lin_vel * math.sin(self.yaw))*d_time # Update position self.yaw += d_theta self.pose.position.x += d_x self.pose.position.y += d_y q = tf.transformations.quaternion_from_euler(0.0, 0.0, self.yaw) self.pose.orientation = Quaternion(*q) # Publish the marker for this pose self.pose_marker.place_marker([self.pose.position], [self.pose.orientation]) def initialize_map(self): # Load a map, down sample it and expand it self.m_s = MapStorage() # Down sample in order to reduce graph size self.m_s.downsample_map_by_half() self.m_s.downsample_map_by_half() # Expand in order to avoid collisions # Previously expanded twice (inside down sampling) => 0.1 res -> 0.4 res # Three more expansions will leave us with an object previously taking up a # size of 0.4 m to take up ~4*0.4 -> 1.6. The robot size is 1 metre, so # this leaves a space of 0.6 for errors before collisions occur. # This is *MUCH* needed - anything else leads to a boatload of collisions self.m_s.expand_map() self.m_s.expand_map() self.m_s.expand_map() self.m_s.expand_map() def get_next_path(self): # Here we want to find the item in the list which is the closest to the # current position of the robot. closest = 999999.9 closest_index = 0 for i in range(len(self.target_list)): d = dist((self.pose.position.x, self.pose.position.y), (self.target_list[i][0], self.target_list[i][1])) if d < closest: closest = d closest_index = i # Get the target that is closest pos_t = self.target_list.pop(closest_index) # Find the x,y values for robot position and target x_t, y_t = self.m_s.xy_from_xy_pos(pos_t[0], pos_t[1]) x_r, y_r = self.m_s.xy_from_xy_pos(self.pose.position.x, self.pose.position.y) # Search for path path = astar_search(self.m_s, (x_r, y_r), (x_t, y_t)) return path def get_path_to_start(self): # Convert x_pos,y_pos for self.pose and self.start_pos to x,y for use with A* search x_r, y_r = self.m_s.xy_from_xy_pos(self.pose.position.x, self.pose.position.y) x_s, y_s = self.m_s.xy_from_xy_pos(self.start_pos[0], self.start_pos[1]) # Find a path from current position to start position self.xy_path = astar_search(self.m_s, (x_r, y_r), (x_s, y_s)) # Convert the path in to real coordinates self.convert_current_path() def convert_current_path(self): # The path is now in (x,y) format so we want to convert this to a list in # (x_pos,y_pos) format in order to use it for navigation self.xy_pos_path = list() for p in self.xy_path: x_pos, y_pos = self.m_s.xy_pos_from_xy(p[0], p[1]) self.xy_pos_path.append((x_pos, y_pos)) def mark_current_path(self): o = Quaternion() # Need for interface o_list = list() p_list = list() for pos in self.xy_pos_path: o_list.append(o) p_list.append(Point(pos[0], pos[1], 0.0)) self.planned_markers.place_marker(p_list, o_list) def normalize(self, theta): while theta <= math.pi: theta += 2.0*math.pi while theta > math.pi: theta -= 2.0*math.pi return theta
def __init__(self, odom_rate, sens_median, sens_std_dev, vel_uniform_dist, num_particles, base_scan, start_pos=None, debug=False): # Odom rate is used for re-positioning of particles and assumed pose after applying filter self.odom_rate = odom_rate # Need sensory uncertainty distribution for calculating particle beam probabilities self.sens_median = sens_median self.sens_std_dev = sens_std_dev # Need odometry uncertainty for calculating movement probabilities self.vel_uniform_dist = vel_uniform_dist # Want to keep track of how many particles we are going to have in the swarm self.num_particles = num_particles # If there is a starting position for the robot we want to make use of it # in order to instantly converge on that spot self.start_pos = start_pos # If we want debug messages, let us know! self.debug = debug # Pre-calculate factors for calculating beam unertainty since these are # done millions of times per filter (Well... up to 50 times per beam per # particle, 30 beams or so, say 1000 particles -> 1.5 million times) self.laser_prob_f_1 = (1 / (math.sqrt(2.0*math.pi*(self.sens_std_dev**2.0)))) self.laser_prob_f_2 = -0.5 / (self.sens_std_dev**2.0) # Set up map storage facilities self.map_s = MapStorage(self.debug) # Set up a publisher for publishing the (assumed) position of the robot # found using the particle filter self.particle_p = ParticlePose("map") # Set up a transform listener and broadcaster for utility use self.tf = tf.TransformListener() # Pre-allocate odometry buffers for use when calculating the movement # of particles in the swarm self.lin_vel_buffer = list() self.ang_vel_buffer = list() # Receive odometry self.odom = Odometry() rospy.Subscriber("odom", Odometry, self.get_odom) # Receive laser scan self.scan = LaserScan() rospy.Subscriber(base_scan, LaserScan, self.get_scan) # Initialize particle swarm self.particles = list() for index in range(self.num_particles): self.particles.append(copy.deepcopy(Particle())) self.initiate_particles() # Sleep for a bit to allow data and transforms to be received rospy.sleep(0.2) # Create default marker for display of swarm / debug point_marker = Marker() """ Settings for particle markers with pose """ point_marker.type = Marker.ARROW point_marker.action = Marker.ADD point_marker.scale.x = 1.25 point_marker.scale.y = 0.25 point_marker.scale.z = 0.25 point_marker.color.r = 0.0 point_marker.color.g = 1.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 """ Settings for beam tracing particles point_marker.type = Marker.SPHERE point_marker.action = Marker.ADD point_marker.scale.x = 0.05 point_marker.scale.y = 0.05 point_marker.scale.z = 0.05 point_marker.color.r = 0.0 point_marker.color.g = 1.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 """ self.mark_p = MarkerPlacer("rviz_particles", "map", 1000, point_marker)
class ParticleFilter(object): """docstring for ParticleFilter""" def __init__(self, odom_rate, sens_median, sens_std_dev, vel_uniform_dist, num_particles, base_scan, start_pos=None, debug=False): # Odom rate is used for re-positioning of particles and assumed pose after applying filter self.odom_rate = odom_rate # Need sensory uncertainty distribution for calculating particle beam probabilities self.sens_median = sens_median self.sens_std_dev = sens_std_dev # Need odometry uncertainty for calculating movement probabilities self.vel_uniform_dist = vel_uniform_dist # Want to keep track of how many particles we are going to have in the swarm self.num_particles = num_particles # If there is a starting position for the robot we want to make use of it # in order to instantly converge on that spot self.start_pos = start_pos # If we want debug messages, let us know! self.debug = debug # Pre-calculate factors for calculating beam unertainty since these are # done millions of times per filter (Well... up to 50 times per beam per # particle, 30 beams or so, say 1000 particles -> 1.5 million times) self.laser_prob_f_1 = (1 / (math.sqrt(2.0*math.pi*(self.sens_std_dev**2.0)))) self.laser_prob_f_2 = -0.5 / (self.sens_std_dev**2.0) # Set up map storage facilities self.map_s = MapStorage(self.debug) # Set up a publisher for publishing the (assumed) position of the robot # found using the particle filter self.particle_p = ParticlePose("map") # Set up a transform listener and broadcaster for utility use self.tf = tf.TransformListener() # Pre-allocate odometry buffers for use when calculating the movement # of particles in the swarm self.lin_vel_buffer = list() self.ang_vel_buffer = list() # Receive odometry self.odom = Odometry() rospy.Subscriber("odom", Odometry, self.get_odom) # Receive laser scan self.scan = LaserScan() rospy.Subscriber(base_scan, LaserScan, self.get_scan) # Initialize particle swarm self.particles = list() for index in range(self.num_particles): self.particles.append(copy.deepcopy(Particle())) self.initiate_particles() # Sleep for a bit to allow data and transforms to be received rospy.sleep(0.2) # Create default marker for display of swarm / debug point_marker = Marker() """ Settings for particle markers with pose """ point_marker.type = Marker.ARROW point_marker.action = Marker.ADD point_marker.scale.x = 1.25 point_marker.scale.y = 0.25 point_marker.scale.z = 0.25 point_marker.color.r = 0.0 point_marker.color.g = 1.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 """ Settings for beam tracing particles point_marker.type = Marker.SPHERE point_marker.action = Marker.ADD point_marker.scale.x = 0.05 point_marker.scale.y = 0.05 point_marker.scale.z = 0.05 point_marker.color.r = 0.0 point_marker.color.g = 1.0 point_marker.color.b = 0.0 point_marker.color.a = 1.0 """ self.mark_p = MarkerPlacer("rviz_particles", "map", 1000, point_marker) def get_odom(self, odom): self.odom = odom # Buffer odom data for use when updating particles self.lin_vel_buffer.append(self.odom.twist.twist.linear.x) self.ang_vel_buffer.append(self.odom.twist.twist.angular.z) if self.debug: # print(self.odom) pass def get_scan(self, scan): self.scan = scan if self.debug: # print(self.scan) pass def initiate_particles(self): # If we have a starting position, use that if self.start_pos is not None: for particle in self.particles: particle.p[0] = self.start_pos[0] particle.p[1] = self.start_pos[1] particle.o = self.start_pos[2] # If we do not spread the particles out throughout the map else: min_x = self.map_s.min_x_pos max_x = self.map_s.max_x_pos min_y = self.map_s.min_y_pos max_y = self.map_s.max_y_pos for particle in self.particles: particle.randomize(min_x, max_x, min_y, max_y) def move_particles(self): # Copy and zero odom buffers lin_vel_list = self.lin_vel_buffer self.lin_vel_buffer = list() ang_vel_list = self.ang_vel_buffer self.ang_vel_buffer = list() # Store the most recent scan for use with filter after movement is # complete self.odom_synced_scan = copy.deepcopy(self.scan) # For each particle change its position and rotation according to each # buffered data input with a uniform distribution for particle in self.particles: for i in range(len(lin_vel_list)): lin_vel = lin_vel_list[i] + \ np.random.uniform(-self.vel_uniform_dist, self.vel_uniform_dist) ang_vel = ang_vel_list[i] + \ np.random.uniform(-self.vel_uniform_dist, self.vel_uniform_dist) d_theta = ang_vel / self.odom_rate d_x = (lin_vel * math.cos(particle.o))/self.odom_rate d_y = (lin_vel * math.sin(particle.o))/self.odom_rate particle.p[0] += d_x particle.p[1] += d_y particle.o += d_theta def place_markers(self): pos_list = list() or_list = list() for index in range(len(self.particles)): # print("placing markers index: " + str(index)) pos_list.append(Point(self.particles[index].p[0], self.particles[index].p[1], 0.0)) quat = quaternion_from_euler(0.0, 0.0, self.particles[index].o) or_list.append(Quaternion(quat[0], quat[1], quat[2], quat[3])) self.mark_p.place_marker(pos_list, or_list) def sensor_probability(self, meas, exp): f_2 = math.exp(self.laser_prob_f_2 * (meas-exp)**2.0) prob = self.laser_prob_f_1 * f_2 return prob def apply_filter(self): # Copy all scan parameters so that they do not change while we are # looping through all the parameters since this takes a while d_theta = self.odom_synced_scan.angle_increment scan_min_angle = self.odom_synced_scan.angle_min scan_max_angle = self.odom_synced_scan.angle_max scan_max_range = self.odom_synced_scan.range_max ranges = self.odom_synced_scan.ranges map_res = self.map_s.resolution # Make an empty list for storing the probability for each particle probability_list = list() max_probability = 0 # For all particles: min_x = self.map_s.min_x_pos max_x = self.map_s.max_x_pos min_y = self.map_s.min_y_pos max_y = self.map_s.max_y_pos for i in range(len(self.particles)): # Check if the particle is in a valid position. If it is not # regenerate the position. x_r_m = self.particles[i].p[0] y_r_m = self.particles[i].p[1] while self.map_s.xy_pos_is_occupied(x_r_m, y_r_m): self.particles[i].randomize(min_x, max_x, min_y, max_y) x_r_m = self.particles[i].p[0] y_r_m = self.particles[i].p[1] # Get P(x_r_m,y_r_m,theta_r_m) (robot in map frame) from stored # particle point_r_m = PointStamped() point_r_m.point.x = self.particles[i].p[0] point_r_m.point.y = self.particles[i].p[1] point_r_m.point.z = 0.0 point_r_m.header.frame_id = "base_link" point_r_m.header.stamp = self.tf.getLatestCommonTime("base_link", "base_laser_link") # Transform in to L(x_L_m,y_L_m,theta_L_m) (laser in map frame) # using /base_link to /base_laser_link (need a transform listener!) point_l_m = self.tf.transformPoint("base_laser_link", point_r_m) x_l_m = point_l_m.point.x y_l_m = point_l_m.point.y # The orientation of the laser scanner in the map frame is the # same as the robot, so we are taking a shortcut here theta_l_m = self.particles[i].o # Publish a transform that will be used to translate from a point # in the laser frame of reference to the laser in map frame of # reference. theta_b_l = scan_min_angle theta_b_l_max = scan_max_angle beam_index = 0 probability = 0 """ Debug to trace beam! pos_list = list() or_list = list() """ # For each beam angle theta_b_L (beam in laser frame of reference) while theta_b_l < theta_b_l_max + d_theta/10.0: # Find check angle theta_b_m = # theta_b_L + theta_L_m (beam in map frame of reference) theta_b_m = theta_l_m + theta_b_l x_slope = math.cos(theta_b_m)*float(map_res) y_slope = math.sin(theta_b_m)*float(map_res) # For each N deltaDistance, N = 1 -> # N = maxDistance/resolution, deltaDistance = map resolution for N in range(1, int(scan_max_range/map_res)): # Check if x_b_m = # x_L_m + x_b_L = # x_L_m + N*dDist*cos(theta_L_m + theta_b_L) and # y_b_m = # y_L_m + y_b_L = # y_L_m + N*dDist*sin(theta_L_m + theta_b_L) is occupied. x_b_m = x_l_m + float(N)*x_slope y_b_m = y_l_m + float(N)*y_slope """ Debug to trace beam! pos_list.append(Point(x_b_m, y_b_m, 0.0)) or_list.append(Quaternion(0.0, 0.0, 0.0, 0.0)) """ # If occupied calculate expected distance between them and # exit loop if self.map_s.xy_pos_is_occupied(x_b_m, y_b_m): exp_dist = self.dist(x_l_m, y_l_m, x_b_m, y_b_m) break # If no intersection was detected use max range as expected # distance # (PS: I was amazed this was possible - an else clause if the # break in the loop was not executed!) else: exp_dist = scan_max_range # Calculate probability for this beam angle measured_range = np.random.normal(ranges[beam_index], self.sens_std_dev) exp_dist = exp_dist """ measured_range = Decimal(ranges[beam_index]) exp_dist = Decimal(exp_dist) """ probability_beam = self.sensor_probability(measured_range, exp_dist) # Add to previous calculated probabilities probability += probability_beam # Increment theta of beam and index of beam theta_b_l += 2.0*d_theta beam_index += 2 """ Debug to trace beam self.mark_p.place_marker(pos_list, or_list) """ # Add probability for particle to list probability_list.append(probability) # Check if the beam probability is the "best", if it is store the # position as the assumed particle swarm position if probability > max_probability: max_probability = probability self.avg_x = x_r_m self.avg_y = y_r_m self.avg_theta = theta_l_m # print(probability_list) # Do hat-search using the weighting in probability list and add found # particles to new list new_list = self.hat_search(probability_list) # Assign new list to be used for next iteration self.particles = new_list def dist(self, x0, y0, x1, y1): d_x = abs(x0-x1) d_y = abs(y0-y1) return math.sqrt(d_x**2.0 + d_y**2.0) def hat_search(self, prob_list): new_part_list = list() sum_x = 0.0 sum_y = 0.0 sum_theta = 0.0 sum_count = 0 total = sum(prob_list) # Ensure that we have at least one value with probability larger than 0 if total >= 0: end_hat = len(self.particles) - len(self.particles)/20 for i in range(end_hat): rnd_num = sp.random.uniform(0.0, total) pick_index = 0 picking = True while picking: rnd_num -= prob_list[pick_index] if rnd_num <= 0: # Add x, y, theta to sum """ sum_x += self.particles[pick_index].p[0] sum_y += self.particles[pick_index].p[1] sum_theta += self.particles[pick_index].o sum_count += 1 """ new_part = Particle([self.particles[pick_index].p[0], self.particles[pick_index].p[1]], self.particles[pick_index].o) new_part_list.append(new_part) picking = False else: pick_index += 1 min_x = self.map_s.min_x_pos max_x = self.map_s.max_x_pos min_y = self.map_s.min_y_pos max_y = self.map_s.max_y_pos for i in range(end_hat, len(self.particles)): new_part = Particle() new_part.randomize(min_x, max_x, min_y, max_y) new_part_list.append(copy.deepcopy(new_part)) # If the total for some reason ended up being all zeroes let us just # use the old list, but also need to do the centre of mass calculation # for publishing centre off mass else: new_part_list = self.particles for i in range(self.particles): # Add x, y, theta to sum sum_x += self.particles[i].p[0] sum_y += self.particles[i].p[1] sum_theta += self.particles[i].o sum_count += 1 """ self.avg_x = sum_x/sum_count self.avg_y = sum_y/sum_count self.avg_theta = sum_theta/sum_count """ print("total sum: " + str(total)) return new_part_list def run(self): # First we want to update the particle locations using the most recent # odometry data self.move_particles() # Then apply filter to produce a list of new and higher probability # particles self.apply_filter() # Place a selection of markers in rviz to illustrate the particle cloud # self.mark_p.clear_markers() # self.place_markers() # Update the position with most recent odometry updates received since # particle filter was started self.update_particle_pose() # Publish the centre off mass position and orientation for particles avg_rot = quaternion_from_euler(0.0, 0.0, self.avg_theta) self.particle_p.publish(Point(self.avg_x, self.avg_y, 0.0), Quaternion(*avg_rot)) def update_particle_pose(self): lin_vel_list = self.lin_vel_buffer ang_vel_list = self.ang_vel_buffer for i in range(len(lin_vel_list)): lin_vel = lin_vel_list[i] + \ np.random.uniform(-self.vel_uniform_dist, self.vel_uniform_dist) ang_vel = ang_vel_list[i] + \ np.random.uniform(-self.vel_uniform_dist, self.vel_uniform_dist) d_theta = ang_vel / self.odom_rate d_x = (lin_vel * math.cos(self.avg_theta))/self.odom_rate d_y = (lin_vel * math.sin(self.avg_theta))/self.odom_rate self.avg_x += d_x self.avg_y += d_y self.avg_theta += d_theta
class DriftingPoseBroadcaster(object): def __init__(self): rospy.init_node("drifting_pose_broadcaster") self.odom = Odometry() self.br = tf.TransformBroadcaster() self.rate = rospy.Rate(10) self.linear = Point(0.0, 0.0, 0.0) self.quat = Quaternion(0.0, 0.0, 0.0, 0.0) self.yaw = 0.0 self.odom_initialised = False rospy.Subscriber("base_pose_ground_truth", Odometry, self.get_real_odom) rospy.Subscriber("odom", Odometry, self.get_drift_odom) self.mark_placer = MarkerPlacer("rviz_drifting_robot_pose", "map") self.mark_placer.set_color(0.674509804, 0.337254902, 0.0, 1.0) while not rospy.is_shutdown(): self.br.sendTransform(translation=(self.linear.x, self.linear.y, self.linear.z), rotation=(self.quat.x, self.quat.y, self.quat.z, self.quat.w), time=rospy.Time.now(), child="drifting_robot_pose", parent="map") self.mark_placer.place_marker([self.linear], [self.quat]) self.rate.sleep() def get_drift_odom(self, odometry): if self.odom_initialised: self.odom = odometry time = rospy.get_time() d_time = time-self.last_time self.last_time = time # Get the linear and angular velocities lin_vel = self.odom.twist.twist.linear.x ang_vel = self.odom.twist.twist.angular.z # Calculate change in yaw, x and y d_theta = ang_vel * d_time d_x = (lin_vel * math.cos(self.yaw))*d_time d_y = (lin_vel * math.sin(self.yaw))*d_time # Update position self.yaw += d_theta self.linear.x += d_x self.linear.y += d_y # Calculate quat used to broadcast transform and place marker quat = tf.transformations.quaternion_from_euler(0.0, 0.0, self.yaw) self.quat = Quaternion(*quat) def get_real_odom(self, odometry): if not self.odom_initialised: self.odom = odometry self.linear = self.odom.pose.pose.position self.quat = self.odom.pose.pose.orientation # Also calculate the euler yaw since this will reduce the number of # transformations required in other parts of the program pitch, roll, yaw = tf.transformations.euler_from_quaternion([self.quat.x, self.quat.y, self.quat.z, self.quat.w]) self.yaw = yaw self.last_time = rospy.get_time() self.odom_initialised = True