Master Thesis Code
by Simon Moser
Loading...
Searching...
No Matches
derivation6Ekf6D.m
Go to the documentation of this file.
1% =========================================================================== %
2%> @example derivation6Ekf6D.m
3%>
4%> In this example, a 6D orientation and position estimation is performed.
5%>
6%> @note a more detailed explanation can be found here: @ref derivation6Ekf6D
7%>
8%> @warning This document is in the debug branch. The state vector is augmentet with the sensitivity. This is not the final version.
9%>
10%> <details>
11% =========================================================================== %
12clear; close all; clc;
13
14% set seed for reproducibility
15rng(1);
16
17%% Sensor Data Generation
18% load predefined trajectory
19load('rotatingCube.mat');
20
21js = JumpSensor;
22
23data = js.generateFromTrajectory(traj);
24
25% define time step
26dt = 1/data.mpu_rate;
27
28% get actual bias and sensitivity
29bias_acc = js.getParams.mpu.acc.ConstantBias;
30bias_gyr = js.getParams.mpu.gyr.ConstantBias;
31
32%% Zero velocity detection
33std_acc = max(abs(movstd(data.tt.acc, 100)),[],2);
34std_gyr = max(abs(movstd(data.tt.gyr, 100)),[],2);
35
36zero_velocity = std_acc < 0.15 & std_gyr < 0.15;
37
38%% measurement vector
39z = zeros(12, size(data.tt.acc,1));
40z(1:3,:) = data.tt.acc';
41z(4:6,:) = data.tt.gyr' * pi/180; %convert to radians/s
42% transitional velocity in rows 7,8,9 is zero
43% angular velocity in rows 10,11,12 is zero
44
45%% filter Equations
46% number of observations
47N = size(z,2);
48
49% number of states
50% 4 q, 3 a, 3 v, 3 w, 3 b_a, 3 b_w, 3 s_a
51n_x = 25;
52
53% state vector
54x_fm = nan(n_x, N); % fm: forward, minus
55x_fp = nan(n_x, N); % fp: forward, plus
56x_bp = nan(n_x, N); % bp: backward, plus
57x_0 = zeros(n_x, 1);
58x_0(1) = 1; % initial quaternion
59x_0(11:13) = traj.getTrajectory.pos(1,:); % initial position
60x_0(23:25) = 1; % initial sensitivity
61
62% covariance matrix
63P_fm = nan(n_x, n_x, N);
64P_fp = nan(n_x, n_x, N);
65P_bp = nan(n_x, n_x, N);
66
67% initial covariance matrix
68alpha = 1e-4;
69beta = 0.6;
70gamma = 0.5;
71zeta = 5 * pi / 180;
72kappa = 0.03;
73P_0 = diag([repmat(alpha,1,16), repmat(beta,1,2), gamma, repmat(zeta,1,3), repmat(kappa,1,3)]) .^ 2;
74
75% process noise
76% defined in a function
77
78% measurement noise
79std_acc_xy = 0.045;
80std_acc_z = 0.07;
81std_gyr = 0.07 * pi / 180;
82std_pseudo_acc = 1e-9;
83std_pseudo_w = 1e-9;
84R = diag([std_acc_xy, std_acc_xy, std_acc_z, std_gyr, std_gyr, std_gyr, repmat(std_pseudo_acc,1,3), repmat(std_pseudo_w,1,3)]) .^ 2;
85
86%% iterate over all observations (filtering)
87bad_iterations = false(1,N);
88warning("off");
89for k = 1:N
90 if k == 1
91 % initial prediction
92 F = F_jac(x_0, dt);
93 x_fm(:, k) = f_fun(x_0,dt);
94 P_fm(:, :, k) = F * P_0 * F' + Q_cov(x_0, dt, zero_velocity(k));
95 else
96 % prediction
97 F = F_jac(x_fp(:, k-1),dt);
98 x_fm(:, k) = f_fun(x_fp(:, k-1),dt);
99 P_fm(:, :, k) = F * P_fp(:, :, k-1) * F' + Q_cov(x_fp(:, k-1), dt, zero_velocity(k));
100 end
101
102 % normalize quaternion
103 x_fm(1:4, k) = quaternion(x_fm(1:4, k)').normalize.compact';
104
105 h = h_fun(x_fm(:, k));
106 H = H_jac(x_fm(:, k));
107
108 % handle measurements
109 if ~zero_velocity(k)
110 % no zero velocity detected
111 H(7:12, :) = 0;
112 end
113
114 % check observability
115 % O = obsv(F,H);
116 % if rank(O) < n_x
117 % warning("System not observable at time step %d", k);
118 % bad_iterations(k) = true;
119 % end
120
121 % kalman gain
122 K = P_fm(:, :, k) * H' * pinv(H * P_fm(:, :, k) * H' + R);
123
124 % % alternative Kalman gain (nudging biases towards zero)
125 % lambda = 1e-5;
126 % lambda_matrix = zeros(size(R)); lambda_matrix(7:12,7:12) = lambda * eye(6);
127 % K = P_fm(:, :, k) * H' * pinv(H * P_fm(:, :, k) * H' + R + lambda_matrix);
128
129 % update
130 x_fp(:, k) = x_fm(:, k) + K * (z(:, k) - h);
131 P_fp(:, :, k) = (eye(n_x) - K * H) * P_fm(:, :, k) * (eye(n_x) - K * H)' + K * R * K';
132
133 % normalize quaternion
134 x_fp(1:4, k) = quaternion(x_fp(1:4, k)').normalize.compact';
135
136 % check if a warning occured
137 [~, warnID] = lastwarn;
138 if strcmp(warnID, 'MATLAB:nearlySingularMatrix') || strcmp(warnID, 'MATLAB:singularMatrix')
139 bad_iterations(k) = true;
140 lastwarn("",""); % reset warning
141 end
142end
143warning("on");
144
145%% smoothing
146if 1
147 % iterate over observations
148 for k = N-1:-1:1
149 if k == N-1
150 % initialization
151 x_bp(:, N) = x_fp(:, N);
152 P_bp(:, :, N) = P_fp(:, :, N);
153 end
154
155 F = F_jac(x_fp(:, k), dt);
156
157 I = pinv(P_fm(:, :, k+1));
158 K = P_fp(:, :, k) * F' * I;
159 x_bp(:,k) = x_fp(:,k) + K * (x_bp(:,k+1) - x_fm(:,k+1));
160 P_bp(:,:,k) = P_fp(:,:,k) - K * (P_fm(:,:,k+1) - P_bp(:,:,k+1)) * K';
161
162 if any(isnan(x_bp(:,k))) | any(isnan(P_bp(:,:,k)))
163 bad_iterations(k) = true;
164 end
165
166 end
167end
168
169%% try the VQF filter
170try
171 vqf = MyVqf();
172 vqf_offline = vqf.filterOffline(data.tt.acc, data.tt.gyr, data.tt.mag);
173 vqf_offline = quaternion(vqf_offline.quat6D);
174 vqf_offline = quaternion(0, sqrt(2)/2, sqrt(2)/2, 0).normalize .* vqf_offline; % ENU -> NED
175 vqf_filter = vqf.filter(data.tt.acc, data.tt.gyr, data.tt.mag);
176 vqf_filter = quaternion(vqf_filter.quat6D);
177 vqf_filter = quaternion(0, sqrt(2)/2, sqrt(2)/2, 0).normalize .* vqf_filter; % ENU -> NED
178catch ME
179 warning("VQF Filtering failed. Python package 'vqf' installed?")
180 vqf_offline = quaternion.ones(1,N);
181end
182
183%% prepare results
184% ground truth
185ori_gt = quaternion(traj.getTrajectory.ori);
186pos_gt = traj.getTrajectory.pos;
187
188% filter results
189ori_filt = quaternion(x_fp(1:4,:)');
190pos_filt = x_fp(11:13,:)';
191
192% smoothing results
193ori_smoo = quaternion(x_bp(1:4,:)');
194pos_smoo = x_bp(11:13,:)';
195
196%% compare results
197
198% orientation animation
199animateQuatPos(ori_filt, pos_filt, ori_smoo, pos_smoo, ori_gt, pos_gt, ...
200 "names", ["Filter", "Smoother", "Ground Truth"], "title", "Orientation and Position Estimation", ...
201 "position", [0,0.25,0.5,0.5]);
202
203% position plot
204figure("Units", "normalized", "Position", [0.5,0.25,0.5,0.5]);
205plot3(pos_gt(:,1), pos_gt(:,2), pos_gt(:,3), 'DisplayName', 'Ground Truth');
206hold on;
207plot3(pos_filt(:,1), pos_filt(:,2), pos_filt(:,3), 'DisplayName', 'Filter');
208plot3(pos_smoo(:,1), pos_smoo(:,2), pos_smoo(:,3), 'DisplayName', 'Smoother');
209hold off;
210grid on;
211xlabel("x [m]");
212ylabel("y [m]");
213zlabel("z [m]");
214legend;
215
216
217%% Helper functions
218% this function calculates the state transition function from a given state vector x
219function x = f_fun(x, dt)
220
221 % new state vector
222 x_new = nan(size(x));
223
224 % new quaternion
225 epsilon = 1e-12;
226 q_old = x(1:4);
227 omega = x(14:16)';
228 omega_norm = norm(omega,2);
229 omega_matrix = [0, -omega(1) -omega(2) -omega(3); ...
230 omega(1) 0 omega(3) -omega(2); ...
231 omega(2) -omega(3) 0 omega(1); ...
232 omega(3) omega(2) -omega(1) 0];
233
234 q_new = (cos(omega_norm * dt / 2) * eye(4) + 1/(omega_norm+epsilon) * sin(omega_norm * dt / 2) * omega_matrix) * q_old;
235 x_new(1:4) = q_new;
236
237
238 % new acceleration
239 x_new(5:7) = x(5:7);
240
241 % new velocity
242 x_new(8:10) = x(8:10) + x(5:7) * dt;
243
244 % new position
245 x_new(11:13) = x(11:13) + x(8:10) * dt + 0.5 * x(5:7) * dt^2;
246
247 % new angular velocity
248 x_new(14:16) = x(14:16);
249
250 % new biases
251 x_new(17:22) = x(17:22);
252
253 % new sensitivity
254 x_new(23:25) = x(23:25);
255
256 x = x_new;
257
258end
259
260
261% this function calculates the jacobian of the state transition function
262% from a given state vector x
263function F = F_jac(x, dt)
264
265 % F1 ( quaternion and angular velocity )
266 epsilon = 1e-12;
267 q0 = x(1); q1 = x(2); q2 = x(3); q3 = x(4);
268 wx = x(14); wy = x(15); wz = x(16);
269 w_norm = norm([wx, wy, wz],2);
270 alpha = cos(w_norm * dt / 2) * w_norm;
271 beta = sin(w_norm * dt / 2);
272
273 F_1_1 = 1/(w_norm+epsilon) * [alpha, -wx * beta, -wy * beta, -wz * beta; ...
274 wx * beta, alpha, wz * beta, -wy * beta; ...
275 wy * beta, -wz * beta, alpha, wx * beta; ...
276 wz * beta, wy * beta, -wx * beta, alpha];
277
278 F_1_2 = beta/(w_norm+epsilon) * [-q1, -q2, -q3; ...
279 q0, -q3, q2; ...
280 q3, q0, -q1; ...
281 -q2, q1, q0];
282
283 F_1 = [F_1_1, zeros(4,9), F_1_2, zeros(4,9)];
284
285 % F2 (position, velocity, acceleration)
286
287 F_2_1 = eye(9);
288 F_2_1(4:6,1:3) = dt * eye(3);
289 F_2_1(7:9,4:6) = dt * eye(3);
290 F_2_1(7:9,1:3) = dt^2;
291
292 F_2 = [zeros(9,4), F_2_1, zeros(9,12)];
293
294 % F3 (angular velocity)
295 F_3 = zeros(3,numel(x));
296 F_3(1:3,14:16) = eye(3);
297
298 % F4 (biases)
299 F_4 = zeros(9,numel(x));
300 F_4(:,17:25) = eye(9);
301
302 F = [F_1; F_2; F_3; F_4];
303
304end
305
306% this function calculates the measurement function from a given state vector x
307function h=h_fun(x)
308
309 g = [0;0;-9.81]; % gravity vector in world frame
310
311 % acceleration
312 s_a = x(23:25); % sensitivity
313 a_g = x(5:7); % acceleration in global frame
314 b_a = x(17:19); % acceleration biases
315 R = quaternion(x(1:4)').rotmat("frame");
316
317 h1 = s_a .* R * (a_g + g) + b_a;
318
319 % angular velocity
320 w_s = x(14:16); % angular velocity in sensor frame
321 b_w = x(20:22); % angular velocity biases
322
323 h2 = w_s + b_w;
324
325 % pseudo measurements
326 v_g = x(8:10); % velocity in global frame
327 h3 = [v_g; w_s];
328
329 h = [h1; h2; h3];
330
331end
332
333% this function calculates the jacobian of the measurement function from a
334% given state vector x
335function H = H_jac(x)
336
337 g = 9.81;
338
339 % H1 (acceleration)
340 q0 = x(1); q1 = x(2); q2 = x(3); q3 = x(4);
341 s_a = x(23:25); % sensitivity
342 alpha = x(5) + g; % ax_g + g
343 beta = x(6) + g; % ay_g + g
344 gamma = x(7) + g; % az_g + g
345
346 H_1_1 = s_a .* 2 .* [alpha * q0 + beta * q3 - gamma * q2, alpha * q1 + beta * q2 + gamma * q3, beta * q1 - alpha * q2 - gamma * q0, beta * q0 - alpha * q3 + gamma * q1; ...
347 beta * q0 - alpha * q3 + gamma * q1, alpha * q2 - beta * q1 + gamma * q0, alpha * q1 + beta * q2 + gamma * q3, gamma * q2 - beta * q3 - alpha * q0; ...
348 alpha * q2 - beta * q1 + gamma * q0, alpha * q3 - beta * q0 - gamma * q1, alpha * q0 + beta * q3 - gamma * q2, alpha * q1 + beta * q2 + gamma * q3];
349
350 H_1_2 = s_a .* quaternion(x(1:4)').rotmat("frame");
351
352 H_1_3 = diag([alpha * (q0^2 + q1^2 - q2^2 - q3^2) + 2 * beta * (q0*q3 + q1*q2) - 2 * gamma * (q0 * q2 - q1 * q3), ...
353 -2 * alpha * (q0*q3 - q1*q2) + beta * (q0^2 - q1^2 + q2^2 - q3^2) + 2 * gamma * (q0 * q1 + q2 * q3), ...
354 2 * alpha * (q0*q2 + q1*q3) - 2 * beta * (q0*q1 - q2*q3) + gamma * (q0^2 - q1^2 - q2^2 + q3^2)]);
355
356 H_1 = [H_1_1, H_1_2, zeros(3,9), eye(3), zeros(3,3), H_1_3];
357
358 % H2 (angular velocity)
359 H_2 = [zeros(3,13), eye(3), zeros(3,3), eye(3), zeros(3,3)];
360
361 % H3 (pseudo measurements)
362 H_3 = [zeros(3,7), eye(3), zeros(3,15); ...
363 zeros(3,13), eye(3), zeros(3,9)];
364
365 H = [H_1; H_2; H_3];
366
367end
368
369% this function calculates the process noise covariance matrix
370function Q = Q_cov(x, dt, zero_velocity)
371
372 %> \todo change process noise based on zero velocity detection
373
374 sigma_w_p = 0.1 * pi / 180; % educated guess
375 sigma_a_p = 0.025; % educated guess
376 sigma_b_a = 0.001; % from allan variance
377 sigma_b_w = 0.002 * pi / 180; % from allan variance
378 sigma_s_a = 1e-5; % educated guess
379
380 % quaternion
381 q0 = x(1); q1 = x(2); q2 = x(3); q3 = x(4);
382 Q_q = (dt * sigma_w_p / 2)^2 * [...
383 1 - q0^2, -q0 * q1, -q0 * q2, -q0 * q3; ...
384 -q0 * q1, 1 - q1^2, -q1 * q2, -q1 * q3; ...
385 -q0 * q2, -q1 * q2, 1 - q2^2, -q2 * q3; ...
386 -q0 * q3, -q1 * q3, -q2 * q3, 1 - q3^2];
387
388 % acceleration, velocity and position
389 Q_a = zeros(9);
390 Q_a(1:3, 1:3) = eye(3);
391 Q_a(4:6, 4:6) = dt^2 * eye(3);
392 Q_a(7:9, 7:9) = dt^4 / 4 * eye(3);
393 Q_a(4:6, 1:3) = dt * eye(3);
394 Q_a(7:9, 4:6) = dt^3 / 2 * eye(3);
395 Q_a(7:9, 1:3) = dt^2 / 2 * eye(3);
396 Q_a = (triu(Q_a.',1) + tril(Q_a)) * sigma_a_p^2;
397
398 % angular velocity
399 Q_w = eye(3) * sigma_w_p^2;
400
401 % biases
402 Q_b = diag([sigma_b_a^2, sigma_b_a^2, sigma_b_a^2, sigma_b_w^2, sigma_b_w^2, sigma_b_w^2]);
403
404 % sensitivity
405 Q_s = eye(3) * sigma_s_a^2;
406
407 % combine
408 Q = blkdiag(Q_q, Q_a, Q_w, Q_b, Q_s);
409
410end
function animateQuatPos(in quat, in pos, in options)
JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
Definition JumpSensor.m:41
function generateFromTrajectory(in obj, in traj)
generates data from a Trajectory object.
MyVqf is an interface to the VQF python package.
Definition MyVqf.m:29
handle class from MATLAB.
function f_fun(in x)
function h_fun(in x)
function H_jac(in x)
function F_jac(in x, in dt)
function Q_cov(in x, in dt, in zero_velocity)