Part VI: Motion, Location, and Inertial Intelligence
Chapter 23: Inertial Sensors and Motion Signals

Step detection and gait analysis

"They asked me to count steps. I counted every time the arm swung, every pothole in the bus, every laugh that shook the phone. Two thousand of those were not walking."

An Overcounting AI Agent

Why this section matters

A step is the single most-shipped inference in all of sensing. The pedometer in a billion pockets, the cadence readout on a running watch, the fall-risk score in a geriatric clinic, and the freezing-of-gait alarm for a Parkinson's patient all rest on one deceptively simple question: given a stream of acceleration, when did a foot strike the ground, and what does the rhythm of those strikes reveal about the walker? This section builds step detection from the raw signal upward, then turns a train of detected steps into the clinical vocabulary of gait: cadence, stride time, symmetry, and variability. The recurring lesson is that the easy 90 percent (a person marching on a treadmill) and the hard last 10 percent (a phone loose in a coat during a bumpy bus ride) demand completely different engineering.

This section assumes you can already separate gravity from linear acceleration (Section 23.3) and that the raw stream has been resampled to a uniform grid and low-pass filtered. If sampling jitter or the choice of cutoff frequency is fuzzy, revisit Chapter 3 for timing and Chapter 6 for filter design. Throughout, we work with the accelerometer magnitude \(a[n] = \lVert \mathbf{a}[n] \rVert\) or the gravity-projected vertical acceleration \(a_v[n]\), sampled at rate \(f_s\) (50 to 100 Hz is typical for a wrist or waist device).

The accelerometer signature of a step

Walking is a periodic collision. Each heel strike sends an impulse up the skeleton that the accelerometer records as a sharp positive-then-negative swing in vertical acceleration, followed by a quieter mid-stance and a push-off before the next strike. On the magnitude signal \(a[n]\), a step shows up as a prominent local peak roughly once every 0.4 to 0.7 seconds during normal walking, which is a step frequency of about 1.4 to 2.5 Hz. That narrow, physiologically bounded band is the single most useful prior you have, and every robust detector exploits it.

The naive detector is a peak finder with a threshold and a refractory period: declare a step whenever \(a[n]\) crosses above a threshold \(\tau\), is a local maximum, and lies at least \(\Delta t_{\min}\) seconds after the previous step. The refractory window \(\Delta t_{\min}\) (say 250 ms) suppresses the double-count that a single strike's ringing would otherwise produce. This works beautifully on a waist-mounted sensor during steady walking and fails the moment the signal leaves that regime: a fixed \(\tau\) that is right for a brisk walk misses the shallow peaks of a slow shuffle and fires on the jolt of setting a bag down.

From peaks to a detector that survives real life

Three ideas turn the toy detector into something deployable. First, make the threshold adaptive: track a running estimate of the signal's mean and standard deviation and set \(\tau = \mu + k\sigma\) over a sliding window, so the detector rescales itself to a shuffle or a sprint without hand-tuning. Second, exploit periodicity directly. The step rhythm is so regular that the short-time autocorrelation of \(a[n]\) has a strong peak at the step-period lag; estimating cadence from that lag, or from the dominant bin of a short-time Fourier transform (Chapter 7), gives a count that is immune to individual missed or spurious peaks. Third, gate on context: if an activity classifier (Chapter 26) says the wearer is stationary or driving, count zero regardless of what the peak finder sees. Production pedometers blend all three, typically running a peak counter for low latency and cross-checking it against a spectral cadence estimate for robustness.

Placement rewrites the whole problem

The same algorithm behaves like a different system depending on where the sensor sits. A waist or foot sensor sees clean, high-amplitude vertical impacts and step detection is nearly solved. A wrist sensor sees arm swing superimposed on gait, so the dominant periodicity can be the swing (one arm cycle per two steps) rather than the strike, and a pocketed phone sees whatever orientation it happened to fall into. Never quote a step-detection accuracy number without stating the mounting location; a "99 percent" figure from a rigidly strapped shank sensor tells you almost nothing about the same code on a loosely worn smartwatch.

A freezing-of-gait monitor in a movement-disorders clinic

A neurology group instruments Parkinson's patients with a waist IMU to catch freezing of gait, the sudden involuntary halt where the feet feel glued to the floor. Their first pedometer, a fixed-threshold peak counter tuned on healthy volunteers, reported near-zero steps during the very freezing episodes they cared about and looked broken to the clinicians. The fix was not a fancier model but a reframing: they added a high-frequency band-power feature (freezing produces trembling in the 3 to 8 Hz band even as forward progress stops) and switched step counting to an adaptive threshold. The freezing index, the ratio of that trembling band to the normal locomotor band, became the clinical signal, and step cadence variability became a secondary marker of gait instability. The lesson generalizes: the interesting patients are exactly the ones outside the distribution your detector was tuned on.

The snippet below implements the adaptive-threshold peak detector on gravity-removed magnitude, using SciPy to do the peak bookkeeping. It is the reference we compare against for the rest of the section.

