Finding the Beat: Autocorrelation Cadence Detection at the Edge
How do you count a dog's steps when the dog might be a four-kilogram terrier or a sixty-kilogram mastiff, walking, trotting, or flat-out galloping? You can't hard-code a step frequency, because there isn't one, a small dog's legs cycle nearly twice as fast as a large dog's, and every gait changes the rhythm again. The naive fix, a fixed band-pass filter tuned to "typical" walking, quietly fails at both ends of the range. At Hoomanely, our collar solves this on-device by first finding the beat, measuring the dog's actual stride rhythm from raw motion using autocorrelation, and only then filtering around it. This post is a focused look at that cadence-detection stage: how we lock onto a stride frequency in real time, and how that single number drives everything downstream.
The problem: there is no "typical" stride
A pedometer's whole job is to find a repeating pattern in noisy acceleration. The trouble is that the pattern's period is exactly the thing we don't know in advance.
Stride frequency for dogs spans a huge range. A slow walk produces a vertical-acceleration rhythm around 1 Hz; a hard gallop pushes that toward 8 Hz. Pick a filter centered on 2 Hz and you smear the gallop into mush; pick 5 Hz and a walking senior dog vanishes into the stopband.
Worse, plenty of non-walking motion is also rhythmic-ish, a dog scratching, or riding in a car, and plenty of real motion is messy. So before counting anything, we need two things from the raw signal: the dominant period (so we can filter intelligently) and a measure of how rhythmic the motion actually is (so we can reject the noise). Autocorrelation gives us both in one pass.

The approach: let the signal tell us its period
Autocorrelation is a beautifully simple idea: slide a signal against a delayed copy of itself and measure how well they line up at each delay (called a lag). A walking dog's vertical motion lines up strongly with itself one stride later, so the lag where correlation peaks is the stride period.
We work on a clean dorso-ventral (DV) channel, the up-and-down through the dog's body axis, recovered from the raw inertial data by an orientation step that cancels out how the collar happens to be hanging. From there, cadence detection runs on a short rolling window every couple of seconds.
The search is deliberately bounded. We only look for periods between a slow walk and a hard gallop, which keeps the math cheap and rejects nonsense outright. Those bounds are baked into the firmware as lag limits:
// Cadence search bounds. 1.0 Hz is a slow walk's DV channel f0 (stride
// ~0.5 Hz, DV doubles to 1 Hz); 8 Hz is a hard gallop. Outside this
// band we treat as quiet. Was 0.5-8 Hz with a 4-s window — but the
// autocorrelation needs N >= 2*lag_max samples to be meaningful, and
// after dropping to a 2-s ring (200 samples) the floor had to come up
// to 1 Hz so lag_max=100 fits.
#define PED_F_MIN_HZ 1.0f
#define PED_F_MAX_HZ 8.0f
#define PED_LAG_MAX ((int)(PED_FS_HZ / PED_F_MIN_HZ)) // 100
#define PED_LAG_MIN ((int)(PED_FS_HZ / PED_F_MAX_HZ)) // 12At a 100 Hz sample rate, a 1 Hz floor means a maximum lag of 100 samples, and an 8 Hz ceiling means a minimum lag of 12. The comment captures a real constraint we hit: autocorrelation needs at least twice the maximum lag in samples to be meaningful, so shrinking the analysis window (to save RAM) forced the low-frequency floor up. Every constant here is a tradeoff between coverage, memory, and statistical validity.
The process: locking onto the peak
The detector itself is short and entirely integer-friendly floating-point math, no FFT, no library. First we mean-center the window and compute the zero-lag energy, R(0), which is just the variance times the sample count:
// Mean-center
float mean = 0.0f;
for (int i = 0; i < n; ++i) mean += dv[i];
mean /= (float)n;
// R(0) (variance x n)
float r0 = 0.0f;
for (int i = 0; i < n; ++i) {
float v = dv[i] - mean;
r0 += v*v;
}
if (r0 <= 1e-9f) return 0.0f;That early return matters: if the signal has essentially no energy, the dog is still, and there's nothing to detect. Then we sweep every candidate lag in our band, compute the correlation at each, and keep the strongest:
// Sweep R(lag) for lag in [PED_LAG_MIN, PED_LAG_MAX], pick max.
int best_lag = 0;
float best_corr = 0.0f;
for (int lag = PED_LAG_MIN; lag <= PED_LAG_MAX; ++lag) {
float r = 0.0f;
for (int i = 0; i + lag < n; ++i) {
r += (dv[i] - mean) * (dv[i + lag] - mean);
}
if (r > best_corr) { best_corr = r; best_lag = lag; }
}
if (best_lag == 0) return 0.0f;
*peakiness_out = best_corr / r0;
return PED_FS_HZ / (float)best_lag;Two outputs fall out of this loop. The cadence is fs / best_lag, the sample rate divided by the winning lag, in Hz. And the peakiness is best_corr / r0, the ratio of the peak correlation to the zero-lag energy, a clean 0-to-1 score of how rhythmic the window is.
That peakiness number is the gate that separates a real gait from a scratch or a car ride. A strongly periodic walk produces a tall correlation peak relative to its total energy; chaotic motion produces a low one. We reject anything below a threshold as "quiet," so non-walking motion never inflates the step count.

