Calibration¶
Calibrate magnetometer and accelerometer readings
Author(s): Phil Underwood
Implementation Notes¶
Software and Dependencies:
Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads
Axes¶
There are several axis conventions used.
World coordinates: this represents the “real world”, X is due east, Y is due North, Z is Up
Device coordinates: this represents the device. Y is the primary axis - the direction of travel of direction or direction of a pointer. Z is up, and X is to the right if Y is facing away from you and Z is up.
- class mag_cal.Calibration(mag_axes: str = '+X+Y+Z', grav_axes: str = None)¶
Object representing a magnetometer and accelerometer calibration
Create an object representing the calibration coefficients for a combined magnetometer and accelerometer
- Parameters:
mag_axes (str) – A string representing how your magnetometer is mounted with respect to your device. For each axis XYZ of your device (see above for axis descriptions), state the corresponding axis of your sensor. Add a
+or-to let us know if it is inverted. So for a sensor that is correctly mounted it will be"+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as"-Y+X-Z"grav_axes (str) – Same format as
mag_axesbut for the accelerometer. Default is copy of mag_axes.
- ELLIPSOID = 0¶
Fit to Ellipsoid
- AXIS_CORRECTION = 1¶
Fit to ellipsoid then correct any axis misalignment
- NON_LINEAR = 2¶
as per
AXIS_CORRECTIONand then do correction for non-linear effects
- FAST_NON_LINEAR = 3¶
as per
AXIS_CORRECTIONand then do quick non-linear correction
- calibrate(mag_data: ndarray, grav_data: ndarray, routine: int = 3)¶
Perform a full calibration, with an algorithm depending on the value of
routine. If you select a routine other thanELLIPSOIDyou must provide at least one run of at least four shots in the same direction with varying amonts of roll. Ideally two sets of eight readings, but this is not vital.- Parameters:
mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)
grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)
routine –
what level of calibration to perform:
ELLIPSOID: Simplest form of calibration, very fast, does not require any sets of readings to be aligned. Does not correct for misalignment between pointer and sensors.AXIS_CORRECTION: This routine performs theELLIPSOIDand then applies a rotation to offset any misalignment between the pointer and sensors (and also misalignment between accelerometer and magnetometer if relevant). This process will automatically identify which shots have been taken in the same directionNON_LINEAR: Performs calibration as perAXIS_CORRECTION, then uses an optimisation process to account for non-linear sensor response. Seefit_non_linearfor details.FAST_NON_LINEAR: Performs calibration as perAXIS_CORRECTION, then uses a least-squares process to account for non-linear sensor response. A lot faster thanNON_LINEAR, but slightly less accurate. Seefit_non_linear_quickfor details
- Returns:
Measure of error: percentage error of fit for
ELLIPSOID, standard deviation of error in degrees for other methods. Normally <1 degree is acceptable, <0.5 degrees is good.
- get_angles(mag, grav) Tuple[ndarray, ndarray, ndarray]¶
Get device azimuth(bearing), inclination, and roll, given the magnetic and gravity readings
- Parameters:
mag (np.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (np.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- Returns:
(azimuth, inclination, roll) in degrees
- as_dict() Dict¶
Convert the current calibration to a dictionary, suitable for serialising via json.
- Returns:
The calibration as a dictionary
- Return type:
- classmethod from_dict(dct: Dict) Calibration¶
Create a Calibration object based on a dict previously produced by
as_dict- Parameters:
dct (dict) – dict to instantiate
- Returns:
Calibration object
- fit_ellipsoid(mag_data: ndarray, grav_data: ndarray) Tuple[float, float]¶
Take multiple sets of readings in various directions. You can then use this function to determine an ideal set of calibration coefficients
- Parameters:
mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)
grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)
- Returns:
(mag_accuracy, grav_accuracy) How well the calibrated model fits the data. Lower numbers are better
- fit_to_axis(data, axis='Y') float¶
Take multiple reading with the device pointing in the same direction, but rotated around
axis. You can repeat this with several directions. This function will take this data and ensure that your sensors aligned relative to each other. This function requires that you have runCalibration.fit_ellipsoidbeforehand.- Parameters:
data – A list of paired magnetic and gravity readings e.g.:
[(mag_data1, grav_data1), (mag_data2, grav_data2), ...], wheremag_data1andgrav_data1are (N,3) numpy arrays of readings around the axis in the first direction, andmag_data2andgrav_data2are (M,3) numpy arrays of readings around the specified axis in another direction.axis – Axis you have rotated your device around. Defaults to
"Y"
- Returns:
Average standard deviation of readings in degrees, after calibration
- fit_non_linear(data, axis: str = 'Y', param_count: int = 3, sensor: int = 1)¶
Compensate for devices which do not have a linear response between the magnetic or gravity field and their output. It is recommended to use a
param_countof either 1, 3 or 5 for this function; odd numbers generally get better results, but there is a significant risk of overfitting if higher numbers are used. See Underwood, Phil (2021) Non-linear Calibration of a Digital Compass and Accelerometer, Cave Radio Electronics Group Journal 114, pp7-10. June 2021 for more details on the algorithm used. This function uses a Nelder-Mead minimisation process to find optimal values for each non-linear parameter.This function requires that you have run both
Calibration.fit_ellipsoidandCalibration.fit_to_axisbeforehand.- Parameters:
data – A list of paired magnetic and gravity readings e.g.:
[(mag_data1, grav_data1), (mag_data2, grav_data2), ...], wheremag_data1andgrav_data1is a (N,3) numpy array of readings around the axis in the first direction, andmag_data2andgrav_data2is a (M,3) numpy array of readings around the specified axis in another direction.axis (str) – Axis along which the device has been rotated for the given readings.
param_count (int) – Number of parameters to use per sensor axis. Larger numbers will take substantially more time to calculate. Default is 3
sensor – Whether to calibrate the magnetometer, accelerometer or both. Must be one of
Calibration.MAGNETOMETER,Calibration.ACCELEROMETER, orCalibration.BOTH. Default isMAGNETOMETER, as accelerometers are generally quite well-behaved.
- Returns:
Standard deviation of accuracy of calibration in degrees
- fit_non_linear_quick(data, param_count: int = 3)¶
Compensate for devices which do not have a linear response between the magnetic or gravity field and their output. It is recommended to use a
param_countof either 1, 3 or 5 for this function; odd numbers generally get better results, but there is a significant risk of overfitting if higher numbers are used. See Underwood, Phil (2021) Non-linear Calibration of a Digital Compass and Accelerometer, Cave Radio Electronics Group Journal 114, pp7-10. June 2021 for more details on the algorithm used. This function uses a least squares method to rapidly find a good set of parameters to use. It is much faster thanfit_non_linear, but gives slightly less good results. Note it will only calibrate the magnetometer and is unable currently to apply a non-linear correction to the accelerometer. It will also only calibrate for rotations around the Y axis.This function requires that you have run both
Calibration.fit_ellipsoidandCalibration.fit_to_axisbeforehand.- Parameters:
data – A list of paired magnetic and gravity readings e.g.:
[(mag_data1, grav_data1), (mag_data2, grav_data2), ...], wheremag_data1andgrav_data1is a (N,3) numpy array of readings around the axis in the first direction, andmag_data2andgrav_data2is a (M,3) numpy array of readings around the specified axis in another direction.param_count (int) – Number of parameters to use per sensor axis. Larger numbers will take substantially more time to calculate. Default is 3
- Returns:
Standard deviation of accuracy of calibration in degrees
- accuracy(data) float¶
Calculate average accuracy for a set of multiple readings taken
- Parameters:
data – A list of paired magnetic and gravity readings e.g.:
[(mag_data1, grav_data1), (mag_data2, grav_data2)], wheremag_data1andgrav_data1is a (N,3) numpy array of readings around the axis in the first direction, andmag_data2andgrav_data2is a (M,3) numpy array of readings around the specified axis in another direction.- Returns:
Average standard deviation of readings in degrees
- uniformity(mag_data, grav_data)¶
Check the uniformity of the data - how well the calibrated data points fit on a sphere of radius 1.0
- Parameters:
mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)
grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)
- Returns:
(mag_accuracy, grav_accuracy) How well the calibrated model fits the data. Lower numbers are better
- get_orientation_vector(mag, grav)¶
- Given a set of magnetic readings and gravity readings, get the orientation
of the device in world coordinates.
- Parameters:
mag (np.array) – Numpy array of shape(N,3) or (3,)
grav (np.array) – Numpy array of shape(N,3) or (3,)
- Returns:
Device orientation in world coordinates.
- Return type:
Numpy array of same shape as mag, with device orientation.
- get_orientation_matrix(mag, grav) ndarray¶
Get the device orientation as an orthonormal matrix, given the magnetic and gravity readings
- Parameters:
mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- Returns:
Orthonormal matrix converting device coordinates to real world coordinates
- Return type:
- static matrix_to_angles(matrix: ndarray)¶
Extract the rotation angles from a matrix. Angles are “zxy” rotations (azimuth, pitch, roll)
- Parameters:
matrix (np.ndarray) –
- Returns:
azimuth, pitch, roll
- classmethod angles_to_matrix(azimuth: ndarray, inclination: ndarray, roll: ndarray)¶
Create a rotation matrix from angles “zxy” i.e azimmuth, inclination, roll. This is the invers of matrix_to_angles
- Parameters:
azimuth – float or np.array of floats for azimuth
inclination – pitch(rotation around x axis)
roll – roll around y axis
- Returns:
- find_similar_shots(mag: ndarray, grav: ndarray, precision=30, min_run=4)¶
Find runs of shots that are within precision degrees of each other
- Parameters:
mag – numpy array of magnetic data
grav – numpy array of accelerometer data
precision – number of degrees shots should be within
min_run – minimum length of run to find
- Returns:
list of start and finish indices for each run
- get_field_strengths(mag, grav)¶
Get field strength for magnetic and gravity components
- Parameters:
mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- Returns:
mag_field, grav_field
- Return type:
numpy.ndarrays if multiple readings given or floats
- get_dips(mag, grav)¶
Get the magnetic field dip
- Parameters:
mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- Returns:
dip angle(s) in degrees
- Return type:
numpy.ndarrays if multiple readings given or floats
- set_expected_mean_dip(mag, grav)¶
Store an expected dip and standard deviations. This will be used for magnetic and (gravitational!!) anomaly detection
- Parameters:
mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- set_field_characteristics(mag, grav)¶
Store magnetic and gravity field strengths and also dip angles. This will be used for magnetic and (gravitational!!) anomaly detection
- Parameters:
mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats
- Returns:
- raise_if_anomaly(mag, grav, strictness: Strictness = (2.0, 2.0, 3.0))¶
Raises an error if magnetic field strength and dip are not similar to during calibration
- Parameters:
mag (numpy.ndarray) – Magnetic readings, sequence of 3 floats
grav (numpy.ndarray) – Gravity readings, sequence of 3 floats
strictness (Strictness) –
NamedTuple containing the following entries
mag: Acceptable percentage difference in magnetic field strengthgrav: Acceptable percentage difference in gravity field strengthdip: Acceptable difference in dip in degrees
If not specified will default to 2% for
magandgravand 3° fordip
- Returns:
None
- Raises:
MagneticAnomalyErrorif magnetic field strength too big or small.GravityAnomalyErrorif gravity field strength too big or small - usually occurs if movement during readDipAnomalyErrorif magnetic field dip too big or small
- class mag_cal.Axes(axes)¶
This represents the orientation of a sensor in respect to the device orientation
- Parameters:
axes (str) – A string representing how your sensor is mounted with respect to your device. For each axis XYZ of your device (see note regarding Axes above), state the corresponding axis of your sensor. Add a
+or-in front to let us know if it is inverted. So for a sensor that is correctly mounted it will be"+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as"-Y+X+Z"
- class mag_cal.Sensor(axes='+X+Y+Z')¶
This represents the calibration coefficients for a single sensor
Create a sensor object, with axes set
- Parameters:
axes (str) – A string representing how your sensor is mounted with respect to your device. For each axis XYZ of your device (see above for axis descriptions), state the corresponding axis of your sensor. Add a
+or-to let us know if it is inverted. So for a sensor that is correctly mounted it will be"+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as"-Y+X+Z". Default is+X+Y+Z
- fit_ellipsoid(data: ndarray) float¶
Take multiple sets of readings in various directions. You can then use this function to determine an ideal set of calibration coefficients.
- Parameters:
data (np.ndarray) – Numpy array of readings of shape (N,3)
- Returns:
accuracy - How well the calibrated model fits the data; lower numbers are better.
- Return type:
- align_along_axis(data, axis='Y')¶
Calibrate for any mechanical placement error.
- Parameters:
data (List[np.ndarray]) – A list of sets of data - numpy array of shape (N,3). Each set should have been taken with the device pointing at a fixed target, but rotated through different angles
axis (str) – Axis that the device has been rotated around, must be one of “X”, “Y”, or “Z”. Default is “Y”.
- Returns:
None
- apply(data: ndarray) ndarray¶
Take a set of raw data and apply the calibration to it
- Parameters:
data (np.ndarray) – Numpy array of shape (3,) or (N,3)
- Returns:
numpy array with same shape as
data
- set_non_linear_params(params: ndarray)¶
Set the parameters for the radial basis functions
- Parameters:
params (np.ndarray) – Numpy matrix or vector with N*3 elements, where N is the number of parameters per axis
- set_linear()¶
Disable non-linear adjustments
- uniformity(data: ndarray)¶
Check the uniformity of the data points after calibration. This is measured as the standard deviation of the absolute magnitude of each measurement
- Parameters:
data (np.ndarray) – Numpy array of shape (N,3)
- Returns:
Calculated uniformity as above
- as_dict()¶
Return the calibration parameters as a dict, can be reloaded using
Sensor.from_dict- Returns:
Dict containing all the calibration data for this sensor
- classmethod from_dict(dct: dict) Sensor¶
Create a new
Sensorinstance from the given dict, which should have been created usingas_dict.
- get_field_strength(data)¶
Get the field strength from this sensor, using the original units that the data was provided in. :param numpy.ndarray data: Sensor readings, either as numpy array or sequence of 3 floats :return: Single float or numpy array of field strengths
- set_expected_field_strengths(data)¶
Store an expected field strength and standard deviations. This will be used for magnetic and (gravitational!!) anomaly detection :param numpy.ndarray data: Sensor readings, either as numpy array or sequence of 3 floats
- raise_if_anomaly(data, tolerance=0.05)¶
Raise SensorAnomaly if given reading is not within expected range
- Parameters:
data – sequence of 3 floats
tolerance – Amount if difference in field strength to accept
- Raises:
SensorErrorif field strength is greater than tolerance different from the mean
- exception mag_cal.NotCalibrated¶
Error to raise when calibration has not been done yet