-3

I am trying to print particle on Rviz for ParticleFilterLocalization. In the attached file Line #Start of Task1 to #End of Task1; is where I am trying to print partciles. However they arenot printing at all.

Rest of The code has Map which is already displaying correctly.

Can anyone suggest, what I am missing.

TRied printing out random values which worked. But Displaying particles is not working.

#!/usr/bin/env python3

import rospy
import math
from nav_msgs.msg import OccupancyGrid
from nav_msgs.msg import Odometry
from nav_msgs.srv import GetMap
from geometry_msgs.msg import Pose
from geometry_msgs.msg import PoseArray
from geometry_msgs.msg import PoseStamped
from geometry_msgs.msg import TransformStamped
from sensor_msgs.msg import LaserScan
# import cv2 as cv # OpenCV2
import random
import numpy as np
import copy
import tf2_ros
from threading import Lock
from visualization_msgs.msg import Marker
from geometry_msgs.msg import Point
from std_msgs.msg import Float32

from distance_transform import distance_transform
import time


class Particle:
    def __init__(self, x, y, theta, weight):
        self.x = x
        self.y = y
        self.theta = theta
        self.weight = weight

def wrap_angle(angle):
    # Function to wrap an angle between 0 and 2*Pi
    while angle < 0.0:
        angle = angle + 2 * math.pi

    while angle > 2 * math.pi:
        angle = angle - 2 * math.pi

    return angle

def random_uniform(a, b):
    # Returns a random number with uniform distribution between "a" and "b"
    if b < a:
        rospy.logerror("The first argument must be less than the second argument when using the random_uniform method")
    return random.uniform(a, b)

def random_normal(stddev):
    # Returns a random number with normal distribution, 0 mean and a standard deviation of "stddev"
    return np.random.normal(0.0, stddev)

