Loading...

A runningMedian Class for Arduino.

remarks & comments

Intro

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.

RunningMedian library

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

  • first templated version (code and test prg) at the end.
  • added getSize() to see the buffer size.
  • added getCount() to see how far buffer is filled.

new in 0.1.02

  • The standard datatype is set to float (you can easily change it to long)
  • the constructor got a parameter which must be between MEDIAN_MIN and MEDIAN_MAX
  • three new functions have been added (bet these may be removed

RunningMedian(uint8_t size);    // constructor
        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.

Usage

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.

Notes

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.

Version 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 added constructor with size and some extra functions, fixed a bug if getMedian was called when nothing was added. Now it returns NAN = Not A Number which is actual a float value.
  • 0.2.00 2012-05-03 Template version by Ronny

Todo

  • use qsort() from avr lib
  • use malloc for arrays?
  • smarter use of arrays?

Done

  • known BUG: if no values are added or directly after clear(), getMedian() will return an undefined result. This can be solved in the float domain as there exists NaN as value; [SOLVED 0.1.02]
  • do a partial sort as only the lower half+1 is needed [OBSOLETE, the functions getLowest and getHighest use the full sort]
  • add getAverage() [DONE 0.1.02]

Enjoy tinkering,

rob.tillaart@removethisgmail.com

RunningMedian.h

#ifndef RunningMedian_h
#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

RunningMedian.cpp

//
//    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

Template version

RunningMedian.h (template version, no cpp version)


#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 ---