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

Gravity separation and linear acceleration

"Every waking moment I feel a relentless force hurling me toward the sky at nine point eight meters per second squared. My users call this sitting still."

A Perpetually Falling AI Agent

Prerequisites

This section assumes you know what a three-axis accelerometer reports and how body and world frames relate, both developed earlier in this chapter (Chapter 23). The idea that a sensor measures a physical quantity you did not directly ask for comes from Chapter 2. The low-pass and high-pass filters used here are built from scratch in Chapter 6, and the orientation estimate we lean on is the subject of Chapter 24.

The Big Picture

An accelerometer never measures "how the device is moving." It measures the total force per unit mass pressing on its tiny proof mass, and gravity is by far the loudest voice in that measurement. On a phone lying still, roughly \(9.8\) of the \(9.8\ \mathrm{m/s^2}\) it reports is gravity and none of it is motion. Almost every motion task you will build, step counting, gesture spotting, harsh-braking detection, fall alarms, depends on splitting that reading into the part caused by the Earth and the part caused by whatever the user actually did. This section is about that split: why it is genuinely ambiguous, the two ways people solve it, and the price each way charges.

What an accelerometer actually reports: specific force

An accelerometer measures specific force, the acceleration it feels relative to free fall, expressed in its own body frame. Write the true acceleration of the device in a fixed world frame as \(\mathbf{a}^w\), gravity as \(\mathbf{g}^w = [0,0,-9.81]^\top\ \mathrm{m/s^2}\), and let \(C^b_w\) be the rotation that carries world-frame vectors into the body frame. The reading is

\[ \mathbf{a}_m = C^b_w\,(\mathbf{a}^w - \mathbf{g}^w) + \mathbf{b} + \boldsymbol{\eta}, \]

where \(\mathbf{b}\) is a slowly drifting bias and \(\boldsymbol{\eta}\) is noise (both handled in Section 23.4). Set the device at rest, so \(\mathbf{a}^w = \mathbf{0}\), and the reading collapses to \(-C^b_w\mathbf{g}^w\): a vector of magnitude \(9.81\) pointing up in the body frame. This is the fact that trips up every beginner. A stationary device reports a full \(1\,g\) upward, because the table pushing it up is a real force, while a device in genuine free fall reports zero. The instrument is not confused; it is telling the literal truth about force, which is subtly not the question a motion model wants answered.

The quantity we usually want is linear acceleration, the dynamic part \(\mathbf{a}^w\) caused by the user, with gravity removed. Rearranging the equation gives the clean target,

\[ \mathbf{a}^w = C^w_b\,(\mathbf{a}_m - \mathbf{b}) + \mathbf{g}^w, \]

which says: rotate the (debiased) reading back into the world frame and add gravity back in, cancelling the \(+9.81\) that the world frame's own gravity term contributes. Everything hard about this section hides inside \(C^w_b\), the orientation. If you knew orientation perfectly, gravity separation would be a subtraction. You never do, so it is not.

Key Insight

Gravity separation is fundamentally an underdetermined problem from a single accelerometer. One reading is three numbers; the unknowns are three components of linear acceleration plus (through orientation) two tilt angles that fix where "down" is. A tilted-but-still device and an upright-but-accelerating device can produce the identical reading. Nothing in one instantaneous sample distinguishes them. Every method below breaks the tie by adding an assumption: either about frequency content, or about orientation supplied by a second sensor.

The frequency trick: low-pass gravity estimation

The cheapest method exploits a lucky separation of timescales. For most human and vehicle motion, gravity in the body frame changes slowly, because it only moves when the device reorients, while genuine linear acceleration is transient: a footfall, a brake tap, a wrist flick lasts a fraction of a second. So a heavily smoothed copy of the accelerometer signal is a decent estimate of the gravity direction, and whatever is left after you subtract it is linear acceleration. Concretely, run a low-pass filter to get \(\hat{\mathbf{g}}_t\) and take the residual:

\[ \hat{\mathbf{g}}_t = \alpha\,\hat{\mathbf{g}}_{t-1} + (1-\alpha)\,\mathbf{a}_{m,t}, \qquad \mathbf{a}^{\text{lin}}_t = \mathbf{a}_{m,t} - \hat{\mathbf{g}}_t. \]

The single-pole smoothing factor \(\alpha\) sets the cutoff: values near \(1\) (a cutoff well below \(1\ \mathrm{Hz}\), commonly \(0.2\) to \(0.5\ \mathrm{Hz}\)) trust that gravity is nearly constant. Listing 23.3 implements exactly this on a synthetic trace and recovers the injected motion.

import numpy as np