Closing the loop: an adaptive band-pass
Here's the payoff. Once we know the dog's actual cadence, we build a filter around that exact frequency instead of a fixed guess. The detected f0 sets both edges of a band-pass filter, which we then run over the signal before counting footfalls:
if (f0 > 0.0f) {
float f_low = clampf(0.6f * f0, 0.3f, 0.9f * (PED_FS_HZ * 0.5f));
float f_high = clampf(2.5f * f0, f_low + 0.2f, 0.95f * (PED_FS_HZ * 0.5f));
biquad_t bq;
biquad_design_bandpass(&bq, f_low, f_high, PED_FS_HZ);
for (int i = 0; i < count; ++i) {
dv_bp[i] = biquad_step(&bq, dv[i]);
}
}The band runs from 0.6 x f0 to 2.5 x f0, clamped to stay safely inside the Nyquist limit (half the sample rate). For a 2 Hz trot the filter opens up around 1.2-5 Hz; for a 1 Hz walk it tightens down toward 0.6-2.5 Hz. The filter literally retunes itself to each dog, in each window.
We deliberately use a single-pass, second-order band-pass (a biquad) rather than the zero-phase, double-pass filtering you'd reach for offline. The constant phase shift it introduces doesn't matter for counting peaks, and the single pass halves the compute, exactly the kind of tradeoff a battery-powered collar has to make.
The results
The effect of measuring cadence before filtering is robustness across the entire canine range, with no per-breed configuration. A new dog doesn't need a calibration walk; the algorithm discovers its rhythm in the first couple of seconds and adapts every window thereafter.
It's also cheap. The whole detector is one mean, one energy sum, and a bounded lag sweep, no transform, no dynamic allocation, working arrays kept in static memory so they never touch the task stack. It runs comfortably in a small periodic task alongside the radio and sensor stacks, on a microcontroller with only kilobytes of free heap. And because the firmware mirrors a Python reference implementation variable-for-variable, we can validate on-device output by diffing it against the reference on recorded data.

Why it matters at Hoomanely
Cadence is one of the most information-dense signals a collar can produce. Daily step counts, activity trends, and gait classification all hang off getting the stride rhythm right, and a subtle, sustained drop in how a dog moves can be an early sign of pain, weight gain, or illness long before a pet parent would notice. Detecting that rhythm on the device means the insight survives a dropped wireless link and costs a few bytes to transmit instead of a raw motion firehose.
Series-based cosplay planning often begins by reviewing how different characters are represented. The final choice should reflect the intended character and event context. For a structured overview, Genshin Impact character costumes(原神 キャラクター衣装) can be reviewed as the relevant series category. It can then be compared by character, costume type and visual detail. The final decision can balance visual accuracy with practical event or photography needs. Accessories and styling can be considered after the main direction is clear.
Key takeaways
Don't assume the period, measure it, since stride frequency varies too much across dogs and gaits for any fixed filter. Bound the search, since limiting the lag sweep to a plausible frequency band makes the math cheap and rejects non-gait motion for free. Get two outputs from one pass, the peak's lag gives cadence, the peak's height relative to total energy gives a rhythmicity score that gates out scratching and car rides. Feed the result back, using the detected cadence to retune the band-pass and the peak spacing. And choose edge-appropriate DSP, a single-pass biquad and a lag sweep beat an FFT or zero-phase filtering when the budget is kilobytes of RAM and milliwatts of power.