Master Thesis Code
by Simon Moser
Loading...
Searching...
No Matches
process_participant.m
Go to the documentation of this file.
1% =========================================================================== %
2%> @brief preprocess and filter the data of a single participant
3%>
4%> this function preprocesses the data of a single participant. is is written
5%> in specific for the SonEMS study participants. the following steps are
6%> executed:
7%> - load the VICON data
8%> - load the Zurich Move data
9%> - load the gait summary data
10%> - create ground truth positions from the VICON data, i.e. estimate the
11%> position of the JUMP sensor based on the VICON markers
12%> - determine the walking axes
13%> - determine the walking speed
14%> - accumulate the distance based on the walking speed
15%> - align the IMU data with the VICON data
16%> - find the yaw of the IMU data
17%> - filter the data
18%>
19%> @param basepath: (string) the basepath of the data, e.g. "D:/ETH_Data/"
20%> @param ptp: (string) the participant to process, e.g. "SonE_01"
21%> @param options the options for the processing:
22%> - GT_POS: (logical) create ground truth positions from the VICON data
23%> - FIND_WALKING_AXES: (logical) determine the walking axes
24%> - FIND_WALKING_SPEED: (logical) determine the walking speed
25%> - ACCUMULATE_DIST: (logical) accumulate the distance based on the walking speed
26%> - ALIGN_IMU_VICON: (logical) align the IMU data with the VICON data
27%> - FIND_YAW: (logical) find the yaw of the IMU data
28%> - FILTER: (logical) filter the data
29%> - ekf_tuning_file: (string) the file with the tuning parameters for the EKF,
30%> see @ref EKF1::importTuning()
31%>
32%> @retval none
33%>
34%> @note the tasks to do which can be set in the options are used to ensure
35%> that a task is redone, even if it has been done before, e.g. if filter
36%> parameters were changed. if a task is not done yet, it will be executed
37%> regardless of the options.
38%>
39%> @copyright see the file @ref LICENSE in the root directory of the repository
40% =========================================================================== %
41function process_participant(basepath, ptp, options)
42
43arguments
44 basepath string
45 ptp string
46 options.GT_POS logical = false
47 options.FIND_WALKING_AXES logical = false
48 options.FIND_WALKING_SPEED logical = false
49 options.ACCUMULATE_DIST logical = false
50 options.ALIGN_IMU_VICON logical = false
51 options.FIND_YAW logical = false
52 options.FILTER logical = false
53 options.ekf_tuning_file string {mustBeFile}
54end
55
56% convert options
57GT_POS = options.GT_POS;
58FIND_WALKING_AXES = options.FIND_WALKING_AXES;
59FIND_WALKING_SPEED = options.FIND_WALKING_SPEED;
60ACCUMULATE_DIST = options.ACCUMULATE_DIST;
61ALIGN_IMU_VICON = options.ALIGN_IMU_VICON;
62FIND_YAW = options.FIND_YAW;
63FILTER = options.FILTER;
64
65% preprocess options to ensure dependencies
66if GT_POS
67 FIND_WALKING_AXES = true;
68 FIND_WALKING_SPEED = true;
69 ACCUMULATE_DIST = true;
70end
71
72if FIND_WALKING_AXES
73 FIND_WALKING_SPEED = true;
74 ACCUMULATE_DIST = true;
75end
76
77if FIND_WALKING_SPEED
78 ACCUMULATE_DIST = true;
79 FILTER = true;
80end
81
82if ALIGN_IMU_VICON
83 FIND_YAW = true;
84 FILTER = true;
85end
86
87if FIND_YAW
88 FILTER = true;
89end
90
91%% path definitions
92ptp_folder = fullfile(basepath, ptp);
93diary(fullfile(ptp_folder, ptp + ".log"))
94ptp_num = regexp(ptp, "SonE_(\d+)|Pilot_(\d+)", "tokens");
95ptp_num = str2double(ptp_num{1});
96vicon_path = fullfile(ptp_folder, "Processed/Vicon/");
97zm_raw_path = fullfile(ptp_folder, "RawData/ZM/*.BIN");
98summary_path = fullfile(ptp_folder,"Result/GaitSummaryIMU_SonE_" + ptp_num + ".mat");
99temp_path = fullfile(ptp_folder, "temp/");
100
101log_entry("Start processing participant", ptp_num)
102
103if ~isfolder(temp_path)
104 % create the temp folder if it does not exist
105 mkdir(temp_path)
106 log_entry("Created temp folder", ptp_num, 5)
107end
108
109% log the tasks that need to be done
110log_entry("Tasks to do:", ptp_num, 5)
111log_entry("GT_POS: " + string(GT_POS), ptp_num, 5)
112log_entry("FIND_WALKING_AXES: " + string(FIND_WALKING_AXES), ptp_num, 5)
113log_entry("FIND_WALKING_SPEED: " + string(FIND_WALKING_SPEED), ptp_num, 5)
114log_entry("ACCUMULATE_DIST: " + string(ACCUMULATE_DIST), ptp_num, 5)
115log_entry("ALIGN_IMU_VICON: " + string(ALIGN_IMU_VICON), ptp_num, 5)
116log_entry("FIND_YAW: " + string(FIND_YAW), ptp_num, 5)
117log_entry("FILTER: " + string(FILTER), ptp_num, 5)
118
119% check if the participants JSON already exists
120if isfile(fullfile(ptp_folder, "proc_info.json"))
121 % load the JSON file
122 proc_info = readstruct(fullfile(ptp_folder, "proc_info.json"));
123 log_entry("JSON file loaded", ptp_num, 5)
124
125 % set status to not done
126 proc_info.filtering_done = false;
127 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
128else
129 % create the JSON file
130 proc_info = struct();
131 log_entry("JSON file not found, created new struct", ptp_num, 4)
132
133 % set status to not done
134 proc_info.filtering_done = false;
135 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
136end
137
138%% load files (only if not a Pilot participant)
139
140if contains(ptp, "SonE")
141
142 % vicon data position data
143 if ~isfile(temp_path + "vicon_1.mat")
144
145 load(fullfile(vicon_path,"GaitSummary_YK","SonE_" + ptp_num + ".mat"), "GaitSummary");
146 vicon_1 = GaitSummary.("SonE_" + ptp_num);
147 save(temp_path + "vicon_1.mat", "vicon_1");
148
149 log_entry("VICON data loaded", ptp_num, 5)
150
151 % remove unnecessary variables (only large ones)
152 clear GaitSummary
153 end
154
155 % zurich move, data ankles
156 if ~isfile(temp_path + "zm_l_raw.mat") || ~isfile(temp_path + "zm_r_raw.mat")
157
158 zm_raw_files = dir(zm_raw_path);
159 for i = 1:numel(zm_raw_files)
160 this_header = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name), header_only=true);
161
162 if strcmp(this_header.name, "ankle_L")
163 zm_l_raw = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name));
164 log_entry(sprintf("Left ankle data loaded from %s", zm_raw_files(i).name), ptp_num, 5);
165 elseif strcmp(this_header.name, "ankle_R")
166 zm_r_raw = jumpReadData(fullfile(zm_raw_files(i).folder, zm_raw_files(i).name));
167 log_entry(sprintf("Right ankle data loaded from %s", zm_raw_files(i).name), ptp_num, 5);
168 end
169
170 end
171
172 save(temp_path + "zm_l_raw.mat", "zm_l_raw");
173 save(temp_path + "zm_r_raw.mat", "zm_r_raw");
174
175 log_entry("Zurich Move data loaded", ptp_num, 5)
176
177 end
178
179 % gait summary
180 if ~isfile(temp_path + "sum_zm_l.mat") || ~isfile(temp_path + "sum_zm_r.mat")
181
182 load(summary_path, "GaitSummaryIMU");
183 sum_zm_l = GaitSummaryIMU.ModulesData.ankle_L;
184 sum_zm_r = GaitSummaryIMU.ModulesData.ankle_R;
185
186 save(temp_path + "sum_zm_l.mat", "sum_zm_l");
187 save(temp_path + "sum_zm_r.mat", "sum_zm_r");
188
189 log_entry("Gait summary data loaded", ptp_num, 5)
190
191 % remove unnecessary variables (only large ones)
192 clear GaitSummaryIMU
193 end
194
195else
196
197 % load the pilot data
198 if ~isfile(temp_path + "vicon_1.mat")
199
200
201 load(fullfile(vicon_path,sprintf("P%02d.mat",ptp_num)), "GaitSummary");
202 vicon_1 = GaitSummary.("P01");
203 save(temp_path + "vicon_1.mat", "vicon_1");
204
205 log_entry("VICON data loaded", ptp_num, 5)
206
207 % remove unnecessary variables (only large ones)
208 clear GaitSummary
209 end
210
211 % zurich move, data ankles
212 if ~isfile(temp_path + "zm_l_raw.mat") || ~isfile(temp_path + "zm_r_raw.mat")
213
214 zm_l_raw = load(fullfile(ptp_folder, "RawData", "calibrated_ankle_L.mat"));
215 zm_r_raw = load(fullfile(ptp_folder, "RawData", "calibrated_ankle_R.mat"));
216
217 save(temp_path + "zm_l_raw.mat", "zm_l_raw");
218 save(temp_path + "zm_r_raw.mat", "zm_r_raw");
219
220 log_entry("Zurich Move data loaded", ptp_num, 5)
221
222 end
223
224 % gait summary is not needed for pilot participants
225
226end
227
228%% create VICON ground truth from markers
229% there are 4 markers on each foot:
230% - heel (Calcaneus - RHEE, LHEE)
231% - Toe 1 (Caput metatarsale I - RTO1, LTO1)
232% - Toe 3 (Caput metatarsale III - RTO3, LTO3)
233% - Toe 5 (Caput metatarsale V - RTO5, LTO5)
234%
235% the JumpSensor was placed on the metatarsal bones on the proximal side of the
236% toe markers. The exact Position was not recorded. Therefore, we will use the
237% mean position of the toe markers and transform this position with a fixed
238% offset towards the heel marker. An offset of 5cm is used.
239%
240% note: the vicon data is in mm
241
242if ~isfile(fullfile(temp_path,"vicon_2.mat")) || GT_POS
243
244 % load the vicon data, if not already done
245 if ~exist("vicon_1", "var"); load(temp_path + "vicon_1.mat", "vicon_1"); end
246
247 % loop over all trials
248 trials = string(fieldnames(vicon_1))';
249 for trial = trials
250
251 % mean toe left
252 vicon_1.(trial).KinematicData.Marker.LTO = (vicon_1.(trial).KinematicData.Marker.LTO1 + vicon_1.(trial).KinematicData.Marker.LTO3 + vicon_1.(trial).KinematicData.Marker.LTO5) / 3;
253 % mean toe right
254 vicon_1.(trial).KinematicData.Marker.RTO = (vicon_1.(trial).KinematicData.Marker.RTO1 + vicon_1.(trial).KinematicData.Marker.RTO3 + vicon_1.(trial).KinematicData.Marker.RTO5) / 3;
255
256 % offset from toe towards heel
257 offset = 65; % 6.5 cm
258
259 % left foot
260 left_vec = vicon_1.(trial).KinematicData.Marker.LHEE - vicon_1.(trial).KinematicData.Marker.LTO;
261 left_vec = left_vec ./ vecnorm(left_vec,2,2);
262 vicon_1.(trial).KinematicData.Marker.LF = vicon_1.(trial).KinematicData.Marker.LTO + offset * left_vec;
263
264 % right foot
265 right_vec = vicon_1.(trial).KinematicData.Marker.RHEE - vicon_1.(trial).KinematicData.Marker.RTO;
266 right_vec = right_vec ./ vecnorm(right_vec,2,2);
267 vicon_1.(trial).KinematicData.Marker.RF = vicon_1.(trial).KinematicData.Marker.RTO + offset * right_vec;
268
269 % add the trial name to the struct
270 proc_info.trials.(trial).name = trial;
271 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
272
273 end
274
275 % rename vicon_1 to vicon_2
276 vicon_2 = vicon_1;
277
278 % save the data
279 save(temp_path + "vicon_2.mat", "vicon_2");
280
281 log_entry("Ground truth positions created", ptp_num)
282
283 % remove unnecessary variables (only large ones)
284 clear vicon_1 left_vec right_vec
285
286end
287
288%% determine the walking axis
289if ~isfield(proc_info, "walking_axes") || FIND_WALKING_AXES
290
291 if ~FIND_WALKING_AXES
292 log_entry("The walking axes have to be determined even though it was not marked (not done yet)", ptp_num, 5)
293 end
294
295 % load the vicon data, if not already done
296 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
297
298 % loop over all trials
299 trials = string(fieldnames(vicon_2))';
300
301 sag_axis = 0; % sagital axis
302 lon_axis = 0; % longitudinal axis
303 lat_axis = 0; % lateral axis
304
305 for trial = trials
306
307 % get the standard deviation of all three axes
308 std_axis_left = std(vicon_2.(trial).KinematicData.Marker.LF);
309 std_axis_right = std(vicon_2.(trial).KinematicData.Marker.RF);
310
311 % find the walking axis (saggiatal axis) -> highest standard deviation
312 [~, idx_left] = max(std_axis_left);
313 [~, idx_right] = max(std_axis_right);
314
315 % check if the axis is the same for both feet
316 if idx_left == idx_right
317 if sag_axis > 0 && sag_axis ~= idx_left
318 log_entry("The sagital axis of the left and right foot is not constant over all trials", ptp_num, 2, trial)
319
320 log_entry(sprintf("Detected Standard Deviations: Left: %.2f %.2f %.2f, Right: %.2f %.2f %.2f", ...
321 std_axis_left(1), std_axis_left(2), std_axis_left(3), std_axis_right(1), std_axis_right(2), std_axis_right(3)), ptp_num, 5, trial)
322
323 error("The sagital axis of the left and right foot is not constant over all trials")
324 else
325 sag_axis = idx_left;
326 end
327 else
328 log_entry("The sagital axis of the left and right foot is not the same", ptp_num, 2, trial)
329
330 log_entry(sprintf("Detected Standard Deviations: Left: %.2f %.2f %.2f, Right: %.2f %.2f %.2f", ...
331 std_axis_left(1), std_axis_left(2), std_axis_left(3), std_axis_right(1), std_axis_right(2), std_axis_right(3)), ptp_num, 5, trial)
332
333 error("The sagital axis of the left and right foot is not the same")
334 end
335
336 % find the longitudinal axis -> very skewed distribution because feet
337 % cannot be inside the ground
338 skew_left = abs(skewness(vicon_2.(trial).KinematicData.Marker.LF, 1, 1));
339 skew_right = abs(skewness(vicon_2.(trial).KinematicData.Marker.RF, 1, 1));
340 skew_comb = skew_left + skew_right;
341
342 % find the axis with the highest skewness
343 [~, idx_long] = max(skew_comb);
344
345 % check if the axis is the same over trials
346 if lon_axis > 0 && lon_axis ~= idx_long
347 log_entry("The longitudinal axis of the left and right foot is not constant over all trials", ptp_num, 2, trial)
348
349 log_entry(sprintf("Combined Skewness: %.2f %.2f %.2f", skew_comb(1), skew_comb(2), skew_comb(3)), ptp_num, 5, trial)
350
351 log_entry(sprintf("Detected Skewnesses: Left: %.2f %.2f %.2f, Right: %.2f %.2f %.2f", ...
352 skew_left(1), skew_left(2), skew_left(3), skew_right(1), skew_right(2), skew_right(3)), ptp_num, 5, trial)
353 else
354 lon_axis = idx_long;
355 end
356
357 % find the lateral axis -> the remaining axis
358 lat_axis = setdiff(1:3, [sag_axis, lon_axis]);
359
360 end
361
362 if sag_axis == 0 || lon_axis == 0 || lat_axis == 0
363 log_entry("The walking axes could not be determined", ptp_num, 2)
364
365 error("The walking axes could not be determined")
366 end
367
368 % save the axis
369 proc_info.walking_axes.sagittal = num2axis(sag_axis);
370 proc_info.walking_axes.longitudinal = num2axis(lon_axis);
371 proc_info.walking_axes.lateral = num2axis(lat_axis);
372 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
373
374 log_entry(sprintf("Walking axes determined: sagittal: %s, longitudinal: %s, lateral: %s", proc_info.walking_axes.sagittal, proc_info.walking_axes.longitudinal, proc_info.walking_axes.lateral), ptp_num, 5)
375
376end
377
378%% determine the walking speed
379if ~isfield(proc_info, "walking_speed") || FIND_WALKING_SPEED
380
381 if ~FIND_WALKING_SPEED
382 log_entry("The walking speed has to be determined even though it was not marked (not done yet)", ptp_num, 5)
383 end
384
385 % load the vicon data, if not already done
386 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
387
388 % get the walking axis
389 try
390 sag_axis = axis2num(proc_info.walking_axes.sagittal);
391 lon_axis = axis2num(proc_info.walking_axes.longitudinal);
392 catch ME
393 log_entry("The walking axes are not defined", ptp_num, 2)
394
395 % log the error
396 log_entry(ME.message, ptp_num, 2)
397 for i = 1:numel(ME.stack)
398 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
399 end
400
401 error("The walking axes are not defined")
402 end
403
404 % loop over all trials
405 trials = string(fieldnames(vicon_2))';
406 walking_direction_positions = nan(60 * 60 * 200 * 2,1); % preallocate 1h at 200Hz for both feet
407 walking_direction_idx = 1;
408 up_down_positions = nan(60 * 60 * 200 * 2,1); % preallocate 1h at 200Hz for both feet
409 up_down_idx = 1;
410
411 for trial = trials
412
413 % store all positions of both feet in the walking direction and the
414 % up-down direction
415 numel_left = height(vicon_2.(trial).KinematicData.Marker.LF);
416 numel_right = height(vicon_2.(trial).KinematicData.Marker.RF);
417 walking_direction_positions(walking_direction_idx:walking_direction_idx+numel_left-1) = vicon_2.(trial).KinematicData.Marker.LF(:,sag_axis);
418 walking_direction_idx = walking_direction_idx + numel_left;
419 walking_direction_positions(walking_direction_idx:walking_direction_idx+numel_right-1) = vicon_2.(trial).KinematicData.Marker.RF(:,sag_axis);
420 walking_direction_idx = walking_direction_idx + numel_right;
421
422 up_down_positions(up_down_idx:up_down_idx+numel_left-1) = vicon_2.(trial).KinematicData.Marker.LF(:,lon_axis);
423 up_down_idx = up_down_idx + numel_left;
424 up_down_positions(up_down_idx:up_down_idx+numel_right-1) = vicon_2.(trial).KinematicData.Marker.RF(:,lon_axis);
425 up_down_idx = up_down_idx + numel_right;
426
427 end
428
429 % remove nan values
430 walking_direction_positions = walking_direction_positions(~isnan(walking_direction_positions));
431 up_down_positions = up_down_positions(~isnan(up_down_positions));
432
433 % get upwards direction
434 % since the foot is always above the ground, we expect a positive or
435 % negative skewness. if the skewness is positive, the upwards direction
436 % is positive, otherwise negative
437 if skewness(up_down_positions) > 0.5
438 proc_info.walking_axes.upwards = num2direction(1);
439 elseif skewness(up_down_positions) < -0.5
440 proc_info.walking_axes.upwards = num2direction(-1);
441 else
442 log_entry("The upwards direction could not be determined", ptp_num, 2)
443
444 error("The upwards direction could not be determined")
445 end
446
447 log_entry(sprintf("Upwards direction determined: %s %s-axis", proc_info.walking_axes.upwards, proc_info.walking_axes.longitudinal), ptp_num, 5)
448
449 % calculate derivative -> speed
450 speed = diff(walking_direction_positions) / 1000 * 200; % mm/5ms -> m/s
451 speed = filloutliers(speed, "previous", "quartiles", "ThresholdFactor", 1.8); % 1.8 because it worked well between 1.2 and 2.4
452
453 % get walking direction
454 % because the foot has to accelerate and slow down, the maximum
455 % velocities are higher in the walking direction. in the negative
456 % walking direction, they are smaller, because there the constant
457 % speed of the treadmill is measured. this results in a left- or
458 % right-skewed distribution. if the distribution is left-skewed
459 % (negative skew), the walking direction is in the negative
460 % direction, otherwise in the positive
461 if skewness(speed) < -0.5
462 proc_info.walking_axes.forwards = num2direction(-1);
463 elseif skewness(speed) > 0.5
464 proc_info.walking_axes.forwards = num2direction(1);
465 else
466 log_entry("The walking direction could not be determined", ptp_num, 2)
467
468 error("The walking direction could not be determined")
469 end
470
471 log_entry(sprintf("Walking direction determined: %s %s-axis", proc_info.walking_axes.forwards, proc_info.walking_axes.sagittal), ptp_num, 5)
472
473 % find the sinister direction of the lateral axis
474 % we know that a right hand system is used. the lateral axis is the
475 % remaining axis.
476 %
477 % we form a right hand system following forward, sinister, upwards
478 %
479 % utilizing the cross product of the upwards and forwards direction
480 % we can determine the sinister direction
481 sinister_vec = cross(...
482 axisdir2vec(proc_info.walking_axes.sagittal, proc_info.walking_axes.forwards), ...
483 axisdir2vec(proc_info.walking_axes.longitudinal, proc_info.walking_axes.upwards)...
484 ) * -1;
485
486 [axis, direction] = vec2axisdir(sinister_vec);
487 try
488 assert(axis == lat_axis, "The sinister direction is not on the lateral axis")
489 catch ME
490 log_entry("The sinister direction is not the lateral axis", ptp_num, 2)
491
492 % log the error
493 log_entry(ME.message, ptp_num, 2)
494 for i = 1:numel(ME.stack)
495 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
496 end
497
498 error("The sinister direction is not the lateral axis")
499 end
500
501 proc_info.walking_axes.sinister = num2direction(direction);
502
503 log_entry(sprintf("sinister direction determined: %s %s-axis", proc_info.walking_axes.sinister, proc_info.walking_axes.lateral), ptp_num, 5)
504
505 % find actual speed (maximum of the distribution)
506 [f,xi] = ksdensity(speed, linspace(min(speed), max(speed), 1000));
507 proc_info.walking_speed = abs(xi(f == max(f))); % m/s
508
509 % save the struct
510 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
511
512 log_msg = sprintf("Walking speed determined: %.2f m/s, %.1f km/h", proc_info.walking_speed, proc_info.walking_speed*3.6);
513 log_entry(log_msg, ptp_num, 5)
514
515 % remove unnecessary variables (only large ones)
516 clear walking_direction_positions up_down_positions speed f xi
517
518end
519
520% accumulate the distance based on the speed
521if ~isfile(temp_path + "vicon_3.mat") || ACCUMULATE_DIST
522
523 if ~ACCUMULATE_DIST
524 log_entry("The distance has to be accumulated even though it was not marked (not done yet)", ptp_num, 5)
525 end
526
527 % load the vicon data, if not already done
528 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
529
530 % get the walking axis (sagital axis)
531 try
532 axis = axis2num(proc_info.walking_axes.sagittal);
533 catch ME
534 log_entry("The walking axis is not defined", ptp_num, 2)
535
536 % log the error
537 log_entry(ME.message, ptp_num, 2)
538 for i = 1:numel(ME.stack)
539 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
540 end
541
542 error("The walking axis is not defined")
543 end
544
545 % get the walking direction
546 try
547 walking_direction = direction2num(proc_info.walking_axes.forwards);
548 catch ME
549 log_entry("The walking direction is not defined", ptp_num, 2)
550
551 % log the error
552 log_entry(ME.message, ptp_num, 2)
553 for i = 1:numel(ME.stack)
554 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
555 end
556
557 error("The walking direction is not defined")
558 end
559
560 % get the walking speed
561 try
562 speed = proc_info.walking_speed;
563 catch ME
564 log_entry("The walking speed is not defined", ptp_num, 2)
565
566 % log the error
567 log_entry(ME.message, ptp_num, 2)
568 for i = 1:numel(ME.stack)
569 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
570 end
571
572 error("The walking speed is not defined")
573 end
574
575 % create a new struct
576 vicon_3 = struct();
577
578 % loop over all trials
579 trials = string(fieldnames(vicon_2))';
580 for trial = trials
581
582 % detect if the treadmill has been started or stopped at the start or
583 % the end. if so, cut the data at the start and the end
584 % the thresholds have been experimentally determined analyzing the
585 % fastest and slowest walking speeds
586
587 % create a detector based on the standard deviation of the feet in the
588 % sagital axis
589 movstd_result_l = movstd(vicon_2.(trial).KinematicData.Marker.LF(:,axis), 500);
590 movstd_result_r = movstd(vicon_2.(trial).KinematicData.Marker.RF(:,axis), 500);
591 movstd_result = max([movstd_result_l, movstd_result_r], [], 2);
592
593 % check if there's a phase where the treadmill is not running (std < 100)
594 if any(movstd_result < 100)
595
596 log_entry("Treadmill start or stop detected", ptp_num, 3, trial)
597
598 % not walking detector (remove double detections)
599 se = ones(200,1); % 1s kernel
600 not_walking = movstd_result < 100;
601 not_walking = imdilate(not_walking, se);
602 not_walking = imerode(not_walking, se);
603 not_walking = diff(not_walking); % -1: start of movement, 1: end of movement
604
605 % check if there are multiple starts and stops
606 if sum(not_walking == -1) > 1 || sum(not_walking == 1) > 1
607 log_entry("Multiple starts or stops detected", ptp_num, 3, trial)
608
609 % keep only last start in the first half and the first stop in
610 % after the first start
611
612 % find the last start in the first half
613 length_half = floor(length(not_walking) / 2);
614 start_idx = find(not_walking(1:length_half) == -1, 1, "last");
615 if isempty(start_idx)
616 start_idx = 1;
617 end
618
619 % find the first stop after the first start
620 stop_idx = find(not_walking(start_idx:end) == 1, 1, "first");
621 if isempty(stop_idx)
622 stop_idx = length(not_walking);
623 else
624 stop_idx = stop_idx + start_idx - 1;
625 end
626
627 % set all other starts and stops to 0
628 not_walking(1:start_idx-1) = 0;
629 not_walking(stop_idx+1:end) = 0;
630 end
631
632 % define the start
633 if any(not_walking == -1)
634 idx_start = find(not_walking == -1, 1, "first") + 1000; % 5s after the start
635
636 log_entry("Beginning of the trial must be cut", ptp_num, 5, trial)
637 else
638 idx_start = 1;
639 end
640
641 % define the end
642 if any(not_walking == 1)
643 idx_end = find(not_walking == 1, 1, "last") - 1000; % 5s before the end
644
645 log_entry("End of the trial must be cut", ptp_num, 5, trial)
646 else
647 idx_end = height(vicon_2.(trial).KinematicData.Marker.LF);
648 end
649
650 % plot the start and stop detection
651 start_stop_figure = figure(Visible="off");
652 plot(movstd_result)
653 xline(idx_start, "g", "Start", "LineWidth", 2)
654 xline(idx_end, "r", "Stop", "LineWidth", 2)
655 title("Start and stop detection")
656 legend("Standard deviation", "Start/Stop", "Location", "best")
657 grid on;
658 filename = fullfile(temp_path, string(datetime("now", "Format", "yy-MM-dd_HH-mm-ss")) + "_" + trial + "_start_stop_detection.png");
659 saveas(gcf, filename);
660 close(start_stop_figure)
661
662 log_entry("Start and stop detection plot saved", ptp_num, 5, trial)
663
664 % save the indices
665 vicon_2.(trial).idx_start = idx_start;
666 vicon_2.(trial).idx_end = idx_end;
667
668 % clip the vicon data
669 vicon_2.(trial).KinematicData.Marker.LF = vicon_2.(trial).KinematicData.Marker.LF(idx_start:idx_end,:);
670 vicon_2.(trial).KinematicData.Marker.RF = vicon_2.(trial).KinematicData.Marker.RF(idx_start:idx_end,:);
671 vicon_2.(trial).KinematicData.Marker.LHEE = vicon_2.(trial).KinematicData.Marker.LHEE(idx_start:idx_end,:);
672 vicon_2.(trial).KinematicData.Marker.RHEE = vicon_2.(trial).KinematicData.Marker.RHEE(idx_start:idx_end,:);
673
674 % clip the gait events
675 vicon_2.(trial).GaitEvents.HSleftlocs(vicon_2.(trial).GaitEvents.HSleftlocs < idx_start) = [];
676 vicon_2.(trial).GaitEvents.HSleftlocs(vicon_2.(trial).GaitEvents.HSleftlocs > idx_end) = [];
677 vicon_2.(trial).GaitEvents.HSleftlocs = vicon_2.(trial).GaitEvents.HSleftlocs - idx_start + 1;
678 vicon_2.(trial).GaitEvents.TOleftlocs(vicon_2.(trial).GaitEvents.TOleftlocs < idx_start) = [];
679 vicon_2.(trial).GaitEvents.TOleftlocs(vicon_2.(trial).GaitEvents.TOleftlocs > idx_end) = [];
680 vicon_2.(trial).GaitEvents.TOleftlocs = vicon_2.(trial).GaitEvents.TOleftlocs - idx_start + 1;
681 vicon_2.(trial).GaitEvents.HSrightlocs(vicon_2.(trial).GaitEvents.HSrightlocs < idx_start) = [];
682 vicon_2.(trial).GaitEvents.HSrightlocs(vicon_2.(trial).GaitEvents.HSrightlocs > idx_end) = [];
683 vicon_2.(trial).GaitEvents.HSrightlocs = vicon_2.(trial).GaitEvents.HSrightlocs - idx_start + 1;
684 vicon_2.(trial).GaitEvents.TOrightlocs(vicon_2.(trial).GaitEvents.TOrightlocs < idx_start) = [];
685 vicon_2.(trial).GaitEvents.TOrightlocs(vicon_2.(trial).GaitEvents.TOrightlocs > idx_end) = [];
686 vicon_2.(trial).GaitEvents.TOrightlocs = vicon_2.(trial).GaitEvents.TOrightlocs - idx_start + 1;
687
688 % clip the imu data in the gait summary
689 % note: this is done further below, because we don't want to load the
690 % gait summary data here.
691
692 % add flag to the struct
693 proc_info.trials.(trial).start_stop_clipped = true;
694 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
695
696 end
697
698 % copy necessary data
699 vicon_3.(trial).KinematicData.Marker.LF = vicon_2.(trial).KinematicData.Marker.LF / 1000; % mm -> m
700 vicon_3.(trial).KinematicData.Marker.RF = vicon_2.(trial).KinematicData.Marker.RF / 1000; % mm -> m
701 vicon_3.(trial).KinematicData.Marker.LHEE = vicon_2.(trial).KinematicData.Marker.LHEE / 1000; % mm -> m
702 vicon_3.(trial).KinematicData.Marker.RHEE = vicon_2.(trial).KinematicData.Marker.RHEE / 1000; % mm -> m
703 vicon_3.(trial).GaitEvents = vicon_2.(trial).GaitEvents;
704
705 % calculate the distance based on the speed
706 dist = cumsum(speed * 0.005 * ones(height(vicon_2.(trial).KinematicData.Marker.LF),1)); %distance in meters
707
708 % add to the position data
709 vicon_3.(trial).KinematicData.Marker.LF(:,axis) = vicon_3.(trial).KinematicData.Marker.LF(:,axis) + (dist * walking_direction);
710 vicon_3.(trial).KinematicData.Marker.RF(:,axis) = vicon_3.(trial).KinematicData.Marker.RF(:,axis) + (dist * walking_direction);
711
712 end
713
714 % save the struct
715 save(temp_path + "vicon_3.mat", "vicon_3")
716
717 log_entry("Distance accumulated", ptp_num)
718
719 % remove unnecessary variables
720 clear vicon_2 dist axis walking_direction
721
722end
723
724%% find IMU data start and end
725% in sum_zm_l.synced.trim(i).acc and sum_zm_r.synced.trim(i).acc the already
726% trimmed and processed data of the IMU is stored. Since we do not want to use
727% preprocessed data, we sync this preprocessed data to the raw data in zm_l_raw and
728% zm_r_raw. This can be done with alignsignals.
729
730if ~isfile(fullfile(temp_path, "zm_aligned.mat")) || ~isfile(fullfile(temp_path, "vicon_4.mat")) || ALIGN_IMU_VICON
731
732 % load the vicon data, if not already done
733 if ~exist("vicon_3", "var"); load(temp_path + "vicon_3.mat","vicon_3"); end
734
735 if contains(ptp_folder, "SonE")
736
737
738 if ~ALIGN_IMU_VICON
739 log_entry("The IMU data has to be aligned even though it was not marked (not done yet)", ptp_num, 5)
740 end
741
742 % load the raw data, if not already done
743 if ~exist("zm_l_raw", "var"); load(temp_path + "zm_l_raw.mat", "zm_l_raw"); end
744 if ~exist("zm_r_raw", "var"); load(temp_path + "zm_r_raw.mat", "zm_r_raw"); end
745
746 % load the gait summary data, if not already done
747 if ~exist("sum_zm_l", "var"); load(temp_path + "sum_zm_l.mat", "sum_zm_l"); end
748 if ~exist("sum_zm_r", "var"); load(temp_path + "sum_zm_r.mat", "sum_zm_r"); end
749
750 % create a new struct
751 zm_aligned = struct();
752
753 % loop over all trials
754 trials = string(fieldnames(vicon_3))';
755 for trial = trials
756
757 % find the row where the trial is
758 trial_row = find(string({sum_zm_l.synced.trim.Trials}) == trial);
759
760 % if the data had to be clipped, the gait summary data has to be clipped as well
761 % note: only the acc data is clipped, because the other data is not used,
762 % however, this (c|sh)ould be done as well
763 if isfield(vicon_3.(trial), "idx_start") || isfield(vicon_3.(trial), "idx_end")
764 sum_zm_l.synced.trim(trial_row).acc = sum_zm_l.synced.trim(trial_row).acc(vicon_3.(trial).idx_start:vicon_3.(trial).idx_end,:);
765 sum_zm_r.synced.trim(trial_row).acc = sum_zm_r.synced.trim(trial_row).acc(vicon_3.(trial).idx_start:vicon_3.(trial).idx_end,:);
766 end
767
768 % left
769 idx_start_l = finddelay(vecnorm(sum_zm_l.synced.trim(trial_row).acc, 2, 2), vecnorm(zm_l_raw.tt.acc, 2, 2)) + 1;
770
771 % right
772 idx_start_r = finddelay(vecnorm(sum_zm_r.synced.trim(trial_row).acc, 2, 2), vecnorm(zm_r_raw.tt.acc, 2, 2)) + 1;
773
774 % clip zm data TODO: the clipping should only be done after the check if the max correlation
775 zm_aligned.(trial).l = zm_l_raw;
776 zm_aligned.(trial).l.tt = zm_aligned.(trial).l.tt(idx_start_l:idx_start_l+height(sum_zm_l.synced.trim(trial_row).acc)-1,:);
777 zm_aligned.(trial).l.t_start = zm_aligned.(trial).l.tt.t(1);
778
779 zm_aligned.(trial).r = zm_r_raw;
780 zm_aligned.(trial).r.tt = zm_aligned.(trial).r.tt(idx_start_r:idx_start_r+height(sum_zm_r.synced.trim(trial_row).acc)-1,:);
781 zm_aligned.(trial).r.t_start = zm_aligned.(trial).r.tt.t(1);
782
783 % check if the data is properly aligned
784
785 corr_l_x = corr(sum_zm_l.synced.trim(trial_row).acc(:,1), zm_aligned.(trial).l.tt.acc(:,1));
786 corr_l_y = corr(sum_zm_l.synced.trim(trial_row).acc(:,2), zm_aligned.(trial).l.tt.acc(:,2));
787 corr_l_z = corr(sum_zm_l.synced.trim(trial_row).acc(:,3), zm_aligned.(trial).l.tt.acc(:,3));
788 corr_r_x = corr(sum_zm_r.synced.trim(trial_row).acc(:,1), zm_aligned.(trial).r.tt.acc(:,1));
789 corr_r_y = corr(sum_zm_r.synced.trim(trial_row).acc(:,2), zm_aligned.(trial).r.tt.acc(:,2));
790 corr_r_z = corr(sum_zm_r.synced.trim(trial_row).acc(:,3), zm_aligned.(trial).r.tt.acc(:,3));
791 try
792 assert(corr_l_x > 0.95, "The x-axis of the left IMU data is not properly aligned")
793 assert(corr_l_y > 0.95, "The y-axis of the left IMU data is not properly aligned")
794 assert(corr_l_z > 0.95, "The z-axis of the left IMU data is not properly aligned")
795 assert(corr_r_x > 0.95, "The x-axis of the right IMU data is not properly aligned")
796 assert(corr_r_y > 0.95, "The y-axis of the right IMU data is not properly aligned")
797 assert(corr_r_z > 0.95, "The z-axis of the right IMU data is not properly aligned")
798 catch ME
799 log_entry(ME.message, ptp_num, 2, trial)
800
801 % check if the left and right sensor were swapped
802 corr_l_x_sw = corr(sum_zm_l.synced.trim(trial_row).acc(:,1), zm_aligned.(trial).r.tt.acc(:,1));
803 corr_l_y_sw = corr(sum_zm_l.synced.trim(trial_row).acc(:,2), zm_aligned.(trial).r.tt.acc(:,2));
804 corr_l_z_sw = corr(sum_zm_l.synced.trim(trial_row).acc(:,3), zm_aligned.(trial).r.tt.acc(:,3));
805 corr_r_x_sw = corr(sum_zm_r.synced.trim(trial_row).acc(:,1), zm_aligned.(trial).l.tt.acc(:,1));
806 corr_r_y_sw = corr(sum_zm_r.synced.trim(trial_row).acc(:,2), zm_aligned.(trial).l.tt.acc(:,2));
807 corr_r_z_sw = corr(sum_zm_r.synced.trim(trial_row).acc(:,3), zm_aligned.(trial).l.tt.acc(:,3));
808
809 if corr_l_x_sw > 0.95 && corr_l_y_sw > 0.95 && corr_l_z_sw > 0.95 && corr_r_x_sw > 0.95 && corr_r_y_sw > 0.95 && corr_r_z_sw > 0.95
810 log_entry("The left and right IMU data were swapped", ptp_num, 3, trial)
811
812 % add flag to the struct
813 proc_info.trials.(trial).imu_swapped = true;
814
815 else
816
817 % remove the trial from the structs
818 zm_aligned = rmfield(zm_aligned, trial);
819 vicon_3 = rmfield(vicon_3, trial);
820
821 log_entry("Trial removed", ptp_num, 3, trial)
822
823 corr_min = min([corr_l_x, corr_l_y, corr_l_z, corr_r_x, corr_r_y, corr_r_z]);
824 corr_min_sw = min([corr_l_x_sw, corr_l_y_sw, corr_l_z_sw, corr_r_x_sw, corr_r_y_sw, corr_r_z_sw]);
825
826 log_entry(sprintf("Minimum correlation: %.2f", corr_min), ptp_num, 5, trial)
827 log_entry(sprintf("Minimum correlation swapped: %.2f", corr_min_sw), ptp_num, 5, trial)
828
829 % add flag to the struct
830 proc_info.trials.(trial).imu_raw_data_missmatch = true;
831 proc_info.trials.(trial).removal_reason = "IMU raw data missmatch";
832
833 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
834
835 continue
836 end
837 end
838
839 %% check the alignment of the VICON and IMU data
840 % the data should be aligned in time. also the number of samples should be the
841 % same. here, first the number of samples is checked and then the periods of
842 % movement are compared.
843
844 % check number of samples
845 d_samples_l = height(vicon_3.(trial).KinematicData.Marker.LF) - height(zm_aligned.(trial).l.tt.acc);
846 d_samples_r = height(vicon_3.(trial).KinematicData.Marker.RF) - height(zm_aligned.(trial).r.tt.acc);
847 try
848 assert(d_samples_l <= 1, "The number of samples of the left IMU data and the VICON data is not the same")
849 assert(d_samples_r <= 1, "The number of samples of the right IMU data and the VICON data is not the same")
850 catch ME
851 log_entry(ME.message, ptp_num, 3, trial)
852
853 % add flag to the struct
854 proc_info.trials.(trial).imu_vicon_num_of_samples_missmatch = true;
855 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
856
857 if d_samples_l > 5 || d_samples_r > 5
858 % remove the trial from the structs
859 zm_aligned = rmfield(zm_aligned, trial);
860 vicon_3 = rmfield(vicon_3, trial);
861
862 log_entry("The samples difference is too high. The trial was removed.", ptp_num, 2, trial)
863
864 log_entry(sprintf("Difference in samples: left %d, right %d", d_samples_l, d_samples_r), ptp_num, 5, trial)
865
866 % add removal reason
867 proc_info.trials.(trial).removal_reason = "IMU VICON num of samples missmatch";
868 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
869
870 continue
871
872 else
873 log_entry("The samples difference is small. The trial was not removed.", ptp_num, 4, trial)
874
875 log_entry(sprintf("Difference in samples: left %d, right %d", d_samples_l, d_samples_r), ptp_num, 5, trial)
876
877 end
878
879 end
880
881 % check periods of movement
882 % here the following is done:
883 % VICON:
884 % - the speed of movement is calculated from the position data
885 % - the speed is filtered with a lowpass filter
886 % - take the norm of the speed
887 % - the periods of movement are detected with a threshold
888 %
889 % IMU:
890 % - the magnitude of the angular velocity is calculated
891 % - the magnitude is filtered with a lowpass filter
892 % - the norm of the angular velocity is taken
893 % - the periods of movement are detected with a threshold
894
895 % VICON
896 % speed
897 v_vicon_l = diff(vicon_3.(trial).KinematicData.Marker.LF) / 0.005; % m/s
898 v_vicon_r = diff(vicon_3.(trial).KinematicData.Marker.RF) / 0.005; % m/s
899
900 % lowpass filter
901 [b, a] = butter(2, 0.1);
902 v_vicon_l = filtfilt(b, a, v_vicon_l);
903 v_vicon_r = filtfilt(b, a, v_vicon_r);
904
905 % norm
906 v_vicon_l = vecnorm(v_vicon_l,2,2);
907 v_vicon_r = vecnorm(v_vicon_r,2,2);
908
909 % threshold
910 th_vicon = 0.06; % m/s
911 vicon_periods_l = v_vicon_l > th_vicon;
912 vicon_periods_r = v_vicon_r > th_vicon;
913
914 % IMU
915 % angular velocity
916 w_l = zm_aligned.(trial).l.tt.gyr - mean(zm_aligned.(trial).l.tt.gyr);
917 w_r = zm_aligned.(trial).r.tt.gyr - mean(zm_aligned.(trial).r.tt.gyr);
918
919 % lowpass filter
920 [b, a] = butter(2, 0.1);
921 w_l = filtfilt(b, a, w_l);
922 w_r = filtfilt(b, a, w_r);
923
924 % norm
925 w_l = vecnorm(w_l,2,2);
926 w_r = vecnorm(w_r,2,2);
927
928 % threshold
929 th_imu = 65; % deg/s
930 imu_periods_l = w_l > th_imu;
931 imu_periods_r = w_r > th_imu;
932
933 % check if the periods align
934 l_min = min([height(vicon_periods_l), height(imu_periods_l)]);
935 r_min = min([height(vicon_periods_r), height(imu_periods_r)]); % should be the same, but just in case
936
937 corr_l = corr(vicon_periods_l(1:l_min), imu_periods_l(1:l_min));
938 corr_r = corr(vicon_periods_r(1:r_min), imu_periods_r(1:r_min));
939 try
940 assert(corr_l > 0.75, "The periods of movement of the left IMU data and the VICON data do not align")
941 assert(corr_r > 0.75, "The periods of movement of the right IMU data and the VICON data do not align")
942 catch ME
943 log_entry(ME.message, ptp_num, 3, trial)
944
945 % check if left and right are swapped
946 corr_l_sw = corr(vicon_periods_l(1:l_min), imu_periods_r(1:l_min));
947 corr_r_sw = corr(vicon_periods_r(1:r_min), imu_periods_l(1:r_min));
948
949 if corr_l_sw > 0.5 && corr_r_sw > 0.5
950 log_entry("The left and right IMU data were swapped", ptp_num, 3, trial)
951
952 this_zm_l = zm_aligned.(trial).l;
953 this_zm_r = zm_aligned.(trial).r;
954
955 zm_aligned.(trial).l = this_zm_r;
956 zm_aligned.(trial).r = this_zm_l;
957
958 corr_l = corr_l_sw;
959 corr_r = corr_r_sw;
960
961 end
962
963 % add flag to the struct
964 proc_info.trials.(trial).imu_vicon_periods_missmatch = true;
965 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
966
967 if corr_l < 0.5 || corr_r < 0.5
968
969 % remove the trial from the structs
970 zm_aligned = rmfield(zm_aligned, trial);
971 vicon_3 = rmfield(vicon_3, trial);
972
973 log_entry("The periods of movement do not align. The trial was removed.", ptp_num, 2, trial)
974
975 log_entry(sprintf("Correlation of periods: left %.2f, right %.2f", corr_l, corr_r), ptp_num, 5, trial)
976
977 % add removal reason
978 proc_info.trials.(trial).removal_reason = "movement periods missmatch";
979 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
980
981 continue
982
983 else
984
985 log_entry("The periods of movement do not perfectly align, but we'll try.", ptp_num, 4, trial)
986
987 log_entry(sprintf("Correlation of periods: left %.2f, right %.2f", corr_l, corr_r), ptp_num, 5, trial)
988
989 end
990
991 % save a plot of the periods
992 period_figure = figure(Visible="on");
993 subplot(2,1,1)
994 plot(vicon_periods_l(1:l_min), '.', "MarkerSize", 10)
995 hold on
996 plot(imu_periods_l(1:l_min), '.', "MarkerSize", 10)
997 xlim([length(vicon_periods_l)/2 - 500, length(vicon_periods_l)/2 + 499]);
998 title(sprintf("Left foot, correlation: %.2f", corr_l))
999 legend("VICON", "IMU", "Location", "best")
1000 grid on;
1001 subplot(2,1,2)
1002 plot(vicon_periods_r(1:r_min), '.', "MarkerSize", 10)
1003 hold on
1004 plot(imu_periods_r(1:r_min), '.', "MarkerSize", 10)
1005 xlim([length(vicon_periods_r)/2 - 500, length(vicon_periods_r)/2 + 499]);
1006 title(sprintf("Right foot, correlation: %.2f", corr_r))
1007 legend("VICON", "IMU", "Location", "best")
1008 grid on;
1009 filename = fullfile(temp_path, string(datetime("now", "Format", "yy-MM-dd_HH-mm-ss")) + "_" + trial + "_periods.fig");
1010 saveas(period_figure, filename);
1011 close(period_figure)
1012
1013 log_entry("Periods plot saved", ptp_num, 5, trial)
1014 end
1015
1016 log_entry("IMU data aligned", ptp_num, 5, trial)
1017
1018
1019 end
1020
1021 else
1022
1023 % load the already aligned data
1024 zm_ankle_l = load(fullfile(ptp_folder, "Processed", "ZM", "calibrated_ankle_L.mat"));
1025 zm_ankle_r = load(fullfile(ptp_folder, "Processed", "ZM", "calibrated_ankle_R.mat"));
1026
1027 % create a new struct
1028 zm_aligned = struct();
1029
1030 % the only trial is "Walk_Norm"
1031 trial = "Walk_Norm";
1032
1033 % left
1034 zm_aligned.(trial).l = zm_ankle_l;
1035 zm_aligned.(trial).r = zm_ankle_r;
1036
1037 end
1038
1039 % save the aligned data
1040 save(fullfile(temp_path, "zm_aligned.mat"), "zm_aligned")
1041 vicon_4 = vicon_3;
1042 save(fullfile(temp_path, "vicon_4.mat"), "vicon_4")
1043 log_entry("IMU data aligned", ptp_num)
1044
1045 % remove unnecessary variables
1046 clear vicon_3 sum_zm_l sum_zm_r zm_l_raw zm_r_raw trial_row idx_start_l
1047 clear idx_start_r corr_l_x corr_l_y corr_l_z corr_r_x corr_r_y corr_r_z
1048 clear d_samples_l d_samples_r b a v_vicon_l v_vicon_r th_vicon
1049 clear vicon_periods_l vicon_periods_r w_l w_r th_imu imu_periods_l
1050 clear imu_periods_r l_min r_min corr_l corr_r
1051end
1052
1053%% filter the data
1054
1055% load the proc info
1056proc_info = readstruct(fullfile(ptp_folder, "proc_info.json"));
1057
1058
1059if ~isfile(fullfile(temp_path, "processed_data.mat")) || FILTER
1060
1061 if ~FILTER
1062 log_entry("The data has to be filtered even though it was not marked (not done yet, 3)", ptp_num, 5)
1063 end
1064
1065 % flag as not done
1066 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"));
1067
1068 % load the aligned data, if not already done
1069 if ~exist("zm_aligned", "var"); load(temp_path + "zm_aligned.mat", "zm_aligned"); end
1070
1071 % create struct to store the filtered data
1072 filtered = struct();
1073
1074 % check if at least one trial is available
1075 if ~isempty(fieldnames(zm_aligned))
1076
1077 if ~exist("vicon_4", "var"); load(temp_path + "vicon_4.mat", "vicon_4"); end
1078
1079 % get the walking speed
1080 try
1081 speed = proc_info.walking_speed;
1082 catch ME
1083 log_entry("The walking speed is not defined", ptp_num, 2)
1084
1085 % log the error
1086 log_entry(ME.message, ptp_num, 2)
1087 for i = 1:numel(ME.stack)
1088 log_entry(ME.stack(i).name + " (Line: " + string(ME.stack(i).line) + ")", ptp_num, 2)
1089 end
1090
1091 error("The walking speed is not defined")
1092 end
1093
1094 %% instance of filter
1095 ekf = EKF1;
1096 dt = 0.005;
1097
1098 % import tuning parameters, if available
1099 if isfield(options, "ekf_tuning_file")
1100 assert(ekf.importTuning(options.ekf_tuning_file), "The tuning parameters could not be imported")
1101 log_entry("Tuning parameters imported", ptp_num, 5)
1102 end
1103
1104 % loop over all trials
1105 trials = string(fieldnames(zm_aligned))';
1106
1107 % check if the yaw angles are already defined
1108 % the yaw angles are used to calculate the initial position. also,
1109 % during the filtering process, the filtered angle is nudged
1110 % towards this angle. is is ASSUMED that the mean yaw angle of each
1111 % foot is constant over all the trials and during each trial.
1112 % please note also, that this angle does not necessarly reflect the
1113 % actual toe out angle since the JUMP sensor might nit be perfectly
1114 % aligned with the feet.
1115 %
1116 % if no yaw angle is determined yet, the walking trial with the
1117 % longest duration is filtered without x and y position nudging.
1118 % therefore the trajectory will drift in a directrion. based on
1119 % this drift, the mean yaw angle can be calculated which is then
1120 % used for the rest of the trials.
1121 find_yaw = false;
1122 if ~isfield(proc_info, "left_yaw") || isnan(proc_info.left_yaw) || ~isfield(proc_info, "right_yaw") || isnan(proc_info.right_yaw) || FIND_YAW
1123 % find the trial with the longest duration
1124 durations = zeros(1, numel(trials));
1125 for i = 1:numel(trials)
1126 durations(i) = height(zm_aligned.(trials(i)).l.tt);
1127 end
1128 [~, idx] = max(durations);
1129 trials = [trials(idx), trials];
1130
1131 log_msg = sprintf("No yaw angle is defined yet. The trial %s is filtered first to determine the yaw angle", trials(1));
1132 log_entry(log_msg, ptp_num, 4);
1133
1134 find_yaw = true;
1135 end
1136
1137 for trial = trials
1138
1139 t_before_filter = tic;
1140
1141 %% left foot
1142
1143 % zero velocity detection
1144 zv_l = jumpZeroVelocityDetection(zm_aligned.(trial).l, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
1145
1146 % start and end during stance
1147 idx_l(1) = find(zv_l, 1, "first");
1148 idx_l(2) = find(zv_l, 1, "last");
1149
1150 % initial values
1151 % initial gyroscope bias
1152 w_zv_l = zm_aligned.(trial).l.tt.gyr(zv_l,:);
1153 ekf.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;
1154
1155 % measurements
1156 ekf.zv = zv_l(idx_l(1):idx_l(2));
1157 ekf.z_acc = zm_aligned.(trial).l.tt.acc(idx_l(1):idx_l(2),:)';
1158 ekf.z_vel = zeros(size(ekf.z_acc));
1159 ekf.z_gyr = zm_aligned.(trial).l.tt.gyr(idx_l(1):idx_l(2),:)' * pi/180; % convert to rad/s
1160 ekf.z_pos = zeros(size(ekf.z_acc));
1161
1162 % check if the left foot yaw angle is already defined
1163 if find_yaw
1164 proc_info.left_yaw = nan;
1165
1166 yaw = 0; % rad
1167
1168 else
1169 yaw = proc_info.left_yaw; % rad
1170 end
1171
1172 % initial orientation
1173 %> @remark: there's something odd with the way I calculate the initial
1174 %> orientation. Since the trust in this initial value is very low and
1175 %> the results are pretty well, I will leave it as it is for the moment.
1176 %> But do not take this as a good example.
1177
1178 % mean accelerations during stance
1179 a_zv_l = zm_aligned.(trial).l.tt.acc(zv_l,:);
1180 a_x = mean(a_zv_l(1:20,1));
1181 a_y = mean(a_zv_l(1:20,2));
1182 a_z = mean(a_zv_l(1:20,3));
1183 % calculate initial orientation
1184 roll = atan2(a_y, a_z);
1185 pitch = atan2(-a_x, sqrt(a_y^2 + a_z^2));
1186 rot_vec = [roll, pitch, yaw];
1187 ekf.q_0 = quaternion(rot_vec, "rotvec").normalize.compact';
1188
1189 log_entry(sprintf("Initial orientation left: roll %.1f°, pitch %.1f°, yaw %.1f°", rad2deg(roll), rad2deg(pitch), rad2deg(yaw)), ptp_num, 5, trial)
1190
1191 % pseudo position update
1192 ekf.z_pos(1,92:end) = cumsum(speed * dt * ones(size(ekf.z_pos(1,92:end))));
1193 if find_yaw
1194 % disable x and y position pseudo updates if yaw must be found
1195 ekf.z_pos(1,:) = nan; %this line turns off x-position pseudo updates
1196 ekf.z_pos(2,:) = nan; %this line turns off y-position pseudo updates
1197
1198 log_entry("Pseudo position updates disabled", ptp_num, 5, trial)
1199 end
1200
1201 % pseudo yaw update
1202 ekf.z_yaw = yaw * ones(1, size(ekf.z_acc,2));
1203
1204 % Run left filter
1205 t_before_filter_l = tic;
1206 smoothed_l = ekf.run(plot=false);
1207 log_entry(sprintf("Filtering left foot took %.2f seconds", toc(t_before_filter_l)), ptp_num, 5, trial)
1208
1209 %% right foot
1210
1211 % zero velocity detection
1212 zv_r = jumpZeroVelocityDetection(zm_aligned.(trial).r, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
1213
1214 % start and end during stance
1215 idx_r(1) = find(zv_r, 1, "first");
1216 idx_r(2) = find(zv_r, 1, "last");
1217
1218 % initial values
1219 % initial gyroscope bias
1220 w_zv_r = zm_aligned.(trial).r.tt.gyr(zv_r,:);
1221 ekf.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;
1222
1223 % check if the right foot yaw angle is already defined
1224 if find_yaw
1225 proc_info.right_yaw = nan;
1226
1227 yaw = 0; % rad
1228
1229 else
1230 yaw = proc_info.right_yaw; % rad
1231 end
1232
1233 % initial orientation
1234 % mean accelerations during stance
1235 a_x = mean(zm_aligned.(trial).r.tt.acc(38:104,1));
1236 a_y = mean(zm_aligned.(trial).r.tt.acc(38:104,2));
1237 a_z = mean(zm_aligned.(trial).r.tt.acc(38:104,3));
1238 % calculate initial orientation
1239 roll = atan2(a_y, a_z);
1240 pitch = atan2(-a_x, sqrt(a_y^2 + a_z^2));
1241 rot_vec = [roll, pitch, yaw];
1242 ekf.q_0 = quaternion(rot_vec, "rotvec").normalize.compact';
1243
1244 log_entry(sprintf("Initial orientation right: roll %.1f°, pitch %.1f°, yaw %.1f°", rad2deg(roll), rad2deg(pitch), rad2deg(yaw)), ptp_num, 5, trial)
1245
1246 % measurements
1247 ekf.zv = zv_r(idx_r(1):idx_r(2));
1248 ekf.z_acc = zm_aligned.(trial).r.tt.acc(idx_r(1):idx_r(2),:)';
1249 ekf.z_vel = zeros(size(ekf.z_acc));
1250 ekf.z_gyr = zm_aligned.(trial).r.tt.gyr(idx_r(1):idx_r(2),:)' * pi/180; % convert to rad/s
1251 ekf.z_pos = zeros(size(ekf.z_acc));
1252
1253 % pseudo position update
1254 ekf.z_pos(1,92:end) = cumsum(speed * dt * ones(size(ekf.z_pos(1,92:end))));
1255 if find_yaw
1256 % disable x and y position pseudo updates if yaw must be found
1257 ekf.z_pos(1,:) = nan; %this line turns off x-position pseudo updates
1258 ekf.z_pos(2,:) = nan; %this line turns off y-position pseudo updates
1259
1260 log_entry("Pseudo position updates disabled", ptp_num, 5, trial)
1261 end
1262
1263 % pseudo yaw update
1264 ekf.z_yaw = yaw * ones(1, size(ekf.z_acc,2));
1265
1266 % Run right filter
1267 t_before_filter_r = tic;
1268 smoothed_r = ekf.run(plot=false);
1269 log_entry(sprintf("Filtering right foot took %.2f seconds", toc(t_before_filter_r)), ptp_num, 5, trial)
1270
1271 % save the yaw angles and dismiss the filtered data
1272 if find_yaw
1273
1274 % calculate the mean yaw angle
1275 % left
1276 end_x = smoothed_l(8, end);
1277 end_y = smoothed_l(9, end);
1278 proc_info.left_yaw = -1 * atan2(end_y, end_x);
1279
1280 % right
1281 end_x = smoothed_r(8, end);
1282 end_y = smoothed_r(9, end);
1283 proc_info.right_yaw = -1 * atan2(end_y, end_x);
1284
1285 % save the struct
1286 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
1287
1288 find_yaw = false;
1289
1290 log_msg = sprintf("Yaw angles determined: left: %.1f °, right: %.1f °", rad2deg(proc_info.left_yaw), rad2deg(proc_info.right_yaw));
1291 log_entry(log_msg, ptp_num)
1292
1293 continue
1294 end
1295
1296 % extract trajectories
1297 traj_l = smoothed_l(8:10,:)';
1298 traj_r = smoothed_r(8:10,:)';
1299
1300 % add start and end (match original length)
1301 filtered.(trial).l = [nan(idx_l(1)-1,3); traj_l; nan(height(zm_aligned.(trial).r.tt)-idx_l(2),3)];
1302 filtered.(trial).r = [nan(idx_r(1)-1,3); traj_r; nan(height(zm_aligned.(trial).r.tt)-idx_r(2),3)];
1303
1304 log_entry("Data filtered", ptp_num, 4, trial)
1305
1306 log_entry(sprintf("Filtering took %.2f seconds", toc(t_before_filter)), ptp_num, 5, trial)
1307
1308 % add filtered flag to the struct
1309 proc_info.trials.(trial).filtered = true;
1310 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
1311
1312 end
1313
1314 %% save the data
1315 result = struct();
1316 result.jump_filtered = filtered;
1317 result.vicon = vicon_4;
1318 save(fullfile(temp_path, "processed_data.mat"), "result")
1319
1320 % write the EKF parameters to the log file
1321 log_entry("EKF parameters", ptp_num, 5)
1322 log_entry(sprintf("P_0_q: %.1e", ekf.P_0_q), ptp_num, 5)
1323 log_entry(sprintf("P_0_v: %.1e", ekf.P_0_v), ptp_num, 5)
1324 log_entry(sprintf("P_0_p: %.1e", ekf.P_0_p), ptp_num, 5)
1325 log_entry(sprintf("P_0_b_w: %.1e", ekf.P_0_b_w), ptp_num, 5)
1326 log_entry(sprintf("var_acc_xy: %.1e", ekf.var_acc_xy), ptp_num, 5)
1327 log_entry(sprintf("var_acc_z: %.1e", ekf.var_acc_z), ptp_num, 5)
1328 log_entry(sprintf("var_vel_ZUPT: %.1e", ekf.var_vel_ZUPT), ptp_num, 5)
1329 log_entry(sprintf("var_gyr: %.1e", ekf.var_gyr), ptp_num, 5)
1330 log_entry(sprintf("var_pos_x: %.1e", ekf.var_pos_x), ptp_num, 5)
1331 log_entry(sprintf("var_pos_y: %.1e", ekf.var_pos_y), ptp_num, 5)
1332 log_entry(sprintf("var_pos_z: %.1e", ekf.var_pos_z), ptp_num, 5)
1333 log_entry(sprintf("var_yaw_pseudo: %.1e", ekf.var_yaw_pseudo), ptp_num, 5)
1334 log_entry(sprintf("sigma_w_p: %.1e", ekf.sigma_w_p), ptp_num, 5)
1335 log_entry(sprintf("var_a: %.1e", ekf.var_a), ptp_num, 5)
1336 log_entry(sprintf("var_b_w: %.1e", ekf.var_b_w), ptp_num, 5)
1337
1338 else
1339 log_entry("No trials available", ptp_num, 2)
1340 end
1341
1342end
1343
1344log_entry("Processing completed", ptp_num);
1345
1346% set the status to completed
1347proc_info.filtering_done = true;
1348proc_info.filtering_done_time = datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss');
1349writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
1350
1351
1352end
function axis2num(in axis)
convert an axis to a number
function axisdir2vec(in axis, in dir)
convert an axis and a direction to a vector
EKF1 is an implementation of a state estimator for JUMP sensors.
Definition EKF1.m:36
JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
Definition JumpSensor.m:41
function direction2num(in dir)
convert a direction to a number
function jumpReadData(in filePath, in options)
function jumpZeroVelocityDetection(in data, in options)
function log_entry(in msg, in ptp_num, in level, in trial)
log entry to a log file
function num2axis(in num)
convert a number to an axis
function num2direction(in num)
convert a number to a direction
function process_participant(in basepath, in ptp, in options)
preprocess and filter the data of a single participant
function vec2axisdir(in vec)
convert a vector to an axis and a direction