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. (That orientation correction is its own story, covered in an earlier post.) 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:
42 // Cadence search bounds. 1.0 Hz is a slow walk's DV channel f0 (stride
43 // ~0.5 Hz, DV doubles to 1 Hz); 8 Hz is a hard gallop. Outside this
44 // band we treat as quiet. Was 0.5-8 Hz with a 4-s window — but the
45 // autocorrelation needs N >= 2*lag_max samples to be meaningful, and
46 // after dropping to a 2-s ring (200 samples) the floor had to come up
47 // to 1 Hz so lag_max=100 fits.
48 #define PED_F_MIN_HZ 1.0f
49 #define PED_F_MAX_HZ 8.0f
50 #define PED_LAG_MAX ((int)(PED_FS_HZ / PED_F_MIN_HZ)) // 100
51 #define PED_LAG_MIN ((int)(PED_FS_HZ / PED_F_MAX_HZ)) // 12
At 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:
184 // Mean-center
185 float mean = 0.0f;
186 for (int i = 0; i < n; ++i) mean += dv[i];
187 mean /= (float)n;
188
189 // R(0) (variance × n)
190 float r0 = 0.0f;
191 for (int i = 0; i < n; ++i) {
192 float v = dv[i] - mean;
193 r0 += v*v;
194 }
195 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:
197 // Sweep R(lag) for lag in [PED_LAG_MIN, PED_LAG_MAX], pick max.
198 int best_lag = 0;
199 float best_corr = 0.0f;
200 for (int lag = PED_LAG_MIN; lag <= PED_LAG_MAX; ++lag) {
201 float r = 0.0f;
202 for (int i = 0; i + lag < n; ++i) {
203 r += (dv[i] - mean) * (dv[i + lag] - mean);
204 }
205 if (r > best_corr) { best_corr = r; best_lag = lag; }
206 }
207 if (best_lag == 0) return 0.0f;
208
209 *peakiness_out = best_corr / r0;
210 return PED_FS_HZ / (float)best_lag;
211 }
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:
404 if (f0 > 0.0f) {
405 float f_low = clampf(0.6f * f0, 0.3f, 0.9f * (PED_FS_HZ * 0.5f));
406 float f_high = clampf(2.5f * f0, f_low + 0.2f, 0.95f * (PED_FS_HZ * 0.5f));
407 biquad_t bq;
408 biquad_design_bandpass(&bq, f_low, f_high, PED_FS_HZ);
409 for (int i = 0; i < count; ++i) {
410 dv_bp[i] = biquad_step(&bq, dv[i]);
411 }
The band runs from 0.6 × f0 to 2.5 × 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 — which is exactly the kind of tradeoff a battery-powered collar has to make. The filtered signal then goes to a local-maximum peak detector whose minimum peak spacing is also derived from f0, so it can't double-count within a single stride.
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.

Hoomanely is reinventing healthcare for pets — replacing reactive, imprecise care with continuous, clinical-grade monitoring that catches problems early. Our devices form a Physical Intelligence ecosystem: sensors fused at the edge, feeding the Biosense AI Engine that turns raw signals into personalized, preventive insights.
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.
Our guiding principle is that every signal matters and every detail counts. Finding the beat — accurately, cheaply, and for every dog regardless of size — is a small piece of signal processing that makes the larger promise of preventive care trustworthy.
Key Takeaways
- Don't assume the period — measure it. Stride frequency varies too much across dogs and gaits for any fixed filter; autocorrelation recovers the actual cadence per window.
- Bound the search. 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. Use the detected cadence to retune the band-pass and the peak spacing, so the whole counter adapts to each dog automatically.
- 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.
Author's Note
This cadence detector lives inside the on-device pedometer of the collar-worn tracker that anchors Hoomanely's Physical Intelligence ecosystem. It sits between the orientation-correction stage and the gait classifier — the quiet bit of math that turns a wobble of acceleration into a measured rhythm, and a measured rhythm into something the Biosense AI Engine can reason about. No FFT, no calibration walk, no cloud round-trip: just the beat, found on the dog.