#!/usr/bin/env python ''' fit best estimate of magnetometer offsets ''' from __future__ import print_function import sys, time, os, math from argparse import ArgumentParser parser = ArgumentParser(description=__doc__) parser.add_argument("--no-timestamps", dest="notimestamps", action='store_true', help="Log doesn't have timestamps") parser.add_argument("--condition", default=None, help="select packets by condition") parser.add_argument("--noise", type=float, default=0, help="noise to add") parser.add_argument("--mag2", action='store_true', help="use 2nd mag from DF log") parser.add_argument("--radius", default=500.0, type=float, help="target radius") parser.add_argument("--plot", action='store_true', help="plot points in 3D") parser.add_argument("logs", metavar="LOG", nargs="+") args = parser.parse_args() from pymavlink import mavutil from pymavlink.rotmat import Vector3 from pymavlink.rotmat import Matrix3 def noise(): '''a noise vector''' from random import gauss v = Vector3(gauss(0, 1), gauss(0, 1), gauss(0, 1)) v.normalize() return v * args.noise def select_data(data): ret = [] counts = {} for d in data: mag = d key = "%u:%u:%u" % (mag.x/20,mag.y/20,mag.z/20) if key in counts: counts[key] += 1 else: counts[key] = 1 if counts[key] < 3: ret.append(d) print((len(data), len(ret))) return ret def constrain(v, min, max): if v < min: return min if v > max: return max return v def correct(mag, offsets, diag, offdiag): '''correct a mag sample''' diag.x = 1.0 mat = Matrix3( Vector3(diag.x, offdiag.x, offdiag.y), Vector3(offdiag.x, diag.y, offdiag.z), Vector3(offdiag.y, offdiag.z, diag.z)) mag = mag + offsets mag = mat * mag return mag def radius(mag, offsets, diag, offdiag): '''return radius give data point and offsets''' mag = correct(mag, offsets, diag, offdiag) return mag.length() def radius_cmp(a, b, offsets, diag, offdiag): '''return +1 or -1 for for sorting''' diff = radius(a, offsets, diag, offdiag) - radius(b, offsets, diag, offdiag) if diff > 0: return 1 if diff < 0: return -1 return 0 def sphere_error(p, data): x,y,z,r,dx,dy,dz,odx,ody,odz = p if args.radius is not None: r = args.radius ofs = Vector3(x,y,z) diag = Vector3(dx, dy, dz) offdiag = Vector3(odx, ody, odz) ret = [] for d in data: mag = correct(d, ofs, diag, offdiag) err = r - mag.length() ret.append(err) return ret def fit_data(data): from scipy import optimize p0 = [0.0, 0.0, 0.0, # offsets 500.0, # radius 1.0, 1.0, 1.0, # diagonals 0.0, 0.0, 0.0 # offdiagonals ] if args.radius is not None: p0[3] = args.radius p1, ier = optimize.leastsq(sphere_error, p0[:], args=(data)) if not ier in [1, 2, 3, 4]: print(p1) raise RuntimeError("Unable to find solution: %u" % ier) if args.radius is not None: r = args.radius else: r = p1[3] return (Vector3(p1[0], p1[1], p1[2]), r, Vector3(p1[4], p1[5], p1[6]), Vector3(p1[7], p1[8], p1[9])) def magfit(logfile): '''find best magnetometer offset fit to a log file''' print("Processing log %s" % filename) mlog = mavutil.mavlink_connection(filename, notimestamps=args.notimestamps) data = [] last_t = 0 offsets = None # now gather all the data while True: m = mlog.recv_match(condition=args.condition) if m is None: break if m.get_type() == "SENSOR_OFFSETS": # update current offsets offsets = Vector3(m.mag_ofs_x, m.mag_ofs_y, m.mag_ofs_z) if m.get_type() == "RAW_IMU": mag = Vector3(m.xmag, m.ymag, m.zmag) # add data point after subtracting the current offsets if offsets is not None: data.append(mag - offsets + noise()) if m.get_type() == "MAG" and not args.mag2: offsets = Vector3(m.OfsX,m.OfsY,m.OfsZ) mag = Vector3(m.MagX,m.MagY,m.MagZ) data.append(mag - offsets + noise()) if m.get_type() == "MAG2" and args.mag2: offsets = Vector3(m.OfsX,m.OfsY,m.OfsZ) mag = Vector3(m.MagX,m.MagY,m.MagZ) data.append(mag - offsets + noise()) print("Extracted %u data points" % len(data)) print("Current offsets: %s" % offsets) orig_data = data # find average values avg = Vector3() count = 0 for d in data: avg += d count += 1 avg /= count # subtract average data = [] for d in orig_data: data.append(d - avg) print("Average %s" % avg) #data = select_data(data) diag = Vector3(1,1,1) offdiag = Vector3(0,0,0) # remove initial outliers if False: data.sort(lambda a,b : radius_cmp(a,b,offsets,diag,offdiag)) data = data[len(data)/16:-len(data)/16] # do an initial fit (offsets, field_strength, diag, offdiag) = fit_data(data) for count in range(3): # sort the data by the radius data.sort(lambda a,b : radius_cmp(a,b,offsets,diag,offdiag)) print("Fit %u : %s %s %s field_strength=%6.1f to %6.1f" % ( count, offsets, diag, offdiag, radius(data[0], offsets,diag,offdiag), radius(data[-1], offsets,diag,offdiag))) # discard outliers, keep the middle data = data[len(data)/32:-len(data)/32] # fit again (offsets, field_strength, diag, offdiag) = fit_data(data) print("Final : %s %s %s field_strength=%6.1f to %6.1f" % ( offsets, diag, offdiag, radius(data[0], offsets, diag, offdiag), radius(data[-1], offsets, diag, offdiag))) offsets -= avg print("With average : %s" % offsets) if args.plot: data2 = [correct(d,offsets,diag,offdiag) for d in orig_data] plot_data(orig_data, data2) def plot_data(orig_data, data): '''plot data in 3D''' from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt fig = plt.figure() for dd, c, p in [(orig_data, 'r', 1), (data, 'b', 2)]: ax = fig.add_subplot(1, 2, p, projection='3d') xs = [ d.x for d in dd ] ys = [ d.y for d in dd ] zs = [ d.z for d in dd ] ax.scatter(xs, ys, zs, c=c, marker='o') ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') plt.show() total = 0.0 for filename in args.logs: magfit(filename)