Source code for biomechzoo.imu.step_detection
import numpy as np
from scipy.signal import find_peaks, butter, filtfilt
[docs]
def imu_mcgrath(ch_line, fsamp, min_stance_t, is_filtered=False):
"""
This function detects the steps based on the method of McGrath et al. (2012) https://doi.org/10.1007/s12283-012-0093-8
in short, the first minimum after a local maximum is the heel strike. The local maxima are the mid-swing.
Data should be filtered
"""
if is_filtered:
yd = ch_line
else:
# Butterworth filter
order = 5
Fc = 5
Wn = Fc / (fsamp / 2)
[b, a] = butter(order, Wn, btype='low')
yd = filtfilt(b, a, ch_line)
# Identify midswing peaks
t1 = round(fsamp / 2)
potential_midswing_ind, _ = find_peaks(yd, distance=t1)
potential_midswing_mag = yd[potential_midswing_ind]
# Thresholds for midswing
th2 = 0.8 * np.mean(yd[yd > np.mean(yd)])
mask = potential_midswing_mag >= th2
potential_midswing_ind = potential_midswing_ind[mask]
potential_midswing_mag = potential_midswing_mag[mask]
th1 = 0.3 * potential_midswing_mag
# Find minima
minima_ind, _ = find_peaks(-yd)
minima_mag = -yd[minima_ind]
# Validate midswing peaks
valid_inds = []
for i in range(len(potential_midswing_ind) - 1, -1, -1):
peak_idx = potential_midswing_ind[i]
preceding_min = minima_ind[minima_ind < peak_idx]
if preceding_min.size > 0:
closest_min_idx = preceding_min[-1]
pos = np.where(minima_ind == closest_min_idx)[0][0]
if (potential_midswing_mag[i] - minima_mag[pos]) >= th1[i]:
valid_inds.append(peak_idx)
potential_midswing_ind = np.array(valid_inds)
# Additional thresholds
th3 = 0.8 * abs(np.mean(yd[yd < np.mean(yd)]))
th4 = 0.8 * np.mean(yd[yd < np.mean(yd)])
th5 = np.mean(yd)
th6 = 2 * th3
maxima_ind, _ = find_peaks(yd)
maxima_mag = yd[maxima_ind]
IC, TC = [], []
t2 = round(1.5 * fsamp)
# Loop through confirmed midswing peaks
for step_idx in range(len(potential_midswing_ind) - 1, -1, -1):
peak_idx = potential_midswing_ind[step_idx]
# IC candidates (minima after midswing)
end_idx = min(peak_idx + t2, len(yd))
ic_candidates, _ = find_peaks(-yd[peak_idx:end_idx])
ic_candidates = ic_candidates + peak_idx
ic_mags = yd[ic_candidates]
# Filter IC by threshold
ic_candidates = ic_candidates[ic_mags < th5]
ic_mags = ic_mags[ic_mags < th5]
# Validate IC with preceding maxima
for ic_idx in ic_candidates:
preceding_max = maxima_ind[maxima_ind < ic_idx]
if preceding_max.size > 0:
closest_max_idx = preceding_max[-1]
if yd[closest_max_idx] >= yd[ic_idx] + th3:
IC.append(ic_idx)
break
# TC candidates (minima before midswing)
start_idx = max(peak_idx - t2, 0)
tc_candidates, _ = find_peaks(-yd[start_idx:peak_idx])
tc_candidates = tc_candidates + start_idx
tc_mags = yd[tc_candidates]
# Filter TC by threshold
tc_candidates = tc_candidates[tc_mags < th4]
tc_mags = tc_mags[tc_mags < th4]
# Validate TC with following maxima
for tc_idx in tc_candidates:
following_max = maxima_ind[maxima_ind > tc_idx]
if following_max.size > 0:
closest_max_idx = following_max[0]
if yd[closest_max_idx] >= yd[tc_idx] + th6:
TC.append(tc_idx)
break
# Crash handling
IC, TC = crash_catch(int(min_stance_t * fsamp / 1000), IC, TC)
return IC, TC
[docs]
def crash_catch(min_stance_samples, IC, TC):
# Ensure IC and TC arrays are same length and valid
IC = np.array(IC)
TC = np.array(TC)
if len(IC) != len(TC):
min_len = min(len(IC), len(TC))
IC = IC[:min_len]
TC = TC[:min_len]
return IC, TC