Correcting a Spinning Collar: Orientation Fusion at the Edge

Correcting a Spinning Collar: Orientation Fusion at the Edge

A pet collar is the worst-behaved sensor mount in consumer hardware. It rotates, flips, rides up the neck, and settles at a different angle every time the dog shakes off after a nap. Yet the very first thing our activity pipeline must do is count steps from the inertial sensor riding on that collar — and a step counter that assumes "up is up" falls apart the moment the device twists ninety degrees. At Hoomanely, we treat the collar's unpredictable orientation as a solved problem, not an excuse for noisy data. This post walks through how we recover a stable, gravity-aligned signal from an arbitrarily mounted inertial sensor using a closed-form solution to a sixty-year-old aerospace problem — and why that bit of linear algebra is the quiet foundation under every daily step count we report.

The Problem: Raw Axes Are Meaningless

When a pedometer runs on a phone in your pocket, it can lean on a comforting assumption: the device is roughly upright most of the time, so the vertical axis carries most of the walking signal. A collar grants no such luxury.

The inertial sensor measures acceleration along its own three axes — X, Y, Z fixed to the circuit board. But the board's relationship to the dog's body changes constantly. The same trot can show up mostly on the Z axis for one dog, mostly on X for another, and smeared across all three after the collar slips.

If you naively take the magnitude of the acceleration vector to dodge the orientation problem, you throw away the directional structure that makes gait detectable in the first place. Footfalls become indistinguishable from a vigorous head-scratch. The orientation problem is not a nuisance to filter out — it is the first computation in the pipeline.

The Approach: Borrow From Spacecraft

The cleanest way to make the signal orientation-independent is not to filter harder — it is to rotate the data into a canonical frame before we analyze it. We want every sample expressed as if the sensor had always been mounted in one ideal posture, with gravity pointing down a known axis.

Gravity is the gift that makes this possible. A roughly stationary sensor always feels a 1 g pull toward the earth, regardless of how the collar is twisted. If we can estimate that gravity direction in the sensor's own frame, we know exactly how the sensor is tilted — and therefore exactly how to rotate the data to undo that tilt.

Finding the single rotation that best aligns a set of measured vectors to a set of reference vectors is a classic problem in aerospace attitude determination, formalized by Grace Wahba in 1965. Wahba's problem asks for the rotation matrix that minimizes the alignment error between two sets of paired vectors. Spacecraft use it to align star-tracker readings; we use it to align a dog's collar. The elegant part is that it has a closed-form solution via the singular value decomposition (SVD) — no iterative solver, no gradient descent, just one matrix factorization.

The Process: Four Moves From Raw to Aligned

The orientation correction in our tracker firmware's companion analysis stage breaks into four deliberate steps. Here is how each one works, with the real code pulled straight from our implementation.

Step 1 — Isolate gravity with a very low-pass filter. Walking, scratching, and bumps all live above ~1 Hz. Gravity is, by definition, the part of the signal that barely changes. We run a 4th-order zero-phase low-pass at 0.1 Hz over each axis to extract a clean gravity estimate per sample.

92	    b, a = _butter_lp(cfg.gravity_lp_hz, cfg.fs, order=cfg.zc_filter_order)
93	    g_est = filtfilt(b, a, accel, axis=0)
94	
95	    norms = np.linalg.norm(g_est, axis=1)
96	    valid = np.abs(norms - 1.0) < cfg.sphere_tol_g

Step 2 — Reject samples where gravity is a lie. During a hard bounce or a sudden direction change, the measured vector is dominated by motion, not gravity, so its magnitude drifts away from 1 g. We keep only samples whose estimated gravity sits within a tolerance of unit length, and then we go one step further: we drop any sample whose surrounding 10-second window straddles a posture change.

98	    # 10 s posture-transition window: only accept samples whose surrounding
99	    # window has a high fraction of in-tolerance gravity.
100	    win = max(int(cfg.posture_window_s * cfg.fs), 1)
101	    if win > 1:
102	        kernel = np.ones(win) / win
103	        accept_rate = np.convolve(valid.astype(float), kernel, mode="same")
104	        valid = valid & (accept_rate > 0.8)

