00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035 import roslib; roslib.load_manifest('calibration_estimation')
00036
00037 from calibration_estimation.urdf_params import UrdfParams
00038 from calibration_estimation.single_transform import SingleTransform
00039 import numpy
00040 from numpy import array, matrix, zeros, cumsum, concatenate, reshape
00041 import scipy.optimize
00042 import sys
00043 import rospy
00044 from visualization_msgs.msg import Marker, MarkerArray
00045 import geometry_msgs.msg
00046
00047 class ErrorCalc:
00048 """
00049 Helpers for computing errors and jacobians
00050 """
00051 def __init__(self, robot_params, free_dict, multisensors, use_cov):
00052 self._robot_params = robot_params
00053 self._expanded_params = robot_params.deflate()
00054 self._free_list = robot_params.calc_free(free_dict)
00055 self._multisensors = multisensors
00056 self._use_cov = use_cov
00057 self._j_count = 0
00058 self.marker_pub = rospy.Publisher("pose_guesses", Marker)
00059
00060
00061 def calculate_full_param_vec(self, opt_param_vec):
00062 '''
00063 Take the set of optimization params, and expand it into the bigger param vec
00064 '''
00065 full_param_vec = self._expanded_params.copy()
00066 full_param_vec[numpy.where(self._free_list), 0] = opt_param_vec
00067
00068 return full_param_vec
00069
00070 def calculate_error(self, opt_all_vec):
00071 opt_param_vec, full_pose_arr = self.split_all(opt_all_vec)
00072
00073 full_param_vec = self.calculate_full_param_vec(opt_param_vec)
00074
00075
00076 self._robot_params.inflate(full_param_vec)
00077
00078
00079 for multisensor in self._multisensors:
00080 multisensor.update_config(self._robot_params)
00081
00082 r_list = []
00083 id = 0
00084 for multisensor, cb_pose_vec in zip(self._multisensors, list(full_pose_arr)):
00085
00086 cb_points = SingleTransform(cb_pose_vec).transform * self._robot_params.checkerboards[multisensor.checkerboard].generate_points()
00087 if (self._use_cov):
00088 r_list.append(multisensor.compute_residual_scaled(cb_points))
00089 else:
00090 r_list.append(multisensor.compute_residual(cb_points))
00091
00092 cb_points_msgs = [ geometry_msgs.msg.Point(cur_pt[0,0], cur_pt[0,1], cur_pt[0,2]) for cur_pt in cb_points.T]
00093
00094 m = Marker()
00095 m.header.frame_id = self._robot_params.base_link
00096 m.ns = "points_3d"
00097 m.id = id
00098 m.type = Marker.SPHERE_LIST
00099 m.action = Marker.MODIFY
00100 m.points = cb_points_msgs
00101 m.color.r = 1.0
00102 m.color.g = 0.0
00103 m.color.b = 1.0
00104 m.color.a = 1.0
00105 m.scale.x = 0.01
00106 m.scale.y = 0.01
00107 m.scale.z = 0.01
00108 self.marker_pub.publish(m)
00109
00110 cur_pt = cb_points.T[0]
00111
00112 m_id = Marker()
00113 m_id.header.frame_id = self._robot_params.base_link
00114 m_id.ns = "points_ids"
00115 m_id.id = id + 10000
00116 m_id.type = Marker.TEXT_VIEW_FACING
00117 m_id.action = Marker.MODIFY
00118 m_id.text = str(id)
00119 m_id.color.r = 1.0
00120 m_id.color.g = 0.0
00121 m_id.color.b = 0.0
00122 m_id.color.a = 1.0
00123 m_id.scale.x = 0.06
00124 m_id.scale.y = 0.06
00125 m_id.scale.z = 0.06
00126 m_id.pose.position.x = cur_pt[0,0];
00127 m_id.pose.position.y = cur_pt[0,1];
00128 m_id.pose.position.z = cur_pt[0,2] + 0.05;
00129 m_id.pose.orientation.x = 0.0;
00130 m_id.pose.orientation.y = 0.0;
00131 m_id.pose.orientation.z = 0.0;
00132 m_id.pose.orientation.w = 1.0;
00133 self.marker_pub.publish(m_id)
00134
00135 id += 1
00136
00137 r_vec = concatenate(r_list)
00138
00139 rms_error = numpy.sqrt( numpy.mean(r_vec**2) )
00140
00141 print "\t\t\t\t\tRMS error: %.3f \r" % rms_error,
00142 sys.stdout.flush()
00143
00144 return array(r_vec)
00145
00146 def calculate_jacobian(self, opt_all_vec):
00147 """
00148 Full Jacobian:
00149 The resulting returned jacobian can be thought of as a set of several concatenated jacobians as follows:
00150
00151 [ J_multisensor_0 ]
00152 J = [ J_multisensor_1 ]
00153 [ : ]
00154 [ J_multisensor_M ]
00155
00156 where M is the number of multisensors, which is generally the number of calibration samples collected.
00157
00158 Multisensor Jacobian:
00159 The Jacobian for a given multisensor is created by concatenating jacobians for each sensor
00160
00161 [ J_sensor_m_0 ]
00162 J_multisensor_m = [ J_sensor_m_1 ]
00163 [ : ]
00164 [ J_sensor_m_S ]
00165
00166 where S is the number of sensors in this multisensor.
00167
00168 Sensor Jacobian:
00169 A single sensor jacobian is created by concatenating jacobians for the system parameters and checkerboards
00170
00171 J_sensor_m_s = [ J_params_m_s | J_m_s_pose0 | J_m_s_pose1 | --- | J_m_s_CB_poseC ]
00172
00173 The number of rows in J_sensor_m_s is equal to the number of rows in this sensor's residual.
00174 J_params_m_s shows how modifying the free system parameters affects the residual.
00175 Each J_m_s_pose_c is 6 columns wide and shows how moving a certain target affects the residual. All
00176 of the J_m_s_pose blocks are zero, except J_sensor_pose_m, since target m is the only target that
00177 was viewed by the sensors in this multisensor.
00178 """
00179 self._j_count += 1;
00180 sys.stdout.write(" \r")
00181 sys.stdout.write("Computing Jacobian.. (cycle #%d)\r" % self._j_count)
00182 sys.stdout.flush()
00183
00184
00185
00186 opt_param_vec, full_pose_arr = self.split_all(opt_all_vec)
00187
00188
00189 ms_r_len = [ms.get_residual_length() for ms in self._multisensors]
00190 J = zeros([sum(ms_r_len), len(opt_all_vec)])
00191
00192
00193 ms_end_row = list(cumsum(ms_r_len))
00194 ms_start_row = [0] + ms_end_row[:-1]
00195
00196
00197 ms_end_col = list(cumsum([6]*len(self._multisensors)) + len(opt_param_vec))
00198 ms_start_col = [x-6 for x in ms_end_col]
00199
00200
00201 for i,ms in enumerate(self._multisensors):
00202
00203 J_ms_params = J[ ms_start_row[i]:ms_end_row[i],
00204 0:len(opt_param_vec) ]
00205 s_r_len = [s.get_residual_length() for s in ms.sensors]
00206 s_end_row = list(cumsum(s_r_len))
00207 s_start_row = [0] + s_end_row[:-1]
00208 target_pose_T = SingleTransform(full_pose_arr[i,:]).transform
00209
00210 for k,s in enumerate(ms.sensors):
00211 J_s_params = J_ms_params[ s_start_row[k]:s_end_row[k], :]
00212 J_s_params[:,:] = self.single_sensor_params_jacobian(opt_param_vec, target_pose_T, ms.checkerboard, s)
00213
00214
00215 J_ms_pose = J[ ms_start_row[i]:ms_end_row[i],
00216 ms_start_col[i]:ms_end_col[i]]
00217 assert(J_ms_pose.shape[1] == 6)
00218 J_ms_pose[:,:] = self.multisensor_pose_jacobian(opt_param_vec, full_pose_arr[i,:], ms)
00219
00220 sys.stdout.flush()
00221
00222 return J
00223
00224 def split_all(self, opt_all_vec):
00225 """
00226 Splits the input vector into two parts:
00227 1) opt_param_vec: The system parameters that we're optimizing,
00228 2) full_pose_arr: A Nx6 array where each row is a checkerboard pose
00229 """
00230 opt_param_len = sum(self._free_list)
00231 opt_param_vec = opt_all_vec[0:opt_param_len]
00232
00233 full_pose_vec = opt_all_vec[opt_param_len:]
00234 full_pose_arr = numpy.reshape(full_pose_vec, [-1,6])
00235 return opt_param_vec, full_pose_arr
00236
00237 def single_sensor_params_jacobian(self, opt_param_vec, target_pose_T, target_id, sensor):
00238 """
00239 Computes the jacobian from the free system parameters to a single sensor
00240 Inputs:
00241 - opt_param_vec: Vector of the free system parameters (the parameters we're actuall optimizing)
00242 - target_pose_T: The pose of the target that this sensor is viewing
00243 - target_id: String name of the target that this sensor is viewing. This is necessary to generate the
00244 set of feature points on the target
00245 - sensor: The actual sensor definition
00246 Output:
00247 - J_scaled: An mxn jacobian, where m is the length of sensor's residual and n is the length of opt_param_vec.
00248 If covariance calculations are enabled, then the Jacobian is scaled by sqrt(Gamma), where Gamma
00249 is the information matrix for this measurement.
00250 """
00251 sparsity_dict = sensor.build_sparsity_dict()
00252 required_keys = ['chains', 'tilting_lasers', 'transforms', 'rectified_cams', 'checkerboards']
00253 for cur_key in required_keys:
00254 if cur_key not in sparsity_dict.keys():
00255 sparsity_dict[cur_key] = {}
00256
00257 full_sparsity_list = self._robot_params.calc_free(sparsity_dict)
00258 full_sparsity_vec = numpy.array(full_sparsity_list)
00259
00260
00261
00262 opt_sparsity_vec = full_sparsity_vec[numpy.where(self._free_list)].copy()
00263
00264
00265 full_param_vec = self.calculate_full_param_vec(opt_param_vec)
00266 self._robot_params.inflate(full_param_vec)
00267 sensor.update_config(self._robot_params)
00268
00269
00270 x0 = opt_param_vec
00271 epsilon = 1e-6
00272 target_points = target_pose_T * self._robot_params.checkerboards[target_id].generate_points()
00273 f0 = sensor.compute_residual(target_points)
00274 if (self._use_cov):
00275 gamma_sqrt = sensor.compute_marginal_gamma_sqrt(target_points)
00276 Jt = numpy.zeros([len(x0),len(f0)])
00277 dx = numpy.zeros(len(x0))
00278 for i in numpy.where(opt_sparsity_vec)[0]:
00279 dx[i] = epsilon
00280 opt_test_param_vec = x0 + dx
00281 full_test_param_vec = self.calculate_full_param_vec(opt_test_param_vec)
00282 self._robot_params.inflate(full_test_param_vec)
00283 sensor.update_config(self._robot_params)
00284 target_points = target_pose_T * self._robot_params.checkerboards[target_id].generate_points()
00285 Jt[i] = (sensor.compute_residual(target_points) - f0)/epsilon
00286 dx[i] = 0.0
00287 J = Jt.transpose()
00288 if (self._use_cov):
00289 J_scaled = gamma_sqrt * J
00290 else:
00291 J_scaled = J
00292 return J_scaled
00293
00294 def multisensor_pose_jacobian(self, opt_param_vec, pose_param_vec, multisensor):
00295 """
00296 Generates the jacobian from a target pose to the multisensor's residual.
00297
00298 Input:
00299 - opt_param_vec: Vector of the free system parameters (the parameters we're actuall optimizing)
00300 - pose_param_vec: Vector of length 6 encoding the target's pose 0:3=translation 3:6=rotation_axis
00301 - multisensor: The actual multisensor definition.
00302 Output:
00303 - J_scaled: An mx6 jacobian, where m is the length of multisensor's residual. There are 6 columns since the
00304 target's pose is encoded using 6 parameters.
00305 If covariance calculations are enabled, then the Jacobian is scaled by sqrt(Gamma), where Gamma
00306 is the information matrix for this measurement.
00307 """
00308
00309 full_param_vec = self.calculate_full_param_vec(opt_param_vec)
00310 self._robot_params.inflate(full_param_vec)
00311 multisensor.update_config(self._robot_params)
00312 cb_model = self._robot_params.checkerboards[multisensor.checkerboard]
00313 local_cb_points = cb_model.generate_points()
00314
00315
00316 x0 = pose_param_vec
00317 epsilon = 1e-6
00318 world_pts = SingleTransform(x0).transform * local_cb_points
00319 f0 = multisensor.compute_residual(world_pts)
00320 if (self._use_cov):
00321 gamma_sqrt = multisensor.compute_marginal_gamma_sqrt(world_pts)
00322
00323 Jt = numpy.zeros([len(x0),len(f0)])
00324 dx = numpy.zeros(len(x0))
00325 for i in range(len(x0)):
00326 dx[i] = epsilon
00327 test_vec = x0 + dx
00328 fTest = multisensor.compute_residual(SingleTransform(test_vec).transform * local_cb_points)
00329 Jt[i] = (fTest - f0)/epsilon
00330
00331 dx[i] = 0.0
00332 J = Jt.transpose()
00333 if (self._use_cov):
00334 J_scaled = gamma_sqrt * J
00335 else:
00336 J_scaled = J
00337 return J_scaled
00338
00339 def build_opt_vector(robot_params, free_dict, pose_guess_arr):
00340 """
00341 Construct vector of all the parameters that we're optimizing over. This includes
00342 both the free system parameters and the checkerboard poses
00343 Inputs:
00344 - robot_params: Dictionary with all of the system parameters
00345 - free_dict: Dictionary that specifies which system parameters are free
00346 - pose_guess_arr: Mx6 array storing the current checkerboard poses, where M is
00347 the number of calibration samples
00348 Returns: Vector of length (F + Mx6), where F is the number of 1's in the free_dict
00349 """
00350
00351
00352 expanded_param_vec = robot_params.deflate()
00353 free_list = robot_params.calc_free(free_dict)
00354
00355
00356 opt_param_vec = array(expanded_param_vec[numpy.where(free_list)].T)[0]
00357
00358 assert(pose_guess_arr.shape[1] == 6)
00359 opt_pose_vec = reshape(pose_guess_arr, [-1])
00360
00361 return numpy.concatenate([opt_param_vec, opt_pose_vec])
00362
00363 def compute_errors_breakdown(error_calc, multisensors, opt_pose_arr):
00364 errors_dict = {}
00365
00366 for ms, k in zip(multisensors, range(len(multisensors))):
00367 cb = error_calc._robot_params.checkerboards[ms.checkerboard]
00368 cb_pose_vec = opt_pose_arr[k]
00369 target_pts = SingleTransform(cb_pose_vec).transform * cb.generate_points()
00370 for sensor in ms.sensors:
00371 r_sensor = sensor.compute_residual(target_pts) * numpy.sqrt(sensor.terms_per_sample)
00372 if sensor.sensor_id not in errors_dict.keys():
00373 errors_dict[sensor.sensor_id] = []
00374 errors_dict[sensor.sensor_id].append(r_sensor)
00375 return errors_dict
00376
00377 def opt_runner(robot_params, pose_guess_arr, free_dict, multisensors, use_cov):
00378 """
00379 Runs a single optimization step for the calibration optimization.
00380 robot_params - Instance of UrdfParams
00381 free_dict - Dictionary storing which parameters are free
00382 multisensor - list of list of measurements. Each multisensor corresponds to a single checkerboard pose
00383 pose_guesses - List of guesses as to where all the checkerboard are. This is used to initialze the optimization
00384 """
00385 error_calc = ErrorCalc(robot_params, free_dict, multisensors, use_cov)
00386
00387
00388 opt_all = build_opt_vector(robot_params, free_dict, pose_guess_arr)
00389
00390 x, cov_x, infodict, mesg, iter = scipy.optimize.leastsq(error_calc.calculate_error, opt_all, Dfun=error_calc.calculate_jacobian, full_output=1)
00391
00392 J = error_calc.calculate_jacobian(x)
00393
00394
00395 opt_param_vec, pose_vec = error_calc.split_all(x)
00396 expanded_param_vec = error_calc.calculate_full_param_vec(opt_param_vec)
00397 opt_pose_arr = reshape(pose_vec, [-1, 6])
00398
00399 output_dict = error_calc._robot_params.params_to_config(expanded_param_vec)
00400
00401 errors_dict = compute_errors_breakdown(error_calc, multisensors, opt_pose_arr)
00402
00403 print ""
00404 print "Errors Breakdown:"
00405 for sensor_id, error_list in errors_dict.items():
00406 error_cat = numpy.concatenate(error_list)
00407 rms_error = numpy.sqrt( numpy.mean(error_cat**2) )
00408 print " %s: %.6f" % (sensor_id, rms_error)
00409
00410 return output_dict, opt_pose_arr, J
00411