Master Thesis Code
by Simon Moser
Loading...
Searching...
No Matches
exp_SonEMS_2.m
Go to the documentation of this file.
1%
2% For LICENSE / TERMS OF USE, see the file LICENSE in the root directory of the repository.
3
4clear; close all; clc;
5
6%% Data Variables
7SonE = 80;
8speed = 2.8 / 3.6; % m/s
9trial = "WN44"; % PN38, 65; WN35, 38, 44, 50, 56, 65
10%> @TODO introduce an automatic speed detection
11%> @TODO make everything in SI units
12
13%% Import Data Data
14
15% path definitions
16datapath = "D:\ETH_Data\SonE_" + SonE + "\";
17vicon_path = fullfile(datapath, "Processed/Vicon/");
18zm_raw_path = fullfile(datapath, "RawData/ZM/*.BIN");
19summary_path = fullfile(datapath,"Result/GaitSummaryIMU_SonE_" + SonE + ".mat");
20
21% load files
22% vicon data position data
23load(fullfile(vicon_path,"GaitSummary_YK","SonE_" + SonE + ".mat"));
24vicon = GaitSummary.("SonE_" + SonE).(trial);
25
26% zurich move, data ankle L
27zm_raw_files = dir(zm_raw_path);
28for i = 1:numel(zm_raw_files)
29 this_header = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name), header_only=true);
30
31 if strcmp(this_header.name, "ankle_L")
32 zm_l = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name));
33 elseif strcmp(this_header.name, "ankle_R")
34 zm_r = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name));
35 end
36
37end
38
39
40% gait summary
41load(summary_path)
42sum_zm_l = GaitSummaryIMU.ModulesData.ankle_L;
43sum_zm_r = GaitSummaryIMU.ModulesData.ankle_R;
44
45% delete unused variables
46clearvars -except vicon sum_zm_l sum_zm_r zm_l zm_r speed trial SonE
47
48%% create VICON ground truth from markers
49% there are 4 markers on each foot:
50% - heel (Calcaneus - RHEE, LHEE)
51% - Toe 1 (Caput metatarsale I - RTO1, LTO1)
52% - Toe 3 (Caput metatarsale III - RTO3, LTO3)
53% - Toe 5 (Caput metatarsale V - RTO5, LTO5)
54%
55% the JumpSensor was placed on the metatarsal bones on the proximal side of the
56% toe markers. The exact Position was not recorded. Therefore, we will use the
57% mean position of the toe markers and transform this position with a fixed
58% offset towards the heel marker. An offset of 5cm is used.
59%
60% note: the vicon data is in mm
61
62% mean toe left
63vicon.KinematicData.Marker.LTO = (vicon.KinematicData.Marker.LTO1 + vicon.KinematicData.Marker.LTO3 + vicon.KinematicData.Marker.LTO5) / 3;
64% mean toe right
65vicon.KinematicData.Marker.RTO = (vicon.KinematicData.Marker.RTO1 + vicon.KinematicData.Marker.RTO3 + vicon.KinematicData.Marker.RTO5) / 3;
66
67% offset from toe to heel
68offset = 50; % 5cm
69
70% left foot
71left_vec = vicon.KinematicData.Marker.LHEE - vicon.KinematicData.Marker.LTO;
72left_vec = left_vec ./ vecnorm(left_vec,2,2);
73vicon.KinematicData.Marker.LF = vicon.KinematicData.Marker.LTO + offset * left_vec;
74
75% right foot
76right_vec = vicon.KinematicData.Marker.RHEE - vicon.KinematicData.Marker.RTO;
77right_vec = right_vec ./ vecnorm(right_vec,2,2);
78vicon.KinematicData.Marker.RF = vicon.KinematicData.Marker.RTO + offset * right_vec;
79
80% accumulate the distance based on the speed
81
82% find the axis in walking direction
83%> @TODO: introduce a detection of the walking axis and the axis direction. for
84%> now, we assume that the negative y-axis is the walking direction
85
86dist = cumsum(speed * 0.005 * ones(height(vicon.KinematicData.Marker.LF),1)) * 1000; %distance in millimeters
87vicon.KinematicData.Marker.LF(:,2) = vicon.KinematicData.Marker.LF(:,2) - dist;
88vicon.KinematicData.Marker.RF(:,2) = vicon.KinematicData.Marker.RF(:,2) - dist;
89
90% delete unused variables
91clearvars -except vicon sum_zm_l sum_zm_r zm_l zm_r trial speed SonE
92
93%% find IMU data start and end
94% in sum_zm_l.synced.trim(8).acc and sum_zm_r.synced.trim(8).acc the already
95% trimmed and processed data of the IMU is stored. Since we do not want to use
96% preprocessed data, we sync this preprocessed data to the raw data in zm_l and
97% zm_r. This can be done with alignsignals.
98
99% find the row where the trial is
100trial_row = find(string({sum_zm_l.synced.trim.Trials}) == trial);
101
102% left
103[~, ~, idx_start_l_x] = alignsignals(sum_zm_l.synced.trim(trial_row).acc(:,1), zm_l.tt.acc(:,1), Method="xcorr");
104[~, ~, idx_start_l_y] = alignsignals(sum_zm_l.synced.trim(trial_row).acc(:,2), zm_l.tt.acc(:,2), Method="xcorr");
105[~, ~, idx_start_l_z] = alignsignals(sum_zm_l.synced.trim(trial_row).acc(:,3), zm_l.tt.acc(:,3), Method="xcorr");
106
107% check if all left indices are the same
108assert(abs(idx_start_l_x-idx_start_l_y) <= 1 && abs(idx_start_l_x-idx_start_l_z) <= 1, "Indices are not the same")
109idx_start_l = round(mean([idx_start_l_x, idx_start_l_y, idx_start_l_z])) + 1;
110
111% right
112[~, ~, idx_start_r_x] = alignsignals(sum_zm_r.synced.trim(trial_row).acc(:,1), zm_r.tt.acc(:,1), Method="xcorr");
113[~, ~, idx_start_r_y] = alignsignals(sum_zm_r.synced.trim(trial_row).acc(:,2), zm_r.tt.acc(:,2), Method="xcorr");
114[~, ~, idx_start_r_z] = alignsignals(sum_zm_r.synced.trim(trial_row).acc(:,3), zm_r.tt.acc(:,3), Method="xcorr");
115
116% check if all right indices are the same
117assert(abs(idx_start_r_x-idx_start_r_y) <= 1 && abs(idx_start_r_x-idx_start_r_z) <= 1, "Indices are not the same")
118idx_start_r = round(mean([idx_start_r_x, idx_start_r_y, idx_start_r_z])) + 1;
119
120% clip zm data
121zm_l.tt = zm_l.tt(idx_start_l:idx_start_l+height(sum_zm_l.synced.trim(trial_row).acc)-1,:);
122zm_l.t_start = zm_l.tt.t(1);
123zm_r.tt = zm_r.tt(idx_start_r:idx_start_r+height(sum_zm_r.synced.trim(trial_row).acc)-1,:);
124zm_r.t_start = zm_r.tt.t(1);
125
126% check if the data is properly aligned
127assert(corr(sum_zm_l.synced.trim(trial_row).acc(:,1), zm_l.tt.acc(:,1)) > 0.99, "The x-axis of the left IMU data is not properly aligned")
128assert(corr(sum_zm_l.synced.trim(trial_row).acc(:,2), zm_l.tt.acc(:,2)) > 0.99, "The y-axis of the left IMU data is not properly aligned")
129assert(corr(sum_zm_l.synced.trim(trial_row).acc(:,3), zm_l.tt.acc(:,3)) > 0.99, "The z-axis of the left IMU data is not properly aligned")
130assert(corr(sum_zm_r.synced.trim(trial_row).acc(:,1), zm_r.tt.acc(:,1)) > 0.99, "The x-axis of the right IMU data is not properly aligned")
131assert(corr(sum_zm_r.synced.trim(trial_row).acc(:,2), zm_r.tt.acc(:,2)) > 0.99, "The y-axis of the right IMU data is not properly aligned")
132assert(corr(sum_zm_r.synced.trim(trial_row).acc(:,3), zm_r.tt.acc(:,3)) > 0.99, "The z-axis of the right IMU data is not properly aligned")
133
134% delete unused variables
135clearvars -except vicon zm_l zm_r speed trial SonE
136
137%% check the alignment of the VICON and IMU data
138% the data should be aligned in time. also the number of samples should be the
139% same. here, first the number of samples is checked and then the periods of
140% movement are compared.
141
142% check number of samples
143assert(abs(height(vicon.KinematicData.Marker.LHEE) - height(zm_l.tt.acc)) <= 1, "The number of samples of the left IMU data and the VICON data is not the same")
144assert(abs(height(vicon.KinematicData.Marker.RHEE) - height(zm_r.tt.acc)) <= 1, "The number of samples of the right IMU data and the VICON data is not the same")
145
146% check periods of movement
147% here the following is done:
148% VICON:
149% - the speed of movement is calculated from the position data
150% - the speed is filtered with a lowpass filter
151% - take the norm of the speed
152% - the periods of movement are detected with a threshold
153%
154% IMU:
155% - the magnitude of the angular velocity is calculated
156% - the magnitude is filtered with a lowpass filter
157% - the norm of the angular velocity is taken
158% - the periods of movement are detected with a threshold
159
160% VICON
161% speed
162v_vicon_l = diff(vicon.KinematicData.Marker.LF) / 0.005; % mm/s
163v_vicon_r = diff(vicon.KinematicData.Marker.RF) / 0.005; % mm/s
164
165% lowpass filter
166[b, a] = butter(2, 0.1);
167v_vicon_l = filtfilt(b, a, v_vicon_l);
168v_vicon_r = filtfilt(b, a, v_vicon_r);
169
170% norm
171v_vicon_l = vecnorm(v_vicon_l,2,2);
172v_vicon_r = vecnorm(v_vicon_r,2,2);
173
174% threshold
175th_vicon = 60; % mm/s
176vicon_periods_l = v_vicon_l > th_vicon;
177vicon_periods_r = v_vicon_r > th_vicon;
178
179% IMU
180% angular velocity
181w_l = zm_l.tt.gyr - mean(zm_l.tt.gyr);
182w_r = zm_r.tt.gyr - mean(zm_r.tt.gyr);
183
184% lowpass filter
185[b, a] = butter(2, 0.1);
186w_l = filtfilt(b, a, w_l);
187w_r = filtfilt(b, a, w_r);
188
189% norm
190w_l = vecnorm(w_l,2,2);
191w_r = vecnorm(w_r,2,2);
192
193% threshold
194th_imu = 65; % deg/s
195imu_periods_l = w_l > th_imu;
196imu_periods_r = w_r > th_imu;
197
198% check if the periods align
199l_min = min([height(vicon_periods_l), height(imu_periods_l)]);
200assert(corr(vicon_periods_l(1:l_min), imu_periods_l(1:l_min)) > 0.75, "The periods of movement of the left IMU data and the VICON data do not align")
201assert(corr(vicon_periods_r(1:l_min), imu_periods_r(1:l_min)) > 0.75, "The periods of movement of the right IMU data and the VICON data do not align")
202
203% transform vicon data
204vic_l = vicon.KinematicData.Marker.LF;
205vic_r = vicon.KinematicData.Marker.RF;
206
207% delete unused variables
208clearvars -except vicon vic_l vic_r zm_l zm_r speed trial SonE
209
210%% filter the data of the left foot
211
212close all
213
214
215% dt and zero velocity detection
216dt = 0.005;
217zv_l = jumpZeroVelocityDetection(zm_l, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
218zv_r = jumpZeroVelocityDetection(zm_r, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
219
220% start and end during stance
221idx_l(1) = find(zv_l, 1, "first");
222idx_l(2) = find(zv_l, 1, "last");
223idx_r(1) = find(zv_r, 1, "first");
224idx_r(2) = find(zv_r, 1, "last");
225
226% instance of filter
227ekf = EKF1;
228
229% initial values
230% initial gyroscope bias
231w_zv_l = zm_l.tt.gyr(zv_l,:);
232w_zv_r = zm_r.tt.gyr(zv_r,:);
233ekf.b_w_0 = [mean(w_zv_l(1:20,1)), mean(w_zv_l(1:20,2)), mean(w_zv_l(1:20,3))] * pi/180;
234
235% initial orientation
236% mean accelerations during stance
237a_x = mean(zm_l.tt.acc(38:104,1)); % DEBUG: mean(zm_l.tt.acc(zv_l,1));
238a_y = mean(zm_l.tt.acc(38:104,2)); % DEBUG: mean(zm_l.tt.acc(zv_l,2));
239a_z = mean(zm_l.tt.acc(38:104,3)); % DEBUG: mean(zm_l.tt.acc(zv_l,3));
240% calculate initial orientation
241roll = atan2(a_y, a_z);
242pitch = atan2(-a_x, sqrt(a_y^2 + a_z^2));
243yaw = 0; deg2rad(60);
244rot_vec = [roll, pitch, yaw];
245ekf.q_0 = quaternion(rot_vec, "rotvec").normalize.compact';
246
247% initial covariances
248ekf.P_0_q = 1e-1; % original 5e-4
249ekf.P_0_v = 1e-4; % original 1e-7
250ekf.P_0_p = 1e-5; % original 1e-7
251ekf.P_0_b_w = 1e-3; % original 5e-3
252
253% measurement noise
254ekf.var_acc_xy = ekf.var_acc_xy / 10; %original: 45e-3
255ekf.var_acc_z = ekf.var_acc_z / 10; %original: 70e-3
256ekf.var_vel_ZUPT = 1e-5; %original: 1e-6
257ekf.var_gyr = ekf.var_gyr * 2;
258ekf.var_pos_x = 1e-4; % original 1e-6
259ekf.var_pos_y = 1e-4; % original 1e-6
260ekf.var_pos_z = 1e-5; % original 1e-6
261ekf.var_yaw_pseudo = 1e-4; % original 1e-4
262
263% process noise
264ekf.sigma_w_p = ekf.sigma_w_p * 5; % as not many zv points are available, trust in accel measurements must be bigger.
265ekf.var_a = 0.045; % original 0.045
266ekf.var_b_w = 1e-8; % original 1e-9
267
268% measurements
269ekf.zv = zv_l(idx_l(1):idx_l(2));
270ekf.z_acc = zm_l.tt.acc(idx_l(1):idx_l(2),:)';
271ekf.z_vel = zeros(size(ekf.z_acc));
272ekf.z_gyr = zm_l.tt.gyr(idx_l(1):idx_l(2),:)' * pi/180; % convert to rad/s
273ekf.z_pos = zeros(size(ekf.z_acc));
274
275% pseudo position update
276ekf.z_pos(1,92:end) = cumsum(speed * dt * ones(size(ekf.z_pos(1,92:end))));
277ekf.z_pos(1,:) = nan; %this line turns off x-position pseudo updates
278% ekf.z_pos(2,:) = nan; %this line turns off y-position pseudo updates
279
280% pseudo yaw update
281ekf.z_yaw = deg2rad(4.5) * ones(1, size(ekf.z_acc,2));
282%ekf.z_yaw = nan(1, size(ekf.z_acc,2));
283
284% Run left filter
285smoothed_l = ekf.run(plot=true);
286
287% right filter
288% initial values
289% initial gyroscope bias
290ekf.b_w_0 = [mean(w_zv_r(1:20,1)), mean(w_zv_r(1:20,2)), mean(w_zv_r(1:20,3))] * pi/180;
291
292% initial orientation
293% mean accelerations during stance
294a_x = mean(zm_r.tt.acc(zv_r,1));
295a_y = mean(zm_r.tt.acc(zv_r,2));
296a_z = mean(zm_r.tt.acc(zv_r,3));
297% calculate initial orientation
298roll = atan2(a_y, a_z);
299pitch = atan2(-a_x, sqrt(a_y^2 + a_z^2));
300yaw = 0; deg2rad(-60);
301rot_vec = [roll, pitch, yaw];
302ekf.q_0 = quaternion(rot_vec, "rotvec").normalize.compact';
303
304% measurements
305ekf.zv = zv_r(idx_r(1):idx_r(2));
306ekf.z_acc = zm_r.tt.acc(idx_r(1):idx_r(2),:)';
307ekf.z_vel = zeros(size(ekf.z_acc));
308ekf.z_gyr = zm_r.tt.gyr(idx_r(1):idx_r(2),:)' * pi/180; % convert to rad/s
309ekf.z_pos = zeros(size(ekf.z_acc));
310
311% run right filter
312%smoothed_r = ekf.run(plot=true);
313
314% extract trajectories
315traj_l = smoothed_l(8:10,:)';
316% traj_r = smoothed_r(8:10,:)';
317
318% add start and end (match original length)
319traj_l = [nan(idx_l(1)-1,3); traj_l; nan(height(zm_l.tt)-idx_l(2),3)];
320% traj_r = [nan(idx_r(1)-1,3); traj_r; nan(height(zm_r.tt)-idx_r(2),3)];
321
322%% compare results
323
324% gait events
325TO_left = vicon.GaitEvents.TOleftlocs;
326HS_left = vicon.GaitEvents.HSleftlocs;
327
328% the amount of gait events might differ. the first TO must be before the first
329% HS and the last TO must be before the last HS. the rest is cut off.
330% while(TO_left(1) > HS_left(1))
331% HS_left(1) = [];
332% end
333% while(TO_left(end) > HS_left(end))
334% TO_left(end) = [];
335% end
336
337% stride length vicon left
338stride_length_vicon = nan(length(HS_left),1);
339stride_width_vicon = nan(length(HS_left),1);
340for i = 1:numel(stride_length_vicon)-1
341 stride_length_vicon(i) = norm(vic_l(HS_left(i),2) - vic_l(HS_left(i+1),2)) / 1000; % only y axis
342 %stride_width_vicon(i) = -(vic_l(HS_left(i),1) - vic_l(TO_left(i),1)) / 1000; % only x axis
343end
344
345% stride length IMU left
346stride_length_imu_l = nan(length(HS_left),1);
347stride_width_imu_l = nan(length(HS_left),1);
348for i = 1:numel(stride_length_imu_l)-1
349 stride_length_imu_l(i) = norm(traj_l(HS_left(i),1) - traj_l(HS_left(i+1),1)); % only x-axis
350 %stride_width_imu_l(i) = (traj_l(HS_left(i),2) - traj_l(TO_left(i),2)); % only x-axis
351end
352
353% plot of stride length
354figure("Units","normalized","OuterPosition",[0,0,0.5,0.5])
355subplt(1) = subplot(2,2,1:2);
356plot(stride_length_vicon, '.-', 'DisplayName', 'VICON', "LineWidth", 0.5, "MarkerSize", 20)
357hold on
358plot(stride_length_imu_l, '.-', 'DisplayName', 'Jump Sensor', "LineWidth", 0.5, "MarkerSize", 20)
359legend("Location","best")
360title(sprintf("Stride Length (SonEMS %d, %s)", SonE, trial))
361xlabel("Stride")
362ylabel("Stride Length [m]")
363grid on
364
365% error over time
366subplt(2) = subplot(223);
367error = (stride_length_vicon - stride_length_imu_l)*100; % in cm
368mean_squared_error = rmse(stride_length_vicon, stride_length_imu_l)*100;
369stem(error, "LineWidth",1)
370title("Error, mean squared error: " + mean_squared_error + " cm")
371xlabel("Stride")
372ylabel("Error [cm]")
373grid on
374
375% error distribution
376subplt(3) = subplot(224);
377histogram(error, 20)
378title("Error Distribution")
379xlabel("Error [cm]")
380ylabel("Frequency")
381grid on
382
383% % plot of stride width
384% figure("Units","normalized","OuterPosition",[0.5,0,0.5,0.5])
385% subplt(1) = subplot(2,2,1:2);
386% plot(stride_width_vicon*100, '.-', 'DisplayName', 'VICON', "LineWidth", 0.5, "MarkerSize", 20)
387% hold on
388% plot(stride_width_imu_l*100, '.-', 'DisplayName', 'Jump Sensor', "LineWidth", 0.5, "MarkerSize", 20)
389% legend("Location","best")
390% title(sprintf("Stride Width (SonEMS %d, %s)", SonE, trial))
391% xlabel("Stride")
392% ylabel("Stride Width [cm]")
393% grid on
394%
395% % error over time
396% subplt(2) = subplot(223);
397% error = (stride_width_vicon - stride_width_imu_l)*100; % in cm
398% mean_squared_error = rmse(stride_width_vicon, stride_width_imu_l)*100;
399% stem(error, "LineWidth",1)
400% title("Error, RMSE: " + mean_squared_error + " cm")
401% xlabel("Stride")
402% ylabel("Error [cm]")
403% grid on
404%
405% % error distribution
406% subplt(3) = subplot(224);
407% histogram(error, 20)
408% title("Error Distribution")
409% xlabel("Error [cm]")
410% ylabel("Frequency")
411% grid on