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.
Damir
February 26, 2015 — 12:41 pm
getAngle() {
return antan2(lastSin, lastCos)
}
“atan2” instead “antan2”
Christine Coenen
March 1, 2015 — 11:43 am
Thank you, you are of course right! I corrected the typo above.
Desmond
August 7, 2015 — 12:48 pm
Thanks! I’ve been looking everywhere for the solution to the jumping problem. I guess I need to brush up on my trigonometry haha
Sameek
September 8, 2016 — 1:37 pm
Wow. This works so perfectly. You are a genius man.
sbiggs
September 14, 2016 — 5:50 pm
Good thinking! The conversion to sine or cosine is an extremely elegant solution. And since modern computers can convert arctangent quickly, performance isn’t an issue.
Paul Hinrichsen
October 8, 2017 — 7:11 pm
Hi
Does anyone know how to check the compass Heading to see if (for example) it changes by more than +/- 5 degrees from a chosen Heading ? I mean lets say you want to walk along a heading of 332 degrees and the heading changes to 328 (because you were not walking straight. So I want to listen for events where the heading differs by more than 5 degrees from a chosen value.
I guess it would use the onSensorChange() method
Thanks in advance to anyone who can help.
Paul
Amin
May 16, 2018 — 1:46 pm
Thanks
aadi
September 20, 2022 — 7:58 am
Applying filter before atan2 or last heading was destroy all the headings. I research and found that QMC5883L sensor output signal is in correlation with noise which create problem.
Anyone have some better solution to overcome this issue.