ParticleOdometry.py
Go to the documentation of this file.
00001 #! /usr/bin/env python
00002 
00003 import rospy
00004 import numpy
00005 import scipy.stats
00006 import random
00007 import threading
00008 import itertools
00009 import tf
00010 import time
00011 import copy
00012 from operator import itemgetter 
00013 
00014 from nav_msgs.msg import Odometry
00015 from std_msgs.msg import Empty
00016 from sensor_msgs.msg import Imu
00017 from geometry_msgs.msg import PoseWithCovariance, TwistWithCovariance, Twist, Pose, Point, Quaternion, Vector3, TransformStamped
00018 from jsk_recognition_msgs.msg import HistogramWithRangeArray, HistogramWithRange, HistogramWithRangeBin
00019 from odometry_utils import norm_pdf_multivariate, transform_quaternion_to_euler, transform_local_twist_to_global, transform_local_twist_covariance_to_global, update_pose, update_pose_covariance, broadcast_transform
00020 from diagnostic_msgs.msg import *
00021 
00022 class ParticleOdometry(object):
00023     ## initialize
00024     def __init__(self):
00025         # init node
00026         rospy.init_node("ParticleOdometry", anonymous=True)
00027         # instance valiables
00028         self.rate = float(rospy.get_param("~rate", 100))
00029         self.particle_num = float(rospy.get_param("~particle_num", 100))
00030         self.valid_particle_num = min(float(rospy.get_param("~valid_particle_num", int(self.particle_num / 2.0))), self.particle_num) # select valid_particle_num particles in ascending weight sequence when estimating normal distributions
00031         self.odom_frame = rospy.get_param("~odom_frame", "feedback_odom")
00032         self.base_link_frame = rospy.get_param("~base_link_frame", "BODY")
00033         self.odom_init_frame = rospy.get_param("~odom_init_frame", "odom_init")
00034         self.z_error_sigma = rospy.get_param("~z_error_sigma", 0.01) # z error probability from source. z is assumed not to move largely from source odometry
00035         self.use_imu = rospy.get_param("~use_imu", False)
00036         self.use_imu_yaw = rospy.get_param("~use_imu_yaw", False) # referenced only when use_imu is True
00037         self.roll_error_sigma = rospy.get_param("~roll_error_sigma", 0.05) # roll error probability from imu. (referenced only when use_imu is True)
00038         self.pitch_error_sigma = rospy.get_param("~pitch_error_sigma", 0.05) # pitch error probability from imu. (referenced only when use_imu is True)
00039         self.yaw_error_sigma = rospy.get_param("~yaw_error_sigma", 0.1) # yaw error probability from imu. (referenced only when use_imu and use_imu_yaw are both True)
00040         self.min_weight = rospy.get_param("~min_weight", 1e-10)
00041         self.r = rospy.Rate(self.rate)
00042         self.lock = threading.Lock()
00043         self.odom = None
00044         self.source_odom = None
00045         self.measure_odom = None
00046         self.imu = None
00047         self.imu_rotation = None
00048         self.particles = None
00049         self.weights = []
00050         self.measurement_updated = False
00051         self.prev_rpy = None
00052         self.init_sigma = [rospy.get_param("~init_sigma_x", 0.1),
00053                            rospy.get_param("~init_sigma_y", 0.1),
00054                            rospy.get_param("~init_sigma_z", 0.0001),
00055                            rospy.get_param("~init_sigma_roll", 0.0001),
00056                            rospy.get_param("~init_sigma_pitch", 0.0001),
00057                            rospy.get_param("~init_sigma_yaw", 0.05)]
00058         # tf
00059         self.publish_tf = rospy.get_param("~publish_tf", True)
00060         if self.publish_tf:
00061             self.broadcast = tf.TransformBroadcaster()
00062             self.invert_tf = rospy.get_param("~invert_tf", True)
00063         # publisher
00064         self.pub = rospy.Publisher("~output", Odometry, queue_size = 1)
00065         self.diag_pub = rospy.Publisher('~diagnostics', DiagnosticArray)
00066         # histogram
00067         self.publish_histogram = rospy.get_param("~publish_histogram", False)
00068         if self.publish_histogram:
00069             self.pub_hist = rospy.Publisher("~particle_histograms", HistogramWithRangeArray, queue_size = 1)
00070         # subscriber
00071         self.source_odom_sub = rospy.Subscriber("~source_odom", Odometry, self.source_odom_callback, queue_size = 10)
00072         self.measure_odom_sub = rospy.Subscriber("~measure_odom", Odometry, self.measure_odom_callback, queue_size = 10)
00073         self.imu_sub = rospy.Subscriber("~imu", Imu, self.imu_callback, queue_size = 10) # imu is assumed to be in base_link_frame relative coords
00074         self.init_transform_sub = rospy.Subscriber("~initial_base_link_transform", TransformStamped, self.init_transform_callback) # init_transform is assumed to be transform of init_odom -> base_link
00075         # init
00076         self.initialize_odometry([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0])
00077 
00078     def initialize_odometry(self, trans, rot):
00079         with self.lock:
00080             self.particles = self.initial_distribution(Pose(Point(*trans), Quaternion(*rot)))
00081             self.weights = [1.0 / self.particle_num] * int(self.particle_num)
00082             self.odom = Odometry()
00083             mean, cov = self.guess_normal_distribution(self.particles, self.weights)
00084             self.odom.pose.pose = self.convert_list_to_pose(mean)
00085             self.odom.pose.covariance = list(itertools.chain(*cov))
00086             self.odom.twist.twist = Twist(Vector3(0, 0, 0), Vector3(0, 0, 0))
00087             self.odom.twist.covariance = numpy.diag([0.001**2]*6).reshape(-1,).tolist()
00088             self.odom.header.stamp = rospy.Time.now()
00089             self.odom.header.frame_id = self.odom_frame
00090             self.odom.child_frame_id = self.base_link_frame
00091             self.source_odom = None
00092             self.measure_odom = None
00093             self.measurement_updated = False
00094             self.imu = None
00095             self.imu_rotation = None
00096             self.prev_rpy = None
00097             
00098     ## particle filter functions
00099     # input: particles(list of pose), source_odom(control input)  output: list of sampled particles(pose)
00100     def sampling(self, particles, source_odom):
00101         global_twist_with_covariance = self.transform_twist_with_covariance_to_global(source_odom.pose, source_odom.twist)
00102         sampled_velocities = self.state_transition_probability_rvs(global_twist_with_covariance.twist, global_twist_with_covariance.covariance) # make sampeld velocity at once because multivariate_normal calculates invert matrix and it is slow
00103         dt = (source_odom.header.stamp - self.odom.header.stamp).to_sec()
00104         return [update_pose(prt, Twist(Vector3(*vel[0:3]), Vector3(*vel[3:6])), dt) for prt, vel in zip(particles, sampled_velocities)]
00105         
00106     # input: particles(list of pose), min_weight(float) output: list of weights
00107     def weighting(self, particles, min_weight):
00108         if not self.measure_odom:
00109             rospy.logwarn("[%s] measurement does not exist.", rospy.get_name())
00110             weights = [1.0 / self.particle_num] * int(self.particle_num) # use uniform weights when measure_odom has not been subscribed yet
00111         else:
00112             measure_to_source_dt = (self.source_odom.header.stamp - self.measure_odom.header.stamp).to_sec() # adjust timestamp of pose in measure_odom to source_odom
00113             current_measure_pose_with_covariance = self.update_pose_with_covariance(self.measure_odom.pose, self.measure_odom.twist, measure_to_source_dt) # assuming dt is small and measure_odom.twist is do not change in dt
00114             measurement_pose_array = numpy.array(self.convert_pose_to_list(current_measure_pose_with_covariance.pose))
00115             try:
00116                 measurement_cov_matrix_inv = numpy.linalg.inv(numpy.matrix(current_measure_pose_with_covariance.covariance).reshape(6, 6)) # calculate inverse matrix first to reduce computation cost
00117                 weights = [max(min_weight, self.calculate_weighting_likelihood(prt, measurement_pose_array, measurement_cov_matrix_inv)) for prt in particles]
00118             except numpy.linalg.LinAlgError:
00119                 rospy.logwarn("[%s] covariance matrix is not singular.", rospy.get_name())
00120                 weights = [min_weight] * len(particles)
00121             # if all([x == min_weight for x in weights]):
00122             #     rospy.logwarn("[%s] likelihood is too small and all weights are limited by min_weight.", rospy.get_name())
00123             normalization_coeffs = sum(weights) # normalization and each weight is assumed to be larger than 0
00124             weights = [w / normalization_coeffs for w in weights]
00125         return weights
00126 
00127     def calculate_weighting_likelihood(self, prt, measurement_pose_array, measurement_cov_matrix_inv):
00128         measurement_likelihood = self.measurement_pdf(prt, measurement_pose_array, measurement_cov_matrix_inv)
00129         z_error_likelihood = self.z_error_pdf(prt.position.z) # consider difference from ideal z height to prevent drift
00130         if self.use_imu:
00131             imu_likelihood = self.imu_error_pdf(prt)
00132             return measurement_likelihood * z_error_likelihood * imu_likelihood
00133         else:
00134             return  measurement_likelihood * z_error_likelihood
00135 
00136     # input: list of particles, list of weights output: list of particles
00137     def resampling(self, particles, weights):
00138         uniform_probability = 1.0 / self.particle_num
00139         ret_particles = []
00140         probability_seed = numpy.random.rand() * uniform_probability
00141         weight_amount = self.weights[0]
00142         index = 0
00143         # for i in range(int(self.particle_num)):
00144         for i in range(int(self.particle_num)):
00145             selector = probability_seed + i * uniform_probability
00146             while selector > weight_amount and index < len(weights):
00147                 index += 1
00148                 weight_amount += weights[index]
00149             ret_particles.append(particles[index])
00150         return ret_particles
00151 
00152     ## probability functions
00153     # input: u(twist), u_cov(twist.covariance)  output: sampled velocity
00154     def state_transition_probability_rvs(self, u, u_cov): # rvs = Random Varieties Sampling
00155         u_mean = [u.linear.x, u.linear.y, u.linear.z,
00156                   u.angular.x, u.angular.y, u.angular.z]
00157         u_cov_matrix = zip(*[iter(u_cov)]*6)
00158         return numpy.random.multivariate_normal(u_mean, u_cov_matrix, int(self.particle_num)).tolist()
00159 
00160     # input: x(pose), mean(array), cov_inv(matrix), output: pdf value for x
00161     def measurement_pdf(self, x, measure_mean_array, measure_cov_matrix_inv): # pdf = Probability Dencity Function
00162         # w ~ p(z(t)|x(t))
00163         x_array = numpy.array(self.convert_pose_to_list(x))
00164         pdf_value = norm_pdf_multivariate(x_array, measure_mean_array, measure_cov_matrix_inv) # ~ p(x(t)|z(t))
00165         return pdf_value
00166 
00167     def z_error_pdf(self, particle_z):
00168         z_error = particle_z - self.source_odom.pose.pose.position.z
00169         # return scipy.stats.norm.pdf(z_error, loc = 0.0, scale = self.z_error_sigma) # scale is standard divasion
00170         return scipy.stats.norm.pdf(z_error / self.z_error_sigma, loc = 0.0, scale = 1.0) # standard pdf
00171 
00172     def imu_error_pdf(self, prt):
00173         if not self.imu:
00174             rospy.logwarn("[%s]: use_imu is True but imu is not subscribed", rospy.get_name())
00175             return 1.0 # multiply 1.0 make no effects to weight
00176         prt_euler = self.convert_pose_to_list(prt)[3:6]
00177         imu_matrix = tf.transformations.quaternion_matrix([self.imu.orientation.x, self.imu.orientation.y, self.imu.orientation.z, self.imu.orientation.w])[:3, :3]
00178         imu_euler = tf.transformations.euler_from_matrix(numpy.dot(self.imu_rotation, imu_matrix)) # imu is assumed to be in base_link relative and imu_rotation is base_link->particle_odom transformation
00179         # roll_pitch_pdf = scipy.stats.norm.pdf(prt_euler[0] - imu_euler[0], loc = 0.0, scale = self.roll_error_sigma) * scipy.stats.norm.pdf(prt_euler[1] - imu_euler[1], loc = 0.0, scale = self.pitch_error_sigma)
00180         roll_std_pdf = scipy.stats.norm.pdf((prt_euler[0] - imu_euler[0]) / self.roll_error_sigma, loc = 0.0, scale = 1.0) # std pdf: Z = (X - mu) / sigma
00181         pitch_std_pdf = scipy.stats.norm.pdf((prt_euler[1] - imu_euler[1]) / self.pitch_error_sigma, loc = 0.0, scale = 1.0)
00182         roll_pitch_pdf = roll_std_pdf * pitch_std_pdf
00183         if self.use_imu_yaw:
00184             return roll_pitch_pdf * scipy.stats.norm.pdf((prt_euler[2] - imu_euler[2]) / self.yaw_error_sigma, loc = 0.0, scale = 1.0)
00185         else:
00186             return roll_pitch_pdf
00187 
00188     # input: init_pose(pose), output: initial distribution of pose(list of pose)
00189     def initial_distribution(self, init_pose):
00190         pose_list = numpy.random.multivariate_normal(numpy.array(self.convert_pose_to_list(init_pose)), numpy.diag([x ** 2 for x in self.init_sigma]), int(self.particle_num))
00191         return [self.convert_list_to_pose(pose) for pose in pose_list]
00192 
00193     ## top odometry calculations 
00194     def calc_odometry(self):
00195         # sampling
00196         self.particles = self.sampling(self.particles, self.source_odom)
00197         if self.measurement_updated:
00198             # weighting
00199             self.weights = self.weighting(self.particles, self.min_weight)
00200             # resampling
00201             self.particles = self.resampling(self.particles, self.weights)
00202             # wait next measurement
00203             self.measurement_updated = False
00204 
00205     def publish_odometry(self):
00206         # relfect source_odom information
00207         self.odom.header.stamp = self.source_odom.header.stamp
00208         self.odom.twist = self.source_odom.twist
00209         # use only important particels
00210         combined_prt_weight = zip(self.particles, self.weights)
00211         selected_prt_weight = zip(*sorted(combined_prt_weight, key = itemgetter(1), reverse = True)[:int(self.valid_particle_num)]) # [(p0, w0), (p1, w1), ..., (pN, wN)] -> [(sorted_p0, sorted_w0), (sorted_p1, sorted_w1), ..., (sorted_pN', sorted_wN')] -> [(sorted_p0, ..., sorted_pN'), (sorted_w0, ..., sorted_wN')]
00212         # estimate gaussian distribution for Odometry msg 
00213         mean, cov = self.guess_normal_distribution(selected_prt_weight[0], selected_prt_weight[1])
00214         self.odom.pose.pose = self.convert_list_to_pose(mean)
00215         self.odom.pose.covariance = list(itertools.chain(*cov))
00216         self.pub.publish(self.odom)
00217         if self.publish_tf:
00218             broadcast_transform(self.broadcast, self.odom, self.invert_tf)
00219         # update prev_rpy to prevent jump of angles
00220         self.prev_rpy = transform_quaternion_to_euler([self.odom.pose.pose.orientation.x, self.odom.pose.pose.orientation.y, self.odom.pose.pose.orientation.z, self.odom.pose.pose.orientation.w], self.prev_rpy)
00221         # diagnostics
00222         self.update_diagnostics(self.particles, self.weights, self.odom.header.stamp)
00223 
00224     ## callback functions
00225     def source_odom_callback(self, msg):        
00226         with self.lock:
00227             self.source_odom = msg
00228 
00229     def measure_odom_callback(self, msg):
00230         with self.lock:
00231             self.measure_odom = msg
00232             self.measurement_updated = True # raise measurement flag
00233 
00234     def init_transform_callback(self, msg):
00235         self.initialize_odometry([getattr(msg.transform.translation, attr) for attr in ["x", "y", "z"]],
00236                                  [getattr(msg.transform.rotation, attr) for attr in ["x", "y", "z", "w"]])
00237 
00238     def imu_callback(self, msg):
00239         with self.lock:
00240             if self.odom == None:
00241                 return # cannot calculate imu_rotation
00242             self.imu_rotation = numpy.linalg.inv(tf.transformations.quaternion_matrix([getattr(self.odom.pose.pose.orientation, attr) for attr in ["x", "y", "z", "w"]])[:3, :3]) # base_link -> particle_odom
00243             self.imu = msg
00244         
00245     # main functions
00246     def update(self):
00247         if not self.odom or not self.particles or not self.source_odom:
00248             rospy.logwarn("[%s]: odometry is not initialized", rospy.get_name())
00249             return
00250         else:
00251             self.calc_odometry()
00252             self.publish_odometry()
00253             if self.publish_histogram:
00254                 histgram_msg = self.make_histogram_array(self.particles, self.source_odom.header.stamp)
00255                 self.pub_hist.publish(histgram_msg)
00256 
00257     def execute(self):
00258         while not rospy.is_shutdown():
00259             with self.lock:
00260                 self.update() # call update() when control input is subscribed
00261             self.r.sleep()
00262         
00263     ## utils
00264     def convert_pose_to_list(self, pose):
00265         euler = transform_quaternion_to_euler((pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w), self.prev_rpy)
00266         return [pose.position.x, pose.position.y, pose.position.z, euler[0], euler[1], euler[2]]
00267 
00268     def convert_list_to_pose(self, lst):
00269         return Pose(Point(*lst[0:3]), Quaternion(*tf.transformations.quaternion_from_euler(*lst[3:6])))
00270 
00271     def guess_normal_distribution(self, particles, weights):
00272         # particles_lst = [self.convert_pose_to_list(prt) for prt in particles]
00273         # mean = numpy.mean(particles_lst, axis = 0)
00274         # cov = numpy.cov(particles_lst, rowvar = 0)
00275         
00276         # particles_list = [numpy.array(self.convert_pose_to_list(prt)) for prt in particles]
00277         # mean = None
00278         # cov = None
00279         # w2_sum = 0.0
00280         # mean = numpy.average(particles_list, axis = 0, weights = weights)
00281         # for prt, w in zip(particles_list, weights):
00282         #     if cov == None:
00283         #         cov = w * numpy.vstack(prt - mean) * (prt - mean)
00284         #     else:
00285         #         cov += w * numpy.vstack(prt - mean) * (prt - mean)
00286         #     w2_sum += w ** 2
00287         # cov = (1.0 / (1.0 - w2_sum)) * cov # unbiased covariance
00288 
00289         # calculate weighted mean and covariance (cf. https://en.wikipedia.org/wiki/Weighted_arithmetic_mean#Weighted_sample_covariance)        
00290         particle_array = numpy.array([self.convert_pose_to_list(prt) for prt in particles])
00291         weights_array = numpy.array(weights)
00292         mean = numpy.average(particle_array, axis = 0, weights = weights_array)
00293         diffs = particle_array - mean # array of x - mean
00294         cov = numpy.dot((numpy.vstack(weights_array) * diffs).T, diffs) # sum(w * (x - mean).T * (x - mean))
00295         cov = (1.0 / (1.0 - sum([w ** 2 for w in weights]))) * cov # unbiased covariance
00296         
00297         return (mean.tolist(), cov.tolist())
00298 
00299     def transform_twist_with_covariance_to_global(self, pose_with_covariance, twist_with_covariance):
00300         global_twist = transform_local_twist_to_global(pose_with_covariance.pose, twist_with_covariance.twist)
00301         global_twist_cov = transform_local_twist_covariance_to_global(pose_with_covariance.pose, twist_with_covariance.covariance)
00302         return TwistWithCovariance(global_twist, global_twist_cov)
00303 
00304     def update_pose_with_covariance(self, pose_with_covariance, twist_with_covariance, dt):
00305         global_twist_with_covariance = self.transform_twist_with_covariance_to_global(pose_with_covariance, twist_with_covariance)
00306         ret_pose = update_pose(pose_with_covariance.pose, global_twist_with_covariance.twist, dt)
00307         ret_pose_cov = update_pose_covariance(pose_with_covariance.covariance, global_twist_with_covariance.covariance, dt)
00308         return PoseWithCovariance(ret_pose, ret_pose_cov)
00309     def make_histogram_array(self, particles, stamp):
00310         # initialize
00311         histogram_array_msg = HistogramWithRangeArray()
00312         histogram_array_msg.header.frame_id = self.odom_frame
00313         histogram_array_msg.header.stamp = stamp
00314         for i in range(6):
00315             range_msg = HistogramWithRange()
00316             range_msg.header.frame_id = self.odom_frame
00317             range_msg.header.stamp = stamp
00318             histogram_array_msg.histograms.append(range_msg)
00319         # count
00320         particle_array = numpy.array([self.convert_pose_to_list(prt) for prt in particles])
00321         data = zip(*particle_array) # [(x_data), (y_data), ..., (yaw_data)]
00322         for i, d in enumerate(data):
00323             hist, bins = numpy.histogram(d, bins=50)
00324             for count, min_value, max_value in zip(hist, bins[:-1], bins[1:]):
00325                 msg_bin = HistogramWithRangeBin()
00326                 msg_bin.max_value = max_value
00327                 msg_bin.min_value = min_value
00328                 msg_bin.count = count
00329                 histogram_array_msg.histograms[i].bins.append(msg_bin)
00330         return histogram_array_msg
00331     def update_diagnostics(self, particles, weights, stamp):
00332         diagnostic = DiagnosticArray()
00333         diagnostic.header.stamp = stamp
00334         # TODO: check particles
00335 
00336         # check weights
00337         status = DiagnosticStatus(name = "{0}: Weights".format(rospy.get_name()), level = DiagnosticStatus.OK, message = "Clear")
00338         if all([x == self.min_weight for x in weights]):
00339             status.level = DiagnosticStatus.WARN
00340             status.message = "likelihood is too small and all weights are limited by min_weight"
00341         diagnostic.status.append(status)
00342 
00343         self.diag_pub.publish(diagnostic)


jsk_robot_startup
Author(s):
autogenerated on Sat Jul 1 2017 02:42:18