1% =========================================================================== %
2%> @example derivation5Ekf6D.m
4%> In
this example, a 6D orientation estimation filter is implemented and
5%> compared to the 6D VQF filter.
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
11%> @note a more detailed explanation can be found here: @ref derivation5Ekf6D
14% =========================================================================== %
17% set seed
for reproducibility
20% flag to use the sensitivity states
22USE_SENSITIVITY =
false;
24%% Sensor Data Generation
25% load predefined trajectory
26load(
'rotatingCube.mat');
28% set all positions to zero
32traj.generateTrajectory;
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;
48%% Zero velocity detection
49std_acc = max(abs(movstd(data.tt.acc, 100)),[],2);
50std_gyr = max(abs(movstd(data.tt.gyr, 100)),[],2);
52zero_velocity = std_acc < 0.15 & std_gyr < 0.15;
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
61% number of observations
66 n_x = 19; % 13 states + 6 sensitivity states
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
78 x_0 = [1; zeros(12,1); ones(6,1)];
80 x_0 = [1; zeros(12,1)];
84P_fm = nan(n_x, n_x, N);
85P_fp = nan(n_x, n_x, N);
86P_bp = nan(n_x, n_x, N);
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;
90 P_0 = diag([repmat(1e-3,1,4),repmat(1e-2,1,6),0.6,0.6,0.8]).^2;
94Q = diag([repmat(1e-5,1,4), repmat(1e-3,1,3), 1e-12 * ones(1,n_x-7)]) .^ 2;
99std_gyr = 0.1 * pi / 180;
101R = diag([std_acc_xy, std_acc_xy, std_acc_z, std_gyr, std_gyr, std_gyr, std_pseudo, std_pseudo, std_pseudo]) .^ 2;
103%% iterate over all observations (filtering)
104bad_iterations =
false(1,N);
110 x_fm(:, k) =
f_fun(x_0);
111 P_fm(:, :, k) = F * P_0 * F
' + Q;
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;
119 % normalize quaternion
120 x_fm(1:4, k) = quaternion(x_fm(1:4, k)
').normalize.compact';
122 h =
h_fun(x_fm(:, k));
123 H =
h_jac(x_fm(:, k));
127 % no zero velocity detected
132 K = P_fm(:, :, k) * H
' * pinv(H * P_fm(:, :, k) * H' + R);
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';
138 % normalize quaternion
139 x_fp(1:4, k) = quaternion(x_fp(1:4, k)
').normalize.compact';
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
152 % iterate over observations
156 x_bp(:, N) = x_fp(:, N);
157 P_bp(:, :, N) = P_fp(:, :, N);
160 F =
f_jac(x_fp(:, k));
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';
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
180 warning("VQF Filtering failed. Python package 'vqf' installed?")
181 vqf_offline = quaternion.ones(1,N);
184%% calculate orientation differences
186[d_ori_filt, ori_filt] =
oriDiff(quaternion(x_fp(1:4,:)'), traj.getTrajectory.ori);
189[d_ori_smoo, ori_smoo] =
oriDiff(quaternion(x_bp(1:4,:)'), traj.getTrajectory.ori);
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);
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)));
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);
203legend("filtered","smoothed", "VQF filtered", "VQF offline");
204ylabel("Orientation absolute error: ||dq|| [°]")
206title("Comparison of 6D orientation estimation filter and smoother versus 9D VQF")
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"])
214% this function calculates the state transition function from a given state vector x
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);
234% this function calculates the jacobian of the state transition function
235% from a given state vector x
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;
245 F_2_6 = diag(ones(n_x-4,1));
247 F = [[F1, zeros(4,n_x-7)]; [zeros(n_x-4,4), F_2_6]];
250% this function calculates the measurement function from a given state vector x
253 global USE_SENSITIVITY
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)];
265 h2 = [x(14) * x(5) + x(8); ...
266 x(15) * x(6) + x(9); ...
267 x(16) * x(7) + x(10)];
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)];
277 h2 = [x(5) + x(8); ...
284 h3 = [x(5); x(6); x(7)];
289% this function calculates the jacobian of the measurement function from a
290% given state vector x
293 global USE_SENSITIVITY n_x
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;
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];
307 H_1 = [H_1_1, zeros(3,6), diag([1,1,1]), zeros(3,3), H_1_2];
310 H_2_1 = [x(14), 0, 0; ...
314 H_2_2 = [x(5), 0, 0; ...
318 H_2 = [zeros(3,4), H_2_1, diag([1,1,1]), zeros(3,3), H_2_2, zeros(3,3)];
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;
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];
330 H_1 = [H_1_1, zeros(3,6), diag([1,1,1])];
333 H_2_1 = [1, 0, 0; ...
337 H_2_2 = [x(5), 0, 0; ...
341 H_2 = [zeros(3,4), H_2_1, diag([1,1,1]), zeros(3,3)];
346 H_3 = [zeros(3,4), diag([1,1,1]), zeros(3,n_x-7)];
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)
357 ori_est (:,1) quaternion
358 ori_true (:,1) quaternion
361 assert(length(ori_est) == length(ori_true), "Length of orientations must be equal");
363 % initial reference yaw
364 ref_yaw = ori_true(1).eulerd("ZYX", "frame");
365 ref_yaw = ref_yaw(1);
367 % Initial estimation yaw
368 est_yaw = ori_est(1).eulerd("ZYX", "frame");
369 est_yaw = est_yaw(1);
372 diff = ref_yaw - est_yaw;
373 q_corr = quaternion([diff,0,0], "eulerd", "ZYX", "frame");
376 ori_est = q_corr .* ori_est;
378 % calculate differences
379 d = ori_est.conj .* ori_true;
381 % get absolute differende
382 d = 2*acos(d.normalize.parts) * 180 / pi;
383 d = mod(d + 180, 360) - 180;
function animateQuat(in quat, in options)
JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
function generateFromTrajectory(in obj, in traj)
generates data from a Trajectory object.
MyVqf is an interface to the VQF python package.
handle class from MATLAB.
function oriDiff(in ori_est, in ori_true)