import numpy as np
from scipy.signal import butter, filtfilt, find_peaks

def detect_steps(acc_xyz, fs, k=0.7, fmin=1.2, fmax=2.6):
    # acc_xyz: (N, 3) array in m/s^2, gravity already removed (Section 23.3)
    mag = np.linalg.norm(acc_xyz, axis=1)

    # Band-limit to the physiological step band before peak finding.
    b, a = butter(2, [fmin, fmax], btype="band", fs=fs)
    x = filtfilt(b, a, mag)

    # Adaptive height: mean + k * std over the whole record (use a sliding
    # window online). Refractory distance enforces a plausible max cadence.
    height = x.mean() + k * x.std()
    min_dist = int(fs / fmax)                 # samples between steps at fastest cadence
    peaks, _ = find_peaks(x, height=height, distance=min_dist)

    step_times = peaks / fs
    step_intervals = np.diff(step_times)      # seconds between consecutive strikes
    cadence = 60.0 / np.median(step_intervals) if len(step_intervals) else 0.0
    return len(peaks), cadence, step_intervals
A reference step detector: band-pass to the 1.2 to 2.6 Hz walking band, set an amplitude-adaptive peak height, enforce a refractory distance, then read cadence off the median step interval. The returned step_intervals feed the gait metrics in the next subsection.

Right tool: don't hand-roll the peak bookkeeping

The local-maximum search, height gating, and minimum-distance enforcement are exactly what scipy.signal.find_peaks already does correctly, including edge cases (plateaus, ties, boundary peaks) that a hand-written loop gets wrong. Using it collapses roughly 30 lines of index juggling into one call. For higher-level gait metrics, the open-source gaitpy and skdh (scikit-digital-health) packages take a raw accelerometer record and return bout detection, cadence, and stride timing directly, turning a several-hundred-line pipeline into a dozen. Reach for them once your detector is validated; write the loop once, by hand, to understand what they assume.

From steps to gait: the clinical vocabulary

Counting steps is the floor, not the ceiling. A train of accurate strike times is a temporal skeleton from which the standard gait parameters follow. Let \(t_1, t_2, \dots\) be successive heel-strike times. The step time is \(t_{i+1} - t_i\); a stride is two steps (a full gait cycle, same foot to same foot), so stride time is \(t_{i+2} - t_i\). Cadence is steps per minute, \(60 / \overline{(t_{i+1}-t_i)}\). If you can distinguish left from right strikes (two foot sensors, or the alternating asymmetry in a single waist signal), you also recover stance and swing phase durations and, from their imbalance, gait symmetry.

The parameter that carries the most clinical weight is gait variability: the beat-to-beat fluctuation in stride time, usually reported as a coefficient of variation \(\text{CV} = \sigma_{\text{stride}} / \mu_{\text{stride}}\). Healthy steady walking is metronomic, with stride-time CV around 1 to 3 percent. Elevated variability is a validated predictor of fall risk in older adults and a marker of neurodegenerative disease. This is why step timing accuracy matters more than the raw count for health applications: a detector that gets the count right but scatters the strike times will inflate the variability estimate and manufacture a fall risk that is not there. Estimating and reporting the uncertainty on these parameters (Chapter 4) is not optional in a clinical setting.

Evaluating a step detector without fooling yourself

Two evaluation traps dominate this field. The first is leakage across subjects: because one person's gait is highly self-similar, random-splitting windows from the same walker into train and test leaks their signature and inflates every metric. Always split by subject, and ideally by session, so the test set contains gaits the tuner never saw. The second is metric mismatch. Report step detection as event-level precision and recall against a reference (a foot switch or motion-capture ground truth) with a tolerance window, not just total-count error, because over- and under-counts can cancel to a flattering net-count accuracy while the strike timing is a mess. The zero-velocity moment at mid-stance, which every step provides, is also the anchor that pedestrian dead reckoning uses to reset drift; that connection is developed in Chapter 24.

Exercise

Take a 60-second wrist-IMU walking record (or synthesize one by summing a 1.8 Hz sinusoid, a 0.9 Hz arm-swing component, and noise). Run the reference detector, then deliberately halve the low-pass cutoff and re-run. Explain why the count changes, and identify which of the two cadence estimates (peak-interval median versus dominant FFT bin) is more robust to the change and why. Then corrupt 5 percent of the samples with impulsive spikes and report how each estimate degrades.

Self-check

1. Why does a wrist-worn sensor often show a dominant periodicity at half the step frequency, and how would you detect and correct for it?

2. A pedometer reports the correct total step count but a stride-time CV of 9 percent for a healthy young adult. What has most likely gone wrong, and which metric would have caught it?

3. Give one reason a fixed amplitude threshold fails on a slow shuffling gait, and state the adaptive rule that fixes it.

What's Next

In Section 23.6, we look at what happens when the accelerometer itself misbehaves: clipping during a hard impact, saturation, dropouts, and the failure modes that quietly poison every step count and gait metric built on top of the raw stream.