def separate_gravity(accel, fs, cutoff_hz=0.3):
    """accel: (N,3) specific force in m/s^2, fs: sample rate (Hz).
    Returns (gravity_est, linear_accel) via a one-pole low-pass."""
    dt = 1.0 / fs
    rc = 1.0 / (2 * np.pi * cutoff_hz)
    alpha = rc / (rc + dt)                 # smoothing factor in [0,1)
    g = accel[0].copy()                    # seed with first sample
    grav, lin = np.empty_like(accel), np.empty_like(accel)
    for t, a in enumerate(accel):
        g = alpha * g + (1 - alpha) * a    # slow-moving gravity
        grav[t], lin[t] = g, a - g         # residual is the motion
    return grav, lin

fs = 100.0
t = np.arange(0, 6, 1 / fs)
gravity = np.tile([0, 0, 9.81], (t.size, 1))          # device held upright
motion = np.zeros_like(gravity)
motion[(t > 2) & (t < 2.4), 0] = 4.0                  # a 0.4 s forward shove
accel = gravity + motion + np.random.normal(0, 0.05, gravity.shape)
grav_hat, lin_hat = separate_gravity(accel, fs)
print("peak recovered forward accel:", lin_hat[:, 0].max().round(2), "m/s^2")
Listing 23.3. A one-pole low-pass separates the quasi-static gravity vector from a transient forward shove. The printed peak recovers close to the injected \(4.0\ \mathrm{m/s^2}\); the residual lin_hat is what a downstream motion model consumes. Note the warm-up: the first tenth of a second is unreliable while the filter's memory fills.

As Listing 23.3 shows, this works and costs almost nothing, which is why it ships in millions of pedometers. But its single assumption, "gravity is the low-frequency part," fails in two predictable ways. First, any sustained linear acceleration (a car pulling away from a light for several seconds, an elevator, a centrifuge) is slow enough that the low-pass swallows it into the gravity estimate and it silently vanishes from the residual. Second, a genuinely fast reorientation (a rolling wrist, a phone yanked from a pocket) is fast enough that the low-pass cannot track it, so a chunk of real gravity leaks into the linear channel as a phantom acceleration. The method has no way to tell these cases apart from the ordinary case, because it never knows which way is down.

Common Misconception

It is tempting to fix leakage by simply pushing the cutoff lower, reasoning that a slower filter tracks gravity more purely. This trades one failure for the other. A lower cutoff does reject fast rotation better, but it also swallows more sustained motion and lengthens the warm-up transient, so a hard brake or a long acceleration disappears more completely. There is no single \(\alpha\) that is safe for both a still tilt and a long push; the frequency trick is a compromise, not a solution. When both failure modes matter, you need orientation, not a better cutoff.

The principled fix: subtract gravity through orientation

The robust approach removes gravity geometrically. If a fusion filter gives you an orientation estimate \(\hat{C}^w_b\) at each timestep, typically by blending the accelerometer's slow, absolute sense of "down" with the gyroscope's fast, drift-prone sense of rotation, then you know exactly where gravity points in the body frame and can subtract the correct vector regardless of frequency. Rotate the reading to the world frame and add gravity back:

\[ \mathbf{a}^{\text{lin},w}_t = \hat{C}^w_{b,t}\,\mathbf{a}_{m,t} + \mathbf{g}^w. \]

Because \(\hat{C}^w_b\) is updated by the gyroscope at the sensor's full rate, this tracks a fast wrist roll without leaking gravity, and because it does not treat "slow" as a proxy for "gravity," a sustained brake survives in full. This is why the accelerometer and gyroscope are almost never used alone: the gyro fixes the low-pass method's rotation failure, and the accelerometer keeps the gyro's integrated angle from drifting away. That mutual rescue is the complementary filter, and building it properly is the whole of Chapter 24. The cost is honest: you now depend on an orientation estimate that has its own error budget, and near-free-fall or during violent shaking the accelerometer's "down" reference degrades, so orientation (and therefore your gravity subtraction) briefly gets worse exactly when motion is most extreme.

In Practice: automotive crash and harsh-event telematics

A usage-based-insurance app runs on a phone loose in a cupholder, and its whole business is scoring harsh braking, hard cornering, and collisions from the phone's accelerometer. The phone sits at some unknown, arbitrary tilt, so the raw reading mixes a large gravity component into every axis. A naive low-pass gravity split works for a quick brake tap but fails on the two events that matter most for a claim: a long, hard deceleration (which the low-pass eats as gravity, under-reporting the event) and the phone sliding and reorienting during the crash itself (which injects a phantom spike). Production telematics stacks instead fuse the gyroscope to hold orientation through the event, subtract gravity geometrically, and then re-express linear acceleration in the vehicle frame so that "forward" braking and "lateral" cornering can be scored separately. The lesson generalizes: whenever the device orientation is unknown and the motion can be sustained, geometric gravity removal is not a luxury, it is the difference between a defensible number and a fabricated one.