That convolution is a cheap moving average over the boolean "is this gravity trustworthy" flag. If fewer than 80% of the samples in the window are trustworthy, we treat the whole neighborhood as a transition (the dog lying down, standing up, reorienting) and exclude it from the rotation fit. We only want stable, gravity-dominated samples teaching us which way is down.

Step 3 — Solve for the rotation. With a set of trustworthy gravity vectors and a single target direction (we want gravity to map to the +Z axis), we solve Wahba's problem in closed form. The whole solver is six lines:

63	    if weights is None:
64	        weights = np.ones(len(v))
65	    B = (weights[:, None, None] * (w[:, :, None] @ v[:, None, :])).sum(axis=0)
66	    U, _, Vt = np.linalg.svd(B)
67	    M = np.diag([1.0, 1.0, np.linalg.det(U) * np.linalg.det(Vt)])
68	    return U @ M @ Vt

The B matrix accumulates the outer products of each measured vector with its target. The SVD factors it, and the M diagonal — using the determinants of U and Vt — guarantees the result is a proper rotation rather than a reflection. That single line is what stops the math from accidentally mirroring the signal, a subtle bug that would silently corrupt every downstream metric.

Step 4 — Project into the canonical frame. Finally, we apply the rotation to the entire stream and pull out the now-stable dorso-ventral component — the "up-and-down through the dog's body" axis where footfalls show up most cleanly.

110	        target = np.tile(np.asarray(cfg.target_axis, dtype=float), (valid.sum(), 1))
111	        # Each gravity vector should map to +Z; normalise to unit length first.
112	        v = g_est[valid] / norms[valid, None]
113	        R = _solve_wahba_svd(v, target)
114	        rotated = accel @ R.T
115	
116	    return rotated[:, 2]

Notice the guard built into the broader function: if too few valid gravity samples survive the gate, we fall back to the identity rotation rather than fit a wild transform to noise. A bad rotation is worse than no rotation, so the conservative default protects the metric when the dog simply hasn't held still.

The Results

Once the signal is in the canonical frame, everything downstream gets simpler and more honest. The dorso-ventral axis carries a clean, repeatable gait waveform whether the collar sat at twelve o'clock or rolled to three o'clock overnight. The step-detection stages that follow — zero-crossing detection, a stride-frequency band gate, and a periodicity check — operate on a signal that means the same thing for every dog and every mounting angle.

Crucially, this stage is deterministic and cheap. There is no learned model to drift, no per-device calibration ritual for the pet parent to perform, and no orientation assumptions baked into the hardware design. The collar can be clipped on however it lands, and the firmware-side capture plus this analysis stage absorb the messiness for us. That robustness is what lets us promise an activity number that holds up across a Chihuahua and a Great Dane.

Why It Matters at Hoomanely

Hoomanely exists to reinvent healthcare for pets — to replace a reactive, imprecise, stressful model of care with continuous, clinical-grade monitoring that surfaces problems before they become emergencies. Our devices form a Physical Intelligence ecosystem: interconnected sensors fused at the edge, feeding the Biosense AI Engine that turns raw signals into hyper-personalized, preventive insights.

Activity is one of the most powerful early-warning signals in that picture. A dog that quietly walks less each day may be developing joint pain, cardiac issues, or simply aging faster than its breed should. But that insight is only as trustworthy as the step count underneath it — and a step count built on un-corrected, orientation-dependent data would generate false alarms and miss real declines.

Orientation fusion is therefore not a nice-to-have flourish. It is the layer that makes the rest of the pipeline credible. Hoomanely's guiding principle is that every signal matters and every detail counts — and getting the very first transform right, before any AI ever sees the data, is exactly what that principle looks like in firmware. By solving the mounting problem in software, we keep our hardware comfortable for the pet and our metrics comparable across an entire population of dogs.

This orientation-correction stage sits at the front of the activity pipeline behind Hoomanely's collar-worn tracker, the entry point of our Physical Intelligence ecosystem. The edge device captures raw motion; this transform makes that motion comparable; the Biosense AI Engine turns it into preventive insight. It is a small piece of linear algebra doing quietly important work — and a good reminder that in pet health monitoring, the credibility of the AI begins with the very first line of signal processing.

Read more