class ParticleFilter:
    def __init__(self):

        # Parameters
        # Default values will be used if no ROS parameter exists (see launch file)
        self.num_particles_ = rospy.get_param("~num_particles", 500) # Number of particles
        self.num_motion_updates_ = rospy.get_param("~num_motion_updates", 5) # Number of motion updates before a sensor update
        self.num_scan_rays_ = rospy.get_param("~num_scan_rays", 6) # (Approximate) number of scan rays to evaluate
        self.num_sensing_updates_ = rospy.get_param("~num_sensing_updates", 5) # Number of sensing updates before resampling
        self.motion_distance_noise_stddev_ = rospy.get_param("~motion_distance_noise_stddev", 0.01) # Standard deviation of distance noise for motion update
        self.motion_rotation_noise_stddev_ = rospy.get_param("~motion_rotation_noise_stddev", math.pi / 60.) # Standard deviation of rotation noise for motion update
        self.sensing_noise_stddev_ = rospy.get_param("~sensing_noise_stddev", 0.5) # Standard deviation of sensing noise
        self.magnetometer_noise_stddev_ = 0.349066 # 20 degrees # Standard deviation of magnetometer noise

        # Get the map via a ROS service call
        rospy.loginfo("Waiting for static_map service...")
        rospy.wait_for_service('static_map')
        get_map_service = rospy.ServiceProxy('static_map', GetMap)
        try:
            resp = get_map_service()
            self.map_ = resp.map
        except rospy.ServiceException as exc:
            rospy.logerror('Service did not process request: ' + str(exc))
            rospy.signal_shutdown()
        rospy.loginfo("Map received")

        # Convert occupancy grid into a numpy array
        self.map_image_ = np.reshape(self.map_.data, (self.map_.info.height, self.map_.info.width)).astype(np.int32)
        print(self.map_image_)

        # Limits of map in meters
        self.map_x_min_ = self.map_.info.origin.position.x
        self.map_x_max_ = self.map_.info.width * self.map_.info.resolution + self.map_.info.origin.position.x
        self.map_y_min_ = self.map_.info.origin.position.y
        self.map_y_max_ = self.map_.info.height * self.map_.info.resolution + self.map_.info.origin.position.y

        # Preprocess the distance transform for fast ray casting
        self.map_image_distance_transform_ = distance_transform(self.map_image_)

        # Variables
        self.particles_ = [] # Vector that holds the particles
        self.prev_odom_msg_ = None # Stores the previous odometry message to determine distance travelled
        self.estimated_pose_ = Pose() 
        self.compass_ = 0.
        self.compass_valid_ = False

        # Counters
        self.motion_update_count_ = 0 # Number of motion updates since last sensor update
        self.sensing_update_count_ = 0 # Number of sensing updates since resampling
        self.estimated_pose_valid_ = False # Don't use the estimated pose just after initialisation

        # Create a thread lock to avoid having different callbacks changing the particles_
        self.lock_ = Lock()
        self.lock_.acquire()

        # ROS Subscribers
        self.odom_sub_ = rospy.Subscriber('odom', Odometry, self.odom_callback, queue_size=1) # Subscribes to wheel odometry
        self.scan_sub_ = rospy.Subscriber('base_scan', LaserScan, self.scan_callback, queue_size=1) # Subscribes to laser scan
        self.compass_sub_ = rospy.Subscriber('compass', Float32, self.compass_callback, queue_size=1) # Subscribes to compass

        # ROS Publishers
        self.particles_pub_ = rospy.Publisher('particles', PoseArray, queue_size=1)
        self.particles_seq_ = 0
        self.particles_pub_timer_ = rospy.Timer(rospy.Duration(0.1), self.publish_particles)

        self.estimated_pose_pub_ = rospy.Publisher('estimated_pose', PoseStamped, queue_size=1)
        self.estimated_pose_seq_ = 0
        self.estimated_pose_pub_timer_ = rospy.Timer(rospy.Duration(0.1), self.publish_estimated_pose)

        self.transform_broadcaster_ = tf2_ros.TransformBroadcaster()
        self.transform_seq_ = 0

        # Marker
        self.marker_ = Marker()
        self.marker_.header.frame_id = "map"
        self.marker_.ns = "ns"
        self.marker_.id = 0
        self.marker_.type = Marker.LINE_LIST
        self.marker_.action = Marker.ADD
        self.marker_.pose.position.x = 0.0
        self.marker_.pose.position.y = 0.0
        self.marker_.pose.position.z = 0.0
        self.marker_.pose.orientation.x = 0.0
        self.marker_.pose.orientation.y = 0.0
        self.marker_.pose.orientation.z = 0.0
        self.marker_.pose.orientation.w = 1.0
        self.marker_.scale.x = 0.01
        self.marker_.scale.y = 0.1
        self.marker_.scale.z = 0.1
        self.marker_.color.a = 1.0
        self.marker_.color.r = 0.0
        self.marker_.color.g = 0.7
        self.marker_.color.b = 0.2
        self.marker_pub_ = rospy.Publisher('marker', Marker, queue_size=1)


        # Initialise particles
        self.initialise_particles()

        self.lock_.release()

    def initialise_particles(self):
        # "num_particles_" is the number of particles you will create

        # Clear the particles array
        self.particles_ = []

        # You want to initialise the particles in the "particles_" array
        # "random_uniform(a, b)" will give you a random value with uniform distribution between "a" and "b"
        # "self.map_x_min_", "self.map_x_max_", "self.map_y_min_", and "self.map_y_max_" give you the limits of the map
        # Orientation (theta) should be between 0 and 2*Pi
        # You probably need to use a "." in your numbers (e.g. "1.0") when calculating the weights

        # Start of Task 1: Initialise the particles


