evaluate_ate_scale.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # Modified by Raul Mur-Artal
  2. # Automatically compute the optimal scale factor for monocular VO/SLAM.
  3. # Software License Agreement (BSD License)
  4. #
  5. # Copyright (c) 2013, Juergen Sturm, TUM
  6. # All rights reserved.
  7. #
  8. # Redistribution and use in source and binary forms, with or without
  9. # modification, are permitted provided that the following conditions
  10. # are met:
  11. #
  12. # * Redistributions of source code must retain the above copyright
  13. # notice, this list of conditions and the following disclaimer.
  14. # * Redistributions in binary form must reproduce the above
  15. # copyright notice, this list of conditions and the following
  16. # disclaimer in the documentation and/or other materials provided
  17. # with the distribution.
  18. # * Neither the name of TUM nor the names of its
  19. # contributors may be used to endorse or promote products derived
  20. # from this software without specific prior written permission.
  21. #
  22. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  23. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  24. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  25. # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  26. # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  27. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  28. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  29. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  30. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  31. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  32. # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. # POSSIBILITY OF SUCH DAMAGE.
  34. #
  35. # Requirements:
  36. # sudo apt-get install python-argparse
  37. """
  38. This script computes the absolute trajectory error from the ground truth
  39. trajectory and the estimated trajectory.
  40. """
  41. import sys
  42. import numpy
  43. import argparse
  44. import associate
  45. def align(model,data):
  46. """Align two trajectories using the method of Horn (closed-form).
  47. Input:
  48. model -- first trajectory (3xn)
  49. data -- second trajectory (3xn)
  50. Output:
  51. rot -- rotation matrix (3x3)
  52. trans -- translation vector (3x1)
  53. trans_error -- translational error per point (1xn)
  54. """
  55. numpy.set_printoptions(precision=3,suppress=True)
  56. model_zerocentered = model - model.mean(1)
  57. data_zerocentered = data - data.mean(1)
  58. W = numpy.zeros( (3,3) )
  59. for column in range(model.shape[1]):
  60. W += numpy.outer(model_zerocentered[:,column],data_zerocentered[:,column])
  61. U,d,Vh = numpy.linalg.linalg.svd(W.transpose())
  62. S = numpy.matrix(numpy.identity( 3 ))
  63. if(numpy.linalg.det(U) * numpy.linalg.det(Vh)<0):
  64. S[2,2] = -1
  65. rot = U*S*Vh
  66. rotmodel = rot*model_zerocentered
  67. dots = 0.0
  68. norms = 0.0
  69. for column in range(data_zerocentered.shape[1]):
  70. dots += numpy.dot(data_zerocentered[:,column].transpose(),rotmodel[:,column])
  71. normi = numpy.linalg.norm(model_zerocentered[:,column])
  72. norms += normi*normi
  73. s = float(dots/norms)
  74. transGT = data.mean(1) - s*rot * model.mean(1)
  75. trans = data.mean(1) - rot * model.mean(1)
  76. model_alignedGT = s*rot * model + transGT
  77. model_aligned = rot * model + trans
  78. alignment_errorGT = model_alignedGT - data
  79. alignment_error = model_aligned - data
  80. trans_errorGT = numpy.sqrt(numpy.sum(numpy.multiply(alignment_errorGT,alignment_errorGT),0)).A[0]
  81. trans_error = numpy.sqrt(numpy.sum(numpy.multiply(alignment_error,alignment_error),0)).A[0]
  82. return rot,transGT,trans_errorGT,trans,trans_error, s
  83. def plot_traj(ax,stamps,traj,style,color,label):
  84. """
  85. Plot a trajectory using matplotlib.
  86. Input:
  87. ax -- the plot
  88. stamps -- time stamps (1xn)
  89. traj -- trajectory (3xn)
  90. style -- line style
  91. color -- line color
  92. label -- plot legend
  93. """
  94. stamps.sort()
  95. interval = numpy.median([s-t for s,t in zip(stamps[1:],stamps[:-1])])
  96. x = []
  97. y = []
  98. last = stamps[0]
  99. for i in range(len(stamps)):
  100. if stamps[i]-last < 2*interval:
  101. x.append(traj[i][0])
  102. y.append(traj[i][1])
  103. elif len(x)>0:
  104. ax.plot(x,y,style,color=color,label=label)
  105. label=""
  106. x=[]
  107. y=[]
  108. last= stamps[i]
  109. if len(x)>0:
  110. ax.plot(x,y,style,color=color,label=label)
  111. if __name__=="__main__":
  112. # parse command line
  113. parser = argparse.ArgumentParser(description='''
  114. This script computes the absolute trajectory error from the ground truth trajectory and the estimated trajectory.
  115. ''')
  116. parser.add_argument('first_file', help='ground truth trajectory (format: timestamp tx ty tz qx qy qz qw)')
  117. parser.add_argument('second_file', help='estimated trajectory (format: timestamp tx ty tz qx qy qz qw)')
  118. parser.add_argument('--offset', help='time offset added to the timestamps of the second file (default: 0.0)',default=0.0)
  119. parser.add_argument('--scale', help='scaling factor for the second trajectory (default: 1.0)',default=1.0)
  120. parser.add_argument('--max_difference', help='maximally allowed time difference for matching entries (default: 10000000 ns)',default=20000000)
  121. parser.add_argument('--save', help='save aligned second trajectory to disk (format: stamp2 x2 y2 z2)')
  122. parser.add_argument('--save_associations', help='save associated first and aligned second trajectory to disk (format: stamp1 x1 y1 z1 stamp2 x2 y2 z2)')
  123. parser.add_argument('--plot', help='plot the first and the aligned second trajectory to an image (format: png)')
  124. parser.add_argument('--verbose', help='print all evaluation data (otherwise, only the RMSE absolute translational error in meters after alignment will be printed)', action='store_true')
  125. parser.add_argument('--verbose2', help='print scale eror and RMSE absolute translational error in meters after alignment with and without scale correction', action='store_true')
  126. args = parser.parse_args()
  127. first_list = associate.read_file_list(args.first_file, False)
  128. second_list = associate.read_file_list(args.second_file, False)
  129. matches = associate.associate(first_list, second_list,float(args.offset),float(args.max_difference))
  130. if len(matches)<2:
  131. sys.exit("Couldn't find matching timestamp pairs between groundtruth and estimated trajectory! Did you choose the correct sequence?")
  132. first_xyz = numpy.matrix([[float(value) for value in first_list[a][0:3]] for a,b in matches]).transpose()
  133. second_xyz = numpy.matrix([[float(value)*float(args.scale) for value in second_list[b][0:3]] for a,b in matches]).transpose()
  134. dictionary_items = second_list.items()
  135. sorted_second_list = sorted(dictionary_items)
  136. second_xyz_full = numpy.matrix([[float(value)*float(args.scale) for value in sorted_second_list[i][1][0:3]] for i in range(len(sorted_second_list))]).transpose() # sorted_second_list.keys()]).transpose()
  137. rot,transGT,trans_errorGT,trans,trans_error, scale = align(second_xyz,first_xyz)
  138. second_xyz_aligned = scale * rot * second_xyz + trans
  139. second_xyz_notscaled = rot * second_xyz + trans
  140. second_xyz_notscaled_full = rot * second_xyz_full + trans
  141. first_stamps = first_list.keys()
  142. first_stamps.sort()
  143. first_xyz_full = numpy.matrix([[float(value) for value in first_list[b][0:3]] for b in first_stamps]).transpose()
  144. second_stamps = second_list.keys()
  145. second_stamps.sort()
  146. second_xyz_full = numpy.matrix([[float(value)*float(args.scale) for value in second_list[b][0:3]] for b in second_stamps]).transpose()
  147. second_xyz_full_aligned = scale * rot * second_xyz_full + trans
  148. if args.verbose:
  149. print "compared_pose_pairs %d pairs"%(len(trans_error))
  150. print "absolute_translational_error.rmse %f m"%numpy.sqrt(numpy.dot(trans_error,trans_error) / len(trans_error))
  151. print "absolute_translational_error.mean %f m"%numpy.mean(trans_error)
  152. print "absolute_translational_error.median %f m"%numpy.median(trans_error)
  153. print "absolute_translational_error.std %f m"%numpy.std(trans_error)
  154. print "absolute_translational_error.min %f m"%numpy.min(trans_error)
  155. print "absolute_translational_error.max %f m"%numpy.max(trans_error)
  156. print "max idx: %i" %numpy.argmax(trans_error)
  157. else:
  158. # print "%f, %f " % (numpy.sqrt(numpy.dot(trans_error,trans_error) / len(trans_error)), scale)
  159. # print "%f,%f" % (numpy.sqrt(numpy.dot(trans_error,trans_error) / len(trans_error)), scale)
  160. print "%f,%f,%f" % (numpy.sqrt(numpy.dot(trans_error,trans_error) / len(trans_error)), scale, numpy.sqrt(numpy.dot(trans_errorGT,trans_errorGT) / len(trans_errorGT)))
  161. # print "%f" % len(trans_error)
  162. if args.verbose2:
  163. print "compared_pose_pairs %d pairs"%(len(trans_error))
  164. print "absolute_translational_error.rmse %f m"%numpy.sqrt(numpy.dot(trans_error,trans_error) / len(trans_error))
  165. print "absolute_translational_errorGT.rmse %f m"%numpy.sqrt(numpy.dot(trans_errorGT,trans_errorGT) / len(trans_errorGT))
  166. if args.save_associations:
  167. file = open(args.save_associations,"w")
  168. file.write("\n".join(["%f %f %f %f %f %f %f %f"%(a,x1,y1,z1,b,x2,y2,z2) for (a,b),(x1,y1,z1),(x2,y2,z2) in zip(matches,first_xyz.transpose().A,second_xyz_aligned.transpose().A)]))
  169. file.close()
  170. if args.save:
  171. file = open(args.save,"w")
  172. file.write("\n".join(["%f "%stamp+" ".join(["%f"%d for d in line]) for stamp,line in zip(second_stamps,second_xyz_notscaled_full.transpose().A)]))
  173. file.close()
  174. if args.plot:
  175. import matplotlib
  176. matplotlib.use('Agg')
  177. import matplotlib.pyplot as plt
  178. import matplotlib.pylab as pylab
  179. from matplotlib.patches import Ellipse
  180. fig = plt.figure()
  181. ax = fig.add_subplot(111)
  182. plot_traj(ax,first_stamps,first_xyz_full.transpose().A,'-',"black","ground truth")
  183. plot_traj(ax,second_stamps,second_xyz_full_aligned.transpose().A,'-',"blue","estimated")
  184. label="difference"
  185. for (a,b),(x1,y1,z1),(x2,y2,z2) in zip(matches,first_xyz.transpose().A,second_xyz_aligned.transpose().A):
  186. ax.plot([x1,x2],[y1,y2],'-',color="red",label=label)
  187. label=""
  188. ax.legend()
  189. ax.set_xlabel('x [m]')
  190. ax.set_ylabel('y [m]')
  191. plt.axis('equal')
  192. plt.savefig(args.plot,format="pdf")