본문 바로가기
프로세싱+아두이노+안드로이드

Moving Average Filter (Physical Computing)

by YJHTPII 2022. 6. 21.
반응형

https://makeabilitylab.github.io/physcomp/advancedio/smoothing-input.html#noisy-input

 

L1: Smoothing Input

A learning resource for prototyping interactive digital-physical systems

makeabilitylab.github.io

 

 

 

Lesson 1: Smoothing Input

TABLE OF CONTENTS

  1. Noisy input
  2. Moving window filters
    1. Moving average filter
      1. Arduino implementation
      2. Arduino code
      3. Simple C++ class
    2. Weighted moving average
    3. Exponential moving average
      1. Arduino EWMA implementation
    4. Moving median filter
    5. Why use a median filter?
  3. Other filters
  4. Activity
  5. Arduino filtering libraries
  6. Resources

지금쯤이면 아셨을텐데 아날로그 입력 데이터에 노이즈가 있을 수 있다는 사실을. 어떻게 입력을 부드럽게 할 수 있으며 어떤 요소를 고려해야 할까요?

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/MovingAverageFilterWindowSize10-Optimized.mp4

Video Smoothing an analog input signal using a moving average filter with window size of 10. The raw analog input is shown in blue; the smoothed data is shown in red. Graph made with the built-in Arduino Serial Plotter.

 

This is a big, complex question. Indeed, there is an entire field called digital signal processing (DSP), which explores techniques to sample, smooth, or otherwise transform signals, like sensor data, into more usable forms. In signal processing, filters refer to algorithmic methods and/or hardware approaches to remove or attenuate unwanted components of a signal.

In this lesson, we will cover a class of digital filters called smoothing algorithms (aka signal filters), why they’re helpful, and potential tradeoffs in their implementation and use.


DIVE DEEPER:

DSP is a vast, complex area but even simple signal processing techniques like those covered in this lesson are helpful. If you’d like to dive deeper into DSP, see our Signal Processing lessons, which introduce quantization and sampling, signal comparisons, and frequency analysis. For a more technical introduction to digital filters, see Chapter 14: Introduction to Digital Filters in Steven W. Smith’s book The Scientist and Engineer’s Guide to Digital Signal Processing. We also recommend Jack Schaelder’s interactive primer on DSP.


Noisy input

When reading sensor data using analog input pins on a microcontroller (e.g., via analogRead on the Arduino), there are many sources of noise, including electromagnetic interference, sensor noise, mechanical noise (for electro-mechanical sensors like potentiometers), stray capacitance, unstable voltage sources, and/or small imperfections in the ADC. Oh my!

Even with a simple potentiometer, we can observe noise on our input pin. In the video below, we are not touching the potentiometer and yet the analog input is oscillating between 142 and 143 (0.694V and 0.699V)—shown as the blue line. You may have experienced this too in your own potentiometer-based projects or in the Arduino potentiometer lesson. In this case, we fixed this “input noise” by smoothing the signal using a moving average filter—shown in red—which we will describe in this lesson.

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/PotentiometerOscillatingWithNoInputButFixedWithMovingAverage-Optimized.mp4

Video. In this video, we’re graphing the raw analog input (blue line) from a potentiometer along with a “smoothed” version (red line). Although we’re not touching or using the potentiometer, the analog input is oscillating between 142 and 143 (0.694V and 0.699V). We smooth this noise using a moving average filter (window size = 10)—shown in red. Note that, depending on the oscillation pattern, a different window size or smoothing approach may be necessary. Read more about potentiometer noise here. Graph made with the built-in Arduino Serial Plotter.

In addition to smoothing a potentiometer input signal with a digital filter (a software solution), we could, instead, use a hardware solution: add in a small ceramic capacitor (0.1µF or 0.47 µF) from the potentiometer wiper to ground. However, the focus of this lesson is on software solutions (putting the digital in DSP).

Moving window filters

The most common digital filters use a moving window (or buffer) to smooth a signal in realtime. Typically, larger window sizes result in a “smoother” but more distorted signal. Larger windows also incur “lag”—the filtered signal’s responsiveness to changes in the raw signal—and require more storage and computation time.

