Studio deployment page with C++ library selected
run_classifier_continuous()
run_classifier()
and run_classifier_continuous()
expect raw data to be passed in through a signal_t struct. The definition of signal_t
can be found in edge-impulse-sdk/dsp/numpy_types.h. This struct has two properties:
total_length
- total number of values, which should be equal to EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE
(from model-parameters/model_metadata.h). For example, if you have an accelerometer with 3 axes sampling at 100 Hz for 2 seconds, total_length
would be 600.get_data
- a callback function that retrieves slices of data as required by the preprocessing (DSP) step. Some DSP algorithms (e.g. computing MFCCs for keyword spotting) page raw features in one slice at a time to save memory. This function allows you to store the raw data in other locations (e.g. internal RAM, external RAM, flash) and page it in when required. We will show how to configure this callback function later in the tutorial.numpy::signal_from_buffer()
(found in edge-impulse-sdk/dsp/numpy.h to construct the signal_t
for you.
model-parameters/ contains the settings for preprocessing your data (in dsp_blocks.h) and for running the trained machine learning model. In that directory, model_metadata.h defines the many settings needed by the impulse. In particular, you’ll probably care about the following:
ei_classifier_inferencing_categories[]
if you need the labels for your categories in string form.
tflite-model/ contains the actual trained model stored in an array. You should not need to access any of the variables or functions in these files, as inference is handled by the impulse library.
run_classifier()
or run_classifier_continuous()
is known as a “signal” and is passed in through a signal_t struct. Signals are always a flat buffer, so you must flatten any sensor data to a 1-dimensional array.
Time-series data with multiple axes are flattened so that the value from each axis is listed from each time step before moving on to the next time step. For example, here is how sensor data with 3 axes would be flattened:
run_impulse
. If you want to adjust the size of the buffer that is used to read from the signal in this case, you can set EI_DSP_IMAGE_BUFFER_STATIC_SIZE
, which also allocates the buffer statically. For example, you might set:
input_buf
array. Note that this buffer is constant for this particular program. However, it demonstrates how you can fill an array with floating point values from a sensor to pass to the impulse SDK library.
features[]
array with values from a connected sensor.
Important! Make sure that the length of the array matches the expected length for the preprocessing block in the impulse library. This value is given by EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE (which is 200 values * 3 axes = 600 total values for the given window in our case). Also note how the values are stored: {x0, y0, z0, x1, y1, z1, ...}
. You will need to construct a similar array if you are sampling live data from a sensor.
Save your main.cpp.
ei_run_classifier.h
file includes any other files we might need from the library and gives us access to the necessary functions.
The run_classifier() function expects a signal_t struct as an input. So, we set the members here:
signal.total_length
is the number of array elements in the input buffer. For our case, it should match the expected total number of elements (EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE
).
signal.get_data
must be set to a callback function. run_classifier()
will use this callback function to grab data from our buffer as needed. It is up to you to create this function. Let’s take a look at the simplest form of this callback:
offset
and length
will be for any given call, but we must be ready with valid data. We do know that this function will not attempt to index beyond the provided signal.total_length
amount.
The callback structure is used here so that data can be paged in from any location (e.g. RAM or ROM), which means we don’t necessarily need to save the entire sample in RAM. This process helps save precious RAM space on resource-constrained devices.
With our signal_t
struct configured, we can call our inference function:
run_classifier()
will perform any necessary preprocessing steps (such as computing the power spectral density) prior to running inference. The inference results are stored in the second argument (of type ei_impulse_result_t. The third parameter is debug
, which is used to print out internal states of the preprocessing and inference steps. We leave debugging disabled in this example. The function should return a value equal to EI_IMPULSE_OK
if everything ran without error.
We print out the time it took (in milliseconds) to perform preprocessing (“dsp”), classification, and any anomaly detection we had enabled:
result.classification[i].value
where i
is the index of our label. Labels are stored in alphabetical order in ei_classifier_inferencing_categories[]
. Each prediction value must be between 0.0 and 1.0. Additionally, thanks to the softmax function at the end of our neural network, all of the predictions should add up to 1.0.
If your model has anomaly detection enabled, EI_CLASSIFIER_HAS_ANOMALY will be set to 1. We can access the anomaly value via result.anomaly
. Additionally, if you are using an object detection impulse, EI_CLASSIFIER_OBJECT_DETECTION will be set to 1, and bounding box information will be an array stored in result.bounding_boxes[].
-I
flag to the directory that holds edge-impulse-sdk/, model-parameters/, and tflite-model/ so that the build system can find the required header files. If you unzipped your C++ library into a lib/ folder, for example, this flag should be -Ilib/
.
We then define a number of compiler flags that are set by both the C and the C++ compiler. What each of these do has been commented in the script:
edge-impulse-sdk/porting/posix/*.c*
and edge-impulse-sdk/porting/mingw32/*.c*
point to C++ files that provide implementations for the Functions That Require Definition. If you are using something other than a POSIX-based system or MinGW, you will want to change these files to one of the other supported platforms or to your own custom definitions for those functions.
Note the directory locations given in these lists. Many IDEs will ask you for the location of source files to include in the build process. You will want to include these directories (such as edge-impulse-sdk/CMSIS/DSP/Source/TransformFunctions/, etc.).
If you unzipped the C++ library into a different location (e.g. into a separate lib/ directory), then all of these source locations should be updated to reflect that. For example, tflite-model/*.cpp
would become lib/tflite-model/*.cpp
.
To use pure C++ for inference on almost any target with the SDK library, we can use LiteRT (previously Tensorflow Lite) for Microcontrollers (TFLM). TFLM comes bundled with the downloaded library. All we need to do is include it. Once again, note the compiler flag and source files that are added to the lists:
make
command. You can use the -j [jobs]
command to have Make use multiple threads to speed up the build process (especially if you have multiple cores in your CPU):
build/app.exe
on Windows.
Take a look at the output predictions–they should match the predictions we saw earlier in the Edge Impulse Studio!
features[]
with your raw sensor data, ensure it’s the correct length and format (e.g. float), and call run_classifier()
. However, we did not cover use cases where you might need to run inference on a sliding window of data. Instead of retaining a large window in memory and calling run_classifier()
for each new slice of data (which will re-compute features for the whole window), you can use run_classifier_continuous()
. This function will remember features from one call to the next so you just need to provide the new data. See this tutorial for a demonstration on how to run your impulse continuously.
We recognize that the embedded world is full of different build systems and IDEs. While we can’t support every single IDE, we hope that this guide showed how to include the required header and source files to build your project. Additionally, here are some IDE-specific guides for popular platforms to help you run your impulse locally.