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)