As each sensor and physical computing project is unique, we encourage you to experiment with different smoothing algorithms and window sizes to achieve your desired behavior.


SPECIAL CASE: DATA LOGGING

Note that if you’re logging data (e.g., to a storage card or the cloud) for research or data experiments, it’s often best to first capture and transmit the raw data so that you can experiment with signal processing approaches post hoc (e.g., offline in Jupyter Notebook). Through offline analysis with these raw logs, you can determine an ideal sampling frequency and filtering approach for your particular hardware components and problem domain. Then, you can implement this approach in your field-deployed system, which can result in reduced computational overhead, bandwidth, power, etc..


Moving average filter

The most common filter in DSP is the moving average filter (or moving mean filter), which slides a window of size N over a raw signal, computes the average over that window, and uses this average as the smoothed value.

MA=X1+X2+…+XNN

This filter is a type of low-pass filter because it smooths out (eliminates) the high frequency oscillations in the signal.

You can control the filter’s performance by tweaking the size of the sliding window. The animation below demonstrates a sliding window of size 3. The blue line corresponds to the raw input signal; the orange line, the smoothed filter output. For illustrative purposes, we only show the sliding window applied to a subset of data.

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/MovingAverageFilter_PowerPointAnimation_TrimmedAndCropped.mp4

Video This video illustrates a moving average filter of window size 3 over a subset of data. Animation made in PowerPoint.

Despite its simplicity, a moving average filter often all you will need in your physical computing projects. This, of course, depends on your use context and the underlying sensors + circuit setup. Thus, we encourage you to play around with various smoothing approaches and tuning parameters. If you’re sampling a sensor 20 times per second (~20Hz), then a window size of 10 will capture roughly 500ms of data. So, it may be useful to think in terms of time rather than samples.

Below, we compute three different moving average filter window sizes: 5, 10, and 20 and show the resulting filter output in red, green, and yellow, respectively.

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/MovingAverageWithThreeWindowSizes-Optimized.mp4

Video This video graphs raw analog input (blue) and filtered output from three different moving average window sizes: 5 (red line), 10 (green), and 20 (yellow). To produce this video, we used this code and the Arduino Serial Plotter. You should try it yourself!

ARDUINO IMPLEMENTATION

The official Arduino signal smoothing tutorial uses a moving average filter. Cleverly, their code uses an optimization (which we borrow below) to avoid iterating over the entire window to compute each new average. Instead, we simply subtract the least recent reading in our sliding window from a running total. The code also uses a circular buffer to eliminate needless memory allocations, which is important on constrained systems like microcontrollers.

// read the sensor value
int sensorVal = analogRead(SENSOR_INPUT_PIN);

// subtract the last reading from our sliding window
_sampleTotal = _sampleTotal - _samples[_curReadIndex];

// add in current reading to our sliding window
_samples[_curReadIndex] = sensorVal;

// add the reading to the total
_sampleTotal = _sampleTotal + _samples[_curReadIndex];

// calculate the average:
_sampleAvg = _sampleTotal / SMOOTHING_WINDOW_SIZE;

// advance to the next position in the array
_curReadIndex = _curReadIndex + 1;

// if we're at the end of the array...
if (_curReadIndex >= SMOOTHING_WINDOW_SIZE) {
  // ...wrap around to the beginning:
  _curReadIndex = 0;
}

 

ARDUINO CODE

You can find our full implementation on GitHub as MovingAverageFilter.ino, which is also displayed below:

/*
 * Example of smoothing input on A0 using a moving average filter.  
 *
 * This example is based on:
 * https://www.arduino.cc/en/Tutorial/BuiltInExamples/Smoothing
 *
 * By Jon E. Froehlich
 * @jonfroehlich
 * http://makeabilitylab.io
 * 
 * For a walkthrough and circuit diagram, see:
 * https://makeabilitylab.github.io/physcomp/arduino/potentiometers.html
 * 
 * 
 */

// The Arduino Uno ADC is 10 bits (thus, 0 - 1023 values)
#define MAX_ANALOG_INPUT_VAL 1023

const int LED_OUTPUT_PIN = LED_BUILTIN;
const int SENSOR_INPUT_PIN = A0;

