ClassifierDemo Arduino Sketch¶
N.B. this is new and still being tested.
This sketch demonstrates an example of ultrasonic range sensor data processing using a combination of signal filters and a classification tree. The classifier code was generated using the Python script classify_gen.py on recorded and labeled training data. The underlying classification tree was automatically generated using the Python scikit-learn library. For more details on the filtering, please see FilterDemos Arduino Sketch.
The purpose of the classifier is to categorize a multi-dimensional data point into an integer representing membership within a discrete set of classifications. In the sample model, the data is two-dimensional for clarity of plotting the result. The data points could be extended to higher dimensions by including multiple samples over time or other sensor channels.
There are a couple of steps to using this approach in your own system.
Decide how to create different physical conditions which produce meaningful categories of data.
Decide what combination of sensor inputs and processed signals might disambiguate the categories. This will constitute the definition of each data point.
Set up a sketch with the data sampling and filtering portion of your system as a means to recording real-world data. The example uses integer units for efficiency; you may wish to prescale your data for increased integer accuracy, or you may decide to enable float values.
If your system can support a few extra user inputs, the data collection process will be easier if the data can be labeled while it is being collected. E.g., adding a ‘Record’ button and some category buttons could support emitting labeled data directly from the Arduino. (This was not done in the sample code below).
Record data from the real system under the different conditions.
Trim the data as needed to remove spurious startup transients or other confounding inputs.
If needed, label each data sample and merge into a single training file. For an example, see label_and_merge.py.
Run classify_gen.py to process the training data file into code.
For 2-D data, inspect the plot output as a sanity check. You may wish to tune the modeling parameters or adjust your data set and regenerate the model.
Incorporate the final classifier code in your sketch.
Decide whether the classifier output needs additional processing, e.g. debouncing to remove spurious transients.
The sketch files may be downloaded in a single archive file as ClassifierDemo.zip, or browsed in raw form in the source folder. The individual files are documented below.
Sample Model¶
The sample model was built by recording filtered data generated by this sketch under four different physical conditions. The individual files were manually trimmed, then labeled and combined using label_and_merge.py.
This particular example is somewhat contrived, since a reasonable two-dimensional classifier could be built by hand after inspecting the data. But this can be significantly harder in higher dimensions, e.g., if each data point were extended to include a few samples of history.
Related files:
Main Sketch¶
The main sketch file is ClassifierDemo.ino. It includes an event loop to sample a sonar range finder sensor, filter and fit the range signal to estimate position and velocity, then classify the state. The output is printed in a form suitable for real-time plotting using the IDE Serial Plotter.
1// ClassifierDemo.ino : Arduino program to demonstrate application of a decision tree.
2// No copyright, 2020, Garth Zeglin. This file is explicitly placed in the public domain.
3
4// The decision tree function and is kept in a separate .ino files which will
5// automatically be compiled with this one by the Arduino IDE. The tree code
6// was generated from data using classify_gen.py.
7
8// The baud rate is the number of bits per second transmitted over the serial port.
9const long BAUD_RATE = 115200;
10
11//================================================================
12// Hardware definitions. You will need to customize this for your specific hardware.
13const int sonarTriggerPin = 7; // Specify a pin for a sonar trigger output.
14const int sonarEchoPin = 8; // Specify a pin for a sonar echo input.
15
16//================================================================
17// Standard Arduino initialization function to configure the system.
18void setup()
19{
20 // initialize the Serial port
21 Serial.begin( BAUD_RATE );
22
23 // Initialize the digital input/output pins.
24 pinMode(sonarTriggerPin, OUTPUT);
25 pinMode(sonarEchoPin, INPUT);
26}
27
28//================================================================
29// Standard Arduino polling function. This function is called repeatedly to
30// handle all I/O and periodic processing. This loop should never be allowed to
31// stall or block so that all tasks can be constantly serviced.
32
33void loop()
34{
35 // Calculate the interval in microseconds since the last polling cycle.
36 static unsigned long last_time = 0;
37 unsigned long now = micros();
38 unsigned long interval = now - last_time;
39 last_time = now;
40
41 // Poll the sonar at regular intervals.
42 static long sonar_timer = 0;
43 sonar_timer -= interval;
44 if (sonar_timer < 0) {
45 sonar_timer += 100000; // 10 Hz sampling rate
46
47 // read the sonar; zeros represent a no-ping condition
48 int raw_ping = ping_sonar();
49
50 // suppress zeros in the input, just repeating the last input
51 int nz_ping = suppress_value(raw_ping, 0);
52
53 // convert the value from microseconds to centimeters
54 float cm = fmap(nz_ping, 0.0, 5900.0, 0.0, 100.0);
55
56 // apply a low-pass filter to smooth the raw data
57 cm = lowpass(cm);
58
59 // fit a trajectory curve to recent sample history
60 float traj[3];
61 trajfit(cm, traj);
62
63 // quantize and classify the current estimation
64 int posvel[2];
65 posvel[0] = (int) traj[0];
66 posvel[1] = (int) traj[1];
67 int cls = classify(posvel);
68
69 // debounce the classification to eliminate transient changes
70 cls = debounce(cls, 5);
71
72 // emit some data to plot
73 // Serial.print(raw_ping); Serial.print(" "); // ping time in microseconds
74 // Serial.print(cm); Serial.print(" "); // centimeter-scaled, zero-suppressed
75 // Serial.print(traj[0]); Serial.print(" "); // quadratic position
76 // Serial.print(traj[1]); Serial.print(" "); // quadratic velocity
77 Serial.print(posvel[0]); Serial.print(","); // integer position for classification
78 Serial.print(posvel[1]); Serial.print(" "); // integer velocity for classification
79 Serial.print(20*cls); Serial.print(" "); // integer sample classification, amplified for live plotting
80
81 Serial.println();
82 }
83}
classify.ino¶
1// Decision tree classifier generated using classify_gen.py
2int classify(int input[2])
3{
4 if (input[0] <= 53) {
5 if (input[0] <= 39) {
6 if (input[1] <= 7) {
7 if (input[1] <= -4) {
8 if (input[0] <= 30) {
9 if (input[1] <= -13) {
10 return 0;
11 } else {
12 return 0;
13 }
14 } else {
15 return 2;
16 }
17 } else {
18 if (input[0] <= 29) {
19 if (input[0] <= 24) {
20 return 0;
21 } else {
22 if (input[1] <= 2) {
23 if (input[1] <= 0) {
24 return 0;
25 } else {
26 return 0;
27 }
28 } else {
29 if (input[0] <= 27) {
30 return 0;
31 } else {
32 return 0;
33 }
34 }
35 }
36 } else {
37 if (input[0] <= 37) {
38 return 0;
39 } else {
40 return 0;
41 }
42 }
43 }
44 } else {
45 if (input[0] <= 23) {
46 return 0;
47 } else {
48 return 1;
49 }
50 }
51 } else {
52 if (input[1] <= -1) {
53 return 2;
54 } else {
55 return 1;
56 }
57 }
58 } else {
59 if (input[1] <= 3) {
60 if (input[1] <= -20) {
61 return 3;
62 } else {
63 if (input[0] <= 78) {
64 if (input[0] <= 64) {
65 if (input[1] <= -7) {
66 return 3;
67 } else {
68 return 3;
69 }
70 } else {
71 if (input[1] <= -3) {
72 return 3;
73 } else {
74 return 3;
75 }
76 }
77 } else {
78 return 3;
79 }
80 }
81 } else {
82 if (input[0] <= 78) {
83 if (input[0] <= 65) {
84 if (input[1] <= 11) {
85 return 3;
86 } else {
87 return 1;
88 }
89 } else {
90 return 1;
91 }
92 } else {
93 return 3;
94 }
95 }
96 }
97}
filters.ino¶
1// filters.ino : filtering primitives used by the ClassifierDemo sketch.
2// No copyright, 2020, Garth Zeglin. This file is explicitly placed in the public domain.
3
4//================================================================
5// Suppress a specific value in an input stream. One integer of state is required.
6int suppress_value(int input, int value)
7{
8 static int previous = 0;
9 if (input != value) previous = input;
10 return previous;
11}
12
13//================================================================
14// Debounce an integer stream by suppressing changes from the previous value
15// until a specific new value has been observed a minimum number of times. Three
16// integers of state are required.
17
18int debounce(int input, int samples)
19{
20 static int current_value = 0;
21 static int new_value = 0;
22 static int count = 0;
23
24 if (input == current_value) {
25 count = 0;
26 } else {
27 if (count == 0) {
28 new_value = input;
29 count = 1;
30 } else {
31 if (input == new_value) {
32 count += 1;
33 if (count >= samples) {
34 current_value = new_value;
35 count = 0;
36 }
37 } else {
38 new_value = input;
39 count = 1;
40 }
41 }
42 }
43 return current_value;
44}
45
46//================================================================
47// Floating-point version of map(). The standard Arduino map() function only
48// operates using integers; this extends the idea to floating point. The
49// Arduino function can be found in the WMath.cpp file within the Arduino IDE
50// distribution. Note that constrain() is defined as a preprocessor macro and
51// so doesn't have data type limitations.
52
53float fmap(float x, float in_min, float in_max, float out_min, float out_max) {
54 float divisor = in_max - in_min;
55 if (divisor == 0.0) {
56 return out_min;
57 } else {
58 return (x - in_min) * (out_max - out_min) / divisor + out_min;
59 }
60}
61//================================================================
62// Low-Pass Butterworth IIR digital filter, generated using filter_gen.py.
63// Sampling rate: 10 Hz, frequency: 1.0 Hz.
64// Filter is order 4, implemented as second-order sections (biquads).
65// Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html
66float lowpass(float input)
67{
68 float output = input;
69 {
70 static float z1, z2; // filter section state
71 float x = output - -1.04859958*z1 - 0.29614036*z2;
72 output = 0.00482434*x + 0.00964869*z1 + 0.00482434*z2;
73 z2 = z1;
74 z1 = x;
75 }
76 {
77 static float z1, z2; // filter section state
78 float x = output - -1.32091343*z1 - 0.63273879*z2;
79 output = 1.00000000*x + 2.00000000*z1 + 1.00000000*z2;
80 z2 = z1;
81 z1 = x;
82 }
83 return output;
84}
85
86//================================================================
87// Trajectory estimation filter generated using trajfit_gen.py.
88// Based on Savitzky-Golay polynomial fitting filters.
89// Sampling rate: 10 Hz.
90// The output array will contain the trajectory parameters representing the signal
91// at the current time: [position, velocity, acceleration], with units of [1, 1/sec, 1/sec/sec].
92// Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.savgol_coeffs.html
93void trajfit(float input, float output[3])
94{
95 const float coeff[3][5] =
96 {{ 0.085714, -0.142857, -0.085714, 0.257143,
97 0.885714},
98 { 3.714286, -3.857143, -5.714286, -1.857143,
99 7.714286},
100 { 28.571429, -14.285714, -28.571429, -14.285714,
101 28.571429}};
102 static float ring[5]; // buffer for recent time history
103 static unsigned oldest = 0; // index of oldest sample
104
105 // save the new sample by overwriting the oldest sample
106 ring[oldest] = input;
107 if (++oldest >= 5) oldest = 0;
108
109 // iterate over the coefficient rows
110 unsigned index = oldest;
111 for (int i = 0; i < 3; i++) {
112 output[i] = 0.0; // clear accumulator
113
114 // Iterate over the samples and the coefficient rows. The index cycles
115 // around the circular buffer once per row.
116 for (int j = 0; j < 5; j++) {
117 output[i] += coeff[i][j] * ring[index];
118 if (++index >= 5) index = 0;
119 }
120 }
121}
122//================================================================
sonar.ino¶
1// sonar.ino: operate a HC04 ultrasonic range sensor
2// No copyright, 2020, Garth Zeglin. This file is explicitly placed in the public domain.
3
4// Run a measurement cycle on the sonar range sensor. Returns the round-trip
5// time in microseconds. Returns zero if no ping is detected. This code
6// assumes the pin constants are defined in another file.
7int ping_sonar(void)
8{
9 // Generate a short trigger pulse.
10 digitalWrite(sonarTriggerPin, HIGH);
11 delayMicroseconds(10);
12 digitalWrite(sonarTriggerPin, LOW);
13
14 // Measure the echo pulse length. The ~6 ms timeout is chosen for a maximum
15 // range of 100 cm assuming sound travels at 340 meters/sec. With a round
16 // trip of 2 meters distance, the maximum ping time is 2/340 = 0.0059
17 // seconds. You may wish to customize this for your particular hardware.
18 const unsigned long TIMEOUT = 5900;
19 unsigned long ping_time = pulseIn(sonarEchoPin, HIGH, TIMEOUT);
20
21 return ping_time;
22}
23//================================================================
Development Tools¶
The development of this sketch involves several other tools which are not documented:
A Python script for generating a classifier using scikit-learn: classify_gen.py
A customizable Python script for capturing a serial data stream: record_Arduino_data.py
A customizable Python script for merging and labeling data files: label_and_merge.py
A set of recorded training data files: data/
The Python scripts use several third-party libraries:
SciPy: comprehensive numerical analysis library
scikit-learn: machine learning library built on top of SciPy
Matplotlib: plotting library for visualizing data
pySerial: portable support for the serial port used for Arduino communication