class ParticleFilterLocalization:
    def __init__(self, num_particles, map_x_min, map_x_max, map_y_min, map_y_max):
        self.num_particles_ = num_particles
        self.map_x_min_ = map_x_min
        self.map_x_max_ = map_x_max
        self.map_y_min_ = map_y_min
        self.map_y_max_ = map_y_max
        self.particles_ = []


        num_particles_=500
        self.map_x_min_=0
        self.map_x_max_=100
        self.map_y_min_=0
        self.map_y_max_=100
        
        for _ in range(self.num_particles_):
        # Initialize particle pose with random values within the map boundaries
             x = random_uniform(self.map_x_min_, self.map_x_max_)
             rospy.loginfo('Value of  x' + str(x))
             y = random_uniform(self.map_y_min_, self.map_y_max_)
             rospy.loginfo('Value of  y' + str(y))
             theta = random_uniform(0, 2 * math.pi)  # Angle between 0 and 2 * Pi
             weight = 1.0  # Initial weight

        # Initialize particle weight
        weight = 1.0 / self.num_particles_  # Uniform weight distribution initially

        # Create a Particle instance and add it to the particles list
        particle = Particle(x, y, theta, weight)
        self.particles_.append(particle)
        

    
        particle_filter = ParticleFilterLocalization(num_particles, map_x_min, map_x_max, map_y_min, map_y_max)
        particle_filter.initialise_particles()

 # End of Task 1: Initialise the particles

        



        # Particles may be initialised in occupied space but the map has thin walls so it should be OK
        # TODO inflate the occupancy grid and check that particles are not in occupied space
        # Don't use the estimated pose just after initialisation
    def normalise_weights(self):
        # Normalise the weights of the particles in "particles_"

    
         def normalise_weights(self):
        # Calculate the total weight sum dynamically
                total_weight = sum(particle[3] for particle in self.particles_)
        
         if total_weight > 0:
            # Normalize the weights
            for i in range(len(self.particles_)):
                self.particles_[i] = (
                    self.particles_[i][0],
                    self.particles_[i][1],
                    self.particles_[i][2],
                    self.particles_[i][3] / total_weight
                )
         else:
            # If total_weight is 0, assign equal weights to all particles
            equal_weight = 1.0 / len(self.particles_)
            for i in range(len(self.particles_)):
                self.particles_[i] = (
                    self.particles_[i][0],
                    self.particles_[i][1],
                    self.particles_[i][2],
                    equal_weight
                )


    pass # Added to avoid an Indentation Error



    def hit_scan(self, start_x, start_y, theta, max_range, draw=False):
        # Find the nearest obstacle from position start_x, start_y (in metres) in direction theta

        # Start point in occupancy grid coordinates
        start_point = [int(round((start_x - self.map_.info.origin.position.x) / self.map_.info.resolution)),
                             int(round((start_y - self.map_.info.origin.position.y) / self.map_.info.resolution))]

        # End point in real coordinates
        end_x = start_x + math.cos(theta) * max_range
        end_y = start_y + math.sin(theta) * max_range

        # End point in occupancy grid coordinates
        end_point = [int(round((end_x - self.map_.info.origin.position.x) / self.map_.info.resolution)),
                           int(round((end_y - self.map_.info.origin.position.y) / self.map_.info.resolution))]

        # Find the first "hit" along scan
        # (unoccupied is value 0, occupied is value 100)
        # hit = find_hit(self.map_image_, start_point, end_point, 50)
        hit = find_hit_df(self.map_image_distance_transform_, start_point, end_point)

        # Convert hit back to world coordinates
        hit_x = hit[0] * self.map_.info.resolution + self.map_.info.origin.position.x
        hit_y = hit[1] * self.map_.info.resolution + self.map_.info.origin.position.y

        # Add a debug visualisation marker
        if draw:
            point = Point(start_x, start_y, 0.)
            self.marker_.points.append(point)
            point = Point(hit_x, hit_y, 0.)
            self.marker_.points.append(point)

        # Get distance to hit
        return math.sqrt(math.pow(start_x - hit_x, 2) + math.pow(start_y - hit_y, 2))


    def estimate_pose(self):
        # Position of the estimated pose
        estimated_pose_x = 0.0
        estimated_pose_y = 0.0
        estimated_pose_theta = 0.0

        # Choose a method to estimate the pose from the particles in the "particles_" vector
        # Put the values into "estimated_pose_x", "estimated_pose_y", and "estimated_pose_theta"
        # If you just use the pose of the particle with the highest weight the maximum mark you can get for this part is 0.5

        ####################
        ## YOUR CODE HERE ##
        ####################
        








        # Set the estimated pose message
        self.estimated_pose_.position.x = estimated_pose_x
        self.estimated_pose_.position.y = estimated_pose_y

        self.estimated_pose_.orientation.w = math.cos(estimated_pose_theta / 2.)
        self.estimated_pose_.orientation.z = math.sin(estimated_pose_theta / 2.)

        self.estimated_pose_valid_ = True


    def resample_particles(self):
        # Resample the particles
        # Weights are expected to be normalised

        # Copy old particles
        old_particles = copy.deepcopy(self.particles_)
        self.particles_ = []

        # Iterator for old_particles
        old_particles_i = 0

        # Find a new set of particles by randomly stepping through the old set, biased by their probabilities
        while len(self.particles_) < self.num_particles_:
            value = random_uniform(0.0, 1.0)
            sum = 0.0

            # Loop until a particle is found
            particle_found = False
            while not particle_found:

                # If the random value is between the sum and the sum + the weight of the particle
                if value > sum and value < (sum + old_particles[old_particles_i].weight):

                    # Add the particle to the "particles_" vector
                    self.particles_.append(copy.deepcopy(old_particles[old_particles_i]))

                    # Add jitter to the particle
                    self.particles_[-1].x = self.particles_[-1].x + random_normal(0.02)
                    self.particles_[-1].y = self.particles_[-1].y + random_normal(0.02)
                    self.particles_[-1].theta = wrap_angle(self.particles_[-1].theta + random_normal(math.pi / 30.))

                    # The particle may be out of the map, but that will be fixed by the motion update
                    
                    # Break out of the loop
                    particle_found = True

                # Add particle weight to sum and increment the iterator
                sum = sum + old_particles[old_particles_i].weight
                old_particles_i = old_particles_i + 1

                # If the iterator is past the vector, loop back to the beginning
                if old_particles_i >= len(old_particles):
                    old_particles_i = 0

        # Normalise the new particles
        self.normalise_weights()

        # Don't use the estimated pose just after resampling
        self.estimated_pose_valid_ = False

        # Induce a sensing update
        self.motion_update_count_ = self.num_motion_updates_      


    def publish_particles(self, event):
        self.lock_.acquire()
        pose_array = PoseArray()

        pose_array.header.stamp = rospy.Time.now()
        pose_array.header.frame_id = "map"
        pose_array.header.seq = self.particles_seq_
        self.particles_seq_ = self.particles_seq_ + 1

        for p in self.particles_:
            pose = Pose()
            pose.position.x = p.x
            pose.position.y = p.y
            pose.orientation.w = math.cos(p.theta / 2.)
            pose.orientation.z = math.sin(p.theta / 2.)

            pose_array.poses.append(pose)

        self.particles_pub_.publish(pose_array)
        self.lock_.release()

    def publish_estimated_pose(self, event):        
        if not self.estimated_pose_valid_:
            return

        self.lock_.acquire()

        # Publish the estimated pose
        pose_stamped = PoseStamped()
        pose_stamped.header.frame_id = "map"
        pose_stamped.header.stamp = rospy.Time.now()
        pose_stamped.header.seq = self.estimated_pose_seq_
        self.estimated_pose_seq_ = self.estimated_pose_seq_ + 1

        pose_stamped.pose = self.estimated_pose_

        self.estimated_pose_pub_.publish(pose_stamped)

        # Broadcast "map" to "base_footprint" transform
        transform = TransformStamped()

        transform.header.frame_id = "map"
        transform.header.stamp = rospy.Time.now()
        transform.header.seq = self.transform_seq_
        self.transform_seq_ = self.transform_seq_ + 1

        transform.child_frame_id = "base_footprint"

        transform.transform.translation.x = self.estimated_pose_.position.x
        transform.transform.translation.y = self.estimated_pose_.position.y

        transform.transform.rotation.w = self.estimated_pose_.orientation.w
        transform.transform.rotation.z = self.estimated_pose_.orientation.z

        # self.transform_broadcaster_.sendTransform(transform) # Commented by Graeme to avoid warning
        self.lock_.release()

    def odom_callback(self, odom_msg):
        # Receive an odometry message

        # Skip the first call since we are looking for movements
        if not self.prev_odom_msg_:
            self.prev_odom_msg_ = odom_msg
            return

        # Distance moved since the previous odometry message
        global_delta_x = odom_msg.pose.pose.position.x - self.prev_odom_msg_.pose.pose.position.x
        global_delta_y = odom_msg.pose.pose.position.y - self.prev_odom_msg_.pose.pose.position.y

        distance = math.sqrt(math.pow(global_delta_x, 2.) + math.pow(global_delta_y, 2.))

        # Previous robot orientation
        prev_theta = 2 * math.acos(self.prev_odom_msg_.pose.pose.orientation.w)

        if self.prev_odom_msg_.pose.pose.orientation.z < 0.:
            prev_theta = -prev_theta

        # Figure out if the direction is backward
        if (prev_theta < 0. and global_delta_y > 0.) or (prev_theta > 0. and global_delta_y < 0.):
            distance = -distance

        # Current orientation
        theta = 2 * math.acos(odom_msg.pose.pose.orientation.w)

        if odom_msg.pose.pose.orientation.z < 0.:
            theta = -theta

        # Rotation since the previous odometry message
        rotation = theta - prev_theta

        # Return if the robot hasn't moved
        if distance == 0 and rotation == 0:
            return

        self.lock_.acquire()

        # Motion update: add "distance" and "rotation" to each particle
        # You also need to add noise, which should be different for each particle
        # Use "random_normal()" with "self.motion_distance_noise_stddev_" and "self.motion_rotation_noise_stddev_" to get random values
        # You will probably need "math.cos()" and "math.sin()", and you should wrap theta with "wrap_angle()" too

        ####################
        ## YOUR CODE HERE ##
        ####################
        













        # Overwrite the previous odometry message
        self.prev_odom_msg_ = odom_msg

        # Delete any particles outside of the map
        old_particles = copy.deepcopy(self.particles_)
        self.particles_ = []

        for p in old_particles:
            if not(p.x < self.map_x_min_ or p.x > self.map_x_max_ or p.y < self.map_y_min_ or p.y > self.map_y_max_):
                # Keep it
                self.particles_.append(p)

        # Normalise particle weights because particles have been deleted
        self.normalise_weights()

        # If the estimated pose is valid move it too
        if self.estimated_pose_valid_:
            estimated_pose_theta = 2. * math.acos(self.estimated_pose_.orientation.w)

            if self.estimated_pose_.orientation.z < 0.:
                estimated_pose_theta = -estimated_pose_theta

            self.estimated_pose_.position.x += math.cos(estimated_pose_theta) * distance
            self.estimated_pose_.position.y += math.sin(estimated_pose_theta) * distance

            estimated_pose_theta = wrap_angle(estimated_pose_theta + rotation)

            self.estimated_pose_.orientation.w = math.cos(estimated_pose_theta / 2.)
            self.estimated_pose_.orientation.z = math.sin(estimated_pose_theta / 2.)

        # Increment the motion update counter
        self.motion_update_count_ = self.motion_update_count_ + 1

        self.lock_.release()

    def scan_callback(self, scan_msg):
        # Receive a laser scan message

        # Only do a sensor update after num_motion_updates_
        if self.motion_update_count_ < self.num_motion_updates_:
            return

        self.lock_.acquire()

        # Determine step size (the step may not result in the correct number of rays
        step = int(math.floor(float(len(scan_msg.ranges)) / self.num_scan_rays_))

        # Setup visualisation marker
        self.marker_.points = []

        start = time.time()

        # For each particle
        first_particle = True
        for p in self.particles_:
    
            # The likelihood of the particle is the product of the likelihood of each ray
            likelihood = 1.

            # Compare each scan ray
            for i in range(0, len(scan_msg.ranges), step): 
                # The range value from the scan message
                scan_range = scan_msg.ranges[i]

                # The angle of the ray in the frame of the robot
                local_angle = (scan_msg.angle_increment * i) + scan_msg.angle_min

                # The angle of the ray in the map frame
                global_angle = wrap_angle(p.theta + local_angle)

                # The expected range value for the particle
                particle_range = self.hit_scan(p.x, p.y, global_angle, 7.0, first_particle) #scan_msg.range_max)

                # Use "scan_range" and "particle_range" to get a likelihood
                # Multiply the ray likelihood into the "likelihood" variable
                # You will probably need "math.pi", math.sqrt()", "math.pow()", and "math.exp()"


                ####################
                ## YOUR CODE HERE ##
                ####################

                





            # Update the particle weight with the likelihood
            p.weight *= likelihood

            first_particle = False

        end = time.time()

        print('ray casting time', end - start)

        # Normalise particle weights
        self.normalise_weights()

        # Estimate the pose of the robot
        self.estimate_pose()

        self.motion_update_count_ = 0

        self.sensing_update_count_ = self.sensing_update_count_ + 1

        if self.sensing_update_count_ > self.num_sensing_updates_:
            self.resample_particles()
            self.sensing_update_count_ = 0

        self.lock_.release()

        # Publish debug marker
        self.marker_pub_.publish(self.marker_)


    def compass_fusion(self):

        # Return if haven't received a compass measurement
        if not self.compass_valid_:
            return

        # Extension task: Can you think of a way to incorporate the magnetometer measurements into the particles?
        #
        # Compass measurements are in self.compass_ and are in the same orientation as particle.theta 
        # Compass measurements have Gaussian noise with standard deviation self.magnetometer_noise_stddev_ 

        ####################
        ## YOUR CODE HERE ##
        ####################









    def compass_callback(self, compass_msg):

        self.lock_.acquire()

        self.compass_ = compass_msg.data
        self.compass_valid_ = True

        self.compass_fusion()

        self.lock_.release()


