Frame Rate-Independent Low-Pass Filter

Summary

If you want a frame rate-independent low-pass filter (an infinite impulse response, or IIR) to apply to a stream of data, use this formula:

filteredValue = oldValue + (newValue - oldValue) / (smoothing / timeSinceLastUpdate)

Background

Imagine that you have a stream of data coming in that has some noise in it. This could be the frame rate of a game, the mouse motion of the user drawing a line, the voltage measured across a resistor…whatever.

Dots plotting a noisy 'line' of thousands of points. The noise has a normal distribution around a softly-varying value with two sharp jumps in value.

You want to graph this value, but you want to hide the noise. One solution is a "low-pass filter".

A Simple Low-Pass Filter

Similar to an audio graphic equalizer with the treble dialed down, a low-pass filter muffles high-frequency (fast) changes to the signal. Unlike a moving average, it requires only three numbers to cover the entire spectrum of effect, from no-change-to-the-input to unyielding-snail-paced-graph.

Here's an example of using a low-pass filter to change values of an array, using JavaScript:

// values: an array of numbers that will be modified in place // smoothing: the strength of the smoothing filter; 1=no change, larger values smoothes more function smoothArray( values, smoothing ){ var value = values[0]; // start with the first input for (var i=1, len=values.length; i<len; ++i){ var currentValue = values[i]; value += (currentValue - value) / smoothing; values[i] = value; } }

All the power of the filter is in the line value += (currentValue - value) / smoothing. This finds the difference between the new value and the current (smoothed) value, shrinks it based on the strength of the filter, and then adds it to the smoothed value. You can see that if smoothing is set to 1 then the smoothed value always becomes the next value. If the smoothing is set to 2 then the smoothed value moves halfway to each new point on each new frame. The larger the smoothing value, the less the smoothed line is perturbed by new changes.

Try it yourself. The following green line is the smoothed value of our blue noise. Adjust the smoothing below and see the effects on the line. Find a value that you think looks smooth and nice, while still giving a good representation of what's going on in the blue.

smoothing =

A single line whose fit is based on the 'smoothing' value.

The Impact of Lower Frame Rates

If you're like me, you might have chosen a smoothing value somewhere between 20 and 60. The higher values smooth out the noise nicely, at the cost of a slight delay when representing the two instantaneous, sharp signal jumps.

However, we're not in a good place (yet). If our frame rate changes—the speed at which we're sampling the signal—the effect of our carefully-chosen smoothing can have drastically different effects.

The following graph shows the effects of 2x lowered frame rate (magenta), 5x lowered (red) and varying sample rate (yellow). A smoothing that works nicely at a high sampling rate is far too strong when the rate drops. (If you cannot see the differences between the lines, try turning up the smoothing above.)

Three lines plotted on top of the original line.

What we need is a formula that accounts for changes to the frame rate.

Achieving Frame Rate Independence

You may have noticed that there is a similar effect between increasing the smoothing value and lowering the sampling rate. In fact, it turns out that they are more than similar, they are equivalent. A doubling of the smoothing is the same as a halving of the rate. As such, only a minor tweak is needed to our smoothing formula:

value += (currentValue - value) / (smoothing / timeSinceLastSample);

Following are the various sampling rates with smoothing adjusted based on the rate. Although they vary slightly from one another (based on which of the horrendously noisy samples they latch onto) you can see that they generally match, regardless of the smoothing setting above:

Three lines plotted on top of the original line.

Finally, here is a code block similar to what I use:

var smoothed = 0; // or some likely initial value var smoothing = 10; // or whatever is desired var lastUpdate = new Date; function smoothedValue( newValue ){ var now = new Date; var elapsedTime = now - lastUpdate; smoothed += elapsedTime * ( newValue - smoothed ) / smoothing; lastUpdate = now; return smoothed; }