00001 
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     
00024     def __init__(self):
00025         
00026         rospy.init_node("ParticleOdometry", anonymous=True)
00027         
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) 
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) 
00035         self.use_imu = rospy.get_param("~use_imu", False)
00036         self.use_imu_yaw = rospy.get_param("~use_imu_yaw", False) 
00037         self.roll_error_sigma = rospy.get_param("~roll_error_sigma", 0.05) 
00038         self.pitch_error_sigma = rospy.get_param("~pitch_error_sigma", 0.05) 
00039         self.yaw_error_sigma = rospy.get_param("~yaw_error_sigma", 0.1) 
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         
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         
00064         self.pub = rospy.Publisher("~output", Odometry, queue_size = 1)
00065         self.diag_pub = rospy.Publisher('~diagnostics', DiagnosticArray)
00066         
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         
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) 
00074         self.init_transform_sub = rospy.Subscriber("~initial_base_link_transform", TransformStamped, self.init_transform_callback) 
00075         
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     
00099     
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) 
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     
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) 
00111         else:
00112             measure_to_source_dt = (self.source_odom.header.stamp - self.measure_odom.header.stamp).to_sec() 
00113             current_measure_pose_with_covariance = self.update_pose_with_covariance(self.measure_odom.pose, self.measure_odom.twist, measure_to_source_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)) 
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             
00122             
00123             normalization_coeffs = sum(weights) 
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) 
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     
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         
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     
00153     
00154     def state_transition_probability_rvs(self, u, u_cov): 
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     
00161     def measurement_pdf(self, x, measure_mean_array, measure_cov_matrix_inv): 
00162         
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) 
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         
00170         return scipy.stats.norm.pdf(z_error / self.z_error_sigma, loc = 0.0, scale = 1.0) 
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 
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)) 
00179         
00180         roll_std_pdf = scipy.stats.norm.pdf((prt_euler[0] - imu_euler[0]) / self.roll_error_sigma, loc = 0.0, scale = 1.0) 
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     
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     
00194     def calc_odometry(self):
00195         
00196         self.particles = self.sampling(self.particles, self.source_odom)
00197         if self.measurement_updated:
00198             
00199             self.weights = self.weighting(self.particles, self.min_weight)
00200             
00201             self.particles = self.resampling(self.particles, self.weights)
00202             
00203             self.measurement_updated = False
00204 
00205     def publish_odometry(self):
00206         
00207         self.odom.header.stamp = self.source_odom.header.stamp
00208         self.odom.twist = self.source_odom.twist
00209         
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)]) 
00212         
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         
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         
00222         self.update_diagnostics(self.particles, self.weights, self.odom.header.stamp)
00223 
00224     
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 
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 
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]) 
00243             self.imu = msg
00244         
00245     
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() 
00261             self.r.sleep()
00262         
00263     
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         
00273         
00274         
00275         
00276         
00277         
00278         
00279         
00280         
00281         
00282         
00283         
00284         
00285         
00286         
00287         
00288 
00289         
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 
00294         cov = numpy.dot((numpy.vstack(weights_array) * diffs).T, diffs) 
00295         cov = (1.0 / (1.0 - sum([w ** 2 for w in weights]))) * cov 
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         
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         
00320         particle_array = numpy.array([self.convert_pose_to_list(prt) for prt in particles])
00321         data = zip(*particle_array) 
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         
00335 
00336         
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)