Smooth compass needle in Android (or any angle) with low-pass filter

Getting my first compass inside a native Android app right was a bit of an odyssey. The orientation sensor I knew is deprecated quite a while. And the suggested method of using SensorManager.getOrientation got me a very jumpy and unsteady compass needle.

From the good old days of mouse following flash movies I knew I had to smooth the heading angle. So I wrote something like this pseudo code, which of course gave me some errors:

smoothingFactor = 0.9
onSensorChanged(angle) {
lastAngle = smoothingFactor * lastAngle  + (1-smoothingFactor) * angle
}

This type of smoothing numerical values may work well with pixel values but does fail with angles. Whenever the compass was close to pointing north I got this really odd behavior of a spinning and jumping needle.

Why the smoothing does not work

When you think about angle values being cyclic this behavior is not a big surprise. I will use small examples to make this evident. For simplicity the smoothing factor is set to 0.5:

lastAngle = 0°
newAngle  = 90°----
lastAngle = 0.5 * 0° + 0.5 * 90° = 45°

This is pretty much what you expect. The next example however, isn’t:

lastAngle = 358°
newAngle  = 0°
----
lastAngle = 0.5 * 358° + 0.5 * 0° = 179°

Nope, this is odd. 358° and 0° are really close, nevertheless the algorithm decides to smooth it to 179° – a value far off from the others. It does explain the jumping and spinning when the angle is close to zero.

So what to do now? Trigonometry to the rescue!

As you may know you can express angle values through their sine and/or cosine values. And you can convert these sine and cosine values back to their corresponding angle value, by using the atan2 method.

atan2(sin(angle), cos(angle)) = angle

This becomes very handy because now we can smooth the angles sine and cosine values individually (no jumping here, because in relation to a full 360° circle they begin where they end) and get the smoothed angle back whenever we want, by using the atan2 method.

onSensorChanged(angle) {
     lastSin = smoothingFactor * lastSin + (1-smoothingFactor) * sin(angle)
     lastCos = smoothingFactor * lastCos + (1-smoothingFactor) * cos(angle)
}

getAngle() {
     return atan2(lastSin, lastCos)
}

And that’s it. Now you can smooth angle values without any jumping. Please note that in most programming languages you have to pass angle values to those trigonometric functions in radians, rather than in degrees. Also, thanks SharkAlley for for his or her explanation on stackoverflow.

Where is the low-pass filter in all this?

As it turned out this algorithm I have been using for years is basically a low-pass filter. It can be refactored a bit, but basically it uses similar code.

« »