The Right Tool

You rarely write either method by hand in production. Mobile platforms already run a tuned inertial fusion and expose both channels directly: on iOS, Core Motion's CMDeviceMotion gives you gravity and userAcceleration as separate vectors; on Android, TYPE_GRAVITY and TYPE_LINEAR_ACCELERATION do the same. Off-device, a maintained fusion library collapses the orientation-plus-subtraction pipeline to a few lines:

from ahrs.filters import Madgwick
import numpy as np
q = Madgwick(gyr=gyro, acc=accel, frequency=100.0).Q   # (N,4) orientation
# rotate each sample's gravity out using the estimated quaternion
lin = accel - np.array([rotate_gravity(qi) for qi in q])
Listing 23.4. A library-backed orientation filter (here Madgwick from the ahrs package) turns the from-scratch complementary-filter-plus-projection pipeline, easily 60 to 100 lines with quaternion bookkeeping, into roughly 3. The platform APIs reduce it further to a single subscription. The library owns the rotation math; you still own the choice of gains and the decision to trust it during extreme motion.

As Listing 23.4 shows, the geometric method's line count is dominated by orientation math that a library already solves; reach for the built-in channels first, and hand-roll the low-pass only when you are on a bare microcontroller with no gyroscope.

Choosing a method, and what leaks downstream

The decision reduces to two questions. Do you have a usable gyroscope, and can your motion sustain acceleration for more than a second or so? If you have a gyro and motion is sometimes sustained (vehicles, elevators, machinery, any device at unknown tilt), use geometric subtraction. If you are gyro-less or power-starved and your motion is purely transient (a wrist-worn step counter, a tap detector), the low-pass is cheaper and adequate, provided you accept that it will misbehave during long pushes and fast rotations. A useful hybrid, common in wearables, low-passes for gravity but gates the estimate: it freezes \(\hat{\mathbf{g}}\) whenever the gyroscope reports significant rotation, so real gravity cannot leak in during a flick.

One warning about what happens after separation. It is tempting to integrate linear acceleration once for velocity and twice for position, "the phone knows how far it moved." It does not, for long. Any residual gravity your separation leaves behind, even a stubborn few percent, is a near-constant bias on linear acceleration, and integrating a constant bias twice grows position error quadratically in time. A \(0.1\ \mathrm{m/s^2}\) leftover becomes half a metre of phantom displacement after just three seconds. This is the drift wall that makes standalone inertial navigation hopeless without external corrections, and it is why Chapter 24 spends its energy on zero-velocity updates and fusion rather than on ever-cleaner gravity removal. Clean separation buys you good features for a classifier; it does not buy you a trustworthy trajectory. Feeding the separated linear-acceleration channel, rather than raw specific force, into an activity or gesture model is one of the most reliable preprocessing wins in this domain, a point Section 23.7 makes concrete.

Exercise

Extend Listing 23.3 with a second injected event: a sustained \(2.0\ \mathrm{m/s^2}\) forward acceleration lasting three seconds (a car pulling away), and a fast \(90^\circ\) reorientation of the gravity vector over \(0.3\) seconds (a phone flipped in the hand). Run the low-pass separator on both and plot the linear-acceleration residual. Confirm the two documented failure modes: the sustained event decays toward zero as the filter absorbs it into gravity, and the flip produces a phantom spike in the linear channel. Then sweep the cutoff from \(0.1\) to \(1.0\ \mathrm{Hz}\) and show that no single value fixes both.

Self-Check

1. A phone lies flat and perfectly still. Why does its accelerometer report a vector of magnitude \(9.81\) rather than zero, and in which direction does that vector point in the body frame?

2. Explain, in terms of the timescale assumption, why the low-pass method loses a five-second-long car acceleration but a geometric (orientation-based) method does not.

3. Your gravity separation leaves a constant residual of \(0.1\ \mathrm{m/s^2}\). Roughly how large is the resulting position error after \(5\) seconds of double integration, and why does the error grow with the square of time?

What's Next

In Section 23.4, we confront the term we quietly carried along as \(\mathbf{b}\): the accelerometer's bias, together with scale-factor and axis-misalignment errors. These corrupt gravity separation directly, a biased sensor mislocates "down," so before trusting any linear-acceleration channel you need to calibrate the instrument that produced it.