// Define the number of samples to keep track of. The higher the number, the
// more the readings will be smoothed, but the slower the output will respond to
// the input. Using a constant rather than a normal variable lets us use this
// value to determine the size of the readings array.
const int SMOOTHING_WINDOW_SIZE = 10; // 10 samples

int _samples[SMOOTHING_WINDOW_SIZE];  // the readings from the analog input
int _curReadIndex = 0;                // the index of the current reading
int _sampleTotal = 0;                 // the running total
int _sampleAvg = 0;                   // the average

void setup() {
  pinMode(LED_OUTPUT_PIN, OUTPUT);
  Serial.begin(9600);

  // initialize all the readings to 0:
  for (int i = 0; i < SMOOTHING_WINDOW_SIZE; i++) {
    _samples[i] = 0;
  }
}

void loop() {

  // subtract the last reading:
  _sampleTotal = _sampleTotal - _samples[_curReadIndex];
  
  // read the sensor value
  int sensorVal = analogRead(SENSOR_INPUT_PIN);
  _samples[_curReadIndex] = sensorVal;
  
  // add the reading to the total:
  _sampleTotal = _sampleTotal + _samples[_curReadIndex];
  
  // advance to the next position in the array:
  _curReadIndex = _curReadIndex + 1;

  // if we're at the end of the array...
  if (_curReadIndex >= SMOOTHING_WINDOW_SIZE) {
    // ...wrap around to the beginning:
    _curReadIndex = 0;
  }

  // calculate the average:
  _sampleAvg = _sampleTotal / SMOOTHING_WINDOW_SIZE;
  
  // set the LED brightness to the smoothed value
  int ledVal = map(_sampleAvg, 0, MAX_ANALOG_INPUT_VAL, 0, 255);
  analogWrite(LED_OUTPUT_PIN, ledVal);

  // print the raw pot value and the converted led value
  Serial.print("AnalogIn:");
  Serial.print(sensorVal);
  Serial.print(", ");
  Serial.print("Smoothed:");
  Serial.println(_sampleAvg);


  delay(50); // read samples at ~20Hz (once every 50ms)
}

SIMPLE C++ CLASS

Signal filtering is a perfect opportunity to create a class to hide complexity, avoid code redundancy, and handle data processing. In our Makeability Lab Arduino Library, we created the MovingAveragefilter.hpp class, which simplifies using a moving average filter. But you could make your own, of course, or use other libraries

Here’s a demonstration of how to use MovingAveragefilter.hpp:

/*
 * Example of smoothing input on A0 using a moving average filter.  
 * 
 * This code requires the MovingAverageFilter.hpp class from the MakeabilityLab_Arduino_Library
 * 
 * To install and use this library:
 * 1. Get it from here: https://github.com/makeabilitylab/arduino/tree/master/MakeabilityLab_Arduino_Library
 * 2. Move the entire folder to the Arduino libraries folder on your computer.
 *   - On my Windows box, this is C:\Users\jonfr\Documents\Arduino\libraries
 *   - On Mac, it's: /Users/jonf/Documents/Arduino/libraries
 * 3. Then include the relevant libraries via #include <libraryname.h> or <libraryname.hpp>
 *
 * By Jon E. Froehlich
 * @jonfroehlich
 * http://makeabilitylab.io
 * 
 */

#include <MovingAverageFilter.hpp>

// The Arduino Uno ADC is 10 bits (thus, 0 - 1023 values)
#define MAX_ANALOG_INPUT_VAL 1023

const int LED_OUTPUT_PIN = LED_BUILTIN;
const int SENSOR_INPUT_PIN = A0;

MovingAverageFilter _movingAverageFilter(10);

