1% =========================================================================== %
2%> @brief preprocess and filter the data of a single participant
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
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
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()
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.
39%> @copyright see the file @ref LICENSE in the root directory of the repository
40% =========================================================================== %
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}
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;
65% preprocess options to ensure dependencies
67 FIND_WALKING_AXES =
true;
68 FIND_WALKING_SPEED =
true;
69 ACCUMULATE_DIST =
true;
73 FIND_WALKING_SPEED =
true;
74 ACCUMULATE_DIST =
true;
78 ACCUMULATE_DIST =
true;
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/");
101log_entry(
"Start processing participant", ptp_num)
103if ~isfolder(temp_path)
104 % create the temp folder
if it does not exist
106 log_entry(
"Created temp folder", ptp_num, 5)
109% log the tasks that need to be done
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)
119% check if the participants JSON already exists
120if isfile(fullfile(ptp_folder, "proc_info.json"))
122 proc_info = readstruct(fullfile(ptp_folder, "proc_info.json"));
123 log_entry("JSON file loaded", ptp_num, 5)
125 % set status to not done
126 proc_info.filtering_done = false;
127 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
129 % create the JSON file
130 proc_info = struct();
131 log_entry("JSON file not found, created new struct", ptp_num, 4)
133 % set status to not done
134 proc_info.filtering_done = false;
135 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
138%% load files (only if not a Pilot participant)
140if contains(ptp, "SonE")
142 % vicon data position data
143 if ~isfile(temp_path + "vicon_1.mat")
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");
149 log_entry("VICON data loaded", ptp_num, 5)
151 % remove unnecessary variables (only large ones)
155 % zurich move, data ankles
156 if ~isfile(temp_path + "zm_l_raw.mat") || ~isfile(temp_path + "zm_r_raw.mat")
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);
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);
172 save(temp_path + "zm_l_raw.mat", "zm_l_raw");
173 save(temp_path + "zm_r_raw.mat", "zm_r_raw");
175 log_entry("Zurich Move data loaded", ptp_num, 5)
180 if ~isfile(temp_path + "sum_zm_l.mat") || ~isfile(temp_path + "sum_zm_r.mat")
182 load(summary_path, "GaitSummaryIMU");
183 sum_zm_l = GaitSummaryIMU.ModulesData.ankle_L;
184 sum_zm_r = GaitSummaryIMU.ModulesData.ankle_R;
186 save(temp_path + "sum_zm_l.mat", "sum_zm_l");
187 save(temp_path + "sum_zm_r.mat", "sum_zm_r");
189 log_entry("Gait summary data loaded", ptp_num, 5)
191 % remove unnecessary variables (only large ones)
197 % load the pilot data
198 if ~isfile(temp_path + "vicon_1.mat")
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");
205 log_entry("VICON data loaded", ptp_num, 5)
207 % remove unnecessary variables (only large ones)
211 % zurich move, data ankles
212 if ~isfile(temp_path + "zm_l_raw.mat") || ~isfile(temp_path + "zm_r_raw.mat")
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"));
217 save(temp_path + "zm_l_raw.mat", "zm_l_raw");
218 save(temp_path + "zm_r_raw.mat", "zm_r_raw");
220 log_entry("Zurich Move data loaded", ptp_num, 5)
224 % gait summary is not needed for pilot participants
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)
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.
240% note: the vicon data is in mm
242if ~isfile(fullfile(temp_path,"vicon_2.mat")) || GT_POS
244 % load the vicon data, if not already done
245 if ~exist("vicon_1", "var"); load(temp_path + "vicon_1.mat", "vicon_1"); end
247 % loop over all trials
248 trials =
string(fieldnames(vicon_1))';
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;
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;
256 % offset from toe towards heel
257 offset = 65; % 6.5 cm
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;
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;
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"))
275 % rename vicon_1 to vicon_2
279 save(temp_path + "vicon_2.mat", "vicon_2");
281 log_entry("Ground truth positions created", ptp_num)
283 % remove unnecessary variables (only large ones)
284 clear vicon_1 left_vec right_vec
288%% determine the walking axis
289if ~isfield(proc_info, "walking_axes") || FIND_WALKING_AXES
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)
295 % load the vicon data, if not already done
296 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
298 % loop over all trials
299 trials =
string(fieldnames(vicon_2))';
301 sag_axis = 0; % sagital axis
302 lon_axis = 0; % longitudinal axis
303 lat_axis = 0; % lateral axis
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);
311 % find the walking axis (saggiatal axis) -> highest standard deviation
312 [~, idx_left] = max(std_axis_left);
313 [~, idx_right] = max(std_axis_right);
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)
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)
323 error("The sagital axis of the left and right foot is not constant over all trials")
328 log_entry("The sagital axis of the left and right foot is not the same", ptp_num, 2, trial)
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)
333 error("The sagital axis of the left and right foot is not the same")
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;
342 % find the axis with the highest skewness
343 [~, idx_long] = max(skew_comb);
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)
349 log_entry(sprintf("Combined Skewness: %.2f %.2f %.2f", skew_comb(1), skew_comb(2), skew_comb(3)), ptp_num, 5, trial)
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)
357 % find the lateral axis -> the remaining axis
358 lat_axis = setdiff(1:3, [sag_axis, lon_axis]);
362 if sag_axis == 0 || lon_axis == 0 || lat_axis == 0
363 log_entry("The walking axes could not be determined", ptp_num, 2)
365 error("The walking axes could not be determined")
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"))
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)
378%% determine the walking speed
379if ~isfield(proc_info, "walking_speed") || FIND_WALKING_SPEED
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)
385 % load the vicon data, if not already done
386 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
388 % get the walking axis
390 sag_axis =
axis2num(proc_info.walking_axes.sagittal);
391 lon_axis =
axis2num(proc_info.walking_axes.longitudinal);
393 log_entry("The walking axes are not defined", 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)
401 error("The walking axes are not defined")
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
413 % store all positions of both feet in the walking direction and the
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;
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;
430 walking_direction_positions = walking_direction_positions(~isnan(walking_direction_positions));
431 up_down_positions = up_down_positions(~isnan(up_down_positions));
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
439 elseif skewness(up_down_positions) < -0.5
442 log_entry("The upwards direction could not be determined", ptp_num, 2)
444 error("The upwards direction could not be determined")
447 log_entry(sprintf("Upwards direction determined: %s %s-axis", proc_info.walking_axes.upwards, proc_info.walking_axes.longitudinal), ptp_num, 5)
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
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
463 elseif skewness(speed) > 0.5
466 log_entry("The walking direction could not be determined", ptp_num, 2)
468 error("The walking direction could not be determined")
471 log_entry(sprintf("Walking direction determined: %s %s-axis", proc_info.walking_axes.forwards, proc_info.walking_axes.sagittal), ptp_num, 5)
473 % find the sinister direction of the lateral axis
474 % we know that a right hand system is used. the lateral axis is the
477 % we form a right hand system following forward, sinister, upwards
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)...
488 assert(axis == lat_axis, "The sinister direction is not on the lateral axis")
490 log_entry("The sinister direction is not the lateral axis", 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)
498 error("The sinister direction is not the lateral axis")
503 log_entry(sprintf("sinister direction determined: %s %s-axis", proc_info.walking_axes.sinister, proc_info.walking_axes.lateral), ptp_num, 5)
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
510 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
512 log_msg = sprintf("Walking speed determined: %.2f m/s, %.1f km/h", proc_info.walking_speed, proc_info.walking_speed*3.6);
515 % remove unnecessary variables (only large ones)
516 clear walking_direction_positions up_down_positions speed f xi
520% accumulate the distance based on the speed
521if ~isfile(temp_path + "vicon_3.mat") || ACCUMULATE_DIST
524 log_entry("The distance has to be accumulated even though it was not marked (not done yet)", ptp_num, 5)
527 % load the vicon data, if not already done
528 if ~exist("vicon_2", "var"); load(temp_path + "vicon_2.mat", "vicon_2"); end
530 % get the walking axis (sagital axis)
532 axis =
axis2num(proc_info.walking_axes.sagittal);
534 log_entry("The walking axis is not defined", 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)
542 error("The walking axis is not defined")
545 % get the walking direction
547 walking_direction =
direction2num(proc_info.walking_axes.forwards);
549 log_entry("The walking direction is not defined", 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)
557 error("The walking direction is not defined")
560 % get the walking speed
562 speed = proc_info.walking_speed;
564 log_entry("The walking speed is not defined", 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)
572 error("The walking speed is not defined")
575 % create a new struct
578 % loop over all trials
579 trials =
string(fieldnames(vicon_2))';
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
587 % create a detector based on the standard deviation of the feet in the
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);
593 % check if there's a phase where the treadmill is not running (std < 100)
594 if any(movstd_result < 100)
596 log_entry("Treadmill start or stop detected", ptp_num, 3, trial)
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
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)
609 % keep only last start in the first half and the first stop in
610 % after the first start
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)
619 % find the first stop after the first start
620 stop_idx = find(not_walking(start_idx:end) == 1, 1, "first");
622 stop_idx = length(not_walking);
624 stop_idx = stop_idx + start_idx - 1;
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;
633 if any(not_walking == -1)
634 idx_start = find(not_walking == -1, 1, "first") + 1000; % 5s after the start
636 log_entry("Beginning of the trial must be cut", ptp_num, 5, trial)
642 if any(not_walking == 1)
643 idx_end = find(not_walking == 1, 1, "last") - 1000; % 5s before the end
645 log_entry("End of the trial must be cut", ptp_num, 5, trial)
647 idx_end = height(vicon_2.(trial).KinematicData.Marker.LF);
650 % plot the start and stop detection
651 start_stop_figure = figure(Visible="off");
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")
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)
662 log_entry("Start and stop detection plot saved", ptp_num, 5, trial)
665 vicon_2.(trial).idx_start = idx_start;
666 vicon_2.(trial).idx_end = idx_end;
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,:);
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;
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.
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"))
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;
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
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);
715 save(temp_path + "vicon_3.mat", "vicon_3")
717 log_entry("Distance accumulated", ptp_num)
719 % remove unnecessary variables
720 clear vicon_2 dist axis walking_direction
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.
730if ~isfile(fullfile(temp_path, "zm_aligned.mat")) || ~isfile(fullfile(temp_path, "vicon_4.mat")) || ALIGN_IMU_VICON
732 % load the vicon data, if not already done
733 if ~exist("vicon_3", "var"); load(temp_path + "vicon_3.mat","vicon_3"); end
735 if contains(ptp_folder, "SonE")
739 log_entry("The IMU data has to be aligned even though it was not marked (not done yet)", ptp_num, 5)
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
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
750 % create a new struct
751 zm_aligned = struct();
753 % loop over all trials
754 trials =
string(fieldnames(vicon_3))';
757 % find the row where the trial is
758 trial_row = find(
string({sum_zm_l.synced.trim.Trials}) == trial);
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,:);
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;
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;
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);
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);
783 % check
if the data is properly aligned
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));
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")
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));
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)
812 % add flag to the struct
813 proc_info.trials.(trial).imu_swapped = true;
817 % remove the trial from the structs
818 zm_aligned = rmfield(zm_aligned, trial);
819 vicon_3 = rmfield(vicon_3, trial);
821 log_entry("Trial removed", ptp_num, 3, trial)
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]);
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)
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";
833 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
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.
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);
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")
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"))
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);
862 log_entry("The samples difference is too high. The trial was removed.", ptp_num, 2, trial)
864 log_entry(sprintf("Difference in samples: left %d, right %d", d_samples_l, d_samples_r), ptp_num, 5, trial)
867 proc_info.trials.(trial).removal_reason = "IMU VICON num of samples missmatch";
868 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
873 log_entry("The samples difference is small. The trial was not removed.", ptp_num, 4, trial)
875 log_entry(sprintf("Difference in samples: left %d, right %d", d_samples_l, d_samples_r), ptp_num, 5, trial)
881 % check periods of movement
882 % here the following is done:
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
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
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
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);
906 v_vicon_l = vecnorm(v_vicon_l,2,2);
907 v_vicon_r = vecnorm(v_vicon_r,2,2);
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;
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);
920 [b, a] = butter(2, 0.1);
921 w_l = filtfilt(b, a, w_l);
922 w_r = filtfilt(b, a, w_r);
925 w_l = vecnorm(w_l,2,2);
926 w_r = vecnorm(w_r,2,2);
930 imu_periods_l = w_l > th_imu;
931 imu_periods_r = w_r > th_imu;
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
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));
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")
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));
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)
952 this_zm_l = zm_aligned.(trial).l;
953 this_zm_r = zm_aligned.(trial).r;
955 zm_aligned.(trial).l = this_zm_r;
956 zm_aligned.(trial).r = this_zm_l;
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"))
967 if corr_l < 0.5 || corr_r < 0.5
969 % remove the trial from the structs
970 zm_aligned = rmfield(zm_aligned, trial);
971 vicon_3 = rmfield(vicon_3, trial);
973 log_entry("The periods of movement do not align. The trial was removed.", ptp_num, 2, trial)
975 log_entry(sprintf("Correlation of periods: left %.2f, right %.2f", corr_l, corr_r), ptp_num, 5, trial)
978 proc_info.trials.(trial).removal_reason = "movement periods missmatch";
979 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
985 log_entry("The periods of movement do not perfectly align, but we'll try.", ptp_num, 4, trial)
987 log_entry(sprintf("Correlation of periods: left %.2f, right %.2f", corr_l, corr_r), ptp_num, 5, trial)
991 % save a plot of the periods
992 period_figure = figure(Visible="on");
994 plot(vicon_periods_l(1:l_min), '.', "MarkerSize", 10)
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")
1002 plot(vicon_periods_r(1:r_min), '.', "MarkerSize", 10)
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")
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)
1013 log_entry("Periods plot saved", ptp_num, 5, trial)
1016 log_entry("IMU data aligned", ptp_num, 5, trial)
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"));
1027 % create a new struct
1028 zm_aligned = struct();
1030 % the only trial is "Walk_Norm"
1031 trial = "Walk_Norm";
1034 zm_aligned.(trial).l = zm_ankle_l;
1035 zm_aligned.(trial).r = zm_ankle_r;
1039 % save the aligned data
1040 save(fullfile(temp_path, "zm_aligned.mat"), "zm_aligned")
1042 save(fullfile(temp_path, "vicon_4.mat"), "vicon_4")
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
1056proc_info = readstruct(fullfile(ptp_folder, "proc_info.json"));
1059if ~isfile(fullfile(temp_path, "processed_data.mat")) || FILTER
1062 log_entry("The data has to be filtered even though it was not marked (not done yet, 3)", ptp_num, 5)
1066 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"));
1068 % load the aligned data, if not already done
1069 if ~exist("zm_aligned", "var"); load(temp_path + "zm_aligned.mat", "zm_aligned"); end
1071 % create struct to store the filtered data
1072 filtered = struct();
1074 % check if at least one trial is available
1075 if ~isempty(fieldnames(zm_aligned))
1077 if ~exist("vicon_4", "var"); load(temp_path + "vicon_4.mat", "vicon_4"); end
1079 % get the walking speed
1081 speed = proc_info.walking_speed;
1083 log_entry("The walking speed is not defined", 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)
1091 error("The walking speed is not defined")
1094 %% instance of filter
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)
1104 % loop over all trials
1105 trials =
string(fieldnames(zm_aligned))';
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.
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.
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);
1128 [~, idx] = max(durations);
1129 trials = [trials(idx), trials];
1131 log_msg = sprintf("No yaw angle is defined yet. The trial %s is filtered first to determine the yaw angle", trials(1));
1139 t_before_filter = tic;
1143 % zero velocity detection
1144 zv_l =
jumpZeroVelocityDetection(zm_aligned.(trial).l, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
1146 % start and end during stance
1147 idx_l(1) = find(zv_l, 1, "first");
1148 idx_l(2) = find(zv_l, 1, "last");
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;
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));
1162 % check if the left foot yaw angle is already defined
1164 proc_info.left_yaw = nan;
1169 yaw = proc_info.left_yaw; % rad
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.
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';
1189 log_entry(sprintf("Initial orientation left: roll %.1f°, pitch %.1f°, yaw %.1f°", rad2deg(roll), rad2deg(pitch), rad2deg(yaw)), ptp_num, 5, trial)
1191 % pseudo position update
1192 ekf.z_pos(1,92:end) = cumsum(speed * dt * ones(size(ekf.z_pos(1,92:end))));
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
1198 log_entry("Pseudo position updates disabled", ptp_num, 5, trial)
1202 ekf.z_yaw = yaw * ones(1, size(ekf.z_acc,2));
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)
1211 % zero velocity detection
1212 zv_r =
jumpZeroVelocityDetection(zm_aligned.(trial).r, "threshold_acc", 1.2, "threshold_gyr", 15, "windowSize", 30, "plot", false);
1214 % start and end during stance
1215 idx_r(1) = find(zv_r, 1, "first");
1216 idx_r(2) = find(zv_r, 1, "last");
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;
1223 % check if the right foot yaw angle is already defined
1225 proc_info.right_yaw = nan;
1230 yaw = proc_info.right_yaw; % rad
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';
1244 log_entry(sprintf("Initial orientation right: roll %.1f°, pitch %.1f°, yaw %.1f°", rad2deg(roll), rad2deg(pitch), rad2deg(yaw)), ptp_num, 5, trial)
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));
1253 % pseudo position update
1254 ekf.z_pos(1,92:end) = cumsum(speed * dt * ones(size(ekf.z_pos(1,92:end))));
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
1260 log_entry("Pseudo position updates disabled", ptp_num, 5, trial)
1264 ekf.z_yaw = yaw * ones(1, size(ekf.z_acc,2));
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)
1271 % save the yaw angles and dismiss the filtered data
1274 % calculate the mean yaw angle
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);
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);
1286 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
1290 log_msg = sprintf("Yaw angles determined: left: %.1f °, right: %.1f °", rad2deg(proc_info.left_yaw), rad2deg(proc_info.right_yaw));
1296 % extract trajectories
1297 traj_l = smoothed_l(8:10,:)';
1298 traj_r = smoothed_r(8:10,:)';
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)];
1304 log_entry("Data filtered", ptp_num, 4, trial)
1306 log_entry(sprintf("Filtering took %.2f seconds", toc(t_before_filter)), ptp_num, 5, trial)
1308 % add filtered flag to the struct
1309 proc_info.trials.(trial).filtered = true;
1310 writestruct(proc_info, fullfile(ptp_folder, "proc_info.json"))
1316 result.jump_filtered = filtered;
1317 result.vicon = vicon_4;
1318 save(fullfile(temp_path, "processed_data.mat"), "result")
1320 % write the EKF parameters to the log file
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)
1339 log_entry("No trials available", ptp_num, 2)
1344log_entry("Processing completed", ptp_num);
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"))
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.
JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
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