1%> @brief
this function estimates accelerometer calibration parameters
3%> @details
this function estimates accelerometer calibration parameters
4%> (sensitivity and bias)
using zero velocity ranges. The function uses data
5%> coming from a 6 axis calibration sequence (rotating a cube on a flat surface)
6%> or from a 12 axis calibration sequence (rotating a dodecahedron on a flat
7%> surface). The function uses a least squares optimization to estimate the
8%> calibration parameters. The function also estimates the gyroscope bias.
10%> @param data -
struct as defined in @ref
jumpCheckData, the data must contain
11%> zero movement sequences where only one axis is aligned with gravity and the
12%> other two axes are perpendicular to gravity. The data must contain at least
13%> six different sequences, i.e. every side must lay towards the ground at least
14%> once. The order of the sequences is not important.
15%> @param zeroVelocity - logical vector indicating zero velocity ranges
16%> @param options - the available options are:
17%> - plot - logical,
if true, plots the results to the console (
default false)
18%> - minZvDuration - minimum duration of zero velocity range in seconds (
default 0.1 s)
19%> - maxZvDuration - maximum duration of zero velocity range in seconds, before
20%> splitting it into smaller ranges (
default 60 s)
21%> - dodecahedron - logical,
if true, the function expects data from a dodecahedron
22%> movement (
default false). If enabled, the function expects all zero velocity
23%> ranges to be one of the 12 faces, i.e. it will not check
if any wrong orientations
26%> @retval params -
struct containing the estimated calibration parameters
27%>
for the accelerometer and high g sensor. The
struct has the following fields:
28%> - acc - struct containing the accelerometer calibration parameters:
29%> - bias - 1x3 vector containing the estimated accelerometer biases in g
30%> - sensitivity - 1x3 vector (six faces case) or 3x3 matrix (twelve faces case)
31%> - hig - struct containing the high g sensor calibration parameters:
32%> - bias - 1x3 vector containing the estimated high g sensor biases in g
33%> - sensitivity - 1x3 vector (six faces case) or 3x3 matrix (twelve faces case)
34%> containing the estimated accelerometer sensitivities (and cross-axis sensitivities).
35%> - gyro - struct containing the gyroscope calibration parameters:
36%> - bias - 1x3 vector containing the estimated gyroscope biases in rad/s
38%> @note see @ref derivation7IntialParamEst for more detailes information.
41%> The calibration parameters are estimated using the following error models.
43%> <b>For the six faces case</b>:
45%> The accelerometer measurements are modeled as:
48%> a_x^\text{(meas)} &= s_{x}^\text{(a)} \cdot a_x^\text{(
true)} + b_{x}^\text{(a)} + \varepsilon_{ax} \\
49%> a_y^\text{(meas)} &= s_{y}^\text{(a)} \cdot a_y^\text{(
true)} + b_{y}^\text{(a)} + \varepsilon_{ay} \\
50%> a_z^\text{(meas)} &= s_{z}^\text{(a)} \cdot a_z^\text{(
true)} + b_{z}^\text{(a)} + \varepsilon_{az} \\
53%> where @f$ \varepsilon @f$ is the residual error.
55%> The
return value
for the sensitivity is a @f$ 1 \times 3 @f$ vector containing the
56%> sensitivities in the following order: @f$ s_{x}^\text{(a)} @f$,
57%> @f$ s_{y}^\text{(a)} @f$, @f$ s_{z}^\text{(a)} @f$. The
return value
for the bias
58%> is a @f$ 1 \times 3 @f$ vector containing the biases in the following order:
59%> @f$b_{x}^\text{(a)} @f$, @f$b_{y}^\text{(a)} @f$, @f$b_{z}^\text{(a)} @f$.
61%> The gyroscope measurements are modeled as:
64%> \omega_x^\text{(meas)} &= \omega_x^\text{(
true)} + b_{x}^{(\omega)} + \varepsilon_{\omega x} \\
65%> \omega_y^\text{(meas)} &= \omega_y^\text{(
true)} + b_{y}^{(\omega)} + \varepsilon_{\omega y} \\
66%> \omega_z^\text{(meas)} &= \omega_z^\text{(
true)} + b_{z}^{(\omega)} + \varepsilon_{\omega z} \\
70%> The
return value
for the bias is a @f$ 1 \times 3 @f$ vector containing the biases
71%> in the following order: @f$b_{x}^{(\omega)} @f$, @f$b_{y}^{(\omega)} @f$,
72%> @f$b_{z}^{(\omega)} @f$.
74%> <b>For the twelve faces
case</b>:
76%> The accelerometer measurements are modeled as:
79%> a_x^\text{(meas)} &= s_{x}^{(a)} a_{x} + s_{xy}^{(a)} a_{y} + s_{xz}^{(a)} a_{z} + b_x^{(a)} + \varepsilon_{ax} \\[3mm]
80%> a_y^\text{(meas)} &= s_{yx}^{(a)} a_{x} + s_{y}^{(a)} a_{y} + s_{yz}^{(a)} a_{z} + b_y^{(a)} + \varepsilon_{ay} \\[3mm]
81%> a_z^\text{(meas)} &= s_{zx}^{(a)} a_{x} + s_{zy}^{(a)} a_{y} + s_{z}^{(a)} a_{z} + b_z^{(a)} + \varepsilon_{az},
85%> where the returned sensitivity matrix is
87%> \mathbf{s}_a = \begin{bmatrix}
88%> s_{xx}^{(a)} & s_{xy}^{(a)} & s_{xz}^{(a)} \\
89%> s_{yx}^{(a)} & s_{yy}^{(a)} & s_{yz}^{(a)} \\
90%> s_{zx}^{(a)} & s_{zy}^{(a)} & s_{zz}^{(a)}
93%> and the returned bias vector is identical to the six faces
case.
95%> The gyroscope measurements are modeled identically to the six faces
case.
99%> @remark The dodecahedron math is from Battaglia, F., & Prato, E. (2022). Generalized Laurent monomials in nonrational toric geometry. https:
104%> @copyright see the file @ref LICENSE in the root directory of the repository
111 zeroVelocity (:,1) logical
112 options.plot (1,1) logical = false
113 options.minZvDuration (1,1) double = 0.5
114 options.maxZvDuration (1,1) double = 30
115 options.dodecahedron (1,1) logical = false
118% check data structure
120assert(height(data.tt == height(zeroVelocity)),
'Data and zeroVelocity must have the same length')
127% check the amount of zero velocity ranges (consecutive zero velocity detections)
128d_zero = diff([0; zeroVelocity; 0]);
129zero_ranges = [find(d_zero == 1), find(d_zero == -1)-1];
131% remove zero velocity ranges that are too
short
132zero_ranges = zero_ranges((zero_ranges(:,2) - zero_ranges(:,1))/data.mpu_rate > options.minZvDuration, :);
134%
if any zero velocity range is larger than maxZvDuration, split it into smaller ranges
136for i = size(zero_ranges,1):-1:1
137 if (zero_ranges(i,2) - zero_ranges(i,1))/fs > options.maxZvDuration
138 % split the range into smaller ranges
139 n = ceil((zero_ranges(i,2) - zero_ranges(i,1))/fs/options.maxZvDuration);
140 split_ranges = round(linspace(zero_ranges(i,1), zero_ranges(i,2), n+1));
141 zero_ranges = [zero_ranges(1:i-1,:); split_ranges(1:end-1)', split_ranges(2:end)'; zero_ranges(i+1:end,:)];
145% check
if there are any zero velocity ranges
146if isempty(zero_ranges)
147 error(
'No zero velocity ranges found')
150% number of zero velocity ranges
151n_ranges = size(zero_ranges,1);
153% extract mean measurements for each zero velocity range
154acc_meas = zeros(n_ranges, 3); % accelerometer measurements
155hig_meas = zeros(n_ranges, 3); % high g accelerometer measurements
156gyr_meas = zeros(n_ranges, 3); % gyroscope measurements
158 acc_meas(i,:) = mean(data.tt.acc(zero_ranges(i,1):zero_ranges(i,2),:),1);
159 hig_meas(i,:) = mean(data.tt.hig(zero_ranges(i,1):zero_ranges(i,2),:),1);
160 gyr_meas(i,:) = mean(data.tt.gyr(zero_ranges(i,1):zero_ranges(i,2),:),1);
164% perform various checks depending on the calibration sequence (cube or dodecahedron)
166if ~options.dodecahedron
167 % check that all detected zero velocity ranges are valid (i.e. only one axis is
168 % aligned with gravity). since we know from the datasheet that we can expect
169 % cross-axis sensitivity of +-2% and biases of +- 0.08 g, we can use these values
170 % to check if the zero velocity range is valid.
172 fac = 2; % factor to extend typical datasheet range
173 max_value_perpendicular = fac * (0.1 * g);
174 min_value_aligned = (1 - (fac * 0.1)) * g;
175 for i = n_ranges:-1:1
179 if abs(acc_meas(i,j)) < max_value_perpendicular
181 elseif abs(acc_meas(i,j)) > min_value_aligned
182 aligned = aligned + 1;
186 if perp ~= 2 || aligned ~= 1
187 warning('Found invalid range and removed it. Was the sensor laying flat on a surface?')
188 zero_ranges(i,:) = [];
192 n_ranges = n_ranges - 1;
196 % extract true gravity vector for each zero velocity range
197 true_acc = zeros(n_ranges, 3);
199 [~, idx] = max(abs(acc_meas(i,:)));
200 true_acc(i,idx) = sign(acc_meas(i,idx)) * g;
203 % check if all six sides of the cube are present at least once
205 assert(any(true_acc(:,1) == g & true_acc(:,2) == 0 & true_acc(:,3) == 0)); % x axis positive
206 assert(any(true_acc(:,1) == -g & true_acc(:,2) == 0 & true_acc(:,3) == 0)); % x axis negative
207 assert(any(true_acc(:,1) == 0 & true_acc(:,2) == g & true_acc(:,3) == 0)); % y axis positive
208 assert(any(true_acc(:,1) == 0 & true_acc(:,2) == -g & true_acc(:,3) == 0)); % y axis negative
209 assert(any(true_acc(:,1) == 0 & true_acc(:,2) == 0 & true_acc(:,3) == g)); % z axis positive
210 assert(any(true_acc(:,1) == 0 & true_acc(:,2) == 0 & true_acc(:,3) == -g)); % z axis negative
212 error('Not all six sides of the cube are present in the data')
215else % dodecahedron case
217 % define the dodecahedron normal vectors
219 phi = (1 + sqrt(5)) / 2;
229 dod_norm = [dod_norm; -dod_norm]'; % add the negative vectors
231 % rotate around x to align with gravity vector
232 alpha = asin( (1/phi) / norm([1/phi, 1]) );
233 R = rotx(rad2deg(alpha));
234 dod_norm = R * dod_norm;
237 dod_norm = dod_norm ./ vecnorm(dod_norm);
240 dod_norm = dod_norm' * g;
242 % swap the first two columns to math the frame
243 dod_norm = dod_norm(:,[2,1,3]);
245 % extract true gravity vector for each zero velocity range
246 true_acc = zeros(n_ranges, 3);
249 % find the closest normal vector
250 [this_min, idx] = min(vecnorm(dod_norm - acc_meas(i,:), 2, 2));
252 % assign the true gravity vector
253 true_acc(i,:) = dod_norm(idx,:);
255 if norm(this_min) > 0.65
256 warning("Found possible wrong orientation. Please
double check the data")
261 % check if all twelve faces of the dodecahedron are present at least once
262 if ~isempty(setdiff(dod_norm, unique(true_acc, 'rows'), 'rows'))
263 error('Not all twelve faces of the dodecahedron are present in the data')
268%% extracting gyro bias
269params.gyr.bias = mean(gyr_meas);
275prob = optimproblem('ObjectiveSense', 'minimize');
277% variables accelerometer
278% biases (2x typical datasheet values)
279b_acc_x = optimvar('b_acc_x', 1, 'Type', 'continuous', 'LowerBound', -0.12 * g, 'UpperBound', 0.12 * g);
280b_acc_y = optimvar('b_acc_y', 1, 'Type', 'continuous', 'LowerBound', -0.12 * g, 'UpperBound', 0.12 * g);
281b_acc_z = optimvar('b_acc_z', 1, 'Type', 'continuous', 'LowerBound', -0.16 * g, 'UpperBound', 0.16 * g);
283% sensitivities (2x typical datasheet values)
284s_acc_x = optimvar('s_acc_x', 1, 'Type', 'continuous', 'LowerBound', 0.94, 'UpperBound', 1.06);
285s_acc_y = optimvar('s_acc_y', 1, 'Type', 'continuous', 'LowerBound', 0.94, 'UpperBound', 1.06);
286s_acc_z = optimvar('s_acc_z', 1, 'Type', 'continuous', 'LowerBound', 0.94, 'UpperBound', 1.06);
288% cross axis sensitivities (2x typical datasheet values)
289if options.dodecahedron
290 s_acc_xy = optimvar('s_acc_xy', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
291 s_acc_xz = optimvar('s_acc_xz', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
292 s_acc_yx = optimvar('s_acc_yx', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
293 s_acc_yz = optimvar('s_acc_yz', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
294 s_acc_zx = optimvar('s_acc_zx', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
295 s_acc_zy = optimvar('s_acc_zy', 1, 'Type', 'continuous', 'LowerBound', -0.06, 'UpperBound', 0.06);
299eps_acc_x = optimvar('eps_acc_x', n_ranges, 'Type', 'continuous');
300eps_acc_y = optimvar('eps_acc_y', n_ranges, 'Type', 'continuous');
301eps_acc_z = optimvar('eps_acc_z', n_ranges, 'Type', 'continuous');
303% variables high g sensor
304% biases (max datasheet values)
305b_hig_x = optimvar('b_hig_x', 1, 'Type', 'continuous', 'LowerBound', -6 * g, 'UpperBound', 6 * g);
306b_hig_y = optimvar('b_hig_y', 1, 'Type', 'continuous', 'LowerBound', -6 * g, 'UpperBound', 6 * g);
307b_hig_z = optimvar('b_hig_z', 1, 'Type', 'continuous', 'LowerBound', -6 * g, 'UpperBound', 6 * g);
309% sensitivities (max datasheet values)
310s_hig_x = optimvar('s_hig_x', 1, 'Type', 'continuous', 'LowerBound', 0.87, 'UpperBound', 1.13);
311s_hig_y = optimvar('s_hig_y', 1, 'Type', 'continuous', 'LowerBound', 0.87, 'UpperBound', 1.13);
312s_hig_z = optimvar('s_hig_z', 1, 'Type', 'continuous', 'LowerBound', 0.87, 'UpperBound', 1.13);
314% cross axis sensitivities (2x typical datasheet values)
315if options.dodecahedron
316 s_hig_xy = optimvar('s_hig_xy', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
317 s_hig_xz = optimvar('s_hig_xz', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
318 s_hig_yx = optimvar('s_hig_yx', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
319 s_hig_yz = optimvar('s_hig_yz', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
320 s_hig_zx = optimvar('s_hig_zx', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
321 s_hig_zy = optimvar('s_hig_zy', 1, 'Type', 'continuous', 'LowerBound', -0.075, 'UpperBound', 0.075);
325eps_hig_x = optimvar('eps_hig_x', n_ranges, 'Type', 'continuous');
326eps_hig_y = optimvar('eps_hig_y', n_ranges, 'Type', 'continuous');
327eps_hig_z = optimvar('eps_hig_z', n_ranges, 'Type', 'continuous');
330if ~options.dodecahedron
332 meas_acc_x = s_acc_x * true_acc(:,1) + b_acc_x - acc_meas(:,1) == eps_acc_x;
333 meas_acc_y = s_acc_y * true_acc(:,2) + b_acc_y - acc_meas(:,2) == eps_acc_y;
334 meas_acc_z = s_acc_z * true_acc(:,3) + b_acc_z - acc_meas(:,3) == eps_acc_z;
337 meas_hig_x = s_hig_x * true_acc(:,1) + b_hig_x - hig_meas(:,1) == eps_hig_x;
338 meas_hig_y = s_hig_y * true_acc(:,2) + b_hig_y - hig_meas(:,2) == eps_hig_y;
339 meas_hig_z = s_hig_z * true_acc(:,3) + b_hig_z - hig_meas(:,3) == eps_hig_z;
343 meas_acc_x = s_acc_x * true_acc(:,1) + s_acc_xy * true_acc(:,2) + s_acc_xz * true_acc(:,3) + b_acc_x - acc_meas(:,1) == eps_acc_x;
344 meas_acc_y = s_acc_y * true_acc(:,2) + s_acc_yx * true_acc(:,1) + s_acc_yz * true_acc(:,3) + b_acc_y - acc_meas(:,2) == eps_acc_y;
345 meas_acc_z = s_acc_z * true_acc(:,3) + s_acc_zx * true_acc(:,1) + s_acc_zy * true_acc(:,2) + b_acc_z - acc_meas(:,3) == eps_acc_z;
348 meas_hig_x = s_hig_x * true_acc(:,1) + s_hig_xy * true_acc(:,2) + s_hig_xz * true_acc(:,3) + b_hig_x - hig_meas(:,1) == eps_hig_x;
349 meas_hig_y = s_hig_y * true_acc(:,2) + s_hig_yx * true_acc(:,1) + s_hig_yz * true_acc(:,3) + b_hig_y - hig_meas(:,2) == eps_hig_y;
350 meas_hig_z = s_hig_z * true_acc(:,3) + s_hig_zx * true_acc(:,1) + s_hig_zy * true_acc(:,2) + b_hig_z - hig_meas(:,3) == eps_hig_z;
355obj = sum(eps_acc_x.^2) + sum(eps_acc_y.^2) + sum(eps_acc_z.^2) + ...
356 sum(eps_hig_x.^2) + sum(eps_hig_y.^2) + sum(eps_hig_z.^2);
358% add constraints and objective
359prob.Constraints.meas_acc_x = meas_acc_x;
360prob.Constraints.meas_acc_y = meas_acc_y;
361prob.Constraints.meas_acc_z = meas_acc_z;
362prob.Constraints.meas_hig_x = meas_hig_x;
363prob.Constraints.meas_hig_y = meas_hig_y;
364prob.Constraints.meas_hig_z = meas_hig_z;
368opts = optimoptions("lsqlin", ...
369 "Algorithm","interior-point", ...
371 "OptimalityTolerance", 1e-18, ...
372 "StepTolerance", 1e-15, ...
373 "MaxIterations", 1e6);
376 opts.Display = "iter-detailed";
379[sol, ~, flag] = solve(prob, "Options",opts);
382 error('Optimization did not converge')
386params.acc.bias = [sol.b_acc_x, sol.b_acc_y, sol.b_acc_z];
387params.hig.bias = [sol.b_hig_x, sol.b_hig_y, sol.b_hig_z];
389if ~options.dodecahedron
390 params.acc.sensitivity = [sol.s_acc_x, sol.s_acc_y, sol.s_acc_z];
391 params.hig.sensitivity = [sol.s_hig_x, sol.s_hig_y, sol.s_hig_z];
393 params.acc.sensitivity = [sol.s_acc_x, sol.s_acc_xy, sol.s_acc_xz; ...
394 sol.s_acc_yx, sol.s_acc_y, sol.s_acc_yz; ...
395 sol.s_acc_zx, sol.s_acc_zy, sol.s_acc_z];
396 params.hig.sensitivity = [sol.s_hig_x, sol.s_hig_xy, sol.s_hig_xz; ...
397 sol.s_hig_yx, sol.s_hig_y, sol.s_hig_yz; ...
398 sol.s_hig_zx, sol.s_hig_zy, sol.s_hig_z];
function jumpCheckData(in data)
function jumpEstimateCalibrationParameters(in data, in zeroVelocity, in options)