void setup() {
  pinMode(LED_OUTPUT_PIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {

  // Read the sensor value
  int sensorVal = analogRead(SENSOR_INPUT_PIN);

  // Get the smoothed version using a moving average filter
  _movingAverageFilter.add(sensorVal);
  int smoothedVal = _movingAverageFilter.getAverage();

  // write out the LED value. 
  int ledVal = map(smoothedVal, 0, MAX_ANALOG_INPUT_VAL, 0, 255);
  analogWrite(LED_OUTPUT_PIN, ledVal);

  // print the raw pot value and the converted led value
  Serial.print("AnalogIn:");
  Serial.print(sensorVal);
  Serial.print(", ");
  Serial.print("MovingAverageFilter-WindowSize10:");
  Serial.println(smoothedVal);

  

  delay(50);
}

We also use the MovingAveragefilter.hpp class in our demonstration of various sliding window sizes on the moving average output (code here).

Weighted moving average

In the moving average algorithm above, we assign equal weight to all data in our filter window. You could imagine, however, designing an algorithm that assigns higher weights to more recent data (with the theory that recency correlates to relevancy). And, indeed, there are a class of algorithms called weighted moving averages (WMA) that do just this, including linear weighting schemes with weights that drop off linearly in the filter window and exponential weighting schemes where weights drop off exponentially.

Recall that a regular moving average is:

MA=X1+X2+…+XNN

Then the weighted version is simply:

WMA=w1X1+w2X2+…+wnXnw1+w2+…+wn

If the weight coefficients are precomputed to sum to one (∑i=1nwi=1), then the above equation simply becomes:

WMA=w1X1+w2X2+…+wnXn

Below, we’ve included sample weights for both linear and exponential weighting schemes with a sliding window size of 15.

Exponential moving average

Interestingly, you can implement the exponential moving average (EMA)—also known as the exponentially weighted moving average (EWMA)—without storing a sliding window buffer! This is a very cool optimization and means that you can achieve smoothing without the memory and computation overhead of many other “moving” filter techniques.

The algorithm is:

Si={X1if i = 1α⋅Xi+(1−α)⋅Si−1if i > 1

Where:

  • The coefficient α represents the degree of weighting decrease between 0 and 1. A higher α discounts older observations faster.
  • Xi is the value at index i.
  • Si is the value of the EMA at index i.

The coefficient α determines the exponential dropoff. A higher α weights more recent data more. For example, the video below shows EWMA performance with α equal to 0.5 (red line), 0.1 (green line), and 0.01 (yellow line). Notice how closely the α=0.5 EWMA filter tracks the underlying raw signal whereas the α=0.01 filter is quite distorted and lagged. The α=0.1 filter may still be appropriate, depending on your needs. Again, it’s up to you to experiment!

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/ExponentialMovingAverageFilter-ThreeAlphaValues-Optimized.mp4

Video This video shows EWMA performance with α equal to 0.5 (red line), 0.1 (green line), and 0.01 (yellow line). The code used to produce this video is here. Graph made with the built-in Arduino Serial Plotter.

ARDUINO EWMA IMPLEMENTATION

If you’re not used to reading equations, then perhaps the Arduino code below is more clear. The algorithm is quite straightforward and, again, unlike the traditional moving average algorithm, does not require a window buffer!

const int SENSOR_INPUT_PIN = A0;

float _ewmaAlpha = 0.1;  // the EWMA alpha value (α)
double _ewma = 0;        // the EWMA result (Si), initialized to zero

void setup()
{
  Serial.begin(9600); // for printing values to console
  _ewma = analogRead(SENSOR_INPUT_PIN);  //set EWMA (S1) for index 1
}

void loop()
{
  int sensorVal = analogRead(A0); // returns 0 - 1023 (due to 10 bit ADC)
  
  // Apply the EWMA formula 
  _ewma = (_ewmaAlpha * sensorVal) + (1 - _ewmaAlpha) * _ewma;

  Serial.print(sensorVal);      
  Serial.print(",");  
  Serial.println(_ewma);  
  delay(50); // Reading new values at ~20Hz
}

Code. This code is on GitHub.

Moving median filter

A moving median filter is almost the exact same as a moving average filter but takes the median over the sliding window rather than the average.

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/MovingMedianFilter_PowerPointAnimation_TrimmedAndCropped.mp4

Video This video shows a moving median filter with a window size of three. Animation made in PowerPoint.

 

The challenge, however, is calculating the median efficiently. The median is simply the middle value of a sorted array Xs or, if the size of the array is odd, then it is the average of the two middle values.

Med(X)={Xs[n2]if n is even(Xs[n−12]+[n+12])2if n is odd

Note: The array X must be sorted (indicated by the ‘s’ subscript in Xs). The brackets in the equation above indicate indices, similar to arrays in programming.

But how can we obtain this “middle” value efficiently? We know that re-sorting an array on each value insert can be computationally expensive. Thus, typically realtime median filters use other data structures, like indexable skiplists, to efficiently keep a sorted data structure on inserts and removals.

Similar to the moving average filter, we can tweak the moving median filter’s performance by modifying the filter window size. Below, we show a moving median filter with window sizes 5, 11, and 21. For this video, we used our test code MovingMedianFilterWindowSizeDemo.ino, which relies on Luis Llama’s Arduino Median Filter 2 library based on Phil Ekstrom’s “Better Than Average” article.

https://makeabilitylab.github.io/physcomp/advancedio/assets/videos/MovingMedianFilter_PowerPointAnimation_TrimmedAndCropped.mp4

Video This video shows moving median filter performance with window sizes 5, 11, and 21. Notice how a median filter tends to flatten “peaks” in the signal, which is unlike the other filters we’ve examined. To make this video, we used this code and the built-in Arduino Serial Plotter.

Why use a median filter?

Median filters are widely used in image processing to remove noise from images (image processing is its own subfield of signal processing focusing on 2D DSP techniques). Unlike mean (or average) filters, median filters remove noise while preserving edges—and edges are often a crucial part of other image processing algorithms like the Canny edge detector.

Figure. Median filtering is widely used in image processing where it is particularly effective at removing “speckle” or “salt-and-pepper” noise while preserving edges. Image from Wikipedia.

In the case of 1-dimensional signal processing, which we’ve focused on in this lesson, median filters have the same advantage: they provide a smoothing technique the preserves edges in our signal. For example, what if our data is from an underlying clock signal that should have sharp rising and falling edges, which we don’t wish to smooth. A moving average filter distorts rise and fall times while the median filter sharpens them.

 

Other filters

In this lesson, we covered only a few basic digital filters but many others exist—some which allow you to actually control which frequencies in your signal to eliminate. For example, high-pass filters can remove specific low-frequency components from your signal while keeping high frequencies, low-pass filters eliminate high-frequency components while keeping low frequencies, bandpass filters let you specify a range (or band) of frequencies to keep, etc.

Other popular filters include the Savitzky-Golay filter, the Butterworth filter, and the Kalman filter. The Savitzky-Golay filter, for example, is like a weighted moving average filter that attempts to fit a polynomial over the sliding window (to better fit the underlying signal)—see this MathWorks article.

In general, it is not helpful or appropriate to perform filtering investigations in real-time on sensor data because it’s difficult to replicate input signals and test and compare filtering algorithms. Yes, it’s fine to experiment with filters+real-time data for gut checking or to get something working quickly but not for more deep signal analysis.

Thus, if you’re interested in this area, we suggest experimenting with the filters in SciPy with Jupyter Notebook. You should log the sensor data to your computer and then analyze it “offline” in a testbed environment, as we do in our Step Tracker and Gesture Recognition assignments. This will allow you to experiment with and compare different algorithm approaches and parameter tuning.

Activity

For your prototyping activity, we would like you to choose a sensor from your hardware kits—it could be a potentiometer, a hall effect sensor, a force-sensitive resistor, a photoresistor, an accelerometer, a DIY sensor, etc.—and two different smoothing algorithms to apply to it.

More specifically, build an appropriate circuit for your sensor, read the input on an analog input pin, process this input with two different smoothing algorithms (either those we covered in this lesson or beyond), graph the three signals (raw signal, smoothing algorithm 1 output, smoothing algorithm 2 output), record a video of your input + the graphs, and report on your observations in the prototyping journal. Include a link to your video (or an embedded animated gif).

Arduino filtering libraries

There are lots of Arduino filtering libraries online and general C++ filtering code that could be adapted to the Arduino context. The usual disclaimers apply: we have not had a chance to evaluate all of these libraries. So, use at your own risk. :)

Resources

반응형

댓글