def find_hit(img, p1, p2, threshold):
    # Draws a line from p1 to p2
    # Stops at the first pixel that is a "hit", i.e. above the threshold
    # Returns the pixel coordinates for the first hit

    # Extract the vector
    x1 = float(p1[0])
    y1 = float(p1[1])
    x2 = float(p2[0])
    y2 = float(p2[1])

    step = 3.0 # pixels

    dx = x2 - x1
    dy = y2 - y1
    l = math.sqrt(dx**2. + dy**2.)
    dx = step * dx / l
    dy = step * dy / l

    max_steps = int(l / step)

    for i in range(max_steps):

        # Get the next pixel
        x = int(round(x1 + dx*i))
        y = int(round(y1 + dy*i))

        # Check if it's outside of the image
        if x < 0 or x >= img.shape[1] or y < 0 or y >= img.shape[0]:
            return [x, y] #p2

        # Check for "hit"
        if img[y, x] >= threshold:
            return [x, y]

    # No hits found
    return p2

def find_hit_df(img, p1, p2):
    # Draws a line from p1 to p2
    # Stops at the first pixel that is a "hit", i.e. above the threshold
    # Returns the pixel coordinates for the first hit
    #
    # similar to find_hit but uses distance transform image to speed things up

    # Extract the vector
    x1 = float(p1[0])
    y1 = float(p1[1])
    x2 = float(p2[0])
    y2 = float(p2[1])

    dx = x2 - x1
    dy = y2 - y1
    l = math.sqrt(dx**2. + dy**2.)
    dx = dx / l
    dy = dy / l

    step = 1.0 # pixels
    min_step = 2.5 # pixels -- too large risks jumping over obstacles

    max_steps = int(l)

    dist = 0

    while dist < max_steps:

        # Get the next pixel
        x = int(round(x1 + dx*dist))
        y = int(round(y1 + dy*dist))

        # Check if it's outside of the image
        if x < 0 or x >= img.shape[1] or y < 0 or y >= img.shape[0]:
            return [x, y] #p2

        current_distance = img[y, x] 

        # Check for "hit"
        if current_distance <= 0:
            return [x, y]

        # Otherwise, adjust the step size according to the distance transform
        step = current_distance-1.
        if step < min_step:
            step = min_step

        # Move along the ray
        dist += step

    # No hits found
    return p2


if __name__ == '__main__':
    # Create the ROS node
    rospy.init_node('particle_filter_localisation')

    # Create the particle filter
    ParticleFilter()

    # Loop forever while processing callbacks
    rospy.spin()
  • 2
    4 lines of explanation for 750 lines of code? No thanks. Nobody's going to want to help you until you remove all the code except the relevant code, and provide a [mre], and extensively demonstrate what debugging efforts you've gone through. The amount of disparity in the work you're asking other people to do vs. what you've provided in return is too large for anybody to volunteer their time. – Random Davis Aug 31 '23 at 15:17
  • Not sure what your task is or what the issue you have is. Just a shot in the dark here but did you define `__str__()` and `__repr__()` on your class you want to "print"? – JonSG Aug 31 '23 at 15:23

0 Answers0