magfit_elliptical.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. #!/usr/bin/env python
  2. '''
  3. fit best estimate of magnetometer offsets
  4. '''
  5. from __future__ import print_function
  6. import sys, time, os, math
  7. from argparse import ArgumentParser
  8. parser = ArgumentParser(description=__doc__)
  9. parser.add_argument("--no-timestamps", dest="notimestamps", action='store_true', help="Log doesn't have timestamps")
  10. parser.add_argument("--condition", default=None, help="select packets by condition")
  11. parser.add_argument("--noise", type=float, default=0, help="noise to add")
  12. parser.add_argument("--mag2", action='store_true', help="use 2nd mag from DF log")
  13. parser.add_argument("--radius", default=500.0, type=float, help="target radius")
  14. parser.add_argument("--plot", action='store_true', help="plot points in 3D")
  15. parser.add_argument("logs", metavar="LOG", nargs="+")
  16. args = parser.parse_args()
  17. from pymavlink import mavutil
  18. from pymavlink.rotmat import Vector3
  19. from pymavlink.rotmat import Matrix3
  20. def noise():
  21. '''a noise vector'''
  22. from random import gauss
  23. v = Vector3(gauss(0, 1), gauss(0, 1), gauss(0, 1))
  24. v.normalize()
  25. return v * args.noise
  26. def select_data(data):
  27. ret = []
  28. counts = {}
  29. for d in data:
  30. mag = d
  31. key = "%u:%u:%u" % (mag.x/20,mag.y/20,mag.z/20)
  32. if key in counts:
  33. counts[key] += 1
  34. else:
  35. counts[key] = 1
  36. if counts[key] < 3:
  37. ret.append(d)
  38. print((len(data), len(ret)))
  39. return ret
  40. def constrain(v, min, max):
  41. if v < min:
  42. return min
  43. if v > max:
  44. return max
  45. return v
  46. def correct(mag, offsets, diag, offdiag):
  47. '''correct a mag sample'''
  48. diag.x = 1.0
  49. mat = Matrix3(
  50. Vector3(diag.x, offdiag.x, offdiag.y),
  51. Vector3(offdiag.x, diag.y, offdiag.z),
  52. Vector3(offdiag.y, offdiag.z, diag.z))
  53. mag = mag + offsets
  54. mag = mat * mag
  55. return mag
  56. def radius(mag, offsets, diag, offdiag):
  57. '''return radius give data point and offsets'''
  58. mag = correct(mag, offsets, diag, offdiag)
  59. return mag.length()
  60. def radius_cmp(a, b, offsets, diag, offdiag):
  61. '''return +1 or -1 for for sorting'''
  62. diff = radius(a, offsets, diag, offdiag) - radius(b, offsets, diag, offdiag)
  63. if diff > 0:
  64. return 1
  65. if diff < 0:
  66. return -1
  67. return 0
  68. def sphere_error(p, data):
  69. x,y,z,r,dx,dy,dz,odx,ody,odz = p
  70. if args.radius is not None:
  71. r = args.radius
  72. ofs = Vector3(x,y,z)
  73. diag = Vector3(dx, dy, dz)
  74. offdiag = Vector3(odx, ody, odz)
  75. ret = []
  76. for d in data:
  77. mag = correct(d, ofs, diag, offdiag)
  78. err = r - mag.length()
  79. ret.append(err)
  80. return ret
  81. def fit_data(data):
  82. from scipy import optimize
  83. p0 = [0.0, 0.0, 0.0, # offsets
  84. 500.0, # radius
  85. 1.0, 1.0, 1.0, # diagonals
  86. 0.0, 0.0, 0.0 # offdiagonals
  87. ]
  88. if args.radius is not None:
  89. p0[3] = args.radius
  90. p1, ier = optimize.leastsq(sphere_error, p0[:], args=(data))
  91. if not ier in [1, 2, 3, 4]:
  92. print(p1)
  93. raise RuntimeError("Unable to find solution: %u" % ier)
  94. if args.radius is not None:
  95. r = args.radius
  96. else:
  97. r = p1[3]
  98. return (Vector3(p1[0], p1[1], p1[2]),
  99. r,
  100. Vector3(p1[4], p1[5], p1[6]),
  101. Vector3(p1[7], p1[8], p1[9]))
  102. def magfit(logfile):
  103. '''find best magnetometer offset fit to a log file'''
  104. print("Processing log %s" % filename)
  105. mlog = mavutil.mavlink_connection(filename, notimestamps=args.notimestamps)
  106. data = []
  107. last_t = 0
  108. offsets = None
  109. # now gather all the data
  110. while True:
  111. m = mlog.recv_match(condition=args.condition)
  112. if m is None:
  113. break
  114. if m.get_type() == "SENSOR_OFFSETS":
  115. # update current offsets
  116. offsets = Vector3(m.mag_ofs_x, m.mag_ofs_y, m.mag_ofs_z)
  117. if m.get_type() == "RAW_IMU":
  118. mag = Vector3(m.xmag, m.ymag, m.zmag)
  119. # add data point after subtracting the current offsets
  120. if offsets is not None:
  121. data.append(mag - offsets + noise())
  122. if m.get_type() == "MAG" and not args.mag2:
  123. offsets = Vector3(m.OfsX,m.OfsY,m.OfsZ)
  124. mag = Vector3(m.MagX,m.MagY,m.MagZ)
  125. data.append(mag - offsets + noise())
  126. if m.get_type() == "MAG2" and args.mag2:
  127. offsets = Vector3(m.OfsX,m.OfsY,m.OfsZ)
  128. mag = Vector3(m.MagX,m.MagY,m.MagZ)
  129. data.append(mag - offsets + noise())
  130. print("Extracted %u data points" % len(data))
  131. print("Current offsets: %s" % offsets)
  132. orig_data = data
  133. # find average values
  134. avg = Vector3()
  135. count = 0
  136. for d in data:
  137. avg += d
  138. count += 1
  139. avg /= count
  140. # subtract average
  141. data = []
  142. for d in orig_data:
  143. data.append(d - avg)
  144. print("Average %s" % avg)
  145. #data = select_data(data)
  146. diag = Vector3(1,1,1)
  147. offdiag = Vector3(0,0,0)
  148. # remove initial outliers
  149. if False:
  150. data.sort(lambda a,b : radius_cmp(a,b,offsets,diag,offdiag))
  151. data = data[len(data)/16:-len(data)/16]
  152. # do an initial fit
  153. (offsets, field_strength, diag, offdiag) = fit_data(data)
  154. for count in range(3):
  155. # sort the data by the radius
  156. data.sort(lambda a,b : radius_cmp(a,b,offsets,diag,offdiag))
  157. print("Fit %u : %s %s %s field_strength=%6.1f to %6.1f" % (
  158. count, offsets, diag, offdiag,
  159. radius(data[0], offsets,diag,offdiag), radius(data[-1], offsets,diag,offdiag)))
  160. # discard outliers, keep the middle
  161. data = data[len(data)/32:-len(data)/32]
  162. # fit again
  163. (offsets, field_strength, diag, offdiag) = fit_data(data)
  164. print("Final : %s %s %s field_strength=%6.1f to %6.1f" % (
  165. offsets, diag, offdiag,
  166. radius(data[0], offsets, diag, offdiag), radius(data[-1], offsets, diag, offdiag)))
  167. offsets -= avg
  168. print("With average : %s" % offsets)
  169. if args.plot:
  170. data2 = [correct(d,offsets,diag,offdiag) for d in orig_data]
  171. plot_data(orig_data, data2)
  172. def plot_data(orig_data, data):
  173. '''plot data in 3D'''
  174. from mpl_toolkits.mplot3d import Axes3D
  175. import matplotlib.pyplot as plt
  176. fig = plt.figure()
  177. for dd, c, p in [(orig_data, 'r', 1), (data, 'b', 2)]:
  178. ax = fig.add_subplot(1, 2, p, projection='3d')
  179. xs = [ d.x for d in dd ]
  180. ys = [ d.y for d in dd ]
  181. zs = [ d.z for d in dd ]
  182. ax.scatter(xs, ys, zs, c=c, marker='o')
  183. ax.set_xlabel('X')
  184. ax.set_ylabel('Y')
  185. ax.set_zlabel('Z')
  186. plt.show()
  187. total = 0.0
  188. for filename in args.logs:
  189. magfit(filename)