2

I'm trying to implement a low-pass filter for compass bearing input, and I've put together a basic function that looks like this:

var smoothing = 0.15;

function lowPass(degree) {
    var oldVal = $scope.compassBearing || 0;
    var smoothedValue = oldVal + smoothing * (degree - oldVal);
    return smoothedValue;
}

Works great, except for when the compass bearing passes north (ie. abruptly changes from 0 to 359 or vice-versa.

Has anyone run into a similar issue and, if so, how was it resolved?

opticon
  • 3,494
  • 5
  • 37
  • 61
  • What is the issue? `smoothValue` becoming e.g. 361, -2, etc? – dannyjolie Mar 20 '16 at 21:02
  • It's that the smoothing tries to 'smooth' the reading around to the other side - so instead of smoothing transitioning from, say, 356 -> 358 -> 0 -> 2, it tries to average out the two values so the compass rapidly spins around in the opposite direction to meet up with the new values. – opticon Mar 20 '16 at 21:06
  • Yeah, I figured that. Just tried your script myself, and saw your real issue. Transition from e.g. 359 to 10 goes "backwards", not via 0. It wasn't clear from the question. – dannyjolie Mar 20 '16 at 21:10
  • @SharkAlley uses trig to solve this http://stackoverflow.com/a/18911252/4602928 – Wainage Mar 20 '16 at 21:38

2 Answers2

4

Let's say you always want it to smoothen in the shortest path. So if the change is from 270 degrees to 5 degrees, it goes via north, then you have to check the values and see if the difference between them is more than 180. If so, then modify the function a bit.

If the difference is less than -180, then flip a few operators and add 360 to the mix. The same goes if the difference is more than 180. Finally check if the result is more than 360 or less than 0, and correct that.

function lowPass(degree) {
    var oldVal = $scope.compassBearing || 0;
    var smoothedValue;

    if(oldVal - degree < -180){
        // Invert the logic
      smoothedValue = oldVal - smoothing * (oldVal + 360 - degree);
      if (smoothedValue < 0){
        smoothedValue = smoothedValue + 360;
      }
    } else if (oldVal - degree > 180){
        smoothedValue = oldVal + (360 + degree - oldVal) * smoothing;
      if (smoothedValue > 360){
        smoothedValue = smoothedValue - 360;
      }
    }
    else {
      smoothedValue = oldVal + smoothing * (degree - oldVal);
    }   
    return smoothedValue;
}

It looks a mess, but I just played around with it. Hope it's what you needed.

dannyjolie
  • 10,959
  • 3
  • 33
  • 28
  • Awesome. That did it. I was just teasing this out but you beat me to it. Sunday hangovers make for some stunted logical thinking :) – opticon Mar 20 '16 at 21:48
1

There is a more elegant way to do this if you leverage complex math. Basically, with a complex number you can represent a spot on the compass.

enter image description here

If you want to "average" or "smooth" two compass readings, you can do that in the complex domain and then convert back to the angle domain.

With this, we can use the math js library and leave your function basically in the same form that you had:

var smoothing = .9;


function lowPass(degree, oldVal) {
    // var oldVal = $scope.compassBearing || 0;
    var complexOldVal = math.complex({r:1, phi:oldVal * (math.pi/180)});
    var complexNewVal = math.complex({r:1, phi:degree * (math.pi/180)});    
    var complexSmoothedValue = math.chain(complexNewVal)
                                   .subtract(complexOldVal)
                                   .multiply(smoothing)
                                   .add(complexOldVal)
                                   .done();
    var smoothedValue = math.arg(complexSmoothedValue) * (180/math.pi);
    if (smoothedValue < 0) { return 360 + smoothedValue; }
    return smoothedValue;
}

document.write(lowPass(20, 30)+'<br>');
// 20.99634415156203
document.write(lowPass(10, 350)+'<br>');
// 8.029256754215519

You can fiddle with it here: http://jsbin.com/zelokaqoju/edit?html,output

Bob Baxley
  • 3,551
  • 1
  • 22
  • 28
  • I have to admit, I can really geek out on this solution over mine. But for this one simple application I think bringing in an external library, and alienating the code for those unfamiliar with radians, might be a bit overkill. Still love it though :) – dannyjolie Mar 20 '16 at 22:29
  • I hear you and generally agree... One advantage of this approach is it easily scales if you want to do fancier smoothing. Doing anything other than averaging in the angle domain is likely to lead to undesired results. – Bob Baxley Mar 20 '16 at 22:31
  • Agree on all counts :P Picked danny's and implemented it purely for readability. This solution appears elegant, but it's essentially a black box to me! – opticon Mar 21 '16 at 02:24