Master Thesis Code
by Simon Moser
Loading...
Searching...
No Matches
derivation5Ekf6D.m
Go to the documentation of this file.
1% =========================================================================== %
2%> @example derivation5Ekf6D.m
3%>
4%> In this example, a 6D orientation estimation filter is implemented and
5%> compared to the 6D VQF filter.
6%>
7%> Using the flag USE_SENSITIVITY, the sensitivity states can be included in
8%> the filter. The sensitivity states are used to estimate the sensor
9%> sensitivities.
10%>
11%> @note a more detailed explanation can be found here: @ref derivation5Ekf6D
12%>
13%> <details>
14% =========================================================================== %
15clear; close all; clc;
16
17% set seed for reproducibility
18rng(1);
19
20% flag to use the sensitivity states
21global USE_SENSITIVITY
22USE_SENSITIVITY = false;
23
24%% Sensor Data Generation
25% load predefined trajectory
26load('rotatingCube.mat');
27
28% set all positions to zero
29tab = traj.getTab;
30tab.pos(:,:) = 0;
31traj.setTab(tab);
32traj.generateTrajectory;
33
34js = JumpSensor;
35
36data = js.generateFromTrajectory(traj);
37
38% define time step
39global dt
40dt = 1/data.mpu_rate;
41
42% get actual bias and sensitivity
43bias_acc = js.getParams.mpu.acc.ConstantBias;
44sensitivity_acc = [js.getParams.mpu.acc.AxesMisalignment(1,1),js.getParams.mpu.acc.AxesMisalignment(2,2),js.getParams.mpu.acc.AxesMisalignment(3,3)] / 100;
45bias_gyr = js.getParams.mpu.gyr.ConstantBias;
46sensitivity_gyr = [js.getParams.mpu.gyr.AxesMisalignment(1,1),js.getParams.mpu.acc.AxesMisalignment(2,2),js.getParams.mpu.acc.AxesMisalignment(3,3)] / 100;
47
48%% Zero velocity detection
49std_acc = max(abs(movstd(data.tt.acc, 100)),[],2);
50std_gyr = max(abs(movstd(data.tt.gyr, 100)),[],2);
51
52zero_velocity = std_acc < 0.15 & std_gyr < 0.15;
53
54%% measurement vector
55z = zeros(9, size(data.tt.acc,1));
56z(1:3,:) = data.tt.acc';
57z(4:6,:) = data.tt.gyr' * pi/180; %convert to radians
58% angular velocity in rows 7,8,9 is zero
59
60%% filter Equations
61% number of observations
62N = size(z,2);
63
64global n_x
65if USE_SENSITIVITY
66 n_x = 19; % 13 states + 6 sensitivity states
67else
68 n_x = 13; % 13 states
69end
70
71% state vector
72% q0, q1, q2, q3, gyr_x, gyr_y, gyr_z, bias_gyr_x, bias_gyr_y, bias_gyr_z, ...
73% bias_acc_x, bias_acc_y, bias_acc_z
74x_fm = nan(n_x, N); % fm: forward, minus
75x_fp = nan(n_x, N); % fp: forward, plus
76x_bp = nan(n_x, N); % bp: backward, plus
77if USE_SENSITIVITY
78 x_0 = [1; zeros(12,1); ones(6,1)];
79else
80 x_0 = [1; zeros(12,1)];
81end
82
83% covariance matrix
84P_fm = nan(n_x, n_x, N);
85P_fp = nan(n_x, n_x, N);
86P_bp = nan(n_x, n_x, N);
87if USE_SENSITIVITY
88 P_0 = diag([repmat(1e-3,1,4),repmat(1e-2,1,6),0.6,0.6,0.8, repmat(3e-2,1,6)]).^2;
89else
90 P_0 = diag([repmat(1e-3,1,4),repmat(1e-2,1,6),0.6,0.6,0.8]).^2;
91end
92
93% process noise
94Q = diag([repmat(1e-5,1,4), repmat(1e-3,1,3), 1e-12 * ones(1,n_x-7)]) .^ 2;
95
96% measurement noise
97std_acc_xy = 0.045;
98std_acc_z = 0.07;
99std_gyr = 0.1 * pi / 180;
100std_pseudo = 1e-9;
101R = diag([std_acc_xy, std_acc_xy, std_acc_z, std_gyr, std_gyr, std_gyr, std_pseudo, std_pseudo, std_pseudo]) .^ 2;
102
103%% iterate over all observations (filtering)
104bad_iterations = false(1,N);
105warning("off");
106for k = 1:N
107 if k == 1
108 % initial prediction
109 F = f_jac(x_0);
110 x_fm(:, k) = f_fun(x_0);
111 P_fm(:, :, k) = F * P_0 * F' + Q;
112 else
113 % prediction
114 F = f_jac(x_fp(:, k-1));
115 x_fm(:, k) = f_fun(x_fp(:, k-1));
116 P_fm(:, :, k) = F * P_fp(:, :, k-1) * F' + Q;
117 end
118
119 % normalize quaternion
120 x_fm(1:4, k) = quaternion(x_fm(1:4, k)').normalize.compact';
121
122 h = h_fun(x_fm(:, k));
123 H = h_jac(x_fm(:, k));
124
125 % handle measurements
126 if ~zero_velocity(k)
127 % no zero velocity detected
128 H(7:9, :) = 0;
129 end
130
131 % kalman gain
132 K = P_fm(:, :, k) * H' * pinv(H * P_fm(:, :, k) * H' + R);
133
134 % update
135 x_fp(:, k) = x_fm(:, k) + K * (z(:, k) - h);
136 P_fp(:, :, k) = (eye(n_x) - K * H) * P_fm(:, :, k) * (eye(n_x) - K * H)' + K * R * K';
137
138 % normalize quaternion
139 x_fp(1:4, k) = quaternion(x_fp(1:4, k)').normalize.compact';
140
141 % check if a warning occured
142 [~, warnID] = lastwarn;
143 if strcmp(warnID, 'MATLAB:nearlySingularMatrix') || strcmp(warnID, 'MATLAB:singularMatrix')
144 bad_iterations(k) = true;
145 lastwarn("",""); % reset warning
146 end
147end
148warning("on");
149
150%% smoothing
151if 1
152 % iterate over observations
153 for k = N-1:-1:1
154 if k == N-1
155 % initialization
156 x_bp(:, N) = x_fp(:, N);
157 P_bp(:, :, N) = P_fp(:, :, N);
158 end
159
160 F = f_jac(x_fp(:, k));
161
162 I = P_fm(:, :, k+1)^-1;
163 K = P_fp(:, :, k) * F' * I;
164 x_bp(:,k) = x_fp(:,k) + K * (x_bp(:,k+1) - x_fm(:,k+1));
165 P_bp(:,:,k) = P_fp(:,:,k) - K * (P_fm(:,:,k+1) - P_bp(:,:,k+1)) * K';
166
167 end
168end
169
170%% try the VQF filter
171try
172 vqf = MyVqf();
173 vqf_offline = vqf.filterOffline(data.tt.acc, data.tt.gyr, data.tt.mag);
174 vqf_offline = quaternion(vqf_offline.quat6D);
175 vqf_offline = quaternion(0, sqrt(2)/2, sqrt(2)/2, 0).normalize .* vqf_offline; % ENU -> NED
176 vqf_filter = vqf.filter(data.tt.acc, data.tt.gyr, data.tt.mag);
177 vqf_filter = quaternion(vqf_filter.quat6D);
178 vqf_filter = quaternion(0, sqrt(2)/2, sqrt(2)/2, 0).normalize .* vqf_filter; % ENU -> NED
179catch ME
180 warning("VQF Filtering failed. Python package 'vqf' installed?")
181 vqf_offline = quaternion.ones(1,N);
182end
183
184%% calculate orientation differences
185% filter results
186[d_ori_filt, ori_filt] = oriDiff(quaternion(x_fp(1:4,:)'), traj.getTrajectory.ori);
187
188% smoothing results
189[d_ori_smoo, ori_smoo] = oriDiff(quaternion(x_bp(1:4,:)'), traj.getTrajectory.ori);
190
191% VQF results
192[d_ori_vqf_off, vqf_offline] = oriDiff(vqf_offline, traj.getTrajectory.ori);
193[d_ori_vqf_fil, vqf_filter] = oriDiff(vqf_filter, traj.getTrajectory.ori);
194
195fprintf("Mean absolute orientation errors:" + newline + newline + "Filtered: %.2f°" + ...
196 newline + "Smoothed: %.2f°" + newline + "VQF offline: %.2f°" + newline + "VQF filtered: %.2f°" + newline, ...
197 mean(abs(d_ori_filt)), mean(abs(d_ori_smoo)), mean(abs(d_ori_vqf_off)), mean(abs(d_ori_vqf_fil)));
198
199%% plot orientation differences
200figure("Units","normalized",Position=[0.2,0.3,0.6,0.4])
201plot(traj.getTrajectory.t, abs([d_ori_filt, d_ori_smoo, d_ori_vqf_fil, d_ori_vqf_off]),'.-','LineWidth',0.5);
202grid on;
203legend("filtered","smoothed", "VQF filtered", "VQF offline");
204ylabel("Orientation absolute error: ||dq|| [°]")
205xlabel("Time");
206title("Comparison of 6D orientation estimation filter and smoother versus 9D VQF")
207
208%% compare results
209animateQuat(ori_filt,ori_smoo,traj.getTrajectory.ori, vqf_filter, vqf_offline, ...
210 "position", [0.1,0.25,0.9,0.5], "speedup",1, "names", ["filtered","smoothed","ground truth", "vqf filtered", "vqf offline"])
211
212
213%% Helper functions
214% this function calculates the state transition function from a given state vector x
215function x = f_fun(x)
216 global dt n_x
217
218 % quaternion
219 x1 = [2/dt, -x(5), -x(6), -x(7); ...
220 x(5), 2/dt, x(7), -x(6); ...
221 x(6), -x(7), 2/dt, x(5); ...
222 x(7), x(6), -x(5), 2/dt] * dt/2 * x(1:4);
223
224 % angular speed
225 x2 = x(5:7);
226
227 % biases
228 x3 = x(8:n_x);
229
230 x = [x1; x2; x3];
231end
232
233
234% this function calculates the jacobian of the state transition function
235% from a given state vector x
236function F = f_jac(x)
237
238 global dt n_x
239
240 F1 = [2/dt, -x(5), -x(6), -x(7), -x(2), -x(3), -x(4); ...
241 x(5), 2/dt, x(7), -x(6), x(1), -x(4), x(3); ...
242 x(6), -x(7), 2/dt, x(5), x(4), x(1), -x(2); ...
243 x(7), x(6), -x(5), 2/dt, -x(3), x(2), x(1)] * dt/2;
244
245 F_2_6 = diag(ones(n_x-4,1));
246
247 F = [[F1, zeros(4,n_x-7)]; [zeros(n_x-4,4), F_2_6]];
248end
249
250% this function calculates the measurement function from a given state vector x
251function h=h_fun(x)
252
253 global USE_SENSITIVITY
254
255 g = 9.81;
256
257 if USE_SENSITIVITY
258
259 % accelerometer
260 h1 = [x(17) * -g * 2 * (x(2)*x(4) - x(1)*x(3)) + x(11); ...
261 x(18) * -g * 2 * (x(3)*x(4) + x(1)*x(2)) + x(12); ...
262 x(19) * -g * (x(1)^2 - x(2)^2 - x(3)^2 + x(4)^2) + x(13)];
263
264 % gyroscope
265 h2 = [x(14) * x(5) + x(8); ...
266 x(15) * x(6) + x(9); ...
267 x(16) * x(7) + x(10)];
268
269 else
270
271 % accelerometer
272 h1 = [-g * 2 * (x(2)*x(4) - x(1)*x(3)) + x(11); ...
273 -g * 2 * (x(3)*x(4) + x(1)*x(2)) + x(12); ...
274 -g * (x(1)^2 - x(2)^2 - x(3)^2 + x(4)^2) + x(13)];
275
276 % gyroscope
277 h2 = [x(5) + x(8); ...
278 x(6) + x(9); ...
279 x(7) + x(10)];
280
281 end
282
283 % pseudo measurement
284 h3 = [x(5); x(6); x(7)];
285
286 h = [h1; h2; h3];
287end
288
289% this function calculates the jacobian of the measurement function from a
290% given state vector x
291function H = h_jac(x)
292
293 global USE_SENSITIVITY n_x
294
295 g = 9.81;
296
297 if USE_SENSITIVITY
298 % H1
299 H_1_1 = [ x(3)*x(17), -x(4)*x(17), x(1)*x(17), -x(2)*x(17); ...
300 -x(2)*x(18), -x(1)*x(18), -x(4)*x(18), -x(3)*x(18); ...
301 -x(1)*x(19), x(2)*x(19), x(3)*x(19), -x(4)*x(19)] * 2 * g;
302
303 H_1_2 = g * [2 * (x(1) * x(3) - x(2) * x(4)), 0, 0 ; ...
304 0, -2 * (x(1) * x(2) + x(3) * x(4)), 0; ...
305 0, 0, -x(1)^2 + x(2)^2 + x(3)^2 + x(4)^2];
306
307 H_1 = [H_1_1, zeros(3,6), diag([1,1,1]), zeros(3,3), H_1_2];
308
309 % H2
310 H_2_1 = [x(14), 0, 0; ...
311 0, x(15), 0; ...
312 0, 0, x(16)];
313
314 H_2_2 = [x(5), 0, 0; ...
315 0, x(6), 0; ...
316 0, 0, x(7)];
317
318 H_2 = [zeros(3,4), H_2_1, diag([1,1,1]), zeros(3,3), H_2_2, zeros(3,3)];
319
320 else
321 % H1
322 H_1_1 = [ x(3), -x(4), x(1), -x(2); ...
323 -x(2), -x(1), -x(4), -x(3); ...
324 -x(1), x(2), x(3), -x(4)] * 2 * g;
325
326 H_1_2 = g * [2 * (x(1) * x(3) - x(2) * x(4)), 0, 0 ; ...
327 0, -2 * (x(1) * x(2) + x(3) * x(4)), 0; ...
328 0, 0, -x(1)^2 + x(2)^2 + x(3)^2 + x(4)^2];
329
330 H_1 = [H_1_1, zeros(3,6), diag([1,1,1])];
331
332 % H2
333 H_2_1 = [1, 0, 0; ...
334 0, 1, 0; ...
335 0, 0, 1];
336
337 H_2_2 = [x(5), 0, 0; ...
338 0, x(6), 0; ...
339 0, 0, x(7)];
340
341 H_2 = [zeros(3,4), H_2_1, diag([1,1,1]), zeros(3,3)];
342
343 end
344
345 % H3
346 H_3 = [zeros(3,4), diag([1,1,1]), zeros(3,n_x-7)];
347
348 H = [H_1; H_2; H_3];
349
350end
351
352% this function calculates the difference between estimated and true
353% orientations. the initial yaw is aligned first.
354function [d, ori_est] = oriDiff(ori_est, ori_true)
355
356 arguments
357 ori_est (:,1) quaternion
358 ori_true (:,1) quaternion
359 end
360
361 assert(length(ori_est) == length(ori_true), "Length of orientations must be equal");
362
363 % initial reference yaw
364 ref_yaw = ori_true(1).eulerd("ZYX", "frame");
365 ref_yaw = ref_yaw(1);
366
367 % Initial estimation yaw
368 est_yaw = ori_est(1).eulerd("ZYX", "frame");
369 est_yaw = est_yaw(1);
370
371 % align initial yaw
372 diff = ref_yaw - est_yaw;
373 q_corr = quaternion([diff,0,0], "eulerd", "ZYX", "frame");
374
375 % apply correction
376 ori_est = q_corr .* ori_est;
377
378 % calculate differences
379 d = ori_est.conj .* ori_true;
380
381 % get absolute differende
382 d = 2*acos(d.normalize.parts) * 180 / pi;
383 d = mod(d + 180, 360) - 180;
384
385end
function animateQuat(in quat, 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_jac(in x)
function f_jac(in x)
function h_fun(in x)
function oriDiff(in ori_est, in ori_true)