One of the main applications for the Arduino board is reading and logging of sensor data. For instance one monitors the CO concentration every second of the day. As samples may fluctuate and generate "spikes" in the graphs one can make multple measurements and take the median as "working" value. As the measurements are not static in time what one often wants is the median of a last defined period, a sort of "running median".
The median is defined as the middle value of an array of sorted values. The two main advantages of using the median above average is that it is not influenced by a single outlier and it allways represent a real measurement. On the other hand by averaging one could create extra precision.
The RunningMedian library is a small class that can have multiple instances in a sketch. Please note that every instance adds its own internal array to hold measurements, and that this adds up to the memory usage. The interface of the class is kept small but has grown a bit.
new in 0.2.00
new in 0.1.02
RunningMedian(); // constructor
void clear(); // clears internal state
void add(float); // add a measurement
float getMedian(); // returns the running Median
// new functions
float getAverage();
float getHighest();
float getLowest();
The class initializes a runningMedian of a certain size. Then it uses add() to fill a small internal array with measurements, these are kept in chronological order. If a new value is added and the array is full the oldest is overwritten. Simple circular buffer. If getMedian() is called the work begins, all values in the internal array are copied to a temp array and sorted. Then the middle element is returned. Clear() resets all internal counters to there initial values, to start over again.
A small sketch shows how it can be used. e.g connect a potmeter to the analog A0 pin.
#include <RunningMedian.h>
// RunningMedian samples = RunningMedian(9);
RunningMedian samples = RunningMedian();
void setup()
{
Serial.begin(115200);
Serial.print("Running Median Version: ");
Serial.println(RUNNINGMEDIANVERSION);
}
void loop()
{
test1();
}
void test1()
{
int x = analogRead(A0);
samples.add(x);
long l = samples.getLowest();
long m = samples.getMedian();
long a = samples.getAverage();
long h = samples.getHighest();
Serial.print(millis());
Serial.print(" ");
Serial.print(x);
Serial.print(" ");
Serial.print(l);
Serial.print(" ");
Serial.print(a);
Serial.print(" ");
Serial.print(m);
Serial.print(" ");
Serial.println(h);
delay(100);
}
In loop() first a sample is read from an analog sensor. It is added to the running median class. Then the median of the set sofar is fetched and displayed. Because after every analogRead a median can be fetched from the class there is no need to do multiple samples every time.
To use the library, make a folder in your SKETCHBOOKPATH\libaries with the name RunningMedian and put the .h and .cpp there. Optionally make a examples subdirectory to place the sample app.
Enjoy tinkering,
rob.tillaart@removethisgmail.com
#define RunningMedian_h
//
// FILE: RunningMedian.h
// AUTHOR: Rob dot Tillaart at gmail dot com
// PURPOSE: RunningMedian library for Arduino
// VERSION: 0.1.02
// URL: http://arduino.cc/playground/Main/RunningMedian
// HISTORY: See RunningMedian.cpp
//
// Released to the public domain
//
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include <inttypes.h>
#define RUNNINGMEDIANVERSION "0.1.02"
// should at least be 5 to be practical
#define MEDIAN_MIN 1
#define MEDIAN_MAX 19
class RunningMedian
{
public:
RunningMedian(uint8_t);
RunningMedian();
void clear();
void add(float);
float getMedian();
float getAverage();
float getHighest();
float getLowest();
protected:
uint8_t _size;
uint8_t _cnt;
uint8_t _idx;
float _ar[MEDIAN_MAX];
float _as[MEDIAN_MAX];
void sort();
};
#endif
// END OF FILE
// FILE: RunningMedian.cpp
// AUTHOR: Rob dot Tillaart at gmail dot com
// VERSION: 0.1.02
// PURPOSE: RunningMedian library for Arduino
//
// HISTORY:
// 0.1.00 - 2011-02-16 initial version
// 0.1.01 - 2011-02-22 added remarks from CodingBadly
// 0.1.02 - 2012-03-15
//
// Released to the public domain
//
#include "RunningMedian.h"
RunningMedian::RunningMedian(uint8_t size)
{
_size = constrain(size, MEDIAN_MIN, MEDIAN_MAX);
clear();
}
RunningMedian::RunningMedian()
{
_size = 5; // default size
clear();
}
// resets all counters
void RunningMedian::clear()
{
_cnt = 0;
_idx = 0;
}
// adds a new value to the data-set
// or overwrites the oldest if full.
void RunningMedian::add(float value)
{
_ar[_idx++] = value;
if (_idx >= _size) _idx = 0; // wrap around
if (_cnt < _size) _cnt++;
}
float RunningMedian::getMedian()
{
if (_cnt > 0)
{
sort();
return _as[_cnt/2];
}
return NAN;
}
float RunningMedian::getHighest()
{
if (_cnt > 0)
{
sort();
return _as[_cnt-1];
}
return NAN;
}
float RunningMedian::getLowest()
{
if (_cnt > 0)
{ sort();
return _as[0];
}
return NAN;
}
float RunningMedian::getAverage()
{
if (_cnt > 0)
{
float sum = 0;
for (uint8_t i=0; i< _cnt; i++) sum += _ar[i];
return sum / _cnt;
}
return NAN;
}
void RunningMedian::sort()
{
// copy
for (uint8_t i=0; i< _cnt; i++) _as[i] = _ar[i];
// sort all
for (uint8_t i=0; i< _cnt-1; i++)
{
uint8_t m = i;
for (uint8_t j=i+1; j< _cnt; j++)
{
if (_as[j] < _as[m]) m = j;
}
if (m != i)
{
long t = _as[m];
_as[m] = _as[i];
_as[i] = t;
}
}
}
// END OF FILE
#ifndef RunningMedian_h
#define RunningMedian_h
//
// FILE: RunningMedian.h
// AUTHOR: Rob dot Tillaart at gmail dot com
// PURPOSE: RunningMedian library for Arduino
// VERSION: 0.2.00 - template edition
// URL: http://arduino.cc/playground/Main/RunningMedian
// HISTORY: 0.2.00 first template version by Ronny
//
// Released to the public domain
//
#include <inttypes.h>
template <typename T, int N> class RunningMedian {
public:
enum STATUS {OK = 0, NOK = 1};
RunningMedian() {
_size = N;
clear();
};
void clear() {
_cnt = 0;
_idx = 0;
};
void add(T value) {
_ar[_idx++] = value;
if (_idx >= _size) _idx = 0; // wrap around
if (_cnt < _size) _cnt++;
};
STATUS getMedian(T& value) {
if (_cnt > 0) {
sort();
value = _as[_cnt/2];
return OK;
}
return NOK;
};
STATUS getAverage(float &value) {
if (_cnt > 0) {
float sum = 0;
for (uint8_t i=0; i< _cnt; i++) sum += _ar[i];
value = sum / _cnt;
return OK;
}
return NOK;
};
STATUS getHighest(T& value) {
if (_cnt > 0) {
sort();
value = _as[_cnt-1];
return OK;
}
return NOK;
};
STATUS getLowest(T& value) {
if (_cnt > 0) {
sort();
value = _as[0];
return OK;
}
return NOK;
};
unsigned getSize() {
return _size;
};
unsigned getCount() {
return _cnt;
}
STATUS getStatus() {
return (_cnt > 0 ? OK : NOK);
};
private:
uint8_t _size;
uint8_t _cnt;
uint8_t _idx;
T _ar[N];
T _as[N];
void sort() {
// copy
for (uint8_t i=0; i< _cnt; i++) _as[i] = _ar[i];
// sort all
for (uint8_t i=0; i< _cnt-1; i++) {
uint8_t m = i;
for (uint8_t j=i+1; j< _cnt; j++) {
if (_as[j] < _as[m]) m = j;
}
if (m != i) {
T t = _as[m];
_as[m] = _as[i];
_as[i] = t;
}
}
};
};
#endif
// --- END OF FILE ---
Test program for templated version
// FILE: TestRunningMedian.ino
// AUTHOR: Rob Tillaart
// DATE: 2012-05-03
//
// PUPROSE: test templated version of RunningMedian class
//
// Released to the public domain
//
#include "RunningMedian.h"
const int SENSOR_PIN = A0;
RunningMedian<unsigned int,32> myMedian;
void setup ()
{
Serial.begin(9600);
pinMode(SENSOR_PIN, INPUT);
};
void loop()
{
unsigned _median;
unsigned _lowest;
unsigned _highest;
float _average;
Serial.print(myMedian.getCount());
Serial.print(":");
// one way of working is that we ask for the status before getting data.
if (myMedian.getStatus() == myMedian.OK) {
myMedian.getMedian(_median);
Serial.print("Median = ");
Serial.print(_median);
Serial.println(" ");
}
else { // myMedian.NOK
Serial.println("No median. ");
}
Serial.print(myMedian.getCount());
Serial.print(":");
// The other way is that we check the return before relying on the data.
if (myMedian.getMedian(_median) == myMedian.OK) {
Serial.print(" Median = ");
Serial.print(_median);
}
else { // myMedian.NOK
Serial.print(" No median, ");
}
if (myMedian.getAverage(_average) == myMedian.OK) {
Serial.print(" Average = ");
Serial.print(_average);
}
else { // myMedian.NOK
Serial.print("No average, ");
}
if (myMedian.getLowest(_lowest) == myMedian.OK) {
Serial.print(" lowest = ");
Serial.print(_lowest);
}
else { // myMedian.NOK
Serial.print("No lowest, ");
}
if (myMedian.getHighest(_highest) == myMedian.OK) {
Serial.print(" highest = ");
Serial.println(_highest);
}
else { // myMedian.NOK
Serial.println(" No highest. ");
}
// now.. add some sensor-data and loop...
myMedian.add(analogRead(SENSOR_PIN));
delay(500);
};
// --- END OF FILE ---