Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 340 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Documentation

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Edge Impulse Studio

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Time-series

Data

Audio

Visual regression

Visual anomaly detection

Computer vision

Collecting image data from the Studio

This page is part of Image classification and describes how you can use development boards with an integrated camera to import image data into Edge Impulse.

First, make sure your device is connected on the Devices page in the Edge Impulse Studio. Then, head to Data acquisition, and under 'Record new data', set a label and select 'Camera' as a sensor (most devices have multiple resolutions). This shows you a nice preview of the camera. Then click Start sampling.

Record new data from a camera

A few moments later - depending on the speed of the development board and the resolution - you'll now have an image collected!

You now have collected data from your development board

Do this until you have captured 30 images per class from a variety of angles. Also make sure to vary the things you capture for the unknown class.

Getting started

Welcome to Edge Impulse! We enable professional developers and researchers to create the next generation of intelligent products with Edge AI. In this documentation, you'll find user guides, tutorials, and API documentation. If at any point you have questions, visit our .

If you are a beginner, an advanced embedded engineer, an ML engineer, or a data scientist, you may want to use Edge Impulse differently. We have tailored Edge Impulse to suit your needs. Check out the following getting-started guides for a smooth start:

If you're new to the idea of embedded machine learning, or machine learning in general, you may enjoy our quick articles: and

Developer Plan

Edge Impulse offers a highly capable and feature rich Developer plan, for free! If you are a professional developer, hobbyist, maker, student, innovator, or the like who wants to develop edge AI applications without the need for enterprise level features, this is the plan for you.

Enterprise Plan

For startups and enterprises looking to scale edge ML algorithm development from prototype to production, we offer an with additional functionality and access to your own . This plan includes all of the tools needed to go from data collection to model deployment, such as a robust dataset builder to future-proof your data, integrations with all major cloud vendors, dedicated technical support, custom DSP and ML capabilities, and full access to the Edge Impulse APIs to automate your algorithm development.

Sign up for a free today!

Suitable for any type of edge AI application

We have some great tutorials, but you have full freedom in the models that you design in Edge Impulse. You can plug in new signal processing blocks and completely new neural networks. See and .

Edge Impulse Datasets

Need inspiration? Check out our that contains publicly available datasets collected, generated or curated by Edge Impulse or its partners.

These datasets highlight specific use cases, helping you understand the types of data commonly encountered in projects like object detection, audio classification, and visual anomaly detection.

API Documentation

You can access any feature in the Edge Impulse Studio through the . We also have the if you want to send data directly, and we have an open to control devices from the Studio.

Community

Edge Impulse offers a thriving community of engineers, developers, researchers, and machine learning experts. Connect with like-minded professionals, share your knowledge, and collaborate to enhance your embedded machine-learning projects. Head to the to ask questions or share your awesome ideas!

Public projects

Think your model is awesome, and want to share it with the world? Go to Dashboard and click Make this project public. This will make your whole project - including all data, machine learning models, and visualizations - available, and can be viewed and cloned by anyone with the URL.

We reference all the public projects here: . If you need some inspiration, just clone a project and fine-tune it to your needs!

Object detection

Object detection tasks take an image and output information about the class and number of objects, position, (and, eventually, size) in the image.

Edge Impulse provides, by default, two different model architectures to perform object detection, MobileNetV2 SSD FPN-Lite uses bounding boxes (objects location and size) and FOMO uses centroids (objects location only).

Want to compare the two models?

See

Bounding Boxes

  • (bounding boxes) Can run on systems starting from Linux CPUs up to powerful GPUs

Centroid

  • (centroids) Can run on high-end MCUs, Linux CPUs, and GPUs

Collecting image data with your mobile phone

This page is part of and describes how you can use your mobile phone to import image data into Edge Impulse.

To add your phone to your project, go to the Devices page, select Connect a new device and select Use your mobile phone. A QR code will pop up. Scan this code with your phone and your phone will pop up on the devices screen.

1. Collecting images

With your phone connected to your project, it's time to start capturing some images and build our dataset. We have a special UI for collecting images quickly, on your phone choose Collecting images?.

On your phone a permission prompt will show up, and then the viewfinder will be displayed. Set the label (in the top corner) to 'lamp', point your camera at your lamp and press Capture.

Afterwards the photo shows up in the studio on the Data acquisition page.

Do this until you have captured 30 images per class from a variety of angles. Also make sure to vary the things you capture for the unknown class.

2. Alternative: upload data directly

Alternatively you can also capture your dataset directly through a different app, and then upload the data directly to Edge Impulse There are both options to do this visually (click the 'Upload' icon on the data acquisition screen), or via the CLI. You can find instructions here: . In this case it's highly recommended to you use square images, as the transfer learning model expects these; and you probably want to resize these images before uploading them to make sure training remains fast.

Object detection learning blocks documentation
Detect objects using MobileNet SSD
Object detection using FOMO
Image classification
Uploader
Mobile phone connected to Edge Impulse
Choose Collecting images? to load the image-specific UI
Taking a photo with your phone.
Photo shows up in the studio.

End-to-end tutorials

This section provides detailed end-to-end tutorials to help you get started with Edge Impulse:

Movement classification and anomaly detection

  • Continuous motion recognition

Sound classification

  • Keyword spotting

  • Sound recognition

Image classification

  • Image classification

Object detection

  • Detect objects using MobileNet SSD (bounding boxes)

  • Object detection using FOMO (centroids)

Classification using several sensors

  • Sensor fusion

HR/HRV features

  • Getting Started with the HR/HRV Features Block

Edge Impulse Datasets

Need inspiration? Check out our Edge Impulse datasets collection that contains publicly available datasets collected, generated or curated by Edge Impulse or its partners.

These datasets highlight specific use cases, helping you understand the types of data commonly encountered in projects like object detection, audio classification, and visual anomaly detection.

Data ingestion

forum
Getting started for beginners
Getting started for ML Practitioners
Getting started for embedded engineers
What is embedded ML, anyway?
What is edge machine learning?
Enterprise plan
organization
Enterprise Trial
Custom blocks
Bring your own model
Edge Impulse datasets collection
Edge Impulse API
Ingestion service
Remote management protocol
forum
https://edgeimpulse.com/projects/overview

Feature extraction

Labeling

Collecting image data with the OpenMV Cam H7 Plus

This page is part of Image classification and describes how you can use the OpenMV Cam H7 Plus to build a dataset, and import the data into Edge Impulse.

1. Setting up your environment

To set up your OpenMV camera, and collect some data:

  1. Install the OpenMV IDE.

  2. Follow the OpenMV hardware setup guide to clean the sensor and focus the lens.

  3. Connect a micro-USB cable to the camera, and open the OpenMV IDE. The camera should automatically update to the latest firmware.

  4. Verify that the camera can capture live images, by clicking on the Connect button in the bottom left corner, then pressing Play to run the application.

Set up your OpenMV camera. Press the 'Connect' button, then press 'Play' to run the application.

A live feed from your camera will be displayed in the top right corner of the IDE.

2. Collecting images

Once your camera is up and running, it's time to start capturing some images and build our dataset.

First, set up a new dataset via Tools -> Dataset Editor, select New Dataset.

Creating a new dataset in the OpenMV IDE

This opens the 'Dataset editor' panel on the left side, and the 'dataset capture script' in the main panel of the IDE. Here, create three classes: "plant", "lamp" and "unknown". It's important to add an unknown class that contains random images which are neither lamps nor plants.

Create three classes in the OpenMV IDE by clicking the 'New class folder' (highlighted in yellow).

As we'll build a model that takes in square images, change the 'Dataset capture script' to read:

import sensor, image, time

sensor.reset()
sensor.set_pixformat(sensor.RGB565) # Modify as you like.
sensor.set_framesize(sensor.QVGA) # Modify as you like.
sensor.set_windowing((240, 240)) # Modify as you like.
sensor.skip_frames(time = 2000)

clock = time.clock()

while(True):
    clock.tick()
    img = sensor.snapshot()
    print(clock.fps())

Now you can capture data for the three classes.

  1. Click the Play icon to run the 'dataset capture script' on your OpenMV camera.

  2. Select one of the classes by clicking on the folder name in the 'Dataset editor'.

  3. Take a snap by clicking the Capture data (camera icon) button.

Do this until you have captured 30 images per class from a variety of angles. Also make sure to vary the things you capture for the unknown class.

Capturing data (a plant image shown on the left) into a dataset using the OpenMV camera

3. Sending the dataset to Edge Impulse

To import the dataset into Edge Impulse go to Tools > Dataset Editor > Export > Upload to Edge Impulse project.

Synchronize your dataset with Edge Impulse straight from the OpenMV IDE

Then, choose the project name, and the split between training and testing data (recommended to keep this to 80/20).

Choose a project, and then the dataset split to upload your data

A duplicate check runs when you upload new data, so you can upload your dataset multiple times (for example, when you've added new files) without adding the same data twice.

Training and testing data split

The split between training and testing data is based on the hash of the file in order to have a deterministic process. As a consequence you may not have a perfect 80/20 split between training and testing, but this process ensures samples are always placed in the same category.

Our dataset now appears under the Data acquisition section of our project.

Collected data

You can now go back to the Image classification tutorial to build your machine learning model.

Inferencing & post-processing

In the section, you will discover useful techniques to leverage our inferencing libraries or how you can use the inference results in your application logic:

Ingest multi-labeled data using the API

This guide provides a quick walk-through on how to upload and update time-series data with multiple labels using the Edge Impulse API.

Getting Started

Prerequisites

  • API Key - Obtain from your project settings on Edge Impulse.

  • Example Github repository - Clone the following repository, it contains the scripts and the example files:

Setup Your Environment

Export your API key to use in the upload scripts:

Prepare Your Data

  • Data File: JSON formatted time-series data. See the

  • structured_labels.labels File: JSON with structured labels. See the

Upload Data

Use the upload.sh script to send your data and labels to Edge Impulse:

Update Existing Samples

To update a sample, run update-sample.sh with the required project and sample IDs:

🚀

We hope this tutorial has helped you to understand how to ingest multi-label data samples to your Edge Impulse project. If you have any questions, please reach out to us on our .

Machine learning

In this ML & data engineering section, you will discover useful techniques to train your models, generate synthetic datasets, or to perform advanced feature extraction:

Edge Impulse Python SDK

Synthetic data

Learn about synthetic data and how to integrate synthetic data models into your Edge Impulse project with the following guides:

The following tutorials detail how to work with synthetic datasets in Edge Impulse:

See also:

Other

Synthetic data

Synthetic datasets are a collection of data artificially generated rather than being collected from real-world observations or measurements. They are created using algorithms, simulations, or mathematical models to mimic the characteristics and patterns of real data. Synthetic datasets are a valuable tool to generate data for experimentation, testing, and development when obtaining real data is challenging, costly, or undesirable.

You might want to generate synthetic datasets for several reasons:

Cost Efficiency: Creating synthetic data can be more cost-effective and efficient than collecting large volumes of real data, especially in resource-constrained environments.

Data Augmentation: Synthetic datasets allow users to augment their real-world data with variations, which can improve model robustness and performance.

Data Diversity: Synthetic datasets enable the inclusion of uncommon or rare scenarios, enriching model training with a wider range of potential inputs.

Privacy and Security: When dealing with sensitive data, synthetic datasets provide a way to train models without exposing real information, enhancing privacy and security.

You can generate synthetic data directly from Edge Impulse using the Synthetic Data tab in the Data acquisition view. This tab provides a user-friendly interface to generate synthetic data for your projects. You can create synthetic datasets using a variety of tools and models.

We have put together the following tutorials to help you get started with synthetic datasets generation:

Using integrated models directly available inside Edge Impulse Studio

  • DALL-E Image Generation Block: Generate image datasets using Dall·E using the .

  • Whisper Keyword Spotting Generation Block: Generate keyword-spotting datasets using the . Ideal for keyword spotting and speech recognition applications.

  • Eleven Labs Sound Generation Block: Generate sound datasets using the . Ideal for generating realistic sound effects for various applications.

Note that you will need an API Key/Access Token from the different providers to run the model used to generate the synthetic data.

If you want to create your own synthetic data block, see .

Other tutorials

  • (Jupyter Notebook and Transformation block source code available).

  • (Jupyter Notebook source code available).

  • (Jupyter Notebook source code available).

  • .

advanced inferencing tutorials
Continuous audio sampling
Multi-impulse
Count objects using FOMO
Using the Edge Impulse Python SDK to run EON Tuner
Using the Edge Impulse Python SDK to upload and download data
Using the Edge Impulse Python SDK with Hugging Face
Using the Edge Impulse Python SDK with SageMaker Studio
Using the Edge Impulse Python SDK with TensorFlow and Keras
Using the Edge Impulse Python SDK with Weights & Biases
Synthetic data overview
Synthetic data integration
Generate audio with Eleven Labs Sound Effects model datasets
Generate image datasets using Dall·E
Generate keyword-spotting datasets
Generate physics simulation datasets
Generate synthetic datasets using custom transformation blocks
Generate synthetic datasets using NVIDIA Omniverse
Classification with multiple 2D inputs
FOMO self-attention
Label audio data using your existing models
Label image data using GPT-4o
git clone [email protected]:edgeimpulse/example-multi-label-ingestion-via-api.git
export EI_PROJECT_API_KEY='your_api_key_here'
#!/bin/bash
if [[ -z "$EI_PROJECT_API_KEY" ]]; then
    echo "Missing EI_PROJECT_API_KEY env variable"
    exit 1
fi

curl -X POST \
    -H "x-api-key: $EI_PROJECT_API_KEY" \
    -H "Content-Type: multipart/form-data" \
    -F "[email protected]" \
    -F "data=@structured_labels.labels" \
    https://ingestion.edgeimpulse.com/api/training/files
sh update-sample.sh --project-id <your_project_id> --sample-id <your_sample_id>
Data acquisition format speicification
specification format
forum
After upload
After update
DALL-E model
Whisper model
Eleven Labs model
Custom synthetic data blocks
Generate image datasets using Dall·E
Generate keyword-spotting datasets
Generate physics simulation datasets
Generate synthetic datasets using NVIDIA Omniverse
Synthetic Data Tab

Python SDK examples

While the Edge Impulse Studio is a great interface for guiding you through the process of collecting data and training a model, the edgeimpulse Python SDK allows you to programmatically Bring Your Own Model (BYOM), developed and trained on any platform. See here for the Python SDK API reference documentation.

With the following tutorials, you will learn how to use the Edge Impulse Python SDK with a number of other machine-learning frameworks and platforms:

  • Using the Edge Impulse Python SDK to run EON Tuner

  • Using the Edge Impulse Python SDK to upload and download data

  • Using the Edge Impulse Python SDK with Hugging Face

  • Using the Edge Impulse Python SDK with SageMaker Studio

  • Using the Edge Impulse Python SDK with TensorFlow and Keras

  • Using the Edge Impulse Python SDK with Weights & Biases

For beginners

Welcome to Edge Impulse! If you're new to the world of edge machine learning, you've come to the right place. This guide will walk you through the essential steps to get started with Edge Impulse, a suite of engineering tools for building, training, and deploying machine learning models on edge devices.

Check out our Edge AI Fundamentals course to learn more about edge computing, machine learning, and edge MLOps.

Why Edge Impulse, for beginners?

Edge Impulse empowers you to bring intelligence to your embedded projects by enabling devices to understand and respond to their environment. Whether you want to recognize sounds, identify objects, or detect motion, Edge Impulse makes it accessible and straightforward. Here's why beginners like you are diving into Edge Impulse:

  • No Coding Required: You don't need to be a coding expert to use Edge Impulse. Our platform provides a user-friendly interface that guides you through the process - this includes many optimized preprocessing and learning blocks, various neural network architectures, and pre-trained models and can generate ready-to-flash binaries to test your models on real devices.

  • Edge Computing: Your machine learning models are optimized to run directly on your edge devices, ensuring low latency and real-time processing.

  • Support for Various Sensors: Edge Impulse supports a wide range of sensors, from accelerometers and microphones to cameras, making it versatile for different projects.

  • Community and Resources: You're not alone on this journey. Edge Impulse offers a supportive community and extensive documentation to help you succeed.

Getting started in a few steps

Ready to begin? Follow these simple steps to embark on your Edge Impulse journey:

1. Sign up

Start by creating an Edge Impulse account. It's free to get started, and you'll gain access to all the tools and resources you need.

2. Create a project

Once you're logged in, create your first project. Give it a name that reflects your project's goal, whether it's recognizing sounds, detecting objects, or something entirely unique.

3. Collect/import data

To teach your device, you need data. Edge Impulse provides user-friendly tools for collecting data from your sensors, such as recording audio, capturing images, or reading sensor values. We recommend using a hardware target from this list or your smartphone to start collecting data when you begin with Edge Impulse.

You can also import existing datasets or clone a public project to get familiar with the platform.

Edge Impulse Datasets

Need inspiration? Check out our Edge Impulse datasets collection that contains publicly available datasets collected, generated or curated by Edge Impulse or its partners.

These datasets highlight specific use cases, helping you understand the types of data commonly encountered in projects like object detection, audio classification, and visual anomaly detection.

4. Label your data

Organize your data by labeling it. For example, if you're working on sound recognition, label audio clips with descriptions like "dog barking" or "car horn." You can label your data as you collect it or add labels later, our data explorer is also particularly useful to understand your data.

5. Pre-process your data and train your model

This is where the magic happens. Edge Impulse offers an intuitive model training process through processing blocks and learning blocks. You don't need to write complex code; the platform guides you through feature extraction, model creation, and training.

6. Run the inference on a device

After training your model, you can easily export your model to run in a web browser or on your smartphone, but you can also run it on a wide variety of edge devices, whether it's a Raspberry Pi, Arduino, or other compatible hardware. We also provide ready-to-flash binaries for all the officially supported hardware targets. You don't even need to write embedded code to test your model on real devices!

If you have a device that is not supported, no problem, you can export your model as a C++ library that runs on any embedded device. See Running your impulse locally for more information.

7. Go further

Building Edge AI solutions is an iterative process. Feel free to try our organization hub to automate your machine-learning pipelines, collaborate with your colleagues, and create custom blocks.

Tutorials and resources for beginners

The end-to-end tutorials are perfect for learning how to use Edge Impulse Studio. Try the tutorials:

  • Motion recognition + anomaly detection,

  • Keyword spotting,

  • Sound recognition,

  • Image classification,

  • Object detection using bounding boxes (size and location),

  • Object detection using centroids (location)

These will let you build machine-learning models that detect things in your home or office.

Join the Edge Impulse Community

Remember, you're not alone on your journey. Join the Edge Impulse community to connect with other beginners, experts, and enthusiasts. Share your experiences, ask questions, and learn from others who are passionate about embedded machine learning.

Now that you have a roadmap, it's time to explore Edge Impulse and discover the exciting possibilities of embedded machine learning. Let's get started!

Lifecycle management

At Edge Impulse, we recognize that the lifecycle of your impulse is dynamic. As data grows, unanticipated factors, or drift occurs retraining and redeployment becomes essential. Many of our partners have already starting to address this with integrations to our platform, or documenting details for implementation on aspects like OTA updates to your impulse, and Lifecycle Management. We have put together this section to help you understanding and explore how to create your own implementation of a Lifecycle Management system.

MLOps

MLOps is a set of practices that combines Machine Learning, DevOps, and Data Engineering. The goal of MLOps is to streamline and automate the machine learning lifecycle, including integration, testing, releasing, deployment, and infrastructure management.\

MLOps Venn Diagram

Continuous Integration, Continuous Deployment and Continuous Learning

Continuous Learning is a key concept in the domain of Machine Learning Operations (MLOps), which is a set of practices that combines Machine Learning, DevOps, and Data Engineering. Here is an example of the process:

Continuous Learning Sample Architecture

OTA Infrastructure

In this section we will explore how firmware updates and other scenarios are currently addressed, with traditional OTA. It should help you to get started planning your own updated impulse across a range of platforms. Starting with platform-specific examples like Arduino Cloud, Nordic nRF Connect SDK, Zephyr, and Golioth, Particle Workbench and Blues Wireless.

Finally we will explore building an end-to-end example on the Espressif IDF. By covering a cross section of platforms we hope to provide a good overview of the process and how it can be applied to your own project.

With more generic examples like Arduino, Zephyr and C++ which can be applicable to all other vendors.

These OTA Model Update Tutorials tutorials will help you to get started.

Closing the Loop

Edge AI solutions are typically not just about deploying once; it’s about building a Lifecycle Management ecosystem. You can configure your device to send labeled data back to Edge Impulse for ongoing model refinement, and leverage our version control to track your model performance over time.

This bidirectional data flow can be established with a straightforward call to our ingestion API you can explore how to collect data from your board in the following tutorial:

  • Collect Data from Board

By integrating these elements, you establish an Lifecycle Management cycle, where the impulse is not static but evolves, learns, and adapts. This adaptation is can be as simple as adding new data to the existing model, or as complex as retraining the model with new data and deploying a new model to the device. Based on metrics you can define, you can trigger this process automatically, or manually. In the esp-idf example, we will explore how to trigger this process manually, and conditionally based on metrics.

  • Espressif IDF end-to-end example

Conclusion

We hope this section has helped you to understand the process of Lifecycle Management and how to implement it in your own project. If you have any questions, please reach out to us on our forum.

Devices

There is a wide variety of devices that you can connect to your Edge Impulse project. These devices can help you collect datasets for your project, test your trained ML model and even deploy your ML model directly to your development board with a pre-built binary application (for fully supported development platforms).

On the Devices tab, you'll find a list of all your connected devices and a guide on how to connect new devices that are currently supported by Edge Impulse.

Devices tab displaying connected devices.

To connect a new device, click on the Connect a new device button on the top right of your screen.

Connect a new device button.

You will get a pop-up with multiple options of devices you can connect to your Edge Impulse project. Available options include:

  • Connecting a fully supported development board.

  • Using your mobile phone.

  • Using your computer (Linux/Mac).

  • Using the serial port to connect devices that are currently not fully supported.

Frequently asked questions (FAQ)

Data

Does Edge impulse integrate with cloud storage services?

Yes. The enterprise version of Edge Impulse can integrate directly with your cloud storage provider to access and transform data.

How is the labeling of the data performed?

Using the Edge Impulse Studio data acquisition tools (like the or ), you can collect data samples manually with a pre-defined label.

If you have a dataset that was collected outside of Edge Impulse, you can upload your dataset using the , , , or . You can then use the Edge Impulse Studio to split up your data into labeled chunks, crop your data samples, and more to create high quality machine learning datasets.

Processing

What signal processing is available in Edge Impulse?

A big part of Edge Impulse are the processing blocks, as they clean up the data, and extract important features from your data before passing it to a machine learning model.

The source code for these processing blocks can be found on GitHub: (and you can build as well).

How does the feature explorer visualize data that has more that 3 dimensions?

Edge Impulse uses (a dimensionality reduction algorithm) to project high dimensionality input data into a 2 or 3 dimensional space. This even works for extremely high dimensionality data such as images.

Learning

What frameworks does Edge Impulse use to train the machine learning models?

We use a wide variety of tools, depending on the machine learning model. For neural networks we typically use TensorFlow and Keras, for object detection models we use TensorFlow with Google's Object Detection API, and for 'classic' non-neural network machine learning algorithms we mainly use sklearn. For neural networks you can see (and modify) the Keras code by clicking ⋮, and selecting Switch to expert mode.

Can I use a model that has been trained outside of Edge Impulse?

Yes you can! Check out our documentation on to see how to import your model into your Edge Impulse project, and using the !

Deployment

What are the minimum hardware requirements to run the Edge Impulse inferencing library on my embedded device?

The minimum hardware requirements for the embedded device depends on the use case. Anything from a Cortex-M0+ for vibration analysis to Cortex-M4F for audio, Cortex-M7 for image classification to Cortex-A for object detection in video should work.

View our for more details.

What is the typical power consumption of the Edge Impulse machine learning processes on my device?

Simple answer: To get an indication of time per inference we show performance metrics in every DSP and ML block in Studio. Multiply this by the active power consumption of your MCU to get an indication of power cost per inference.

More complicated answer: It depends. Normal techniques to conserve power still apply to ML, so try to do as little as possible (do you need to classify every second, or can you do it once a minute?), be smart about when to run inference (can there be an external trigger like a motion sensor before you run inference on a camera?), and collect data in a lower power mode (don't run at full speed when sampling low-resolution data, and see if your sensor can use an interrupt to wake your MCU - rather than polling).

Also see .

What engine does Edge Impulse use to compile the impulse?

It depends on the hardware.

For general-purpose MCUs we typically use EON Compiler with TFLite Micro and additional kernels (including hardware optimization, e.g. via CMSIS-NN, ESP-NN).

On Linux, if you run the impulse on CPU, we use LiteRT (previously Tensorflow Lite).

For accelerators we use a wide variety of other runtimes, e.g. hardcoded network in silicon for Syntiant, custom SNN-based inference engine for Brainchip Akida, DRP-AI for Renesas RZV2L, etc...

Is there a downside to enabling the EON Compiler?

The compiles your neural networks to C++ source code, which then gets compiled into your application. This is great if you need the lowest RAM and ROM possible (EON typically uses 30-50% less memory than LiteRT (previously Tensorflow Lite) but you also lose some flexibility to update your neural networks in the field - as it is now part of your firmware).

By disabling EON we place the full neural network (architecture and weights) into ROM, and load it on demand. This increases memory usage, but you could just update this section of the ROM (or place the neural network in external flash, or on an SD card) to make it easier to update.

What is the .eim model format for Edge Impulse for Linux?

See on the Edge Impulse for Linux pages.

Can I use an unsupported development board or a custom PCB (with a different microcontroller or microprocessor) with Edge Impulse?

Yes! A "supported board" simply means that there is an official or community-supported firmware that has been developed specifically for that board that helps you collect data and run impulses. Edge Impulse is designed to be extensible to computers, smartphones, and a nearly endless array of microcontroller build systems.

You can collect data from you custom board and upload it to Edge Impulse in a variety of ways. For example:

  • Transmitting data to the

  • Using the SDK

  • By (e.g. CBOR, JSON, CSV, WAV, JPG, PNG)

Your trained model can be deployed as part a . It requires some effort, but most build systems will work with our C++ library, as long as that build system has a C++ compiler and there is enough flash/RAM on your device to run the library (which includes the DSP block and model).

Other

How can I share my Edge Impulse project?

To collaboration on your projects, go to your , find the Collaborators section, and click the '+' icon.

You can also create a public version of your Edge Impulse project. This makes your project available to the whole world - including your data, your impulse design, your models, and all intermediate information - and can easily be cloned by anyone in the community. To do so, go to Dashboard, and click Make this project public.

How can I cite Edge Impulse in scientific publications?

If you use Edge Impulse in a scientific publication, we would appreciate citations to the following paper:

Transformation blocks

Transformation blocks are a flexible way to pre-process, i.e. transform, your through transformation jobs before using it within a project or to create another dataset. They can also be used standalone (not operating on organizational data) as a way to run cloud processing jobs for specific actions.

You can use transformation blocks to fetch external datasets, augment and create variants of your data samples, extract metadata from config files, create helper graphs, align and interpolate measurements across sensors, remove duplicate entries, and more.

Running a transformation block

Transformation blocks can be run individually through , stacked together to run in series in , or run as a (standalone mode only) importing data into a project. Please refer to the respective documentation for details.

Operating modes

Transformation blocks operate in one of three modes: file, directory, or standalone.

File

Transformation blocks that have been configured with the file operating mode will only appear in the block dropdown for transformation jobs.

As the name implies, file transformation blocks operate on files. The directory specified in the input data path field for the transformation job is automatically scanned for files. Any files that are found are shown on the right hand side as the selected items.

Directory

Transformation blocks that have been configured with the directory operating mode will only appear in the block dropdown for transformation jobs.

As the name implies, directory transformation blocks operate on directories. The directory specified in the input data path field for the transformation job is automatically scanned for directories. Any directories that are found are shown on the right hand side as the selected items.

Standalone

Transformation blocks that have been configured with the standalone operating mode can appear in the block dropdown for transformation jobs, for project data sources, or both depending on how the block has been set up.

As the name implies, standalone transformation blocks do not operate on any files or directories. No dataset selection fields will be shown for standalone blocks when creating a transformation job. There will be no files or directories shown on the right hand side as the selected items.

Pre-built transformation blocks

Edge Impulse has the ability to incorporate pre-built transformation blocks into the platform and make these available for all organizations to use. Pre-built transformation blocks may be added over time as recurring needs or interests emerge.

As thees blocks are added to the platform, they will be found within your organization by going to the Transformation left sidebar menu item under Custom blocks. The pre-built blocks will be listed under the Public blocks section at the bottom of the transformation blocks overview page.

Custom transformation blocks

Please refer to the documentation for details.

Troubleshooting

Additional Resources

Organization hub

Your Edge Impulse Organization enables your team to collaborate on multiple datasets, automation, and models in a shared workspace. It provides tools to automate data preparation tasks with reusable pipelines, enabling data transformation, preparation, and analysis of sensor data at scale. Allowing anyone in your team to quickly access relevant data through familiar tools, add versions and add traceability to your machine learning models, and lets you quickly create and monitor your Edge Impulse projects for optimal on-device performance.

To get started, follow these guides:

  • - to add collaborators with different access rights.

  • - to track and visualize all your metrics over time.

  • - to connect a storage bucket, to learn how to deal with such complex data infrastructure and to import your data samples into your projects.

  • - to chain several transformation blocks and to import data into your projects.

  • - to run your transformation blocks and get an overview of the running jobs.

  • - to allow external parties to securely contribute data to your datasets.

  • - to match any specific use cases using dedicated cloud jobs.

Health reference design

We have built a that describes an end-to-end ML workflow for building a wearable health product using Edge Impulse. It is a good tutorial to understand how we handle complex data infrastructure and discover the organization's advanced features.

Usage metrics

Existing enterprise users or enterprise trial users can view their entitlement limits via the dashboard of their enterprise organization:

This view allows you to see your organization's current usage of total users, projects, compute time and storage limits. To increase your organization's limits, select the Request limit increase button to contact sales.

Validating clinical data

Using Checklists

You can optionally show a check mark in the list of data items, and show a check list for data items. This can be used to quickly view which data items are complete (if you need to capture data from multiple sources) or whether items are in the right format.

Checklists look trivial, but are actually very powerful as they give quick insights in dataset issues. Missing these issues until after the study is done can be very expensive.

Checklists are written to ei-metadata.json and are automatically being picked up by the UI.

Checklists are driven by the metadata for a data item. Set the ei_check metadata item to 0 or 1 to show a check mark in the list. To show an item in the checklist, set an ei_check_KEYNAME metadata item to 0 or 1.

To query for items with or without a check mark, use a filter in the form of:

To make it easy to create these lists on the fly you can set these metadata items directly from a

Example

For the reference design described and used in the previous pages, the combiner takes in a data item, and writes out:

  1. A checklist, e.g.:

    • ✔ - PPG file present

    • ✔ - Accelerometer file present

    • ✘ - Correlation between HR/PPG HR is at least 0.5

  2. If the checklist is OK, a combined.parquet file.

  3. A hr.png file with the correlation between HR found from PPG, and HR from the reference device. This is useful for two reasons:

    • If the correlation is too low we're looking at the wrong file, or data is missing.

    • Verify if the PPG => HR algorithm actually works.

This makes it easy to quickly see if the data is in the right format, and if the data is complete. If the checklist is not OK, the data item is not used in the training set.

Querying clinical data

Organizational datasets contain a powerful query system which lets you explore and slice data. You control the query system through the 'Filter' text box, and you use a language which is very similar to SQL ().

For example, here are some queries that you can make:

  • dataset like '%PPG%' - returns all items and files from the study.

  • bucket_name = 'edge-impulse-health-reference-design' AND -- labels sitting,walking - returns data whose label is 'sitting' and 'walking, and that is stored in the 'edge-impulse-health-reference-design' bucket.

  • metadata->>'ei_check' = 0 - return data that have a metadata field 'ei_check' which is '0'.

  • created > DATE('2022-08-01') - returns all data that was created after Aug 1, 2022.

After you've created a filter, you can select one or more data items, and select Actions...>Download selected to create a ZIP file with the data files. The file count reflects the number of files returned by the filter.

The previous queries all returned all files for a data item. But you can also query files through the same filter. In that case the data item will be returned, but only with the files selected. For example:

  • file_name LIKE '%.png' - returns all files that end with .png.

If you have an interesting query that you'd like to share with your colleagues, you can just share the URL. The query is already added to it automatically.

All available fields

These are all the available fields in the query interface:

  • dataset - Dataset.

  • bucket_id - Bucket ID.

  • bucket_name - Bucket name.

  • bucket_path - Path of the data item within the bucket.

  • id - Data item ID.

  • name - Data item name.

  • total_file_count - Number of files for the data item.

  • total_file_size - Total size of all files for the data item.

  • created - When the data item was created.

  • metadata->key - Any item listed under 'metadata'.

  • file_name - Name of a file.

  • file_names - All filenames in the data item, that you can use in conjunction with CONTAINS. E.g. find all items with file X, but not file Y: file_names CONTAINS 'x' AND not file_names CONTAINS 'y'.

API examples

The exposes programmatic access to most functionality in the studio and it is particularly useful when it comes to automating tasks. You can use the API to edit the labels of many samples at once, train models, or create new impulses. In addition, you can subscribe to events, such as when a new file is processed by the ingestion service. An Edge Impulse Python API bindings is also available as the edgeimpulse-api pip package.

See the following examples:

Edge Impulse API
Running jobs using the API
Using the Python SDK API Bindings
Customize the EON Tuner
Trigger connected board data sampling
@misc{hymel2023edgeimpulsemlopsplatform,
      title={Edge Impulse: An MLOps Platform for Tiny Machine Learning},
      author={Shawn Hymel and Colby Banbury and Daniel Situnayake and Alex Elium and Carl Ward and Mat Kelcey and Mathijs Baaijens and Mateusz Majchrzycki and Jenny Plunkett and David Tischler and Alessandro Grande and Louis Moreau and Dmitry Maslov and Artie Beavis and Jan Jongboom and Vijay Janapa Reddi},
      year={2023},
      eprint={2212.03332},
      archivePrefix={arXiv},
      primaryClass={cs.DC},
      url={https://arxiv.org/abs/2212.03332},
}
serial daemon
data forwarder
Edge Impulse CLI
data ingestion API
web uploader
enterprise data storage bucket tools
enterprise upload portals
edgeimpulse/processing-blocks
your own processing blocks
UMAP
Bring your own model (BYOM)
Edge Impulse Python SDK
inference performance metrics
Analyse Power Consumption in Embedded ML Solutions
EON Compiler
.eim models?
Data forwarder
Edge Impulse for Linux
uploading files directly
C++ library
project dashboard
Edge Impulse: An MLOps Platform for Tiny Machine Learning
Managing collaborators on a project
Public project versioning on the Edge Impulse dashboard

with Docker on NVIDIA Jetson

Introduction

Welcome to the tutorial series on OTA Model Updates with Edge Impulse Docker Deploy on Jetson Nano! In this series, we will explore how to update machine learning models over-the-air (OTA) using Edge Impulse and Docker on the Jetson Nano platform.

Prerequisites

Before getting started, make sure you have the following prerequisites:

  • Jetson Nano Developer Kit

  • Docker installed on Jetson Nano

  • Edge Impulse account

  • Be familiar with Edge Impulse and Docker deploy

Overview

In this tutorial, we will explore how to enable GPU usage and use a camera with the Jetson Nano. We will then deploy a machine learning model using Edge Impulse and Docker on the Jetson Nano. Finally, we will update the model over-the-air (OTA) using Edge Impulse.

Step 1: Enable GPU Usage on Jetson Nano

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

After installing the toolkit, restart the Docker service:


sudo systemctl restart docker

Now you can use the GPU for machine learning tasks in Docker containers.

Step 2: Use a Camera with Jetson Nano

To use a camera with Jetson Nano, you need to install the libgstreamer and libv4l libraries. Run the following commands to install the libraries:


sudo apt-get update
sudo apt-get install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav libgstrtspserver-1.0-0 libv4l-0 v4l-utils

After installing the libraries, you can use a camera with Jetson Nano.

Step 3: Deploy Machine Learning Model with Edge Impulse and Docker

To deploy a machine learning model with Edge Impulse and Docker, follow these steps:

Export your model from Edge Impulse as a Docker container. Copy the generated Docker command from the deployment section. Modify the Docker command to use the GPU and camera on Jetson Nano. Run the Docker command on Jetson Nano to deploy the model.

Step 4: Update Model Over-the-Air (OTA) with Edge Impulse

To update the model over-the-air (OTA) with Edge Impulse, follow these steps:

Train a new model in Edge Impulse. Export the new model as a Docker container. Copy the generated Docker command from the deployment section and build a new Docker image.


docker build -t my_video_inference_container .
Modify the Docker command to use the GPU and camera on Jetson Nano.

```Dockerfile

FROM nvcr.io/nvidia/l4t-base:r32.5.0

# Install necessary dependencies for video streaming
RUN apt-get update && apt-get install -y \
    ffmpeg \
    v4l-utils \
    && rm -rf /var/lib/apt/lists/*

# Set environment variables
ENV DISPLAY=:0
ENV QT_X11_NO_MITSHM=1

# Expose port for streaming
EXPOSE 80

# Mount USB camera device
RUN ln -s /dev/bus/usb/001/002 /dev/video0

# Command to run your model as a Docker container
CMD ["docker", "run", "--rm", "-it", \
    "-p", "80:80", \
    "public.ecr.aws/z9b3d4t5/inference-container:73d6ea64bf931f338de5183438915dc390120d5d", \
    "--api-key", "ei_07e1e4fad55f06b30839c062076a2ad4bbc174386330493011e75566405a5603", \
    "--run-http-server", "1337"]
  1. Run the Docker command on Jetson Nano to deploy the new model.

docker run --rm -it --privileged --runtime nvidia -v /dev/bus/usb/001/002:/dev/video0 -p 80:80 public.ecr.aws/z9b3d4t5/inference-container:73d6ea64bf931f338de5183438915dc390120d5d --api-key ei_07e1e4fad55f06b30839c062076a2ad4bbc174386330493011e75566405a5603 --run-http-server 1337

  1. Test the new model on Jetson Nano.

  2. Monitor the model performance and update as needed.

Summary

In this tutorial series, we explored how to update machine learning models over-the-air (OTA) using Edge Impulse and Docker on the Jetson Nano platform. We enabled GPU usage, used a camera with Jetson Nano, deployed a machine learning model, and updated the model over-the-air.

Now you can easily update your machine learning models on Jetson Nano devices using Edge Impulse and Docker.

metadata->ei_check = 1
transformation block
Checklists in the your Data overview
AMS_010 hr.png
organizational data
transformation jobs
data pipelines
data source
custom transformation blocks
Data transformation
Data pipelines
Data sources
Custom transformation blocks
Transformation blocks overview page
Transformation job with 'file' transformation block selected
Transformation job with 'directory' transformation block selected
Transformation job with 'standalone' transformation block selected
User management
Data campaigns
Data
Data pipelines
Data transformations
Upload portals
Custom blocks
health reference design
Synchronizing clinical data with a bucket
Validating clinical data
Querying clinical data
Transforming clinical data
Buildling data pipelines
Organization dashboard
Enterprise organization usage metrics
documentation
Downloading files from organizational datasets.
Selecting only a subset of files through advanced filters.

CI/CD with GitHub Actions

Introduction

In today’s tech world, CI/CD (Continuous Integration/Continuous Deployment) is crucial for delivering fully tested and up-to-date software or firmware to your customers. This tutorial will guide you through integrating Edge Impulse Studio with GitHub workflows, enabling seamless build and deployment of your Edge Impulse model into your workflow.

Edge Impulse provides a comprehensive REST API for seamless integration with third-party services, allowing for the automation of tasks within Edge Impulse Studio. The GitHub Action we created available here simplifies the process of building and deploying models into your workflow.

This example was adapted from the Edge Impulse Blog - Integrate Your GitHub Workflow with Edge Impulse Studio By Mateusz Majchrzycki.

Prerequisites

  • GitHub repository for your firmware source code.

  • Edge Impulse project created in the Studio.

Steps

  1. Obtain Project ID and API Key

  • Navigate to your Edge Impulse project in the Studio.

  • Select "Dashboard" from the left pane, then click on "Keys" at the top.

  • Note down the Project ID and Project API Key.

  1. Add GitHub Action to Your Workflow

  • Open your workflow YAML file in your GitHub repository.

  • Add the following code to your workflow YAML file:

    - name: Build and deploy Edge Impulse Model
     uses: edgeimpulse/build-deploy@v1
     id: build-deploy
     with:
      project_id: ${{ secrets.PROJECT_ID }}
      api_key: ${{ secrets.API_KEY }}

    Replace ${{ secrets.PROJECT_ID }} and ${{ secrets.API_KEY }} with your actual Edge Impulse Project ID and API Key.

  1. Extract the Model and SDK

  • After the build and deployment action, you may want to extract the model and SDK.

  • Use the following example code in your workflow:

    - name: Extract the Model and SDK
     run: |
      mkdir temp
      unzip -q "${{ steps.build-deploy.outputs.deployment_file_name }}" -d temp
      mv temp/edge-impulse-sdk/ .
      mv temp/model-parameters/ .
      mv temp/tflite-model/ .
      rm -rf "${{ steps.build-deploy.outputs.deployment_file_name }}"
      rm -rf temp/
  1. Customize Deployment Type (Optional)

  • By default, the GitHub Action downloads the C++ library. You can customize the deployment type using the deployment_type input parameter. We can use a simple Python script here

  • Here's an example of downloading the Arduino library:

    - name: Build and deploy Edge Impulse Model
     uses: edgeimpulse/build-deploy@v1
     id: build-deploy
     with:
      project_id: ${{ secrets.PROJECT_ID }}
      api_key: ${{ secrets.API_KEY }}
      deployment_type: "arduino"
  1. Real-world Use Case

  • Utilize the GitHub Action for CI/CD purposes.

  • For example, testing public examples to prevent breaking changes.

  • Here's an example of using the Action with Nordic Semiconductor/Zephyr inference example:

GitHub Actions - IDE
```yaml
- name: Build and deploy EI Model
 uses: ./.github/actions/build-deploy
 id: build-deploy
 with:
  project_id: ${{ secrets.PROJECT_ID }}
  api_key: ${{ secrets.API_KEY }}
- name: Extract the EI Model
 run: |
  mkdir ei-model
  unzip -q "${{ steps.build-deploy.outputs.deployment_file_name }}" -d ei-model
  mv ei-model/edge-impulse-sdk/ .
  mv ei-model/model-parameters/ .
  mv ei-model/tflite-model/ .
  rm -rf "${{ steps.build-deploy.outputs.deployment_file_name }}"
  rm -rf ei-model/
- name: Build test app for nRF52840DK
 run: |
  docker run --rm -v $PWD:/app zephyr-ncs-1.9.1:latest west build -b nrf52840dk_nrf52840
```

6. Notification for Workflow Errors

  • Thanks to GitHub Actions notification, the person responsible for the commit that created an error in workflow will be notified.

Conclusion

Integrating Edge Impulse Studio with GitHub workflows enhances your CI/CD pipeline by automating the build and deployment process of your Edge Impulse models. This simplifies the development and testing of firmware, ensuring its accuracy and reliability. GitHub repository for your firmware source code. Edge Impulse project created in the Studio.

Label audio data using your existing models

This example comes from the Edge Impulse Linux Inferencing Python SDK that has been slightly modify to upload the raw data back to Edge Impulse based on the inference results.

To run the example:

  1. Clone this repository:

git clone [email protected]:edgeimpulse/example-active-learning-linux-python-sdk.git
  1. Install the dependencies:

pip3 install -r requirements.txt
  1. Grab your the API key of the project you want to upload the inferred results raw data:

EI API KEY
  1. Past the new key in the EI_API_KEY variable in the audio-classify-export.py file. Alternatively, load it from your environment variable:

export EI_API_KEY='ei_xxxx'
  1. Download your modelfile.eim:

edge-impulse-linux-runner --download modelfile.eim
  1. Run the script:

python3 audio-classify-export.py modelfile.eim yes,no 0.6 0.8

Here are the arguments you can set:

  • modelfile.eim, path the model.eim

  • yes,no, labels to upload, separated by comas, no space

  • 0.6, low confidence threshold

  • 0.8, high confidence threshold

  • <audio_device_ID, optional>

In a keyword spotting model, it can give the following results:

python3 audio-classify-export.py modelfile.eim yes,no 0.6 0.8
['modelfile.eim', 'yes,no', '0.6', '0.8']
{'model_parameters': {'axis_count': 1, 'frequency': 16000, 'has_anomaly': 0, 'image_channel_count': 0, 'image_input_frames': 0, 'image_input_height': 0, 'image_input_width': 0, 'input_features_count': 16000, 'interval_ms': 0.0625, 'label_count': 4, 'labels': ['no', 'noise', 'unknown', 'yes'], 'model_type': 'classification', 'sensor': 1, 'slice_size': 4000, 'use_continuous_mode': True}, 'project': {'deploy_version': 29, 'id': 10487, 'name': 'Keywords Detection', 'owner': 'Demo Team'}}
Loaded runner for "Demo Team / Keywords Detection"
0 --> MacBook Pro Microphone
2 --> Microsoft Teams Audio
3 --> Descript Loopback Recorder
4 --> ZoomAudioDevice
Type the id of the audio device you want to use: 
0
selected Audio device: 0

Result (0 ms.) no: 0.18 noise: 0.16     unknown: 0.20   yes: 0.46
Result (0 ms.) no: 0.13 noise: 0.58     unknown: 0.22   yes: 0.07
Result (0 ms.) no: 0.00 noise: 0.89     unknown: 0.10   yes: 0.01
Result (0 ms.) no: 0.00 noise: 0.01     unknown: 0.04   yes: 0.95
Result (0 ms.) no: 0.00 noise: 0.82     unknown: 0.10   yes: 0.07
Result (0 ms.) no: 0.02 noise: 0.77     unknown: 0.13   yes: 0.08
Result (0 ms.) no: 0.01 noise: 0.14     unknown: 0.26   yes: 0.59
Result (0 ms.) no: 0.07 noise: 0.76     unknown: 0.15   yes: 0.01
Result (0 ms.) no: 0.04 noise: 0.24     unknown: 0.11   yes: 0.61       Uploading sample to Edge Impulse...
Successfully uploaded audio to Edge Impulse.

Result (0 ms.) no: 0.02 noise: 0.93     unknown: 0.04   yes: 0.00
Result (0 ms.) no: 0.01 noise: 0.67     unknown: 0.32   yes: 0.01
Result (0 ms.) no: 0.02 noise: 0.18     unknown: 0.23   yes: 0.57
Result (0 ms.) no: 0.07 noise: 0.70     unknown: 0.22   yes: 0.01
Result (0 ms.) no: 0.03 noise: 0.83     unknown: 0.12   yes: 0.02
Result (0 ms.) no: 0.24 noise: 0.44     unknown: 0.21   yes: 0.11
Result (0 ms.) no: 0.23 noise: 0.25     unknown: 0.42   yes: 0.10
Result (0 ms.) no: 0.04 noise: 0.76     unknown: 0.18   yes: 0.02
Result (0 ms.) no: 0.16 noise: 0.67     unknown: 0.12   yes: 0.05
Result (0 ms.) no: 0.12 noise: 0.81     unknown: 0.06   yes: 0.01
Result (0 ms.) no: 0.54 noise: 0.24     unknown: 0.12   yes: 0.10
Result (0 ms.) no: 0.01 noise: 0.91     unknown: 0.05   yes: 0.03
Result (0 ms.) no: 0.65 Uploading sample to Edge Impulse...
Successfully uploaded audio to Edge Impulse.
noise: 0.08     unknown: 0.17   yes: 0.10
Result (0 ms.) no: 0.00 noise: 0.96     unknown: 0.03   yes: 0.00
Result (0 ms.) no: 0.04 noise: 0.80     unknown: 0.13   yes: 0.03
Result (0 ms.) no: 0.03 noise: 0.27     unknown: 0.16   yes: 0.54
Result (0 ms.) no: 0.05 noise: 0.66     unknown: 0.15   yes: 0.14
Result (0 ms.) no: 0.08 noise: 0.74     unknown: 0.14   yes: 0.04
Result (0 ms.) no: 0.01 noise: 0.87     unknown: 0.11   yes: 0.02
Result (0 ms.) no: 0.01 noise: 0.87     unknown: 0.06   yes: 0.06
...
results

with Docker on Allxon

Allxon provides essential remote device management solutions to simplify and optimize edge AI device operations. As an AI/IoT ecosystem enabler, connecting hardware (IHV), software (ISV), and service providers (SI/MSP), Allxon serves as the catalyst for fast, seamless connectivity across all systems.

Allxon - Device management

Allxon Over-the-Air (OTA) deployment works perfectly with Edge Impulse OTA model update on NVIDIA Jetson devices. This tutorial guides you through the steps to deploy a new impulse on multiple devices.

Introduction

This guide demonstrates how to deploy and manage Edge Impulse models on NVIDIA Jetson devices using Allxon's Over-the-Air (OTA) deployment capabilities. Allxon provides essential remote device management solutions to streamline and optimize edge AI device operations.

Prerequisites

Before you begin, ensure you have the following:

  1. Updated impulse as a Docker container from Edge Impulse.

  2. Get Allxon officially supported devices(https://www.allxon.com/).

  3. Create an Allxon account.

Getting Started with Allxon

Allxon's services are compatible with a variety of hardware models. Follow these steps to complete the required preparations.

Add a Device to Allxon Portal

  1. Install Allxon Agent: Use the command prompt to install the Allxon Agent on your device.

  2. Pair Your Device: Follow the instructions to add your device to Allxon Portal.

Once added, your devices will appear in the Allxon Portal for management and monitoring.

Allxon OTA Deployment

To perform an OTA deployment, ensure you have your updated Impulse deployed as a Docker container from Edge Impulse.

Steps to Deploy

  1. Generate OTA Artifact: Use the Allxon CLI to generate the OTA artifact.

  2. Deploy OTA Artifact: Follow the Deploy OTA Artifact guide to complete the deployment.

Example Scripts

Below are example scripts to help you set up the OTA deployment.

ota_deploy.sh

#!/bin/bash
set -e
mkdir -p /opt/allxon/tmp/core/appies_ota/model_logs/
./install.sh > /opt/allxon/tmp/core/appies_ota/model_logs/log.txt 2>&1
echo "Model deployment has started. Please check /opt/allxon/tmp/core/appies_ota/model_logs/log.txt for progress."

install.sh


#!/bin/bash
docker run --rm --privileged --runtime nvidia \
 -v /dev/bus/usb/001/002:/dev/video0 \
 -p 80:80 \
 public.ecr.aws/z9b3d4t5/inferencecontainer:73d6ea64bf931f338de5183438915dc390120d5d \
 --api-key <replace with your project api key e.g. ei_07XXX > \
 --run-http-server 1337 &

Two minor modifications have been made to the standard Docker command from Edge Impulse docker deploy:

The -it option has been removed from the Docker command to avoid an error related to the lack of standard input during deployment. An & has been added to the end of the Docker command to send the process to the background.

Conclusion

By following these steps, you can efficiently deploy and manage Edge Impulse models on NVIDIA Jetson devices using Docker through Allxon. This setup leverages Allxon's remote management capabilities to streamline the process of updating and maintaining your edge AI devices.

We hope this section has helped you understand the process of Lifecycle Management and how to implement it in your own project. If you have any questions, please reach out to us on our forum.

Users

Within an organization you can work on one or more projects with multiple people. These can be colleagues, outside researchers, or even members of the community. They will only get access to the specific data in the project, and not to any of the raw data in your organizational datasets.

List of users in an organization view

To invite a user in an organization, click on the "Add user button, enter the email address and select the role:

Invite user to an organization

Organization Users vs Project Users

It is important to note that there are two types of users in Edge Impulse: Project Users and Organization Users.

Organization Users, typically holding roles like Admin, are responsible for the overarching management and customization of organizational elements, including datasets, storage buckets, and white label attributes. These users also encompass the capabilities of Project Users.

Conversely, Project Users, often in roles such as Member or Guest, are limited to specific project involvement, focusing on collaboration and contributions at the project level, without access to broader organizational management functions. They are granted access only to certain project data to maintain the security of raw data in organizational datasets.

Organization User Roles

For a more granular look at the capabilities of each role, see the table below:

Admin

Admins have full rights on the organization, overseeing organizational and white label functionalities, including dataset management and storage bucket updates. They also have all the rights of a Project Member.

  • Full Rights on the Organization

  • Project User rights

  • Manage organization datasets

  • Update and add storage buckets

  • Verify bucket connectivity

  • Customize white label (where applicable) attributes like themes and information

  • API access for organization and white label management

Member

Members have full access on the datasets, custom blocks but cannot join a project without being invited.

  • Broad Access, with Restrictions on Project Joining

  • Project User rights

  • Full access to datasets and custom blocks

  • Can collaborate on projects, but only by invitation

  • Can access metrics via API

Guest

Guests have restricted access, limited to selected datasets within the projects they are associated with.

  • Limited Access to Selected Datasets

  • Project User rights

  • Access to selected datasets within the project they are invited to

  • Cannot access raw data in organizational datasets

  • Cannot access metrics via API

To give someone access to a project only, go to your project's dashboard, and find the "Collaborators" widget. Click the '+' icon, and type the username or e-mail address of the other user.

Giving a user access to your Edge Impulse project through the Collaborators widget.

Data campaigns

The "data campaigns" feature allows you to quickly track your experiments and your models' development progresses. It is an overview of your pipelines where you can easily extract useful information from your datasets and correlate those metrics with your model performances.

It has been primarily designed to follow clinical research data processes. In August 2023, we released this feature for every enterprise user as we see value in being able to track metrics between your datasets and your projects.

Data campaigns overview

Setting up your dashboard

To get started, navigate to the Data campaigns tab in your organization:

Data campaigns

Click on + Create new dashboard.

Give your dashboard a name, and select one or more collaborators to receive the daily updates by email. If you don't want to be spammed, you can select when you want to receive these updates, either Always, On new data, changes or on error, or Never. Finally, set the last number of days shown in the graphs:

Add data campaign dashboard

You can create as many dashboards as needed, simply click on + Create a new dashboard from the dropdown available under your current dashboard:

If you want to delete a dashboard, Click on Actions... -> Delete dashboard

Setting up your campaign

Once your dashboard is created, you can add your custom campaigns. It's where you will specify which metrics are important to you and your use case. Click on Actions... -> Add campaign

Add data campaign

Fill the form to create your campaign:

Name: Name of your data campaign.

Description: Description of your data campaign.

Campaign coordinators: Add the collaborators that are engaged with this campaign

Datasets: Select the datasets you want to visualize in your campaign. You can add several datasets.

Projects: Select the projects you want to visualize in your campaign. You can add several projects.

Pipelines: Select the pipeline that is associated with your campaign.Note that this is for reference only, it is currently not displayed in your campaign

Links: Select between the link type you need. Options are Github, Spreadsheet, Text Document, Code repository, List or Folder. Add a name and the link. This place is useful for other collaborators to have all the needed information about your project, gathered in one place under your campaign.

Addition queries to track: These queries are data filters that need to be written in the SQL WHERE format. See Querying data for more information. For example metadata->age >= 18` will return the data samples from adult patients.

You can then save your data campaign and it will be added to your dashboard:

Create or edit data campaign

This dashboard shows the metrics' progress from the Health reference design data

Create or edit data campaign

If you want to edit or delete your campaign, click on the "⋮" button on the right side of your campaign:

Edit campaign

Generate audio datasets using Eleven Labs

Generate audio data using the Eleven Labs Sound Effects models. This integration allows you to generate realistic sound effects for your projects, such as glass breaking, car engine revving, or other custom sounds. You can customize the sound prompts and generate high-quality audio samples for your datasets.

This integration allows you to expand your datasets with sounds that may be difficult or expensive to record naturally. This approach not only saves time and money but also enhances the accuracy and reliability of the models we deploy on edge devices.

Example: Glass Breaking Sound

In this tutorial, we focus on a practical application that can be used in a smart security system, or in a factory to detect incidents, such as detecting the sounds of glass breaking.

There is also a video version of this guide:

Getting Started

You will also need an Eleven Labs account and API Key.

Navigate to Data Acquisition: Once you're in your project, navigate to the Data Acquisition section, go to Synthetic data and select the ElevenLabs Synthetic Audio Generator data source.

Generate audio with ElevenLabs

Step 1: Get your ElevenLabs API Key

First, get your Eleven Labs API Key. Navigate to the Eleven Labs web interface to get your key and optionally test your prompt.

Step 2: Parameters

Here we will be trying to collect a glass-breaking sound or impact.

  • Prompt: "glass breaking"

    Simple prompts are just that: they are simple, one-sided prompts where we try to get the AI to generate a single sound effect. This could be, for example, “person walking on grass” or “glass breaking.” These types of prompts will generate a single type of sound effect with a few variations either in the same generation or in subsequent generations. All in all, they are fairly simple.

    There are a few ways to improve these prompts, however, and that is by adding a little bit more detail. Even if they are simple prompts, they can be made to give better output by improving the prompt itself. For example, something that sometimes works is adding details like “high-quality, professionally recorded footsteps on grass, sound effects foley.” It can require some experimentation to find a good balance between being descriptive and keeping it brief enough to have AI understand the prompt. e.g. high quality audio of window glass breaking

  • Label: The label of the generated audio sample.

  • Prompt influence: Between 0 and 1, this setting ranges from giving the AI more creativity in how it interprets the prompt to telling the AI to be more strict in following the exact prompt that you’ve given. 1 being more creative.

  • Number of samples: Number of samples to generate

  • Minimum length (seconds): Minimum length of generated audio samples. Audio samples will be padded with silence to minimum length. It also determines how long your generations should be. Depending on what you set this as, you can get quite different results. For example, if I write “kick drum” and set the length to 11 seconds, I might get a full drum loop with a kick drum in it, but that might not be what I want. On the other hand, if I set the length to 1 second, I might just get a one-shot with a single instance of a kick drum.

  • Frequency (Hz): Audio frequency, ElevenLabs generates data at 44100Hz, so any other value will be resampled.

  • Upload to category: Data will be uploaded to this category in your project.

See ElevenLabs API documentation for more information

Step 3: Generate samples

Once you've set up your prompt, and api key, run the pipeline to generate the sound samples. You can then view the output in the Data Acquisition section.

Benefits of Using Generative AI for Sound Generation

  • Enhance Data Quality: Generative AI can create high-quality sound samples that are difficult to record naturally.

  • Increase Dataset Diversity: Access a wide range of sounds to enrich your training dataset and improve model performance.

  • Save Time and Resources: Quickly generate the sound samples you need without the hassle of manual recording.

  • Improve Model Accuracy: High-quality, diverse sound samples can help fill gaps in your dataset and enhance model performance.

Conclusion

By leveraging generative AI for sound generation, you can enhance the quality and diversity of your training datasets, leading to more accurate and reliable edge AI models. This innovative approach saves time and resources while improving the performance of your models in real-world applications. Try out the Eleven Labs block in Edge Impulse today and start creating high-quality sound datasets for your projects.

For ML practitioners

Welcome to Edge Impulse! Whether you are a machine learning engineer, MLOps engineer, data scientist, or researcher, we have developed professional tools to help you build and optimize models to run efficiently on any edge device.

In this guide, we'll explore how Edge Impulse empowers you to bring your expertise and your own models to the world of edge AI using either the Edge Impulse Studio, our visual interface, and the Edge Impulse Python SDK, available as a pip package.

Why Edge Impulse, for ML practitioners?

Flexibility: You can choose to work with the tools they are already familiar with and import your models, architecture, and feature processing algorithms into the platform. This means that you can leverage your existing knowledge and workflows seamlessly. Or, for those who prefer an all-in-one solution, Edge Impulse provides enterprise-grade tools for your entire machine-learning pipeline.

Optimized for edge devices: Edge Impulse is designed specifically for deploying machine learning models on edge devices, which are typically resource-constrained, from low-power MCUs up to powerful edge GPUs. We provide tools to optimize your models for edge deployment, ensuring efficient resource usage and peak performance. Focus on developing the best models, we will provide feedback on whether they can run on your hardware target!

Data pipelines: We developed a strong expertise in complex data pipelines (including clinical data) while working with our customers. We support data coming from multiple sources, in any format, and provide tools to perform data alignment and validation checks. All of this in customizable multi-stage pipelines. This means you can build gold-standard labeled datasets that can then be imported into your project to train your models.

Getting started in a few steps

In this getting started guide, we'll walk you through the two different approaches to bringing your expertise to edge devices. Either starting from your dataset or from an existing model.

First, start by creating your .

Start with existing data

You can import data using , , or our . These allow you to easily upload and manage your existing data samples and datasets to Edge Impulse Studio.

We currently accept various file types, including .cbor, .json, .csv, .wav, .jpg, .png, .mp4, and .avi.

If you are working with image datasets, the Studio uploader and the CLI uploader currently handle these types of : Edge Impulse object detection, COCO JSON, Open Images CSV, Pascal VOC XML, Plain CSV, and YOLO TXT.

Organization data

Since the creation of Edge Impulse, we have been helping our customers deal with complex data pipelines, complex data transformation methods and complex clinical validation studies.

The organizational data gives you tools to centralize, validate and transform datasets so they can be easily imported into your projects.

See the documentation.

To visualize how your labeled data items are clustered, use the feature available for most dataset types, where we apply dimensionality reduction techniques (t-SNE or PCA) on your embeddings.

To extract features from your data items, either choose an available (MFE, MFCC, spectral analysis using FFT or Wavelets, etc.) or from your expertise. These can be written in any language.

Similarly, to train your machine learning model, you can choose from different (Classification, Anomaly Detection, Regression, Image or Audio Transfer Learning, Object Detection). In most of these blocks, we expose the Keras API in an . You can also bring your own architecture/training pipeline as a .

Each block will provide on-device performance information showing you the estimated RAM, flash, and latency.

Start with an existing model

If you already have been working on different models for your Edge AI applications, Edge Impulse offers an easy way to upload your models and profile them. This way, in just a few minutes, you will know if your model can run on real devices and what will be the on-device performances (RAM, flash usage, and latency).

You can do this directly from the or using .

Edge Impulse Python SDK is available as a pip package:

From there, you can profile your existing models:

And then directly generate a customizable library or any other supported

Run the inference on a device

You can easily in a .eim format, a Linux executable that contains your signal processing and ML code, compiled with optimizations for your processor or GPU. This executable can then be called with our . We have inferencing libraries and examples for Python, Node.js, C++, and Go.

If you target MCU-based devices, you can generate ready-to-flash binaries for all the officially supported hardware targets. This method will let you test your model on real hardware very quickly.

In both cases, we will provide profiling information about your models so you can make sure your model will fit your edge device constraints.

Tutorials and resources, for ML practitioners

End-to-end tutorials

If you want to get familiar with the full end-to-end flow using Edge Impulse Studio, please have a look at our :

  • ,

  • ,

  • ,

  • ,

  • ,

To understand the full potential of Edge Impulse, see our that describes an end-to-end ML workflow for building a wearable health product using Edge Impulse. It handles data coming from multiple sources, data alignment, and a multi-stage pipeline before the data is imported into an Edge Impulse project.

Edge Impulse Python SDK tutorials

While the Edge Impulse Studio is a great interface for guiding you through the process of collecting data and training a model, the Python SDK allows you to programmatically Bring Your Own Model (BYOM), developed and trained on any platform:

Other useful resources

  • (access Keras API in the studio)

Integrations

Environmental (Sensor fusion)

Neural networks are not limited to working with one type of data at a time. One of their biggest advantages is that they are incredibly flexible with the type of input data, so long as the format and ordering of that data stays the same from training to inference. As a result, we can use them to perform sensor fusion for a variety of tasks.

Sensor fusion is the process of combining data from different types of sensors or similar sensors mounted in different locations, which gives us more information to make decisions and classifications. For example, you could use temperature data with accelerometer data to get a better idea of a potential anomaly!

In this tutorial, you will learn how to use Edge Impulse to perform sensor fusion on the Arduino Nano 33 BLE Sense.

Example Project: You can find the dataset and impulse used throughout this tutorial in .

Multi-impulse vs multi-model vs sensor fusion

Running multi-impulse refers to running two separate projects (different data, different DSP blocks and different models) on the same target. It will require modifying some files in the EI-generated SDKs. See the

Running multi-model refers to running two different models (same data, same DSP block but different tflite models) on the same target. See how to run a motion classifier model and an anomaly detection model on the same device in .

Sensor fusion refers to the process of combining data from different types of sensors to give more information to the neural network. To extract meaningful information from this data, you can use the same DSP block (like in this tutorial), multiples DSP blocks, or use neural networks embeddings like this tutorial.

1. Prerequisites

For this tutorial, you'll need a .

2. Building a dataset

For this demo, we'll show you how to identify different environments by using a fusion of temperature, humidity, pressure, and light data. In particular, I'll have the Arduino board identify different rooms in my house as well as outside. Note that the we assume that the environment is static--if I turn out lights or the outside temperature changes, the model will not work. However, it demonstrates how we can combine different sensor data with machine learning to do classification!

As we will be collecting data from our Arduino board connected to a computer, it helps to have a laptop that you can move to different rooms.

Create a new project on the Edge Impulse studio.

Connect the Arduino Nano 33 BLE to your computer. Follow the to upload the Edge Impulse firmware to the board and connect it to your project.

Go to Data acquisition. Under Record new data, select your device and set the label to bedroom. Change Sensor to Environmental + Interactional, set the Sample length to 10000 ms and Frequency to 12.5Hz.

Stand in one of your rooms with your Arduino board (and laptop). Click Start sampling and slowly move the board around while data is collected. After sampling is complete, you should see a new data plot with a different line for each sensor.

Variations

Try to stand in different parts of each room while collecting data.

Repeat this process to record about 3 minutes of data for the bedroom class. Try to stand in a different spot in the room while collecting data--we want a robust dataset that represents the features of each room. Head to another room and repeat data collection. Continue doing this until you have around 3 minutes of data for each of the following classes:

  • Bedroom

  • Hallway

  • Outside

You are welcome to try other rooms or locations. For this demo, I found that my bedroom, kitchen, and living room all exhibited similar environmental and lighting properties, so the model struggled to tell them apart.

Head to Dashboard and scroll down to Danger zone. Click Perform train/test split and follow the instructions in the pop-up window to split your dataset into training and testing groups. When you're done, you can head back to Data acquisition to see that your dataset has been split. You should see about 80% of your samples in Training data and about 20% in Test data.

4. Design an Impulse

An impulse is a combination of preprocessing (DSP) blocks followed by machine learning blocks. It will slice up our data into smaller windows, use signal processing to extract features, and then train a machine learning model. Because we are using environmental and light data, which are slow-moving averages, we will use the for preprocessing.

Head to Create impulse. Change the Window increase to 500 ms. Add a Flatten block. Notice that you can choose which environmental and interactional sensor data to include. Deselect proximity and gesture, as we won't need those to detect rooms. Add a Classification (Keras) learning block

Click Save impulse.

5. Configure the Flatten block

Head to Flatten. You can select different samples and move the window around to see what the DSP result will look like for each set of features to be sent to the learning block.

The Flatten block will compute the average, minimum, maximum, root-mean square, standard deviation, skewness, and kurtosis of each axis (e.g. temperature, humidity, brightness, etc.). With 7 axes and 7 features computed for each axis, that gives us 49 features for each window being sent to the learning block. You can see these computed features under Processed features.

Click Save parameters. On the next screen, select Calculate feature importance and click Generate features.

After a few moments, you should be able to explore the features of your dataset to see if your classes are easily separated into categories.

You can also look at the Feature importance section to get an idea of which features are the most important in determining class membership. You can read more about feature importance .

Interestingly enough, it looks like temperature and red light values were the most important features in determining the location of the Arduino board.

6. Configure the neural network

With our dataset collected and features processed, we can train our machine learning model. Click on NN Classifier. Change the Number of training cycles to 300 and click Start training. We will leave the neural network architecture as the default for this demo.

During training, parameters in the neural network's neurons are gradually updated so that the model will try to guess the class of each set of data as accurately as possible. When training is complete, you should see a Model panel appear on the right side of the page.

The Confusion matrix gives you an idea of how well the model performed at classifying the different sets of data. The top row gives the predicted label and the column on the left side gives the actual (ground-truth) label. Ideally, the model should predict the classes correctly, but that's not always the case. You want the diagonal cells from the top-left to the bottom-right to be as close to 100% as possible.

If you see a lot of confusion between classes, it means you need to gather more data, try different features, use a different model architecture, or train for a longer period of time (more epochs). See to learn about ways to increase model performance.

The On-device performance provides some statistics about how the model will likely run on a particular device. By default, an Arm Cortex-M4F running at 80 MHz is assumed to be your target device. The actual memory requirements and run time may vary on different platforms.

7. Model testing

Rather than simply assume that our model will work when deployed, we can run inference on our test dataset as well as on live data.

First, head to Model testing, and click Classify all. After a few moments, you should see results from your test set.

You can click on the three dots next to an item and select Show classification. This will give you a classification result screen where you can see results information in more detail.

Additionally, we can test the impulse in a real-world environment to make sure the model has not overfit the training data. To do that, head to Live classification. Make sure your device is connected to the Studio and that the Sensor, Sample length, and Frequency match what we used to initially capture data.

Click Start sampling. A new sample will be captured from your board, uploaded, and classified. Once complete, you should see the classification results.

In the example above, we sampled 10 seconds of data from the Arduino. This data is split into 1-second windows (the window moves over 0.5 seconds each time), and the data in that window is sent to the DSP block. The DSP block computes the 49 features that are then sent to the trained machine learning model, which performs a forward pass to give us our inference results.

As you can see, the inference results from all of the windows claimed that the Arduino board was in the bedroom, which was true! This is great news for our model--it seems to work even on unseen data.

8. Running the impulse on your device

Now that we have an impulse with a trained model and we've tested its functionality, we can deploy the model back to our device. This means the impulse can run locally without an internet connection to perform inference!

Edge Impulse can package up the entire impulse (preprocessing block, neural network, and classification code) into a single library that you can include in your embedded software.

Click on Deployment in the menu. Select the library that you would like to create, and click Build at the bottom of the page.

Running your impulse locally

See to learn how to deploy your impulse to a variety of platforms.

9. Conclusion

Well done! You've trained a neural network to determine the location of a development board based on a fusion of several sensors working in tandem. Note that this demo is fairly limited--as the daylight or temperature changes, the model will no longer be valid. However, it hopefully gives you some ideas about how you can mix and match sensors to achieve your machine learning goals.

If you're interested in more, see our tutorials on or . If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Image classification

In this tutorial, you'll use machine learning to build a system that can recognize objects in your house through a camera - a task known as image classification - connected to a microcontroller. Adding sight to your embedded devices can make them see the difference between poachers and elephants, do quality control on factory lines, or let your RC cars drive themselves. In this tutorial you'll learn how to collect images for a well-balanced dataset, how to apply transfer learning to train a neural network, and deploy the system to an embedded device.

At the end of this tutorial, you'll have a firm understanding of how to classify images using Edge Impulse.

There is also a video version of this tutorial:

You can view the finished project, including all data, signal processing and machine learning blocks here: .

1. Prerequisites

For this tutorial, you'll need a .

If you don't have any of these devices, you can also upload an existing dataset through the . After this tutorial you can then deploy your trained machine learning model as a C++ library and run it on your device.

2. Building a dataset

In this tutorial we'll build a model that can distinguish between two objects in your house - we've used a plant and a lamp, but feel free to pick two other objects. To make your machine learning model see it's important that you capture a lot of example images of these objects. When training the model these example images are used to let the model distinguish between them. Because there are (hopefully) a lot more objects in your house than just lamps or plants, you also need to capture images that are neither a lamp or a plant to make the model work well.

Capture the following amount of data - make sure you capture a wide variety of angles and zoom levels:

  • 50 images of a lamp.

  • 50 images of a plant.

  • 50 images of neither a plant nor a lamp - make sure to capture a wide variation of random objects in the same room as your lamp or plant.

You can collect data from the following devices:

  • - for all other officially supported boards with camera sensors.

Or you can capture your images using another camera, and then upload them by going to Data acquisition and clicking the 'Upload' icon.

Afterwards you should have a well-balanced dataset listed under Data acquisition in your Edge Impulse project. You can switch between your training and testing data with the two buttons above the 'Data collected' widget.

3. Designing an impulse

With the training set in place you can design an impulse. An impulse takes the raw data, adjusts the image size, uses a preprocessing block to manipulate the image, and then uses a learning block to classify new data. Preprocessing blocks always return the same values for the same input (e.g. convert a color image into a grayscale one), while learning blocks learn from past experiences.

For this tutorial we'll use the 'Images' preprocessing block. This block takes in the color image, optionally makes the image grayscale, and then turns the data into a features array. If you want to do more interesting preprocessing steps - like finding faces in a photo before feeding the image into the network -, see the tutorial. Then we'll use a 'Transfer Learning' learning block, which takes all the images in and learns to distinguish between the three ('plant', 'lamp', 'unknown') classes.

In the studio go to Create impulse, set the image width and image height to 96, and add the 'Images' and 'Transfer Learning (Images)' blocks. Then click Save impulse.

Configuring the processing block

To configure your processing block, click Images in the menu on the left. This will show you the raw data on top of the screen (you can select other files via the drop down menu), and the results of the processing step on the right. You can use the options to switch between 'RGB' and 'Grayscale' mode, but for now leave the color depth on 'RGB' and click Save parameters.

This will send you to the 'Feature generation' screen. In here you'll:

  • Resize all the data.

  • Apply the processing block on all this data.

  • Create a 3D visualization of your complete dataset.

Click Generate features to start the process.

Afterwards the 'Feature explorer' will load. This is a plot of all the data in your dataset. Because images have a lot of dimensions (here: 96x96x3=27,648 features) we run a process called 'dimensionality reduction' on the dataset before visualizing this. Here the 27,648 features are compressed down to just 3, and then clustered based on similarity. Even though we have little data you can already see some clusters forming (lamp images are all on the right), and can click on the dots to see which image belongs to which dot.

Configuring the transfer learning model

With all data processed it's time to start training a neural network. Neural networks are a set of algorithms, modeled loosely after the human brain, that are designed to recognize patterns. The network that we're training here will take the image data as an input, and try to map this to one of the three classes.

It's very hard to build a good working computer vision model from scratch, as you need a wide variety of input data to make the model generalize well, and training such models can take days on a GPU. To make this easier and faster we are using transfer learning. This lets you piggyback on a well-trained model, only retraining the upper layers of a neural network, leading to much more reliable models that train in a fraction of the time and work with substantially smaller datasets.

To configure the transfer learning model, click Transfer learning in the menu on the left. Here you can select the base model (the one selected by default will work, but you can change this based on your size requirements), optionally enable data augmentation (images are randomly manipulated to make the model perform better in the real world), and the rate at which the network learns.

Set:

  • Number of training cycles to 20.

  • Learning rate to 0.0005.

  • Data augmentation: enabled.

  • Minimum confidence rating: 0.7.

Important: If you're using a development board with less memory, like the click Choose a different model and select MobileNetV1 96x96 0.25. This is a smaller transfer learning model.

And click Start training. After the model is done you'll see accuracy numbers, a confusion matrix and some predicted on-device performance on the bottom. You have now trained your model!

4. Validating your model

With the model trained let's try it out on some test data. When collecting the data we split the data up between a training and a testing dataset. The model was trained only on the training data, and thus we can use the data in the testing dataset to validate how well the model will work in the real world. This will help us ensure the model has not learned to overfit the training data, which is a common occurrence.

To validate your model, go to Model testing, select the checkbox next to 'Sample name' and click Classify selected. Here we hit 89% accuracy, which is great for a model with so little data.

To see a classification in detail, click the three dots next to an item, and select Show classification. This brings you to the Live classification screen with much more details on the file (if you collected data with your mobile phone you can also capture new testing data directly from here). This screen can help you determine why items were misclassified.

5. Running the model on your device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the preprocessing steps, neural network weights, and classification code - in a single C++ library that you can include in your embedded software.

To run your impulse on either the OpenMV camera or your phone, follow these steps:

  • OpenMV Cam H7 Plus:

  • Mobile phone: just click Switch to classification mode at the bottom of your phone screen.

For other boards: click on Deployment in the menu. Then under 'Build firmware' select your development board, and click Build. This will export the impulse, and build a binary that will run on your development board in a single step. After building is completed you'll get prompted to download a binary. Save this on your computer.

Flashing the device

When you click the Build button, you'll see a pop-up with text and video instructions on how to deploy the binary to your particular device. Follow these instructions. Once you are done, we are ready to test your impulse out.

Running the model on the device

We can connect to the board's newly flashed firmware over serial. Open a terminal and run:

To also see a preview of the camera, run:

To run continuous (without a pause every 2 seconds), but without the preview, run:

Congratulations! You've added sight to your sensors. Now that you've trained your model you can integrate your impulse in the firmware of your own embedded device, see . There are examples for Mbed OS, Arduino, STM32CubeIDE, and any other target that supports a C++ compiler. Note that the model we trained in this tutorial is relatively big, but you can choose a smaller transfer learning model.

Or if you're interested in more, see our tutorials on or . If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Label image data using GPT-4o

In this tutorial, we will explore how to label image data using GPT-4o, a powerful language model developed by OpenAI. GPT-4o is capable of generating accurate and meaningful labels for images, making it a valuable tool for image classification tasks. By leveraging the capabilities of GPT-4o, we can automate the process of labeling image data, saving time and effort in data preprocessing.

We packaged in a "pre-built " (available for all Enterprise plans), an innovative method to distill LLM knowledge.

This pre-built transformation block can be found under the tab in the Data acquisition view.

The block takes all your unlabeled image files and asks GPT-4o to label them based on your prompt - and we automatically add the reasoning as metadata to your items!

Your prompt should return a single label, e.g.

How to use it

The GPT-4o model processes images and assigns labels based on the content, filtering out any images that do not meet the quality criteria.

Step 1: Data Collection

Navigate to the page and add images to your project's dataset. In the video tutorial above, we show how to collect a video recorded directly from a phone, upload it to Edge Impulse and split the video into individual frames.

Step 2: Add the labeling block

In the tab, add the "Label image data using GPT-4o" block:

Step 4: Configure the labeling block

  • OpenAI API key: Add your OpenAI API key. This value will be stored as a secret, and won't be shown again.

  • Prompt: Your prompt should return a single label. For example:

  • Disable samples w/ label: If a certain label is output, disable the data item - these are excluded from training. Multiple labels are accepted, separate them with a coma.

  • Max. no. of samples to label: Number of samples to label.

  • Concurrency: Number of samples to label in parallel.

  • Auto-convert videos: If set, all videos are automatically split into individual images before labeling.

Optional: Editing your labeling block

To edit your configuration, you need to update the json-like steps of your block:

Step 5: Execute

Then, run the block to automatically label the frames.

And here is an example of the returned logs:

Step 6: Train your model

Use the labeled data to train a machine learning model. See the end-to-end tutorial .

Step 7: Deployment

In the video tutorial, we deployed the trained model to an MCU-based edge device - the Arduino Nicla Vision.

Results

The small model we tested this on performed exceptionally well, identifying toys in various scenes quickly and accurately. By distilling knowledge from the large LLM, we created a specialized, efficient model suitable for edge deployment.

Conclusion

The latest multimodal LLMs are incredibly powerful but too large for many practical applications. At Edge Impulse, we enable the transfer of knowledge from these large models to smaller, specialized models that run efficiently on edge devices.

Our "Label image data using GPT-4o" block is available for enterprise customers, allowing you to experiment with this technology.

For further assistance, visit our .

Examples & Resources

  • Blog post:

OTA model updates

Introduction

In this section we will explore how firmware updates and other scenarios are currently addressed, with traditional OTA. It should help you to get started planning your own updated impulse across a range of platforms.

Starting with platform-specific examples like Arduino Cloud, Nordic nRF Connect SDK / Zephyr and Golioth, Particle Workbench and Blues Wireless. Finally we will explore building an end-to-end example on the Espressif IDF.

By covering a cross section of platforms we hope to provide a good overview of the process and how it can be applied to your own project. With more generic examples like Arduino, Zephyr and C++ which can be applicable to all other vendors.

These tutorials will help you to get started with the following platforms:

Prerequisites

  • Edge Impulse Account: If you haven't got one, .

  • Trained Impulse: If you're new, follow one of our

Overview

Edge Impulse recognises the need for OTA model updates, as the process is commonly referred to although we are going to be updating the impulse which includes more than just a model, a complete review of your infrastructure is required. Here is an example of the process:

Detect a change

The initiation of an update to your device can be as straightforward as a call to our API to verify the availability of a new deployment. This verification can be executed either by a server or a device with adequate capabilities. Changes can be dependent on a range of factors, including but not limited to the last modified date of the project, the performance of the model, or the release version of the project e.g. last modified date of the project endpoint:

Download the latest impulse

After we inquire about the last modification, and if an update is available, proceed to download the latest build through:

We could add further checking for impulse model performance, project release version tracking or other metrics to ensure the update is valid. However in this series we will try to keep it simple and focus on the core process. Here is an example of a more complete process:

  • Identify components that influence change: Determine the components of your project that require updates. This could be based on performance metrics, data drift, or new data incorporation.

  • Retrain: Focus on retraining based on the identified components of your project.

  • Test and Validate: Before deploying the updated components, ensure thorough testing and validation to confirm their performance before sending the update.

  • Deploy Updated Components: Utilize available OTA mechanisms to deploy the updated components to your devices. Ensure seamless integration with the existing deployment that remains unchanged.

  • Monitor on device Performance: Post-deployment, continuously monitor the performance of the updated model to ensure it meets the desired objectives. See Lifecycle Management for more details.

The aim will be to make sure your device is always equipped with the most recent and efficient impulse, enhancing performance and accuracy.

Conclusion

We hope this section has helped you to understand the process of OTA model updates and how to implement it in your own project. If you have any questions, please reach out to us on our .

Classification with multiple 2D input features

Neural networks can work with multiple types of data simultaneously, allowing for sophisticated sensor fusion techniques. Here we demonstrate how to use a single model to perform classification using two separate sets of two-dimensional (2D) input features.

For this tutorial, the two sets of input features are assumed to be spectrograms that are passed through two different convolutional branches of a neural network. The two branches of the network are then combined and passed through a final dense output layer for classification. In the general case, however, there could be more than two branches and the input features could represent different sensor data, channels, or outputs from feature extractors.

This example is for when you would like to pass sets of input features to independent branches of a neural network and then combine the results for a final classification.

If you plan on passing all input features through a fully connected network, then these steps are unnecessary; Edge Impulse automatically concatenates output features from processing blocks and learning blocks operate on the concatenated array.

Example use cases

By using this architecture, you can perform sophisticated sensor fusion tasks, potentially improving classification accuracy and robustness compared to single-input models.

This type of model is particularly useful in situations such as:

  • Combining data from different sensor types (e.g. accelerometer and gyroscope)

  • Handling output from multiple feature extractors (e.g. time-domain and frequency-domain features)

  • Analyzing data from sensors in different locations

Model architecture

The architecture in this tutorial consists of separating the model input into two sets of 2D input features (input layer and reshape layers), passing them through independent convolutional branches, and then combining the results for classification (concatenate layer and dense layer). See below for additional details.

Input layer

The input layer accepts a one-dimensional (1D) input of length input_length. The input_length argument is available if you are using the pre-built by Edge Impulse and modifying them through . By default, Edge Impulse flattens the output of all before passing them to the learning block, so the shape is a 1D array.

Reshape layers

The model input is first split into two halves and then reshaped back into 2D formats. In this example, we assumed spectrogram inputs with 50 columns (time frames) each. We calculated the number of rows based on the number of columns and channels, then used the row and column information for the reshape layer.

Convolutional branches

Each half of the input goes through its own convolutional branch that contains multiple layers:

  • Conv2D layers (8 and 16 filters)

  • MaxPooling2D layers for downsampling

  • Dropout layers for regularization

  • Flatten layer to prepare for concatenation

Output layers

Finally, the outputs of the two independent branches are combined with a concatenation layer. This combined output is then passed through a final dense layer with a softmax activation to perform classification.

Keras model

The following code snippet can be copied and pasted into a using the expert mode feature within Studio.

Extending the model architecture

To extend this model architecture to handle more inputs, you can:

  1. Split the input into more sections (e.g. thirds or quarters instead of halves).

  2. Create additional convolutional branches for each new input section.

  3. Include the new branches in the concatenation step before the final dense layer.

Conclusion

The architecture defined in this tutorial allows the model to process two separate sets of 2D input features independently before combining them for a final classification. Each branch can learn model parameters specific to its input, which can be particularly useful when dealing with different types of sensor data or feature extractors.

with Nordic Thingy53 and the Edge Impulse app

Introduction

This tutorial is part of the series. If you haven't read the introduction yet, we recommend doing so .

We'll guide you through deploying updated machine learning models over-the-air (OTA) to the Nordic Thingy:53 using Edge Impulse. This process leverages the Nordic Thingy:53 app, allowing users to deploy firmware updates and facilitating on-device testing for Lifecycle Management.

Key Features of Nordic Thingy:53 OTA Updates:

  • User-initiated firmware deployment via the Nordic Thingy:53 app.

  • Remote data collection and on-device testing for machine learning models.

  • Seamless integration with Edge Impulse for Lifecycle Management.

Prerequisites

  • Edge Impulse Account: Sign up if you don't have one .

  • Trained Impulse: If you're new, follow one of our

  • Nordic Thingy:53: Have the device ready and charged.

  • Nordic Thingy:53 App: Installed on your smartphone or tablet.

Preparation

Begin by connecting your Nordic Thingy:53 to the Edge Impulse platform and setting it up for data collection and model deployment.

Step-by-Step Guide

1. Setting Up Nordic Thingy:53 with Edge Impulse

  • Connect your Nordic Thingy:53 to the Edge Impulse using the Nordic Thingy:53 app. This will be your interface for managing the device and deploying updates.

2. Collecting Data and Training the Model

  • Use the Nordic Thingy:53 to collect relevant data for your machine learning application.

  • Upload this data to Edge Impulse and train your model.

3. Deploying the Model via the Nordic Thingy:53 App

  • Once your model is trained and ready, use the Nordic Thingy:53 app to deploy it to the device.

  • The app allows you to initiate the OTA update, which downloads and installs the latest firmware containing the new model.

4. Remote On-Device Testing

  • Conduct remote testing through the app to evaluate the model's performance in real-world scenarios.

  • This step is crucial for validating the effectiveness of your machine learning model.

5. Continuous Improvement Cycle

  • Continuously collect new data with the Nordic Thingy:53.

  • Re-train your model on Edge Impulse with this new data.

  • Deploy these updates to the Thingy:53 via the app, maintaining the cycle of Lifecycle Management.

Conclusion

This tutorial provides a straightforward approach to implementing OTA updates and Lifecycle Management on the Nordic Thingy:53 using Edge Impulse. The user-friendly Nordic Thingy:53 app facilitates easy deployment of firmware updates, making it ideal for rapid prototyping and iterative machine learning model development.

Additional Resources

This guide helps users leverage the capabilities of the Nordic Thingy:53 for advanced IoT applications, ensuring devices are always updated with the latest intelligence and improvements.

Data pipelines

Building data pipelines is a very useful feature where you can stack several transformation blocks similar to the . They can be used in a standalone mode (just execute several transformation jobs in a pipeline), to feed a dataset or to feed a project.

The examples in the screenshots below shows how to create and use a pipeline to create the 'AMS Activity 2022' dataset.

Create a pipeline

To create a new pipeline, click on '+Add a new pipeline:

Get the steps from your transformation blocks

In your organization workspace, go to Custom blocks -> Transformation and select Run job on the job you want to add.

Select Copy as pipeline step and paste it to the configuration json file.

You can then paste the copied step directly to the respected field.

Below, you have an option to feed the data to either a organisation dataset or an Edge Impulse project

Schedule and notify

By default, your pipeline will run every day. To schedule your pipeline jobs, click on the ⋮ button and select Edit pipeline.

Once the pipeline has successfully finished, it can send an email to the Users to notify.

Run the pipeline

Once your pipeline is set, you can run it directly from the UI, from external sources or by scheduling the task.

Run the pipeline from the UI

To run your pipeline from Edge Impulse studio, click on the ⋮ button and select Run pipeline now.

Run the pipeline from code

To run your pipeline from Edge Impulse studio, click on the ⋮ button and select Run pipeline from code. This will display an overlay with curl, Node.js and Python code samples.

You will need to create an API key to run the pipeline from code.

Webhooks

Another useful feature is to create a webhook to call a URL when the pipeline has ran. It will run a POST request containing the following information:

Select AI hardware

The target configuration tool allows you to define your Target device and Application budget according to your project's requirements. This flow is designed to help you optimize your impulse, processing, learn block, or imported model for your specific target hardware, ensuring that your impulse will run efficiently on your device or custom architecture.

The configuration form can be accessed from the top-level navigation. The form allows you to select from a range of processor types, architectures, and clock rates. For a custom device, you could for example select Low-end MCU and specify the clock rate, RAM, ROM, and maximum allowed latency for your application.

Accessing the configuration panel

By default, the form shows 'Cortex-M4F 80MHz' as the target device. You can change this by clicking on Change Target device. You can select from a range of processor types, architectures, and clock rates. For a custom device, you could for example select Low-end MCU and specify the clock rate, RAM, ROM, and maximum allowed latency for your application.

  1. Navigate to the top-level menu in Edge Impulse Studio.

  2. Click on Change Target Device to open the configuration panel.

Configure your target device and application budget

Lets walk you through some of the current options for configuring your device and application budget:

  • Target Device: Select the type of target device you are configuring from options like "Custom" or specific development boards.

  • Processor Type Selection: Selecting a processor type dynamically adjusts available architecture options and fields to suit your hardware:

    • For Low-end MCU: This option allows you to specify clock rate, RAM, and ROM, suitable for 'Cortex-M' architectures.

    • For AI Accelerators: Selecting this disables the clock rate field, reflecting the unique requirements of AI accelerator devices.

  • Custom Device Configuration: Choosing to configure a custom device opens fields to precisely define its capabilities, ensuring your project setup is accurately tailored to your hardware.

Special options for Custom Targets:

The form allows you to select from a range of processor types, architectures, and clock rates. For a custom device, you could for example select Low-end MCU and specify the clock rate, RAM, ROM, and maximum allowed latency for your application.

  • Custom: Select this for custom hardware specifications or devices not listed in Edge Impulse, allowing for a customized hardware profile. Selection Options

Processor Type & Architecture

Choose from a variety of processor types and architectures. Your selection determines which options and fields are available to accurately configure your device. Estimations for GPU, AI accelerator, or NPU devices are not computed using clock speed, or but rather the device's unique capabilities.

  • Processor Type: Selections range from various processor types. Choosing GPU, AI accelerator' or NPU deactivates the clock speed option, as it's irrelevant for device estimation.

  • Processor Architecture (Optional): Specify your device's architecture to refine its configuration (e.g., Cortex-M0+, Cortex-M4F, Cortex-M7).

  • Clock Rate (Optional): Set the clock rate for relevant processor types to estimate operational capabilities accurately. The units shown will be indicated by the | MHz | GHz as relevant to the scale of processor. As previously stated the clock rate field is disabled for GPU, AI accelerator, or NPU devices.

  • Accelerator: If the device supports hardware acceleration, select from available options such as Arm Cortex-U55, NVIDIA Jetson Nano, and others.

  • Device ID (Optional): Provide a unique identifier for your custom device model or chip architecture variant for easy recognition and setup.

  • Custom Device Name (Optional): Provide a unique name for your custom device to easily identify it in your project.

Application Budget - RAM, ROM, and Latency

The application budget section allows you to specify the maximum allowed latency, RAM, and ROM for your application. These values are used to estimate the performance of your model on your target device.

  • RAM: Specify the amount of RAM available on your device in kilobytes (kB).

  • ROM: Specify the amount of ROM available on your device in kilobytes (kB).

  • Latency: Specify the maximum allowed latency for your application in milliseconds (ms).

  • Save Target: Save your custom device and application budget configuration to apply it to your project.

After customizing your target device and application budget, click Save target. With the target device set, navigate to the EON Tuner to see the configuration in action. The target device can be seen at the top level of navigation on all screens within your project. Your custom device name (e.g., 'my first mcu') and the specified parameters (100 ms latency, 256 kB RAM, 1024 kB ROM) are visible. The target device configuration is also taken into account during the performance estimation for deployment.

Top-level Navigation

Once saved the target device can be seen at the top level of navigation on all screens within your project. Your custom device name (e.g., 'my first mcu') and the specified parameters (100 ms latency, 256 kB RAM, 1024 kB ROM) are visible. The target device configuration is also taken into account during the performance estimation for deployment.

Additional functionalities:

There are some additional considerations for targets mentioned in the tabs below, particularly for their generated binaries models and deployment formats.

For Ethos enabled targets, you can select them as profiling targets. After training, the Vela compiled model will be available on the dashboard. This feature is exclusive to Ethos enabled targets. These assets are only available in our studio, when deploying and selecting the deployment format, the library will include the Vela compiled model converted to a C header. While this is sufficient for most users, those needing the binary file directly on the flash may find it inconvenient to convert it back from a C header.

  • Key Differences:

    • C++ Library: The library will include the Vela compiled model converted to a C header.

    • Vela Library: The Vela compiled model will be available on the dashboard.

C2 and IMIX users, who need Vela compiled models using other non standard inferencing engine that need to be converted to TFLite files instead of the C++ library.

  • Key Differences:

    • Deployment Format: The deployment format will include the TFLite model.

When selecting the Neox target as a profiling target, the training pipeline involves training a float model, which is then converted to an int8 model using a custom quantizer, rather than the TensorFlow framework's quantizer.

  • Key Differences:

    • Deployment Format: The deployment format will include the int8 model.

    • Custom Quantizer: The int8 model is generated using a custom quantizer.

Summary

The target-driven flow in Edge Impulse Studio allows you to configure your target device and application budget according to your project's requirements. This flow is designed to help you optimize your impulse for your specific target hardware, ensuring that your impulse will run efficiently on your device.

We hope this feature is helpful, and intuitive. If you have any questions or suggestions, feel free to reach out to us at . We're always happy to hear from you!

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, InputLayer, Dropout, Conv1D, Conv2D, Flatten, Reshape, MaxPooling1D, MaxPooling2D, AveragePooling2D, BatchNormalization, Permute, ReLU, Softmax, Concatenate, Input
from tensorflow.keras.optimizers.legacy import Adam
from tensorflow.keras.callbacks import EarlyStopping

EPOCHS = args.epochs or 50
LEARNING_RATE = args.learning_rate or 0.0005
# If True, non-deterministic functions (e.g. shuffling batches) are not used.
# This is False by default.
ENSURE_DETERMINISM = args.ensure_determinism
# this controls the batch size, or you can manipulate the tf.data.Dataset objects yourself
BATCH_SIZE = args.batch_size or 32
if not ENSURE_DETERMINISM:
    train_dataset = train_dataset.shuffle(buffer_size=BATCH_SIZE*4)
train_dataset=train_dataset.batch(BATCH_SIZE, drop_remainder=False)
validation_dataset = validation_dataset.batch(BATCH_SIZE, drop_remainder=False)

channels = 1
columns = 50
rows = input_length // (columns * channels)

# Input layer
input_layer = Input(shape=(input_length, ))

# Split the input into two halves
half_length = input_length // 2
# First half
first_half = Reshape((rows // 2, columns, channels))(input_layer[:, :half_length])
branch1 = Conv2D(8, kernel_size=3, kernel_constraint=tf.keras.constraints.MaxNorm(1), padding='same', activation='relu')(first_half)
branch1 = MaxPooling2D(pool_size=2, strides=2, padding='same')(branch1)
branch1 = Dropout(0.5)(branch1)
branch1 = Conv2D(16, kernel_size=3, kernel_constraint=tf.keras.constraints.MaxNorm(1), padding='same', activation='relu')(branch1)
branch1 = MaxPooling2D(pool_size=2, strides=2, padding='same')(branch1)
branch1 = Dropout(0.5)(branch1)
branch1 = Flatten()(branch1)

# Second half
second_half = Reshape((rows // 2, columns, channels))(input_layer[:, half_length:])
branch2 = Conv2D(8, kernel_size=3, kernel_constraint=tf.keras.constraints.MaxNorm(1), padding='same', activation='relu')(second_half)
branch2 = MaxPooling2D(pool_size=2, strides=2, padding='same')(branch2)
branch2 = Dropout(0.5)(branch2)
branch2 = Conv2D(16, kernel_size=3, kernel_constraint=tf.keras.constraints.MaxNorm(1), padding='same', activation='relu')(branch2)
branch2 = MaxPooling2D(pool_size=2, strides=2, padding='same')(branch2)
branch2 = Dropout(0.5)(branch2)
branch2 = Flatten()(branch2)

# Concatenate the outputs of both branches
merged = Concatenate()([branch1, branch2])

# Final dense layer
output_layer = Dense(classes, name='y_pred', activation='softmax')(merged)

# Create model
model = Model(inputs=input_layer, outputs=output_layer)

# this controls the learning rate
opt = Adam(learning_rate=LEARNING_RATE, beta_1=0.9, beta_2=0.999)
callbacks.append(BatchLoggerCallback(BATCH_SIZE, train_sample_count, epochs=EPOCHS, ensure_determinism=ENSURE_DETERMINISM))
# Early stop
callbacks.append(EarlyStopping(patience=3))

# train the neural network
model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
model.fit(train_dataset, epochs=EPOCHS, validation_data=validation_dataset, verbose=2, callbacks=callbacks)

# Use this flag to disable per-channel quantization for a model.
# This can reduce RAM usage for convolutional models, but may have
# an impact on accuracy.
disable_per_channel_quantization = False
learning blocks
expert mode
processing blocks
classification learning block
Lifecycle Management with Edge Impulse
here
here
end-to-end tutorials
Nordic Thingy:53 Documentation
Edge Impulse Documentation
this example project
multi-impulse tutorial
this tutorial
sensor fusion using Embeddings
supported device
Arduino Nano 33 BLE Sense tutorial
Flatten block
here
this guide
this tutorial
Sound recognition
Image classification
custom processing blocks
Arduino board connected to Edge Impulse project
Record data from multiple sensors
Raw sensor readings
Data split into training and testing sets
Impulse designed to work with sensor fusion
View processed features from one sample
View groupings of the most prominent features
View the most important features
Neural network architecture
Confusion matrix of the validation set
Results from running inference on the test set
View detailed classification results from a test sample
Classify live data
Live classification results
Deploy a trained machine learning model to any number of devices
Arduino
Particle Workbench
Blues Wireless
C++ Espressif IDF
Nordic / Zephyr on Golioth
sign up here
end-to-end tutorials
https://docs.edgeimpulse.com/reference/getprojectlastmodificationdate
https://docs.edgeimpulse.com/reference/downloadbuild
forum
Continuous Learning Sample Architecture
forum.edgeimpulse.com
Target-driven Default Settings
Custom Targets
Save
Navigating to EON Tuner with Updated Target Device
Top-level Navigation

For embedded engineers

Welcome to Edge Impulse! When we started Edge Impulse, we initially focused on developing a suite of engineering tools designed to empower embedded engineers to harness the power of machine learning on edge devices. As we grew, we also started to develop advanced tools for ML practitioners to ease the collaboration between teams in organizations.

In this getting started guide, we'll walk you through the essential steps to dive into Edge Impulse and leverage it for your embedded projects.

Why Edge Impulse, for embedded engineers?

Embedded systems are becoming increasingly intelligent, and Edge Impulse is here to streamline the integration of machine learning into your hardware projects. Here's why embedded engineers are turning to Edge Impulse:

  • Extend hardware capabilities: Edge Impulse can extend hardware capabilities by enabling the integration of machine learning models, allowing edge devices to process complex tasks, recognize patterns, and make intelligent decisions that are complex to develop using rule-based algorithms.

  • Open-source export formats: Exported models and libraries contain both digital signal processing code and machine learning models, giving you full explainability of the code.

  • Powerful integrations: Edge Impulse provides complete and documented integrations with various hardware platforms, allowing you to focus on the application logic rather than the intricacies of machine learning.

  • Support for diverse sensors: Whether you're working with accelerometers, microphones, cameras, or custom sensors, Edge Impulse accommodates a wide range of data sources for your projects.

  • Predict on-device performances: Models trained in Edge Impulse run directly on your edge devices, ensuring real-time decision-making with minimal latency. We provide tools to ensure the DSP and models developed with Edge Impulse can fit your device constraints.

  • Device-aware optimization: You have full control over model optimization, enabling you to tailor your machine-learning models to the specific requirements and constraints of your embedded systems. Our EON tuner can help you select the best model by training many different variants of models only from an existing dataset and your device constraints!

Getting started in a few steps

Ready to embark on your journey with Edge Impulse? Follow these essential steps to get started:

1. Sign Up

Start by creating your Edge Impulse account. Registration is straightforward, granting you immediate access to the comprehensive suite of tools and resources.

2. Create a Project

Upon logging in, initiate your first project. Select a name that resonates with your project's objectives. If you already which hardware target or system architecture you will be using, you can set it up directly in the dashboard's project info section. This will help you to make sure your model fits your device constraints.

3. Data Collection and Labeling

We offer various methods to collect data from your sensors or to import datasets (see Data acquisition for all methods). For the officially supported hardware targets, we provide binaries or simple steps to attach your device to Edge Impulse Studio and collect data from the Studio. However, as an embedded engineer, you might want to collect data from sensors that are not necessarily available on these devices. To do so, you can use the Data forwarder and print out your sensor values over serial (up to 8kHz) or use our C Ingestion SDK, a portable header-only library (designed to reliably store sampled data from sensors at a high frequency in very little memory).

4. Pre-process your data and train your model

Edge Impulse offers an intuitive model training process through processing blocks and learning blocks. You don't need to write Python code to train your model; the platform guides you through feature extraction, model creation, and training. Customize and fine-tune your blocks for optimal performance on your hardware. Each block will provide on-device performance information showing you the estimated RAM, flash, and latency.

5. Run the inference on a device

This is where the fun start, you can easily export your model as ready-to-flash binaries for all the officially supported hardware targets. This method will let you test your model on real hardware very quickly.

In addition, we also provide a wide variety of export methods to easily integrate your model with your application logic. See C++ library to run your model on any device that supports C++ or our guides for Arduino library, Cube.MX CMSIS-PACK, DRP-AI library, DRP-AI TVM i8 library, OpenMV library, Ethos-U library, Meta TF model, Simplicity Studio Component, Tensai Flow library, TensorRT library, TIDL-RT library, etc...

The C++ inferencing library is a portable library for digital signal processing and machine learning inferencing, and it contains native implementations for both processing and learning blocks in Edge Impulse. It is written in C++11 with all dependencies bundled and can be built on both desktop systems and microcontrollers. See Inferencing SDK documentation.

6. Go further

Building Edge AI solutions is an iterative process. Feel free to try our organization hub to automate your machine-learning pipelines, collaborate with your colleagues, and create custom blocks.

Tutorials and resources, for embedded engineers

End-to-end tutorials

If you want to get familiar with the full end-to-end flow, please have a look at our end-to-end tutorials:

  • Motion recognition + anomaly detection,

  • Keyword spotting,

  • Sound recognition,

  • Image classification,

  • Object detection using bounding boxes (size and location),

  • Object detection using centroids (location)

Advanced inferencing tutorials

In the advanced inferencing tutorials section, you will discover useful techniques to leverage our inferencing libraries or how you can use the inference results in your application logic:

  • Continuous audio sampling

  • Multi-impulse

  • Count objects using FOMO

Other useful resources

  • EON compiler

  • Inference performance metrics

Join the Edge Impulse Community

Edge Impulse offers a thriving community of embedded engineers, developers, and experts. Connect with like-minded professionals, share your knowledge, and collaborate to enhance your embedded machine-learning projects.

Now that you have a roadmap, it's time to explore Edge Impulse and discover the exciting possibilities of embedded machine learning. Let's get started!

python -m pip install edgeimpulse
import edgeimpulse as ei
ei.API_KEY = "ei_dae27..."
profile = ei.model.profile(model=model, device='cortex-m4f-80mhz')
print(profile.summary())
ei.model.deploy(model=model,
                model_output_type=ei.model.output_type.Classification(),
                deploy_target='zip')
Edge Impulse account
Studio Uploader
CLI Uploader
Ingestion API
dataset annotation formats
Organization data
Data explorer
processing block
create your own
learning blocks
expert mode
custom learning block
Studio BYOM feature
Edge Impulse Python SDK
deployment type
export your model
Linux inferencing libraries
end-to-end tutorials
Motion recognition + anomaly detection
Keyword spotting
Sound recognition
Image classification
Object detection using bounding boxes (size and location)
Object detection using centroids (location)
health reference design
edgeimpulse
Using the Edge Impulse Python SDK with TensorFlow and Keras
Using the Edge Impulse Python SDK with Hugging Face
Using the Edge Impulse Python SDK with Weights & Biases
Using the Edge Impulse Python SDK with SageMaker Studio
Expert mode
BYOM (Bring Your Own Model)
Custom learning blocks
Generate synthetic datasets
Weight & Biases
NVIDIA Omniverse
$ edge-impulse-run-impulse
$ edge-impulse-run-impulse --debug
$ edge-impulse-run-impulse --continuous
Tutorial: adding sight to your sensors
supported device
Uploader
Collecting image data with the OpenMV Cam H7 Plus
Collecting image data from the Studio
Collecting image data with your mobile phone
Building custom processing blocks
Arduino Nano 33 BLE Sense
Running your impulse on your OpenMV camera
Running your impulse locally
Continuous motion recognition
Sound recognition
custom processing blocks
Data acquisition showing images of a plant. Data is automatically split in a training and a test set when collecting, thus showing the 221 items in the training set here.
Designing an impulse
Configuring the processing block.
The feature explorer visualizing the data in the dataset. Clusters that separate well in the feature explorer will be easier to learn for the machine learning model.
A trained model showing on-device performance estimations.
Verifying our model on real world data
An item that could not be classified (as the highest score was under the 0.7 threshold). As the data is very far outside of any known cluster this is likely data that was unlike anything seen before - perhaps due to part of the window being present. It'd be good to add additional images to the training set.
The machine learning model running in real-time on device, classifying a plant.
Is there a person in this picture? Answer with just 'yes' or 'no'.
Is there a person in this picture? Respond only with "yes", "no" or "unsure" if you're not sure.
Transformation block
Data sources
Data acquisition
Data sources
Image classification
forum
Label image data using GPT-4o blog post
Add pre-built transformation block
Edit block
Job ran successfully
GPT-4o labeling block logs
{
    "organizationId":XX,
    "pipelineId":XX,
    "pipelineName":"Fetch, sort, validate and combine",
    "projectId":XXXXX,
    "success":true,
    "newItems":0,
    "newChecklistOK":0,
    "newChecklistFail":0
}
Data sources pipelines
clinical data pipelines example
Add a new clinical data pipeline
Transformation blocks
Copy
Edit pipeline
Run the pipeline from code
Data sources webhooks

Object detection with bounding boxes

In this tutorial, you'll use machine learning to build a system that can recognize and track multiple objects in your house through a camera - a task known as object detection. Adding sight to your embedded devices can make them see the difference between poachers and elephants, count objects, find your lego bricks, and detect dangerous situations. In this tutorial, you'll learn how to collect images for a well-balanced dataset, how to apply transfer learning to train a neural network and deploy the system to an edge device.

At the end of this tutorial, you'll have a firm understanding of how to do object detection using Edge Impulse.

There is also a video version of this tutorial:

You can view the finished project, including all data, signal processing and machine learning blocks here: Tutorial: object detection.

Running on a microcontroller?

We recently released a brand-new approach to perform object detection tasks on microcontrollers, FOMO, if you are using a constraint device that does not have as much compute, RAM, and flash as Linux platforms, please head to this end-to-end tutorial: Detect objects using FOMO

Alternatively, if you only need to recognize a single object, you can follow our tutorial on Image classification - which performs image classification, hence, limits you to a single object but can also fit on microcontrollers.

You can view the finished project, including all data, signal processing and machine learning blocks here: Tutorial: object detection.

1. Prerequisites

For this tutorial, you'll need a supported device.

If you don't have any of these devices, you can also upload an existing dataset through the Uploader - including annotations. After this tutorial you can then deploy your trained machine learning model as a C++ library and run it on your device.

2. Building a dataset

In this tutorial we'll build a model that can distinguish between two objects on your desk - we've used a lamp and a coffee cup, but feel free to pick two other objects. To make your machine learning model see it's important that you capture a lot of example images of these objects. When training the model these example images are used to let the model distinguish between them.

Capturing data

Capture the following amount of data - make sure you capture a wide variety of angles and zoom level. It's fine if both images are in the same frame. We'll be cropping the images later to be square so make sure the objects are in the frame.

  • 30 images of a lamp.

  • 30 images of a coffee cup.

You can collect data from the following devices:

  • Collecting image data from the Studio - for the Raspberry Pi 4 and the Jetson Nano.

  • Collecting image data with your mobile phone

Or you can capture your images using another camera, and then upload them by going to Data acquisition and clicking the 'Upload' icon.

With the data collected we need to label this data. Go to Data acquisition, verify that you see your data, then click on the 'Labeling queue' to start labeling.

Collected data, now let's label the data with the labeling queue.

No labeling queue? Go to Dashboard, and under 'Project info > Labeling method' select 'Bounding boxes (object detection)'.

Labeling data

The labeling queue shows you all the unlabeled data in your dataset. Labeling your objects is as easy as dragging a box around the object, and entering a label. To make your life a bit easier we try to automate this process by running an object tracking algorithm in the background. If you have the same object in multiple photos we thus can move the boxes for you and you just need to confirm the new box. After dragging the boxes, click Save labels and repeat this until your whole dataset is labeled.

AI-Assisted Labeling

Use AI-Assisted Labeling for your object detection project! For more information, check out our blog post.

Labeling multiple objects with the labeling queue. Note the dark borders on both sides of the image, these will be cut off during training, so you don't have to label objects that are located there.

Afterwards you should have a well-balanced dataset listed under Data acquisition in your Edge Impulse project.

Rebalancing your dataset

To validate whether a model works well you want to keep some data (typically 20%) aside, and don't use it to build your model, but only to validate the model. This is called the 'test set'. You can switch between your training and test sets with the two buttons above the 'Data collected' widget. If you've collected data on your development board there might be no data in the testing set yet. You can fix this by going to Dashboard > Perform train/test split.

3. Designing an impulse

With the training set in place you can design an impulse. An impulse takes the raw data, adjusts the image size, uses a preprocessing block to manipulate the image, and then uses a learning block to classify new data. Preprocessing blocks always return the same values for the same input (e.g. convert a color image into a grayscale one), while learning blocks learn from past experiences.

For this tutorial we'll use the 'Images' preprocessing block. This block takes in the color image, optionally makes the image grayscale, and then turns the data into a features array. If you want to do more interesting preprocessing steps - like finding faces in a photo before feeding the image into the network -, see the Building custom processing blocks tutorial. Then we'll use a 'Transfer Learning' learning block, which takes all the images in and learns to distinguish between the two ('coffee', 'lamp') classes.

In the studio go to Create impulse, set the image width and image height to 320, the 'resize mode' to Fit shortest axis and add the 'Images' and 'Object Detection (Images)' blocks. Then click Save impulse.

Designing an impulse

Configuring the processing block

To configure your processing block, click Images in the menu on the left. This will show you the raw data on top of the screen (you can select other files via the drop down menu), and the results of the processing step on the right. You can use the options to switch between 'RGB' and 'Grayscale' mode, but for now leave the color depth on 'RGB' and click Save parameters.

Configuring the processing block.

This will send you to the 'Feature generation' screen. In here you'll:

  • Resize all the data.

  • Apply the processing block on all this data.

  • Create a 3D visualization of your complete dataset.

Click Generate features to start the process.

Afterwards the 'Feature explorer' will load. This is a plot of all the data in your dataset. Because images have a lot of dimensions (here: 320x320x3=307,200 features) we run a process called 'dimensionality reduction' on the dataset before visualizing this. Here the 307,200 features are compressed down to just 3, and then clustered based on similarity. Even though we have little data you can already see the clusters forming (lamp images are all on the left, coffee all on the right), and can click on the dots to see which image belongs to which dot.

The feature explorer visualizing the data in the dataset. Clusters that separate well in the feature explorer will be easier to learn for the machine learning model.

Configuring the transfer learning model

With all data processed it's time to start training a neural network. Neural networks are a set of algorithms, modeled loosely after the human brain, that are designed to recognize patterns. The network that we're training here will take the image data as an input, and try to map this to one of the three classes.

It's very hard to build a good working computer vision model from scratch, as you need a wide variety of input data to make the model generalize well, and training such models can take days on a GPU. To make this easier and faster we are using transfer learning. This lets you piggyback on a well-trained model, only retraining the upper layers of a neural network, leading to much more reliable models that train in a fraction of the time and work with substantially smaller datasets.

To configure the transfer learning model, click Object detection in the menu on the left. Here you can select the base model (the one selected by default will work, but you can change this based on your size requirements), and set the rate at which the network learns.

Leave all settings as-is, and click Start training. After the model is done you'll see accuracy numbers below the training output. You have now trained your model!

A trained model showing the precision score. This is the COCO mean average precision score, which evaluates how well the predicted labels match your earlier labels.

4. Validating your model

With the model trained let's try it out on some test data. When collecting the data we split the data up between a training and a testing dataset. The model was trained only on the training data, and thus we can use the data in the testing dataset to validate how well the model will work in the real world. This will help us ensure the model has not learned to overfit the training data, which is a common occurrence.

To validate your model, go to Model testing and select Classify all. Here we hit 92.31% precision, which is great for a model with so little data.

To see a classification in detail, click the three dots next to an item, and select Show classification. This brings you to the Live classification screen with much more details on the file (you can also capture new data directly from your development board from here). This screen can help you determine why items were misclassified.

Live Classification Result

Live classification helps you determine how well your model works, showing the objects detected and the confidence score side by side.

This view is particularly useful for a direct comparison between the raw image and the model's interpretation. Each object detected in the image is highlighted with a bounding box. Alongside these boxes, you'll find labels and confidence scores, indicating what the model thinks each object is and how sure it is about its prediction. This mode is ideal for understanding the model's performance in terms of object localization and classification accuracy.

Overlay Mode for the Live Classification Result

Changing to overlay mode provides a more integrated view by superimposing the model's detections directly onto the original image

In this view, bounding boxes are drawn around the detected objects, with labels and confidence scores displayed within the image context. This approach offers a clearer view of how the bounding boxes align with the objects in the image, making it easier to assess the precision of object localization. The overlay view is particularly useful for examining the model's ability to accurately detect and outline objects within a complex visual scene.

Summary Table

This table provides a concise summary of the performance metrics for an object detection model, using a specific sample file. The layout and contents are as follows.

Name: This field displays the name of the sample file analyzed by the model. For instance, 'sample.jpg.22l74u4f' is the file name in this case.

CATEGORY: Lists the types of objects that the model has been trained to detect. In this example, two categories are shown: 'coffee' and 'lamp'.

COUNT: Indicates the number of times each category was detected in the sample file. In this case, both 'coffee' and 'lamp' have a count of 1, meaning each object was detected once in the sample.

INFO: This column provides additional information about the model's performance. It displays the 'Precision score', which, in this example, is 95.00%. The precision score represents the model's accuracy in making correct predictions over a range of Intersection over Union (IoU) values, known as the mean Average Precision (mAP).

5. Running the model on your device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the preprocessing steps, neural network weights, and classification code - in a single C++ library or model file that you can include in your embedded software.

Running the impulse on your Raspberry Pi 4 or Jetson Nano

From the terminal just run edge-impulse-linux-runner. This will build and download your model, and then run it on your development board. If you're on the same network you can get a view of the camera, and the classification results directly from your dev board. You'll see a line like:

Want to see a feed of the camera and live classification in your browser? Go to http://192.168.1.19:4912

Open this URL in a browser to see your impulse running!

Object detection model running on a Raspberry Pi 4

Running the impulse on your mobile phone

On your mobile phone just click Switch to classification mode at the bottom of your phone screen. Point it at an object and press 'Capture'.

Integrating the model in your own application

Congratulations! You've added object detection to your sensors. Now that you've trained your model you can integrate your impulse in the firmware of your own edge device, see the Edge Impulse for Linux documentation for the Node.js, Python, Go and C++ SDKs that let you do this in a few lines of code and make this model run on any device. Here's an example of sending a text message through Twilio when an object is seen.

Or if you're interested in more, see our tutorials on Continuous motion recognition or Sound recognition. If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build custom processing blocks to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Trigger connected board data sampling

1. Obtain an API key from your project

Your project API key can be used to enable programmatic access to Edge Impulse. You can create and/or obtain a key from your project's Dashboard, under the Keys tab. API keys are long strings, and start with ei_:

Project API Key

2. Connect your development kit to your project

Open a terminal and run the Edge Impulse daemon. The daemon is the service that connects your hardware with any Edge Impulse project:

edge-impulse-daemon --api-key <your project API key>

3. Obtain your project's ID

Copy your project's ID from the project's Dashboard under the Project Info section:

Project API Key

4. Setup API Connection

Replace the PROJECT_ID below with the ID of your project you selected and enter your API key when prompted:

import requests
import getpass
import json

URL_STUDIO = "https://studio.edgeimpulse.com/v1/api/"
PROJECT_ID = int(input('Enter your Project ID: '))
AUTH_KEY = getpass.getpass('Enter your API key: ')


def check_response(response, debug=False):
    if not response.ok:
        raise RuntimeError("⛔️ Error\n%s" % response.text)
    else:
        if debug:
            print(response)
        return response


def do_get(url, auth, debug=False):
    if debug:
        print(url)
    response = requests.get(url,
                            headers={
                                "Accept": "application/json",
                                "x-api-key": auth
                            })
    return check_response(response, debug)


def parse_response(response, key=""):
    parsed = json.loads(response.text)
    if not parsed["success"]:
        raise RuntimeError(parsed["error"])
    if key == "":
        return json.loads(response.text)
    return json.loads(response.text)[key]


def get_project(project_id, project_auth, debug=False):
    response = do_get(URL_STUDIO + str(project_id), project_auth)
    return parse_response(response, "project")


print("Project %s is accessible" % get_project(PROJECT_ID, AUTH_KEY)["name"])

5. Get the ID of the connected device

# https://studio.edgeimpulse.com/v1/api/{projectId}/devices

def get_devices(project_id, project_auth, debug=False):
    response = do_get(URL_STUDIO + str(project_id) + "/devices", project_auth)
    return parse_response(response, "devices")


device_id = ""
for device in get_devices(PROJECT_ID, AUTH_KEY):
    # if device["remote_mgmt_connected"] and device["supportsSnapshotStreaming"]:
    if device["remote_mgmt_connected"]:
        device_id = device["deviceId"]
        print("Found %s (type %s, id: %s)" %
              (device["name"], device["deviceType"], device_id))
        break
if device_id == "":
    print(
        "Could not find a connected device that supports snapshot streaming!")

6. Trigger data sampling

# https://studio.edgeimpulse.com/v1/api/{projectId}/device/{deviceId}/start-sampling

SAMPLE_CATEGORY = "testing"
SAMPLE_LENGTH_MS = 20000
SAMPLE_LABEL = "squat"

def do_post(url, payload, auth, debug=False):
    if debug:
        print(url)
    response = requests.post(url,
                             headers={
                                 "Accept": "application/json",
                                 "x-api-key": auth
                             },
                             json=payload)
    return check_response(response, debug)


def collect_sample(project_id, device_id, project_auth, debug=False):
    payload = {
        "category": SAMPLE_CATEGORY,
        # "Microphone", "Inertial", "Environmental" or "Inertial + Environmental"
        "sensor": "Inertial",
        # The inverse of frequency in Hz
        "intervalMs": 10,
        "label": SAMPLE_LABEL,
        "lengthMs": SAMPLE_LENGTH_MS
    }
    response = do_post(
        URL_STUDIO + str(project_id) + "/device/" + str(device_id) +
        "/start-sampling", payload, project_auth, debug)
    return parse_response(response, "id")


print("Sample request returned", collect_sample(PROJECT_ID, device_id, AUTH_KEY))

Continuous audio sampling

When you are classifying audio - for example to detect keywords - you want to make sure that every piece of information is both captured and analyzed, to avoid missing events. This means that your device need to capture audio samples and analyze them at the same time. In this tutorial you'll learn how to continuously capture audio data, and then use the continuous inferencing mode in the Edge Impulse SDK to classify the data.

This tutorial assumes that you've completed the Sound recognition tutorial, and have your impulse running on your device.

Continuous inference mode

Continuous inferencing is automatically enabled for any impulses that use audio. Build and flash a ready-to-go binary for your development board from the Deployment tab in the studio, then - from a command prompt or terminal window - run edge-impulse-run-impulse --continuous.

An Arduino sketch that demonstrates continuous audio sampling is part of the Arduino library deployment option. After importing the library into the Arduino IDE, look under 'Examples' for 'nano_ble33_sense_audio_continuous'.

Continuous Inferencing

In the normal (non-continuous) inference mode when classifying data you sample data until you have a full window of data (e.g. 1 second for a keyword spotting model, see the Create impulse tab in the studio), you then classify this window (using the run_classifier function), and a prediction is returned. Then you empty the buffer, sample new data, and run the inferencing again. Naturally this has some caveats when deploying your model in the real world: 1) you have a delay between windows, as classifying the window takes some time and you're not sampling then, making it possible to miss events. 2) there's no overlap between windows, thus if an event is at the very end of the window, not the full event might be captured - leading to a wrong classification.

To mitigate this we have added several new features to the Edge Impulse SDK.

1. Model slices

Using continuous inferencing, smaller sampling buffers (slices) are used and passed to the inferencing process. In the inferencing process, the buffers are time sequentially placed in a FIFO (First In First Out) buffer that matches the model size. After each iteration, the oldest slice is removed at the end of the buffer and a new slice is inserted at the beginning. On each slice now, the inference is run multiple times (depending on the number of slices used for a model). For example, a 1-second keyword model with 4 slices (each 250 ms), will infer each slice 4 times. So if now the keyword is on 2 edges of the slice buffers, they're glued back together in the FIFO buffer and the keyword will be classified correctly.

2. Averaging

Another advantage of this technique is that it filters out false positives. Take for instance a yes-no keyword spotting model. The word 'yesterday' should not be classified as a yes (or no). But if the 'yes-' is sampled in the first buffer and '-terday' in the next, there is a big chance that the inference step will classify the first buffer as a yes.

By running inference multiple times over the slices, continuous inferencing will filter out this false positive. When the 'yes' buffer enters the FIFO it will surely classify as a 'yes'. But as the rest of the word enters, the classified value for 'yes' will drop quickly. We just have to make sure that we don't react on peak values. Therefore a moving average filter averages the classified output and so flattens the peaks. To have a valid 'yes', we now need multiple high-rated classifications.

Continuous audio sampling

In the standard way of running the impulse, the steps of collecting data and running the inference are run sequentially. First, the audio is sampled, filling a block the size of the model. This block is sent to the inferencing part, where first the features are extracted and then the inference is run. Finally, the classified output is used in your application (by default the output will be printed over the serial connection).

Activity diagram of running the impulse in sequential steps

In the continuous sampling method, audio is sampled in parallel with the inferencing and output steps. So while inference is running, audio sampling continues on a background process.

Activity diagram of running the impulse using the parallel audio sampling mechanism

Implementing continuous audio sampling

We've implemented continuous audio sampling already on the ST B-L475E-IOT01A and the Arduino Nano 33 BLE Sense targets (the firmware for both targets is open source), but here's a guideline to implementing this on your own targets.

Prerequisites

The embedded target needs to support running of multiple processes in parallel. This can either be achieved by an operating system; 1 low priority thread will run inferencing and 1 high priority thread will collect sample data. Or the processor should support processor offloading. This is usually done by the audio peripheral or DMA (Direct Memory Access). Here audio samples are collected in a buffer without involvement of the processor.

Double buffering

How do we know when new sample data is available? For this we use a double buffering mechanism. Hereby 2 sample buffers are used:

  • 1 buffer for the audio sampling process, filling the buffer with new sample data

  • 1 buffer for the inference process, get sample data out the buffer, extract the features and run inference

At start, the sampling process starts filling a buffer with audio samples. Meanwhile, the inference process waits until the buffer is full. When that happens, the sampling process passes the buffer to the inference process and starts sampling on the second buffer. Each iteration, the buffers will be switched so that there is always an empty buffer for sampling and a full buffer of samples for inferencing.

Timing and memory is everything

There are 2 constraints in this story: timing and memory. When switching the buffers there must be a 100% guarantee that the inference process is finished when the sampling process passes a full buffer. If not, the sampling process overruns the buffer and sampled data will get lost. When that happens on the ST B-L475E-IOT01A or the Arduino Nano 33 BLE Sense target, running the impulse is aborted and the following error is returned:

Error sample buffer overrun. Decrease the number of slices per model window (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)

The EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW macro is used to set the number of slices to fill the complete model window. The more slices per model, the smaller the slice size, thereby the more inference cycles on the sampled data. Leading to more accurate results. The sampling process uses this macro for the buffer size. Where following rule applies: the bigger the buffer, the longer the sampling cycle. So on targets with lower processing capabilities, we can increase this macro to meet the timing constraint.

Increasing the slice size, increases the volatile memory uses times 2 (since we use double buffering). On a target with limited volatile memory this could be a problem. In this case you want the slice size to be small.

Double buffering in action

On both the ST B-L475E-IOT01A and Arduino Nano 33 BLE Sense targets the audio sampling process calls the audio_buffer_inference_callback() function when there is data. Here the number of samples (inference.n_samples) are stored in one of the buffers. When the buffer is full, the buffers are switched by toggling inference.buf_select. The inference process is signaled by setting the flag inference.buf_ready.

static void audio_buffer_inference_callback(uint32_t n_bytes, uint32_t offset)
{
    for (uint32_t i = 0; i< (n_bytes >>  1); i++) {
        inference.buffers[inference.buf_select][inference.buf_count++] = sampleBuffer[offset + i];

        if (inference.buf_count >= inference.n_samples) {
            inference.buf_select ^= 1;
            inference.buf_count = 0;
            inference.buf_ready = 1;
        }
    }
}

The inferencing process then sets the callback function on the signal_t structure to reference the selected buffer:

int ei_microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr)
{
    numpy::int16_to_float(&inference.buffers[inference.buf_select ^ 1][offset], out_ptr, length);
    return 0;
}

Then run_classifier_continuous() is called which will take the slice of data, run the DSP pipeline over the data, stitch data together, and then classify the data.

Data transformation

Data transformation or transformation jobs refer to processes that apply specific transformations to the data within an Edge Impulse organizational dataset. These jobs are executed using Transformation blocks, which are essentially scripts packaged in Docker containers. They perform a variety of tasks on the data, enabling more advanced and customized dataset transformation and manipulation.

The transformation jobs can be chained together in Data pipelines to automate your workflows.

Overview

Data transformation overview

Transformation jobs

Create a transformation job

You have several options to create a transformation job:

  • From the Data transformation page by selecting the Create job tab.

  • From the Custom blocks->Transformation page by selecting the "⋮" action button and selecting Run job.

  • From the Data page:

Depending on whether you are on a Default dataset or a Clinical dataset, the view will vary:

Transform data from Clinical dataset view
Transform data from Default dataset view

Run a transformation job

Again, depending on whether you are on a Default dataset or a Clinical dataset, the view will vary. The common options are the Name of the transformation job, the Transformation block used for the job.

If your Transformation block has additional custom parameters, the input fields will be displayed below in a Parameters section. For example:

Additional parameters

Dataset type options:

Default vs. Clinical datasets

Clinical Datasets: Operate on "data items" with a strict file structure. Transformation is specified using SQL-like syntax.

Default Datasets: Resemble a typical file system with flexible structure. You can specify data for transformation using wildcards.

For more information about the two dataset types, see the dedicated Data page.

Run a transformation job - Default dataset

Input

After selecting your Input dataset, you can filter which files or directory you want to transform.

In default dataset formats, we use wildcard filters (in a similar format to wildcards in git). This enable you to specify patterns that match multiple files or directories within your dataset:

  • Asterisk ( * ): Represents any number of characters (including zero characters) in a filename or directory name. It is commonly used to match files of a certain type or files whose names follow a pattern.

    Example: /folder/*.png matches all PNG files in the /folder directory.

    Example: /data/*/results.csv matches any results.csv file in a subdirectory under /data.

  • Double Asterisk ( ** ): Used to match any number of directories, including nested ones. This is particularly useful when the structure of directories is complex or not uniformly organized.

    Example: /data/**/experiment-* matches all files or directories starting with experiment- in any subdirectory under /data.

Output

When you work with default datasets in Edge Impulse, you have the flexibility to define how the output from your transformation jobs is structured. There are three main rules to choose from:

  1. No Subfolders: This rule places all transformed files directly into your specified output directory, without creating any subfolders. For example, if you transform .txt files in /data and choose /output as your output directory, all transformed files will be saved directly in /output.

  2. Subfolder per Input Item: Here, a new subfolder is created in the output directory for each input file or folder. This keeps the output from each item organized and separate. For instance, if your input includes folders like /data/2020, /data/2021, and /data/2022, and you apply this rule with /transformed as your output directory, you will get subfolders like /transformed/2020, /transformed/2021, and /transformed/2022, each containing the transformed data from the corresponding input year.

  3. Use Full Path: This rule mirrors the entire input path when creating new sub-folders in the output directory. It's especially useful for maintaining a clear trace of where each piece of output data originated, which is important in complex directory structures. For example, if you're transforming files in /project/data/experiments, and you choose /results as your output directory, the output will follow the full input path, resulting in transformed data being stored in /results/project/data/experiments.

Note: For the transformation blocks operating on files when selecting the Subfolder or Full Path option, we will use the file name without extension to create the base folder. e.g. /activity-detection/Accelerometer.csv will be uploaded to /activity-detection-output/Accelerometer/.

Run a transformation job - Clinical dataset

Input

When running transformation jobs using the Clinical dataset option, you can query your input files or folders in all your clinical datasets. We use a different filtering mechanism for the Clinical datasets.

Filters

You can use a language which is very similar to SQL (documentation). See more on how to query your data on the dedicated documentation page. For example you can use filters like the following:

  • dataset = 'Activity Detection (Clinical view)' AND file_name like 'Accelero%'

  • dataset = 'Activity Detection (Clinical view)' AND metadata->ei_check = 1

Import into project

Transformation job to import data into a project

Import into dataset

Transformation job to import data into a new dataset

Number of parallel jobs

For transformation jobs operating on Data items (directory) or on Files, you can edit the number of parallel jobs to run simultaneously

Users to notify

Finally, you can select users you want to notify over email when this job finishes.

HR/HRV

In this tutorial, you'll learn how to set up the HR/HRV Features block within Edge Impulse Studio to process your physiological data and extract meaningful heart rate (HR) and heart rate variability (HRV) features. These features can be leveraged in various downstream machine learning tasks, such as activity classification, stress detection, and health monitoring. We will extend this section over time with more examples.

For this guide, we will be using a subset of the publicly available PPG-DaLiA dataset, which contains data collected from 15 subjects during daily activities. This dataset includes physiological signals such as photoplethysmography (PPG), accelerometer data, and heart rate ground truth, allowing us to explore how the HR/HRV Features block can be applied to real-world data.

You can download the DaLIA-PPG dataset or clone the project S1_E4 from the links below:

Evaluation available for everyone, deployment only available to Enterprise plan

All users (developer and enterprise) can extract HR/HRV features using this processing block for testing purposes. However, the deployment option is only available for Enterprise users. Contact your Solution Engineer to enable it.

Understanding HR and HRV

Heart Rate (HR)

Heart Rate refers to the number of times your heart beats per minute (BPM). It's a straightforward metric that indicates your cardiac activity and is influenced by factors like physical activity, stress, and overall health.

Heart Rate Variability (HRV)

Heart Rate Variability measures the variation in time intervals between successive heartbeats, known as interbeat intervals (IBIs). HRV provides deeper insights into the autonomic nervous system's regulation of the heart, reflecting your body's ability to adapt to stress, recover, and maintain homeostasis.

Key HRV Metrics:

  • RMSSD (Root Mean Square of Successive Differences): Reflects short-term HRV and parasympathetic activity.

  • SDNN (Standard Deviation of NN intervals): Indicates overall HRV.

  • pNN50: Percentage of intervals differing by more than 50 ms, another measure of parasympathetic activity.

Understanding both HR and HRV can provide a comprehensive view of an individual's cardiovascular and autonomic health.

Prerequisites

  • Physiological Data: You can download a subset of PPG-DaLIA the DaLIA-PPG dataset S1_E4:

Step-by-Step Guide

Step 1: Prepare or Collect Your Time-Series Data

Ensure your CSV file is structured with the following columns:

  • Time: The timestamp for each data point.

  • X, Y, Z: Accelerometer data for motion artifact compensation (optional).

  • ECG/PPG: The PPG or ECG signal for heart rate estimation.

  • HR: Heart rate value, if available (should be uploaded as a multi-label sample for regression).

  • Label: The activity label (optional but useful for classification tasks).

HR Calculation: Avoid uploading data in short snippets. HR calculation relies on history and feedback to accumulate a continuous stream for better calculation and smoothing. Upload long, contiguous data for the most accurate results.

Developer plan users need to use single label and should keep the length to 2 seconds for the best accuracy.

You can download a subset of the PPG-DaLIA dataset S1_E4:

Click to view a sample CSV file structure for HR/HRV data

Step 2: Upload Data to Edge Impulse

  1. Log into your Edge Impulse Studio.

  2. Navigate to Data Acquisition in the left-hand panel.

  3. Click Upload Data and select your CSV file for each subject.

  4. Ensure that the data is correctly parsed, and assign appropriate labels for heartrate applicable (e.g., 100, 90, etc.).

Step 3: Create an Impulse for HR/HRV Processing

  1. Go to Impulse Design > Create Impulse.

  2. Add a HR and HRV Features block:

    • Input Axes: Select accX, accY, accZ, PPG (if using accelerometer data for motion artifact correction).

    • Set Window Size to 2000 ms.

    • Set Frequency (Hz) to 25 Hz (tolerance +/- 1 Hz) or 50 Hz (tolerance +/- 3 Hz).

Step 4: Add the HR/HRV Block

  1. Under Impulse Design, add the HR/HRV Features block.

  • Input Axes: Select your PPG signal for HR estimation.

  • HR Window Size: Set the window size for heart rate computation (e.g., 9 seconds, and no shorter than 2 seconds).

  • HRV Settings: Enable all HRV features to see RMSSD and other params.

  • Input Axes: Select your ECG signal for HR estimation.

  • HR Window Size: Set the window size for heart rate computation (e.g., 10 seconds and upwards of 90 seconds for best accuracy).

  • HRV Settings: Enable HRV features such as RMSSD or time-series statistics.

  • Input Axes: Select your PPG signal for HR estimation.

  • Accelerometer X, Y, Z: Include these axes to filter motion artifacts.

  • HR Window Size: Set the window size for heart rate computation (e.g., 10 seconds).

  • HRV Settings: Enable HRV features such as RMSSD or time-series statistics.

Step 5: Generate Features

  1. Click on Generate Features from the top menu.

  2. Review the feature generation output to ensure that the raw signals are correctly processed.

  3. Edge Impulse will generate the features for both heart rate and HRV over the specified window size.

Step 6: Configure the Regression Block

  1. Add a Learning Block such as a Regression if you wish to estimate HR

  2. Ensure that HR/HRV is selected as the input feature.

  3. Train the model using your labeled data to predict different heart rate.

Step 7: Test and Validate the Model

  1. After training, use Model Testing to evaluate the performance of your HR/HRV feature extraction and heart rate prediction model.

  2. Upload test data to ensure that the heart rate is correctly estimated and any HRV features are extracted as expected.

Step 8: Deploy the Model (Enterprise Only)

For enterprise users, you can deploy the model to your target device and start collecting real-time heart rate and HRV data. When the model is trained and validated, you can deploy it to your target device or C++ binary for real-time heart rate and HRV monitoring.

Deployment will look as follows:

To include heart rate extraction alongside your classifier results, define the following macro in your deployment:

This macro will enable the HR/HRV Features block to calculate heart rate and HRV features in real-time.

Read on in the or speak with your enterprise support team for more guidance.

For Developer plan users, the deployment option is not available, contact our sales team if you want to upgrade to an :

Conclusion

By following this tutorial, you've successfully set up the HR/HRV Features block in Edge Impulse Studio, extracted meaningful cardiovascular features, and optionally trained a machine learning model. This foundation enables you to build robust, real-time heart rate and HRV monitoring solutions for applications like health monitoring, stress detection, and fitness tracking.

Additional Resources

  • Edge Impulse Documentation:

If you have any questions or need further assistance, feel free to reach out on the or consult the documentation.

Time,acc_x,acc_y,acc_z,ppg,eda,temp,hr,activity,activity_label,Subject
2018-06-29 08:44:52.000000,-27,1,58,-0.0,0.0,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.031250,-27,2,59,-0.0,0.0,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.062500,-27,1,59,-0.0,0.0,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.093750,-27,1,58,-0.0,0.0,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.125000,-27,1,59,-0.0,0.0,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.156250,-27,1,59,0.0,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.187500,-27,1,59,0.01,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.218750,-27,1,58,-0.03,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.250000,-27,1,58,-0.05,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.281250,-27,1,59,0.13,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.312500,-27,1,59,0.66,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.343750,-27,1,59,1.37,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.375000,-27,1,59,2.06,0.832765,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.406250,-27,1,59,2.79,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.437500,-27,1,58,3.8,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.468750,-27,1,59,5.06,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.500000,-27,1,58,6.2,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.531250,-27,1,59,7.15,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.562500,-27,1,59,8.31,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.593750,-27,1,59,10.09,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.625000,-27,1,59,11.88,1.180231,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.656250,-27,2,59,13.14,1.618462,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.687500,-27,1,59,14.65,1.618462,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.718750,-27,1,59,17.91,1.618462,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.750000,-27,1,58,22.9,1.618462,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
2018-06-29 08:44:52.781250,-27,1,59,27.58,1.618462,32.15,76.0,NO_ACTIVITY,NO_ACTIVITY,Subject_1
...
Download the CSV here
#if EI_DSP_ENABLE_RUNTIME_HR == 1
    ei_impulse_result_hr_t hr_calcs;
#endif
typedef struct {
    float heart_rate;  // The calculated heart rate in beats per minute (BPM)
} ei_impulse_result_hr_t;
Public Project
Download CSV
HR/HRV Features block documentation for more deployment details
Enterprise plan
HR/HRV Features
Edge Impulse Forum
DaLIA-PPG Public Project
Illustration of deriving HRV data from ECG and PPG signals, (a) RR intervals in the ECG signal, and (b) PP intervals in the PPG signal. N.U.: Normalised units.
Data Upload
Create Impulse
PPG HR-HRV Settings
ECG HR-HRV Settings
PPG + Accelerometer HR-HRV Settings
HR-HRV Block Generate Features
Regression Block Configuration
HR-HRV Test Classification
HR-HRV Enterprise Deployment
HR-HRV Deployment

No common issues have been identified thus far. If you encounter an issue, please reach out on the forum or, if you are on the Enterprise plan, through your support channels.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Detect objects with centroid (FOMO)

FOMO (Faster Objects, More Objects) is a brand-new approach to run object detection models on constrained devices. FOMO is a ground-breaking algorithm that brings real-time object detection, tracking and counting to microcontrollers for the first time. FOMO is 30 times faster than MobileNet SSD and can run under 200K of RAM.

In this tutorial, we will explain how to count cars to estimate parking occupancy using FOMO.

View the finished project, including all data, signal processing and machine learning blocks here: Car Parking Occupancy Detection - FOMO.

Limitations of FOMO

  • FOMO does not output bounding boxes but will give you the object's location using centroids. Hence the size of the object is not available.

  • FOMO works better if the objects have a similar size.

  • Objects shouldn’t be too close to each other, although this can be optimized when increasing the image input resolution.

If you need the size of the objects for your project, head to the default object detection. tutorial.

1. Prerequisites

For this tutorial, you'll need a supported device.

If you don't have any of these devices, you can also upload an existing dataset through the Uploader or use your mobile phone to connect your device to Edge Impulse. After this tutorial, you can then deploy your trained machine learning model as a C++ library or as a WebAssembly package and run it on your device.

2. Building a dataset

Capturing data

You can collect data from the following devices:

  • Collecting image data from the Studio - for the Raspberry Pi 4 and the Jetson Nano.

  • Collecting image data with your mobile phone

  • Collecting image data from any of the fully-supported development boards that have a camera.

Alternatively, you can capture your images using another camera, and then upload them directly from the studio by going to Data acquisition and clicking the 'Upload' icon or using Edge Impulse CLI Uploader.

With the data collected, we need to label this data. Go to Data acquisition, verify that you see your data, then click on the 'Labeling queue' to start labeling.

Labeling data

Why use bounding box inputs?

To keep the interoperability with other models, your training image input will use bounding boxes although we will output centroids in the inference process. As such FOMO will use in the background translation between bounding boxes and segmentation maps in various parts of the end-to-end flow. This includes comparing sets between the bounding boxes and the segmentation maps to run profiling and scoring.

All our collected images will be staged for annotation at the "labeling queue". Labeling your objects is as easy as dragging a box around the object, and entering a label. However, when you have a lot of images, this manual annotation method can become tiresome and time consuming. To make this task even easier, Edge impulse provides 3 AI assisted labeling methods that can help you save time and energy. The AI assisted labeling techniques include:

  • Using YoloV5 - Useful when your objects are part of the common objects in the COCO dataset.

  • Using your own trained model - Useful when you already have a trained model with classes similar to your new task.

  • Using Object tracking - Useful when you have objects that are similar in size and common between images/frames.

For our case, since the 'car' object is part of the COCO dataset, we will use the YoloV5 pre-trained model to accelerate this process. To enable this feature, we will first click the Label suggestions dropdown,then select “Classify using YOLOv5.”

YoloV5 AI Assisted labelling

From the image above, the YOLOV5 model can already help us annotate more than 90% of the cars without us having to do it manually by our hands.

Rebalancing your dataset

To validate whether a model works well you want to keep some data (typically 20%) aside, and don't use it to build your model, but only to validate the model. This is called the 'test set'. You can switch between your training and test sets with the two buttons above the 'Data collected' widget. If you've collected data on your development board there might be no data in the testing set yet. You can fix this by going to Dashboard > Perform train/test split.

3. Designing an impulse

One of the beauties of FOMO is its fully convolutional nature, which means that just the ratio is set. Thus, it gives you more flexibility in its usage compared to the classical object detection. method. For this tutorial, we have been using 96x96 images but it will accept other resolutions as long as the images are square.

To configure this, go to Create impulse, set the image width and image height to 96, the 'resize mode' to Fit shortest axis and add the 'Images' and 'Object Detection (Images)' blocks. Then click Save Impulse.

96*96 input image size

Configuring the processing block

To configure your processing block, click Images in the menu on the left. This will show you the raw data on top of the screen (you can select other files via the drop-down menu), and the results of the processing step on the right. You can use the options to switch between RGB and Grayscale modes. Finally, click on Save parameters.

Configuring the processing block

This will send you to the 'Feature generation' screen. In here you'll:

  • Resize all the data.

  • Apply the processing block on all this data.

  • Create a 3D visualization of your complete dataset.

  • Click Generate features to start the process.

Afterward, the Feature explorer will load. This is a plot of all the data in your dataset. Because images have a lot of dimensions (here: 96x96x1=9216 features for grayscale) we run a process called 'dimensionality reduction' on the dataset before visualizing this. Here the 9216 features are compressed down to 2, and then clustered based on similarity as shown in the feature explorer below.

Feature explorer

Configuring the object detection model with FOMO

With all data processed it's time to start training our FOMO model. The model will take an image as input and output objects detected using centroids. For our case, it will show centroids of cars detected on the images.

FOMO is fully compatible with any MobileNetV2 model, and depending on where the model needs to run you can pick a model with a higher or lower alpha. Transfer learning also works (although you need to train your base models specifically with FOMO in mind). Another advantage of FOMO is that it has very few parameters to learn from compared to normal SSD networks making the network even much smaller and faster to train. Together this gives FOMO the capabilities to scale from the smallest microcontrollers to full gateways or GPUs.

To configure FOMO, head over to the ‘Object detection’ section, and select 'Choose a different model' then select one of the FOMO models as shown in the image below.

Selecting FOMO model

Make sure to start with a learning rate of 0.001 then click start training. After the model is done you'll see accuracy numbers below the training output. You have now trained your FOMO object detection model!

Training results

As you may have noticed from the training results above, FOMO uses F1 Score as its base evaluating metric as compared to SSD MobileNetV2 which uses Mean Average Precision (mAP). Using Mean Average Precision (mAP) as the sole evaluation metric can sometimes give limited insights into the model’s performance. This is particularly true when dealing with datasets with imbalanced classes as it only measures how accurate the predictions are without putting into account how good or bad the model is for each class. The combination between F1 score and a confusion matrix gives us both the balance between precision and recall of our model as well as how the model performs for each class.

4. Validating your model

With the model trained let's try it out on some test data. When collecting the data we split the data up between a training and a testing dataset. The model was trained only on the training data, and thus we can use the data in the testing dataset to validate how well the model will work in the real world. This will help us ensure the model has not learned to overfit the training data, which is a common occurrence. To validate our model, we will go to Model Testing and select Classify all.

Model testing results

Given the little training data we had and the few cycles we trained on, we got an accuracy of 84.62% which can be improved further. To see the classification in detail, we will head to Live Classification* and select one image from our test sample. Click the three dots next to an item, and select Show classification. We can also capture new data directly from your development board from here.

Live Classification Result

Live Classification - Side by Side

From the test image above, our model was able to detect 16 cars out of the actual possible 18 which is a good performance. This can be seen in side by side by default, but you can also switch to overlay mode to see the model's predictions against the actual image content.

Overlay Mode for the Live Classification Result

Live Classification - Overlay

A display option where the original image and the model's detections overlap, providing a clear juxtaposition of the model's predictions against the actual image content.

Summary Table

Summary Table

The summary table for a FOMO classification result provides a concise overview of the model's performance on a specific sample file, such as 'Parking_data_2283.png.2tk8c1on'. This table is organized as follows:

CATEGORY: Metric, Object category, or class label, e.g., car.COUNT: Shows detection accuracy, frequency, e.g., car detected 7 times.

INFO: Provides performance metrics definitions, including F1 Score, Precision, and Recall, which offer insights into the model's accuracy and efficacy in detection:

Table MetricsF1 Score: (77.78%): Balances precision and recall.Precision: (100.00%): Accuracy of correct predictions.Recall: (63.64%): Proportion of actual objects detected.

Viewing Options

Bottom-right controls adjust the visibility of ground truth labels and model predictions, enhancing the analysis of the model's performance:

Prediction Controls: Customize the display of model predictions, including:

  • Show All: Show all detections and confidence scores.

  • Show Correct Only: Focus on accurate model predictions.

  • Show incorrect only: Pinpoint undetected objects in the ground truth.

Ground Truth Controls: Toggle the visibility of original labels for direct comparison with model predictions.

  • Show All: Display all ground truth labels.

  • Hide All: Conceal all ground truth labels.

  • Show detected only: Highlight ground truth labels detected by the model.

  • Show undetected only: Identify ground truth labels missed by the model.

5. Running the model on your device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the preprocessing steps, neural network weights, and classification code - in a single C++ library or model file that you can include in your embedded software.

Running the impulse on a Linux-based device

From the terminal just run edge-impulse-linux-runner. This will build and download your model, and then run it on your development board. If you're on the same network you can get a view of the camera, and the classification results directly from your dev board. You'll see a line like:

Want to see a feed of the camera and live classification in your browser? Go to http://192.168.8.117:4912

Open this URL in a browser to see your impulse running!

Running FOMO object detection on a Raspberry Pi 4

Running the impulse on a fully supported MCU

Go to the Deployment tab, on Build firmware section and select the board-compatible firmware to download it.

Compiling firmware for Arduino Portenta H7

Follow the instruction provided to flash the firmware to your board and head over to your terminal and run the edge-impulse-run-impulse --debug command:

Running impulse using edgeimpulse CLI on terminal

You'll also see a URL you can use to view the image stream and results in your browser:

Want to see a feed of the camera and live classification in your browser? Go to http://192.168.8.117:4912

Running the impulse using a generated Arduino Library

To run using an Arduino library, go to the studio Deployment tab on Create Library section and select Arduino Library to download your custom Arduino library. Go to your Arduino IDE, then click on Sketch >> Include Library >> Add .Zip ( Your downloaded Arduino library). Make sure to follow the instruction provided on Arduino's Library usage. Open Examples >> Examples from custom library and select your library. Upload the ''Portenta_H7_camera'' sketch to your Portenta then open your serial monitor to view results.

Running FOMO model using Arduino Library

Integrating the model in your own application

Congratulations! You've added object detection using FOMO to your sensors. Now that you've trained your model you can integrate your impulse in the firmware of your own edge device, see Deploy your model as a C++ library or the Edge Impulse for Linux documentation for the Node.js, Python, Go and C++ SDKs that let you do this in a few lines of code and make this model run on any device.

Here's an example of sending a text message through Twilio when an object is seen.

Or if you're interested in more, see our tutorials on Continuous motion recognition or Image classification. If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build custom processing blocks to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Generate vision datasets with syntheticAIdata

Vision Datasets is a platform by syntheticAIdata that offers curated, high-quality computer vision datasets. These datasets consist of synthetic images designed to provide diverse and comprehensive training data. By following this guide, you will learn how to create and upload a dataset from Vision Datasets to Edge Impulse, just like this Screws Nuts Bolts and Washers dataset that is available in Edge Impulse Studio.:

Screws Nuts Bolts and Washers Dataset

Getting started with syntheticAIdata

This guide explains how to import image datasets from Vision Datasets into Edge Impulse for training, testing, and deployment of computer vision models. This integration simplifies your workflow by eliminating the need for manual data collection or preprocessing.

Vision Datasets

Prerequisites

  • An Edge Impulse account with a project created.

  • A Vision Datasets account (free to sign up).

Select and upload a dataset to Edge Impulse

To get started with Vision Datasets and Edge Impulse integration, follow these steps:

  1. Log in to the Vision Datasets dashboard.

Vision Datasets - Screws Nuts Bolts and Washers Dataset
  1. Browse or search for a relevant dataset.

  • Each dataset card shows:

    • Description, preview images, image count, file size, classes, version, license

  1. Click Upload to Edge Impulse on your chosen dataset.

Vision Datasets - Download or Upload to Edge Impulse
  1. In the popup, provide:

  • Edge Impulse API Key

    • Find it in your Edge Impulse project: Dashboard → Keys → API Keys

  • Image volume (number of images to upload)

  • Image resolution (e.g., 96×96, 320×320, 512×512)

  • Classes to include (select one or more)

Vision Datasets - Classes to include

Example: Selecting 500 images and 2 classes uploads 1,000 images (500 per class).*

  1. Click Upload to start the transfer.

  2. After upload, you’ll see a success confirmation.

Vision Datasets - Confirmation

All data appears under Data Acquisition in Edge Impulse, pre-labeled and ready to use.

You can also use Download to export the dataset locally for inspection.

Review and verify data in Edge Impulse

  1. Go to Edge Impulse Studio and open your project.

  2. Navigate to Data Acquisition.

  3. Review your uploaded images:

  • Correct labels (per selected class)

  • Selected resolution

  • Training/testing split

  1. Use filters or search to explore specific classes or subsets.

  2. Click any image to preview.

Vision Datasets - Preview image

Verifying your data ensures high-quality inputs for your pipeline. To rebalance or modify your dataset, repeat the upload or manually add/remove data.

You can repeat uploads with different classes, resolutions, or volumes anytime from Vision Datasets.

Vision Datasets - Enter Project API Key

Summary

By combining the breadth of Vision Datasets with the edge-deployment power of Edge Impulse, you can go from zero images to a fully-trained, device-ready model in record time. Try the integration yourself and let us know what you build!

Head over to visiondatasets.com, grab a free sample, and start building computer-vision models faster than ever.

Next steps: building a machine learning model

With everything set up you can now build your first machine learning model with these tutorials:

  • Keyword spotting

  • Sound recognition

  • Image classification

  • Detect objects with bounding boxes

  • Detect objects with centroids (FOMO)

Troubleshooting

If you encounter issues during the registration or upload process, here are some common troubleshooting steps:

Unable to Register or Receive Confirmation Email

  • Check spam/junk folders.

  • Ensure your email is correct.

  • Resend the code or use a different email if needed.

Vision Datasets - Upload confirmed

Edge Impulse API Key Not Accepted

  • Use an API key from your Edge Impulse project (Dashboard → Keys → API Keys).

  • Remove extra spaces/characters.

  • Generate a new API key if needed.

Vision Datasets - Generate a new Key

Data Not Labeled in Edge Impulse Studio

  • In Edge Impulse Studio, go to Dashboard.

  • On the Project info card, set Labeling method to Bounding boxes (object detection) for correct labeling and display.

Vision Datasets - Label with another method

Cloud data storage

Edge Impulse makes it easy to access data that you have stored in the cloud by offering integrations with several storage providers and the flexibility to connect a storage solution to an organization or directly to a project.

Connecting to storage

There are two locations within Edge Impulse where you can connect to cloud storage, from within an organization or within a project. These options are described below. For details related to the specific storage provider integration options available, please see the Storage provider integrations section of this document.

Organization data bucket

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Cloud storage can be connected to an organization. By connecting your data to an organization, you are offered the flexibility to pre-process your datasets through the use of transformation blocks and to feed your datasets into multiple projects.

To connect, access your organization, select Data in the left sidebar menu, select the Buckets tab at the top of the page, then click the + Add new bucket button. Follow the instructions in the modal window that pops up.

Connecting a cloud storage provider to an organization

Project data source

Cloud storage can be connected directly to a project. To connect, access your project, select Data acquisition in the left sidebar menu, select the Data sources tab at the top of the page, then click the + Add new data source. Follow the instructions in the modal window that pops up.

Note that some options in the modal will be greyed out if your project is not on the Enterprise plan.

Connecting a cloud storage provider to a project - step 1
Connecting a cloud storage provider to a project - step 2

Storage provider integrations

Edge Impulse allows you to integrate with several cloud storage options. These include:

  • Amazon S3 buckets

  • Google Cloud Storage buckets

  • Microsoft Azure Blob Storage blob containers

  • Other (S3-compatible) buckets

Amazon S3 bucket

To connect to an Amazon S3 bucket, you will need to provide:

  • The bucket name

  • The bucket region

  • An access key

  • A secret key

  • A path prefix (optional)

If the credentials provided do not have access to the root of the bucket, the prefix is used to specify the path for which the credentials are valid.

Currently only long-term credentials from AWS IAM users are supported; temporary credentials provided to AWS SSO users are not supported.

Connecting an Amazon S3 bucket

CORS policy

For Amazon S3 buckets, you will also need to enable CORS headers for the bucket. You can do this in the S3 console by going to your bucket, going to the permissions tab, and then adding the policy defined below to the cross-origin resource sharing section.

CORS policy (console):

[
    {
        "AllowedOrigins": ["https://studio.edgeimpulse.com"],
        "AllowedMethods": ["PUT", "POST"],
        "AllowedHeaders": ["*"],
        "ExposeHeaders": []
    }
]

Alternatively, you can save the below CORS policy as a cors.json file (note there are some differences in the structure compared to the JSON above) and add it to your bucket using the AWS S3 CLI.

CORS policy (CLI):

{
      "CORSRules": [
        {
            "AllowedOrigins": ["https://studio.edgeimpulse.com"],
            "AllowedMethods": ["PUT", "POST"],
            "AllowedHeaders": ["*"],
            "ExposeHeaders": []
        }
    ]
}
aws s3api put-bucket-cors --bucket <your-bucket-name> --cors-configuration file://cors.json

Google Cloud Storage bucket

To connect to a Google Cloud Storage bucket, you will need to provide:

  • The bucket name

  • The bucket region

  • An access key

  • A secret key

  • A path prefix (optional)

If the credentials provided do not have access to the root of the bucket, the prefix is used to specify the path for which the credentials are valid.

Connecting a Google Cloud Storage bucket

CORS policy

For Google Cloud Storage buckets, you will also need to enable CORS headers for the bucket. You cannot manage CORS policies using the Google Cloud console; you must use the gcloud CLI instead.

CORS policy:

[
    {
        "origin": ["https://studio.edgeimpulse.com"],
        "method": ["PUT", "POST"],
        "responseHeader": ["*"],
        "maxAgeSeconds": 31536000
    }
]

Save the above CORS policy as a cors.json file and add it to your bucket with the gcloud CLI using the following command:

gcloud storage buckets update gs://<your-bucket-name> --cors-file=cors.json

gsutil is not the recommended CLI tool for Cloud Storage. You may have used this tool before, however, Google now recommends using gcloud storage commands in the Google Cloud CLI instead.

Microsoft Azure Blob Storage blob container

To connect to a Microsoft Azure Blob Storage blob container, you will need to provide:

  • The blob container name

  • The storage account name

  • A secret key

  • A path prefix (optional)

If the credentials provided do not have access to the root of the blob container, the prefix is used to specify the path for which the credentials are valid.

Connecting a Microsoft Azure Blob Storage blob container

CORS policy

A CORS policy is not required with Microsoft Azure Blob Storage.

Other (S3-compatible) buckets

To connect to another (S3-compatible) type of bucket, you will need to provide:

  • The bucket name

  • The bucket region

  • The bucket endpoint

  • An access key

  • A secret key

  • A path prefix (optional)

If the credentials provided do not have access to the root of the bucket, the prefix is used to specify the path for which the credentials are valid.

Connecting an other (S3-compatible) storage bucket

CORS policy

For other (S3-compatible) buckets, you will also need to enable CORS headers for the bucket. Please refer to your provider documentation for instructions on how to do so.

The items that you will need to set are the following:

  • Origin: ["https://studio.edgeimpulse.com"]

  • Method: ["PUT", "POST"]

  • Header: ["*"]

Permissions required

For cloud storage integration to work as expected, Edge Impulse needs to be provided with credentials that allow read, write, and delete operations. Please refer to your storage provider documentation for specifics.

Verifying the connection

In order to verify the connection to the cloud storage provider, Edge Impulse will write an .ei-portal-check file that will be subsequently deleted. Once a bucket is successfully connected to your organization, a green dot will appear in the connected column on the buckets overview page.

Cloud data storage providers successfully connected to an organization

Troubleshooting

No common issues have been identified thus far. If you encounter an issue, please reach out on the or, if you are on the Enterprise plan, through your support channels.

Additional resources

  • Organization data

  • Data sources

  • What is Amazon S3?

  • Product overview of Google Cloud Storage

  • Introduction to Azure Blob Storage

Custom synthetic data blocks

Custom synthetic data blocks are a way to extend the synthetic data feature within Edge Impulse. If none of the blocks created by Edge Impulse that are built into the platform fit your needs, you can modify them or develop from scratch to create a custom synthetic data block. This allows you to integrate your own data generation techniques for unique project requirements.

Ready to dive in and start building? Jump to the examples!

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Block structure

Synthetic data blocks are an extension of transformation blocks operating in standalone mode and, as such, follow the same structure without being able to pass a directory or file directly to your scripts. Please see the custom blocks overview page for more details.

Custom synthetic data block structure

Block interface

The sections below define the required and optional inputs and the expected outputs for custom synthetic data blocks.

Inputs

Synthetic data blocks have access to environment variables, command line arguments, and mounted storage buckets.

Environment variables

The following environment variables are accessible inside of synthetic data blocks. Environment variable values are always stored as strings.

Variable
Passed
Description

EI_API_ENDPOINT

Always

The API base URL: https://studio.edgeimpulse.com/v1

EI_API_KEY

Always

The organization API key with member privileges: ei_2f7f54...

EI_INGESTION_HOST

Always

The host for the ingestion API: edgeimpulse.com

EI_ORGANIZATION_ID

Always

The ID of the organization that the block belongs to: 123456

EI_PROJECT_ID

Always

The ID of the project: 123456

EI_PROJECT_API_KEY

Always

The project API key: ei_2a1b0e...

You can also define your own environment variables to pass to your custom block using the requiredEnvVariables property in the parameters.json file. You will then be prompted for the associated values for these properties when pushing the block to Edge Impulse using the CLI. Alternatively, these values can be added (or changed) by editing the block in Studio.

Command line arguments

The parameter items defined in your parameters.json file will be passed as command line arguments to the script you defined in your Dockerfile as the ENTRYPOINT for the Docker image. Please refer to the parameters.json documentation for further details about creating this file, parameter options available, and examples.

In addition to the items defined by you, specific arguments will be automatically passed to your synthetic data block.

Synthetic data blocks are an extension of transformation blocks operating in standalone mode, the arguments that are automatically passed to transformation blocks in this mode are also automatically passed to synthetic data blocks. Please refer to the custom transformation blocks documentation for further details on those parameters.

Along with the transformation block arguments, the following synthetic data specific arguments are passed as well.

Argument
Passed
Description

--synthetic-data-job-id <job-id>

Always

Provides the job ID as an integer. The job ID must be passed as the x-synthetic-data-job-id header value when uploading data to Edge Impulse through the ingestion API.

Additional CLI arguments can also be specified using the CLI arguments field when editing the block in Studio.

Mounted storage buckets

One or more cloud data storage buckets can be mounted inside of your block. If storage buckets exist in your organization, you will be prompted to mount the bucket(s) when initializing the block with the Edge Impulse CLI. The default mount point will be:

/mnt/s3fs/<bucket-name>

The mount point can be changed by editing the block in Studio after pushing.

Outputs

There are no required outputs from synthetic data blocks. In general, the data that is generated is uploaded to Edge Impulse using the data ingestion API.

Setting the ingestion API request header

When uploading synthetic data to Edge Impulse using the ingestion API, you will need to include the the x-synthetic-data-job-id header in your request. The value for this header is the job ID provided to your block through the --synthetic-data-job-id <job-id> argument.

Initializing the block

Testing the block locally

Synthetic data blocks are not supported by the edge-impulse-blocks runner CLI tool

Synthetic data blocks are not currently supported by the blocks runner in the Edge Impulse CLI. To test you custom synthetic data block, you will need to build the Docker image and run the container directly. You will need to pass any environment variables or command line arguments required by your script to the container when you run it.

docker build -t custom-synthetic-data-block .
docker run --rm -e EI_PROJECT_API_KEY='ei_...' -e CUSTOM_ENV_VAR='<env-value>' custom-synthetic-data-block --synthetic-data-job-id 123456789 --custom-param-one foo --custom-param-two bar

Pushing the block to Edge Impulse

Using the block in a project

Examples

Edge Impulse has developed several synthetic data blocks, some of which are built into the platform. The code for these blocks can be found in public repositories under the Edge Impulse GitHub account. The repository names typically follow the convention of example-transform-<description>. As such, they can be found by going to the Edge Impulse account and searching the repositories for example-transform.

Note that when using the above search term you will come across transformation blocks as well. Please read the repository description to identify if it is for a synthetic data block or a transformation block.

Below are direct links to a some examples:

  • Image generation with DALL-E 3

  • Keyword generation with Whisper

  • Composite image generation

Troubleshooting

No common issues have been identified thus far. If you encounter an issue, please reach out on the or, if you are on the Enterprise plan, through your support channels.

Additional resources

  • Custom blocks

  • Synthetic data

  • Ingestion API

  • edge-impulse-blocks

  • parameters.json

with Docker on Balena

Introduction

This page is part of the Lifecycle management with Edge Impulse tutorial series. If you haven't read the introduction yet, we recommend you to do so here. Balena can serve as the platform for deploying OTA updates to edge devices, including new models trained with Edge Impulse.

Prerequisites

  • An active Edge Impulse account with a project and trained impulse. See the Edge Impulse - Object Detection Tutorials for more information on how to create a project and train an impulse.

  • Follow the Edge Impulse Docker documentation.

  • Balena Example Repo - A sample balena project to deploy your impulse as on balena. It exposes an API which the "cam" service uses to get repeated inferences from the images captured by the webcam. See the README

Overview

Balena offers a comprehensive platform for building, deploying and managing large fleets of IoT devices. It simplifies fleet management, enhances security, and streamlines the deployment of software and hostOS updates. This tutorial will guide you through using balena to deploy Edge Impulse model updates across your device fleet efficiently. This can be particularly useful for managing multiple devices in the field, ensuring they are always running the latest model. Devices like the NVIDIA Jetson Nano, NVIDIA Jetson Orin, Raspberry Pi, and other single-board computers are supported by balena and can be used to deploy Edge Impulse models.

For this example, we will deploy an Edge Impulse model as a Docker container on a Raspberry Pi using balenaOS. The model will run an HTTP inference server, allowing you to send data to the device for processing and receive predictions in real-time.

Prerequisites

  • An active Edge Impulse account with a trained model.

  • Follow the Edge Impulse Docker documentation.

Introduction to balena

Balena is a platform that provides tools for building, deploying, and managing large fleets of IoT devices. It simplifies the process of managing fleets of devices, offering a robust framework for deploying software and hostOS updates, monitoring device health, and ensuring security. Balena could serve as the fleet management and device management platform for deploying OTA updates, including new models trained with Edge Impulse.

Step 1: Exporting Your Model as a Docker Container

Go to your Edge Impulse project, navigate to the Deployment section and select Docker container as the deployment option. Follow the instructions to generate the Docker container command. It will look something like this:

Docker - Export

Note the Container address and the api-key, as we will need to use them in a subsequent step below.

Step 2: Preparing Your Balena Fleet

Log in to your balenaCloud dashboard and create a new fleet.

Balena - Add new device

BalenaOS - Add a new Device Select the appropriate device type that matches your hardware. Follow the instructions to add a device to your application and download the balenaOS image for it. Flash the downloaded OS image to your device's SD card using balenaEtcher or a similar tool. Power on your device with the SD card or USB key inserted; it should connect to your balena application automatically.

Step 3: Deploying Your Docker Container to Your balena Fleet

Clone the balena boilerplate Edge Impulse project from Github or start with a Dockerfile.template in a new directory on your local machine. Modify the Dockerfile.template to use your container address and api-key as mentioned earlier. Since balena uses Docker containers, you will simply use the container generated by the deployment screen of your Edge Impulse model.

# Use the specified base image
FROM public.ecr.aws/z9b3d4t5/inference-container:c0fd........97d
# Set the API key as an environment variable (optional, for security reasons you might want to handle this differently)
ENV API_KEY=ei_952ba2......66f3cc
# Expose port 1337 on the container to the host is standard for the Edge Impulse Docker container, we will change this to 80
EXPOSE 80
# Start the inference server when the container launches
CMD ["--api-key", "ei_952ba............66f3cc", "--run-http-server", "80"]

Step 4: Build Your Application

Use the balena CLI to build, and scan for your local device and push your application to balenaCloud:

balena login
balena push <your fleet on balenaCloud>

Wait for the application to build and deploy. You can monitor the progress in the balenaCloud dashboard.

Balena - Dashboard Summary

Step 5: Accessing Your Inference Server

Once deployed, your device will start the Docker container and run the HTTP inference server.

Deployed - Edge Impulse Inference Server

You can access it using the device's IP address on your local network or through the Public Device URL feature provided by balenaCloud if enabled for your device. Remember that we decided to point this server to the port 80, but feel free to use another port.

Step 6: Monitoring and Managing Your Fleet

With your Edge Impulse inference server running on balena, now you can monitor and manage the other services running on your device fleet using balenaCloud's dashboard and tools. This includes monitoring device health, deploying updates, and rolling back changes if needed.

Balena - Monitoring your fleet

Furthermore, to deploy the latest ML model deployed from Edge Impulse after retraining, you will need to restart the edge-impulse service running in the connected devices. You can do it from the balenaCloud Fleet Devices page selecting them.

Balena - Restarting your service

If you have any questions or any issues feel free to contact the balena team using the balena forums.

Conclusion

By following these steps, you should have a functional Edge Impulse inference server running on your balena devices, ready to process data and make predictions. This setup can be integrated into a robust OTA model update process, enabling Lifecycle management and improvement of your Edge AI enabled devices.

Additional Resources

  • Webinar - Edge Impulse and Balena webinar exploring this topic in more detail.

Keyword spotting

In this tutorial, you'll use machine learning to build a system that can recognize audible events, particularly your voice through audio classification. The system you create will work similarly to "Hey Siri" or "OK, Google" and is able to recognize keywords or other audible events, even in the presence of other background noise or background chatter.

You'll learn how to collect audio data from microphones, use signal processing to extract the most important information, and train a deep neural network that can tell you whether your keyword was heard in a given clip of audio. Finally, you'll deploy the system to an embedded device and evaluate how well it works.

At the end of this tutorial, you'll have a firm understanding of how to classify audio using Edge Impulse.

There is also a video version of this tutorial:

You can view the finished project, including all data, signal processing and machine learning blocks here: .

Detect non-voice audio?

We have a tutorial for that too! See .

1. Prerequisites

For this tutorial, you'll need a .

If your device is connected under Devices in the studio you can proceed:

Device compatibility

Edge Impulse can ingest data from any device - including embedded devices that you already have in production. See the documentation for the for more information.

2. Collecting your first data

In this tutorial we want to build a system that recognizes keywords, so your first job is to think of a great one. It can be your name, an action, or even a growl - it's your party. Do keep in mind that some keywords are harder to distinguish from others, and especially keywords with only one syllable (like 'One') might lead to false-positives (e.g. when you say 'Gone'). This is the reason that Apple, Google and Amazon all use at least three-syllable keywords ('Hey Siri', 'OK, Google', 'Alexa'). A good one would be "Hello world".

To collect your first data, go to Data acquisition, set your keyword as the label, set your sample length to 10s., your sensor to 'microphone' and your frequency to 16KHz. Then click Start sampling and start saying your keyword over and over again (with some pause in between).

Note: Data collection from a development board might be slow, you can use your as a sensor to make this much faster.

Afterwards you have a file like this, clearly showing your keywords, separated by some noise.

This data is not suitable for Machine Learning yet though. You will need to cut out the parts where you say your keyword. This is important because you only want the actual keyword to be labeled as such, and not accidentally label noise, or incomplete sentences (e.g. only "Hello"). Fortunately the Edge Impulse Studio can do this for you. Click ⋮ next to your sample, and select Split sample.

If you have a short keyword, enable Shift samples to randomly shift the sample around in the window, and then click Split. You now have individual 1s. long samples in your dataset. Perfect!

3. Building your dataset

Now that you know how to collect data we can consider other data we need to collect. In addition to your keyword we'll also need audio that is not your keyword. Like background noise, the TV playing ('noise' class), and humans saying other words ('unknown' class). This is required because a machine learning model has no idea about right and wrong (unless those are your keywords), but only learns from the data you feed into it. The more varied your data is, the better your model will work.

For each of these three classes ('your keyword', 'noise', and 'unknown') you want to capture an even amount of data (balanced datasets work better) - and for a decent keyword spotting model you'll want at least 10 minutes in each class (but, the more the better).

Thus, collect 10 minutes of samples for your keyword - do this in the same manner as above. The fastest way is probably through your mobile phone, collecting 1 minute clips, then automatically splitting this data. Make sure to capture wide variations of the keyword: leverage your family and your colleagues to help you collect the data, make sure you cover high and low pitches, and slow and fast speakers.

For the noise and unknown datasets you can either collect this yourself, or make your life a bit easier by using dataset of both 'noise' (all kinds of background noise) and 'unknown' (random words) data that we built for you here: .

To import this data, go to Data acquisition, click the Upload icon, and select a number of 'noise' or 'unknown' samples (there's 25 minutes of each class, but you can select less files if you want), and clicking Begin upload. The data is automatically labeled and added to your project.

Rebalancing your dataset

If you've collected all your training data through the 'Record new data' widget you'll have all your keywords in the 'Training' dataset. This is not great, because you want to keep 20% of your data separate to validate the machine learning model. To mitigate this you can go to Dashboard and select Perform train/test split. This will automatically split your data between a training class (80%) and a testing class (20%). Afterwards you should see something like this:

4. Designing your impulse

With the data set in place you can design an impulse. An impulse takes the raw data, slices it up in smaller windows, uses signal processing blocks to extract features, and then uses a learning block to classify new data. Signal processing blocks always return the same values for the same input and are used to make raw data easier to process, while learning blocks learn from past experiences.

For this tutorial we'll use the "MFCC" signal processing block. MFCC stands for Mel Frequency Cepstral Coefficients. This sounds scary, but it's basically just a way of turning raw audio—which contains a large amount of redundant information—into simplified form. Edge Impulse has many other processing blocks for audio, including "MFE" and the "Spectrogram" blocks for non-voice audio, but the "MFCC" block is great for dealing with human speech.

We'll then pass this simplified audio data into a Neural Network block, which will learn to distinguish between the three classes of audio.

In the Studio, go to the Create impulse tab, add a Time series data, an Audio (MFCC) and a Classification (Keras) block. Leave the window size to 1 second (as that's the length of our audio samples in the dataset) and click Save Impulse.

5. Configure the MFCC block

Now that we've assembled the building blocks of our Impulse, we can configure each individual part. Click on the MFCC tab in the left hand navigation menu. You'll see a page that looks like this:

This page allows you to configure the MFCC block, and lets you preview how the data will be transformed. The right of the page shows a visualization of the MFCC's output for a piece of audio, which is known as a . An MFCC spectrogram is a specially tuned spectrogram which highlights frequencies which are common in human speech (Edge Impulse also has normal spectrograms if that's more your thing).

In the spectrogram the vertical axis represents the frequencies (the number of frequency bands is controlled by 'Number of coefficients' parameter, try it out!), and the horizontal axis represents time (controlled by 'frame stride' and 'frame length'). The patterns visible in a spectrogram contain information about what type of sound it represents. For example, the spectrogram in this image shows "Hello world":

And the spectrogram in this image shows "On":

These differences are not necessarily easy for a person to describe, but fortunately they are enough for a neural network to learn to identify.

It's interesting to explore your data and look at the types of spectrograms it results in. You can use the dropdown box near the top right of the page to choose between different audio samples to visualize, or play with the parameters to see how the spectrogram changes.

In addition, you can see the performance of the MFCC block on your microcontroller below the spectrogram. This is the complete time that it takes on a low-power microcontroller (Cortex-M4F @ 80MHz) to analyze 1 second of data.

You might think based on this number that we can only classify 2 or 3 windows per second, but we continuously build up the spectrogram (as it has a time component), which takes less time, and we can thus continuously listen for events 5-6x a second, even on an 40MHz processor. This is already implemented on all , and on your own device.

Feature explorer

The spectrograms generated by the MFCC block will be passed into a neural network architecture that is particularly good at learning to recognize patterns in this type of tabular data. Before training our neural network, we'll need to generate MFCC blocks for all of our windows of audio. To do this, click the Generate features button at the top of the page, then click the green Generate features button. This will take a minute or so to complete.

Afterwards you're presented with one of the most useful features in Edge Impulse: the feature explorer. This is a 3D representation showing your complete dataset, with each data-item color-coded to its respective label. You can zoom in to every item, find anomalies (an item that's in a wrong cluster), and click on items to listen to the sample. This is a great way to check whether your dataset contains wrong items, and to validate whether your dataset is suitable for ML (it should separate nicely).

6. Configure the neural network

With all data processed it's time to start training a neural network. Neural networks are algorithms, modeled loosely after the human brain, that can learn to recognize patterns that appear in their training data. The network that we're training here will take the MFCC as an input, and try to map this to one of three classes—your keyword, noise or unknown.

Click on NN Classifier in the left hand menu. You'll see the following page:

A neural network is composed of layers of virtual "neurons", which you can see represented on the left hand side of the NN Classifier page. An input—in our case, an MFCC spectrogram—is fed into the first layer of neurons, which filters and transforms it based on each neuron's unique internal state. The first layer's output is then fed into the second layer, and so on, gradually transforming the original input into something radically different. In this case, the spectrogram input is transformed over four intermediate layers into just two numbers: the probability that the input represents your keyword, and the probability that the input represents 'noise' or 'unknown'.

During training, the internal state of the neurons is gradually tweaked and refined so that the network transforms its input in just the right ways to produce the correct output. This is done by feeding in a sample of training data, checking how far the network's output is from the correct answer, and adjusting the neurons' internal state to make it more likely that a correct answer is produced next time. When done thousands of times, this results in a trained network.

A particular arrangement of layers is referred to as an architecture, and different architectures are useful for different tasks. The default neural network architecture provided by Edge Impulse will work well for our current project, but you can also define your own architectures. You can even import custom neural network code from tools used by data scientists, such as TensorFlow and Keras (click the three dots at the top of the page).

Before you begin training, you should change some values in the configuration. Change the Minimum confidence rating to 0.6. This means that when the neural network makes a prediction (for example, that there is 0.8 probability that some audio contains "hello world") Edge Impulse will disregard it unless it is above the threshold of 0.6.

Next, enable 'Data augmentation'. When enabled your data is randomly mutated during training. For example, by adding noise, masking time or frequency bands, or warping your time axis. This is a very quick way to make your dataset work better in real life (with unpredictable sounds coming in), and prevents your neural network from overfitting (as the data samples are changed every training cycle).

With everything in place, click Start training. You'll see a lot of text flying past in the Training output panel, which you can ignore for now. Training will take a few minutes. When it's complete, you'll see the Last training performance panel appear at the bottom of the page:

Congratulations, you've trained a neural network with Edge Impulse! But what do all these numbers mean?

At the start of training, 20% of the training data is set aside for validation. This means that instead of being used to train the model, it is used to evaluate how the model is performing. The Last training performance panel displays the results of this validation, providing some vital information about your model and how well it is working. Bear in mind that your exact numbers may differ from the ones in this tutorial.

On the left hand side of the panel, Accuracy refers to the percentage of windows of audio that were correctly classified. The higher number the better, although an accuracy approaching 100% is unlikely, and is often a sign that your model has overfit the training data. You will find out whether this is true in the next stage, during model testing. For many applications, an accuracy above 85% can be considered very good.

The Confusion matrix is a table showing the balance of correctly versus incorrectly classified windows. To understand it, compare the values in each row. For example, in the above screenshot, 96 of the helloworld audio windows were classified as helloworld, while 10 of them were incorrectly classified as unknown or noise. This appears to be a great result.

The On-device performance region shows statistics about how the model is likely to run on-device. Inferencing time is an estimate of how long the model will take to analyze one second of data on a typical microcontroller (an Arm Cortex-M4F running at 80MHz). Peak RAM usage gives an idea of how much RAM will be required to run the model on-device.

7. Classifying new data

The performance numbers in the previous step show that our model is working well on its training data, but it's extremely important that we test the model on new, unseen data before deploying it in the real world. This will help us ensure the model has not learned to overfit the training data, which is a common occurrence.

Fortunately we've put aside 20% of our data already in the 'Test set' (see Data acquisition). This is data that the model has never seen before, and we can use this to validate whether our model actually works on unseen data. To run your model against the test set, head to Model testing, select all items and click Classify selected.

To drill down into a misclassified sample, click the three dots (⋮) next to a sample and select Show classification. You're then transported to the classification view, which lets you inspect the sample, and compare the sample to your training data. This way you can inspect whether this was actually a classification failure, or whether your data was incorrectly labeled. From here you can either update the label (when the label was wrong), or move the item to the training set to refine your model.

Misclassifications and uncertain results

It's inevitable that even a well-trained machine learning model will sometimes misclassify its inputs. When you integrate a model into your application, you should take into account that it will not always give you the correct answer.

For example, if you are classifying audio, you might want to classify several windows of data and average the results. This will give you better overall accuracy than assuming that every individual result is correct.

8. Deploying to your device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the MFCC algorithm, neural network weights, and classification code - in a single C++ library that you can include in your embedded software.

Mobile phone

Your mobile phone can build and download the compiled impulse directly from the mobile client. See 'Deploying back to device' on the page.

To export your model, click on Deployment in the menu. Then under 'Build firmware' select your development board, and click Build. This will export the impulse, and build a binary that will run on your development board in a single step. After building is completed you'll get prompted to download a binary. Save this on your computer.

Flashing the device

When you click the Build button, you'll see a pop-up with text and video instructions on how to deploy the binary to your particular device. Follow these instructions. Once you are done, we are ready to test your impulse out.

Running the model on the device

We can connect to the board's newly flashed firmware over serial. Open a terminal and run:

Serial daemon

If the device is not connected over WiFi, but instead connected via the Edge Impulse serial daemon, you'll need stop the daemon. Only one application can connect to the development board at a time.

This will capture audio from the microphone, run the MFCC code, and then classify the spectrogram:

Great work! You've captured data, trained a model, and deployed it to an embedded device. You can now control LEDs, activate actuators, or send a message to the cloud whenever you say a keyword!

Poor performance due to unbalanced dataset?

Is your model working properly in the Studio, but does not recognize your keyword when running in continuous mode on your device? Then this is probably due to dataset imbalance (a lot more unknown / noise data compared to your keyword) in combination with our moving average code to reduce false positives.

When running in continuous mode we run a moving average over the predictions to prevent false positives. E.g. if we do 3 classifications per second you’ll see your keyword potentially classified three times (once at the start of the audio file, once in the middle, once at the end). However, if your dataset is unbalanced (there’s a lot more noise / unknown than in your dataset) the ML model typically manages to only find your keyword in the 'center' window, and thus we filter it out as a false positive.

You can fix this by either:

  1. Adding more data :-)

  2. Or, by disabling the moving average filter by going into ei_run_classifier.h (in the edge-impulse-sdk directory) and removing:

Note that this might increase the number of false positives the model detects.

9. Conclusion

Congratulations! you've used Edge Impulse to train a neural network model capable of recognizing audible events. There are endless applications for this type of model, from monitoring industrial machinery to recognizing voice commands. Now that you've trained your model you can integrate your impulse in the firmware of your own embedded device, see . There are examples for Mbed OS, Arduino, STM32CubeIDE, Zephyr, and any other target that supports a C++ compiler.

Or if you're interested in more, see our tutorials on or . If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Generate image datasets using Dall-E

This notebook explores how we can use generative AI to create datasets which don't exist yet. This can be a good starting point for your project if you have not collected or cannot collect the data required. It is important to note the limitations of generative AI still apply here, biases can be introduced through your prompts, results can include "hallucinations" and quality control is important.

This example uses the OpenAI API to call the Dall-E image generation tool, it explores both generation and variation but there are other tools such as editing which could also be useful for augmenting an existing dataset.

There is also a video version of this tutorial:

We have wrapped this example into a (Enterprise Feature) to make it even easier to generate images and upload them to your organization. See:

Local Software Requirements

  • Python 3

  • Pip package manager

  • Jupyter Notebook: https://jupyter.org/install

  • pip packages (install with pip installpackagename):

    • openai https://pypi.org/project/openai/

Set up OpenAI API

First off you will need to set up and Edge Impulse account and create your first project.

You will also need to create an API Key for OpenAI: https://platform.openai.com/docs/api-reference/authentication

Generate your first image

The API takes in a prompt, number of images and a size

Generate some variations of this image

The API also has a variations call which takes in an existing images and creates variations of it. This could also be used to modify existing images.

Generate a dataset:

Here we are iterate through a number of images and variations to generate a dataset based on the prompts/labels given.

Plot all the output images:

These files can then be uploaded to a project with these commands (run in a separate terminal window):

(run edge-impulse-uploader --clean if you have used the CLI before to reset the target project)

What next?

Now you can use your images to create an image classification model on Edge Impulse.

Why not try some other OpenAI calls, 'edit' could be used to take an existing image and translate it into different environments or add different humans to increase the variety of your dataset. https://platform.openai.com/docs/guides/images/usage

Visualize neural networks decisions with Grad-CAM

In this tutorial, we will see how Grad-CAM can help you visualize neural network decisions. The output of this tutorial will be a Grad-CAM overlay of your test dataset.

Example of a “car vs not car” model, highlighting the learned “features” of a car:

This tutorial will use a custom deployment block that you can add into your Organization (Enterprise only). This will make the Grad-CAM overlay output available in all your organization's projects.

We also have an option to run it using a . This option will work for non-Enterprise projects.

Note: This currently works for image classification and visual regression models where at least one 2D convolution layer is present in your architecture.

Source code:

Jupyter notebook:

Blog post:

Grad-CAM

Grad-CAM (Gradient-weighted Class Activation Mapping) is a technique that helps interpret the predictions of a convolutional neural network (CNN) by visualizing the regions of the input image that most influenced the model's decision. This is achieved by:

  1. Computing gradients of the target output with respect to the feature maps of the last convolutional layer.

  2. Weighting the feature maps by the importance of these gradients.

  3. Generating a heatmap that highlights the areas of interest.

This script extends Grad-CAM for:

  • Classification Models: Highlights areas contributing to the predicted class.

  • Visual Regression Models: Highlights areas contributing to the numerical regression output.

If you want more information on the Grad-CAM technique, we invite you to read this paper .

Google Colab

To test the functionality without setting up locally, use this . It comes pre-configured to run in a browser with no local setup required.

What does this code do?

  1. Dataset export:

    • Exports the test dataset from an Edge Impulse project.

    • Automatically retries until the dataset is ready for download.

  2. Model download:

    • Retrieves the trained model (.h5 format) for your Edge Impulse project.

    • Ensures compatibility with models containing 2D convolutional layers.

  3. Grad-CAM visualization:

    • Applies the Grad-CAM technique to visualize the regions of the input image that contributed most to the model's predictions.

    • Works for:

      • Classification models: Highlights regions associated with the predicted class.

      • Regression models: Highlights regions contributing to the predicted regression value.

  4. Output generation:

    • Saves Grad-CAM heatmaps overlaid on the original images.

    • Separates correctly predicted and incorrectly predicted samples into separate directories for easy analysis.

Custom deployment block

Clone this repository:

Initialize the block:

Push the deployment block to your organization:

Now you should see your custom deployment block in your organization:

And you can by editing the custom deployment block:

Make sure to enable the Privileged mode, this custom block needs to make API requests to retrieve the dataset and the .h5 model

To use your custom block within your project, head to the Deployment page and select the Grad-CAM visualization option, the output will be a .zip file containing your test dataset with the Grad-Cam overlay.

Testing locally with Docker

Build:

Run:

Fine-tuning the visualization

You can adjust three parameters to fine-tune the visualization: alpha, pooling-gradients, and heatmap-normalization.

1. Alpha (--alpha)

The alpha parameter controls the transparency of the Grad-CAM overlay when it is superimposed on the original image.

Default Value: 0.4

Range:

  • A value between 0 and 1.

    • 0: Fully transparent (only the original image is visible).

    • 1: Fully opaque (only the Grad-CAM overlay is visible).

How to Choose:

  • Recommended Default (0.4): Provides a balance between showing the original image and highlighting the Grad-CAM heatmap.

  • Higher Alpha (> 0.5): Use this if you want the Grad-CAM heatmap to dominate the visualization.

  • Lower Alpha (< 0.4): Use this if you want the original image to be more prominent.

2. Pooling Gradients (--pooling-gradients)

The pooling-gradients parameter determines how gradients (importance signals) are combined across the spatial dimensions of the last convolutional layer to generate the heatmap.

Options:

  • mean (default):

    • Averages the gradients across the spatial dimensions.

    • Smooth out the heatmap, providing a general overview of important regions.

  • sum_abs:

    • Takes the sum of the absolute values of the gradients.

    • Highlights areas with strong activations, which can sometimes create sharper heatmaps.

How to Choose:

  • Classification Models:

    • Use mean for a smoother and more generalized heatmap.

    • Use sum_abs if you want to emphasize the most critical regions (e.g., sharp object boundaries).

  • Regression Models:

    • sum_abs is often more useful for regression tasks, as it highlights features contributing to extreme values.

  • Experiment: Try both options to see which one provides more meaningful visualizations for your model and dataset.

3. Heatmap Normalization (--heatmap-normalization)

The heatmap-normalization parameter determines how the heatmap values are scaled for visualization.

Options:

  • percentile (default):

    • Ensures the heatmap values are scaled between 0 and 1 based on their maximum value.

    • Best for emphasizing high-activation areas while normalizing the output globally.

  • simple:

    • Normalizes the heatmap by dividing by the global maximum.

    • A simpler approach that may work well for datasets where most activations are relevant.

How to Choose:

  • Default (percentile):

    • Works well in most cases, especially if you expect only certain areas of the image to be significant.

  • Use simple:

    • Suitable for models where the activations are uniformly spread, and all areas are equally important.

Limitations

  1. This script assumes the presence of at least one 2D convolutional layer in the model architecture.

  2. It is designed for image classification and visual regression tasks.

  3. For regression models, the script uses a threshold to determine correctness; adjust this threshold (threshold = 0.1) as needed for your use case.

with Arduino IoT Cloud

Introduction

This page is part of the tutorial series. If you haven't read the introduction yet, we recommend you do so .

In this tutorial, we'll guide you through deploying updated impulses over-the-air (OTA) to Arduino using the Arduino IoT Cloud and Edge Impulse.

Let's get started!

Prerequisites:

  • Edge Impulse Account: .

  • Trained Impulse: If you're new, follow our and guides.

Key Features of Arduino OTA Updates:

  • Remote Update: Update your Arduino board firmware remotely.

  • Integration with Edge Impulse: Seamlessly incorporate machine learning models.

OTA Code

Here’s an example Arduino sketch for implementing OTA updates with Edge Impulse:

To expand the Arduino sketch to work with the Edge Impulse API, you'll need to add functionality to fetch the latest model updates from Edge Impulse and apply them to your device. Below is an extended version of the loop(), connectToWiFi(), and onOTAEvent() functions, integrating Edge Impulse API interactions:

loop() Function

  • The loop() function regularly checks for updates from the Edge Impulse API.

connectToWiFi() Function

  • The connectToWiFi() function includes a maximum number of attempts to connect to WiFi.

onOTAEvent() Function

  • The onOTAEvent() function is triggered when a new update is available, downloading and applying the update.

This sketch sets up the Arduino to connect to the Arduino IoT Cloud and listens for OTA update notifications. When an OTA update is available, the onOTAEvent function is called, where you can implement the logic to download and update the firmware.

Preparing for OTA Updates

  1. Setup Arduino IoT Cloud: Configure your device and variables in the Arduino IoT Cloud.

  2. Connect your device: Ensure your Arduino board is connected to the internet.

  3. Integrate with Edge Impulse: Export your trained impulse from Edge Impulse as an Arduino library.

Steps to Deploy Impulse to Arduino

  1. Update Sketch: Incorporate the Edge Impulse library into your Arduino sketch.

  2. Listen for OTA Updates: Use the Arduino IoT Cloud to push OTA updates to your device.

  3. Test and Monitor: After deploying the update, monitor your device's performance and ensure the new impulse is functioning correctly.

Components

  • ArduinoIoTCloud: Manages cloud connectivity and OTA updates.

  • WiFiNINA: Handles WiFi connections for compatible boards.

Conclusion

This tutorial provides a basic guide for implementing OTA updates on Arduino boards using the Arduino IoT Cloud and integrating with Edge Impulse. Expand and develop this example to suit your project's needs. Remember, testing is crucial before deploying updates in a live environment. Happy coding!

Using the Edge Impulse Python SDK with TensorFlow and Keras

is an open source library for training machine learning models. is an open source Python library that makes creating neural networks in TensorFlow much easier. We use these two libraries together to very quickly train a model to identify handwritten digits. From there, we use the Edge Impulse Python SDK library to profile the model to see how inference will perform on a target edge device. Then, we use the SDK again to convert our trained model to a C++ library that can be deployed to an edge hardware platform, such as a microcontroller.

Follow the code below to see how to train a simple machine learning model and deploy it to a C++ library using Edge Impulse.

To learn more about using the Python SDK, please see: .

You will need to obtain an API key from an Edge Impulse project. Log into and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

Train a machine learning model

We want to create a classifier that can uniquely identify handwritten digits. To start, we will use TensorFlow and Keras to train a very simple convolutional neural network (CNN) on the classic dataset, which consists of handwritten digits from 0 to 9.

Profile your model

To start, we need to list the possible target devices we can use for profiling. We need to pick from this list.

You should see a list printed such as:

A common option is the cortex-m4f-80mhz, as this is a relatively low-power microcontroller family. From there, we can use the Edge Impulse Python SDK to generate a profile for your model to ensure it fits on your target hardware and meets your timing requirements.

Deploy your model

Once you are happy with the performance of the model, you can deploy it to a number of possible hardware targets. To see the available hardware targets, run the following:

You should see a list printed such as:

The most generic target is to download a .zip file that holds a C++ library containing the inference runtime and your trained model, so we choose 'zip' from the above list. To do that, we first need to create a Classification object which contains our label strings (and other optional information about the model). These strings will be added to the C++ library metadata so you can access them in your edge application.

Note that instead of writing the raw bytes to a file, you can also specify an output_directory argument in the .deploy() function. Your deployment file(s) will be downloaded to that directory.

Important! The deployment targets list will change depending on the values provided for model, model_output_type, and model_input_type in the next part. For example, you will not see openmv listed once you upload a model (e.g. using .profile() or .deploy()) if model_input_type is not set to ei.model.input_type.ImageInput(). If you attempt to deploy to an unavailable target, you will receive the error Could not deploy: deploy_target: .... If model_input_type is not provided, it will default to . See for more information about input types.

Your model C++ library should be downloaded as the file my_model_cpp.zip in the same directory as this notebook. You are now ready to use your C++ model in your embedded and edge device application! To use the C++ model for local inference, see our documentation .

Project dashboard

After creating your Edge Impulse Studio project, you will be directed to the project's dashboard. The dashboard gives a quick overview of your project such as your project ID, the number of devices connected, the amount of data collected, the preferred labeling method, among other editable properties. You can also enable some additional capabilities to your project such as collaboration, making your project public, and showcasing your public projects using Markdown READMEs as we will see.

The figure below shows the various sections and widgets of the dashboard that we will cover here.

1. Getting Started

The Getting Started section is here to help. You can choose from 3 different options to get started:

  • Add existing data: When selecting this option, you can then choose to Upload data from your computer or to Add storage bucket

  • Collect new data: When selecting this option, the getting started guide will ask you to either Scan QR code to connect to your phone, Connect to your computer, or to Connect your device or developer board. Make sure that your device or development board is flashed with the Edge Impulse official firmware.

  • Upload your model: This option will change the default workflow. See to learn more about how to import your existing model to Edge Impulse studio.

2. Project Visibility

You have two options for your project's visibility:

Private

Private projects are only visible to you in your developer profile or, if you are on the Enterprise plan, within your organization. There are limits to the number of private projects a developer or enterprise may have. Please refer to our page on our website for details.

Public

When a project is public, all of your data, block configurations, intermediate results, and final models will be shared with the world. Your project will be publicly accessible and can be cloned with a single click with the provided URL. There are no limits to the amount of public projects a developer or enterprise may have.

List your project in the public project repository

If you want to reference your project in the Edge Impulse , you can go to the page, create a version and check the Publish this version under the Apache 2.0 license.

This is a great way to share your work with the community.

3. Run this model

When you have a trained model available in your project, this card will appear to let you test your model with your phone or your computer. This is particularly useful to validate the behaviour of your model before integrating it into your targeted embedded firmware.

4. Collaboration

To add a collaborator, go to your project's dashboard and find the "Collaborators" widget. Click the '+' icon and type the username or e-mail address of the other user. The user will be invited to create an Edge Impulse account if it doesn't exist.

The user will be automatically added to the project and will get an email notification inviting them to start contributing to your project. To remove a user, simply click on the three dots beside the user then tap ‘Delete’ and they will be automatically removed.

There are different limits for how many collaborators can be added to project depending on its visibility (public or private) and the pricing plan it is associated with (Developer plan or Enterprise plan). Please refer to our page on our website for details.

5. Showcasing your public projects with Markdown READMEs

The project README enables you to explain the details of your project in a short way. Using this feature, you can add visualizations such as images, GIFs, code snippets, and text to your project in order to bring your colleagues and project viewers up to speed with the important details of your project. In your README you might want to add things like:

  • What the project does

  • Why the project is useful

  • Motivations of the project

  • How to get started with the project

  • What sensors and target deployment devices you used

  • How you plan to improve your project

  • Where users can get help with your project

To create your first README, navigate to the "about this project" widget and click "add README"

For more README inspiration, check out the public Edge Impulse project tutorials below:

  • .

  • .

  • .

  • .

6. Project info

The project info widget shows the project's specifications such as the project ID, labeling method, and latency calculations for your target device.

  • The project ID is a unique numerical value that identifies your project. Whenever you have any issue with your project on the studio, you can always share your project ID on the for assistance from edge Impulse staff.

  • On the labeling method dropdown, you need to specify the type of labeling your dataset and model expect. This can be either one label per data item or bounding boxes. Bounding boxes only work for object detection tasks in the studio. Note that if you interchange the labeling methods, learning blocks will appear to be hidden when building your impulse.

  • One of the amazing Edge Impulse superpowers is the latency calculation component. This is an approximate time in milliseconds that the trained model and DSP operations are going to take during inference based on the selected target device. This hardware in the loop approach ensures that the target deployment device compute resources are not underutilized or over-utilized. It also saves developers' time associated with numerous inference iterations back and forth the studio in search of optimum models.

7. Block Outputs

In the Block Output section, you can download the results of the DSP and ML operations of your impulse.

The downloadable assets include the extracted features, Tensorflow SavedModel, and both quantized and unquantized LiteRT (previously Tensorflow Lite) models. This is particularly helpful when you want to perform other operations to the output blocks outside the Edge Impulse studio. For example, if you need a TensorflowJS model, you will just need to download the TensorFlow saved model from the dashboard and convert it to TensorFlowJS model format to be served on a browser.

8. Performance Settings

This section consists of editable parameters that directly affect the performance of the studio when building your impulse. Depending on the selected or available settings, your jobs can either be fast or slow.

The use of GPU for training and Parallel DSP jobs is currently an internal experimental feature that will be soon released.

9. Administrative Zone

To bring even more flexibility to projects, the administrative zone gives developers the power to enable other additional features that are not found in edge impulse projects by default. Most of these features are usually advanced features intended for organizations or sometimes experimental features.

To activate these features you just need to check the boxes against the specific features you want to use and click save experiments.

10. Danger Zone

The danger zone widget consists of irrevocable actions that let you to:

  • Perform train/test split. This action re-balances your dataset by splitting all your data automatically between the training and testing set and resets the categories for all data.

  • Launch the getting started wizard. This will remove all data, and clear out your impulse.

  • Transfer ownership. This action is available for users who have one or more organizations linked with their accounts. With it, you can start working on a project with your user profile and then transfer the ownership to your organization.

  • Delete your project. This action removes all devices, data, and impulses from your project.

  • Delete all data in this project.

#include <HTTPClient.h>

void loop() {
  ArduinoIoTCloud.update();

  // Check for updates at regular intervals
  static unsigned long lastCheck = 0;
  if (millis() - lastCheck > CHECK_INTERVAL) {
    checkForUpdates();
    lastCheck = millis();
  }

  // Additional code to handle other tasks
}

void checkForUpdates() {
  // Logic to check for last modification date
  HTTPClient http;
  String apiURL = "https://studio.edgeimpulse.com/v1/api/your-project-id/last-modification-date";
  http.begin(apiURL);
  http.addHeader("x-api-key", "your-edge-impulse-api-key");

  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    // Parse payload to check for a new model version
    // If a new version is available, trigger the OTA event
    onOTAEvent();
  }
  http.end();
}
void connectToWiFi() {
  Serial.print("Attempting to connect to WPA SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(5000);
    Serial.print(".");
    attempts++;
    if (attempts >= MAX_ATTEMPTS) {
      Serial.println("Failed to connect to WiFi. Please check your credentials.");
      return;
    }
  }

  Serial.println("Connected to WiFi");
}
void onOTAEvent() {
  // This function is called when a new OTA firmware is available
  Serial.println("New OTA firmware available. Starting update...");

  // Implement the logic to download and update the firmware
  HTTPClient http;
  String firmwareURL = "https://studio.edgeimpulse.com/v1/api/your-project-id/deployment/download";
  http.begin(firmwareURL);
  http.addHeader("x-api-key", "your-edge-impulse-api-key");

  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    // Here, implement the logic to apply the OTA update.
    // This might involve writing the new firmware to a specific location in flash memory
    // and then rebooting the device to apply the update.
    applyOTAUpdate(http.getStream());
  } else {
    Serial.println("Failed to download the update.");
  }
  http.end();
}

void applyOTAUpdate(WiFiClient &updateStream) {
  // Logic to apply the OTA update
  // Depending on your board and setup, this might involve writing to flash memory
  // and then rebooting the device.
}
Lifecycle Management with Edge Impulse
here
Sign up here
data acquisition
impulse design
git clone [email protected]:edgeimpulse/example-custom-deployment-block-gradcam.git
edge-impulse-blocks init
Edge Impulse Blocks v1.30.3
? In which organization do you want to create this block? Developer Relations
Attaching block to organization 'Developer Relations'
? Choose an option Create a new block
? Do you have an integration URL (shown after deployment, e.g. your docs page), leave empty to skip https://github
.com/edgeimpulse/example-custom-deployment-block-gradcam

Your new block has been created in '/Users/luisomoreau/workspace/ei/custom-deployment-gradcam'.
When you have finished building your block, run 'edge-impulse-blocks push' to update the block in Edge Impulse.
edge-impulse-block push
docker build -t gradcam-deployment-block .
docker run --rm -it gradcam-deployment-block --api-key ei_123456789abcdef
Google Colab
Github
Google Colab
Github
AI Explainability with Grad-CAM: Visualizing Neural Network Decisions
Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization
Google Colab
adjust the parameters
Example of a “car vs not car” model, highlighting the learned “features” of a car
Custom block overview
Custom block edit
BYOM
plans and pricing
public project repository
versioning
plans and pricing
Keyword spotting
Continuous motion recognition
Image classification
Sound recognition
forum
The Edge Impulse Studio dashboard.
A public project in the Edge Impulse Studio.
Collaborators
The Edge Impulse Studio project README.
Edge Impulse Studio project information.
Download Edge Impulse Studio project block output.
The Edge Impulse Studio project performance settings.
Edge Impulse Studio project danger zone settings.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

plans and pricing
Enterprise trial
forum
$ edge-impulse-run-impulse --continuous
Edge Impulse impulse runner v1.9.1
[SER] Connecting to /dev/tty.usbmodem0004401658161
Predictions (DSP: 143 ms., Classification: 13 ms., Anomaly: 0 ms.):
    helloworld: 	0.98828
    noise: 	        0.0039
    unknown: 	    0.00781
    for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        result->classification[ix].value =
            run_moving_average_filter(&classifier_maf[ix], result->classification[ix].value);
    }
Tutorial: responding to your voice
Sound recognition
supported device
Ingestion service
Mobile phone
Keyword Spotting Dataset
spectrogram
fully supported development boards
easy to implement
Using your mobile phone
Running your impulse locally
Continuous motion recognition
Image classification
custom processing blocks
Devices tab with the device connected to the remote management interface.
Recording your keyword from the Studio.
10 seconds of 'Hello world' data
'Split sample' automatically cuts out the interesting parts of an audio file.
Importing the noise and unknown data into your project
Training data, showing an even split between the three classes
Testing data, also showing an even split between the three classes
An impulse to classify human speech
MFCC block looking at an audio file
MFCC Spectrogram for 'Hello world'
MFCC Spectrogram for 'On'
On-device performance is updated automatically when you change parameters
The feature explorer showing 'Hello world' (in blue), vs. 'unknown' (in green) data. This separates well, so the dataset looks to be in good condition.
Neural network configuration
A trained Machine Learning model that can distinguish keywords!
Model testing showing 88.62% accuracy on our test set.
Inspecting a misclassified label. Here the audio actually only says 'Hello', and thus this sample was mislabeled.
! pip install openai
# Imports
import openai
import os
import requests

# Notebook Imports
from IPython.display import Image
from IPython.display import display
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# You can set your API key and org as environment variables in your system like this:
# os.environ['OPENAI_API_KEY'] = 'api string'

# Set up OpenAI API key and organization
openai.api_key = os.getenv("OPENAI_API_KEY")
image_prompt = "A webcam image of a human 1m from the camera sitting at a desk showing that they are wearing gloves with their hands up to the camera."
# image_prompt = "A webcam image of a person 1m from the camera sitting at a desk with their bare hands up to the camera."
# image_prompt = "A webcam image of a human 1m from the camera sitting at a desk showing that they are wearing wool gloves with their hands up to the camera."

response = openai.Image.create(
    prompt=image_prompt,
    n=1,
    size="256x256",
)
Image(url=response["data"][0]["url"])
response2 = openai.Image.create_variation(
  image=requests.get(response['data'][0]['url']).content,
  n=3,
  size="256x256"
)
imgs = []
for img in response2['data']:
  imgs.append(Image(url=img['url']))

display(*imgs)
labels = [{"prompt": "A webcam image of a human 1m from the camera sitting at a desk showing that they are wearing wool gloves with their hands up to the camera.",
          "label": "gloves"},
          {"prompt": "A webcam image of a person 1m from the camera sitting at a desk with their bare hands up to the camera.",
          "label": "no-gloves"}
        ]
output_folder = "output"
base_images_number = 10
variation_per_image = 3
# Check if output directory for noisey files exists and create it if it doesn't
if not os.path.exists(output_folder):
    os.makedirs(output_folder)

for option in labels:
    for i in range(base_images_number):
        response = openai.Image.create(
            prompt=option["prompt"],
            n=1,
            size="256x256",
        )
        try:
            img = response["data"][0]["url"]
            with open(f'{output_folder}/{option["label"]}.{img.split("/")[-1]}.png', 'wb+') as f:
                f.write(requests.get(img).content)
            response2 = openai.Image.create_variation(
                image=requests.get(img).content,
                n=variation_per_image,
                size="256x256"
            )
        except Exception as e:
            print(e)
        for img in response2['data']:
            try:
                with open(f'{output_folder}/{option["label"]}.{img["url"].split("/")[-1]}.png', 'wb') as f:
                    f.write(requests.get(img["url"]).content)
            except Exception as e:
                print(e)
import os


# Define the folder containing the images
folder_path = './output'

# Get a list of all the image files in the folder
image_files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f)) and f.endswith('.png')]

# Set up the plot
fig, axs = plt.subplots(nrows=20, ncols=20, figsize=(10, 10))

# Loop through each image and plot it in a grid cell
for i in range(20):
    for j in range(20):
        img = mpimg.imread(os.path.join(folder_path, image_files[i*10+j]))
        axs[i,j].imshow(img)
        axs[i,j].axis('off')

# Make the plot look clean
fig.subplots_adjust(hspace=0, wspace=0)
plt.tight_layout()
plt.show()
! cd output
! edge-impulse-uploader .
Transformation Block
https://github.com/edgeimpulse/example-transform-Dall-E-images
Images generated from the script
# If you have not done so already, install the following dependencies
!python -m pip install tensorflow==2.12.0 edgeimpulse
from tensorflow import keras
import edgeimpulse as ei
# Settings
ei.API_KEY = "ei_dae2..." # Change this to your Edge Impulse API key
labels = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
num_classes = len(labels)
deploy_filename = "my_model_cpp.zip"
# Load MNIST data
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = keras.utils.normalize(x_train, axis=1)
x_test = keras.utils.normalize(x_test, axis=1)
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
input_shape = x_train[0].shape
# Build the model 
model = keras.Sequential([
    keras.layers.Flatten(),
    keras.layers.Dense(32, activation='relu', input_shape=input_shape),
    keras.layers.Dense(num_classes, activation='softmax')
])

# Compile the model
model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
# Train the model
model.fit(x_train, 
          y_train, 
          epochs=5)
# Evaluate model on test set
score = model.evaluate(x_test, y_test, verbose=0)
print(f"Test loss: {score[0]}")
print(f"Test accuracy: {score[1]}")
# List the available profile target devices
ei.model.list_profile_devices()
['alif-he',
 'alif-hp',
 'arduino-nano-33-ble',
 'arduino-nicla-vision',
 'portenta-h7',
 'brainchip-akd1000',
 'cortex-m4f-80mhz',
 'cortex-m7-216mhz',
 ...
 'ti-tda4vm']
# Estimate the RAM, ROM, and inference time for our model on the target hardware family
try:
    profile = ei.model.profile(model=model,
                               device='cortex-m4f-80mhz')
    print(profile.summary())
except Exception as e:
    print(f"Could not profile: {e}")
# List the available profile target devices
ei.model.list_deployment_targets()
['zip',
 'arduino',
 'tinkergen',
 'cubemx',
 'wasm',
 ...
 'runner-linux-aarch64-tda4vm']
# Set model information, such as your list of labels
model_output_type = ei.model.output_type.Classification(labels=labels)

# Set model input type
model_input_type = ei.model.input_type.OtherInput()

# Create C++ library with trained model
deploy_bytes = None
try:
    
    deploy_bytes = ei.model.deploy(model=model,
                                   model_output_type=model_output_type,
                                   model_input_type=model_input_type,
                                   deploy_target='zip')
except Exception as e:
    print(f"Could not deploy: {e}")
    
# Write the downloaded raw bytes to a file
if deploy_bytes:
    with open(deploy_filename, 'wb') as f:
        f.write(deploy_bytes.getvalue())
TensorFlow
Keras
Edge Impulse Python SDK Overview
edgeimpulse.com
MNIST
OtherInput
this page
here
Copy API key from Edge Impulse project
plans and pricing
Enterprise trial
forum
Works with Edge Impulse
Open in Google Colab

Data

Since the creation of Edge Impulse, we have been helping customers to deal with complex data pipelines, complex data transformation methods and complex clinical validation studies.

In most cases, before even thinking about machine learning algorithms, researchers need to build quality datasets from real-world data. These data come from various devices (prototype devices being developed vs clinical/industrial-grade reference devices), have different formats (excel sheets, images, csv, json, etc...), and be stored in various places (researchers' computers, Dropbox folders, Google Drive, S3 buckets, etc...).

Dealing with such complex data infrastructure is time-consuming and expensive to develop and maintain. With the organizational data, we want to give you tools to centralize, validate and transform datasets so they can be easily imported into your projects to train your machine learning models.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Health reference design

We have built a health reference design that describes an end-to-end ML workflow for building a wearable health product using Edge Impulse.

In this reference resign, we want to help you understand how to create a full clinical data pipeline by using a public dataset from the PPG-DaLiA repository. This tutorial will guide you through the following steps:

  • Synchronizing clinical data with a bucket

  • Validating clinical data

  • Querying clinical data

  • Transforming clinical data

  • Buildling data pipelines

Buckets

Before we get started, you must link your organization with one or more storage buckets. Further details about how to integrate with cloud storage providers can be found in the Cloud data storage document.

Datasets

Two types of dataset structures can be used - Generic datasets (default) and Clinical datasets.

There is no required format for data files. You can upload data in any format, whether it's CSV, Parquet, or a proprietary data format.

However, to import data items to an Edge Impulse project, you will need to use the right format as our studio ingestion API only supports these formats:

  • JPG, PNG images

  • MP4, AVI video files

  • WAV audio files

  • CBOR/JSON files in the Edge Impulse data acquisition format

  • CSV files

Tip: You can use transformation blocks to convert your data

Datasets overview

The default dataset structure is a file-based one, no matter the directory structure:

For example:

images/
├── testing/
│   ├── 1.jpg
│   ├── 2.jpg
│   ├── 3.jpg
│   ...
│   └── 200.jpg
└── training/
    ├── 1.jpg
    ├── 2.jpg
    ├── 3.jpg
    ...
    └── 800.jpg

or:

keywords/
├── french-accent/
│   ├── hello.wav
│   ├── yes.wav
│   ├── no.wav
├── greek-accent/
│   ├── hello.wav
│   ├── yes.wav
│   ├── no.wav
└── unlabeled/
    ├── 1.wav
    ├── 2.wav
    ├── 3.wav
    ...
    └── 20.wav

Note that you will be able to associate the labels of your data items from the file name or the directory name when importing your data in a project.

The clinical datasets structure in Edge Impulse has three layers:

  1. The dataset, a larger set of data items, grouped together.

  2. Data item, an item with metadata and files attached.

  3. Data file, the actual files.

See the health reference design tutorial for a deeper explanation.

Create a new dataset

Once you successfully linked your storage bucket to your organization, head to the Datasets tab and click on + Add new dataset:

Add new dataset

Fill out the following form:

Add dataset

Click on Create dataset

Data

With your datasets imported, you can now navigate into your dataset, create folders, query your dataset, add data items and import your data to an Edge Impulse project.

Default view

The default view lets you navigate in your bucket following the directory structure. You can easily add data using the "+ New folder" button. To add new data, use the right panel - drag and drop your files and folders and it will automatically upload them to your bucket.

Data items overview

Clinical view

The clinical view is slightly different, see synchronizing clinical data with a bucket for more information. This view lets you easily query your clinical dataset but to import data, you will need to set up an upload portal or upload them directly to your bucket.

Tip: You can add two distinct datasets in Edge Impulse that point to the same bucket path, one generic and one clinical. This way you can leverage both the easy upload and the ability to query your datasets.

Clinical dataset overview

Adding data to your project

Go to the Actions...->Import data into a project, select the project you wish to import to and click Next, Configure how to label this data:

Uploading Files

This will import the data into the project and optionally create a new label for each file in the dataset. This labeling step helps you keep track of different classes or categories within your data.

After importing the data into the project, in the Next, post-sync actions step, you can configure a data pipeline to automatically retrieve and trigger actions in your project:

Label your files

Previewing Data

We also have added a data preview feature, allowing you to visualize certain types of data directly within the organization data tab.

Supported data types include tables (CSV/Parquet), images, PDFs, audio files (WAV/MP3), and text files (TXT/JSON). This feature gives you a quick overview of your data and helps ensure its integrity and correctness.

Data items overview - CSV/Parquet type
Data items overview - image type

Recap

If you need to get data into your organization, you can now do this in a few simple steps. To go further and use advanced features, query your datasets or transform your dataset, please have a look at the health reference design tutorial 🚀

Any questions, or interested in the enterprise version of Edge Impulse? Contact us for more information.

Custom AI labeling blocks

Custom AI labeling blocks are a way to extend the AI labeling feature within Edge Impulse. If none of the blocks created by Edge Impulse that are built into the platform fit your needs, you can modify them or develop from scratch to create a custom AI labeling block. This allows you to integrate your own models or prompts for unique project requirements.

Ready to dive in and start building? Jump to the examples!

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Block structure

AI labeling blocks are an extension of transformation blocks operating in standalone mode and, as such, follow the same structure without being able to pass a directory or file directly to your scripts. Please see the custom blocks overview page for more details.

Custom AI labeling block structure

Block interface

The sections below define the required and optional inputs and the expected outputs for custom AI labeling blocks.

Inputs

AI labeling blocks have access to environment variables, command line arguments, and mounted storage buckets.

Environment variables

The following environment variables are accessible inside of AI labeling blocks. Environment variable values are always stored as strings.

Variable
Passed
Description

EI_API_ENDPOINT

Always

The API base URL: https://studio.edgeimpulse.com/v1

EI_API_KEY

Always

The organization API key with member privileges: ei_2f7f54...

EI_INGESTION_HOST

Always

The host for the ingestion API: edgeimpulse.com

EI_ORGANIZATION_ID

Always

The ID of the organization that the block belongs to: 123456

EI_PROJECT_ID

Always

The ID of the project: 123456

EI_PROJECT_API_KEY

Always

The project API key: ei_2a1b0e...

You can also define your own environment variables to pass to your custom block using the requiredEnvVariables key in the metadata section of the parameters.json file. You will then be prompted for the associated values for these keys when pushing the block to Edge Impulse using the CLI. Alternatively, these values can be added (or changed) by editing the block in Studio.

Command line arguments

The parameter items defined in your parameters.json file will be passed as command line arguments to the script you defined in your Dockerfile as the ENTRYPOINT for the Docker image. Please refer to the parameters.json documentation for further details about creating this file, parameter options available, and examples.

In addition to the items defined by you, specific arguments will be automatically passed to your AI labeling block.

AI labeling blocks are an extension of transformation blocks operating in standalone mode, the arguments that are automatically passed to transformation blocks in this mode are also automatically passed to AI labeling blocks. Please refer to the custom transformation blocks documentation for further details on those parameters.

Along with the transformation block arguments, the following AI labeling specific arguments are passed as well.

Argument
Passed
Description

--data-ids-file <file>

Always

Provides the file path for id.json as a string. The ids.json file lists the data sample IDs to operate on as integers. See .

--propose-actions <job-id>

Conditional

Only passed when the user wants to preview label changes. If passed, label changes should be staged and not directly applied. Provides the job ID as an integer. See .

Additional CLI arguments can also be specified using the CLI arguments field when editing the block in Studio.

Mounted storage buckets

One or more cloud data storage buckets can be mounted inside of your block. If storage buckets exist in your organization, you will be prompted to mount the bucket(s) when initializing the block with the Edge Impulse CLI. The default mount point will be:

/mnt/s3fs/<bucket-name>

The mount point can be changed by editing the block in Studio after pushing.

Outputs

There are no required outputs from AI labeling blocks. In general, all changes are applied to data using API calls inside the block itself.

Running in preview mode

AI labeling blocks can run in "preview" mode, which is triggered when a user clicks Label preview data within an AI labeling action configuration. When a user is previewing label changes, the changes are staged and not applied directly.

For preview mode, the --propose-actions <job-id> argument is passed into your block. When you see this option, you should not apply changes directly to the data samples (e.g. via raw_data_api.set_sample_bounding_boxes or raw_data_api.set_sample_structured_labels) but rather use the raw_data_api.set_sample_proposed_changes API call.

if args.propose_actions:
    raw_data_api.set_sample_proposed_changes(project_id=project_id, sample_id=sample.id, set_sample_proposed_changes_request={
        'jobId': args.propose_actions,
        'proposedChanges': {
            'structuredLabels': structured_labels,
            'metadata': new_metadata
        }
    })
else:
    raw_data_api.set_sample_structured_labels(
        project_id, sample.id, set_sample_structured_labels_request={
            'structuredLabels': structured_labels
        }
    )
    raw_data_api.set_sample_metadata(project_id=project_id, sample_id=sample.id, set_sample_metadata_request={
        'metadata': new_metadata
    })
if (proposeActionsJobId) {
      await api.rawData.setSampleProposedChanges(project.id, sample.id, {
          jobId: proposeActionsJobId,
          proposedChanges: {
              boundingBoxes: newBbs,
          },
      });
  }
else {
      await api.rawData.setSampleBoundingBoxes(project.id, sample.id, {
          boundingBoxes: newBbs,
      });
}

Initializing the block

Testing the block locally

AI labeling blocks are not supported by the edge-impulse-blocks runner CLI tool

AI labeling blocks are not currently supported by the blocks runner in the Edge Impulse CLI. To test you custom AI labeling block, you will need to build the Docker image and run the container directly. You will need to pass any environment variables or command line arguments required by your script to the container when you run it.

docker build -t custom-ai-labeling-block .
docker run --rm -e EI_PROJECT_API_KEY='ei_...' -e CUSTOM_ENV_VAR='<env-value>' custom-ai-labeling-block --data-ids-file ids.json --custom-param-one foo --custom-param-two bar

Pushing the block to Edge Impulse

Using the block in a project

Examples

Edge Impulse has developed several AI labeling blocks that are built into the platform. The code for these blocks can be found in public repositories under the Edge Impulse GitHub account. The repository names typically follow the convention of ai-labeling-<description>. As such, they can be found by going to the Edge Impulse account and searching the repositories for ai-labeling.

Below are direct links to some examples:

  • Bounding box labeling with OWL-ViT

  • Bounding box re-labeling with GPT-4o

  • Image labeling with GPT-4o

  • Image labeling with pretrained models

  • Audio labeling with Audio Spectrogram Transformer

Troubleshooting

No common issues have been identified thus far. If you encounter an issue, please reach out on the or, if you are on the Enterprise plan, through your support channels.

Additional resources

  • Custom blocks

  • AI labeling

  • edge-impulse-blocks

  • parameters.json

  • ids.json

Custom deployment blocks

Custom deployment blocks are a way to extend the capabilities of Edge Impulse beyond the deployment options built into the platform. If none of the existing blocks created by Edge Impulse fit your needs, you can create custom deployment blocks to build and export your own libraries or firmware binaries for unique project requirements.

Ready to dive in and start building? Jump to the examples!

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Block structure

The deployment block structure is shown below. Please see the custom blocks overview page for more details.

Custom deployment block structure

Block interface

The sections below define the required and optional inputs and the expected outputs for custom deployment blocks.

Inputs

Deployment blocks have access to command line arguments and input files.

Command line arguments

The following arguments will be automatically passed to your custom deployment block.

Argument
Passed
Description

--metadata <file>

Always

Provides the file path for deployment-metadata.json as a string. The deployment-metadata.json file contains details about the impulse being deployed. See .

CLI arguments can also be specified using the cliArguments property in the parameters.json file. Alternatively, these arguments can be added (or changed) by editing the block in Studio.

Files

Your deployment block will be passed an input directory that contains all the information required for a deployment, including: deployment metadata, the Edge Impulse SDK, the trained model (in multiple formats), and all supporting source code to run the impulse.

The input directory path is stored in the input property under the folders property in the deployment-metadata.json file, which can be loaded using the --metadata <file> argument that is passed to the deployment block.

The input directory structure is shown below.

input/
├── deployment-metadata.json
├── edge-impulse-sdk/
├── model-parameters/
├── tflite-model/
├── trained.h5.zip
├── trained.savedmodel.zip
└── trained.tflite

Outputs

The expected output from your custom deployment block is a ZIP file named deploy.zip located in the output directory. This archive is what will be downloaded for the user after your block has finished building.

The output directory path is stored in the output property under the folders property in the deployment-metadata.json file, which can be loaded using the --metadata <file> argument that is passed to the deployment block.

Creating a build directory

The input and output directories listed in the deployment-metadata.json file are located on network storage. Therefore to improve the speed of your deployment block, it is best practice to create a build directory, copy in the required items for your build, then write the output archive to the output directory.

In the example below, the app_dir contained the build instructions and files required to compile a Linux application.

# Define directories.
input_dir = metadata['folders']['input']
output_dir = metadata['folders']['output']
app_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'app')

# Create the build directory.
build_dir = '/tmp/build'
if os.path.exists(build_dir):
    shutil.rmtree(build_dir)
os.makedirs(build_dir)

# Copy in the data from both 'input' and 'app' directories.
os.system('cp -r ' + input_dir + '/* ' + build_dir)
os.system('cp -r ' + app_dir + '/* ' + build_dir)

Mounting learning block data

If your custom deployment block requires access to the data used to train your model, you can mount the learning block by setting the mountLearnBlock property to true. This will mount all files for the learning block, including the training data, under a /data directory within your block.

The training data is already split into train and test (validation) sets. Please refer to the Data section under Inputs in the custom learning block documentation for additional details.

Accessing the internet

Deployment blocks do not have access to the internet by default. If you need to access information outside of your block, such as project items through the Edge Impulse API, you will need to set the privileged property to true.

This will enable internet access and pass in the project API key in the deployment-metadata.json file (if a project development API key is set) that can be used to authenticate with the Edge Impulse API. Note that if you also require the project ID, this can be retrieved using the list active projects API endpoint.

Showing optimization options

Setting the showOptimizations property to true will present the user with additional optimization options on the Deployment page in Studio.

Firstly, if the supportsEonCompiler property is set to true (see below), the user will be presented with a dropdown to select between building the deployment using the EON Compiler or standard TFLite file inputs.

Secondly, the user will be presented with quantization options, if applicable. If the user selects the quantized model option, the trained.tflite file will be the int8 version of the model; otherwise it will be the float32 version.

Using the EON Compiler

If the supportsEonCompiler property is set to true, the inputs for the deployment block will be the EON Compiler version of the files; otherwise the inputs will be the TFLite version of the files.

However, if the showOptimizations property is set to true (see above), the user will have the option on the Deployment page in Studio to select between the EON Compiler or standard TFLite file inputs.

Setting an image for the block

The default image that will appear for your block in the dropdown in Studio on the Deployment page is the Edge Impulse logo. If you would like to change this, you can do so by editing the block after it has been pushed to Studio.

Initializing the block

Testing the block locally

Testing locally does not mount the learning block

If your custom deployment block requires access to the learning block files after it has been mounted, testing locally will not work as the methods to download data described below do not include the learning block data.

To speed up your development process, you can test your custom deployment block locally. There are two ways to achieve this. You will need to have Docker installed on your machine for either approach.

With blocks runner

For the first method, you can use the CLI edge-impulse-blocks runner tool. See Block runner for additional details. The runner does not expect any command line arguments for deployment blocks. However, if your deployment block requires arguments, you can pass them as a single string using the --extra-args <args> argument.

 edge-impulse-blocks runner --extra-args "--custom-param-one foo --custom-param-two bar"

The first time you enter the above command, you will be asked some questions to configure the runner. Follow the prompts to complete this. If you would like to change the configuration in future, you can execute the runner command with the --clean flag.

Using the above approach will create an ei-block-data directory within your custom block directory. It will contain several subdirectories.

Directory
Description

download/

Download directory for the archive of required input files for the deployment block.

<project-id>/input/

The input files archive will be automatically extracted to this location.

<project-id>/output/

Where the output from your build script is expected to be written.

With Docker

For the second method, you can use the CLI block runner or Studio to download the required data from your project, then build the Docker image and run the container directly.

You can download the data by calling the block runner with the --download-data <dir> argument. The directory specifies the location where the downloaded data should be extracted. To make this work properly the directory needs to be named input/. Before extraction, the data archive will first be downloaded to ei-block-data/download/.

edge-impulse-blocks runner --download-data input/

Alternatively, you can go to the Deployment page for your project in Studio and select Custom block as your deployment option. This will allow you to download a ZIP file of the required input files for you deployment block. Extract this archive to a directory called input/ within your custom deployment block directory.

After downloading the required input files for your block, you can then build the Docker image and run the container.

docker build -t custom-deployment-block .
docker run --rm -v $PWD:/home custom-deployment-block --metadata /home/input/deployment-metadata.json

Pushing the block to Edge Impulse

Using the block in a project

Examples

Edge Impulse has developed several examples of custom deployment blocks. The code for these blocks can be found in public repositories under the Edge Impulse GitHub account. Unfortunately, the repository names don't follow a naming convention. However, they can be found by going to the Edge Impulse account and searching the repositories for deploy.

Below are direct links to a some examples:

  • Custom deployment block example

  • Merge multiple impulses

Troubleshooting

No common issues have been identified thus far. If you encounter an issue, please reach out on the or, if you are on the Enterprise plan, through your support channels.

Additional resources

  • Custom blocks

  • Deployment

  • EON Compiler

  • edge-impulse-blocks

  • parameters.json

  • deployment-metadata.json

Custom blocks

Much functionality in Edge Impulse is based on the concept of blocks. There are existing blocks built into the platform to achieve dedicated tasks. If these pre-built blocks do not fit your needs, you can edit existing blocks or develop from scratch to create custom blocks that extend the capabilities of Edge Impulse. These include:

  • Custom AI labeling blocks

  • Custom deployment blocks

  • Custom learning blocks

  • Custom processing blocks

  • Custom synthetic data blocks

  • Custom transformation blocks

The sections below provide an overview of custom blocks. The details for each specific type of block can be found on its own documentation page linked above.

Block structure

A block in Edge Impulse encapsulates a Docker image and provides information to the container when it is run. Different parameters, environment variables, and data will be passed in and different volumes will be mounted depending on the type of block.

The basic structure of a block is shown below. At a minimum, a custom block consists of a directory containing your scripts, a Dockerfile, and a parameters.json file. Block specific structures are shown in their respective documentation.

Custom block structure

Scripts

The Docker container executes the scripts that you have written for your custom block. At Edge Impulse, block scripts are mostly written in Python, Javascript/Typescript, or Bash. However, these scripts can be written in any language that you are comfortable with.

The initial script that you would like to be executed is defined in the Dockerfile as the ENTRYPOINT for the image.

Dockerfile

Do not set the WORKDIR argument to /home or /data

The /home and /data directory paths are used by Edge Impulse. Therefore, if you set the working directory for your container to this path, your files will be overwritten and rendered inaccessible. You will notice in most examples from Edge Impulse, the argument for the WORKDIR instruction is set to /app.

Use the ENTRYPOINT instruction

It is important to set the ENTRYPOINT instruction at the end of your Dockerfile to specify the default executable for the container. This instruction is used to turn a container into a standalone executable and blocks in Edge Impulse have been designed with this in mind.

Do not use the RUN or CMD instructions to set the default executable. The RUN instruction is not meant for this purpose (it's meant for building layers of an image) and the CMD instruction is not what Edge Impulse expects.

The Dockerfile is the instruction set for building the Docker image that will be run as a container in your custom block. The documentation for each type of custom block contains links to GitHub repositories for block examples, which each contain a Dockerfile. Referencing these is a great starting point when developing your own Dockerfile.

In general, the argument you define as the ENTRYPOINT in your Dockerfile will be your custom script. For processing blocks, however, this will be an HTTP server. In this case, you will also need to expose the port for your server using the EXPOSE instruction.

If you want to leverage GPU compute for your custom learning blocks, you will need to make sure to install the CUDA packages. You can refer to the example-custom-ml-keras repository to see an example Dockerfile that installs these packages.

When running in Edge Impulse, processing and learning block containers do not have network access. Make sure you don't download dependencies while running these containers, only when building the images.

Parameters

A parameters.json file is to be included at the root of your custom block directory. This file describes the block itself and identifies the parameter items that will be exposed for configuration in Studio and, in turn, passed to the script you defined in your Dockerfile as the ENTRYPOINT. See parameters.json for more details.

In most cases, the parameter items defined in your parameters.json file are passed to your script as command line arguments. For example, a parameter named custom-param-one with an associated value will be passed to your script as --custom-param-one <value>.

Processing blocks are handled differently. In the case of processing blocks, parameter items are passed as properties in the body of an HTTP request. In this case, a parameter named custom-param-one with an associated value will be passed to the function generating features in your script as an argument named custom_param_one. Notice the dashes have been converted to underscores.

One additional note in regards to how parameter items are passed is that items of the type secret will be passed as environment variables instead of command line arguments.

Parameter types are enforced and validation is performed automatically when values are being entered in Studio.

Developing a custom block

The first steps to developing a custom block are to write your scripts and Dockerfile. Once those are completed, you can initialize the block, test it locally, and push it to Edge Impulse using the edge-impulse-blocks tool in the Edge Impulse CLI.

Initializing the block

From within your custom block directory, run the edge-impulse-blocks init command and follow the prompts to initialize your block. This will do two things:

  1. Create an .ei-block-config file that associates the block with your organization

  2. Create a parameters.json file (if one does not already exist in your custom block directory)

After the parameters.json file is created, you will want to take a look at it and make modifications as necessary. The CLI creates a basic file for you and you may want to include additional metadata and parameter items.

Testing the block locally

There are several levels of testing locally that you can do while developing your custom block:

  1. Calling your script directly, passing it any required environment variables and arguments

  2. Building the Docker image and running the container directly, passing it any required environment variables and arguments

  3. Using the blocks runner tool in the Edge Impulse CLI to test the complete block

To use the blocks runner tool in the Edge Impulse CLI, run the edge-impulse-blocks runner command from within your custom block directory and follow the prompts to test your block locally. See Block runner.

Refer to the documentation for your type of custom block for additional details about testing locally.

Pushing the block to Edge Impulse

Custom learning blocks can be pushed to a developer profile

Unlike all other types of custom blocks, a custom learning block can be pushed to a developer profile (non-Enterprise plan account).

After initializing and testing your custom block, you can push it to Edge Impulse to make it available for use by everyone in your organization.

From within your custom block directory, run the edge-impulse-blocks push command and follow the prompts to push your block to Edge Impulse.

Once pushed successfully, your block will appear in your organization or, if it is a custom learning block and you are not on the Enterprise plan, in your developer profile. See Editing a custom block in Studio for images showing each block type after being pushed to Edge Impulse.

Resetting the block configuration

If at some point you need to change configuration settings for your block that aren't being shown when you run the edge-impulse-blocks commands, say to download data from a different project with the runner, you can execute any of the respective commands with the --clean flag.

Importing existing Docker images

If you have previously created a Docker image for a custom block and are hosting it on Docker Hub, you can create a custom block that uses this image.

To do so, go to your organization and select the item in the left sidebar menu for the type of custom block you would like to create. On that custom block page, select the + Add new <block-type> block button (or select an existing block to edit). In the modal that pops up, configure your block as desired and in the Docker container field enter the details for your image in the username/image:tag format.

Editing a custom block in Studio

After successfully pushing your custom block to Edge Impulse you can edit it from within Studio.

Click on AI labeling under Custom blocks. You should find your custom AI labeling block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit AI labeling block.

Custom AI labeling block pushed to an organization

Click on Deployment under Custom blocks. You should find your custom deployment block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit block.

Custom deployment block pushed to an organization

Organization (Enterprise plan)

Click on Machine learning under Custom blocks. You should find your custom learning block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit block.

Custom learning block pushed to an organization

Developer profile (all other plans)

Click on your photo in the top right corner of your developer profile, select Custom ML blocks. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit block.

Custom learning block pushed to a developer profile

Click on DSP under Custom blocks. You should find your custom processing block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit DSP block.

Custom processing block pushed to an organization

Click on Synthetic data under Custom blocks. You should find your custom synthetic data block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit synthetic data block.

Custom synthetic data block pushed to an organization

Click on Transformation under Custom blocks. You should find your custom transformation block listed here. To view the configuration settings for your block and edit them, you can click on the three dots and select Edit transformation block.

Custom transformation block pushed to an organization

Setting compute requests and limits

Most blocks have the option to set the compute requests and limits (number of CPUs and memory) and some have the option to set the maximum running time duration. These items cannot, however, be configured from the parameters.json file; they must be configured when editing the block after it has been pushed to Edge Impulse.

Additional resources

  • edge-impulse-blocks

  • parameters.json

with Arduino IDE (for ESP32)

Introduction

This page is part of the tutorial series. If you haven't read the introduction yet, we recommend you to do so .

In this tutorial, we'll guide you through deploying updated impulses over-the-air (OTA) to Arduino using Edge Impulse. We'll build on Arduino firmware update workflow, incorporating Edge Impulse's API to check for updates and download the latest build.

Let's get started!

Prerequisites:

  • Edge Impulse Account: If you haven't got one, .

  • Trained Impulse: If you're new, follow our and guides.

Key Features of Arduino OTA Updates:

Arduino OTA Update

OTA Code

Here’s the complete C code for implementing OTA updates with Edge Impulse on ESP-EYE (ESP32).

Prerequisites

  • A trained impulse in Edge Impulse Studio

  • Installation of required software as detailed in the tutorial

Preparation

Begin by setting up your device for OTA updates following Espressif's OTA firmware update workflow. Use the built binary from the C++ example and modify it to incorporate OTA functionality.

Steps to Deploy Impulse to ESP32

1. Copy the ESP OTA example and configure your wifi settings

Clone the example repository and adjust it according to your project and connectivity settings.

2. Server Side OTA

Modify the ESP OTA example server to check for updates to your project

2. Modify the ESP OTA example to check for updates to your project

Modify the Edge Impulse C++ example for ESP32 to check for updates to your project and download the latest build.

We will need to add a few libraries to the project to facilitate the OTA update process. These are taken from the ESP32 OTA example and are already included in the example project.

Components

1. Non-Volatile Storage (NVS)

NVS is utilized to persistently store data like configuration settings, WiFi credentials, or firmware update times, ensuring retention across reboots.

2. HTTP Client

This library facilitates HTTP requests to the server for checking and retrieving new firmware updates.

3. OTA Operations

These headers aid in executing OTA operations, including writing new firmware to the flash and switching boot partitions.

4. FreeRTOS Task

FreeRTOS ensures OTA updates are conducted in a separate task, preventing blockage of other tasks and maintaining system operations during the update.

3. Updating the Device

Compare the model's timestamp or hash with the stored version. If it's different or newer, call the download_model() function.

4. Monitoring and Repeating the Process

Monitor the device to ensure the new impulse performs as expected and repeat the update process as needed.

Conclusion

This tutorial provides a comprehensive guide for implementing OTA updates on Espressif ESP-EYE (ESP32) with Edge Impulse. Follow each step meticulously, ensuring all prerequisites and preparation steps are completed before proceeding to the deployment phase. Happy coding!

Note: Adjust the code snippets and steps to suit your specific requirements and always ensure to test thoroughly before deploying updates to live environments.

Customize the EON Tuner

The EON Tuner is Edge Impulse's AutoML (automated machine learning) tool to help you find and select the best embedded machine learning model for your application within the constraints of your target device.

This notebook will show you how to configure and run the EON Tuner programmatically using the !

1. Setup

This section will set up your environment and API credentials so that you can start making calls to the from this notebook. Run this block only once per runtime session, or every time you:

  • Open the notebook on your browser or IDE to start working on it, or

  • restart the runtime, or

  • change the project you are working on

API documentation is available at https://docs.edgeimpulse.com/reference/edge-impulse-api

1.1 Update your PROJECT_ID

You will need to enter the correct PROJECT_ID for the project you want to work with, in the code in section 1.3 below. The project ID can be obtained from your Edge Impulse Project's Dashboard under the Project Info section.

1.2 Obtain your API Key

The block below will prompt you for your project's API Key. You can obtain this key from your Project's Dashboard, by selecting the Keys tab from the top navigation bar.

1.3 Run the setup block

Run the block below and enter your API key when prompted. Then continue to the next section.

2. Customization

You can use the code in section 2.2 below to programmatically update the configuration of the EON Tuner.

2.1 Enable advanced EON Tuner mode (optional)

In basic mode (the default) you will be able to modify the datasetCategory, targetLatency and targetDevice. For additional control, ask your User Success or Solutions Engineer to enable the EON Tuner advanced mode for you.

2.2 Update the EON Tuner configuration

2.3 Start the EON Tuner

Run the cell below to start spinning up EON Tuner optimization jobs. If your project is already running an EON Tuner optimization, go instead to section 2.4 to track the job's progress.

2.4 Track the EON Tuner optimization progress

Run the cell below to track the progress of your EON Tuner job. You can safely stop and restart the cell at any time since this will not affect the running EON Tuner jobs.

2.5 Get the EON Tuner optimization results

Use the cell below to retrieve the EON Tuner optimization results and save them to the trials variable.

with Blues Wireless

Introduction

This page is part of the tutorial series. If you haven't read the introduction yet, we recommend you to do so .

Overview

OTA DFU with Notehub Blues Wireless have created a unique way to update firmware on your Notecard device is to perform an over-the-air DFU with Notehub.io. For instructions on how to update your Notecard firmware with Notehub.io, please visit the .

Key Features of Blues Wireless OTA Updates:

  • NOFU: Notehub Outbound Firmware Update allows you to update the firmware on the Swan from Notehub.

Closing the Loop:

  • Blues Wireless has created their own Edge Impulse ingestion API integration which allows you to collect data from the notecard.

Prerequisites:

  • Edge Impulse Account: If you haven't got one, .

  • Trained Impulse: If you're new, follow one of our

  • Blues Wireless Account: If you haven't got one, .

  • Blues Wireless Notecard: If you haven't got one, .

Webinar Recap: Optimized MLOps with Edge Impulse, Blues, and Zephyr:

Watch the full webinar for insights into MLOps and its integration with Edge Impulse, Blues, and Zephyr.


Webinar Overview:

Implementing MLOps Workflow:

  • Data collection using the Notecard.

  • Sending data to Edge Impulse for model training.

  • Deploying the trained model using Zephyr.

  • Monitoring and continuous improvement.


Establishing a Robust MLOps Workflow for TinyML

Author: TJ VanToll link: https://dev.blues.io/blog/robust-ml-ops-workflow/

Introduction

In TinyML projects, the effectiveness of a machine learning model greatly depends on the quality of its training data. Gathering comprehensive training data is particularly challenging in TinyML due to the limited power and connectivity of tiny devices. At Blues, in collaboration with Edge Impulse and Zephyr, we have developed a workflow that facilitates MLOps (Machine Learning Operations) for tiny devices.

This tutorial outlines a method to implement a robust MLOps process, focusing on how to collect ML data from remote devices and update ML models on these devices in the field. The result is a workflow that seamlessly integrates data collection and model updating.

see the for the in depth guide.

Collecting ML Data

Let's assume we're managing devices monitoring industrial equipment with accelerometers to detect abnormalities. For a successful MLOps workflow, these devices need to send accelerometer data to the cloud for training an updated model. Here, we use the Blues Notecard for this purpose.

The Blues Notecard

The Notecard is a system on module designed for easy connectivity in embedded projects. Its features include:

  • 500MB of cellular connectivity

  • Global cellular support over LTE-M, NB-IoT, or Cat-1

  • Secure device-to-cloud communication

  • Low-power hardware and firmware

  • Easy embedding options

To set up the hardware, you'll need a Blues Starter Kit, which includes a development board (Notecarrier) and a Swan microcontroller. Additionally, an LIS3DH accelerometer connected to the Notecarrier via a Qwiic cable is required.

The Firmware

The project’s firmware gathers accelerometer readings and sends them to the cloud via the Notecard. We use Zephyr for firmware implementation, as it offers both low-level access and development conveniences. Zephyr firmware is compatible with STM32-based boards like the Swan, and the Notecard has a Zephyr SDK.

The firmware performs the following functions:

  • Establishes a connection between the Notecard and Notehub

  • Gathers data from the accelerometer

  • Sends the data to Notehub

For example, the firmware takes accelerometer readings at intervals set by the Edge Impulse SDK and sends this data as a binary stream to Notehub.

Sending the Data

Notecard's new firmware (v5.3.1) introduces 'card.binary' APIs, enabling fast data transfer for large data packets. The data is then sent to Notehub using the 'web.post' method.

Getting the Data to Edge Impulse

The data from Notehub is forwarded to Edge Impulse through an HTTP server. This server, which can be created using any language or framework, listens for POST requests and parses the floating-point accelerometer values. The data is then sent to Edge Impulse's ingestion API, adding new entries to the model’s training set.

see the for more information.

Updating ML Models

The final aspect of the MLOps process is updating the models on the devices in the field. For this, we use Notecard Outboard Firmware Update. This feature allows OTA firmware updates without writing any code, offering flexibility and safety against bricking devices.

see the for more information.

Notecard Outboard Firmware Update

  • Requires specific wiring (provided in the Blues Starter Kit)

  • Activated via a 'card.dfu' request in the firmware

  • Involves building a new firmware image file with the updated model

  • The new firmware is uploaded to Notehub and applied to devices

see the for more information.

Conclusion

The combination of data collection and model updating forms a robust MLOps process. This approach ensures continuous improvement of ML models based on real data and updates models remotely. The provided GitHub samples offer a reference implementation, adaptable to various project needs.

For more information, refer to the [Notecard Outboard Firmware Update guide](https://dev.blues.io/guides-and-tutorials/notecard-guides

The combination of data collection and model updating forms a robust MLOps process. This approach ensures continuous improvement of ML models based on real data and updates models remotely. The provided GitHub samples offer a reference implementation, adaptable to various project needs.

Learn More

  • Build Your Edge ML Application: Follow the step-by-step .

Summary

This tutorial acts as a reference to the webinar and tutorial from Blues Wireless: .

Data aquisition from S3 object store - Golioth on AI

In this tutorial we will demonstrate how to securely collect and process sensor data from a managed device with the example project. The managed device is connected via cellular and streams labeled but unstructured data to a mutually accessible Object Storage (S3 Bucket), by collecting labeled sensor data in this method we can also demonstrate a common use for this form of data acquisition, performing automated Data processing or Data Quality steps to the collected data via custom transformation blocks.

We will then apply data processing steps on the collected data using the custom CBOR transformation block to convert this data to a format we can use to later train a model. This same process can be adapted to perform Data Quality techniques that would typically be performed by an ML engineer.

Prerequisites

To follow along with this example, ensure that you have the following:

  • with an organization and a connected compatible device (e.g., nrf9160).

  • Edge Impulse enterprise account.

  • Trained Edge Impulse project ready for deployment.

  • AWS account with access to S3.

  • Basic knowledge of AWS S3, Docker, and Zephyr.

  • Example from the Golioth on AI tutorial repository

This tutorial was created for the Golioth on AI launch see the associated video on youtube .

Step 1: Create a Managed Golioth Device with Configured Firmware

Before proceeding with the integration, ensure that your Golioth device is set up with the appropriate firmware. For detailed instructions on initializing your Zephyr workspace, building, and flashing the firmware, please refer to the .

1. Initialize the Zephyr workspace:

  • Clone the Golioth on AI repository and initialize the Zephyr workspace with the provided manifest file.

2. Create a project on Golioth.

  • Log in to Golioth and create a new project. This project will handle the routing of sensor data and classification results.

3. Create a project on Edge Impulse.

  • Log in to Edge Impulse Studio and create a new project. This project will receive and process the raw accelerometer data uploaded from the S3 bucket.

Step 2: Create Golioth Pipelines

Once your device is set up, follow the instructions in the repository README to create the necessary Golioth pipelines. This includes setting up the Classification Results Pipeline and the Accelerometer Data Pipeline. Latest detailed steps can be found in the repository .

Golioth Pipelines allows you to route data between different services and devices efficiently. You will need to configure two pipelines for this demo:

1. Classification Results Pipeline:

  • This pipeline routes classification results (e.g., gesture predictions) to Golioth's LightDB Stream, which stores data in a timeseries format.

  • You will configure a path for classification results (e.g., /class) and ensure that the data is converted to JSON format.

2. Accelerometer Data Pipeline to S3:

  • This pipeline handles raw accelerometer data by forwarding it to an S3 object storage bucket. Ensure the pipeline is set up to transfer binary data.

  • Important: Configure your AWS credentials by creating secrets in Golioth for AWS_ACCESS_KEY and AWS_SECRET_KEY, and specify the target bucket name and region.

Step 3: Deploy the Edge Impulse Inferencing Library

1. Generate the Model:

  • Follow the instructions in the continuous motion recognition tutorial to generate a gesture recognition model.

2. Download and Extract the Library:

  • Download the generated library from Edge Impulse Studio and extract the contents.

3. Build and Flash Firmware:

  • Build the firmware:

  • Set Golioth Credentials::

  • Add the same configuration to the Golioth secret store

  • Navigate to your Golioth Secret Store: https://console.golioth.io/org/<organization>/project/<application>/secrets

  • Add your AWS credentials (Access Key ID and Secret Access Key) and the S3 bucket details to the Golioth secret store.

Step 4: Data Acquisition

1. Trigger Data Acquisition:

  • Press the button on the Nordic Thingy91 to start sampling data from the device's accelerometer.

2. Data Routing:

  • Raw accelerometer data will be automatically routed to your S3 bucket via the Golioth pipeline. You can later import this data into Edge Impulse Studio for further model training.

3. View Classification Results:

  • Classification results will be stored in Golioth's LightDB Stream. You can access this data for further analysis or visualization.

Step 5: Import Data into Edge Impulse

1. Access Data from S3:

  • In Edge Impulse Studio, use the Data acquisition page to import your raw accelerometer data directly from the S3 bucket

2. Label and Organize Data:

Once imported, label your data appropriately to prepare it for model training.

Example Classification and Raw Data

Below is an example of classification data you can expect to see in the Golioth console:

Raw accelerometer data uploaded to S3 can be imported as an array of X-Y-Z float values, which will appear in the Edge Impulse Studio as time-series data.

Step 6: Data Processing

From here we can perform a number of data processing steps on the collected data:

1. Data Transformation:

  • Use the custom CBOR transformation block to convert raw accelerometer data to a format suitable for training a model in Edge Impulse.

2. Data Quality:

  • Apply custom transformation blocks to perform data quality checks or preprocessing steps on the collected data.

3. Model Training:

  • Import the transformed data into Edge Impulse Studio and train a new model using the collected accelerometer data.

4. Model Deployment:

  • Deploy the trained model back to the Golioth device for real-time gesture recognition.

Summary

In this tutorial, we demonstrated how to securely collect and process sensor data from a managed device using Golioth and Edge Impulse. By leveraging Golioth's data routing capabilities and Edge Impulse's machine learning tools, you can easily build and deploy custom models for gesture recognition or other applications. This example showcases the end-to-end workflow from data acquisition to model training and deployment, highlighting the seamless integration between Golioth and Edge Impulse.

For more information on Golioth and Edge Impulse, visit the official documentation and tutorials:

with Zephyr on Golioth

Introduction

This page is part of the tutorial series. If you haven't read the introduction yet, we recommend you to do so .

In this tutorial, we'll guide you through deploying updated impulses over-the-air (OTA) using the Zephyr RTOS. We'll build on Zephyr's firmware update workflow, incorporating API calls to check for updates to your edge impulse project and download the latest build. As Zephyr does not have an OTA update service, and Golioth is used in their OTA guide. We'll use Golioth's OTA update service to deliver the updated firmware to the device. On devices running Zephyr RTOS, leveraging Golioth's cloud platform for the firmware update portion of the process.

Key Features of Golioth OTA Updates:

  • Secure and encrypted firmware delivery

  • Delta updates to minimize download size

  • Integration with Zephyr RTOS

Prerequisites

  • Edge Impulse Account: If you haven't got one, .

  • Trained Impulse: If you're new, follow one of our

  • Knowledge of Zephyr RTOS and nRF Connect SDK

  • A Golioth account and the Golioth SDK installed

Preparation

  1. Clone the Golioth Zephyr SDK examples repository:

On Your nRF9160 Development Board:

  1. Navigate to the Golioth firmware examples folder we are going to modify the device firmware update example to connect to the ingestion API to send data back from device.

Now copy the edge-impulse-sdk folder from the cloned repository into the dfu folder.

Include the header file the dfu example

Take the run inference example from your existing project

You will need to merge in the run inference function from your existing project. This will be the function that will be called to run inference on the device.

Add the run inference function to the main loop

Now add the run inference function to the main loop of the dfu example. This will run inference every 2 seconds.

Add a call back to send sensor data to Edge Impulse with data captured

Add the send data to Edge Impulse function

Add the sensor trigger to the main function

Building and Running

With the classification, ingestion code and Golioth implementation in place, proceed to build and run the application on the nRF9160 Development Board. Utilize the following commands:

These commands will build and flash the firmware onto your development board.

Verifying The Operation

To verify that the Edge Impulse model is running and updating OTA with Golioth, observe the console output. It should display inference results at regular intervals. Also, you can monitor the device’s firmware version and its updates via the Golioth console.

Testing OTA Updates

Simulate an OTA update by changing the Edge Impulse model and repeating the build and flash process. Monitor the Golioth console for updates and check the device console to observe the changes in inference results.

Conclusion

Congratulations! You've successfully implemented OTA model updates with Edge Impulse on Zephyr RTOS and nRF Connect SDK, facilitated by Golioth. You can now continuously enhance and deploy machine learning models to your edge devices securely and efficiently.

Running jobs using the API

The gives programmatic access to all features in the studio, and many tasks that might normally have to be performed manually can thus be automated. In this tutorial you'll create a job that deploys your model (as a ZIP file), you monitor the job status, and then finally download the deployed model. The tutorial uses Python, but you can use any environment capable of scripting HTTP requests.

To run this script you'll need:

  • A recent version of Python 3.

  • The requests module:

  • Your project ID (can be found on Dashboard in the Edge Impulse Studio).

  • An API Key (under Dashboard > Keys).

Then, create the following script build-model.py:

When you now run python3 download-model.py you'll see something like:

🚀


#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

const char* ssid = "your-SSID";
const char* password = "your-PASSWORD";

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

  // Port defaults to 8266
  ArduinoOTA.setPort(8266);

  // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setHostname("myesp8266");

  // No authentication by default
  ArduinoOTA.setPassword((const char *)"123");

  ArduinoOTA.onStart([]() {
    Serial.println("Start");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}
mkdir ~/ota-esp32
cd ~/ota-esp32
cp -r $IDF_PATH/examples/system/ota .
idf.py set-target esp32
idf.py menuconfig
import requests
import json
import os

API_KEY = 'your-edge-impulse-api-key'
PROJECT_ID = 'your-project-id'
MODEL_PATH = 'path_to_your_local_model'

def get_last_modification_date():
    url = f'https://studio.edgeimpulse.com/v1/api/{PROJECT_ID}/last-modification-date'
    headers = {'x-api-key': API_KEY}

    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        data = response.json()
        return data['lastModificationDate']
    else:
        print(f"Failed to get last modification date: {response.text}")
        return None

def download_model():
    url = f'https://studio.edgeimpulse.com/v1/api/{PROJECT_ID}/deployment/download'
    headers = {'x-api-key': API_KEY}

    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        with open(MODEL_PATH, 'wb') as file:
            file.write(response.content)
        print("Model downloaded successfully.")
    else:
        print(f"Failed to download the model: {response.text}")

# get the stored timestamp or hash
stored_timestamp = None # replace this with logic to get the stored timestamp or hash

# check for recent modifications
last_modification_date = get_last_modification_date()

# compare and download if newer
if last_modification_date and last_modification_date != stored_timestamp:
    print("New model available. Downloading...")
    download_model()

    # update the stored timestamp or hash
    stored_timestamp = last_modification_date

    # restart the device
    os.system('sudo reboot')





#include "nvs.h"
#include "nvs_flash.h"
#include "esp_http_client.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
Lifecycle Management with Edge Impulse
here
sign up here
data acquisition
impulse design
git clone https://github.com/golioth/zephyr-sdk.git
cd zephyr-sdk/samples/dfu
cp -r edge-impulse-sdk zephyr-sdk/samples/dfu
#include "edge-impulse-sdk/classifier/ei_run_classifier.h"
static void run_inference() {
    struct sensor_value accel[3];
    struct device *sensor = device_get_binding(SENSOR_DEV_NAME);
    if (sensor == NULL) {
        printk("Could not get %s device\n", SENSOR_DEV_NAME);
        return;
    }
    sensor_sample_fetch(sensor);
    sensor_channel_get(sensor, SENSOR_CHAN_ACCEL_XYZ, accel);

    float features[3];
    for (int i = 0; i < 3; i++) {
        features[i] = sensor_value_to_double(&accel[i]);
    }

int main() {
    struct device *sensor = device_get_binding(SENSOR_DEV_NAME);
    if (sensor == NULL) {
        printk("Could not get %s device\n", SENSOR_DEV_NAME);
        return -1;
    }
    struct sensor_trigger trig;
    trig.type = SENSOR_TRIG_DATA_READY;
    trig.chan = SENSOR_CHAN_ACCEL_XYZ;
    if (sensor_trigger_set(sensor, &trig, sensor_trigger_handler) < 0) {
        printk("Could not set trigger for %s sensor\n", SENSOR_DEV_NAME);
        return -1;
    }

    while (1) {
        k_sleep(K_SECONDS(1));
    }

    return 0;
}
static void sensor_trigger_handler(struct device *dev, struct sensor_trigger *trigger) {
    run_inference();
    // Send data to Edge Impulse via ingestion API
    send_data_to_edge_impulse();

}
static void send_data_to_edge_impulse() {
    // Set up HTTP client request
    struct http_client_request req;
    struct http_client_ctx ctx;
    char data_buf[512];
    snprintf(data_buf, sizeof(data_buf), "{\"label\":\"%s\",\"data\":\"%s\"}", "my_label", "my_data");
    http_client_request_init(&req);
    req.method = HTTP_POST;
    req.url = "http://ingestion.edgeimpulse.com/api/training/data";
    req.protocol = "HTTP/1.1";
    req.host = "ingestion.edgeimpulse.com";
    req.header_fields = "Content-Type: application/json\r\n"
                        "x-api-key: " EDGE_IMPULSE_API_KEY "\r\n";
    req.payload = data_buf;
    req.payload_len = strlen(data_buf);
    http_client_init(&ctx, &req);
    // Send HTTP request
    int ret = http_client_send(&ctx);
    if (ret < 0) {
        printf("HTTP request failed: %d\n", ret);
    } else {
        printf("HTTP request sent successfully\n");
    }
}
int main() {
    struct device *sensor = device_get_binding(SENSOR_DEV_NAME);
    if (sensor == NULL) {
        printk("Could not get %s device\n", SENSOR_DEV_NAME);
        return -1;
    }
    struct sensor_trigger trig;
    trig.type = SENSOR_TRIG_DATA_READY;
    trig.chan = SENSOR_CHAN_ACCEL_XYZ;
    if (sensor_trigger_set(sensor, &trig, sensor_trigger_handler) < 0) {
        printk("Could not set trigger for %s sensor\n", SENSOR_DEV_NAME);
        return -1;
    }

    while (1) {
        k_sleep(K_SECONDS(1));
    }

    return 0;
}
west build -b nrf9160dk_nrf9160_ns -p
west flash
Lifecycle Management with Edge Impulse
here
sign up here
end-to-end tutorials
pip3 install requests
import requests, json, datetime, time, re

project_id = 1 # YOUR PROJECT ID
api_key = "ei_..." # YOUR API KEY
deploy_type = "zip" # CAN CHANGE TO DIFFERENT TYPE

def build_model():
    url = f"https://studio.edgeimpulse.com/v1/api/{project_id}/jobs/build-ondevice-model"
    querystring = {"type": deploy_type}
    payload = {"engine": "tflite-eon"}
    headers = {
        "x-api-key": api_key,
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    response = requests.request("POST", url, json=payload, headers=headers, params=querystring)
    body = json.loads(response.text)
    if (not body['success']):
        raise Exception(body['error'])
    return body['id']

def get_stdout(job_id, skip_line_no):
    url = f"https://studio.edgeimpulse.com/v1/api/{project_id}/jobs/{job_id}/stdout"
    headers = {
        "x-api-key": api_key,
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    response = requests.request("GET", url, headers=headers)
    body = json.loads(response.text)
    if (not body['success']):
        raise Exception(body['error'])
    stdout = body['stdout'][::-1] # reverse array so it's old -> new
    return [ x['data'] for x in stdout[skip_line_no:] ]

def wait_for_job_completion(job_id):
    skip_line_no = 0

    url = f"https://studio.edgeimpulse.com/v1/api/{project_id}/jobs/{job_id}/status"
    headers = {
        "x-api-key": api_key,
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    while True:
        response = requests.request("GET", url, headers=headers)
        body = json.loads(response.text)
        if (not body['success']):
            raise Exception(body['error'])

        stdout = get_stdout(job_id, skip_line_no)
        for l in stdout:
            print(l, end='')
        skip_line_no = skip_line_no + len(stdout)

        if (not 'finished' in body['job']):
            # print('Job', job_id, 'is not finished yet...', body['job'])
            time.sleep(1)
            continue
        if (not body['job']['finishedSuccessful']):
            raise Exception('Job failed')
        else:
            break

def download_model():
    url = f"https://studio.edgeimpulse.com/v1/api/{project_id}/deployment/download"
    querystring = {"type": deploy_type}
    headers = {
        "x-api-key": api_key,
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    response = requests.request("GET", url, headers=headers, params=querystring)

    d = response.headers['Content-Disposition']
    fname = re.findall("filename\*?=(.+)", d)[0].replace('utf-8\'\'', '')

    return fname, response.content

if __name__ == "__main__":
    job_id = build_model()
    print('Job ID is', job_id)
    wait_for_job_completion(job_id)
    print('Job', job_id, 'is finished')
    bin_filename, bin_file = download_model()
    print('Output file is', len(bin_file), 'bytes')
    with open(bin_filename, 'wb') as f:
        f.write(bin_file)
    print('Written job output to', bin_filename)
Job ID is 1486148
Writing templates OK

Scheduling job in cluster...
Compiling EON model...
Job started
Compiling EON model OK
Removing clutter...
Removing clutter OK
Copying output...
Copying output OK

Job 1486148 is finished
Output file is 4824580 bytes
Written job output to myawesomeproject-v63.zip
Edge Impulse API
plans and pricing
Enterprise trial
import getpass
import requests
import json
import pytz
from datetime import datetime
import time
import sys

PROJECT_ID = 94424  #👈🏼 Update as necessary!

URL_STUDIO = "https://studio.edgeimpulse.com/v1/api/"
URL_PROJECT = URL_STUDIO + str(PROJECT_ID)
KEY = getpass.getpass('Enter your API key: ')
API_HEADERS = {
    "Accept": "application/json",
    "x-api-key": KEY  #👈🏼 Update as necessary!
}


def get_eon_info(project, response_key):
    response = requests.get(URL_STUDIO + str(project) + "/optimize/state",
                            headers=API_HEADERS)
    if response.ok:
        return json.loads(response.text)[response_key]

response = requests.get(URL_PROJECT, headers=API_HEADERS)
if response.ok:
    data = json.loads(response.text)
    print(json.dumps(data, indent=2))
else:
    print("\n⛔️ An Error Ocurred, do you have the correct project ID?")
if "running" == get_eon_info(PROJECT_ID, "status")["status"]:
    print(
        "EON Tuner job is running, run section 2.4 to track the job's progress."
    )
else:
    payload = {
        "datasetCategory": # Select one of:
            # "speech_keyword"
            # "speech_continuous"
            # "audio_event"
            # "audio_continuous"
            # "transfer_learning"
            # "motion_event"
            # "motion_continuous"
            # "audio_syntiant"
            "audio_continuous",
        "targetLatency": 500,  # Latency in ms
        "targetDevice": {
            "name":  # Select one of:
            # cortex-m4f-80mhz
            # cortex-m7-216mhz
            # st-iot-discovery-kit
            # arduino-nano-33-ble
            # nordic-nrf52840-dk
            # nordic-nrf5340-dk
            # nordic-nrf9160-dk
            # silabs-thunderboard-sense-2
            # silabs-xg24
            # synaptics-ka10000
            # himax-we-i
            # wio-terminal
            # sony-spresense
            # ti-launchxl
            # portenta-h7
            # mbp-16-2020
            # raspberry-pi-4
            # raspberry-pi-rp2040
            # jetson-nano
            "jetson-nano",
            "ram": 262144,  # Memory in bytes
            "rom": 1048576  # Memory in bytes
        },
        "trainingCycles": 10,  # Default 100
        "tuningMaxTrials": 3,  # Default 30
        "tuningWorkers": 9,  # Default 3
        "minMACCS": 100,  # Default 0
        "maxMACCS": 1750,  # Default 1750000
        "tuningAlgorithm": # Select one of:
        # "random"
        # "hyperband"
        # "bayesian"
        "random"
    }
    response = requests.post(URL_PROJECT + "/optimize/config",
                            headers=API_HEADERS,
                            json=payload)
    if response.ok:
        # Show me the new configuration
        response = requests.get(URL_PROJECT + "/optimize/state", headers=API_HEADERS)
        if response.ok:
            print("EON Tuner configuration updated successfully!")
            print(json.dumps(get_eon_info(PROJECT_ID, "config"), indent=2))
if "running" == get_eon_info(PROJECT_ID, "status")["status"]:
    print(
        "EON Tuner job is running, run section 2.4 to track the job's progress."
    )
else:
    response = requests.post(URL_PROJECT + "/jobs/optimize", headers=API_HEADERS)
    if response.ok:
        data = json.loads(response.text)
        print("EON Tuner job %s started successfully!" % data["id"])
        # print(json.dumps(data, indent=2))
finished = False
job_id = get_eon_info(PROJECT_ID, "activeTunerJobId")

while not finished:
    response = requests.get(URL_PROJECT + "/jobs/" + str(job_id) + "/status", headers=API_HEADERS)
    if response.ok:
        job_status = json.loads(response.text)
        if "finishedSuccessful" in job_status["job"].keys():
            print("\nJob completed")
            finished = True
        else:
            response = requests.get(URL_PROJECT + "/optimize/state", headers=API_HEADERS)
            if response.ok:
                status = json.loads(response.text)["status"]
                started = datetime.fromisoformat(job_status["job"]["started"].replace("Z", "+00:00"))
                for iter in range(30):
                    now = datetime.now(pytz.utc)
                    diff = now - started
                    sys.stdout.write("\r[%s] " % diff) # Back to the beginning
                    for x in range(status["numCompletedTrials"]):
                        sys.stdout.write("█")
                    for x in range(status["numRunningTrials"]):
                        sys.stdout.write("▒")
                    for x in range(status["numPendingTrials"]):
                        sys.stdout.write(" ")
                    total = status["numCompletedTrials"] + status[
                        "numRunningTrials"] + status["numPendingTrials"]
                    sys.stdout.write(" %d/%d" % (status["numCompletedTrials"], total))
                    time.sleep(1)
trials = get_eon_info(PROJECT_ID, "trials")
print(trials[0].keys())
Edge Impulse API
Edge Impulse API
const emptySignature = Array(64).fill("0").join("");
const body = {
    protected: {
        ver: "v1",
        alg: "none",
        iat: Math.floor(Date.now() / 1000),
    },
    signature: emptySignature,
    payload: {
        device_name: "device-1",
        device_type: "LIS2HH12",
        interval_ms: 1,
        sensors: [
            { name: "accX", units: "m/s2" },
            { name: "accY", units: "m/s2" },
            { name: "accZ", units: "m/s2" },
        ],
        values: data,
    },
};

try {
    await fetch("https://ingestion.edgeimpulse.com/api/training/data", {
        method: "POST",
        headers: {
            "x-api-key": process.env.EDGE_IMPULSE_API_KEY,
            "x-file-name": "test",
            "x-label": "idle",
            "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
    });
} catch (e) {
    console.log("Error publishing data", e);
}
Lifecycle Management with Edge Impulse
here
Manage Notecard Firmware guide
sign up here
end-to-end tutorials
sign up here
order here
here
Establishing a Robust MLOps Workflow for TinyML Blues Guide
section of the Blues Guide
Updating ML Models section of the Blues Guide
What is notecard outboud update? section of the Blues Guide
Blues Swan tutorial
Optimized MLOps with Edge Impulse, Blues, and Zephy view the full webinar
  west init -m https://github.com/golioth/example-edge-impulse.git --mf west-ncs.yml
  west update
mkdir ei-generated
unzip -q <path-to-generated>.zip -d ei-generated/
mv ei-generated/edge-impulse-sdk/ .
mv ei-generated/model-parameters/ .
mv ei-generated/tflite-model/ .
west build -p -b thingy91_nrf9160_ns app/
west flash
  uart:~$ settings set golioth/psk-id <my-psk-id@my-project>
  uart:~$ settings set golioth/psk <my-psk>

  {
    "idle": 0.28515625,
    "snake": 0.16796875,
    "updown": 0.21875,
    "wave": 0.328125
  }
Golioth on AI
Golioth account
here
Read it here
Golioth on AI repository README
Golioth on AI repository README
Golioth Documentation
Golioth on AI - Blog
Golioth Secret Store
ids.json
preview mode
plans and pricing
Enterprise trial
forum

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the overview page. Please refer to that documentation for details.

deployment-metadata.json
plans and pricing
Enterprise trial
forum

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the overview page. Please refer to that documentation for details.

Generate physics simulation datasets using PyBullet

This notebook takes you through a basic example of using the physics simulation tool PyBullet to generate an accelerometer dataset representing dropping the Nordic Thingy:53 devkit from different heights. This dataset can be used to train a regression model to predict drop height.

This idea could be used for a wide range of simulatable environments- for example generating accelerometer datasets for pose estimation or fall detection. The same concept could be applied in an FMEA application for generating strain datasets for structural monitoring.

There is also a video version of this tutorial:

Local Software Requirements

  • Python 3

  • Pip package manager

  • Jupyter Notebook: https://jupyter.org/install

  • Bullet3: https://github.com/bulletphysics/bullet3

The dependencies can be installed with:

! pip install pybullet numpy
# Imports
import pybullet as p
import pybullet_data
import os
import shutil
import csv
import random
import numpy as np
import json

Create object to simulate

We need to load in a Universal Robotics Description Format file describing an object with the dimensions and weight of a Nordic Thingy:53. In this case, measuring our device it is 64x60x23.5mm and its weight 60g. The shape is given by a .obj 3D model file.

	<visual>
		<origin xyz="0.02977180615936878 -0.01182944632717566 0.03176079914341195" rpy="1.57079632679 0.0 0.0" />
		<geometry>
			<mesh filename="thingy53/thingy53 v2.obj" scale="1 1 1" />
		</geometry>
		<material name="texture">
			<color rgba="1.0 1.0 1.0 1.0" />
		</material>
	</visual>
	<collision>
		<origin xyz="0.02977180615936878 -0.01182944632717566 0.03176079914341195" rpy="1.57079632679 0.0 0.0" />
		<geometry>
			<mesh filename="thingy53/thingy53 v2.obj" scale="1 1 1" />
		</geometry>
	</collision>
	<inertial>
		<mass value="0.06" />
  		<inertia ixx="0.00002076125" ixy="0" ixz="0" iyy="0.00002324125" iyz="0" izz="0.00003848"/>
	</inertial>
</link>

Visualizing the problem

To generate the required data we will be running PyBullet in headless "DIRECT" mode so we can iterate quickly over the parameter field. If you run the python file below you can see how pybullet simulates the object dropping onto a plane

! python ../.assets/pybullet/single_simulation.py

Setting up the simulation environment

First off we need to set up a pybullet physics simulation environment. We load in our object file and a plane for it to drop onto. The plane's dynamics can be adjusted to better represent the real world (in this case we're dropping onto carpet)

# Set up PyBullet physics simulation (change from p.GUI to p.DIRECT for headless simulation)
physicsClient = p.connect(p.DIRECT)
p.setAdditionalSearchPath(pybullet_data.getDataPath())
p.setGravity(0, 0, -9.81)

# Load object URDF file
obj_file = "../.assets/pybullet/thingy53/thingy53.urdf"
obj_id = p.loadURDF(obj_file, flags=p.URDF_USE_INERTIA_FROM_FILE)

# Add a solid plane for the object to collide with
plane_id = p.loadURDF("plane.urdf")

# Set length of simulation and sampling frequency
sample_length = 2 # Seconds
sample_freq = 100 # Hz

We also need to define the output folder for our simulated accelerometer files

output_folder = 'output/'
# Check if output directory for noisey files exists and create it if it doesn't
if not os.path.exists(output_folder):
    os.makedirs(output_folder)
else:
    shutil.rmtree(output_folder)
    os.makedirs(output_folder)

And define the drop parameters

# Simulate dropping object from range of heights
heights = 100
sims_per_height = 20
min_height = 0.1 # Metres
max_height = 0.8 # Metres

We also need to define the characteristics of the IMU on the real device we are trying to simulate. In this case the Nordic Thingy:53 has a Bosch BMI270 IMU (https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/) which is set to a range of +-2g with a resolution of 0.06g. These parameters will be used to restrict the raw acceleration output:


range_g = 2
range_acc = range_g * 9.81
resolution_mg = 0.06
resolution_acc = resolution_mg / 1000.0 * 9.81

Finally we are going to give the object and plane restitution properties to allow for some bounce. In this case I dropped the real Thingy:53 onto a hardwood table. You can use p.changeDynamics to introduce other factors such as damping and friction.

p.changeDynamics(obj_id, -1, restitution=0.3)
p.changeDynamics(plane_id, -1, restitution=0.4)

Drop simulation

Here we iterate over a range of heights, randomly changing its start orientation for i number of simulations per height. The acceleration is calculated relative to the orientation of the Thingy:53 object to represent its onboard accelerometer.

metadata = []
for height in np.linspace(max_height, min_height, num=heights):
    print(f"Simulating {sims_per_height} drops from {height}m")
    for i in range(sims_per_height):
        # Set initial position and orientation of object
        x = 0
        y = 0
        z = height
        orientation = p.getQuaternionFromEuler((random.uniform(0, 2 * np.pi), random.uniform(0, 2 * np.pi), random.uniform(0, 2 * np.pi)))
        p.resetBasePositionAndOrientation(obj_id, [x, y, z], orientation)

        prev_linear_vel = np.zeros(3)

        # Initialize the object position and velocity
        pos_prev, orn_prev = p.getBasePositionAndOrientation(obj_id)
        vel_prev, ang_vel_prev = p.getBaseVelocity(obj_id)
        timestamp=0
        dt=1/sample_freq
        p.setTimeStep(dt)
        filename=f"drop_{height}m_{i}.csv"
        with open(f"output/{filename}", mode="w") as csv_file:
                writer = csv.writer(csv_file)
                writer.writerow(['timestamp','accX','accY','accZ'])
        while timestamp < sample_length:
            p.stepSimulation()
            linear_vel, angular_vel = p.getBaseVelocity(obj_id)
            lin_acc = [(v - prev_v)/dt for v, prev_v in zip(linear_vel, prev_linear_vel)]
            prev_linear_vel = linear_vel
            timestamp += dt
            # Get the current position and orientation of the object
            pos, orn = p.getBasePositionAndOrientation(obj_id)

            # Get the linear and angular velocity of the object in world coordinates
            vel, ang_vel = p.getBaseVelocity(obj_id)

             # Calculate the change in position and velocity between steps
            pos_diff = np.array(pos) - np.array(pos_prev)
            vel_diff = np.array(vel) - np.array(vel_prev)

            # Convert the orientation quaternion to a rotation matrix
            rot_matrix = np.array(p.getMatrixFromQuaternion(orn)).reshape(3, 3)

            # Calculate the local linear acceleration of the object, subtracting gravity
            local_acc = np.dot(rot_matrix.T, vel_diff / dt) - np.array([0, 0, -9.81])
            # Restrict the acceleration to the range of the accelerometer
            imu_rel_lin_acc_scaled = np.clip(local_acc, -range_acc, range_acc)
            # Round the acceleration to the nearest resolution of the accelerometer
            imu_rel_lin_acc_rounded = np.round(imu_rel_lin_acc_scaled/resolution_acc) * resolution_acc
            # Update the previous position and velocity
            pos_prev, orn_prev = pos, orn
            vel_prev, ang_vel_prev = vel, ang_vel

            # Save acceleration data to CSV file
            with open(f"{output_folder}{filename}", mode="a") as csv_file:
                writer = csv.writer(csv_file)
                writer.writerow([timestamp*1000] + imu_rel_lin_acc_rounded.tolist())

        nearestheight = round(height, 2)
        metadata.append({
            "path": filename,
            "category": "training",
            "label": { "type": "label", "label": str(nearestheight)}
        })

Finally we save the metadata file to the output folder. This can be used to tell the edge-impulse-uploader CLI tool the floating point labels for each file.

jsonout = {"version": 1, "files": metadata}

with open(f"{output_folder}/files.json", "w") as f:
    json.dump(jsonout, f)

# Disconnect from PyBullet physics simulation
p.disconnect()

These files can then be uploaded to a project with these commands (run in a separate terminal window):

! cd output
! edge-impulse-uploader --info-file files.json

(run edge-impulse-uploader --clean if you have used the CLI before to reset the target project)

What next?

Now you can use your dataset a drop height detection regression model in Edge Impulse Studio!

See if you can edit this project to simulate throwing the object up in the air to predict the maximum height, or add in your own custom object. You could also try to better model the real environment you're dropping the object in- adding air resistance, friction, damping and material properties for your surface.

Using the Edge Impulse Python SDK with Hugging Face

🤗 Hugging Face offers a suite of tools that assist with various AI applications. Most notably, they provide a hub for people to share their pre-trained models. In this tutorial, we will demonstrate how to download a simple ResNet model from the Hugging Face hub, profile it, and convert it to a C++ library for use in your edge application. This particular model was trained to identify species of bean plants using the bean dataset.

To learn more about using the Python SDK, please see: Edge Impulse Python SDK Overview

# If you have not done so already, install the following dependencies
!python -m pip install huggingface_hub edgeimpulse
import json
from huggingface_hub import hf_hub_download
import edgeimpulse as ei

You will need to obtain an API key from an Edge Impulse project. Log into edgeimpulse.com and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Copy API key from Edge Impulse project

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

# Edge Impulse Settings
ei.API_KEY = "ei_dae2..."
target_device = 'cortex-m4f-80mhz'
deploy_filename = "my_model_cpp.zip"

To download a model from the Hugging Face hub, we need to first find a model. Head to huggingface.co/models. On the left side, click Image Classification to filter under the Tasks tab and under the Libraries tab, filter by ONNX (as the Edge Impulse Python SDK easily accepts ONNX models). You should see the resnet-tiny-beans model trained by user fxmarty.

Filter models on Hugging Face

Click on the resnet-tiny-beans entry (or follow this link) to read about the model and view the files. If you click on the Files* tab, you can see all of the files available in this particular model.

View files in Hugging Face model

Set the name of the repo (username/repo-name) and the file we want to download.

# Define file location for our model
repo_name = "fxmarty/resnet-tiny-beans"
download_dir = "./"
model_filename = "model.onnx"

# Download pre-trained model
hf_hub_download(repo_id=repo_name,
                filename=model_filename,
                local_dir=download_dir)

Profile your model

To start, we need to list the possible target devices we can use for profiling. We need to pick from this list.

# List the available profile target devices
ei.model.list_profile_devices()

You should see a list printed such as:

['alif-he',
 'alif-hp',
 'arduino-nano-33-ble',
 'arduino-nicla-vision',
 'portenta-h7',
 'brainchip-akd1000',
 'cortex-m4f-80mhz',
 'cortex-m7-216mhz',
 ...
 'ti-tda4vm']

A common option is the cortex-m4f-80mhz, as this is a relatively low-power microcontroller family. From there, we can use the Edge Impulse Python SDK to generate a profile for your model to ensure it fits on your target hardware and meets your timing requirements.

# Estimate the RAM, ROM, and inference time for our model on the target hardware family
try:
    profile = ei.model.profile(model=model_filename,
                               device='cortex-m4f-80mhz')
    print(profile.summary())
except Exception as e:
    print(f"Could not profile: {e}")

Deploy your model

Once you are happy with the performance of the model, you can deploy it to a number of possible hardware targets. To see the available hardware targets, run the following:

# List the available profile target devices
ei.model.list_deployment_targets()

You should see a list printed such as:

['zip',
 'arduino',
 'tinkergen',
 'cubemx',
 'wasm',
 ...
 'runner-linux-aarch64-tda4vm']

The most generic target is to download a .zip file containing a C++ library containing the inference runtime and your trained model, so we choose 'zip' from the above list. We also need to tell Edge Impulse how we are planning to use the model. In this case, we want to perform classification, so we set the output type to Classification.

Note that instead of writing the raw bytes to a file, you can also specify an output_directory argument in the .deploy() function. Your deployment file(s) will be downloaded to that directory.

# Create C++ library with trained model
deploy_bytes = None
try:
    deploy_bytes = ei.model.deploy(model=model_filename,
                                   model_output_type=ei.model.output_type.Classification(),
                                   deploy_target='zip')
except Exception as e:
    print(f"Could not deploy: {e}")
    
# Write the downloaded raw bytes to a file
if deploy_bytes:
    with open(deploy_filename, 'wb') as f:
        f.write(deploy_bytes.getvalue())

Your model C++ library should be downloaded as the file my_model_cpp.zip in the same directory as this notebook. You are now ready to use your C++ model in your embedded and edge device application! To use the C++ model for local inference, see our documentation here.

Health reference design

In this section, you will find a health reference design that describes an end-to-end machine learning workflow for building a wearable health product using Edge Impulse.

We will utilize a publicly available dataset of PPG and accelerometer data that was collected from 15 subjects performing various activities, and emulate a clinical study to train a machine learning model that can classify activities.

Overview

The dataset selected to use in this example is the PPG-DaLiA dataset, which includes 15 subjects performing 9 activities, resulting in a total of 15 recordings. See the CSV file summary here, and read more about it in the publishers website here,

This dataset covers an activity study where data is recorded from a wearable end device (PPG + accelerometer), along with labels such as Stairs, Soccer, Cycling, Driving, Lunch, Walking, Working, Clean Baseline, and No Activity. The data is collected and validated, then written to a clinical dataset in an Edge Impulse organization, and finally imported into an Edge Impulse project where we train a classifier.

The health reference design builds transformation blocks that sync clinical data, validate the dataset, query the dataset, and transform the data to process raw data files into a unified dataset.

Data Pipeline

The design culminates in a data pipeline that handles data coming from multiple sources, data alignment, and a multi-stage pipeline before the data is imported into an Edge Impulse project.

We won't cover in detail all the code snippets, it should be straightforward to follow through, if issues are encountered our solution engineers can help you set this end-to-end ML workflow.

Health Reference Design Sections

This health reference design section helps you understand how to create a full clinical data pipeline by:

  • Synchronizing clinical data with a bucket: Collect and organize data from multiple sources into a sorted dataset.

  • Validating clinical data: Ensure the integrity and consistency of the dataset by applying checklists.

  • Querying clinical data: Explore and slice data using a query system.

  • Transforming clinical data: Process and transform raw data into a format suitable for machine learning.

Bringing it all together

After you have completed the health reference design, you can go further by combining the individual transformation steps into a data pipeline.

Refer to the following guide to learn how to build data pipelines:

  • Building data pipelines: Build pipelines to automate data processing steps.

The Health Reference Design pipeline consists of the following steps:

  • DataProcessor: Processes raw data files for each subject.

  • MetadataGenerator: Extracts and attaches metadata to each subject's data.

  • DataCombiner: Merges all processed data into a unified dataset.

Repository containing the blocks used in this health reference design:

https://github.com/edgeimpulse/health-reference-design-public-data

Data Pipeline Workflow

The data pipeline workflow for the Health Reference Design is as follows:

Data Pipeline - workflow

Creating and Running the Pipeline in Edge Impulse

Now that all transformation blocks are pushed to Edge Impulse, you can create a pipeline to chain them together.

Steps:

  1. Access Pipelines:

  • In Edge Impulse Studio, navigate to your organization.

  • Go to Data > Pipelines.

  1. Add a New Pipeline:

  • Click on + Add a new pipeline.

  • Name: PPG-DaLiA Data Processing Pipeline

  • Description: Processes PPG-DaLiA data from raw files to a combined dataset.

  1. Configure Pipeline Steps:

  • Paste the following JSON configuration into the pipeline steps:

[
  {
   "name": "Process Subject Data",
   "filter": "name LIKE '%S%_E4%'",
   "uploadType": "dataset",
   "inputDatasetId": "raw-dataset",
   "outputDatasetId": "processed-dataset",
   "transformationBlockId": 1234, // Replace 1234 with your DataProcessor block ID
   "transformationParallel": 3,
   "parameters": {
    "in-directory": "."
   }
  },
  {
   "name": "Generate Metadata",
   "filter": "name LIKE '%S%_E4%'",
   "uploadType": "dataset",
   "inputDatasetId": "processed-dataset",
   "outputDatasetId": "processed-dataset",
   "transformationBlockId": 5678, // Replace 5678 with your MetadataGenerator block ID
   "transformationParallel": 3,
   "parameters": {
    "in-directory": "."
   }
  },
  {
   "name": "Combine Processed Data",
   "filter": "name LIKE '%'",
   "uploadType": "dataset",
   "inputDatasetId": "processed-dataset",
   "outputDatasetId": "combined-dataset",
   "transformationBlockId": 9101, // Replace 9101 with your DataCombiner block ID
   "transformationParallel": 1,
   "parameters": {
    "dataset-name": "ppg_dalia_combined.parquet"
   }
  }
]

Replace the transformationBlockId values with the actual IDs of your transformation blocks.

  1. Save the Pipeline.

  2. Run the Pipeline:

  • Click on the ⋮ (ellipsis) next to your pipeline.

  • Select Run pipeline now.

  1. Monitor Execution:

  • Check the pipeline logs to ensure each step runs successfully.

  • Address any errors that may occur.

  1. Verify Output:

  • After completion, verify that the datasets (processed-dataset and combined-dataset) have been created and populated.

Next Steps

After the pipeline has successfully run, you can import the combined dataset into an Edge Impulse project to train a machine learning model.

If you didn't complete the pipeline, don't worry, this is just a demonstration. However, you can still import the processed dataset from our HRV Analysis tutorial to train a model.

Refer to the following guides to learn how to import datasets into Edge Impulse:

  • HRV Analysis: Analyze Heart Rate Variability (HRV) data.

  • Activity Recognition: Classify activities using accelerometer data.

  • MLOps: Implement MLOps practices in your workflow.

Conclusion

The Health Reference Design provides a comprehensive overview of building a wearable health product using Edge Impulse. By following the steps outlined in this guide, you will gain a practical understanding of a real-world machine learning workflow for processing and analyzing clinical data.

If you have any questions or need assistance with implementing the Health Reference Design, feel free to reach out to our sales team for product development, or share your work on our forum.

Using the Edge Impulse Python SDK with Weights & Biases

is an online framework for helping manage machine learning training, data versioning, and experiments. When running experiments for edge-focused ML projects, it can be helpful to see the required memory (RAM and ROM) along with estimated inference times of your model for your target hardware. By viewing these metrics, you can quickly gauge if your model will fit onto your target device!

Follow the code below to see how to train a simple machine learning model with different hyperparameters and log those values to the Weights & Biases dashboard.

To learn more about using the Python SDK, please see:

You will need to obtain an API key from an Edge Impulse project. Log into and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

To use Weights and Biases, you will need to create an account on and call the wandb.login() function. This will prompt you to log in to your account. Your credentials should be stored, which allows you to use the wandb package in your Python library.

Gather a dataset

We want to create a classifier that can uniquely identify handwritten digits. To start, we will use TensorFlow and Keras to train a very simple convolutional neural network (CNN) on the classic dataset, which consists of handwritten digits from 0 to 9.

Create an experiment

We want to vary the hyperparameters in our model and see how it affects the accuracy and predicted RAM, ROM, and inference time on our target platform. To do that, we construct a function that builds a simple model using Keras, trains the model, and computes the accuracy and loss from our holdout test set. We then use the Edge Impulse Python SDK to generate a profile of our model for our target hardware. We log the hyperparameter (number of nodes in the hidden layer), test loss, test accuracy, estimated RAM, estimated ROM, and estimated inference time (ms) to our Weights and Biases console.

Run the experiment

Now, it's time to run the experiment and log the results in Weights and Biases. Simply call our function and provide a new hyperparameter value for the number of nodes.

Head to and log in (if you have not already done so). Under My projects on the left, click on the nodes-sweep project. You can visualize the results of your experiments with the various charts that Weights & Biases offers. For example, here is a that allows you to quickly visualize the different hyperparameters and metrics (including our new edge profile metrics).

If you would like to deploy your model to your target hardware, the Python SDK can help you with that, too. See our documentation .

Deploy Your Model

Once you are happy with the performance of your model, you can then deploy it to your target hardware. We will assume that 32 nodes in our hidden layer provided the best trade-off of RAM, flash, inference time, and accuracy for our needs. To start, we will retrain the model:

Next, we should evaluate the model on our holdout test set.

From there, we can see the available hardware targets for deployment:

You should see a list printed such as:

The most generic target is the .zip file that holds a C++ library containing our trained model and inference runtime. To pass our labels to the C++ library, we create a Classification object, which contains our label strings.

Note that instead of writing the raw bytes to a file, you can also specify an output_directory argument in the .deploy() function. Your deployment file(s) will be downloaded to that directory.

Your model C++ library should be downloaded as the file my_model_cpp.zip in the same directory as this notebook. You are now ready to use your C++ model in your embedded and edge device application! To use the C++ model for local inference, see our documentation .

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the overview page. Please refer to that documentation for details.

custom blocks

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the custom blocks overview page. Please refer to that documentation for details.

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

custom blocks

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the custom blocks overview page. Please refer to that documentation for details.

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

custom blocks

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the custom blocks overview page. Please refer to that documentation for details.

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

Works with Edge Impulse
Open in Google Colab
# If you have not done so already, install the following dependencies
!python -m pip install tensorflow==2.12.0 wandb edgeimpulse
from tensorflow import keras
import wandb
import edgeimpulse as ei
# Settings
ei.API_KEY = "ei_dae2..." # Change this to your Edge Impulse API key
labels = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
num_classes = len(labels)
num_epochs = 5
profile_device = 'cortex-m4f-80mhz' # Run ei.model.list_profile_devices() to see available devices
deploy_filename = "my_model_cpp.zip"

# Define experiment hyperparameters - sweep across number of nodes
project_name = "nodes-sweep"
num_nodes_sweep = [8, 16, 32, 64, 128]
# Log in to Weights and Biases (will open a prompt)
wandb.login()
# Load MNIST data
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = keras.utils.normalize(x_train, axis=1)
x_test = keras.utils.normalize(x_test, axis=1)
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
input_shape = x_train[0].shape
# Define experiment - Train and test model, log metrics
def do_experiment(num_nodes):

    # Create W&B project
    run = wandb.init(project=project_name,
                     name=f"{num_nodes}-nodes")

    # Build the model (vary number of nodes in the hidden layer)
    model = keras.Sequential([
        keras.layers.Flatten(),
        keras.layers.Dense(num_nodes, activation='relu', input_shape=input_shape),
        keras.layers.Dense(num_classes, activation='softmax')
    ])

    # Compile the model
    model.compile(optimizer='adam',
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])

    # Train the model
    model.fit(x_train, 
              y_train, 
              epochs=num_epochs)
  
    # Evaluate model
    test_loss, test_accuracy = model.evaluate(x_test, y_test)
    
    # Profile model on target device
    try:
        profile = ei.model.profile(model=model,
                                   device=profile_device)
    except Exception as e:
        print(f"Could not profile: {e}")

    # Log metrics
    if profile.success:
        print("Profiling successful. Logging.")
        wandb.log({
            'num_nodes': num_nodes,
            'test_loss': test_loss,
            'test_accuracy': test_accuracy,
            'profile_ram': profile.model.profile_info.float32.memory.tflite.ram,
            'profile_rom': profile.model.profile_info.float32.memory.tflite.rom,
            'inference_time_ms': profile.model.profile_info.float32.time_per_inference_ms
        })
    else:
        print(f"Profiling unsuccessful. Error: {job_resp.error}")

    # Close run
    wandb.finish()
# Perform the experiments - check your dashboard in WandB!
for num_nodes in num_nodes_sweep:
    do_experiment(num_nodes)
# Build the model 
model = keras.Sequential([
    keras.layers.Flatten(),
    keras.layers.Dense(32, activation='relu', input_shape=input_shape),
    keras.layers.Dense(num_classes, activation='softmax')
])


# Compile the model
model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])


# Train the model
model.fit(x_train, 
          y_train, 
          epochs=5)
# Evaluate model on test set
score = model.evaluate(x_test, y_test, verbose=0)
print(f"Test loss: {score[0]}")
print(f"Test accuracy: {score[1]}")
# List the available profile target devices
ei.model.list_deployment_targets()
['zip',
 'arduino',
 'tinkergen',
 'cubemx',
 'wasm',
 ...
 'runner-linux-aarch64-tda4vm']
# Set model information, such as your list of labels
model_output_type = ei.model.output_type.Classification(labels=labels)

# Create C++ library with trained model
deploy_bytes = None
try:
    
    deploy_bytes = ei.model.deploy(model=model,
                                   model_output_type=model_output_type,
                                   deploy_target='zip')
except Exception as e:
    print(f"Could not deploy: {e}")
    
# Write the downloaded raw bytes to a file
if deploy_bytes:
    with open(deploy_filename, 'wb') as f:
        f.write(deploy_bytes.getvalue())
Weights & Biases
Edge Impulse Python SDK Overview
edgeimpulse.com
wandb.ai
MNIST
wandb.ai
parallel coordinates plot
here
here
Copy API key from Edge Impulse project
Weights and Biases parallel coordinates plot

Synchronizing clinical data with a bucket

In this section, we will show how to synchronize research data with a bucket in your organizational dataset. The goal of this step is to gather data from different sources and sort them to obtain a sorted dataset. We will then validate this dataset in the next section.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

The reference design described in the health reference design PPG-DaLiA DOI 10.24432/C53890 is a publicly available dataset for PPG-based heart rate estimation. This multimodal dataset features physiological and motion data, recorded from both a wrist- and a chest-worn device, of 15 subjects while performing a wide range of activities under close to real-life conditions. The included ECG data provides heart rate ground truth. The included PPG- and 3D-accelerometer data can be used for heart rate estimation, while compensating for motion artefacts. Details can be found in the dataset's readme-file.

File Name

Description

S1_activity.csv

Data containing labels of the activities.

S1_quest.csv

Data from the questionnaire, detailing the subjects' attributes.

ACC.csv

Data from 3-axis accelerometer sensor. The accelerometer is configured to measure acceleration in the range [-2g, 2g]. Therefore, the unit in this file is 1/64g. Data from x, y, and z axis are respectively in the first, second, and third column.

BVP.csv

Blood Volume Pulse (BVP) signal data from photoplethysmograph.

EDA.csv

Electrodermal Activity (EDA) data expressed as microsiemens (μS).

tags.csv

Tags for the data, e.g., Stairs, Soccer, Cycling, Driving, Lunch, Walking, Working, Clean Baseline, No Activity.

HR.csv

Heart Rate (HR) data, as measured by the wearable device. Average heart rate extracted from the BVP signal. The first row is the initial time of the session expressed as a Unix timestamp in UTC. The second row is the sample rate expressed in Hz.

IBI.csv

Inter-beat Interval (IBI) data. Time between individual heartbeats extracted from the BVP signal. No sample rate is needed for this file. The first column is the time (relative to the initial time) of the detected inter-beat interval expressed in seconds (s). The second column is the duration in seconds (s) of the detected inter-beat interval (i.e., the distance in seconds from the previous beat).

TEMP.csv

Data from temperature sensor expressed in degrees on the Celsius (°C) scale.

info.txt

Metadata about the participant, e.g., Age, Gender, Height, Weight, BMI.

You can download the complete set of subject 1 files here:

Download zip

We've mimicked a proper research study, and have split the data up into two locations.

  • Initial subject files (ACC.csv, BVP.csv, EDA.csv, HR.csv, IBI.csv, TEMP.csv, info.txt, S1_activity.csv, tags.csv) live in the company data lake in S3. The data lake uses an internal structure with non-human readable IDs for each participant (e.g. Subject 1 as S1_E4 for anonymized data):

Clinical_Dataset/
├── S1_E4/
│   ├── ACC.csv
│   ├── BVP.csv
│   ├── EDA.csv
│   ├── HR.csv
│   ├── IBI.csv
│   ├── TEMP.csv
│   ├── info.txt
│   ├── S1_activity.csv
│   ├── tags.csv
  • Other files are uploaded by the research partner to an upload portal. The files are prefixed with the subject ID:

├── S2_E4/
│   ├── ACC.csv
│   ├── BVP.csv
│   ├── EDA.csv
│   ├── HR.csv
│   ├── IBI.csv
│   ├── TEMP.csv
│   ├── info.txt
│   ├── S2_activity.csv
│   ├── tags.csv

with the directory S2_E4 indicating that this data is from the second subject in the study, or prefixing the files with S2_ (e.g. S2_activity.csv).

Research data upload portal used by research partners

Anonymizing your data (optional)

This is a manual step that some countries regulations may require, this example is for reference, but not needed or used in this example.

To create the mapping between the study ID, subjects name, and the internal data lake ID we can use a study master sheet. It contains information about all participants, ID mapping, and metadata. E.g.:

Subject     Internal ID     Study date     Age     BMI
Subject_1 S1_E4         2022-01-01     25     22.5
Subject_2 S2_E4         2022-01-02     30     23.5

Notes: This master sheet was made using a Google Sheet but can be anything. All data (data lake, portal, output) are hosted in an Edge Impulse S3 bucket but can be stored anywhere (see below).

Configuring a storage bucket for your dataset

Data is stored in cloud storage buckets that are hosted in your own infrastructure. To configure a new storage bucket, head to your organization, choose Data > Buckets, click Add new bucket, and fill in your access credentials. For additional details, refer to Cloud data storage. Our solution engineers are also here to help you set up your buckets.

Storage buckets overview with a single bucket configured.

About datasets

With the storage bucket in place you can create your first dataset. Datasets in Edge Impulse have three layers:

Datasets in Edge Impulse have three layers:

  1. Dataset: A larger set of data items grouped together.

  2. Data item: An item with metadata and Data file attached.

  3. Data file: The actual files.

Adding research data to your organization

There are three ways of uploading data into your organization. You can either:

  1. Upload data directly to the storage bucket (recommended method). In this case use Add data... > Add dataset from bucket and the data will be discovered automatically.

  2. Upload data through the Edge Impulse API.

  3. Upload the files through the Upload Portals.

Sorter and combiner

Sorter

The sorter is the first step of the research pipeline. Its job is to fetch the data from all locations (here: internal data lake, portal, metadata from study master sheet) and create a research dataset in Edge Impulse. It does this by:

  1. Creating a new structure in S3 like this:

    S1_E4
    |_ info.txt
    |_ s1_activity.csv
    |_ acc.csv
    |_ bvp.csv
    |_ eda.csv
    |_ hr.csv
    |_ ibi.csv
    |_ temp.csv
    |_ tags.csv
    S2_E4
    |_ info.txt
    |_ s2_activity.csv
    |_ acc.csv
    |_ bvp.csv
    |_ eda.csv
    |_ hr.csv
    |_ ibi.csv
    |_ temp.csv
    |_ tags.csv
  2. Syncing the S3 folder with a research dataset in your Edge Impulse organization (like PPG-DaLiA Activity Study 2024).

  3. Updating the metadata with the metadata from the master sheet (Age, BMI, etc...). Read on how to add and sync S3 data

Combiner

With the data sorted we then:

  1. Need to verify that the data is correct (see validate your research data)

  2. Combine the data into a single Parquet file. This is essentially the contract we have for our dataset. By settling on a standard format (strong typed, same column names everywhere) this data is now ready to be used for ML, new algorithm development, etc. Because we also add metadata for each file here we're very quickly building up a valuable R&D datastore.

No required format for data files

There is no required format for data files. You can upload data in any format, whether it's CSV, Parquet, or a proprietary data format.

Parquet is a columnar storage format that is optimized for reading and writing large datasets. It is particularly useful for data that is stored in S3 buckets, as it can be read in parallel and is highly compressed. That is why we are converting the data to Parquet in the transform block code.

See Parquet for more information. or an example in our Create a Transform Block Doc

All these steps can be run through different transformation blocks and executed one after the other using data pipelines.

Clinical dataset overview

Motion recognition + anomaly detection

In this tutorial, you'll use machine learning to build a gesture recognition system that runs on a microcontroller. This is a hard task to solve using rule-based programming, as people don't perform gestures in the exact same way every time. But machine learning can handle these variations with ease. You'll learn how to collect high-frequency data from real sensors, use signal processing to clean up data, build a neural network classifier, and how to deploy your model back to a device. At the end of this tutorial, you'll have a firm understanding of applying machine learning in embedded devices using Edge Impulse.

There is also a video version of this tutorial:

You can view the finished project, including all data, signal processing and machine learning blocks here: .

1. Prerequisites

For this tutorial, you'll need a .

Alternatively, use the either or SDK to collect data from any other development board, or your .

If your device is connected (green dot) under Devices in the studio you can proceed:

Data ingestion

Edge Impulse can ingest data from many sources and any device - including embedded devices that you already have in production. See the documentation for the for more information.

2. Collecting your first data

With your device connected, we can collect some data. In the studio go to the Data acquisition tab. This is the place where all your raw data is stored, and - if your device is connected to the remote management API - where you can start sampling new data.

Under Record new data, select your device, set the label to updown, the sample length to 10000, the sensor to Built-in accelerometer and the frequency to 62.5Hz. This indicates that you want to record data for 10 seconds, and label the recorded data as updown. You can later edit these labels if needed.

After you click Start sampling move your device up and down in a continuous motion. In about twelve seconds the device should complete sampling and upload the file back to Edge Impulse. You see a new line appear under 'Collected data' in the studio. When you click it you now see the raw data graphed out. As the accelerometer on the development board has three axes you'll notice three different lines, one for each axis.

Continuous movement

It's important to do continuous movements as we'll later slice up the data in smaller windows.

Machine learning works best with lots of data, so a single sample won't cut it. Now is the time to start building your own dataset. For example, use the following four classes, and record around 3 minutes of data per class:

  • Idle - just sitting on your desk while you're working.

  • Snake - moving the device over your desk as a snake.

  • Wave - waving the device from left to right.

  • Updown - moving the device up and down.

Variations

Make sure to perform variations on the motions. E.g. do both slow and fast movements and vary the orientation of the board. You'll never know how your user will use the device. It's best to collect samples of ~10 seconds each.

Prebuilt dataset

Alternatively, you can load an example test set that has about ten minutes of data in these classes (but how much fun is that?). See the for more information.

3. Designing an impulse

With the training set in place, you can design an impulse. An impulse takes the raw data, slices it up in smaller windows, uses signal processing blocks to extract features, and then uses a learning block to classify new data. Signal processing blocks always return the same values for the same input and are used to make raw data easier to process, while learning blocks learn from past experiences.

For this tutorial we'll use the 'Spectral analysis' signal processing block. This block applies a filter, performs spectral analysis on the signal, and extracts frequency and spectral power data. Then we'll use a 'Neural Network' learning block, that takes these spectral features and learns to distinguish between the four (idle, snake, wave, updown) classes.

In the studio go to Create impulse, set the window size to 2000 (you can click on the 2000 ms. text to enter an exact value), the window increase to 80, and add the 'Spectral Analysis' and 'Classification (Keras)' blocks. Then click Save impulse.

Configuring the spectral analysis block

To configure your signal processing block, click Spectral features in the menu on the left. This will show you the raw data on top of the screen (you can select other files via the drop down menu), and the results of the signal processing through graphs on the right. For the spectral features block you'll see the following graphs:

  • Filter response - If you have chosen a filter (with non zero order), this will show you the response across frequencies. That is, it will show you how much each frequency will be attenuated.

  • After filter - the signal after applying the filter. This will remove noise.

  • Spectral power - the frequencies at which the signal is repeating (e.g. making one wave movement per second will show a peak at 1 Hz).

See the dedicated page for the pre-processing block.

A good signal processing block will yield similar results for similar data. If you move the sliding window (on the raw data graph) around, the graphs should remain similar. Also, when you switch to another file with the same label, you should see similar graphs, even if the orientation of the device was different.

Bonus exercise: filters

Try to reason about the filter parameters. What does the cut-off frequency control? And what do you see if you switch from a low-pass to a high-pass filter?

Set the filter to low pass with the following parameters:

Once you're happy with the result, click Save parameters. This will send you to the 'Feature generation' screen. In here you'll:

  1. Split all raw data up in windows (based on the window size and the window increase).

  2. Apply the spectral features block on all these windows.

  3. Calculate feature importance. We will use this later to set up the anomaly detection.

Click Generate features to start the process.

Afterward the 'Feature explorer' will load. This is a plot of all the extracted features against all the generated windows. You can use this graph to compare your complete data set. A good rule of thumb is that if you can visually identify some clusters by classes, then the machine learning model will be able to do so as well.

Configuring the neural network

With all data processed it's time to start training a neural network. Neural networks are a set of algorithms, modeled loosely after the human brain, that are designed to recognize patterns. The network that we're training here will take the signal processing data as an input, and try to map this to one of the four classes.

So how does a neural network know what to predict? A neural network consists of layers of neurons, all interconnected, and each connection has a weight. One such neuron in the input layer would be the height of the first peak of the X-axis (from the signal processing block); and one such neuron in the output layer would be wave (one the classes). When defining the neural network all these connections are initialized randomly, and thus the neural network will make random predictions. During training, we then take all the raw data, ask the network to make a prediction, and then make tiny alterations to the weights depending on the outcome (this is why labeling raw data is important).

This way, after a lot of iterations, the neural network learns; and will eventually become much better at predicting new data. Let's try this out by clicking on NN Classifier in the menu.

See the dedicated page for the learning block.

Set 'Number of training cycles' to 1. This will limit training to a single iteration. And then click Start training.

Now change 'Number of training cycles' to 2 and you'll see performance go up. Finally, change 'Number of training cycles' to 30 and let the training finish.

You've just trained your first neural networks!

100% accuracy

You might end up with 100% accuracy after training for 100 training cycles. This is not necessarily a good thing, as it might be a sign that the neural network is too tuned for the specific test set and might perform poorly on new data (overfitting). The best way to reduce this is by adding more data or reducing the learning rate.

4. Classifying new data

From the statistics in the previous step we know that the model works against our training data, but how well would the network perform on new data? Click on Live classification in the menu to find out. Your device should (just like in step 2) show as online under 'Classify new data'. Set the 'Sample length' to 10000 (10 seconds), click Start sampling and start doing movements. Afterward, you'll get a full report on what the network thought you did.

If the network performed great, fantastic! But what if it performed poorly? There could be a variety of reasons, but the most common ones are:

  1. There is not enough data. Neural networks need to learn patterns in data sets, and the more data the better.

  2. The data does not look like other data the network has seen before. This is common when someone uses the device in a way that you didn't add to the test set. You can add the current file to the test set by clicking ⋮, then selecting Move to training set. Make sure to update the label under 'Data acquisition' before training.

  3. The model has not been trained enough. Up the number of epochs to 200 and see if performance increases (the classified file is stored, and you can load it through 'Classify existing validation sample').

  4. The model is overfitting and thus performs poorly on new data. Try reducing the learning rate or add more data.

  5. The neural network architecture is not a great fit for your data. Play with the number of layers and neurons and see if performance improves.

As you see there is still a lot of trial and error when building neural networks, but we hope the visualizations help a lot. You can also run the network against the complete validation set through 'Model validation'. Think of the model validation page as a set of unit tests for your model!

With a working model in place, we can look at places where our current impulse performs poorly.

5. Anomaly detection

Neural networks are great, but they have one big flaw. They're terrible at dealing with data they have never seen before (like a new gesture). Neural networks cannot judge this, as they are only aware of the training data. If you give it something unlike anything it has seen before it'll still classify as one of the four classes.

Let's look at how this works in practice. Go to 'Live classification' and record some new data, but now vividly shake your device. Take a look and see how the network will predict something regardless.

So, how can we do better? If you look at the feature explorer, you should be able to visually separate the classified data from the training data. We can use this to our advantage by training a new (second) network that creates clusters around data that we have seen before, and compares incoming data against these clusters. If the distance from a cluster is too large you can flag the sample as an anomaly, and not trust the neural network.

To add this block go to Create impulse, click Add learning block, and select 'Anomaly Detection (K-Means)'. Then click Save impulse.

To configure the clustering model click on Anomaly detection in the menu. Here we need to specify:

  • The number of clusters. Here use 32.

  • The axes that we want to select during clustering. Click on the Select suggested axes button to harness the results of the output. Alternatively, the data separates well on the accX RMS, accY RMS and accZ RMS axes, you can also include these axes.

See the dedicated page for the learning block. We also provide the learning block that is compatible with this tutorial.

Click Start training to generate the clusters. You can load existing validation samples into the anomaly explorer with the dropdown menu.

Axes

The anomaly explorer only plots two axes at the same time. Under 'average axis distance' you see how far away from each axis the validation sample is. Use the dropdown menu's to change axes.

If you now go back to 'Live classification' and load your last sample, it should now have tagged everything as anomaly. This is a great example where signal processing (to extract features), neural networks (for classification) and clustering algorithms (for anomaly detection) can work together.

6. Deploying back to device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the signal processing code, neural network weights, and classification code - up in a single C++ library that you can include in your embedded software.

Mobile phone

Your mobile phone can build and download the compiled impulse directly from the mobile client. See 'Deploying back to device' on the page.

To export your model, click on Deployment in the menu. Then under 'Build firmware' select your development board, and click Build. This will export the impulse, and build a binary that will run on your development board in a single step. After building is completed you'll get prompted to download a binary. Save this on your computer.

Flashing the device

When you click the Build button, you'll see a pop-up with text and video instructions on how to deploy the binary to your particular device. Follow these instructions. Once you are done, we are ready to test your impulse out.

Running the model on the device

We can connect to the board's newly flashed firmware over serial. Open a terminal and run:

Serial daemon

If the device is not connected over WiFi, but instead connected via the Edge Impulse serial daemon, you'll need stop the daemon. Only one application can connect to the development board at a time.

This will sample data from the sensor, run the signal processing code, and then classify the data:

Continuous movement

We trained a model to detect continuous movement in 2 second intervals. Thus, changing your movement while sampling will yield incorrect results. Make sure you've started your movement when 'Sampling...' gets printed. In between sampling, you have two seconds to switch movements.

To run the continuous sampling, run the following command:

Victory! You've now built your first on-device machine learning model.

7. Conclusion

Congratulations! You have used Edge Impulse to train a machine learning model capable of recognizing your gestures and understand how you can build models that classify sensor data or find anomalies. Now that you've trained your model you can integrate your impulse in the firmware of your own embedded device, see . There are examples for Mbed OS, Arduino, STM32CubeIDE, and any other target that supports a C++ compiler.

Or if you're interested in more, see our tutorials on or . If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

Building custom processing blocks

Extracting meaningful features from your data is crucial to building small and reliable machine learning models, and in Edge Impulse this is done through processing blocks. We ship a number of processing blocks for common sensor data (such as vibration and audio), but they might not be suitable for all applications. Perhaps you have a very specific sensor, want to apply custom filters, or are implementing the latest research in digital signal processing. In this tutorial you'll learn how to support these use cases by adding custom processing blocks to the studio.

There is also a complete video covering how to implement your custom DSP block:

Prerequisites

Make sure you follow the tutorial, and have a trained impulse.

Development flow

This tutorial shows you the development flow of building custom processing blocks, and requires you to run the processing block on your own machine or server. Enterprise customers can share processing blocks within their organization, and run these on our infrastructure. See for more details.

1. Building your first custom processing block

Processing blocks take data and configuration parameters in, and return features and visualizations like graphs or images. To communicate to custom processing blocks, Edge Impulse studio will make HTTP calls to the block, and then use the response both in the UI, while generating features, or when training a machine learning model. Thus, to load a custom processing block we'll need to run a small server that responds to these HTTP calls. You can write this in any language, but we have created in Python. To load this example, open a terminal and run:

This creates a copy of the example project locally. Then, you can run the example either through Docker or locally via:

Docker

Locally

Then go to and you should be shown some information about the block.

Exposing the processing block to the world

As this block is running locally the studio cannot reach the block. To resolve this we can use which can make a local port accessible from a public URL. After you've finished development you can move the processing block to a server with a publicly accessible address (or run it on our infrastructure through your enterprise account). To set up a tunnel:

  1. Sign up for .

  2. Install the ngrok binary for your platform.

  3. Get a URL to access the processing block from the outside world via:

This yields a public URL for your block under Forwarding. Note down the address that includes https://.

Adding the custom block to Edge Impulse

Now that the custom processing block was created, and you've made it accessible to the outside world, you can add this block to Edge Impulse. In a project, go to Create Impulse, click Add a processing block, choose Add custom block (in the bottom left corner of the modal), and paste in the public URL of the block:

After you click Add block the block will show like any other processing block.

Add a learning bloc, then click Save impulse to store the impulse.

2. Adding configuration options

Processing blocks have configuration options which are rendered on the block parameter page. These could be filter configurations, scaling options, or control which visualizations are loaded. These options are defined in the parameters.json file. Let's add an option to smooth raw data. Open example-custom-processing-block-python/parameters.json and add a new section under parameters:

Then, open example-custom-processing-block-python/dsp.py and replace its contents with:

Restart the Python script, and then click Custom block in the studio (in the navigation bar). You now have a new option 'Smooth'. Every time an option changes we'll re-run the block, but as we have not written any code to respond to these changes nothing will happen.

2.1 Customizing parameters

For the full documentation on customizing parameters, and a list of all configuration options; see .

3. Implementing smoothing and drawing graphs

To show the user what is happening we can also draw visuals in the processing block. Right now we support graphs (linear and logarithmic) and arbitrary images. By showing a graph of the smoothed sample we can quickly identify what effect the smooth option has on the raw signal. Open dsp.py and replace the content with the following script. It contains a very basic smoothing algorithm and draws a graph:

Restart the script, and click the Smooth toggle to observe the difference. Congratulations! You have just created your first custom processing block.

3.1 Adding features to labels

If you extract set features from the signal, like the mean, that you that return, you can also label these features. These labels will be used in the feature explorer. To do so, add a labels array that contains strings that map back to the features you return (labels and features should have the same length).

4. Other type of graphs

In the previous step we drew a linear graph, but you can also draw logarithmic graphs or even full images. This is done through the type parameter:

4.1 Logarithmic graphs

This draws a graph with a logarithmic scale:

4.2 Images

To show an image you should return the base64 encoded image and its MIME type. Here's how you draw a small PNG image:

4.3 Dimensionality reduction

If you output high-dimensional data (like a spectrogram or an image) you can enable dimensionality reduction for the feature explorer. This will run UMAP over the data to compress the features into three dimensions. To do so, set:

On the info object in parameters.json.

4.4 Full documentation

For all options that you can return in a graph, see the return types in the API documentation.

5. Running on device

Your custom block behaves exactly the same as any of the built-in blocks. You can process all your data, train neural networks or anomaly blocks, and validate that your model works.

However, we cannot automatically generate optimized native code for the block, like we do for built-in processing blocks, but we try to help you write this code as much as possible.

Export as a C++ Library:

  • In the Edge Impulse platform, export your project as a C++ library.

  • Choose the model type that suits your target device (quantized vs. float32).

Forward Declaration:

You don't need to add this part, it is automatically generated!

In the model-parameters/model_variables.h file of the exported C++ library, you can see a forward declaration for the custom DSP block you created.

For example:

The name of that function comes from the cppType field in your custom DSP parameter.json. It takes your {cppType} and generates the following extract_{cppType}_features function.

Implement the Custom DSP Block:

In the main.cpp file of the C++ library, implement the extract_my_preprocessing_features block. This block should:

  1. Call into the Edge Impulse SDK to generate features.

  2. Execute the rest of the DSP block, including neural network inference.

For examples, have a look at our official DSP blocks implementations in our

Also, please have a look at the video on the top of this page (around minute 25) where Jan explains how to implement your custom DSP block with your C++ library.

Compile and Run the App

  • Copy a test sample's raw features into the features[] array in source/main.cpp

  • Enter make -j in this directory to compile the project. If you encounter any OOM memory error try make -j4 (replace 4 with the number of cores available)

  • Enter ./build/app to run the application

  • Compare the output predictions to the predictions of the test sample in the Edge Impulse Studio.

6. Other resources

Blog post:

7. Conclusion

With good feature extraction you can make your machine learning models smaller and more reliable, which are both very important when you want to deploy your model on embedded devices. With custom processing blocks you can now develop new feature extraction pipelines straight from Edge Impulse. Whether you're following the latest research, want to implement proprietary algorithms, or are just exploring data.

For inspiration we have published all our own blocks here: . If you've made an interesting block that you think is valuable for the community, please let us know on the or by opening a pull request. We'd be happy to help write efficient native code for the block, and then publish it as a standard block!

Parameters.json format

This is the format for the parameters.json file:

\

Sensor fusion using embeddings

Sensor fusion is about combining data from various sensors to gain a more comprehensive understanding of your environment. In this tutorial, we will demonstrate sensor fusion by bringing together high-dimensional audio or image data with time-series sensor data. This combination allows you to extract deeper insights from your sensor data.

This is an advanced tutorial where you will need to parse your dataset to create multi-sensor data samples, train several Edge Impulse project in order to extract the embeddings from the tflite models, create and, finally, modify the C++ inferencing SDK.

If you are looking for a more beginner-level tutorial, please head to the tutorial.

Concepts

Multi-impulse vs multi-model vs sensor fusion

Running multi-impulse refers to running two separate projects (different data, different DSP blocks and different models) on the same target. It will require modifying some files in the EI-generated SDKs.

Running multi-model refers to running two different models (same data, same DSP block but different tflite models) on the same target. See how to run a motion classifier model and an anomaly detection model on the same device in .

Sensor fusion refers to the process of combining data from different types of sensors to give more information to the neural network. To extract meaningful information from this data, you can use the same DSP block, multiples DSP blocks, or use neural networks embeddings like we will see in this tutorial.

Also, see this video (starting min 13):

The Challenge of Handling Multiple Data Types

When you have data coming from multiple sources, such as a microphone capturing audio, a camera capturing images, and sensors collecting time-series data. Integrating these diverse data types can be tricky and conventional methods fall short.

The Limitations of Default Workflows

With the standard workflow, if you have data streams from various sources, you might want to create separate DSP blocks for each data type. For instance, if you're dealing with audio data from microphones, image data from cameras, and time-series sensor data from accelerometers, you could create separate DSP blocks for each. For example:

  • A spectrogram-based DSP block for audio data

  • An image DSP block for image data

  • A spectral analysis block for time-series sensor data

This approach initially seems logical but comes with limitations:

When using separate DSP blocks, you're constrained in your choice of neural networks. The features extracted from each data type are fundamentally different. For example, a pixel in an image or an image's spectrogram and a data point from an accelerometer's time-series data have distinct characteristics. This incompatibility makes it challenging to use a convolutional neural network (CNN) that is typically effective for image data or spectrogram. As a result, fully connected networks may be your only option, which are not ideal for audio or image data.

The Role of Embeddings

To bypass the limitation stated above, you may consider using neural networks embeddings. In essence, embeddings are compact, meaningful representations of your data, learned by a neural network.

Embeddings are super powerful, we use them for various features of Edge Impulse, such as the and in this advanced sensor fusion tutorial.

While training the neural network, the model try to find the mathematical formula that best maps the input to the output. This is done by tweaking each neuron (each neuron is a parameter in our formula). The interesting part is that each layer of the neural network will start acting like a feature extracting step but highly tuned for your specific data.

Finally, instead of having a classifier for last layer (usually a softmax layer), we cut the neural network somewhere at the end and we obtained the embeddings.

Thus, we can consider the embeddings as learnt features and we will pass these "features" to the final Impulse:

Sensor Fusion Workflow using Embeddings

Here's how we approach advanced sensor fusion with Edge Impulse.

In this workflow, we will show how to perform sensor fusion using both audio data and accelerometer data to classify different stages of a grinding coffee machine (grind, idle, pump and extract). First, we are going to use a spectrogram DSP block and a NN classifier using two dense network. This first impulse will then be used to generate the embeddings and will be made available in a custom DSP block. Finally, we are going to train a fully connected layer using features coming from both the generated embeddings and a spectral feature DSP block.

We have develop two Edge Impulse public projects, one publicly available dataset and a Github repository containing the source code to help you follow the steps:

  • Dataset:

  • Edge Impulse project 1 (used to generate the embeddings):

  • Edge Impulse project 2 (final impulse):

  • Github repository containing the source code:

Please note that with a few changes, you will be able to change the sensor type (audio to images) or the first pre-processing method (spectrogram to MFE/MFCC).

1. Prepare your dataset

The first step is to have input data samples that contain both sensors. In Edge Impulse studio, you can easily visualize time-series data, like audio and accelerometer data.

Note: it is not trivial to group together images and time-series. Our core-engineering team is working on improving this workflow. In the meantime, as a workaround, you can encode your image as time-series with one axis per channel (red, green, blue) plus the sensor:

2. Training Edge Impulse projects for each high-dimensional sensor data

Train separate projects for high dimensional data (audio or image data). Each project contains both a DSP block and a trained neural network.

See

3. Generate the Embeddings using the exported C++ inferencing SDK

Clone this repository:

Download the generated Impulse to extract the embeddings, which encapsulate distilled knowledge about their respective data types.

Download Model: From the project , download the TensorFlow SavedModel (saved_model). Extract the save_model directory and place it under the /input repository.

Download Test Data: From the same dashboard, download the test or train data NPY file (input.npy). Place this numpy array file under the /input repository. This will allow us to generate a quantized version of the tflite embeddings. Ideally choose the test data if you have some data available.

Generate the embeddings:

This will cut off the last layer of the neural network and convert it to LiteRT (previously Tensorflow Lite) (TFLite) format. You can follow the process outlined in the saved_model_to_embeddings.py script for this conversion for a better understanding.

4. Encapsulate the Embeddings in a Custom DSP Block

To make sensor fusion work seamlessly, Edge Impulse enables you to create custom DSP blocks. These blocks combine the necessary spectrogram/image-processing and neural network components for each data type.

Custom DSP Block Configuration: In the DSP block, perform two key operations as specified in the dsp.py script:

  • Run the DSP step with fixed parameters.

  • Run the neural network.

Replace this following lines in dsp-blocks/features-from-audio-embeddings/dsp.py to match your DSP configuration:

If you want to use another DSP block than the spectrogram one, all the source code of the available DSP code can be found in this public repository:

Return Neural Network Embeddings: The DSP block should be configured to return the neural network embeddings, as opposed to the final classification result.

Implement get_tflite_implementation: Ensure that the get_tflite_implementation function returns the TFLite model. Note that the on-device implementation will not be correct initially when generating the C++ library, as only the neural network part is compiled. We will fix this in the final exported C++ Library.

Now publish your new custom DSP block.

Fill the necessary information and push your block:

During development, it might be easier to host the block locally so you can make changes, see

5. Create a New Impulse

Multiple DSP Blocks: Create a new impulse with three DSP blocks and a classifier block. The routing should be as follows:

  • Audio data routed through the custom block.

  • Sensor data routed through spectral analysis.

See

Training the Model: Train the model within the new impulse, using a fully-connected network.

6. Export and Modify the C++ Library

Export as a C++ Library:

  • In the Edge Impulse platform, export your project as a C++ library.

  • Choose the model type that suits your target device (quantized vs. float32).

  • Make sure to select EON compiler option

  • Copy the exported C++ library to the example-cpp folder for easy access.

Add a Forward Declaration:

  • In the model-parameters/model_variables.h file of the exported C++ library, add a forward declaration for the custom DSP block you created.

For example:

And change &extract_tflite_eon_features into &custom_sensor_fusion_features in the ei_dsp_blocks object.

Implement the Custom DSP Block:

In the main.cpp file of the C++ library, implement the custom_sensor_fusion_features block. This block should:

  1. Call into the Edge Impulse SDK to generate features.

  2. Execute the rest of the DSP block, including neural network inference.

For example, see the main.cpp file in the

7. Compile and run the app

  • Copy a test sample's raw features into the features[] array in source/main.cpp

  • Enter make -j in this directory to compile the project. If you encounter any OOM memory error try make -j4 (replace 4 with the number of cores available)

  • Enter ./build/app to run the application

  • Compare the output predictions to the predictions of the test sample in the Edge Impulse Studio.

Note that if you are using the quantized version of the model, you may encounter a slight difference between the Studio Live Classification page and the above results, the float32 model however should give you the same results.

Congratulations on successfully completing this advanced tutorial. You have been through the complex process of integrating high-dimensional audio or image data with time-series sensor data, employing advanced techniques like custom DSP blocks, neural network embeddings, and modifications to the C++ inferencing SDK. Also, note that you can simplify this workflow using to generate the custom DSP block with the embeddings.

If you are interested in using it for an enterprise project, please sign up for our FREE and our solution engineers can work with you on the integration.

with Espressif IDF

Introduction

This page is part of the tutorial series. If you haven't read the introduction yet, we recommend you to do so .

In this tutorial, we'll guide you through deploying updated impulses over-the-air (OTA) to the ESP32 using Edge Impulse. We'll build on Espressif's end-to-end OTA firmware update workflow, incorporating Edge Impulse's API to check for updates and download the latest build.

We will modify the Edge Impulse C++ example for ESP32, and combine it with the OTA example from the ESP IDF repository. This will allow us to check for updates to your project on the server side of the IDF example and download the latest build. We will then modify the C++ example to incorporate the OTA functionality and update the device with the latest build. Finally we will add calls back to our ingestion service to monitor the performance of the updated impulse, and gather data. Closing the loop on our active learning cycle.

Let's get started!

Key Features of Espressif OTA Updates:

Prerequisites

  • Edge Impulse Account: If you haven't got one, .

  • Trained Impulse: If you're new, follow one of our

  • Knowledge of the ESP IDF development framework and C++ for ESP32

  • Installation of required software as detailed in the tutorial

Preparation

Begin by setting up your device for OTA updates following Espressif's OTA firmware update workflow. Use the built binary from the C++ example and modify it to incorporate OTA functionality.

Let's get started!

On your Espressif ESP-EYE (ESP32) development board

We created an example repository which contains a small application for Espressif ESP32, which takes the raw features as an argument, and prints out the final classification. Download the application as a .zip, or import this repository using Git:

We will need to add a few libraries to the project to facilitate the OTA update process. These will be taken from the ESP32 OTA example and are already included in the example project.

ESP IDF OTA

We are going to use the ESP IDF OTA example as a starting point. Running a python server to check for updates and download the latest build from edge impulse. This detemines if there is a new build available and downloads it to the device.You could add more advanced checking for model performance and versioning to this process. This example will only download the latest build if there is a new build available based on date to save lengthy code examples.

This example is available in the ESP IDF repository. You can find the example in:

Starting with the device side code, we will need to add a few libraries to the project to facilitate the OTA update process. These are taken from the ESP32 OTA example and are already included in the example project.

Step 1: Copy the IDF OTA example and clone a fresh version of the edge impulse example C++ inferencing repository

Now we will set up the Server Side components of the OTA update process

1. Python Server Side OTA

Modify the ESP OTA example python server to check for updates to your project, we will use the date to determine when a new update should be performed to keep things simple. You could add more advanced checking for model performance and versioning to this process. This example will only download the latest build if there is a new build available based on date to save lengthy code examples.

2.

Building the firmware to deploy to our device in the field. We will use the IDF build system to build the firmware. This would be automated but again to save time and complexity we will do this manually.

3. Updating the Device

Compare the model's timestamp or hash with the stored version. If it's different or newer, call the download_model() function.

4. Monitoring and Repeating the Process

Monitor the device to ensure the new impulse performs as expected and repeat the update process as needed.

Here we would make calls back to our ingestion service to monitor the performance of the updated impulse, and gather data. Closing the loop on our active learning cycle.

Device Side of the OTA Update Process

Now lets modify the C++ example to incorporate the device side of the OTA functionality and update our edge impulse project with the device side data. We will add the OTA functionality to the C++ example and update the device with the latest build.

Step 1: Include Necessary Headers

Include the necessary headers and components in your main ESP32 C++ based edge impulse project. There are a number of components that are required for the OTA update process:

Components

1. Non-Volatile Storage (NVS)

NVS is utilized to persistently store data like configuration settings, WiFi credentials, or firmware update times, ensuring retention across reboots.

2. HTTP Client

This library facilitates HTTP requests to the server for checking and retrieving new firmware updates.

3. OTA Operations

These headers aid in executing OTA operations, including writing new firmware to the flash and switching boot partitions.

4. FreeRTOS Task

FreeRTOS ensures OTA updates are conducted in a separate task, preventing blockage of other tasks and maintaining system operations during the update.

Lets start by adding the necessary headers to the main.cpp file in the edge impulse project.

Step 3: OTA Task Implement the OTA task that will handle the OTA update process. Connecting to the python server we created in the beginning of this tutorial. Which is checking for updates to your project

In the simple_ota_example_task function, you'll need to replace http://192.168.1.10/your-ota-firmware.bin with the actual URL where your firmware binary is hosted.

Step 4: App Main Here’s how the app_main function should look like:

Step 5: Set the target and configuration Make sure you have the partition table set up correctly to support OTA. In the IDF Menu Config, An OTA data partition (type data, subtype ota) must be included in the Partition Table of any project which uses the OTA functions.

Step 6: Compile and Flash You should compile the firmware and flash it to your ESP32 using the IDF build system commands.

Note: Before you run this code, make sure to replace http://192.168.1.10.com/your-ota-firmware.bin with the actual URL where your new firmware binary is hosted. Make sure that your ESP32 is connected to the internet to access this URL. Also, always test OTA functionality thoroughly before deploying it in a production environment.

Conclusion

This tutorial provides a basic guide for implementing OTA updates on Espressif ESP-EYE (ESP32) with Edge Impulse. It can be extended to include more advanced checking, utilizing more of espressif OTA functionality, and extending the python server to check for model performance and versioning. Happy coding!

Custom processing blocks

Custom processing blocks are a way to extend the capabilities of Edge Impulse beyond the built into the platform. If none of the existing blocks created by Edge Impulse fit your needs, you can create custom processing blocks to implement your own feature generation algorithms for unique project requirements.

Ready to dive in and start building? Jump to the !

Hosting custom processing blocks in Edge Impulse is only available on the Enterprise plan

Hosting a custom processing block in the Edge Impulse infrastructure, and making it available to everyone in your organization, is only available on the Enterprise plan. Other developers can host their custom processing block themselves and expose it to projects. See in this document.

Block structure

The processing block structure is shown below. A key difference for processing blocks versus other types of blocks is that they implement an HTTP server within the application. Please see the overview page for more details.

Block interface

Processing blocks are expected to implement an HTTP server to handle requests. The sections below define the required and optional inputs (requests) and the expected outputs (responses) for custom processing blocks.

Inputs

Information will be provided to your custom processing block through the request headers and body.

Requests

Method
Path
Description

Request headers

Header
Passed
Description

Request body

The request body adheres to the following interfaces for the POST methods. GET methods do not have a request body.

The data samples that need to be processed by your script to generate features are provided as arrays in the features property.

The axes property provides the names of the signals for the data sample. For example, if this was an accelerometer data sample, the axes could be [ 'accX', 'accY', 'accZ' ]. These names could be mapped to other names in the named_axes property.

The parameters defined in your parameters.json file will be passed to your block in the params property. If your parameter names contain dashes, these are replaced with underscores before being added to the request body. For example, a processing block parameter named custom-processing-param is passed as custom_processing_param. Please refer to the documentation for further details about creating this file, parameter options available, and examples.

Outputs

The expected response from the HTTP server in your custom processing block varies depending on the type of request.

GET methods

GET /:

A plain text response with some information about the block. For example, the response could be the block name and author.

GET /parameters:

The parameters file returned as a JSON object.

POST methods

The POST response bodies are expected to adhere to the following interfaces.

The features property is where you return the features that were generated by processing the data sample(s).

The labels property can be used return the names of the features you generated. For example, if you calculated the average, maximum, and minimum values of the signal, the labels could be [ 'Average', 'Maximum', 'Minimum' ]. These labels will be used for the feature explorer.

Adding visualizations

The results of generating features can be shown in Studio through graphs and the .

Graphs

When configuring parameters for a processing block in Studio, a preview of the feature generation results for a single sample is shown. This preview can include displaying graphs. These are the graphs that you define and return in the graphs property of the response body for the POST /run method. Graphs should be created in your feature generation script conditionally based on the draw_graphs property in the request body. See the interface for a graphs object in the section above.

Graphs can be of different types: linear, logarithmic, or an image. The type of graph is controlled by the type property of a graph object.

Graph
Type value

Feature explorer

The results of generating features on all samples can be shown in the feature explorer. If you output high-dimensional data, you can enable dimensionality reduction for the feature explorer. This will run UMAP over the data to compress the features into two dimensions. To do so, you can set the visualization property in your parameters.json file to dimensionalityReduction.

Initializing the block

Testing the block locally

The most convenient way to test your custom processing block before pushing it to Edge Impulse is to host it locally and then expose it to the internet so that it can be accessed by Studio. There are two ways to achieve this. You will need to have Docker and installed on your machine for either approach.

With blocks runner

For the first method, you can use the CLI edge-impulse-blocks runner tool. See for additional details.

The port you publish for your Docker container can be configured in the parameters.json file. The blocks runner will also look for the EXPOSE instruction in your Dockerfile and publish that port for you if you wish to override the default port.

Note down the public URL that is returned in the terminal. You can use this URL to add the block to an impulse in a Studio project.

With ngrok and Docker

For the second method, you can run Docker and ngrok directly instead of using the CLI blocks runner tool. First, you can build the Docker image and run the container.

Then, after signing up for and installing ngrok, you can use their CLI to create a public forwarding URL. Note down the https:// forwarding address in the response. You can use this URL to add the block to an impulse in a Studio project.

Viewing in Studio

With a public URL for your custom processing block, you can go into your project and add a processing block to your impulse. When the processing block selection modal pops up, go to the bottom left corner and click the Add custom block button. In the next modal that pops up, enter your forwarding URL from above and save. The block can now be used in your project and you will be able to view the processing results, including any visualizations you have created.

Pushing the block to Edge Impulse

Using the block in a project

Running on device

One caveat for custom processing blocks is that Edge Impulse cannot automatically generate optimized code to run on-device as is done with processing blocks built into the platform. This code will need to be written by you. To help you get started, the structure is provided for you.

After exporting the C++ library from the Deployment page in Studio, you can see that a forward declaration for your custom processing block will have been created for you in the model-parameters/model_variables.h file.

The name for the function comes from the cppType property in your parameters.json file.

You will need to implement this function in the main.cpp file of the C++ library. Example implementations for the processing blocks built into Edge Impulse can be found in the .

Examples

Edge Impulse has developed several processing blocks that are built into the platform. The code for these blocks can be found in a public repository under the . See below. Additional examples can also be found in the Edge Impulse account. These repository names typically follow the convention of example-custom-processing-block-<description>. As such, they can be found by searching the repositories for example-custom-processing.

Below are direct links to some examples:

Troubleshooting

Additional resources

git clone https://github.com/edgeimpulse/example-standalone-inferencing-espressif-esp32
$IDF_PATH/examples/system/ota
  mkdir ~/ota-esp32
  cd ~/ota-esp32
  cp -r $IDF_PATH/examples/system/ota .
  git clone https://github.com/edgeimpulse/example-standalone-inferencing-espressif-esp32
cd ~/ota-esp32/ota
import requests
import json
import os
import sys

API_KEY = 'your-edge-impulse-api-key'
PROJECT_ID = 'your-project-id'
MODEL_PATH = 'path_to_your_local_model'
TIMESTAMP_PATH = 'last_modification_date.txt'

def get_last_modification_date():
    try:
        url = f'https://studio.edgeimpulse.com/v1/api/{PROJECT_ID}/last-modification-date'
        headers = {'x-api-key': API_KEY}

        response = requests.get(url, headers=headers)
        response.raise_for_status()

        data = response.json()
        return data['lastModificationDate']
    except Exception as e:
        print(f"Failed to get last modification date: {e}")
        return None

def download_model():
    try:
        url = f'https://studio.edgeimpulse.com/v1/api/{PROJECT_ID}/deployment/download'
        headers = {'x-api-key': API_KEY}

        response = requests.get(url, headers=headers)
        response.raise_for_status()

        with open(MODEL_PATH, 'wb') as file:
            file.write(response.content)
        print("Model downloaded successfully.")
    except Exception as e:
        print(f"Failed to download the model: {e}")

def save_timestamp(timestamp):
    try:
        with open(TIMESTAMP_PATH, 'w') as file:
            file.write(timestamp)
    except Exception as e:
        print(f"Failed to save timestamp: {e}")

def get_stored_timestamp():
    try:
        if os.path.exists(TIMESTAMP_PATH):
            with open(TIMESTAMP_PATH, 'r') as file:
                return file.read().strip()
    except Exception as e:
        print(f"Failed to read stored timestamp: {e}")
    return None

# get the stored timestamp
stored_timestamp = get_stored_timestamp()

# check for recent modifications
last_modification_date = get_last_modification_date()

# compare and download if newer
if last_modification_date and last_modification_date != stored_timestamp:
    print("New model available. Downloading...")
    download_model()

    # update the stored timestamp
    save_timestamp(last_modification_date)
else:
    print("No new model available.")


cd ~/ota-esp32/example-standalone-inferencing-espressif-esp32
idf.py set-target esp32
idf.py build
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_http_client.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_system.h>
#include <esp_log.h>
#include <esp_ota_ops.h>
#include <esp_http_client.h>
#include <esp_flash_partitions.h>
#include <esp_partition.h>
#include <nvs.h>
#include <nvs_flash.h>
void simple_ota_example_task(void *pvParameter)
{
  esp_http_client_config_t config = {
      .url = "http://192.168.1.10/your-ota-firmware.bin",
  };
  
  esp_err_t ret = esp_https_ota(&config);
  if (ret == ESP_OK) {
      esp_restart();
  } else {
      ESP_LOGE("OTA", "Firmware upgrade failed");
  }

  while (1) {
      vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}
void app_main(void)
{
  // Initialize NVS 
  esp_err_t ret = nvs_flash_init();
  if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
  }
  ESP_ERROR_CHECK(ret);

  xTaskCreate(&simple_ota_example_task, "ota_example_task", 8192, NULL, 5, NULL);
}
cd ~/ota-esp32
idf.py set-target esp32
idf.py menuconfig
idf.py build
idf.py -p PORT flash
Lifecycle Management with Edge Impulse
here
sign up here
end-to-end tutorials

GET

/

Requesting general information about the processing block.

GET

/parameters

Requesting the parameters.json file for the block.

POST

/run

Requesting features be generated for a single data sample.

POST

/batch

Requesting features be generated for multiple data samples.

x-ei-project-id

Conditional

Provided with GET /run or GET /batch requests. The ID of the project.

x-ei-sample-id

Conditional

Provided with GET /run request. The ID of the sample to be processed.

x-ei-sample-ids

Conditional

Provided with GET /batch request. A list of IDs of data samples to be processed.

interface DSPRequestBody {
    features: number[];
    axes: string[];
    sampling_freq: number;
    draw_graphs: boolean;
    project_id: number;
    implementation_version: number;
    params: { [k: string]: string | number | boolean | number[] | string[] | null };
    calculate_performance: boolean;
    named_axes: { [k: string]: string | false } | false | undefined;
}
interface DSPBatchRequestBody {
    features: number[][];
    axes: string[];
    sampling_freq: number;
    implementation_version: number;
    params: { [k: string]: string | number | boolean | number[] | string[] | null };
    state: string;
    named_axes: { [k: string]: string | false } | false | undefined;
}
Custom processing block by Awesome Developer.
{
    success: boolean,
    error?: string
} & DSPRunResponse

interface DSPRunResponse {
    features: number[];
    graphs: DSPRunGraph[];
    labels: string[] | undefined;
    fft_used: number[] | undefined;
    performance: {
        error: string | undefined | null,
        result: {
            time_ms: number
            memory: number
        }
    } | undefined;
    benchmark_fw_hash: string;
    output_config: DSPFeatureMetadataOutput;
    state_string: string | undefined;
}

interface DSPRunGraph {
    name: string;
    image?: string;
    imageMimeType?: string;
    X?: { [k: string]: number[] };
    y?: number[];
    suggestedYMin: number | undefined;
    suggestedYMax: number | undefined;
    suggestedXMin: number | undefined;
    suggestedXMax: number | undefined;
    type: string;
    lineWidth: number | undefined;
    smoothing: boolean;
    axisLabels?: { X: string; y: string; };
    highlights?: { [k: string]: number[] };
}

type DSPFeatureMetadataOutput = {
    type: 'image',
    shape: { width: number, height: number, channels: number, frames?: number },
    axes?: number
} | {
    type: 'spectrogram',
    shape: { width: number, height: number },
    axes?: number
} | {
    type: 'flat',
    shape: { width: number },
    axes?: number
};
{
    success: boolean,
    error?: string
} & DSPRunResponse

interface DSPBatchRunResponse {
    features: number[][];
    labels: string[];
    frequency: number | undefined;
    fft_used: number[] | undefined;
    output_config: DSPFeatureMetadataOutput;
    state_string: string | undefined;
    state: string | undefined;
}

type DSPFeatureMetadataOutput = {
    type: 'image',
    shape: { width: number, height: number, channels: number, frames?: number },
    axes?: number
} | {
    type: 'spectrogram',
    shape: { width: number, height: number },
    axes?: number
} | {
    type: 'flat',
    shape: { width: number },
    axes?: number
};

Linear

linear

Logarithmic

logarithmic

Image

image

 edge-impulse-blocks runner
docker build -t custom-processing-block .
docker run -p 4446:4446 -it --rm custom-processing-block
ngrok http 4446
Session Status                online
Account                       Edge Impulse (Plan: Free)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://4d48dca5.ngrok.io -> http://localhost:4446
Forwarding                    https://4d48dca5.ngrok.io -> http://localhost:4446
int extract_my_preprocessing_features(signal_t *signal, matrix_t *output_matrix, void *config_ptr, const float frequency);
extract_{cppType}_features(...)
processing blocks
examples
Testing the block locally
custom blocks
parameters.json
feature explorer
Outputs
ngrok
Block runner
C++ inferencing SDK
Edge Impulse GitHub account
processing-blocks
example-custom-processing-block-python
Custom blocks
Processing blocks
edge-impulse-blocks
parameters.json
Building a custom processing block
Custom processing block structure
Adding a custom processing block using an ngrok forwarding URL

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the custom blocks overview page. Please refer to that documentation for details.

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the custom blocks overview page. Please refer to that documentation for details.

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

No common issues have been identified thus far. If you encounter an issue, please reach out on the forum or, if you are on the Enterprise plan, through your support channels.

plans and pricing
Enterprise trial
$ edge-impulse-run-impulse
Starting inferencing in 2 seconds...
Sampling... Storing in file name: /fs/device-classification261
Tensor shape: 4
Predictions (DSP: 17 ms., Classification: 1 ms., Anomaly: 0 ms.):
    idle: 0.00004
    snake: 0.00012
    updown: 0.00009
    wave: 0.99976
    anomaly score: 0.032
Finished inferencing, raw data is stored in '/fs/device-classification261'. Use AT+UPLOADFILE to send back to Edge Impulse.
$ edge-impulse-run-impulse --continuous
Tutorial: continuous motion recognition
supported device
Data forwarder
Edge Impulse for Linux
mobile phone
Data acquisition
Continuous gestures dataset
Spectral features
Classification (Keras)
feature importance
anomaly detection (K-means)
anomaly detection (GMM)
Using your mobile phone
Running your impulse locally
Sound recognition
Image classification
custom processing blocks
Devices tab with the device connected to the remote management interface.
Record new data screen.
Updown movement recorded from the accelerometer.
First impulse, with one processing block and one learning block.
Spectral features parameters
Spectral features - Generate features
Training performance after a single iteration. On the top-right, is a summary of the accuracy of the network, and in the middle, a confusion matrix. This matrix shows when the network made correct and incorrect decisions. You see that idle is relatively easy to predict. Why do you think this is?
Neural network trained with 30 epochs
Classification result. Showing the conclusions, the raw data and processed features in one overview.
Shake data is easily separated from the training data.
Add anomaly detection block to Create impulse tab
Known clusters in blue, the shake data in orange. It's clearly outside of any known clusters and can thus be tagged as an anomaly.
$ git clone https://github.com/edgeimpulse/example-custom-processing-block-python
$ docker build -t custom-blocks-demo .
$ docker run -p 4446:4446 -it --rm custom-blocks-demo
$ pip3 install -r requirements-blocks.txt
$ python3 dsp-server.py
$ ngrok http 4446
# or
$ ./ngrok http 4446
Session Status                online
Account                       Edge Impulse (Plan: Free)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://4d48dca5.ngrok.io -> http://localhost:4446
Forwarding                    https://4d48dca5.ngrok.io -> http://localhost:4446
        {
            "group": "Filter",
            "items": [
                {
                    "name": "Smooth",
                    "value": false,
                    "type": "boolean",
                    "help": "Whether to smooth the data",
                    "param": "smooth"
                }
            ]
        }
import numpy as np

def generate_features(implementation_version, draw_graphs, raw_data, axes, sampling_freq, scale_axes, smooth):
    return { 'features': raw_data * scale_axes, 'graphs': [] }
import numpy as np

def smoothing(y, box_pts):
    box = np.ones(box_pts) / box_pts
    y_smooth = np.convolve(y, box, mode='same')
    return y_smooth

def generate_features(implementation_version, draw_graphs, raw_data, axes, sampling_freq, scale_axes, smooth):
    # features is a 1D array, reshape so we have a matrix with one raw per axis
    raw_data = raw_data.reshape(int(len(raw_data) / len(axes)), len(axes))

    features = []
    smoothed_graph = {}

    # split out the data from all axes
    for ax in range(0, len(axes)):
        X = []
        for ix in range(0, raw_data.shape[0]):
            X.append(raw_data[ix][ax])

        # X now contains only the current axis
        fx = np.array(X)

        # first scale the values
        fx = fx * scale_axes

        # if smoothing is enabled, do that
        if (smooth):
            fx = smoothing(fx, 5)

        # we save bandwidth by only drawing graphs when needed
        if (draw_graphs):
            smoothed_graph[axes[ax]] = list(fx)

        # we need to return a 1D array again, so flatten here again
        for f in fx:
            features.append(f)

    # draw the graph with time in the window on the Y axis, and the values on the X axes
    # note that the 'suggestedYMin/suggestedYMax' names are incorrect, they describe
    # the min/max of the X axis
    graphs = []
    if (draw_graphs):
        graphs.append({
            'name': 'Smoothed',
            'X': smoothed_graph,
            'y': np.linspace(0.0, raw_data.shape[0] * (1 / sampling_freq) * 1000, raw_data.shape[0] + 1).tolist(),
            'suggestedYMin': -20,
            'suggestedYMax': 20
        })

    return {
            'features': features,
            'graphs': graphs,
            'output_config': {
                # type can be 'flat', 'image' or 'spectrogram'
                'type': 'flat',
                'shape': {
                    # shape should be { width, height, channels } for image, { width, height } for spectrogram
                    'width': len(features)
                }
            }
        }
    graphs.append({
        'name': 'Logarithmic example',
        'X': {
            'Axis title': [ pow(10, i) for i in range(10) ]
        },
        'y': np.linspace(0, 10, 10).tolist(),
        'suggestedXMin': 0,
        'suggestedXMax': 10,
        'suggestedYMin': 0,
        'suggestedYMax': 1e+10,
        'type': 'logarithmic'
    })
    from PIL import Image, ImageDraw, ImageFont, ImageFilter

    # create a new image, and draw some text on it
    im = Image.new ('RGB', (438, 146), (248, 86, 44))
    draw = ImageDraw.Draw(im)
    draw.text((10, 10), 'Hello world!', fill=(255, 255, 255))

    # save the image to a buffer, and base64 encode the buffer
    with io.BytesIO() as buf:
        im.save(buf, format='png', bbox_inches='tight', pad_inches=0)
        buf.seek(0)
        image = (base64.b64encode(buf.getvalue()).decode('ascii'))

        # append as a new graph
        graphs.append({
            'name': 'Image from custom block',
            'image': image,
            'imageMimeType': 'image/png',
            'type': 'image'
        })
"visualization": "dimensionalityReduction"
int extract_my_preprocessing_features(signal_t *signal, matrix_t *output_matrix, void *config_ptr, const float frequency);
type DSPBlockParametersJson = {
    version: 1,
    type: 'dsp',
    info: {
        type: string;
        title: string;
        author: string;
        description: string;
        name: string;
        preferConvolution: boolean;
        convolutionColumns?: 'axes' | string;
        convolutionKernelSize?: number;
        cppType: string;
        visualization: 'dimensionalityReduction' | undefined;
        experimental: boolean;
        hasTfliteImplementation: boolean; // whether we can fetch TFLite file for this DSP block
        latestImplementationVersion: number;
        hasImplementationVersion: boolean; // whether implementation version should be passed in (for custom blocks)
        hasFeatureImportance: boolean;
        hasAutoTune?: boolean;
        minimumVersionForAutotune?: number;
        usesState?: boolean; // Does the DSP block use feedback, do you need to keep the state object and pass it back in
        // Optional: named axes
        axes: {
            name: string,
            description: string,
            optional?: boolean,
        }[] | undefined;
        port?: number;
    },
    // see spec in https://docs.edgeimpulse.com/docs/tips-and-tricks/adding-parameters-to-custom-blocks
    parameters: DSPParameterItem[];
};
Continuous motion recognition
custom processing blocks
an example
http://localhost:4446
ngrok
ngrok
parameters.json
Run DSP
Inferencing C++ SDK
Utilize Custom Processing Blocks in Your Image ML Pipelines
edgeimpulse/processing-blocks
forums
Running your first custom block locally
Adding a custom processing block from an ngrok URL
An impulse with a custom processing block and a neural network.
Custom processing block with a 'smooth' option that shows a graph of the processed features.
git clone https://github.com/edgeimpulse/example-sensor-fusion-using-embeddings.git
python saved-model-to-embeddings.py --input input/ --output dsp-blocks/features-from-audio-embeddings
def generate_features(implementation_version, draw_graphs, raw_data, axes, sampling_freq):
    frame_length = 0.032
    frame_stride = 0.024
    fft_length = 128
    noise_floor_db = -85
    ...
cd dsp-blocks/features-from-audio-embeddings
edge-impulse-block init
edge-impulse-block push
int custom_sensor_fusion_features(signal_t *signal, matrix_t *output_matrix, void *config_ptr, const float frequency);
ei_model_dsp_t ei_dsp_blocks[ei_dsp_blocks_size] = {
    { // DSP block 46
        207,
        &extract_spectral_analysis_features,
        (void*)&ei_dsp_config_46,
        ei_dsp_config_46_axes,
        ei_dsp_config_46_axes_size
    },
    { // DSP block 58
        416,
        &custom_sensor_fusion_features, // <-- change is here
        (void*)&ei_dsp_config_58,
        ei_dsp_config_58_axes,
        ei_dsp_config_58_axes_size
    }
};
/**
 Custom DSP function implementation
 */
int custom_sensor_fusion_features(signal_t *signal, matrix_t *output_matrix, void *config_ptr, const float frequency) {
...
}
run_classifier returned: 0
Timing: DSP 4 ms, inference 0 ms, anomaly 0 ms
Predictions:
  extract: 0.01953
  grind: 0.98047
  idle: 0.00000
  pump: 0.00000
custom DSP blocks
Sensor Fusion
this tutorial
Data Explorer
Coffee Machine Stages
Audio Sensor Fusion - Step 1
Audio Sensor Fusion - Step 2
Sensor fusion using NN Embeddings
Audio Sensor Fusion - Step 1
dashboard
processing-blocks
Custom processing blocks
Audio Sensor Fusion - Step 2
Github repository
custom deployment blocks
Enterprise Trial
Neural Networks Embeddings
Coffee machine example using sensor fusion data sample with 1 audio channels and 3 accelerometer axis
Sensor fusion data sample with images and sensor data encoded as a time-series
Impulse for the first step
Download saved_model and X_train.npy
Impulse for the second step
Final fully connected layers

Generate keyword spotting datasets using Google TTS

Local Software Requirements

  • Python 3

  • Pip package manager

  • Jupyter Notebook: https://jupyter.org/install

  • pip packages (install with pip installpackagename):

    • pydub https://pypi.org/project/pydub/

    • google-cloud-texttospeech https://cloud.google.com/python/docs/reference/texttospeech/latest

    • requests https://pypi.org/project/requests/

# Imports
import os
import json
import time
import io
import random
import requests
from pydub import AudioSegment
from google.cloud import texttospeech

Set up Google TTS API

First off you will need to set up and Edge Impulse account and create your first project. You will also need a Google Cloud account with the Text to Speech API enabled: https://cloud.google.com/text-to-speech, the first million characters generated each month are free (WaveNet voices), this should be plenty for most cases as you'll only need to generate your dataset once. From google you will need to download a credentials JSON file and set it to the correct environment variable on your system to allow the python API to work: (https://developers.google.com/workspace/guides/create-credentials#service-account)


# Insert the path to your service account API key json file here for google cloud
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = '../path-to-google-credentials-file.json'

Generate the desired samples

First off we need to set our desired keywords and labels:


# Keyword or short sentence and label (e.g. 'hello world')
keyword = [
    {'string':'edge','label':'edge'},
    {'string':'impulse','label':'impulse'},
           ]

Then we need to set up the parameters for our speech dataset, all possible combinations will be iterated through:

  • languages - Choose the text to speech voice languages to use (https://cloud.google.com/text-to-speech/docs/voices)

  • pitches - Which voice pitches to apply

  • genders - Which SSML genders to apply

  • speakingRates - Which speaking speeds to apply



# Languages, remove as appropriate
# languages = [
#     'ar-XA', 'bn-IN',  'en-GB',  'fr-CA',
#     'en-US', 'es-ES',  'fi-FI',  'gu-IN',
#     'ja-JP', 'kn-IN',  'ml-IN',  'sv-SE',
#     'ta-IN', 'tr-TR',  'cs-CZ',  'de-DE',
#     'en-AU', 'en-IN',  'fr-FR',  'hi-IN',
#     'id-ID', 'it-IT',  'ko-KR',  'ru-RU',
#     'uk-UA', 'cmn-CN', 'cmn-TW', 'da-DK',
#     'el-GR', 'fil-PH', 'hu-HU',  'nb-NO',
#     'nl-NL', 'pt-PT',  'sk-SK',  'vi-VN',
#     'pl-PL', 'pt-BR',  'ca-ES',  'yue-HK',
#     'af-ZA', 'bg-BG',  'lv-LV',  'ro-RO',
#     'sr-RS', 'th-TH',  'te-IN',  'is-IS'
# ]
languages = [
    'en-GB',
    'en-US',
]
# Pitches to generate (in semitones) range: [-20.0, 20.0]
pitches = [-2, 0, 2]
# Voice genders to use
genders = ["NEUTRAL", "FEMALE", "MALE"]
# Speaking rates to use range: [0.25, 4.0]
speakingRates = [0.9, 1, 1.1]

Then provide some other key parameters:

  • out_length - How long each output sample should be

  • count - Maximum number of samples to output (if all combinations of languages, pitches etc are higher then this restricts output)

  • voice-dir - Where to store the clean samples before noise is added

  • noise-url - Which noise file to download and apply to your samples

  • output-folder - The final output location of the noised samples

  • num-copies - How many different noisy versions of each sample to create

  • max-noise-level - in Db,

# Out length minimum (default: 1s)
out_length = 1
# Maximum number of keywords to generate
count = 30
# Raw sample output directory
voice_dir = 'out-wav'
# Creative commons background noise from freesound.org:https://freesound.org/people/Astounded/sounds/483561/
noise_url = 'https://cdn.freesound.org/previews/483/483561_10201334-lq.ogg'
output_folder = 'out-noisy'
num_copies = 2  # Number of noisy copies to create for each input sample
max_noise_level = -5  # Maximum noise level to add in dBFS (negative value)

Then we need to check all the output folders are ready


# Check if output directory for noisey files exists and create it if it doesn't
if not os.path.exists(output_folder):
    os.makedirs(output_folder)
# Check if output directory for raw voices exists and create it if it doesn't
if not os.path.exists(voice_dir):
    os.makedirs(voice_dir)    

And download the background noise file


# Download background noise file
response = requests.get(noise_url)
response.raise_for_status()
noise_audio = AudioSegment.from_file(io.BytesIO(response.content), format='ogg')

Then we can generate a list of all possible parameter combinations based on the input earlier. If you have set num_copies to be smaller than the number of combinations then these options will be reduced:


# Generate all combinations of parameters
all_opts = []
for p in pitches:
    for g in genders:
        for l in languages:
            for s in speakingRates:
                for kw in keyword:
                    all_opts.append({
                            "pitch": p,
                            "gender": g,
                            "language": l,
                            "speakingRate": s,
                            "text": kw['string'],
                            "label": kw['label']
                        })
if len(all_opts)*num_copies > count:
    selectEvery = len(all_opts)*num_copies // count
    selectNext = 0
    all_opts = all_opts[::selectEvery]
print(f'Generating {len(all_opts)*num_copies} samples')

Finally we iterate though all the options generated, call the Google TTS API to generate the desired sample, and apply noise to it, saving locally with metadata:


# Instantiate list for file label information
downloaded_files = []

# Instantiates a client
client = texttospeech.TextToSpeechClient()

ix = 0
for o in all_opts:
    ix += 1
    # Set the text input to be synthesized
    synthesis_input = texttospeech.SynthesisInput(text=o['text'])
    # Build the voice request
    voice = texttospeech.VoiceSelectionParams(
        language_code=o['language'],
        ssml_gender=o['gender']
    )
    # Select the type of audio file you want returned
    audio_config = texttospeech.AudioConfig(
        audio_encoding=texttospeech.AudioEncoding.LINEAR16,
        pitch=o['pitch'],
        speaking_rate=o['speakingRate'],
        sample_rate_hertz=16000
    )
    # Perform the text-to-speech request on the text input with the selected
    # voice parameters and audio file type

    wav_file_name = f"{voice_dir}/{o['label']}.{o['language']}-{o['gender']}-{o['pitch']}-{o['speakingRate']}.tts.wav"

    if not os.path.exists(wav_file_name):
        print(f"[{ix}/{len(all_opts)}] Text-to-speeching...")
        response = client.synthesize_speech(
            input=synthesis_input, voice=voice, audio_config=audio_config
        )
        with open(wav_file_name, "wb") as f:
            f.write(response.audio_content)
        has_hit_api = True
    else:
        print(f'skipping {wav_file_name}')
        has_hit_api = False

    # Load voice sample
    voice_audio = AudioSegment.from_file(wav_file_name)
    # Add silence to match output length with random padding
    difference = (out_length * 1000) - len(voice_audio)
    if difference > 0:
        padding_before = random.randint(0, difference)
        padding_after = difference - padding_before
        voice_audio = AudioSegment.silent(duration=padding_before) +  voice_audio + AudioSegment.silent(duration=padding_after)

    for i in range(num_copies):
        # Save noisy sample to output folder
        output_filename = f"{o['label']}.{o['language']}-{o['gender']}-{o['pitch']}-{o['speakingRate']}_noisy_{i+1}.wav"
        output_path = os.path.join(output_folder, output_filename)
        if not os.path.exists(output_path):
            # Select random section of noise and random noise level
            start_time = random.randint(0, len(noise_audio) - len(voice_audio))
            end_time = start_time +len(voice_audio)
            noise_level = random.uniform(max_noise_level, 0)

            # Extract selected section of noise and adjust volume
            noise_segment = noise_audio[start_time:end_time]
            noise_segment = noise_segment - abs(noise_level)

            # Mix voice sample with noise segment
            mixed_audio = voice_audio.overlay(noise_segment)
            # Save mixed audio to file
            mixed_audio.export(output_path, format='wav')

            print(f'Saved mixed audio to {output_path}')
        else:
            print(f'skipping {output_path}')
        # Save metadata for file
        downloaded_files.append({
            "path": str(output_filename),
            "label": o['label'],
            "category": "split",
            "metadata": {
                "pitch": str(['pitch']),
                "gender": str(o['gender']),
                "language": str(o['language']),
                "speakingRate": str(o['speakingRate']),
                "text": o['text'],
                "imported_from": "Google Cloud TTS"
            }
        })

    if has_hit_api:
        time.sleep(0.5)

print("Done text-to-speeching")
print("")

input_file = os.path.join(output_folder, 'input.json')
info_file = {
    "version": 1,
    "files": downloaded_files
}
# Output the metadata file
with open(input_file, "w") as f:
    json.dump(info_file, f)

The files in ./out-noisy can be uploaded easily using the Edge Impulse CLI tool:

# Move to the out-noisy folder
! cd out-noisy
# Upload all files in the out-noisy folder with metadata attached in the input.json file
! edge-impulse-uploader --info-file input.json *

What next?

Now you can use your keywords to create a robust keyword detection model in Edge Impulse Studio!

Make use of our pre-built keyword dataset to add noise and 'unknown' words to your model: Keyword Spotting Dataset

Try out both classification models and the transfer learning keyword spotting model to see which works best for your case

Custom transformation blocks

Custom transformation blocks are a way to extend the capabilities of Edge Impulse beyond the transformation blocks built into the platform. If none of the existing blocks created by Edge Impulse fit your needs, you can create custom transformation blocks to integrate your own data pre-processing for unique project requirements.

Ready to dive in and start building? Jump to the examples!

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

Block structure

The transformation block structure is shown below. Please see the custom blocks overview page for more details.

Custom transformation block structure

Block interface

The sections below define the required and optional inputs and the expected outputs for custom transformation blocks.

Inputs

Transformation blocks have access to environment variables, command line arguments, and mounted storage buckets.

Environment variables

The following environment variables are accessible inside of transformation blocks. Environment variable values are always stored as strings.

Variable
Passed
Description

EI_API_ENDPOINT

Always

The API base URL: https://studio.edgeimpulse.com/v1

EI_API_KEY

Always

The organization API key with member privileges: ei_2f7f54...

EI_INGESTION_HOST

Always

The host for the ingestion API: edgeimpulse.com

EI_LAST_SUCCESSFUL_RUN

Always

The last time the block was successfully run, if a part of a data pipeline: 1970-01-01T00:00:00.000Z

EI_ORGANIZATION_ID

Always

The ID of the organization that the block belongs to: 123456

EI_PROJECT_ID

Conditional

Passed if the transformation block is a data source for a project. The ID of the project: 123456

EI_PROJECT_API_KEY

Conditional

Passed if the transformation block is a data source for a project. The project API key: ei_2a1b0e...

You can also define your own environment variables to pass to your custom block using the requiredEnvVariables property in the parameters.json file. You will then be prompted for the associated values for these properties when pushing the block to Edge Impulse using the CLI. Alternatively, these values can be added (or changed) by editing the block in Studio after pushing.

Command line arguments

The parameter items defined in your parameters.json file will be passed as command line arguments to the script you defined in your Dockerfile as the ENTRYPOINT for the Docker image. Please refer to the parameters.json documentation for further details about creating this file, parameter options available, and examples.

In addition to the items defined by you, the following arguments will be automatically passed to your custom transformation block.

Argument
Passed
Description

--in-file <file>

Conditional

Passed if operation mode is set to file. Provides the file path as a string. This is the file to be processed by the block.

--in-directory <dir>

Conditional

Passed if operation mode is set to directory. Provides the directory path as a string. This is the directory to be processed by the block.

--out-directory <dir>

Conditional

Passed if operation mode is set to either file or directory. Provides the directory path to the output directory as a string. This is where block output needs to be written.

--hmac-key <key>

Conditional

Passed if operation mode is set to either file or directory. Provides a project HMAC key as a string, if it exists, otherwise '0'.

--metadata <metadata>

Conditional

Passed if operation mode is set to either file or directory, the pass in metadata property (indMetadata) is set to true, and the metadata exists. Provides the metadata associated with data item as a stringified JSON object.

--upload-category <category>

Conditional

Passed if operation mode is set to file or directory and the transformation job is configured to import the results into a project. Provides the upload category (split, training, or testing) as a string.

--upload-label <label>

Conditional

Passed if operation mode is set to file or directory and the transformation job is configured to import the results into a project. Provides the upload label as a string.

CLI arguments can also be specified using the cliArguments property in the parameters.json file. Alternatively, these arguments can be added (or changed) by editing the block in Studio.

Lastly a user can be prompted for extra CLI arguments when configuring a transformation job if the allowExtraCliArguments property is set to true.

Mounted storage buckets

One or more cloud data storage buckets can be mounted inside of your block. If storage buckets exist in your organization, you will be prompted to mount the bucket(s) when initializing the block with the Edge Impulse CLI. The default mount point will be:

/mnt/s3fs/<bucket-name>

The mount point can be changed by editing your parameters.json file before pushing the block to Edge Impulse or editing the block in Studio after pushing.

Outputs

There are no required outputs from transformation blocks. In general, for blocks operating in file or directory mode, new data is written to the directory given by the --out-directory <dir> argument. For blocks operating in standalone mode, any actions are typically achieved using API calls inside the block itself.

Understanding operating modes

Transformation blocks can operate in one of three modes: file, directory, or standalone.

File

As the name implies, file transformation blocks operate on files. When configuring a transformation job, the user will select a list of files to transform. These files will be individually passed to and processed by the script defined in your transformation block. File transformation blocks can be run in multiple processing jobs in parallel.

Each file will be passed to your block using the --in-file <file> argument.

Directory

As the name implies, directory transformation blocks operate on directories. When configuring a transformation job, the user will select a list of directories to transform. These directories will be individually passed to and processed by the script defined in your transformation block. Directory transformation blocks can be run in multiple processing jobs in parallel.

Each directory will be passed to your block using the --in-directory <dir> argument.

Standalone

Standalone transformation blocks are a flexible way to run generic cloud jobs that can be used for a wide variety of tasks. In standalone mode, no data is passed into your block. If you need to access your data, you will need to mount your storage bucket(s) into your block. Standalone transformation blocks are run as a single processing job; they cannot be run in multiple processing jobs in parallel.

Updating data item metadata

If your custom transformation block is operating in directory mode and transforming a clinical dataset, you can update the metadata associated with the data item after it is processed.

To do so, your custom transformation block needs to write an ei-metadata.json file to the directory specified in the --out-directory <dir> argument. Please refer to the ei-metadata.json documentation for further details about this file.

with open(os.path.join(args.out_directory, 'ei-metadata.json'), 'w') as f:
    f.write(json.dumps({
        'version': 1,
        'action': 'add',
        'metadata': {
            'now': round(time.time() * 1000)
        }
    }))

Showing the block in Studio

There are two locations within Studio that transformation blocks can be found: transformation jobs and project data sources.

Transformation blocks operating in file or directory mode will always been shown as an option in the block dropdown for transformation jobs. They cannot be used as a project data source.

Transformation blocks operating in standalone mode can optionally be shown in the block dropdown for transformation jobs and/or in the block dropdown for project data sources.

For transformation jobs

Operating mode
Shown in block dropdown

file

Always

directory

Always

standalone

If showInCreateTransformationJob property set to true.

For project data sources

Operating mode
Shown in block dropdown

file

Never

directory

Never

standalone

If showInDataSources property set to true.

Initializing the block

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the overview page. Please refer to that documentation for details.

Testing the block locally

To speed up your development process, you can test your custom transformation block locally. There are two ways to achieve this. You will need to have Docker installed on your machine for either approach.

With blocks runner

For the first method, you can use the CLI edge-impulse-blocks runner tool. See Block runner for additional details.

If your custom transformation block is operating in either file or directory mode, you will be prompted for information to look up and download data (a file or a directory) for the block to operate on when using the blocks runner. This can be achieved by providing either a data item name (clinical data) or the path within a dataset for a file or directory (clinical or default data). You can also specify some of this information using the blocks runner command line arguments.

Argument
Description

--dataset <dataset>

Transformation blocks in file or directory mode. Files and directories will be looked up within this dataset. If not provided, you will be prompted for a dataset name.

--data-item <data-item>

Clinical data only. Transformation blocks in directory mode. The data item will be looked up, downloaded, and passed to the container when it is run. If not provided, you will be prompted for the information required to look up a data item.

--file <filename>

Clinical data only. Transformation blocks in file mode. Must be used in conjunction with --data-item <data-item>. The file will be looked up, downloaded, and passed to the container when it is run. If not provided, you will be prompted for the information required to look up a file within a data item.

--skip-download

Skips downloading the data.

--extra-args <args>

Additional arguments for your script.

Additional arguments to your script can be provided as a single string using --extra-args <args> argument.

 edge-impulse-blocks runner --extra-args "--custom-param-one foo --custom-param-two bar"

Using the above approach will create an ei-block-data directory within your custom block directory. It will contain subdirectories for the data that has been downloaded.

With Docker

For the second method, you can build the Docker image and run the container directly. You will need to pass any environment variables or command line arguments required by your script to the container when you run it.

If your transformation block operates in either file or directory mode, you will also need to create a data/ directory within your custom block directory and place your data used for testing here.

docker build -t custom-transformation-block .

file mode:

docker run --rm -v $PWD/data:/data -e CUSTOM_ENV_VAR='<env-value>' custom-transformation-block --in-file /data/<file> --out-directory /data/out --custom-param foo

directory mode:

docker run --rm -v $PWD/data:/data -e CUSTOM_ENV_VAR='<env-value>' custom-transformation-block --in-directory /data  --out-directory /data/out --custom-param foo

standalone mode:

docker run --rm -e CUSTOM_ENV_VAR='<env-value>' custom-transformation-block --custom-param foo

Pushing the block to Edge Impulse

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the overview page. Please refer to that documentation for details.

Using the block in Studio

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

Examples

Edge Impulse has developed several transformation blocks, some of which are built into the platform. The code for these blocks can be found in public repositories under the Edge Impulse GitHub account. The repository names typically follow the convention of example-transform-<description>. As such, they can be found by going to the Edge Impulse account and searching the repositories for example-transform.

Note that when using the above search term you will come across synthetic data blocks as well. Please read the repository description to identify if it is for a transformation block or a synthetic data block.

Further, several example transformation blocks have been gathered into a single repository:

  • Transformation block examples

Troubleshooting

Files in storage bucket cannot be accessed

If you cannot access the files in your storage bucket, make sure that the mount point has been properly configured and that you are referencing this location within your processing script.

You can double check the mount point by looking at the additional mount point section when editing the block in Studio:

If you do not see your storage bucket, you can mount it here.

When using the Edge Impulse CLI to initialize your block, it is a common mistake to forget to press the <space> key to select the bucket for mounting (and therefore the storage bucket is not added to the parameters.json file).

Transformation job runs indefinitely

If you notice that your transformation job runs indefinitely, it is probably because of an error with your processing script or the script has not been properly terminated.

Make sure to exit your script with code 0 (return 0, exit(0) or sys.exit(0)) for success or with any other error code for failure.

Additional resources

  • Custom blocks

  • Transformation blocks

  • edge-impulse-blocks

  • parameters.json

  • ei-metadata.json

with Particle Workbench

Introduction

This page is part of the Lifecycle Management with Edge Impulse tutorial series. If you haven't read the introduction yet, we recommend you to do so here.

In this tutorial, we'll guide you through deploying updated impulses over-the-air (OTA) using the Particle Workbench. We'll build on Particle's firmware update workflow, incorporating Edge Impulse's API to check for updates and download the latest build.

We will modify the Edge Impulse Photon 2 example to incorporate the OTA functionality and update the device with the latest build. Based on the particle workbench OTA example.

Prerequisites

  • Edge Impulse Account: If you haven't got one, sign up here.

  • Trained Impulse: If you're new, follow one of our end-to-end tutorials

  • Knowledge of the Particle Workbench

  • Installation of required software as detailed in the Photon 2 example

Preparation

Begin by setting up your device for OTA updates following the particle documentation. https://docs.particle.io/getting-started/cloud/ota-updates/

Let's get started!

Key Features of Particle Workbench OTA Updates:

  • Development Devices: Specific devices can be marked for internal testing to validate firmware updates before a broader rollout.

  • Firmware Upload: Developers upload the compiled binary to the console, ensuring the firmware version is included.

  • Firmware Locking: Specific devices can be locked to run certain firmware versions for testing purposes.

  • Release Targets: Firmware can be released to all devices or specific groups to control the rollout process.

  • Intelligent Firmware Releases: Ensures timely delivery of updates without disrupting device operation, taking into account the device’s status and activity.

Particle OTA

Particle OTA is a fully-integrated over-the-air software update system that is built into the Particle IoT PaaS and Device OS. It allows customers to safely and reliably push software updates to single devices or entire fleets of devices directly from Particle’s device management console and developer tools, with no implementation work necessary.

Particle OTA allows you to update your entire IoT device (both the Particle device and any other components) by delivering three kinds of updates:

  • Application OTA allows users to update the firmware application they are running on the Particle device in order to introduce new features, fix bugs, and generally improve the software application over time.

  • Device OS OTA allows users to update Device OS to the latest version alongside an application update so that Device OS can be kept up to date with improvements and bug fixes while properly testing against the user-generated software application.

  • Asset OTA allows users to include bundled assets in an OTA software update that can be delivered to other processors and components in the electronics system so that the Particle device can be responsible not just for updating itself but also the system that surrounds the device.

On your Edge Impulse project

Export your impulse to the Particle Workbench library:

Add the library through the Particle Workbench via:

  1. Extract the .zip library.

  2. Particle: Import Project and select project.properties. Examples can then be found in: yourprojectname/examples/

We created an example repository which contains a small application for your particle device.

git clone https://github.com/edgeimpulse/example-standalone-inferencing-photon2

Creating the Intial Impulse Export to Particle Library and Deployment firmware

You will need to treat this as a new project and follow the instructions in the Particle Workbench to set up your environment for local development.

Projects with external libraries

If your project includes a library that has not been registered in the Particle libraries system, you should create a new folder named /lib/<libraryname>/src under /<project dir> and add the .h, .cpp & library.properties files for your library there. Read the Firmware Libraries guide for more details on how to develop libraries. Note that all contents of the /lib folder and subfolders will also be sent to the Cloud for compilation.

Compiling your project

When you're ready to compile your project, make sure you have the correct Particle device target selected and run particle compile <platform> in the CLI or click the Compile button in the Desktop IDE. The following files in your project folder will be sent to the compile service:

  • Everything in the /src folder, including your .ino application file

  • The project.properties file for your project

  • Any libraries stored under lib/<libraryname>/src

You should now have a compiled binary file in your project folder named firmware.bin.

Next lets look at the OTA functionality and how we can incorporate it into our project.

Particle Workbench OTA Update Workflow and example

You will need to have a device deployed with an existing firmware containing an impulse before you can roll out a new version. Then you can use the OTA workflow to update the firmware on your device to a new version. While blocking all other devices from updating to the new version.

Step 1: Testing on Development Devices

Mark Devices for Testing: In the Particle console, mark specific devices for internal testing to validate firmware updates. Upload Firmware: Compile and upload the firmware binary, ensuring to include the product ID and version.

Step 2: Locking Firmware for Testing

Lock Devices: Lock specific devices to the new firmware version and monitor their behavior. Unlock Devices: Unlock the devices once testing is complete and you are satisfied with the firmware's performance.

Step 3: Rolling Out the Firmware

Release Targets: Choose to release the firmware to specific groups or the entire fleet of devices. Intelligent Firmware Release: Opt for this to have the Particle Device Cloud intelligently manage the update rollout. Important Tips: Avoid Disruptions: Utilize Device OS APIs to control OTA availability, ensuring devices aren’t disrupted during critical operations. Managing OTA Updates: Use the console or REST API to force enable OTA updates if needed. Monitoring: Keep an eye on the devices' behaviors post-update to quickly identify and address any potential issues. With Particle's OTA capabilities, developers can ensure that their IoT devices are always running the latest, most secure, and most efficient version of their firmware, enhancing the reliability and functionality of their IoT ecosystems.

Creating a Webhook for the Edge Impulse ingestion API

This guide supplements the tutorial on OTA Model Updates with Edge Impulse on Particle Workbench, focusing on configuring a Particle webhook for sending data to the Edge Impulse ingestion API.

Steps for Webhook Configuration:

  1. Access Particle Console:

    • Visit Particle Console.

    • Log in with your Particle account credentials.

  2. Navigate to Integrations:

    • Click on the "Integrations" tab in the left-hand menu.

    • Select "Webhooks" from the available options.

  3. Create a New Webhook:

    • Click "New Integration".

    • Choose "Webhook".

  4. Webhook Configuration:

    • Name: Assign a descriptive name to your webhook.

    • Event Name: Specify the event name that triggers the webhook (e.g., "edge/ingest").

    • URL: Set this to the Edge Impulse ingestion API URL, typically something like https://ingestion.edgeimpulse.com/api/training/data.

    • Request Type: Choose "POST".

    • Request Format: Select "Custom".

  5. Custom Request Body:

    • Input the JSON structure required by Edge Impulse. This will vary based on your project's data schema.

  6. HTTP Headers:

    • Add necessary headers:

      • x-api-key: Your Edge Impulse API key.

      • Content-Type: "application/json".

      • x-file-name: Use a dynamic data field like {{PARTICLE_EVENT_NAME}}.

  7. Advanced Settings:

    • Response Topic: Create a custom topic for webhook responses, e.g., {{PARTICLE_DEVICE_ID}}/hook-response/{{PARTICLE_EVENT_NAME}}.

    • Enforce SSL: Choose "Yes" for secure transmission.

  8. Save the Webhook:

    • After entering all details, click "Save".

  9. Test the Webhook:

    • Use example device firmware to trigger the webhook.

    • Observe the responses in the Particle Console.

  10. Debugging:

  • If errors occur, review the logs for detailed information.

  • Ensure payload format aligns with Edge Impulse requirements.

  • Verify the accuracy of your API key and other details.

Custom Template Example:

Copy and paste the following into the Custom Template section of the webhook:

{
    "name": "edgeimpulse.com",
    "event": "edge/ingest",
    "responseTopic": "",
    "disabled": true,
    "url": "http://ingestion.edgeimpulse.com/api/training/data",
    "requestType": "POST",
    "noDefaults": true,
    "rejectUnauthorized": false,
    "headers": {
        "x-api-key": "ei_1855db...",
        "x-file-name": "{{PARTICLE_EVENT_NAME}}",
        "x-label": "coffee"
    },
    "json": "{\n  \"payload\": {\n    \"device_name\": \"0a10a...\",\n    \"device_type\": \"photon2\",\n    \"interval_ms\": 20,\n    \"sensors\": [\n      {\n        \"name\": \"volt\",\n        \"units\": \"V\"\n      },\n      {\n        \"name\": \"curr\",\n        \"units\": \"A\"\n      }\n    ],\n    \"values\": [\n{{{PARTICLE_EVENT_VALUE}}}\n    ]\n  },\n  \"protected\": {\n    \"alg\": \"none\",\n    \"ver\": \"v1\"\n  },\n  \"signature\": \"00\"\n}"
}

Closing the Loop to send data to Edge Impulse for Lifecycle Management

Below we will do a basic example of sending data to Edge Impulse for Lifecycle Management. We could further extend this example to include more advanced checking, utilizing more of Particle OTA functionality, and add checks for model performance and versioning. The code gathers data from analog sensors, processes it, and sends it to Edge Impulse for Lifecycle Management. The webhook is triggered with the event name edge/ingest/sample.

#include "Particle.h"

// Sample data array
int sampleData[2][10] = {
    {100, 110, 120, 130, 140, 150, 160, 170, 180, 190}, // Sample voltage data
    {200, 210, 220, 230, 240, 250, 260, 270, 280, 290}  // Sample current data
};

void setup() {
    Serial.begin(9600);
    Particle.connect();
}

void loop() {
    char dataBuf[512];
    JSONBufferWriter writer(dataBuf, sizeof(dataBuf) - 1);

    writer.beginArray();
    for(int i = 0; i < 10; i++) {
        writer.beginArray();
        writer.value(sampleData[0][i]);
        writer.value(sampleData[1][i]);
        writer.endArray();
    }
    writer.endArray();

    Particle.publish("edge/ingest/sample", dataBuf, PRIVATE);
    delay(10000); // Send data every 10 seconds
}

This code continuously samples voltage and current, calculates RMS current, and then sends a JSON array of the sampled data to the Edge Impulse ingestion API. The publishData function uses Particle's Particle.publish method to send the data to the specified event. This triggers the webhook configured to send data to Edge Impulse. See the Particle energy monitor code from our Imagine 2023 demo for a full example for more information.

Conclusion

This tutorial provides a basic guide for implementing OTA updates on your Particle Workbench connected device (Photon 2). It can be extended to include more advanced checking, utilizing more of espressif OTA functionality, and extending the python server to check for model performance and versioning. Happy coding!

Custom learning blocks

Custom learning blocks are a way to extend the capabilities of Edge Impulse beyond the learning blocks built into the platform. If none of the existing blocks created by Edge Impulse fit your needs, you can create custom learning blocks to integrate your own model architectures for unique project requirements.

Ready to dive in and start building? Jump to the examples!

Custom learning blocks are available for all users

Unlike other custom blocks, which are only available to customers on the Enterprise plan, custom learning blocks are available to all users of the platform. If you are an enterprise customer, your custom learning blocks will be available in your organization. If you are not an enterprise customer, your custom learning blocks will be available in your developer profile.

Expert mode

If you only want to make small modifications to the neural network architecture or loss function, you can instead use expert mode directly in Studio, eliminating the need to create a custom learning blocks. Go to any learning block settings page, select the three dots, and select Switch to Keras (expert) mode.

Block structure

The learning block structure is shown below. Please see the custom blocks overview page for more details.

Custom learning block structure

Block interface

The sections below define the required and optional inputs and the expected outputs for custom learning blocks.

Inputs

Learning blocks have access to command line arguments and training data.

Command line arguments

The parameters defined in your parameters.json file will be passed as command line arguments to the script you defined in your Dockerfile as the ENTRYPOINT for the Docker image. Please refer to the parameters.json documentation for further details about creating this file, parameter options available, and examples.

In addition to the items defined by you, the following arguments will be automatically passed to your custom learning block.

Argument
Passed
Description

--info-file <file>

Always

Provides the file path for train_input.json as a string. The train_input.json file contains configuration details for model training options. See .

--data-directory <dir>

Always

Provides the directory path for training/validation datasets as a string.

--out-directory <dir>

Always

Provides the directory path to the output directory as a string. This is where block output needs to be written.

--epochs <value>

Conditional

Passed if no custom parameters are provided. Provides the number of epochs for model training as an integer.

--learning-rate <value>

Conditional

Passed if no custom parameters are provided. Provides the learning rate for model training as a float.

Data

Learning blocks operate on data that has already been processed by an input block and a processing block. This processed data is available to your learning block in a single directory, in the NumPy format, and already split into training (train) and validation (test) datasets. By default the train/validation split is 80/20. You can change this ratio using the advanced training settings. The NumPy datasets can be converted to the required format (e.g. tf.data.Dataset) for your model and batched as desired within your custom learning block training script.

In addition to the datasets, a sample_id_details.json file (see sample_id_details.json) is located within the data directory. The location of this directory is specified by the --data-directory <dir> argument and its structure is shown below.

data/
├── X_split_test.npy
├── X_split_train.npy
├── Y_split_test.npy
├── Y_split_train.npy
└── sample_id_details.json

The X_*.npy files are float32 arrays in the appropriate shape. You can typically load these into your training pipeline without any modification.

The Y_*.npy files are int32 arrays with four columns: label_index, sample_id, sample_slice_start_ms, and sample_slice_end_ms, unless the labels are bounding boxes. See below.

Image data

The X_*.npy files follow the NHWC (batch_size, height, width, channels) format for image data.

The Y_*.npy files are a JSON array in the form of:

[
    {
        "sampleId": 234731,
        "boundingBoxes": [
            {
                "label": 1,
                "x": 260,
                "y": 313,
                "w": 234,
                "h": 261
            }
        ]
    }
]

Image data is formatted as NHWC

If you need your data in the channels-first, NCHW format, you will need to transpose the input data yourself before training your model.

Image data is provided to your custom learning block in the NHWC (batch_size, height, width, channels) format. If you are training a PyTorch model that requires data to be in the NCHW (batch_size, channels, height, width) format, you will need to transpose the data before training your model.

You do not need to worry about this when running on device. As long as your custom learning block outputs an ONNX model, the required transpose will be handled for you in the Edge Impulse SDK.

Image data is formatted as RGB

If you have a model that requires BGR input, you will need to transpose the first and last channels.

For models that require BGR channel format, you can have Edge Impulse automatically transpose the first and last channels by selecting the RGB->BGR option when configuring pixel scaling for your block. See below.

Image data has pixels that are already scaled

There is no need to scale the pixel values yourself for training nor for inference on-device. If the options provided in Edge Impulse do not suit your needs, please contact us to let us know what option(s) you require.

Image data is provided to your learning block with pixels that are already scaled. Pixel scaling is handled automatically by Edge Impulse. There are several options to scale your pixels, some of which include additional processing (e.g. standardization or centering):

  • Pixels ranging 0..1 (not normalized)

  • Pixels ranging -1..1 (not normalized)

  • Pixels ranging -128..127 (not normalized)

  • Pixels ranging 0..255 (not normalized)

  • PyTorch (pixels ranging 0..1, then standardized using ImageNet mean/std)

  • RGB->BGR (pixels ranging 0..255, then centered using ImageNet mean)

This can be configured when initializing your custom learning block with the Edge Impulse CLI, and changed later in Studio if required by editing your custom learning block.

Outputs

The expected output from your custom learning block should be in TensorFlow SavedModel, TFLite, ONNX, or pickled scikit-learn format.

For object detection models, it is also important to ensure that the output layer of your model is supported by Edge Impulse.

File output options

TFLite file(s):

  • model.tflite - a TFLite file with float32 inputs and outputs

  • model_quantized_int8_io.tflite - a quantized TFLite file with int8 inputs and outputs

At least one of the above file options is required (both are recommended). If the source of the TFLite files are a TensorFlow SavedModel, then also write the saved_model.zip file.

TensorFlow SavedModel:

  • saved_model.zip - a TensorFlow SavedModel file

Edge Impulse automatically converts this file to both unquantized and quantized TFLite files after training.

ONNX file:

  • model.onnx - an ONNX file with int8, float16 or float32 inputs and outputs

Edge Impulse automatically converts this file to both unquantized and quantized TFLite files after training.

Pickled scikit-learn file:

  • model.pkl - a pickled instance of the scikit-learn model

Edge Impulse will automatically convert this file to the required format. Note that arbitrary scikit-learn pipelines cannot be converted. For a list of supported model types, please refer to Supported classical ML algorithms.

Internally Edge Impulse uses scikit-learn==1.3.2 for conversion, so pin to this scikit-learn version for best results. LightGBM (3.3.5) and XGBOOST (1.7.6) models are also supported.

Object detection output layers

Unfortunately object detection models typically don't have a standard way to go from neural network output layer to bounding boxes. Currently Edge Impulse supports the following types of output layers. The most up-to-date list can be found in the API documentation for ObjectDetectionLastLayer.

  • FOMO

  • MobileNet SSD

  • NVIDIA TAO RetinaNet

  • NVIDIA TAO SSD

  • NVIDIA TAO YOLOv3

  • NVIDIA TAO YOLOv4

  • YOLOv2 for BrainChip Akida

  • YOLOv5 (coordinates scaled 0..1)

  • YOLOv5 (coordinates in absolute values)

  • YOLOv7

  • YOLOv11 (coordinates scaled 0..1)

  • YOLOv11 (coordinates in absolute values)

  • YOLOX

  • YOLO Pro

Configuring advanced training settings

After pushing your custom learning block to Edge Impulse, in Studio you will notice that below the section of custom parameters that you have exposed for your block, there is another section titled "Advanced training settings". These settings allow you to optionally adjust the train/validation split, split on a metadata key, and profile the int8 version of your model.

If you are testing your block locally using the edge-impulse-blocks runner tool as described below, you can adjust the train/validation split using the --validation-set-size <size> argument but you are unable to split using a metadata key. To profile your model after training locally, see Getting profiling metrics.

Getting profiling metrics

After training a custom learning block locally, you can use the profiling API to get latency, RAM and ROM estimates. This is very useful as you can immediately see whether your model will fit on device or not. Additionally, you can use this API as part your experiment tracking (e.g. in Weights & Biases or MLFlow) to wield out models that won't fit your latency or memory constraints.

You can also use the Python SDK to profile your model easily. See here for an example on how to profile a model created in Keras.

Editing built-in blocks

Most learning blocks built in the Edge Impulse (e.g. classifier, regression, or FOMO blocks) can be edited locally and then pushed back to Edge Impulse as a custom block. This is great if you want to make heavy modifications to these training pipelines, for example to do custom data augmentation. To download a block, go to any learning block settings page in your project, click the three dots, and select Edit block locally. Once downloaded, follow the instructions in the README file.

Option to edit a built-in block locally

Initializing the block

When you are finished developing your block locally, you will want to initialize it. The procedure to initialize your block is described in the overview page. Please refer to that documentation for details.

Testing the block locally

The train_input.json file is not available when training locally

If your script needs information that is contained within train_input.json, you will not be able to train locally. You will either need to push your block to Edge Impulse to train and test in Studio or alter your training script such that you can pass in that information (or eliminate it all together).

To speed up your development process, you can test and train your custom learning block locally. There are two ways to achieve this. You will need to have Docker installed on your machine for either approach.

With blocks runner

For the first method, you can use the CLI edge-impulse-blocks runner tool. See Block runner for additional details. The runner expects the following arguments for learning blocks.

Argument
Description

--epochs <number>

If not provided, you will be prompted to enter a value.

--learning-rate <learningRate>

If not provided, you will be prompted to enter a value.

--validation-set-size <size>

Defaults to 0.2 but can be overwritten.

--input-shape <shape>

Automatically identified but can be overwritten.

--extra-args <args>

Additional arguments for your script.

For the additional arguments, you will need to provide the data directory (/home), an output directory (e.g. /home/out), and any other parameters required for your script.

 edge-impulse-blocks runner --extra-args "--data-directory /home --out-directory /home/out --custom-param foo"

Using the above approach will create an ei-block-data directory within your custom block directory. It will contain a subdirectory with the associated project ID as the name - this is the directory that gets mounted into the container as /home.

The first time you enter the above command, you will be asked some questions to configure the runner. Follow the prompts to complete this. If you would like to change the configuration in future, you can execute the runner command with the --clean flag.

With Docker

For the second method, you can use the block runner to download the required data from your project, then build the Docker image and run the container directly. The advantage of this approach is that you do not need to go through the feature generation and data splitting process each time you want to train your block. If your data changes, you can download it again.

edge-impulse-blocks runner --download-data data/
docker build -t custom-learning-block .
docker run --rm -v $PWD/data:/data custom-learning-block --epochs 30 --learning-rate 0.01 --data-directory /data --out-directory /data/out --custom-param foo

Pushing the block to Edge Impulse

When you have initalized and finished testing your block locally, you will want to push it to Edge Impulse. The procedure to push your block to Edge Impulse is described in the overview page. Please refer to that documentation for details.

Using the block in a project

After you have pushed your block to Edge Impluse, it can be used in the same way as any other built-in block.

Examples

Edge Impulse has developed several example custom learning blocks. The code for these blocks can be found in public repositories under the Edge Impulse GitHub account. The repository names typically follow the convention of example-custom-ml-<description>. As such, they can be found by going to the Edge Impulse account and searching the repositories for ml-block.

Below are direct links to some examples:

  • Fully connected model (Keras)

  • Fully connected model (PyTorch)

  • Logistic regression model (scikit-learn)

  • EfficientNet (Keras)

  • YOLOv5 (PyTorch)

Troubleshooting

Block parameters do not update

If changes you have made to your parameters.json file are not being reflected, or there are no parameters at all, in your block after being pushed to Studio, you may need to update the Edge Impulse CLI:

npm update -g edge-impulse-cli

Additional resources

  • Custom blocks

  • Learning blocks

  • edge-impulse-blocks

  • parameters.json

  • train_input.json

Upload portals

Upload portals are a secure way to let external parties upload data to your datasets. Through an upload portal they get an easy user interface to add data, but they have no access to the content of the dataset, nor can they delete any files. Data that is uploaded through the portal can be stored on-premise or in your own cloud infrastructure.

In this tutorial we'll set up an upload portal, show you how to add new data, and how to show this data in Edge Impulse for further processing.

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our or sign up for our free today.

1. Configuring a storage bucket

Data is stored in cloud storage. For details on how to connect a cloud storage provider to Edge Impulse, refer to the Cloud data storage documentation.

2. Creating an upload portal

With your storage bucket configured you're ready to set up your first upload portal. In your organization go to Data > Upload portals and choose Create new upload portal. Here, select a name, a description, the storage bucket, and a path in the storage bucket.

Creating an upload portal

After your portal is created a link is shown. This link contains an authentication token, and can be shared directly with the third party.

An active upload portal

Click the link to open the portal. If you ever forget the link: no worries. Click the ⋮ next to your portal, and choose View portal.

3. Uploading data to the portal

An upload portal with two folders.

To upload data you can now drag & drop files or folders to the drop zone on the right, or use Create new folder to first create a folder structure. There's no limit to the amount of files you can upload here, and all files are hashed, so if you upload a file that's already present the file will be skipped.

Note: Files with the same name but with a different hash are overwritten.

4. Using your portal in transformation blocks / clinical data

If you want to process data in a portal as part of a Clinical Pipeline you can either:

  1. Mount the portal directly into a transformation block via Custom blocks > Transformation blocks > Edit block, and select the portal under mount points.

  2. Mount the bucket that the portal is in, as a transformation block. This will also give you access to all other data in the bucket, very useful if you need to sync other data (see Synchronizing clinical data).

5. Adding the data to your project

If the data in your portal is already in the right format you can also directly import the uploaded data to your project. In your project view, go to Data Sources, **** select 'Upload portal' and follow the steps of the wizard:

Data Sources - Upload portal method

6. Recap

If you need a secure way for external parties to contribute data to your datasets then upload portals are the way to go. They offer a friendly user interface, upload data directly into your storage buckets, and give you an easy way to use the data directly in Edge Impulse. 🚀

Any questions, or interested in the enterprise version of Edge Impulse? Contact us for more information.

Appendix A: Programmatic access to portals

Here's a Python script which uploads, lists and downloads data to a portal. To upload data you'll need to authenticate with a JWT token, see below this script for more info.

# portal_api.py

import requests
import json
import os
import hashlib

PORTAL_TOKEN = os.environ.get('EI_PORTAL_TOKEN')
PORTAL_ID = os.environ.get('EI_PORTAL_ID')
JWT_TOKEN = os.environ.get('EI_JWT_TOKEN')

if not PORTAL_TOKEN:
    print('Missing EI_PORTAL_TOKEN environmental variable.')
    print('Go to a portal, and copy the part after "?token=" .')
    print('Then run:')
    print('    export EI_PORTAL_TOKEN=ec61e...')
    print('')
    print('You can add the line above to your ~/.bashrc or ~/.zshrc file to automatically load the token in the future.')
    exit(1)

if not PORTAL_ID:
    print('Missing EI_PORTAL_ID environmental variable.')
    print('Go to a portal, open the browser console, and look for "portalId" in the "Hello world from Edge Impulse" object to find it.')
    print('Then run:')
    print('    export EI_PORTAL_ID=122')
    print('')
    print('You can add the line above to your ~/.bashrc or ~/.zshrc file to automatically load the token in the future.')
    exit(1)

if not JWT_TOKEN:
    print('WARN: Missing EI_JWT_TOKEN environmental variable, you will only have write-only access to the portal')
    print('Run `python3 get_jwt_token.py` for instructions on how to set the token')
    print('(this requires access to the organization that owns the portal)')

def get_file_hash(path):
    with open(path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()

def create_upload_link(file_name_in_portal, path):
    url = "https://studio.edgeimpulse.com/v1/api/portals/" + PORTAL_ID + "/upload-link"

    payload = json.dumps({
        'fileName': file_name_in_portal,
        "fileSize": os.path.getsize(path),
        "fileHash": get_file_hash(path)
    })
    headers = {
        'accept': "application/json",
        'content-type': "application/json",
        'x-token': PORTAL_TOKEN
    }

    response = requests.request("POST", url, data=payload, headers=headers)

    if (response.status_code != 200):
        raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

    j = response.json()
    if (not j['success']):
        raise Exception('api request did not succeed ' + str(response.status_code) + ' - ' + response.text)

    return j['url']

def upload_file_to_s3(signed_url, path):
    with open(path, 'rb') as f:
        response = requests.request("PUT", signed_url, data=f, headers={})

        if (response.status_code != 200):
            raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

def upload_file_to_portal(file_name_in_portal, path):
    print('Uploading', file_name_in_portal + '...')
    link = create_upload_link(file_name_in_portal, path)
    upload_file_to_s3(link, path)
    print('Uploading', file_name_in_portal, 'OK')
    print('')


def create_download_link(file_name_in_portal):
    url = "https://studio.edgeimpulse.com/v1/api/portals/" + PORTAL_ID + "/files/download"

    payload = json.dumps({
        'path': file_name_in_portal,
    })
    headers = {
        'accept': "application/json",
        'content-type': "application/json",
        'x-token': PORTAL_TOKEN,
        'x-jwt-token': JWT_TOKEN
    }

    response = requests.request("POST", url, data=payload, headers=headers)

    if (response.status_code != 200):
        raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

    j = response.json()
    if (not j['success']):
        raise Exception('api request did not succeed ' + str(response.status_code) + ' - ' + response.text)

    return j['url']

def download_file_from_s3(signed_url):
    response = requests.request("GET", signed_url, headers={})

    if (response.status_code != 200):
        raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

    return response.content

def download_file_from_portal(file_name_in_portal):
    print('Downloading', file_name_in_portal + '...')
    link = create_download_link(file_name_in_portal)
    f = download_file_from_s3(link)
    print('Downloading', file_name_in_portal, 'OK')
    print('')
    return f


def list_files_in_portal(prefix):
    url = "https://studio.edgeimpulse.com/v1/api/portals/" + PORTAL_ID + "/files"

    payload = json.dumps({
        'prefix': prefix,
    })
    headers = {
        'accept': "application/json",
        'content-type': "application/json",
        'x-token': PORTAL_TOKEN
    }

    response = requests.request("POST", url, data=payload, headers=headers)

    if (response.status_code != 200):
        raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

    j = response.json()
    if (not j['success']):
        raise Exception('api request did not succeed ' + str(response.status_code) + ' - ' + response.text)

    return j['files']

# this is how you upload files to a portal using the Edge Impulse API
# first argument is the path in the portal, second is the location of the file
upload_file_to_portal('test.jpg', '/Users/janjongboom/Downloads/test.jpg')

# uploading to a subdirectory
upload_file_to_portal('flowers/daisy.jpg', '/Users/janjongboom/Downloads/daisy-resized.jpg')

# listing files
print('files in root folder', list_files_in_portal(''))
print('files in "flowers/"', list_files_in_portal('flowers/'))

# downloading a file
if JWT_TOKEN:
    buffer = download_file_from_portal('flowers/daisy.jpg')
    with open('output.jpg', 'wb') as f:
        f.write(buffer)
else:
    print('Not downloading files, EI_JWT_TOKEN not set')

print('Done!')

And here's a script to generate JWT tokens:

# get_jwt_token.py

import requests
import json
import argparse

def get_token(username, password):
    url = "https://studio.edgeimpulse.com/v1/api-login"

    payload = json.dumps({
        'username': username,
        "password": password,
    })
    headers = {
        'accept': "application/json",
        'content-type': "application/json",
    }

    response = requests.request("POST", url, data=payload, headers=headers)

    if (response.status_code != 200):
        raise Exception('status code was not 200, but ' + str(response.status_code) + ' - ' + response.text)

    j = response.json()
    if (not j['success']):
        raise Exception('api request did not succeed ' + str(response.status_code) + ' - ' + response.text)

    return j['token']


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Get Edge Impulse JWT token')
    parser.add_argument('--username', type=str, required=True, help="Username or email address")
    parser.add_argument('--password', type=str, required=True)

    args, unknown = parser.parse_known_args()

    token = get_token(args.username, args.password)

    print('JWT token is:', token)
    print('')
    print('Use this in portal_api.py via:')
    print('    export EI_JWT_TOKEN=' + token)
    print('')
    print('You can add the line above to your ~/.bashrc or ~/.zshrc file to automatically load the token in the future.')
    print('Note: This token is valid for a limited time!')

Data acquisition

All collected data for each project can be viewed on the Data acquisition tab. You can see how your data has been split for train/test set as well as the data distribution for each class in your dataset. You can also send new sensor data to your project either by file upload, WebUSB, Edge Impulse API, or Edge Impulse CLI.

Edge Impulse Studio - Data acquisition view.

Add data to your project

Organization data

Since the creation of Edge Impulse, we have been helping our customers deal with complex data pipelines, complex data transformation methods and complex clinical validation studies.

The organizational data gives you tools to centralize, validate and transform datasets so they can be easily imported into your projects.

See the Organization data documentation.

Collect data

The panel on the right allows you to collect data directly from any fully supported platform:

  • Through WebUSB.

  • Using the Edge Impulse CLI daemon.

  • From the Edge Impulse for Linux CLI.

The WebUSB and the Edge Impulse daemon work with any fully supported device by flashing the pre-built Edge Impulse firmware to your board. See the list of fully supported boards.

When using the Edge Impulse for Linux CLI, run edge-impulse-linux --clean and it will add your platform to the device list of your project. You will then will be able to interact with it from the Collect data panel.

Need more?

If your device is not in the officially supported list, you can also collect data using the CLI data forwarder by directly writing the sensor values over a serial connection. The "data forwarder" then signs the data and sends it to the ingestion service.

Upload existing datasets

Edge Impulse also supports different data sample formats and dataset annotation formats (Pascal VOC, YOLO TXT, COCO JSON, Edge Impulse Object Detection, OpenImage CSV) that you can import into your project to build your edge AI models:

  • Studio uploader

  • CLI uploader

  • CSV Wizard

  • Ingestion API

  • Import from cloud storage

  • Upload portals (Enterprise feature)

Edge Impulse Datasets

Need inspiration? Check out our Edge Impulse datasets collection that contains publicly available datasets collected, generated or curated by Edge Impulse or its partners.

These datasets highlight specific use cases, helping you understand the types of data commonly encountered in projects like object detection, audio classification, and visual anomaly detection.

Data sample preview

Time-series data samples

For time-series data samples (including audio), you can visualize the time-series graphs on the right panel with a dark-blue background:

Time-series data sample preview

If you are dealing with multi-label data samples. Here is the corresponding preview:

Multi-label sample preview

Non-time-series & pre-processed data samples

Preview the values of tabular non-time-series & pre-processed data samples:

Tabular data sample preview

Images data samples

Raw images can be directly visualized from the preview:

Raw image sample preview

For object detection projects, we can overlay the corresponding bounding boxes:

Object detection sample preview

Video data samples

Raw videos (.mp4) can be directly visualized from the preview. Please note that you will need to split the videos into frames as we do not support training on videos files:

Video samples preview

Dataset overview

You can change the default view (list) to a grid view to quickly overview your datasets by clicking on the icon.

List view

Edge Impulse Studio - Data acquisition view.

Grid view

Edge Impulse Studio - Data acquisition view.

Dataset train/test split ratio

The train/test split is a technique for training and evaluating the performance of machine learning algorithms. It indicates how your data is split between training and testing samples. For example, an 80/20 split indicates that 80% of the dataset is used for model training purposes while 20% is used for model testing.

This section also shows how your data samples in each class are distributed to prevent imbalanced datasets which might introduce bias during model training.

Rebalance panel

Data acquisition filters

Manually navigating to some categories of data can be time-consuming, especially when dealing with a large dataset. The data acquisition filter enables the user to filter data samples based on some criteria of choice. This can be based on:

  • Label - class to which a sample represents.

  • Sample name - unique ID representing a sample.

  • Signature validity

  • Enabled and disabled samples

  • Length of sample - duration of a sample.

Filters

The filtered samples can then be manipulated by editing labels, deleting, and moving from the training set to the testing set (and vice versa), a shown in the image above.

Data sample actions

The data manipulations above can also be applied at the data sample level by simply navigating to the individual data sample by clicking on "⋮" and selecting the type of action you might want to perform on the specific sample. This might be renaming, editing its label, disabling, cropping, splitting, downloading, and even deleting the sample when desired.

Actions

Edit label(s)

  • Single label

Edit label
  • Multi-label

Edit labels

See Data Acquisition -> Multi-label -> Edit multi-label samples for more information.

Cropping samples

To crop a data sample, go to the sample you want to crop and click ⋮, then select Crop sample. You can specify a length, or drag the handles to resize the window, then move the window around to make your selection.

Made a wrong crop? No problem, just click Crop sample again and you can move your selection around. To undo the crop, just set the sample length to a high number, and the whole sample will be selected again.

Crop

Splitting data sample

Besides cropping you can also split data automatically. Here you can perform one motion repeatedly, or say a keyword over and over again, and the events are detected and can be stored as individual samples. This makes it easy to very quickly build a high-quality dataset of discrete events. To do so head to Data Acquisition, record some new data, click, and select Split sample. You can set the window length, and all events are automatically detected. If you're splitting audio data you can also listen to events by clicking on the window, the audio player is automatically populated with that specific split.

Split

Samples are automatically centered in the window, which might lead to problems on some models (the neural network could learn a shortcut where data in the middle of the window is always associated with a certain label), so you can select "Shift samples" to automatically move the data a little bit around.

Splitting data is - like cropping data - non-destructive. If you're not happy with a split just click Crop sample and you can move the selection around easily.

Labeling tools

The labeling queue will only appear on your data acquisition page if you are dealing with object detection tasks.

If you are not dealing with an object detection task, you can simply change the Labeling method configuration by going to Dashboard > Project info > Labeling method and clicking the dropdown and selecting "one label per data item" as shown in the image below.

Labeling method

Also, see our Label image data using GPT-4o tutorial to see how to leverage the power of LLMs to automatically label your data samples based on simple prompts.

Using the Edge Impulse Python SDK with SageMaker Studio

Amazon SageMaker Studio is an integrated development environment (IDE) that provides a single web-based visual interface where you can access purpose-built tools to perform all machine learning (ML) development steps, from preparing data to building, training, and deploying your ML models, improving data science team productivity by up to 10x. You can quickly upload data, create new notebooks, train and tune models, move back and forth between steps to adjust experiments, collaborate seamlessly within your organization, and deploy models to production without leaving SageMaker Studio.

To learn more about using the Python SDK, please see: .

This guide has been built from AWS reference project Introduction to SageMaker TensorFlow - Image Classification, please have a look at this .

Below are the changes made to the original training script and configuration:

  • The Python 3 (Data Science 3.0) kernel was used.

  • We used a dataset to classify images as car vs unknown. You can check out to get started and store it in your S3 bucket.

  • The dataset has been imported in the Edge Impulse S3 bucket configured when creating the SageMaker Studio domain. Make sure to adapt to your path or use the AWS reference project.

  • The training instance used is ml.m5.large.

Install dependencies

Transfer Learning

Dataset

Below is the structure of our dataset in our S3 bucket

We have used the default bucket created when configuring SageMaker Studio domain:

Train

You can continue with the default model, or can choose a different model from the list. Note that this tutorial has been tested with MobileNetv2 based models. A complete list of SageMaker pre-trained models can also be accessed at .

Optional, ship this next cell if you don't want to retrain the model. And uncomment the last line of the cell after

Retrieve and prepare the newly trained model

Edge Impulse

You will need to obtain an API key from an Edge Impulse project. Log into and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

Voila! You now have a C++ library ready to be compiled and integrated in your embedded targets. Feel free to have a look at Edge Impulse deployment options on the to understand how you can integrate that to your embedded systems.

You can also have a look at the deployment page of your project to test your model on a web browser or test

plans and pricing
Enterprise trial
custom blocks
custom blocks
Setting up mount point
train_input.json
custom blocks
custom blocks
plans and pricing
Enterprise trial
# If you have not done so already, install the following dependencies
!python -m pip install tensorflow==2.12.0 edgeimpulse
car-vs-unknown
    |--training
        |--car
            |--abc.jpg
            |--def.jpg
        |--unknown
            |--ghi.jpg
            |--jkl.jpg
    |--testing
            |--car
                |--mno.jpg
                |--prs.jpg
            |--unknown
                |--tuv.jpg
                |--wxy.jpg
import sagemaker, boto3, json
from sagemaker.session import Session

sagemaker_session = Session()
aws_role = sagemaker_session.get_caller_identity_arn()
aws_region = boto3.Session().region_name
print(aws_region)
sess = sagemaker.Session()
bucket = sess.default_bucket()
subfolder = 'car-vs-unknown/training/'

s3 = boto3.client('s3')
files = s3.list_objects(Bucket=bucket, Prefix=subfolder)['Contents']
print(f"Number of images: {len(files)}")
# or print the files
# for f in files:
#     print(f['Key'])
from sagemaker.jumpstart.notebook_utils import list_jumpstart_models

# Retrieves all image classification models available by SageMaker Built-In Algorithms.
filter_value = "task in ['ic']"
ic_models = list_jumpstart_models(filter=filter_value)
# od_models = list_jumpstart_models()

print(f"Number of models available for inference: {len(ic_models)}")

# display the model-ids.
for model in ic_models:
    print(model)
from sagemaker import image_uris, model_uris

model_id, model_version = "tensorflow-ic-imagenet-mobilenet-v3-small-075-224", "*" # You can change the based model with one from the list generated above

# Retrieve the base model uri
base_model_uri = model_uris.retrieve( model_id=model_id, model_version=model_version, model_scope="inference")

print(base_model_uri)
from sagemaker import image_uris, model_uris, script_uris, hyperparameters
from sagemaker.estimator import Estimator

training_instance_type = "ml.m5.large"

# Retrieve the Docker image
train_image_uri = image_uris.retrieve(model_id=model_id,model_version=model_version,image_scope="training",instance_type=training_instance_type,region=None,framework=None)

# Retrieve the training script
train_source_uri = script_uris.retrieve(model_id=model_id, model_version=model_version, script_scope="training")

# Retrieve the pretrained model tarball for transfer learning
train_model_uri = model_uris.retrieve(model_id=model_id, model_version=model_version, model_scope="training")

# Retrieve the default hyper-parameters for fine-tuning the model
hyperparameters = hyperparameters.retrieve_default(model_id=model_id, model_version=model_version)

# [Optional] Override default hyperparameters with custom values
hyperparameters["epochs"] = "5"

# The sample training data is available in the following S3 bucket
training_data_bucket = f"{bucket}"
training_data_prefix = f"{subfolder}"
# training_data_bucket = f"jumpstart-cache-prod-{aws_region}"
# training_data_prefix = "training-datasets/tf_flowers/"

training_dataset_s3_path = f"s3://{training_data_bucket}/{training_data_prefix}"

output_bucket = sess.default_bucket()
output_prefix = "ic-car-vs-unknown"
s3_output_location = f"s3://{output_bucket}/{output_prefix}/output"

# Create SageMaker Estimator instance
tf_ic_estimator = Estimator(
    role=aws_role,
    image_uri=train_image_uri,
    source_dir=train_source_uri,
    model_uri=train_model_uri,
    entry_point="transfer_learning.py",
    instance_count=1,
    instance_type=training_instance_type,
    max_run=360000,
    hyperparameters=hyperparameters,
    output_path=s3_output_location
)

# Use S3 path of the training data to launch SageMaker TrainingJob
tf_ic_estimator.fit({"training": training_dataset_s3_path}, logs=True)
def download_from_s3(url):
    # Remove 's3://' prefix from URL
    url = url[5:]
    # Split URL by '/' to extract bucket name and key
    parts = url.split('/')
    bucket_name = parts[0]
    s3_key = '/'.join(parts[1:])
    # Download the file from S3
    s3.download_file(bucket_name, s3_key, 'model.tar.gz')

# Download
trained_model_s3_path = f"{s3_output_location}/{tf_ic_estimator._current_job_name}/output/model.tar.gz"
print(trained_model_s3_path)
download_from_s3(trained_model_s3_path)
# or if you just want to use the based model
#download_from_s3(base_model_uri)
import shutil, os

# Extract the .tar.gz file to a temporary directory
temp_directory = 'tmp'  # Replace with your actual temporary directory
tar_gz_file = 'model.tar.gz'  # Replace with the path to your .tar.gz file

# Create directory if does not exist
if not os.path.exists(temp_directory):
    os.makedirs(temp_directory)

shutil.unpack_archive(tar_gz_file, temp_directory)
import tensorflow as tf

print(tf.__version__)

model = tf.keras.models.load_model('tmp/1/')

converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()

# Save the model.
with open('model.tflite', 'wb') as f:
  f.write(tflite_model)
import edgeimpulse as ei
ei.API_KEY = "ei_0a85c3a5ca92a35ee6f61aab18aadb9d9e167bd152f947f2056a4fb6a60977d8" # Change to your key
ei.model.list_profile_devices()
# Estimate the RAM, ROM, and inference time for our model on the target hardware family
try:
    profile = ei.model.profile(model='model.tflite',
                               device='raspberry-pi-4')
    print(profile.summary())
except Exception as e:
    print(f"Could not profile: {e}")
# List the available profile target devices
ei.model.list_deployment_targets()
# Get the labels from the label_info.json
import json

labels_info = open('tmp/labels_info.json')
labels_obj = json.load(labels_info)
labels = labels_obj['labels']
print(labels)
# Set model information, such as your list of labels
model_output_type = ei.model.output_type.Classification(labels=labels)
deploy_filename = "my_model_cpp.zip"

# Create C++ library with trained model
deploy_bytes = None
try:

    deploy_bytes = ei.model.deploy(model=model,
                                   model_output_type=model_output_type,
                                   engine='tflite',
                                   deploy_target='zip"')
except Exception as e:
    print(f"Could not deploy: {e}")

# Write the downloaded raw bytes to a file
if deploy_bytes:
    with open(deploy_filename, 'wb') as f:
        f.write(deploy_bytes.getvalue())
Edge Impulse Python SDK Overview
AWS documentation page
image classification datasets
Sagemaker pre-trained Models
edgeimpulse.com
documentation
SageMaker Studio
S3 Bucket overview
Copy API key from Edge Impulse project

Regression + anomaly detection

This tutorial demonstrates how to implement and integrate anomaly detection on an Arduino Opta PLC using Edge Impulse. Anomaly detection is a machine learning technique that identifies unusual patterns in data, making it ideal for monitoring industrial processes and equipment. By training a model to recognize normal behavior, you can detect deviations that may indicate faults or malfunctions.

Arduino OPTA plc

Overview

To showcase how easy it is to integrate Edge Impulse with the Arduino Opta PLC, we'll walk through a practical example using the Arduino DIN Celsius board that comes with the kit, but also a Motor to demonstrate this setup can be used interchangeably. This example demonstrates how to set up a temperature-controlled system, collect data, train a machine learning model, and deploy it for anomaly detection.

In this tutorial, you will:

  • Collect temperature data from a sensor connected to the Opta PLC.

  • Train a machine learning model in Edge Impulse to detect anomalies in the data.

  • Deploy the model to the Opta PLC for real-time inference.

  • Optionally, integrate the model with Arduino Cloud for remote monitoring and visualization.

Webinar: Integrating Edge Impulse with Arduino Opta PLC and Blues Wireless

We ran a webinar on integrating Edge Impulse with Arduino Opta PLC and Blues Wireless for remote monitoring and anomaly detection. The webinar covered the following topics:

  • Arduino Opta PLC: Learn about the flexibility and ease of integration of the Arduino Opta micro PLC, designed for industrial environments.

  • Edge Impulse for Machine Learning: Understand how to train and implement ML models for anomaly detection on industrial data directly on your Opta PLC.

  • Blues for Wireless Connectivity: Explore how Blues' Wireless for PLC Expansion enables secure cellular connectivity, allowing your Opta PLC to communicate with cloud-based monitoring systems.

The webinar is now available on-demand here.

Prerequisites

Hardware

  • Arduino Opta PLC (Wi-Fi version recommended)

  • Opta PLC Starter Kit

Software

  • Arduino IDE 2 (Download here)

  • Edge Impulse account (Sign up here)

  • Edge Impulse CLI (Installation guide

Step 1: Set Up Your Hardware

Hardware Setup

About the DIN Celsius Board

DIN Celsius

The DIN Celsius is an all-in-one temperature laboratory offering two independent heaters and a temperature sensor. It allows you to simulate heating scenarios and monitor temperature changes, making it ideal for testing our anomaly detection use case, we can introduce an anomaly by turning off one of the heaters to cause a deviation from the normal condition.

Connecting the DIN Celsius to the Opta PLC

Safety First: Before making any connections, ensure that all power sources are disconnected to prevent electric shock or short circuits.

Connections Overview:

Power Connections:

  • Connect the +24V and GND terminals of the DIN Celsius to the corresponding power supply pins.

Heater Control:

  • Connect Relay 3 (pin 2) on the Opta PLC to Input Heat 1 on the DIN Celsius.

  • Connect Relay 4 (pin 3) on the Opta PLC to Input Heat 2 on the DIN Celsius.

  • These connections will control the two independent heaters on the DIN Celsius.

Temperature Sensor Input:

  • Connect the Output Voltage from the DIN Celsius to the I8 input (analog pin A7) on the Opta PLC.

  • This connection allows the PLC to read the temperature sensor data.

Pin Definitions:

  • HEAT_LEFT (Relay 3) connected to pin 2

  • HEAT_RIGHT (Relay 4) connected to pin 3

  • TEMP_SENS connected to analog pin A7

  • BTN (User Button) on the Opta PLC

Wiring Diagram

Step 2: Set Up Your Software

Arduino IDE Configuration

Install Necessary Libraries:

  • Ensure you have the latest Arduino IDE 2 installed.

  • Install any required libraries via the Library Manager, such as Edge Impulse SDK and Arduino_HTS221 if using temperature/humidity sensors.

Define Pin Constants:

#define BTN         BTN_USER       // User button on Opta WiFi
#define HEAT_LEFT   2              // Left heater control pin
#define HEAT_RIGHT  3              // Right heater control pin
#define TEMP_SENS   A7             // Temperature sensor analog pin

Testing the Connections

Heater Control Test Code:

void setup() {
  pinMode(HEAT_LEFT, OUTPUT);
  pinMode(HEAT_RIGHT, OUTPUT);
}

void loop() {
  digitalWrite(HEAT_LEFT, HIGH);
  digitalWrite(HEAT_RIGHT, HIGH);
  delay(1000);
  digitalWrite(HEAT_LEFT, LOW);
  digitalWrite(HEAT_RIGHT, LOW);
  delay(1000);
}
  • Upload the Code: Use the Arduino IDE to upload the sketch to the Opta PLC.

  • Verify Operation: The LEDs on the DIN Celsius should blink, indicating the heaters are being activated.

Temperature Sensor Reading Test:

void setup() {
  Serial.begin(9600);
  pinMode(TEMP_SENS, INPUT);
}

void loop() {
  int sensorValue = analogRead(TEMP_SENS);
  Serial.println(sensorValue);
  delay(250);
}
  • Open Serial Monitor: Set the baud rate to 9600.

  • Observe Readings: You should see numerical values corresponding to the temperature sensor output.

Step 3: Collecting Data with Edge Impulse

Create a New Project

  • Log In: Access your Edge Impulse account.

  • New Project: Create a project named, for example, "Opta PLC Anomaly Detection."

Data Forwarding Sketch

Upload a sketch to the Opta PLC that reads the temperature sensor and sends data to Edge Impulse.

Steps:

  1. Upload the Data Forwarder Sketch: Use the Arduino IDE to upload the sketch to the Opta PLC.

  2. Run Data Forwarder: In your terminal, execute the data forwarding command.

  3. Select Serial Port: Choose the serial port corresponding to the Opta PLC.

  4. Label the Data: As you collect data, assign labels to your data (e.g., "normal," "anomalous") based on the system's behavior.

Note: If you are new to Edge Impulse, please refer to our CLI Data Forwarder documentation for detailed instructions.

Toggle to expand - Data Forwarder Sketch

Data Forwarder Sketch:

void setup() {
  Serial.begin(115200);
  while (!Serial);

  pinMode(TEMP_SENS, INPUT);
  Serial.println("Edge Impulse Data Forwarder Example");
}

void loop() {
  float temperature = analogRead(TEMP_SENS) * (10.0 / 1024.0); // Convert to voltage
  Serial.print("Temperature: ");
  Serial.println(temperature);

  // Send data in edge-impulse-data-forwarder format
  Serial.print("1,");
  Serial.println(temperature);

  delay(1000);
}
  • Run Data Forwarder: In your terminal, execute:

    edge-impulse-data-forwarder
  • Select Serial Port: Choose the serial port corresponding to the Opta PLC.

  • Label the Data: Assign labels to your data (e.g., "normal," "anomalous") as you collect it.

If you are new to Edge Impulse please see our CLI Data Forwarder documentation.

Step 4: Training the Machine Learning Model

Create an Impulse

Add Blocks: 2. Add Blocks:

  • Processing Block: Select a Time Series or Spectral Analysis block based on your data characteristics.

  • Learning Block: Choose Anomaly Detection using the Gaussian Mixture Model (GMM).

Impulse Studio

Configure the Impulse

  • Window Size: Set according to the data frequency (e.g., 1000 ms).

  • Window Increase: Set overlap (e.g., 500 ms).

Generate Features

  • Compute Features: Navigate to the Generate Features tab and run feature generation.

Generate Features

Train the Model

Training Parameters:

  • Epochs: Start with 100 and adjust as needed.

  • Learning Rate: Default of 0.005 is usually sufficient.Start Training: Monitor the accuracy and loss graphs.

Training Model

Validate the Model

  • Model Testing: Use a separate dataset to evaluate model performance.

  • Adjust if Necessary: Retrain or adjust parameters based on results.

Model Validation

Step 5: Deploying the Model to the Opta PLC

Download the Arduino Library

  • Deployment Tab: In Edge Impulse, go to Deployment.

  • Select Arduino Library: Download the library tailored for your model.

Download Library

Read on here

Include the Library in Your Sketch

  • Add Library to IDE: Import the downloaded library into your Arduino IDE.

Add the Inference Code

If you are new to Arduino inference code, see our Arduino inference code documentation here for more information.

Toggle to expand - Inference Code Example

Inference Code Example:

#include <Your_Edge_Impulse_Inference_Library.h>
#include <edge-impulse-sdk/classifier/ei_run_classifier.h>

// Define the feature buffer
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];

void setup() {
  Serial.begin(115200);
  while (!Serial);

  pinMode(TEMP_SENS, INPUT);
  Serial.println("Edge Impulse Inference Example");
}

void loop() {
  // Collect data
  for (size_t i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i++) {
    features[i] = analogRead(TEMP_SENS) * (10.0 / 1024.0);
    delay(10); // Adjust delay as needed
  }

  // Prepare signal
  signal_t signal;
  numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);

  // Run inference
  ei_impulse_result_t result = { 0 };
  EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);

  if (res != EI_IMPULSE_OK) {
    Serial.print("ERR: Failed to run classifier (");
    Serial.print(res);
    Serial.println(")");
    return;
  }

  // Print results
  Serial.println("Inference results:");
  for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
    Serial.print(result.classification[ix].label);
    Serial.print(": ");
    Serial.println(result.classification[ix].value);
  }

  // Control heaters based on anomaly detection
  if (result.anomaly > ANOMALY_THRESHOLD) {
    // Anomaly detected, take action
    Serial.println("Anomaly detected!");
    digitalWrite(HEAT_LEFT, LOW);
    digitalWrite(HEAT_RIGHT, LOW);
  } else {
    // Normal operation
    digitalWrite(HEAT_LEFT, HIGH);
    digitalWrite(HEAT_RIGHT, HIGH);
  }

  delay(1000);
}
  • Replace Your_Edge_Impulse_Inference_Library.h with the actual header file name from your downloaded library.

  • Set ANOMALY_THRESHOLD to an appropriate value based on your model's performance.

Upload the Sketch

  • Compile and Upload: Use the Arduino IDE to program the Opta PLC with your new inference code.

  • Monitor Output: Open the Serial Monitor to observe inference results and system behavior.

Step 6: Viewing Data on Arduino Cloud via Blues Wireless (Optional)

This section demonstrates integrating Blues Wireless for remote connectivity and monitoring. By connecting your Opta PLC to the cloud, you can visualize data, receive alerts, and monitor system performance from anywhere.

Arduino Cloud

See the full Arduino OPTA for Arduino Cloud guide here.

Blues for Wireless Connectivity:

Blues Wireless

Explore how Blues' Wireless for PLC Expansion enables seamless, secure cellular connectivity, allowing your Opta PLC to communicate with your cloud-based monitoring systems, regardless of location and without the hassle of local Wi-Fi.

See the Blues Wireless here.

This section will be updated following the Blues Wireless Webinar on the 30th of October Sign up here

Step 7: Integration with Ladder Logic (Optional)

In this section, we will integrate the anomaly detection model deployed on the Arduino Opta PLC with a ladder logic program. This integration will allow the machine learning inferences to interact with the PLC's native ladder logic control system, providing intelligent control responses to anomalies in temperature or motor power.

Overview of Ladder Logic Integration

The Arduino PLC IDE allows you to combine an Arduino sketch with ladder logic using Shared Variables. These shared variables act as a bridge between the Edge Impulse inference running on the PLC and the PLC's control logic written in Ladder Diagram (LD) or other IEC-61131-3 programming languages (such as Function Block Diagram or Structured Text). This enables real-time decision-making based on the machine learning model's output.

Steps for Integration

Create Shared Variables

In the Arduino PLC IDE, navigate to the Global_vars section to create shared variables that will store the results from the Edge Impulse inference. Define a shared variable for the anomaly score or classification output of the model.

Example:

Shared variable for storing inference result (e.g., float anomaly_score).

Modify Inference Sketch

Update the Edge Impulse inference sketch to store the inference result in the shared variable. This will allow the ladder logic to access the result.

Example:

float anomaly_score;  // Shared variable to store the inference result

void setup() {
  Serial.begin(115200);
  pinMode(A7, INPUT);  // Sensor input
}

void loop() {
  float sensorValue = analogRead(A7) * (10.0 / 1024.0);  // Read sensor value
  anomaly_score = run_inference(sensorValue);  // Store inference result
  delay(1000);  // Adjust as needed
}

Ladder Logic Program

In the PLC IDE, create a new ladder logic program that will read the anomaly_score shared variable. The logic can then trigger actions based on the value, such as activating relays, generating alarms, or shutting down equipment in response to detected anomalies.

Example Ladder Logic:

Create a rung that monitors the anomaly_score. If the score exceeds a certain threshold, the logic can trigger an alarm (e.g., turn on an LED or activate a relay).

Ladder Logic - Anomaly integration

Add Inputs and Outputs:

  • Define Inputs (e.g., sensor values, inference results from the Arduino sketch) and Outputs (e.g., control signals like turning on a relay).

  • Click Add in the "Shared Inputs" and "Shared Outputs" sections to create global variables. These variables will allow communication between your inference sketch and ladder logic.

Shared Variables

Set up the Inputs/Outputs:

  • Inputs: Define variables that will receive values from the Arduino sketch (e.g., anomaly_score).

  • Outputs: Define variables that will control actuators (e.g., relay_control).

Step 2: Accessing the Ladder Logic Editor

Create a New Ladder Logic Program:

  • Go to the Project tab (top-left section).

  • Right-click on Programs and select New Program.

  • Name the program (e.g., AnomalyDetection_LD) and select Ladder Diagram (LD) as the language.

Opening the Ladder Logic Editor:

  • Once the program is created, double-click it to open the Ladder Diagram editor. You will see a canvas where you can start adding blocks.

Step 3: Designing the Ladder Logic

Ladder Logic Editor

Drag and Drop Components:

  • On the right panel under the Library Tree, you can see various block types, such as Comparison, Logic, and Arithmetic.

  • Comparison blocks will allow you to compare the input (e.g., anomaly_score) to a threshold (e.g., >=).

Creating Logic:

  • Input Condition: Drag a Comparison block (e.g., >=) to compare the anomaly_score to a threshold (e.g., 0.8).

  • Output Control: Connect the result of the comparison to an Output coil that controls the relay (e.g., relay_control).

Steps for Adding Logic:

  • Input: Select anomaly_score as the input to the comparison block.

  • Condition: Set the threshold (e.g., >= 0.8).

  • Output: Set the output to control a relay (e.g., activate relay_control when the condition is met).

Assigning the Ladder Logic to a Task

In the PLC IDE, assign the Arduino sketch (which runs the inference) to a task such as Fast (runs every 10ms) or Background (runs every 500ms) based on your system’s real-time requirements. Attach the ladder logic program to the same or another appropriate task to ensure it reacts to the updated shared variables in a timely manner.

Steps:

  • Go to the Project tab and locate the Tasks section.

  • Assign the ladder logic program (e.g., AnomalyDetection_LD) to an appropriate task (e.g., Fast Task for real-time control).

Ladder Logic - Sketch Integration

Monitor and Debug

Use the Watch window in the PLC IDE to monitor the shared variables and ensure the system responds correctly to anomalies detected by the machine learning model.

Upload the Arduino Sketch:

  • Ensure your Arduino sketch is uploaded to the Opta PLC to provide the inference results.

Run the Ladder Logic:

  • Start the ladder logic program from the Arduino PLC IDE.

  • Monitor the shared variables using the Watch window to see how the ladder logic reacts to the inference results.

Benefits of Ladder Logic Integration

  • Real-time Control: By integrating anomaly detection with ladder logic, you can implement real-time, intelligent control systems that take action based on data-driven decisions from the Edge Impulse model.

  • Easy Troubleshooting: Using ladder logic alongside machine learning allows for clear, visual representation of control logic, making it easier to debug and monitor the system's responses to anomalies.

  • Seamless PLC Integration: The Arduino PLC IDE provides a smooth environment for combining traditional control logic with modern machine learning, ensuring compatibility and ease of use.

Later will will revisit this with C++ and Siemens PLCs, for now, you can explore the Arduino PLC IDE.

Conclusion

Congratulations! You have successfully implemented anomaly detection on an Arduino Opta PLC using Edge Impulse. This tutorial demonstrates the seamless integration of machine learning models with industrial automation systems, enabling real-time monitoring and fault detection.

Please share your experiences and feedback with us on the Edge Impulse forum.

For more information on Edge Impulse and Arduino Opta PLC, visit the official websites:

  • Arduino Opta PLC

Troubleshooting

  • Arduino Cloud Issues: We are aware of issues with Arduino Cloud .properties file format vs IDE and are working with Arduino. If you have issues try moving the .properties file to the same folder as the .ino file and re-uploading the sketch.

Generate timeseries data with MATLAB

MATLAB is a powerful tool for generating synthetic motion data for machine learning applications. With built-in functions such as the Signal Processing Toolbox and Image Processing Toolbox and capabilities, MATLAB makes it easy to simulate real-world sensor data, generate labelled datasets, and preprocess data for edge AI applications.

In this tutorial, you will learn how to:

  1. Define simulation parameters (sampling frequency, signal duration, random seed).

  2. Generate multiple motion classes (e.g., “idle,” “snake,” “up-down”).

  3. Add realistic noise and emulate sensor characteristics.

  4. Label data automatically in MATLAB.

  5. Save your signals to CSV.

  6. Import the labeled CSV into Edge Impulse.

Public project Dataset - Studio updown

Example: Generating continuous motion data (up-down snake wave and idle) in MATLAB

Below, we recreate a continuous motion sample project that could be used to test wearable sensors, monitor vibrations, or simulate small repetitive movements in an industrial setting.

MATLAB Synthetic Motion Data

You can also clone the public project dataset to follow along: MATLAB: Synthetic Data Generation - Continuous motion recognition.

MATLAB Online

You can run MATLAB entirely in the browser using MATLAB Online. This makes it easy to share your project, collaborate, or quickly try out scripts without installing anything locally.

Prerequisites

You will need:

  • A MATLAB license or MathWorks account to access MATLAB Online.

  • Our synthetic motion generation script see the public project description for the full script.

Getting Started

  1. Open MATLAB or go to MATLAB Online.

  2. Create a new Live Script (.mlx) or MATLAB script (.m).

  3. Copy-paste the synthetic motion generation code from the public project description for continuous motion data.

MATLAB Script

Customizing parameters to simulate motion data

There are several parameters to consider when generating synthetic motion data. You can customize:

  • Sampling Frequency (fs): Controls how frequently data is sampled (e.g., 62.5 Hz).

  • Total Duration (t_end): How long the simulated signal is (e.g., 15 minutes).

  • Types of Motion:

    • Up-Down Motion: Simple sinusoidal vertical oscillations.

    • Snake Motion: Horizontal oscillation with amplitude modulation.

    • Wave Motion: Circular or elliptical motion in the XY-plane.

  • Add realistic noise or drift to make the data look more authentic. Consider sensor-specific noise levels and random jitter.

In this basic example we generate a simple up-down motion, but you can extend this to include more complex motions or multiple classes of motion.

Script overview

Running the Script: Generates labeled time-series data (e.g., idle, snake, updown, wave).Save to CSV: The script automatically writes to motion_data.csv.Visualize: MATLAB’s plot and subplot functions help verify that the signals make sense.

Below is a minimal code snippet compare with your own script for advanced features:

% Clear workspace to start fresh
clear; clc;

% Sampling settings
fs = 62.5;
t_end = 15 * 60; 
t = 0:1/fs:t_end-1/fs; 

% Generate signals
jitter = 0.1 * randn(size(t));    % random noise
updown_z = 0.8 * sin(2*pi*0.5 * t) + jitter;  % up-down motion

% Combine or loop over motions
combined_data = [t' updown_z'];
csv_filename = 'motion_data.csv';
writematrix(combined_data, csv_filename);

disp(['Data saved as ', csv_filename]);

% Quick plot
figure;
plot(t, updown_z, 'LineWidth', 1.5);
xlabel('Time (s)');
ylabel('Amplitude');
title('Synthetic Up-Down Motion');

Multi-axis acc_x, acc_y, acc_z example

In many real applications, you have multiple axes (e.g., acc_x, acc_y, acc_z). You can extend the same logic for each axis:

rng(0); % For reproducibility
fs = 62.5;
t_end = 15; 
t = 0:1/fs:t_end-1/fs;

% Generate multiple axes
noise_x = 0.05*randn(size(t));
noise_y = 0.05*randn(size(t));
noise_z = 0.05*randn(size(t));

signal_x = sin(2*pi*1.0 * t) + noise_x;
signal_y = 0.5 * sin(2*pi*0.5 * t) + noise_y;
signal_z = 0.8 * sin(2*pi*0.2 * t) + noise_z;

combined_data_3axis = [t' signal_x' signal_y' signal_z'];

Generating and Labeling Multiple Classes

If you want to generate multiple classes like idle, snake, updown, wave you can segment your time vector and assign labels programmatically.

Here’s a minimal example:

% Clear workspace
clear; clc; rng(0); % set random seed

% Sampling settings
fs = 62.5;
t_end = 60; % 1 minute total for example
t = 0:1/fs:t_end-1/fs;

% Pre-allocate signal arrays
motion_signal = zeros(size(t));
labels = strings(size(t)); % label each sample

% Example: define time ranges for each motion
idle_duration = 10;      % first 10s idle
snake_duration = 20;     % next 20s snake
updown_duration = 30;    % last 30s up-down

% Generate Idle
idle_idx = t <= idle_duration;
motion_signal(idle_idx) = 0 + 0.05*randn(1, sum(idle_idx)); 
labels(idle_idx) = "idle";

% Generate Snake motion
snake_idx = t > idle_duration & t <= (idle_duration + snake_duration);
snake_t = t(snake_idx) - idle_duration;
motion_signal(snake_idx) = 0.3 * sin(2*pi*0.8 * snake_t) + 0.05*randn(size(snake_t));
labels(snake_idx) = "snake";

% Generate Up-Down motion
updown_idx = t > (idle_duration + snake_duration);
updown_t = t(updown_idx) - (idle_duration + snake_duration);
motion_signal(updown_idx) = 0.8 * sin(2*pi*0.5 * updown_t) + 0.1*randn(size(updown_t));
labels(updown_idx) = "updown";

% Combine data and labels into one matrix
combined_data = [t' motion_signal'];
csv_filename = 'motion_data.csv';

% Write numeric data
writematrix(combined_data, csv_filename);

% Write labels in a second file or append to CSV with e.g. "writetable" 
T = table(t', motion_signal', labels', 'VariableNames', {'time','motion','label'});
writetable(T, 'motion_data_labeled.csv');

disp(['Data saved as ', csv_filename, ' and motion_data_labeled.csv']);

% Quick plot
figure;
plot(t, motion_signal, 'LineWidth', 1.2);
xlabel('Time (s)');
ylabel('Amplitude');
title('Synthetic Multi-Class Motion');

In practice, you can repeat this process for each axis (e.g., x, y, z) and store all signals plus labels in a single table.

Benefits of using MATLAB for time-series data generation

  • Enhance Data Quality: Create reliable time-series signals that closely mimic real-world conditions.

  • Increase Dataset Diversity: Generate multiple classes of motion, from subtle vibrations to large, sinusoidal oscillations.

  • Save Time and Resources: No need to set up physical experiments to capture sensor data—scripted generation is repeatable and cost-effective.

  • Improve Model Accuracy: High-quality, diverse signals help close dataset gaps, reducing overfitting and improving real-world performance.

Importing synthetic motion data into Edge Impulse

Once you have your synthetic data, you can use it to train a model in Edge Impulse. Check out the Continuous motion recognition project for a complete example.

Use the CSV Wizard to import your data into a project.

Now that you have your synthetic motion data, you can import it into your project using the CSV Wizard.

CSV - Step 1
  1. Open the Edge Impulse Studio and navigate to the Data Acquisition tab.

CSV - Step 2
  1. Click on the CSV Wizard.

CSV - Step 3
  1. Upload the motion_data.csv file.

CSV - Step 4
  1. Follow the steps to label and import the data.

Public project Dataset - Studio wave
  1. Once the data is imported, you can start training your model using the synthetic motion data.

Create Impulse
  1. Configure the model settings and train the model using the synthetic motion data.

For more advanced motion data generation, consider adding sensor noise, drift, or more complex motion patterns.

Next steps

For a more advanced example, see the public project:Rolling Element Bearing Fault Diagnosis that uses MATLAB to generate synthetic vibration data for bearing fault detection, based on the MATLAB Rolling Element Bearing Fault Diagnosis example.

Conclusion

By leveraging MATLAB for synthetic data generation, you can rapidly prototype and iterate without the overhead of physical sensors or mechanical rigs. This approach helps fill in dataset gaps, improves model robustness, and speeds up development cycles. Please share your own experience with MATLAB and other uses or projects with us on our forum.

Further reading

  • Integrate Custom MATLAB DSP blocks in Edge Impulse for advanced preprocessing before training your models. Check out the MATLAB DSP custom processing block.

  • Bearing wear analysis: Public Project

Sound recognition

In this tutorial, you'll use machine learning to build a system that can recognize when a particular sound is happening—a task known as audio classification. The system you create will be able to recognize the sound of water running from a faucet, even in the presence of other background noise.

You'll learn how to collect audio data from microphones, use signal processing to extract the most important information, and train a deep neural network that can tell you whether the sound of running water can be heard in a given clip of audio. Finally, you'll deploy the system to an embedded device and evaluate how well it works.

At the end of this tutorial, you'll have a firm understanding of how to classify audio using Edge Impulse.

There is also a video version of this tutorial:

You can view the finished project, including all data, signal processing and machine learning blocks here: .

Detecting human speech?

Do you want a device that listens to your voice? We have a specific tutorial for that! See .

1. Prerequisites

For this tutorial, you'll need a .

If you don't see your supported development board listed here, be sure to check the page for the appropriate tutorial.

If your device is connected under Devices in the studio you can proceed:

Device compatibility

Edge Impulse can ingest data from any device - including embedded devices that you already have in production. See the documentation for the for more information.

2. Collecting your first data

To build this project, you'll need to collect some audio data that will be used to train the machine learning model. Since the goal is to detect the sound of a running faucet, you'll need to collect some examples of that. You'll also need some examples of typical background noise that doesn't contain the sound of a faucet, so the model can learn to discriminate between the two. These two types of examples represent the two classes we'll be training our model to detect: background noise, or running faucet.

You can use your device to collect some data. In the studio, go to the Data acquisition tab. This is the place where all your raw data is stored, and - if your device is connected to the remote management API - where you can start sampling new data.

Let's start by recording an example of background noise that doesn't contain the sound of a running faucet. Under Record new data, select your device, set the label to noise, the sample length to 1000, and the sensor to Built-in microphone. This indicates that you want to record 1 second of audio, and label the recorded data as noise. You can later edit these labels if needed.

After you click Start sampling, the device will capture a second of audio and transmit it to Edge Impulse. The LED will light while recording is in progress, then light again during transmission.

When the data has been uploaded, you will see a new line appear under 'Collected data'. You will also see the waveform of the audio in the 'RAW DATA' box. You can use the controls underneath to listen to the audio that was captured.

3. Build a dataset

Since you now know how to capture audio with Edge Impulse, it's time to start building a dataset. For a simple audio classification model like this one, we should aim to capture around 10 minutes of data. We have two classes, and it's ideal if our data is balanced equally between each of them. This means we should aim to capture the following data:

  • 5 minutes of background noise, with the label "noise"

  • 5 minutes of running faucet noise, with the label "faucet"

Real world data

In the real world, there are usually additional sounds present alongside the sounds we care about. For example, a running faucet is often accompanied by the sound of dishes being washed, teeth being brushed, or a conversation in the kitchen. Background noise might also include the sounds of television, kids playing, or cars driving past outside.

It's important that your training data contains these types of real world sounds. If your model is not exposed to them during training, it will not learn to take them into account, and it will not perform well during real-world usage.

For this tutorial, you should try to capture the following:

  • Background noise

    • 2 minutes of background noise without much additional activity

    • 1 minute of background noise with a TV or music playing

    • 1 minute of background noise featuring occasional talking or conversation

    • 1 minutes of background noise with the sounds of housework

  • Running faucet noise

    • 1 minute of a faucet running

    • 1 minute of a different faucet running

    • 1 minute of a faucet running with a TV or music playing

    • 1 minute of a faucet running with occasional talking or conversation

    • 1 minute of a faucet running with the sounds of housework

It's okay if you can't get all of these, as long as you still obtain 5 minutes of data for each class. However, your model will perform better in the real world if it was trained on a representative dataset.

Dataset diversity

There's no guarantee your model will perform well in the presence of sounds that were not included in its training set, so it's important to make your dataset as diverse and representative of real-world conditions as possible.

Data capture and transmission

The amount of audio that can be captured in one go varies depending on a device's memory. The ST B-L475E-IOT01A developer board has enough memory to capture 60 seconds of audio at a time, and the Arduino Nano 33 BLE Sense has enough memory for 16 seconds. To capture 60 seconds of audio, set the sample length to 60000. Because the board transmits data quite slowly, it will take around 7 minutes before a 60 second sample appears in Edge Impulse.

Once you've captured around 10 minutes of data, it's time to start designing an Impulse.

Prebuilt dataset

Alternatively, you can load an example test set that has about ten minutes of data in these classes (but how much fun is that?). See the for more information.

4. Design an Impulse

With the training set in place you can design an impulse. An impulse takes the raw data, slices it up in smaller windows, uses signal processing blocks to extract features, and then uses a learning block to classify new data. Signal processing blocks always return the same values for the same input and are used to make raw data easier to process, while learning blocks learn from past experiences.

For this tutorial we'll use the "MFE" signal processing block. MFE stands for Mel Frequency Energy. This sounds scary, but it's basically just a way of turning raw audio—which contains a large amount of redundant information—into simplified form.

Spectrogram block

Edge Impulse supports three different blocks for audio classification: MFCC, MFE and spectrogram blocks. If your accuracy is not great using the MFE block you can switch to the spectrogram block, which is not tuned to frequencies for the human ear.

We'll then pass this simplified audio data into a Neural Network block, which will learn to distinguish between the two classes of audio (faucet and noise).

In the studio, go to the Create impulse tab. You'll see a Raw data block, like this one.

As mentioned above, Edge Impulse slices up the raw samples into windows that are fed into the machine learning model during training. The Window size field controls how long, in milliseconds, each window of data should be. A one second audio sample will be enough to determine whether a faucet is running or not, so you should make sure Window size is set to 1000 ms. You can either drag the slider or type a new value directly.

Each raw sample is sliced into multiple windows, and the Window increase field controls the offset of each subsequent window from the first. For example, a Window increase value of 1000 ms would result in each window starting 1 second after the start of the previous one.

By setting a Window increase that is smaller than the Window size, we can create windows that overlap. This is actually a great idea. Although they may contain similar data, each overlapping window is still a unique example of audio that represents the sample's label. By using overlapping windows, we can make the most of our training data. For example, with a Window size of 1000 ms and a Window increase of 100 ms, we can extract 10 unique windows from only 2 seconds of data.

Make sure the Window increase field is set to 300 ms. The Raw data block should match the screenshot above.

Next, click Add a processing block and choose the 'MFE' block. Once you're done with that, click Add a learning block and select 'Classification (Keras)'. Finally, click Save impulse. Your impulse should now look like this:

5. Configure the MFE block

Now that we've assembled the building blocks of our Impulse, we can configure each individual part. Click on the MFE tab in the left hand navigation menu. You'll see a page that looks like this:

This page allows you to configure the MFE block, and lets you preview how the data will be transformed. The right of the page shows a visualization of the MFE's output for a piece of audio, which is known as a .

The MFE block transforms a window of audio into a table of data where each row represents a range of frequencies and each column represents a span of time. The value contained within each cell reflects the amplitude of its associated range of frequencies during that span of time. The spectrogram shows each cell as a colored block, the intensity which varies depends on the amplitude.

The patterns visible in a spectrogram contain information about what type of sound it represents. For example, the spectrogram in this image shows a pattern typical of background noise:

You can tell that it is slightly different from the following spectrogram, which shows a pattern typical of a running faucet:

These differences are not necessarily easy for a person to describe, but fortunately they are enough for a neural network to learn to identify.

It's interesting to explore your data and look at the types of spectrograms it results in. You can use the dropdown box near the top right of the page to choose between different audio samples to visualize, and drag the white window on the audio waveform to select different windows of data:

There are a lot of different ways to configure the MFCC block, as shown in the Parameters box:

Handily, Edge Impulse provides sensible defaults that will work well for many use cases, so we can leave these values unchanged. You can play around with the noise floor to quickly see the effect it has on the spectrogram.

The spectrograms generated by the MFE block will be passed into a neural network architecture that is particularly good at learning to recognize patterns in this type of tabular data. Before training our neural network, we'll need to generate MFE blocks for all of our windows of audio. To do this, click the Generate features button at the top of the page, then click the green Generate features button. If you have a full 10 minutes of data, the process will take a while to complete:

Once this process is complete the feature explorer shows a visualization of your dataset. Here dimensionality reduction is used to map your features onto a 3D space, and you can use the feature explorer to see if the different classes separate well, or find mislabeled data (if it shows in a different cluster). You can find more information in .

Next, we'll configure the neural network and begin training.

6. Configure the neural network

With all data processed it's time to start training a neural network. Neural networks are algorithms, modeled loosely after the human brain, that can learn to recognize patterns that appear in their training data. The network that we're training here will take the MFE as an input, and try to map this to one of two classes—noise, or faucet.

Click on NN Classifier in the left hand menu. You'll see the following page:

A neural network is composed of layers of virtual "neurons", which you can see represented on the left hand side of the NN Classifier page. An input—in our case, an MFE spectrogram—is fed into the first layer of neurons, which filters and transforms it based on each neuron's unique internal state. The first layer's output is then fed into the second layer, and so on, gradually transforming the original input into something radically different. In this case, the spectrogram input is transformed over four intermediate layers into just two numbers: the probability that the input represents noise, and the probability that the input represents a running faucet.

During training, the internal state of the neurons is gradually tweaked and refined so that the network transforms its input in just the right ways to produce the correct output. This is done by feeding in a sample of training data, checking how far the network's output is from the correct answer, and adjusting the neurons' internal state to make it more likely that a correct answer is produced next time. When done thousands of times, this results in a trained network.

A particular arrangement of layers is referred to as an architecture, and different architectures are useful for different tasks. The default neural network architecture provided by Edge Impulse will work well for our current project, but you can also define your own architectures. You can even import custom neural network code from tools used by data scientists, such as TensorFlow and Keras.

The default settings should work, and to begin training, click Start training. You'll see a lot of text flying past in the Training output panel, which you can ignore for now. Training will take a few minutes. When it's complete, you'll see the Model panel appear at the right side of the page:

Congratulations, you've trained a neural network with Edge Impulse! But what do all these numbers mean?

At the start of training, 20% of the training data is set aside for validation. This means that instead of being used to train the model, it is used to evaluate how the model is performing. The Last training performance panel displays the results of this validation, providing some vital information about your model and how well it is working. Bear in mind that your exact numbers may differ from the ones in this tutorial.

On the left hand side of the panel, Accuracy refers to the percentage of windows of audio that were correctly classified. The higher number the better, although an accuracy approaching 100% is unlikely, and is often a sign that your model has overfit the training data. You will find out whether this is true in the next stage, during model testing. For many applications, an accuracy above 80% can be considered very good.

The Confusion matrix is a table showing the balance of correctly versus incorrectly classified windows. To understand it, compare the values in each row. For example, in the above screenshot, all of the faucet audio windows were classified as faucet, but a few noise windows were misclassified. This appears to be a great result though.

The On-device performance region shows statistics about how the model is likely to run on-device. Inferencing time is an estimate of how long the model will take to analyze one second of data on a typical microcontroller (here: an Arm Cortex-M4F running at 80MHz). Peak memory usage gives an idea of how much RAM will be required to run the model on-device.

7. Classifying new data

The performance numbers in the previous step show that our model is working well on its training data, but it's extremely important that we test the model on new, unseen data before deploying it in the real world. This will help us ensure the model has not learned to overfit the training data, which is a common occurrence.

Edge Impulse provides some helpful tools for testing our model, including a way to capture live data from your device and immediately attempt to classify it. To try it out, click on Live classification in the left hand menu. Your device should show up in the 'Classify new data' panel. Capture 5 seconds of background noise by clicking Start sampling:

The sample will be captured, uploaded, and classified. Once this has happened, you'll see a breakdown of the results:

Once the sample is uploaded, it is split into windows–in this case, a total of 41. These windows are then classified. As you can see, our model classified all 41 windows of the captured audio as noise. This is a great result! Our model has correctly identified that the audio was background noise, even though this is new data that was not part of its training set.

Of course, it's possible some of the windows may be classified incorrectly. Since our model was 99% accurate based on its validation data, you can expect that at least 1% of windows will be classified wrongly—and likely much more than this, since our validation data doesn't represent every possible type of background or faucet noise. If your model didn't perform perfectly, don't worry. We'll get to troubleshooting later.

Misclassifications and uncertain results

It's inevitable that even a well-trained machine learning model will sometimes misclassify its inputs. When you integrate a model into your application, you should take into account that it will not always give you the correct answer.

For example, if you are classifying audio, you might want to classify several windows of data and average the results. This will give you better overall accuracy than assuming that every individual result is correct.

8. Model testing

Using the Live classification tab, you can easily try out your model and get an idea of how it performs. But to be really sure that it is working well, we need to do some more rigorous testing. That's where the Model testing tab comes in. If you open it up, you'll see the sample we just captured listed in the Test data panel:

In addition to its training data, every Edge Impulse project also has a test dataset. Samples captured in Live classification are automatically saved to the test dataset, and the Model testing tab lists all of the test data.

To use the sample we've just captured for testing, we should correctly set its expected outcome. Click the ⋮ icon and select Edit expected outcome, then enter noise. Now, select the sample using the checkbox to the left of the table and click Classify selected:

You'll see that the model's accuracy has been rated based on the test data. Right now, this doesn't give us much more information that just classifying the same sample in the Live classification tab. But if you build up a big, comprehensive set of test samples, you can use the Model testing tab to measure how your model is performing on real data.

Ideally, you'll want to collect a test set that contains a minimum of 25% the amount of data of your training set. So, if you've collected 10 minutes of training data, you should collect at least 2.5 minutes of test data. You should make sure this test data represents a wide range of possible conditions, so that it evaluates how the model performs with many different types of inputs. For example, collecting test audio for several different faucets is a good idea.

You can use the Data acquisition tab to manage your test data. Open the tab, and then click Test data at the top. Then, use the Record new data panel to capture a few minutes of test data, including audio for both background noise and faucet. Make sure the samples are labelled correctly. Once you're done, head back to the Model testing tab, select all the samples, and click Classify selected:

The screenshot shows classification results from a large number of test samples (there are more on the page than would fit in the screenshot). The panel shows that our model is performing at 85% accuracy, which is 5% less than how it performed on validation data. It's normal for a model to perform less well on entirely fresh data, so this is a successful result. Our model is working well!

For each test sample, the panel shows a breakdown of its individual performance. For example, one of the samples was classified with only 62% accuracy. Samples that contain a lot of misclassifications are valuable, since they have examples of types of audio that our model does not currently fit. It's often worth adding these to your training data, which you can do by clicking the ⋮ icon and selecting Move to training set. If you do this, you should add some new test data to make up for the loss!

Testing your model helps confirm that it works in real life, and it's something you should do after every change. However, if you often make tweaks to your model to try to improve its performance on the test dataset, your model may gradually start to overfit to the test dataset, and it will lose its value as a metric. To avoid this, continually add fresh data to your test dataset.

Data hygiene

It's extremely important that data is never duplicated between your training and test datasets. Your model will naturally perform well on the data that it was trained on, so if there are duplicate samples then your test results will indicate better performance than your model will achieve in the real world.

9. Model troubleshooting

If the network performed great, fantastic! But what if it performed poorly? There could be a variety of reasons, but the most common ones are:

  1. The data does not look like other data the network has seen before. This is common when someone uses the device in a way that you didn't add to the test set. You can add the current file to the test set by adding the correct label in the 'Expected outcome' field, clicking ⋮, then selecting Move to training set.

  2. The model has not been trained enough. Increase number of epochs to 200 and see if performance increases (the classified file is stored, and you can load it through 'Classify existing validation sample').

  3. The model is overfitting and thus performs poorly on new data. Try reducing the number of epochs, reducing the learning rate, or adding more data.

  4. The neural network architecture is not a great fit for your data. Play with the number of layers and neurons and see if performance improves.

As you see, there is still a lot of trial and error when building neural networks. Edge Impulse is continually adding features that will make it easier to train an effective model.

10. Deploying to your device

With the impulse designed, trained and verified you can deploy this model back to your device. This makes the model run without an internet connection, minimizes latency, and runs with minimum power consumption. Edge Impulse can package up the complete impulse - including the MFE algorithm, neural network weights, and classification code - in a single C++ library that you can include in your embedded software.

Mobile phone

Your mobile phone can build and download the compiled impulse directly from the mobile client. See 'Deploying back to device' on the page.

To export your model, click on Deployment in the menu. Then under 'Build firmware' select your development board, and click Build. This will export the impulse, and build a binary that will run on your development board in a single step. After building is completed you'll get prompted to download a binary. Save this on your computer.

Flashing the device

When you click the Build button, you'll see a pop-up with text and video instructions on how to deploy the binary to your particular device. Follow these instructions. Once you are done, we are ready to test your impulse out.

Running the model on the device

We can connect to the board's newly flashed firmware over serial. Open a terminal and run:

Serial daemon

If the device is not connected over WiFi, but instead connected via the Edge Impulse serial daemon, you'll need stop the daemon. Only one application can connect to the development board at a time.

This will capture audio from the microphone, run the MFE code, and then classify the spectrogram:

Great work! You've captured data, trained a model, and deployed it to an embedded device. It's time to celebrate—by pouring yourself a nice glass of water, and checking whether the sound is correctly classified by you model.

11. Conclusion

Congratulations! you've used Edge Impulse to train a neural network model capable of recognizing a particular sound. There are endless applications for this type of model, from monitoring industrial machinery to recognizing voice commands. Now that you've trained your model you can integrate your impulse in the firmware of your own embedded device, see . There are examples for Mbed OS, Arduino, STM32CubeIDE, and any other target that supports a C++ compiler.

Or if you're interested in more, see our tutorials on or . If you have a great idea for a different project, that's fine too. Edge Impulse lets you capture data from any sensor, build to extract features, and you have full flexibility in your Machine Learning pipeline with the learning blocks.

We can't wait to see what you'll build! 🚀

$ edge-impulse-run-impulse
Starting inferencing in 2 seconds...
Recording
Recording OK
Predictions (DSP: 399 ms., Classification: 175 ms., Anomaly: 0 ms.):
    faucet: 0.03757
    noise: 0.96243
Starting inferencing in 2 seconds...
Tutorial: recognize sounds from audio
Keyword spotting
supported device
Hardware specific tutorials
Ingestion service
Running faucet dataset
spectrogram
visualizing complex datasets
Using your mobile phone
Running your impulse locally
Continuous motion recognition
Image classification
custom processing blocks
Devices tab with the device connected to the remote management interface.
Record new data screen.
Audio waveform
The Raw data block with updated parameters.
The impulse, with one processing block and one learning block.
The MFE page.
Spectrogram of background noise.
Spectrogram of a running faucet.
Audio waveform and sample dropdown box.
The MFE parameters box.
Running the feature generation process.
The NN Classifier page.
The Model panel.
The Classify new data panel.
The results of classifying a new sample.
The Test data panel.
Test data classification results.
Test results for a large number of samples.
Machine learning is thirsty work.

Using the Edge Impulse Python SDK to run EON Tuner

The EON Tuner is Edge Impulse's automated machine learning (AutoML) tool to help you find the best combination of blocks and hyperparameters for your model and within your hardware constraints. This example will walk you through uploading data, running the EON Tuner, and interpreting the results.

WARNING: This notebook will add and delete data in your Edge Impulse project, so be careful! We recommend creating a throwaway project when testing this notebook.

To start, create a new project in Edge Impulse. Do not add any data to it.

# If you have not done so already, install the following dependencies
# !python -m pip install matplotlib pandas edgeimpulse
import edgeimpulse as ei
from edgeimpulse.experimental.data import (
    upload_directory
)
from edgeimpulse.experimental.tuner import (
    check_tuner,
    set_impulse_from_trial,
    start_tuner,
    start_custom_tuner,
    tuner_report_as_df,
)
from edgeimpulse.experimental.impulse import (
    build,
)

You will need to obtain an API key from an Edge Impulse project. Log into edgeimpulse.com and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Copy API key from Edge Impulse project

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

# Settings
ei.API_KEY = "ei_dae2..." # Change this to your Edge Impulse API key
deploy_filename = "my_model_cpp.zip"
# Get the project ID
api = ei.experimental.api.EdgeImpulseApi()
project_id = api.default_project_id()

Upload dataset

We start by downloading the continuous motion dataset and uploading it to our project.

# Download and unzip gesture dataset
!mkdir -p dataset/
!wget -P dataset -q https://cdn.edgeimpulse.com/datasets/gestures.zip
!unzip -q dataset/gestures.zip -d dataset/gestures/
# Upload training dataset
resp = upload_directory(
    directory="dataset/gestures/training",
    category="training",
)
print(f"Uploaded {len(resp.successes)} training samples")

# Upload test dataset
resp = upload_directory(
    directory="dataset/gestures/testing",
    category="testing",
)
print(f"Uploaded {len(resp.successes)} testing samples")
# Uncomment the following if you want to delete the temporary dataset folder
#!rm -rf dataset/

Run the Tuner

To start, we need to list the possible target devices we can use for profiling. We need to pick from this list.

# List the available profile targets
ei.model.list_profile_devices()

You should see a list printed such as:

['alif-he',
 'alif-hp',
 'arduino-nano-33-ble',
 'arduino-nicla-vision',
 'portenta-h7',
 'brainchip-akd1000',
 'cortex-m4f-80mhz',
 'cortex-m7-216mhz',
 ...
 'ti-tda4vm']

From there, we start the tuner with start_tuner() and wait for completion via check_tuner(). In this example, we configure the tuner to target for the cortex-m4f-80mhz device. Since we want to classify the motion, we choose classification for our classifcation_type and our dataset as motion continuous. We constrain our model to a latency of 100ms for running the impulse.

NOTE: We set the max trials to 3 here. In a real life situation, you will omit this so the tuner decides the best number of trials.

Once the tuner is done, you can print out the results to determine the best combination of blocks and hyperparameters.

# Choose a device from the list
target_device = "cortex-m4f-80mhz"

# Start tuner. This will take 15+ minutes.
start_tuner(
    target_device=target_device,
    classification_type="classification",
    dataset_category="motion_continuous",
    target_latency=100,
    tuning_max_trials=3,
)

# Wait while checking the tuner's progress.
state = check_tuner(
    wait_for_completion=True
)

Print EON Tuner results

To visualize the results of the tuner trials, you can head to the project page on Edge Impulse Studio.

Alternatively, you can access the results programmatically: the configuration settings and output of the EON Tuner is stored in the variable state. You can access the results of the various trials with state.trials. Note that some trials can fail, so it's a good idea to test the status of each trial.

From there, you will want to sort the results based on some metric. In this example, we will sort based on int8 test set accuracy from highest to lowest.

Note: Edge Impulse supports only one learning block per project at this time (excluding anomaly detection blocks). As a result, we will use the first learning block (e.g. learning_blocks[0]) in the list to extract metrics.

import json
# The easiest way to view the results is to look at the EON Tuner page on your project
print(f"Navigate to https://studio.edgeimpulse.com/studio/{project_id}/tuner to see the results")
# Set quantization ("float32" or "int8")
qtzn = "int8"

# Filter out all failed trials
results = [r for r in state.trials if r.status == "completed"]

# Extract int8 accuracies from the trial results
accuracies = []
for result in results:
    accuracy = result.impulse.learn_blocks[0]["metrics"]["test"][qtzn]["accuracy"]
    accuracies.append(accuracy)

# Sort the results based on int8 accuracies
acc_results = zip(accuracies, results)
sorted_results = sorted(acc_results, reverse=True, key=lambda x: list(x)[0])
sorted_results = [result for _, result in sorted_results]

Now that we have the sorted results, we can extract the values we care about. We will print out the following metrics along with the impulse configuration (processing/learning block configuration and hyperparameters) of the top-performing trial.

This will help you determine if the impulse can fit on your target hardware and run fast enough for your needs. The impulse configuration can be used to recreate the processing and learning blocks on Edge Impulse. Later, we will set the project impulse based on the trial ID to simply deploy (rather than re-train).

Note: we assume the first learning block has the metrics we care about.

def get_metrics(results, qtzn, idx):
    """Calculate metrics for a given trial index"""

    metrics = {}

    # Get model accuracy results
    result_metrics = results[idx].impulse.learn_blocks[0]["metrics"]
    metrics["val-acc"] = result_metrics['validation'][qtzn]['accuracy']
    metrics["test-acc"] = result_metrics['test'][qtzn]['accuracy']
    
    # Calculate processing block RAM
    metrics["processing-block-ram"] = 0
    for i, dsp_block in enumerate(results[idx].impulse.dsp_blocks):
        metrics["processing-block-ram"] += dsp_block["performance"]["ram"]

    # Get latency, RAM, and ROM usage
    device_performance = results[idx].device_performance[qtzn]
    metrics["learning-block-latency-ms"] = device_performance['latency']
    metrics["learning-block-tflite-ram"] = device_performance['tflite']['ramRequired']
    metrics["learning-block-tflite-rom"] = device_performance['tflite']['romRequired']
    metrics["learning-block-eon-ram"] = device_performance['eon']['ramRequired']
    metrics["learning-block-eon-rom"] = device_performance['eon']['romRequired']

    return metrics
# The top performing impulse is the first element (sorted by highest int8 accuracy on test set)
trial_idx = 0

# Print info about the processing (DSP) blocks and store RAM usage
print("Processing blocks")
print("===")
for i, dsp_block in enumerate(sorted_results[trial_idx].impulse.dsp_blocks):
    print(f"Processing block {i}")
    print("---")
    print("Block:")
    print(json.dumps(dsp_block["block"], indent=2))
    print("Config:")
    print(json.dumps(dsp_block["config"], indent=2))
print()

# Print info about the learning blocks
print("Learning blocks")
print("===")
for i, learn_block in enumerate(sorted_results[trial_idx].impulse.learn_blocks):
    print(f"Learn block {i}")
    print("---")
    print("Block:")
    print(json.dumps(learn_block["block"], indent=2))
    print("Config:")
    print(json.dumps(learn_block["config"], indent=2))
    metadata = learn_block["metadata"]
    qtzn_metadata = [m for m in metadata["modelValidationMetrics"] if m.get("type") == qtzn]
print()

# Print metrics
metrics = get_metrics(sorted_results, qtzn, trial_idx)
print(f"Metrics ({qtzn}) for best trial")
print("===")
print(f"Validation accuracy: {metrics['val-acc']}")
print(f"Test accuracy: {metrics['test-acc']}")
print(f"Estimated processing blocks RAM (bytes): {metrics['processing-block-ram']}")
print(f"Estimated learning blocks latency (ms): {metrics['learning-block-latency-ms']}")
print(f"Estimated learning blocks RAM (bytes): {metrics['learning-block-tflite-ram']}")
print(f"Estimated learning blocks ROM (bytes): {metrics['learning-block-tflite-rom']}")
print(f"Estimated learning blocks RAM with EON Compiler (bytes): {metrics['learning-block-eon-ram']}")
print(f"Estimated learning blocks ROM with EON Compiler (bytes): {metrics['learning-block-eon-rom']}")

Graph results

You can optionally use a plotting package like matplotlib to graph the results from the top results to compare the metrics.

import matplotlib.pyplot as plt
# Get metrics for the top 3 trials (sorted by int8 test set accuracy)
num_trials = 3
top_metrics = [get_metrics(sorted_results, qtzn, idx) for idx in range(num_trials)]

# Construct metrics for plotting
test_accs = [top_metrics[x]['test-acc'] for x in range(num_trials)]
proc_rams = [top_metrics[x]['processing-block-ram'] for x in range(num_trials)]
learn_latencies = [top_metrics[x]['learning-block-latency-ms'] for x in range(num_trials)]
learn_tflite_rams = [top_metrics[x]['learning-block-tflite-ram'] for x in range(num_trials)]
learn_tflite_roms = [top_metrics[x]['learning-block-tflite-rom'] for x in range(num_trials)]
learn_eon_rams = [top_metrics[x]['learning-block-eon-ram'] for x in range(num_trials)]
learn_eon_roms = [top_metrics[x]['learning-block-eon-rom'] for x in range(num_trials)]
# Create plots
fig, axs = plt.subplots(7, 1, figsize=(8, 15))
indices = range(num_trials)

# Plot test accuracies
axs[0].barh(indices, test_accs)
axs[0].set_title("Test set accuracy")
axs[0].set_xlabel("Accuracy")
axs[0].set_ylabel("Trial")

# Plot processing block RAM
axs[1].barh(indices, proc_rams)
axs[1].set_title("Processing block RAM")
axs[1].set_xlabel("RAM (bytes)")
axs[1].set_ylabel("Trial")

# Plot learning block latency
axs[2].barh(indices, learn_latencies)
axs[2].set_title("Learning block latency")
axs[2].set_xlabel("Latency (ms)")
axs[2].set_ylabel("Trial")

# Plot learning block RAM (TFLite)
axs[3].barh(indices, learn_tflite_rams)
axs[3].set_title("Learning block RAM (TFLite)")
axs[3].set_xlabel("RAM (bytes)")
axs[3].set_ylabel("Trial")

# Plot learning block ROM (TFLite)
axs[4].barh(indices, learn_tflite_roms)
axs[4].set_title("Learning block ROM (TFLite)")
axs[4].set_xlabel("ROM (bytes)")
axs[4].set_ylabel("Trial")

# Plot learning block RAM (EON)
axs[5].barh(indices, learn_eon_rams)
axs[5].set_title("Learning block RAM (EON)")
axs[5].set_xlabel("RAM (bytes)")
axs[5].set_ylabel("Trial")

# Plot learning block ROM (EON)
axs[6].barh(indices, learn_eon_roms)
axs[6].set_title("Learning block ROM (EON)")
axs[6].set_xlabel("ROM (bytes)")
axs[6].set_ylabel("Trial")

# Prevent overlap
plt.tight_layout()

Results as a DataFrame

If you have pandas installed, you can make the previous section much easier by reporting metrics as a DataFrame.

import pandas as pd
# Convert the state metrics into a DataFrame
df = tuner_report_as_df(state)
df.head()
# Print column names
for col in df.columns:
    print(col)
# Sort the DataFrame by validation (int8) accuracy
df = df.sort_values(by="test_int8_accuracy", ascending=False)

# Print out best trial metrics
print(f"Trial ID: {df.iloc[0]['id']}")
print(f"Test accuracy (int8): {df.iloc[0]['test_int8_accuracy']}")
print(f"Estimated learning blocks latency (ms): {df.iloc[0]['device_performance_int8_latency']}")
print(f"Estimated learning blocks RAM (bytes): {df.iloc[0]['device_performance_int8_tflite_ram_required']}")
print(f"Estimated learning blocks ROM (bytes): {df.iloc[0]['device_performance_int8_tflite_rom_required']}")
print(f"Estimated learning blocks RAM with EON Compiler (bytes): {df.iloc[0]['device_performance_int8_eon_ram_required']}")
print(f"Estimated learning blocks ROM with EON Compiler (bytes): {df.iloc[0]['device_performance_int8_eon_rom_required']}")

Set trial as impulse and deploy

We can replace the current impulse with the top performing trial from the EON Tuner. From there, we can deploy it, just like we would any impulse.

# Get the ID for the top-performing trial and set that to our impulse. This will take about a minute.
trial_id = df.iloc[0].trial_id
response = set_impulse_from_trial(trial_id)
job_id = response.id

# Make sure the impulse update was successful
if not hasattr(response, "success") or getattr(response, "success") == False:
    raise RuntimeError("Could not set project impulse to trial impulse")
# List the available profile target devices
ei.model.list_deployment_targets()

You should see a list printed such as:

['zip',
 'arduino',
 'cubemx',
 'wasm',
 ...
 'runner-linux-aarch64-jetson-orin-6-0']

The most generic target is to download a .zip file that holds a C++ library containing the inference runtime and your trained model, so we choose 'zip' from the above list. To do that, we first need to create a Classification object which contains our label strings (and other optional information about the model). These strings will be added to the C++ library metadata so you can access them in your edge application.

Note that instead of writing the raw bytes to a file, you can also specify an output_directory argument in the .deploy() function. Your deployment file(s) will be downloaded to that directory.

Important! The deployment targets list will change depending on the values provided for model, model_output_type, and model_input_type in the next part. For example, you will not see openmv listed once you upload a model (e.g. using .profile() or .deploy()) if model_input_type is not set to ei.model.input_type.ImageInput(). If you attempt to deploy to an unavailable target, you will receive the error Could not deploy: deploy_target: .... If model_input_type is not provided, it will default to OtherInput. See this page for more information about input types.

# Build and download C++ library with the trained model
deploy_bytes = None
try:
    deploy_bytes = build(
        deploy_model_type=qtzn,
        engine="tflite",
        deploy_target="zip"
    )
except Exception as e:
    print(f"Could not deploy: {e}")
    
# Write the downloaded raw bytes to a file
if deploy_bytes:
    with open(deploy_filename, 'wb') as f:
        f.write(deploy_bytes.getvalue())

Your model C++ library should be downloaded as the file my_model_cpp.zip in the same directory as this notebook. You are now ready to use your C++ model in your embedded and edge device application! To use the C++ model for local inference, see our documentation here.

Configure custom search space

By default, the EON Tuner will make a guess at a search space based on the type of data you uploaded (e.g. using spectral-analysis blocks for feature extraction). As a result, you can run the tuner without needing to construct a search space. However, you may want to define your own search space.

The best way to define a search space is to open your project (after uploading data), head to the EON Tuner page, click Run EON Tuner, and select the Space tab.

EON Tuner search space

The search space is defined in JSON format, so we can just copy that to create a dictionary. This is a good place to start for tuning blocks and hyperparameters.

Note: Functions to get available blocks and search space parameters coming soon

from edgeimpulse_api import (
    OptimizeConfig,
    TunerSpaceImpulse,
)
# Configure the search space
space = {
    "inputBlocks": [
      {
        "type": "time-series",
        "window": [
          {"windowSizeMs": 9000, "windowIncreaseMs": 9000},
          {"windowSizeMs": 10000, "windowIncreaseMs": 10000}
        ],
        "frequencyHz": [62.5],
        "padZeros": [True]
      }
    ],
    "dspBlocks": [
      {
        "type": "spectral-analysis",
        "analysis-type": ["FFT"],
        "fft-length": [16, 64],
        "scale-axes": [1],
        "filter-type": ["none"],
        "filter-cutoff": [3],
        "filter-order": [6],
        "do-log": [True],
        "do-fft-overlap": [True]
      },
      {
        "type": "spectral-analysis",
        "analysis-type": ["Wavelet"],
        "wavelet": ["haar", "bior1.3"],
        "wavelet-level": [1, 2]
      },
      {"type": "raw", "scale-axes": [1]}
    ],
    "learnBlocks": [
      {
        "id": 4,
        "type": "keras",
        "dimension": ["dense"],
        "denseBaseNeurons": [40, 20],
        "denseLayers": [2, 3],
        "dropout": [0.25, 0.5],
        "learningRate": [0.0005],
        "trainingCycles": [30]
      }
    ]
  }
# Wrap the search space
ts = TunerSpaceImpulse.from_dict(space)

# Create a custom configuration
config = OptimizeConfig(
    name=None,
    target_device={"name": "cortex-m4f-80mhz"},
    classification_type="classification",
    dataset_category="motion_continuous",
    target_latency=100,
    tuning_max_trials=2,
    space=[ts]
)
# Start tuner and wait for it to complete
start_custom_tuner(
    config=config
)
state = check_tuner(
    wait_for_completion=True
)
# The easiest way to view the results is to look at the EON Tuner page on your project
print(f"Navigate to https://studio.edgeimpulse.com/studio/{project_id}/tuner to see the results")
# Set quantization ("float32" or "int8")
qtzn = "int8"

# Filter out all failed trials
results = [r for r in state.trials if r.status == "completed"]

# Extract float32 accuracies from the trial results
accuracies = []
for result in results:
    accuracy = result.impulse.learn_blocks[0]["metrics"]["test"][qtzn]["accuracy"]
    accuracies.append(accuracy)

# Sort the results based on int8 accuracies
acc_results = zip(accuracies, results)
sorted_results = sorted(acc_results, reverse=True, key=lambda x: list(x)[0])
sorted_results = [result for _, result in sorted_results]
# The top performing impulse is the first element (sorted by highest int8 accuracy on test set)
trial_idx = 0

# Print info about the processing (DSP) blocks and store RAM usage
print("Processing blocks")
print("===")
for i, dsp_block in enumerate(sorted_results[trial_idx].impulse.dsp_blocks):
    print(f"Processing block {i}")
    print("---")
    print("Block:")
    print(json.dumps(dsp_block["block"], indent=2))
    print("Config:")
    print(json.dumps(dsp_block["config"], indent=2))
print()

# Print info about the learning blocks
print("Learning blocks")
print("===")
for i, learn_block in enumerate(sorted_results[trial_idx].impulse.learn_blocks):
    print(f"Learn block {i}")
    print("---")
    print("Block:")
    print(json.dumps(learn_block["block"], indent=2))
    print("Config:")
    print(json.dumps(learn_block["config"], indent=2))
    metadata = learn_block["metadata"]
    qtzn_metadata = [m for m in metadata["modelValidationMetrics"] if m.get("type") == qtzn]
print()

# Print metrics
metrics = get_metrics(sorted_results, qtzn, trial_idx)
print(f"Metrics ({qtzn}) for best trial")
print("===")
print(f"Validation accuracy: {metrics['val-acc']}")
print(f"Test accuracy: {metrics['test-acc']}")
print(f"Estimated processing blocks RAM (bytes): {metrics['processing-block-ram']}")
print(f"Estimated learning blocks latency (ms): {metrics['learning-block-latency-ms']}")
print(f"Estimated learning blocks RAM (bytes): {metrics['learning-block-tflite-ram']}")
print(f"Estimated learning blocks ROM (bytes): {metrics['learning-block-tflite-rom']}")
print(f"Estimated learning blocks RAM with EON Compiler (bytes): {metrics['learning-block-eon-ram']}")
print(f"Estimated learning blocks ROM with EON Compiler (bytes): {metrics['learning-block-eon-rom']}")

Python API bindings example

The Python SDK is built on top of the Edge Impulse Python API bindings, which is known as the edgeimpulse_api package. These are Python wrappers for all of the web API calls that you can use to interact with Edge Impulse projects programmatically (i.e. without needing to use the Studio graphical interface).

The API reference guide for using the Python API bindings can be found here.

This example will walk you through the process of using the Edge Impulse API bindings to upload data, define an impulse, process features, train a model, and deploy the impulse as a C++ library.

After creating your project and copying the API key, feel free to leave the project open in a browser window so you can watch the changes as we make API calls. You might need to refresh the browser after each call to see the changes take affect.

Important! This project will add data and remove any current features and models in a project. We highly recommend creating a new project when running this notebook! Don't say we didn't warn you if you mess up an existing project.

# Install the Edge Impulse API bindings and the requests package
!python -m pip install edgeimpulse-api requests
import json
import re
import os
import pprint
import time

import requests
# Import the API objects we plan to use
from edgeimpulse_api import (
    ApiClient,
    BuildOnDeviceModelRequest,
    Configuration,
    DeploymentApi,
    DSPApi,
    DSPConfigRequest,
    GenerateFeaturesRequest,
    Impulse,
    ImpulseApi,
    JobsApi,
    ProjectsApi,
    SetKerasParameterRequest,
    StartClassifyJobRequest,
    UpdateProjectRequest,
)

You will need to obtain an API key from an Edge Impulse project. Log into edgeimpulse.com and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Copy API key from Edge Impulse project

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the EI_API_KEY value in the following cell:

# Settings
API_KEY = "ei_dae2..." # Change this to your Edge Impulse API key
API_HOST = "https://studio.edgeimpulse.com/v1"
DATASET_PATH = "dataset/gestures"
OUTPUT_PATH = "."

Initialize API clients

The Python API bindings use a series of submodules, each encapsulating one of the API subsections (e.g. Projects, DSP, Learn, etc.). To use these submodules, you need to instantiate a generic API module and use that to instantiate the individual API objects. We'll use these objects to make the API calls later.

To configure a client, you generally create a configuration object (often from a dict) and then pass that object as an argument to the client.

# Create top-level API client
config = Configuration(
    host=API_HOST,
    api_key={"ApiKeyAuthentication": API_KEY}
)
client = ApiClient(config)

# Instantiate sub-clients
deployment_api = DeploymentApi(client)
dsp_api = DSPApi(client)
impulse_api = ImpulseApi(client)
jobs_api = JobsApi(client)
projects_api = ProjectsApi(client)

Initialize project

Before uploading data, we should make sure the project is in the regular impulse flow mode, rather than BYOM mode. We'll also need the project ID for most of the other API calls in the future.

Notice that the general pattern for calling API functions is to instantiate a configuration/request object and pass it to the API method that's part of the submodule. You can find which parameters a specific API call expects by looking at the call's documentation page.

API calls (links to associated documentation):

  • Projects / List (active) projects

  • Projects / Update project

# Get the project ID, which we'll need for future API calls
response = projects_api.list_projects()
if not hasattr(response, "success") or getattr(response, "success") == False:
    raise RuntimeError("Could not obtain the project ID.")
else:
    project_id = response.projects[0].id

# Print the project ID
print(f"Project ID: {project_id}")
# Create request object with the required parameters
update_project_request = UpdateProjectRequest.from_dict({
    "inPretrainedModelFlow": False,
})

# Update the project and check the response for errors
response = projects_api.update_project(
    project_id=project_id,
    update_project_request=update_project_request,
)
if not hasattr(response, "success") or getattr(response, "success") == False:
    raise RuntimeError("Could not obtain the project ID.")
else:
    print("Project is now in impulse workflow.")

Upload dataset

We'll start by downloading the gesture dataset from this link. Note that the ingestion API is separate from the regular Edge Impulse API: the URL and interface are different. As a result, we must construct the request manually and cannot rely on the Python API bindings.

We rely on the ingestion service using the string before the first period in the filename to determine the label. For example, "idle.1.cbor" will be automatically assigned the label "idle." If you wish to set a label manually, you must specify the x-label parameter in the headers. Note that you can only define a label this way when uploading a group of data at a time. For example, setting "x-label": "idle" in the headers would give all data uploaded with that call the label "idle."

API calls used with associated documentation:

  • Ingestion service

# Download and unzip gesture dataset
!mkdir -p dataset/
!wget -P dataset -q https://cdn.edgeimpulse.com/datasets/gestures.zip
!unzip -q dataset/gestures.zip -d {DATASET_PATH}
def upload_files(api_key, path, subset):
    """
    Upload files in the given path/subset (where subset is "training" or
    "testing")
    """

    # Construct request
    url = f"https://ingestion.edgeimpulse.com/api/{subset}/files"
    headers = {
        "x-api-key": api_key,
        "x-disallow-duplicates": "true",
    }

    # Get file handles and create dataset to upload
    files = []
    file_list = os.listdir(os.path.join(path, subset))
    for file_name in file_list:
        file_path = os.path.join(path, subset, file_name)
        if os.path.isfile(file_path):
            file_handle = open(file_path, "rb")
            files.append(("data", (file_name, file_handle, "multipart/form-data")))

    # Upload the files
    response = requests.post(
        url=url,
        headers=headers,
        files=files,
    )

    # Print any errors for files that did not upload
    upload_responses = response.json()["files"]
    for resp in upload_responses:
        if not resp["success"]:
            print(resp)

    # Close all the handles
    for handle in files:
        handle[1][1].close()
# Upload the dataset to the project
print("Uploading training dataset...")
upload_files(API_KEY, DATASET_PATH, "training")
print("Uploading testing dataset...")
upload_files(API_KEY, DATASET_PATH, "testing")

Create an impulse

Now that we uploaded our data, it's time to create an impulse. An "impulse" is a combination of processing (feature extraction) and learning blocks. The general flow of data is:

data -> input block -> processing block(s) -> learning block(s)

Only the processing and learning blocks make up the "impulse." However, we must still specify the input block, as it allows us to perform preprocessing, like windowing (for time series data) or cropping/scaling (for image data).

Your project will have one input block, but it can contain multiple processing and learning blocks. Specific outputs from the processing block can be specified as inputs to the learning blocks. However, for simplicity, we'll just show one processing block and one learning block.

Note: Historically, processing blocks were called "DSP blocks," as they focused on time series data. In Studio, the name has been changed to "Processing block," as the blocks work with different types of data, but you'll see it referred to as "DSP block" in the API.

It's important that you define the input block with the same parameters as your captured data, especially the sampling rate! Additionally, the processing block axes names must match up with their names in the dataset.

API calls (links to associated documentation):

  • Impulse / Get impulse blocks

  • Impulse / Delete impulse

  • Impulse / Create impulse

# To start, let's fetch a list of all the available blocks
response = impulse_api.get_impulse_blocks(
    project_id=project_id
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not get impulse blocks.")
# Print the available input blocks
print("Input blocks")
print(json.dumps(json.loads(response.to_json())["inputBlocks"], indent=2))
# Print the available processing blocks
print("Processing blocks")
print(json.dumps(json.loads(response.to_json())["dspBlocks"], indent=2))
# Print the available learning blocks
print("Learning blocks")
print(json.dumps(json.loads(response.to_json())["learnBlocks"], indent=2))
# Give our impulse blocks IDs, which we'll use later
processing_id = 2
learning_id = 3

# Impulses (and their blocks) are defined as a collection of key/value pairs
impulse = Impulse.from_dict({
    "inputBlocks": [
        {
            "id": 1,
            "type": "time-series",
            "name": "Time series",
            "title": "Time series data",
            "windowSizeMs": 1000,
            "windowIncreaseMs": 500,
            "frequencyHz": 62.5,
            "padZeros": True,
        }
    ],
    "dspBlocks": [
        {
            "id": processing_id,
            "type": "spectral-analysis",
            "name": "Spectral Analysis",
            "implementationVersion": 4,
            "title": "processing",
            "axes": ["accX", "accY", "accZ"],
            "input": 1,
        }
    ],
    "learnBlocks": [
        {
            "id": learning_id,
            "type": "keras",
            "name": "Classifier",
            "title": "Classification",
            "dsp": [processing_id],
        }
    ],
})
# Delete the current impulse in the project
response = impulse_api.delete_impulse(
    project_id=project_id
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not delete current impulse.")

# Add blocks to impulse
response = impulse_api.create_impulse(
    project_id=project_id,
    impulse=impulse
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not create impulse.")

Configure processing block

Before generating features, we need to configure the processing block. We'll start by printing all the available parameters for the spectral-analysis block, which we set when we created the impulse above.

API calls (links to associated documentation):

  • DSP / Get config

  • DSP / Set config

# Get processing block config
response = dsp_api.get_dsp_config(
    project_id=project_id,
    dsp_id=processing_id
)

# Construct user-readable parameters
settings = []
for group in response.config:
    for item in group.items:
        element = {}
        element["parameter"] = item.param
        element["description"] = item.help
        element["currentValue"] = item.value
        element["defaultValue"] = item.default_value
        element["type"] = item.type
        if hasattr(item, "select_options") and \
            getattr(item, "select_options") is not None:
            element["options"] = [i.value for i in item.select_options]
        settings.append(element)

# Print the settings
print(json.dumps(settings, indent=2))
# Define processing block configuration
config_request = DSPConfigRequest.from_dict({
    "config": {
        "scale-axes": 1.0,
        "input-decimation-ratio": 1,
        "filter-type": "none",
        "analysis-type": "FFT",
        "fft-length": 16,
        "do-log": True,
        "do-fft-overlap": True,
        "extra-low-freq": False,
    }
})

# Set processing block configuration
response = dsp_api.set_dsp_config(
    project_id=project_id,
    dsp_id=processing_id,
    dsp_config_request=config_request
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not start feature generation job.")
else:
    print("Processing block has been configured.")

Run processing block to generate features

After we've defined the impulse, we then want to use our processing block(s) to extract features from our data. We'll skip feature importance and feature explorer to make this go faster.

Generating features kicks off a job in Studio. A "job" involves instantiating a Docker container and running a custom script in the container to perform some action. In our case, that involves reading in data, extracting features from that data, and saving those features as Numpy (.npy) files in our project.

Because jobs can take a while, the API call will return immediately. If the call was successful, the response will contain a job number. We can then monitor that job and wait for it to finish before continuing.

API calls (links to associated documentation):

  • Jobs / Generate features

  • Jobs / Get job status

def poll_job(jobs_api, project_id, job_id):
    """Wait for job to complete"""

    # Wait for job to complete
    while True:

        # Check on job status
        response = jobs_api.get_job_status(
            project_id=project_id,
            job_id=job_id
        )
        if not hasattr(response, "success") or getattr(response, "success") is False:
            print("ERROR: Could not get job status")
            return False
        else:
            if hasattr(response, "job") and hasattr(response.job, "finished"):
                if response.job.finished:
                    print(f"Job completed at {response.job.finished}")
                    return response.job.finished_successful
            else:
                print("ERROR: Response did not contain a 'job' field.")
                return False

        # Print that we're still running and wait
        print(f"Waiting for job {job_id} to finish...")
        time.sleep(2.0)
# Define generate features request
generate_features_request = GenerateFeaturesRequest.from_dict({
    "dspId": processing_id,
    "calculate_feature_importance": False,
    "skip_feature_explorer": True,
})

# Generate features
response = jobs_api.generate_features_job(
    project_id=project_id,
    generate_features_request=generate_features_request,
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not start feature generation job.")

# Extract job ID
job_id = response.id

# Wait for job to complete
success = poll_job(jobs_api, project_id, job_id)
if success:
    print("Features have been generated.")
else:
    print(f"ERROR: Job failed. See https://studio.edgeimpulse.com/studio/{project_id}/jobs#show-job-{job_id} for more details.")
# Optional: download NumPy features (x: training data, y: training labels)
print("Go here to download the generated features in NumPy format:")
print(f"https://studio.edgeimpulse.com/v1/api/{project_id}/dsp-data/{processing_id}/x/training")
print(f"https://studio.edgeimpulse.com/v1/api/{project_id}/dsp-data/{processing_id}/y/training")

Use learning block to train model

Now that we have trained features, we can run the learning block to train the model on those features. Note that Edge Impulse has a number of learning blocks, each with different methods of configuration. We'll be using the "keras" block, which uses TensorFlow and Keras under the hood.

You can use the get_keras and set_keras functions to configure the granular settings. We'll use the defaults for that block and just set the number of epochs and learning rate for training.

API calls (links to associated documentation):

  • Jobs / Train model (Keras)

  • Jobs / Get job status

  • Jobs / Get logs

 # Define training request
keras_parameter_request = SetKerasParameterRequest.from_dict({
    "mode": "visual",
    "training_cycles": 10,
    "learning_rate": 0.001,
    "train_test_split": 0.8,
    "skip_embeddings_and_memory": True,
})

# Train model
response = jobs_api.train_keras_job(
    project_id=project_id,
    learn_id=learning_id,
    set_keras_parameter_request=keras_parameter_request,
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not start training job.")

# Extract job ID
job_id = response.id

# Wait for job to complete
success = poll_job(jobs_api, project_id, job_id)
if success:
    print("Model has been trained.")
else:
    print(f"ERROR: Job failed. See https://studio.edgeimpulse.com/studio/{project_id}/jobs#show-job-{job_id} for more details.")

Now that the model has been trained, we can go back to the job logs to find the accuracy metrics for both the float32 and int8 quantization levels. We'll need to parse the logs to find these. Because the logs are printed with the most recent events first, we'll work backwards through the log to find these metrics.

def get_metrics(response, quantization=None):
    """
    Parse the response to find the accuracy/training metrics for a given
    quantization level. If quantization is None, return the first set of metrics
    found.
    """
    metrics = None
    delimiter_str = "calculate_classification_metrics"

    # Skip finding quantization metrics if not given
    if quantization:
        quantization_found = False
    else:
        quantization_found = True

    # Parse logs
    for log in reversed(response.to_dict()["stdout"]):
        data_field = log["data"]
        if quantization_found:
            substrings = data_field.split("\n")
            for substring in substrings:
                substring = substring.strip()
                if substring.startswith(delimiter_str):
                    metrics = json.loads(substring[len(delimiter_str):])
                    break
        else:
            if data_field.startswith(f"Calculating {quantization} accuracy"):
                quantization_found = True

    return metrics
# Get the job logs for the previous job
response = jobs_api.get_jobs_logs(
    project_id=project_id,
    job_id=job_id
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not get job log.")

# Print training metrics (quantization is "float32" or "int8")
quantization = "float32"
metrics = get_metrics(response, quantization)
if metrics:
    print(f"Training metrics for {quantization} quantization:")
    pprint.pprint(metrics)
else:
    print("ERROR: Could not get training metrics.")

Test the impulse

As with any good machine learning project, we should test the accuracy of the model using our holdout ("testing") set. We'll call the classify API function to make that happen and then parse the job logs to get the results.

In most cases, using int8 quantization will result in a faster, smaller model, but you will slightly lose some accuracy.

API calls (links to associated documentation):

  • Jobs / Classify

  • Jobs / Get job status

  • Jobs / Get logs

 # Set the model quantization level ("float32", "int8", or "akida")
quantization = "int8"
classify_request = StartClassifyJobRequest.from_dict({
    "model_variants": quantization
})

# Start model testing job
response = jobs_api.start_classify_job(
    project_id=project_id,
    start_classify_job_request=classify_request
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not start classify job.")

# Extract job ID
job_id = response.id

# Wait for job to complete
success = poll_job(jobs_api, project_id, job_id)
if success:
    print("Inference performed on test set.")
else:
    print(f"ERROR: Job failed. See https://studio.edgeimpulse.com/studio/{project_id}/jobs#show-job-{job_id} for more details.")
# Get the job logs for the previous job
response = jobs_api.get_jobs_logs(
    project_id=project_id,
    job_id=job_id
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not get job log.")

# Print
metrics = get_metrics(response)
if metrics:
    print(f"Test metrics for {quantization} quantization:")
    pprint.pprint(metrics)
else:
    print("ERROR: Could not get test metrics.")

Deploy the impulse

Now that you've trained the model, let's build it as a C++ library and download it. We'll start by printing out the available target devices. Note that this list changes depending on how you've configured your impulse. For example, if you use a Syntiant-specific learning block, then you'll see Syntiant boards listed. We'll use the "zip" target, which gives us a generic C++ library that we can use for nearly any hardware.

The engine must be one of:

tflite
tflite-eon
tflite-eon-ram-optimized
tensorrt
tensaiflow
drp-ai
tidl
akida
syntiant
memryx

We'll use tflite, as that's the most ubiquitous.

modelType is the quantization level. Your options are:

float32
int8

In most cases, using int8 quantization will result in a faster, smaller model, but you will slightly lose some accuracy.

API calls (links to associated documentation):

  • Deployment / Deployment targets (data sources)

  • Jobs / Build on-device model

  • Deployment / Download

# Get the available devices
response = deployment_api.list_deployment_targets_for_project_data_sources(
    project_id=project_id
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not get device list.")

# Print the available devices
targets = [x.to_dict()["format"] for x in response.targets]
for target in targets:
    print(target)
# Choose the target hardware (from the list above), engine,
target_hardware = "zip"
engine = "tflite"
quantization = "int8"

# Construct request
device_model_request = BuildOnDeviceModelRequest.from_dict({
    "engine": engine,
    "modelType": quantization
})

# Start build job
response = jobs_api.build_on_device_model_job(
    project_id=project_id,
    type=target_hardware,
    build_on_device_model_request=device_model_request,
)
if not hasattr(response, "success") or getattr(response, "success") is False:
    raise RuntimeError("Could not start feature generation job.")

# Extract job ID
job_id = response.id

# Wait for job to complete
success = poll_job(jobs_api, project_id, job_id)
if success:
    print("Impulse built.")
else:
    print(f"ERROR: Job failed. See https://studio.edgeimpulse.com/studio/{project_id}/jobs#show-job-{job_id} for more details.")
# Get the download link information
response = deployment_api.download_build(
    project_id=project_id,
    type=target_hardware,
    model_type=quantization,
    engine=engine,
    _preload_content=False,
)
if response.status != 200:
    raise RuntimeError("Could not get download information.")

# Find the file name in the headers
file_name = re.findall(r"filename\*?=(.+)", response.headers["Content-Disposition"])[0].replace("utf-8''", "")
file_path = os.path.join(OUTPUT_PATH, file_name)

# Write the contents to a file
with open(file_path, "wb") as f:
    f.write(response.data)

You should have a .zip file in the same directory as this notebook. Download or move it to somewhere else on your computer and unzip it. You can now follow this guide to link and compile the library as part of an application.

Multi-impulse (C++)

Once you successfully trained or imported a model, you can use Edge Impulse to download a C++ library that bundles both your signal processing and your machine learning model. Until recently, we could only run one impulse on MCUs.

This tutorial is for advanced users. We will provide limited support on the forum until this feature is fully integrated into the platform. If you have subscribed to an Enterprise plan, you can contact our customer success or solution engineering team.

In this tutorial, we will see how to run multiple impulses using the downloaded C++ libraries of two different projects.

We have put together a custom deployment block that will automate all the processes and provide a C++ library that can be compiled and run as a standalone.

In this page, we will explain the high level concepts of how to merge two impulses. Feel free to look at the code to gain a deeper understanding.

Multi-impulse vs multi-model vs sensor fusion

Running multi-impulse refers to running two separate projects (different data, different DSP blocks and different models) on the same target. It will require modifying some files in the EI-generated SDKs.

Running multi-model refers to running two different models (same data, same DSP block but different tflite models) on the same target. See how to run a motion classifier model and an anomaly detection model on the same device in this tutorial.

Sensor fusion refers to the process of combining data from different types of sensors to give more information to the neural network. See how to use sensor fusion in this tutorial.

Multi-impulse vs Multi-model vs Sensor Fusion

Also see this video (starting min 13):

Examples

Make sure you have at least two impulses fully trained.

You can use one of the following examples:

Audio + Image classification

This example can be used for an intrusion detection system. We will use a first model to detect glass-breaking sounds, if we detected this sound, we will then classify an image to see if there is a person or not in the image. In this tutorial, we will use the following public projects:

  • Glass breaking - audio classification

  • Person detection - image classification

Merge multiple impulses into a single C++ Library

The source code and the generator script can be found here.

By default, the quantized version is used when downloading the C++ libraries. To use float32, add the option --float32 as an argument.

Similarly by default the EON compiled model is used, if you want to use full tflite then add the option --full-tflite and be sure to include a recent version of tensorflow lite compiled for your device architecture in the root of your project in a folder named tensorflow-lite

If you need a mix of quantized and float32, you can look at the dzip.download_model function call in generate.py and change the code accordingly.

By default, the block will download cached version of builds. You can force new builds using the --force-build option.

Locally

Retrieve API Keys of your projects and run the generate.py command as follows:

python generate.py --out-directory output --api-keys ei_0b0e...,ei_acde... --quantization-map <0/1>,<0/1>

Docker

Build the container:docker build -t multi-impulse .

Then run:docker run --rm -it -v $PWD:/home multi-impulse --api-keys ei_0b0e...,ei_acde...

Custom deployment block

Initialize the custom block - select Deployment block and Library when prompted:edge-impulse-blocks init

Push the block:edge-impulse-blocks push

Then go your Organization and Edit the deployment block with:

  • CLI arguments: --api-keys ei_0b0e...,ei_acde...

  • Privaliged mode: Enabled

See Edge Impulse Studio -> Organizations -> Custom blocks -> Deployment blocks documentation for more details about custom deployment blocks.

Understanding the process

If you have a look at the generate.py script, it streamline the process of generating a C++ library from multiple impulses through several steps:

  1. Library Download and Extraction:

  • If the script detects that the necessary projects are not already present locally, it initiates the download of C++ libraries required for edge deployment. These libraries are fetched using API keys provided by the user.

  • Libraries are downloaded and extracted into a temporary directory. If the user specifies a custom temporary directory, it's used; otherwise, a temporary directory is created.

  1. Customization of Files:

For each project's library, the script performs several modifications:

  • At the file name level:

    • It adds a project-specific suffix to certain patterns in compiled files within the tflite-model directory. This customization ensures that each project's files are unique.

    • Renamed files are then copied to a target directory, mainly the first project's directory.

  • At the function name level:

    • It edits model_variables.h functions by adding the project-specific suffix to various patterns. This step ensures that model parameters are correctly associated with each project.

  1. Merging the projects

  • model_variables.h is merged into the first project's directory to consolidate model information.

  • The script saves the intersection of lines between trained_model_ops_define.h files for different projects, ensuring consistency.

  1. Copying Templates:

  • The script copies template files from a templates directory to the target directory. The template available includes files with code structures and placeholders for customization. It's adapted from the example-standalone-inferencing example available on Github.

  1. Generating Custom Code:

  • The script retrieves impulse IDs from model_variables.h for each project. Impulses are a key part of edge machine learning models.

  • Custom code is generated for each project, including functions to get signal data, define raw features, and run the classifier.

  • This custom code is inserted into the main.cpp file of each project at specific locations.

  1. Archiving for Deployment:

  • Finally, the script archives the target directory, creating a zip file ready for deployment. This zip file contains all the customized files and code necessary for deploying machine learning models on edge devices.

When changing between projects and running generate.py locally:

You may need to include the --force-build option to ensure correctness of the combined library.

Compiling and running the multi-impulse library

Now to test the library generated:

  • Download and unzip your Edge Impulse C++ multi-impulse library into a directory

  • Copy a test sample's raw features into the features[] array in source/main.cpp

  • Enter make -j in this directory to compile the project. If you encounter any OOM memory error try make -j4 (replace 4 with the number of cores available)

  • Enter ./build/app to run the application

  • Compare the output predictions to the predictions of the test sample in the Edge Impulse Studio

Want to add your own business logic?

You can change the template you want to use in step 4 to use another compilation method, implement your custom sampling strategy and how to handle the inference results in step 5 (apply post-processing, send results somewhere else, trigger actions, etc.).

Limitations

General limitations:

  • The custom ML accelerator deployments are unlikely to work (TDA4VM, DRPAI, MemoryX, Brainchip).

  • The custom tflite kernels (ESP NN, Silabs MVP, Arc MLI) should work - but may require some additional work. I.e: for ESP32 you may need to statically allocate arena for the image model.

  • In general, running multiple impulses on an MCU can be challenging due to limited processing power, memory, and other hardware constraints. Make sure to thoroughly evaluate the capabilities and limitations of your specific MCU and consider the resource requirements of the impulses before attempting to run them concurrently.

Use case specific limitations:

The model_metadata.h comes from the first API Key of your project. This means some #define statement might be missing or conflicting.

  • Object detection: If you want to run at least one Object Detection project. Make sure to use this project API KEY first! This will set the #define EI_CLASSIFIER_OBJECT_DETECTION 1 and eventually the #define EI_HAS_FOMO 1. Note that you can overwrite them manually but it requires an extra step.

  • Anomaly detection: If your anomaly detection model API Key is not in the first position, the model-parameter/anomaly_metadata.h file will not be included.

  • Visual anomaly detection AND time-series anomaly detection (K-Means or GMM): It is currently not possible to combine two different anomaly detection models. The #define EI_CLASSIFIER_HAS_ANOMALY statement expect ONLY one of the following argument:

    #define EI_ANOMALY_TYPE_UNKNOWN                   0
    #define EI_ANOMALY_TYPE_KMEANS                    1
    #define EI_ANOMALY_TYPE_GMM                       2
    #define EI_ANOMALY_TYPE_VISUAL_GMM                3

Troubleshooting

Segmentation fault

If you see the following segmentation fault, make sure to subtract and merge the trained_model_ops_define.h or tflite_resolver.h

./build/app
run_classifier with audio impulse returned: 0
Timing: DSP 0 ms, inference 0 ms, anomaly 0 ms
Predictions:
  Background: 0.00000
  Glass_Breaking: 0.99609
zsh: segmentation fault  ./build/app

FileExistsError: [Errno 17] File exists

If you see an error like the following, you probably used twice the same API Key:

Project ID is 517331
Export ZIP saved in: temp/517331/cubes-visual-ad-v12.zip (6218297 Bytes)
Project ID is 517331
Traceback (most recent call last):
  File "generate.py", line 49, in <module>
    os.makedirs(download_path)
  File "/Users/luisomoreau/.pyenv/versions/3.8.10/lib/python3.8/os.py", line 223, in makedirs
    mkdir(name, mode)
FileExistsError: [Errno 17] File exists: 'temp/517331'

Make sure you use distinct projects.

Manual procedure

When we first wrote this tutorial, we explained how to merge two impulses manually; This process is now deprecated due to recent changes in our C++ SDK, some files and functions may have been renamed.

See the legacy steps

Some files and function names have changed

The general concepts remain valid but due to recent changes in our C++ inferencing SDK, some files and function names may have changed.

Download the impulses from your projects

Head to your projects' deployment pages and download the C++ libraries:

Deployment page of the glass-breaking project

Make sure to select the same model versions (EON-Compiled enabled/disabled and int8/float32) for your projects.

Extract the two archive in a directory (multi-impulse for example).

Rename the tflite model files

Rename the tflite model files:

Go to the tflite-model directory in your extracted archives and rename the following files by post-fixing them with the name of the project:

  • for EON compiled projects: tflite_learn_[block-id]_compiled.cpp/tflite_learn_[block-id]_compiled.h.

  • for non-EON-compiled projects: tflite_learn_[block-id].cpp/tflite_learn_[block-id].h.

Original structure:

>  multi-impulse % tree -L 3
.
├── audio
│   ├── CMakeLists.txt
│   ├── README.txt
│   ├── edge-impulse-sdk
│   │   ├── CMSIS
│   │   ├── LICENSE
│   │   ├── LICENSE-apache-2.0.txt
│   │   ├── README.md
│   │   ├── classifier
│   │   ├── cmake
│   │   ├── dsp
│   │   ├── porting
│   │   ├── sources.txt
│   │   ├── tensorflow
│   │   └── third_party
│   ├── model-parameters
│   │   ├── model_metadata.h
│   │   └── model_variables.h
│   └── tflite-model
│       ├── tflite_learn_5_compiled.cpp
│       ├── tflite_learn_5_compiled.h
│       └── trained_model_ops_define.h
└── image
    ├── CMakeLists.txt
    ├── README.txt
    ├── edge-impulse-sdk
    │   ├── CMSIS
    │   ├── LICENSE
    │   ├── LICENSE-apache-2.0.txt
    │   ├── README.md
    │   ├── classifier
    │   ├── cmake
    │   ├── dsp
    │   ├── porting
    │   ├── sources.txt
    │   ├── tensorflow
    │   └── third_party
    ├── model-parameters
    │   ├── model_metadata.h
    │   └── model_variables.h
    └── tflite-model
        ├── tflite_learn_5_compiled.cpp
        ├── tflite_learn_5_compiled.h
        └── trained_model_ops_define.h

22 directories, 22 files

New structure after renaming the files:

>multi-impulse % tree -L 3
.
├── audio
│   ├── CMakeLists.txt
│   ├── README.txt
│   ├── edge-impulse-sdk
│   │   ├── CMSIS
│   │   ├── LICENSE
│   │   ├── LICENSE-apache-2.0.txt
│   │   ├── README.md
│   │   ├── classifier
│   │   ├── cmake
│   │   ├── dsp
│   │   ├── porting
│   │   ├── sources.txt
│   │   ├── tensorflow
│   │   └── third_party
│   ├── model-parameters
│   │   ├── model_metadata.h
│   │   └── model_variables.h
│   └── tflite-model
│       ├── trained_model_compiled_audio.cpp
│       ├── trained_model_compiled_audio.h
│       └── trained_model_ops_define.h
└── image
    ├── CMakeLists.txt
    ├── README.txt
    ├── edge-impulse-sdk
    │   ├── CMSIS
    │   ├── LICENSE
    │   ├── LICENSE-apache-2.0.txt
    │   ├── README.md
    │   ├── classifier
    │   ├── cmake
    │   ├── dsp
    │   ├── porting
    │   ├── sources.txt
    │   ├── tensorflow
    │   └── third_party
    ├── model-parameters
    │   ├── model_metadata.h
    │   └── model_variables.h
    └── tflite-model
        ├── trained_model_compiled_image.cpp
        ├── trained_model_compiled_image.h
        └── trained_model_ops_define.h

22 directories, 22 files

Rename the variables in the tflite-model directory

Rename the variables (EON model functions, such as trained_model_input etc. or tflite model array names) by post-fixing them with the name of the project.

e.g: Change the trained_model_compiled_audio.h from:

#ifndef tflite_learn_5_GEN_H
#define tflite_learn_5_GEN_H

#include "edge-impulse-sdk/tensorflow/lite/c/common.h"

// Sets up the model with init and prepare steps.
TfLiteStatus tflite_learn_5_init( void*(*alloc_fnc)(size_t,size_t) );
// Returns the input tensor with the given index.
TfLiteStatus tflite_learn_5_input(int index, TfLiteTensor* tensor);
// Returns the output tensor with the given index.
TfLiteStatus tflite_learn_5_output(int index, TfLiteTensor* tensor);
// Runs inference for the model.
TfLiteStatus tflite_learn_5_invoke();
//Frees memory allocated
TfLiteStatus tflite_learn_5_reset( void (*free)(void* ptr) );


// Returns the number of input tensors.
inline size_t tflite_learn_5_inputs() {
  return 1;
}
// Returns the number of output tensors.
inline size_t tflite_learn_5_outputs() {
  return 1;
}

#endif

to:

#include "edge-impulse-sdk/tensorflow/lite/c/common.h"

// Sets up the model with init and prepare steps.
TfLiteStatus tflite_learn_audio_init( void*(*alloc_fnc)(size_t,size_t) );
// Returns the input tensor with the given index.
TfLiteStatus tflite_learn_audio_input(int index, TfLiteTensor* tensor);
// Returns the output tensor with the given index.
TfLiteStatus tflite_learn_audio_output(int index, TfLiteTensor* tensor);
// Runs inference for the model.
TfLiteStatus tflite_learn_audio_invoke();
//Frees memory allocated
TfLiteStatus tflite_learn_audio_reset( void (*free)(void* ptr) );


// Returns the number of input tensors.
inline size_t tflite_learn_audio_inputs() {
  return 1;
}
// Returns the number of output tensors.
inline size_t tflite_learn_audio_outputs() {
  return 1;
}

#endif

Tip: Use an IDE to use the "Find and replace feature.

Here is a list of the files that need to be modified (the names may change if not compiled with the EON compiler) in folders for both projects:

  • tflite-model/tflite_learn_[block-id]_compiled.h

  • tflite-model/tflite_learn_[block-id]_compiled.cpp

Visual Studio find and replace

Rename the variables and structs in model-parameters/model_variables.h

Be careful here when using the "find and replace" from your IDE, NOT all variables looking like _model_ need to be replaced.

Example for the audio project:

#ifndef _EI_CLASSIFIER_MODEL_VARIABLES_H_
#define _EI_CLASSIFIER_MODEL_VARIABLES_H_

#include <stdint.h>
#include "model_metadata.h"

#include "tflite-model/trained_model_compiled_audio.h"
#include "edge-impulse-sdk/classifier/ei_model_types.h"
#include "edge-impulse-sdk/classifier/inferencing_engines/engines.h"

const char* ei_classifier_inferencing_categories_audio[] = { "Background", "Glass_Breaking" };

uint8_t ei_dsp_config_3_axes_audio[] = { 0 };
const uint32_t ei_dsp_config_3_axes_size_audio = 1;
ei_dsp_config_mfe_t ei_dsp_config_3_audio = {
    3, // uint32_t blockId
    3, // int implementationVersion
    1, // int length of axes
    0.02f, // float frame_length
    0.01f, // float frame_stride
    40, // int num_filters
    256, // int fft_length
    300, // int low_frequency
    0, // int high_frequency
    101, // int win_size
    -52 // int noise_floor_db
};

const size_t ei_dsp_blocks_size_audio = 1;
ei_model_dsp_t ei_dsp_blocks_audio[ei_dsp_blocks_size_audio] = {
    { // DSP block 3
        3960,
        &extract_mfe_features,
        (void*)&ei_dsp_config_3_audio,
        ei_dsp_config_3_axes_audio,
        ei_dsp_config_3_axes_size_audio
    }
};

const ei_config_tflite_eon_graph_t ei_config_tflite_graph_audio_0 = {
    .implementation_version = 1,
    .model_init = &trained_model_audio_init,
    .model_invoke = &trained_model_audio_invoke,
    .model_reset = &trained_model_audio_reset,
    .model_input = &trained_model_audio_input,
    .model_output = &trained_model_audio_output,
};

const ei_learning_block_config_tflite_graph_t ei_learning_block_config_audio_0 = {
    .implementation_version = 1,
    .block_id = 0,
    .object_detection = 0,
    .object_detection_last_layer = EI_CLASSIFIER_LAST_LAYER_UNKNOWN,
    .output_data_tensor = 0,
    .output_labels_tensor = 1,
    .output_score_tensor = 2,
    .graph_config = (void*)&ei_config_tflite_graph_audio_0
};

const size_t ei_learning_blocks_size_audio = 1;
const ei_learning_block_t ei_learning_blocks_audio[ei_learning_blocks_size_audio] = {
    {
        &run_nn_inference,
        (void*)&ei_learning_block_config_audio_0,
    },
};

const ei_model_performance_calibration_t ei_calibration_audio = {
    1, /* integer version number */
    false, /* has configured performance calibration */
    (int32_t)(EI_CLASSIFIER_RAW_SAMPLE_COUNT / ((EI_CLASSIFIER_FREQUENCY > 0) ? EI_CLASSIFIER_FREQUENCY : 1)) * 1000, /* Model window */
    0.8f, /* Default threshold */
    (int32_t)(EI_CLASSIFIER_RAW_SAMPLE_COUNT / ((EI_CLASSIFIER_FREQUENCY > 0) ? EI_CLASSIFIER_FREQUENCY : 1)) * 500, /* Half of model window */
    0   /* Don't use flags */
};


const ei_impulse_t impulse_233502_3 = {
    .project_id = 233502,
    .project_owner = "Edge Impulse Inc.",
    .project_name = "Glass breaking - audio classification",
    .deploy_version = 3,

    .nn_input_frame_size = 3960,
    .raw_sample_count = 16000,
    .raw_samples_per_frame = 1,
    .dsp_input_frame_size = 16000 * 1,
    .input_width = 0,
    .input_height = 0,
    .input_frames = 0,
    .interval_ms = 0.0625,
    .frequency = 16000,
    .dsp_blocks_size = ei_dsp_blocks_size_audio,
    .dsp_blocks = ei_dsp_blocks_audio,

    .object_detection = 0,
    .object_detection_count = 0,
    .object_detection_threshold = 0,
    .object_detection_last_layer = EI_CLASSIFIER_LAST_LAYER_UNKNOWN,
    .fomo_output_size = 0,

    .tflite_output_features_count = 2,
    .learning_blocks_size = ei_learning_blocks_size_audio,
    .learning_blocks = ei_learning_blocks_audio,

    .inferencing_engine = EI_CLASSIFIER_TFLITE,

    .quantized = 1,

    .compiled = 1,

    .sensor = EI_CLASSIFIER_SENSOR_MICROPHONE,
    .fusion_string = "audio",
    .slice_size = (16000/4),
    .slices_per_model_window = 4,

    .has_anomaly = 0,
    .label_count = 2,
    .calibration = ei_calibration_audio,
    .categories = ei_classifier_inferencing_categories_audio
};

const ei_impulse_t ei_default_impulse = impulse_233502_3;

#endif // _EI_CLASSIFIER_MODEL_METADATA_H_

Example for the image project:

#ifndef _EI_CLASSIFIER_MODEL_VARIABLES_H_
#define _EI_CLASSIFIER_MODEL_VARIABLES_H_

#include <stdint.h>
#include "model_metadata.h"

#include "tflite-model/trained_model_compiled_image.h"
#include "edge-impulse-sdk/classifier/ei_model_types.h"
#include "edge-impulse-sdk/classifier/inferencing_engines/engines.h"

const char* ei_classifier_inferencing_categories_image[] = { "person", "unknown" };

uint8_t ei_dsp_config_3_axes_image[] = { 0 };
const uint32_t ei_dsp_config_3_axes_size_image = 1;
ei_dsp_config_image_t ei_dsp_config_3_image = {
    3, // uint32_t blockId
    1, // int implementationVersion
    1, // int length of axes
    "RGB" // select channels
};

const size_t ei_dsp_blocks_size_image = 1;
ei_model_dsp_t ei_dsp_blocks_image[ei_dsp_blocks_size_image] = {
    { // DSP block 3
        27648,
        &extract_image_features,
        (void*)&ei_dsp_config_3_image,
        ei_dsp_config_3_axes_image,
        ei_dsp_config_3_axes_size_image
    }
};

const ei_config_tflite_eon_graph_t ei_config_tflite_graph_image_0 = {
    .implementation_version = 1,
    .model_init = &trained_model_image_init,
    .model_invoke = &trained_model_image_invoke,
    .model_reset = &trained_model_image_reset,
    .model_input = &trained_model_image_input,
    .model_output = &trained_model_image_output,
};

const ei_learning_block_config_tflite_graph_t ei_learning_block_config_image_0 = {
    .implementation_version = 1,
    .block_id = 0,
    .object_detection = 0,
    .object_detection_last_layer = EI_CLASSIFIER_LAST_LAYER_UNKNOWN,
    .output_data_tensor = 0,
    .output_labels_tensor = 1,
    .output_score_tensor = 2,
    .graph_config = (void*)&ei_config_tflite_graph_image_0
};

const size_t ei_learning_blocks_size_image = 1;
const ei_learning_block_t ei_learning_blocks_image[ei_learning_blocks_size_image] = {
    {
        &run_nn_inference,
        (void*)&ei_learning_block_config_image_0,
    },
};

const ei_model_performance_calibration_t ei_calibration_image = {
    1, /* integer version number */
    false, /* has configured performance calibration */
    (int32_t)(EI_CLASSIFIER_RAW_SAMPLE_COUNT / ((EI_CLASSIFIER_FREQUENCY > 0) ? EI_CLASSIFIER_FREQUENCY : 1)) * 1000, /* Model window */
    0.8f, /* Default threshold */
    (int32_t)(EI_CLASSIFIER_RAW_SAMPLE_COUNT / ((EI_CLASSIFIER_FREQUENCY > 0) ? EI_CLASSIFIER_FREQUENCY : 1)) * 500, /* Half of model window */
    0   /* Don't use flags */
};


const ei_impulse_t impulse_233515_5 = {
    .project_id = 233515,
    .project_owner = "Edge Impulse Inc.",
    .project_name = "Person vs unknown - image classification",
    .deploy_version = 5,

    .nn_input_frame_size = 27648,
    .raw_sample_count = 9216,
    .raw_samples_per_frame = 1,
    .dsp_input_frame_size = 9216 * 1,
    .input_width = 96,
    .input_height = 96,
    .input_frames = 1,
    .interval_ms = 1,
    .frequency = 0,
    .dsp_blocks_size = ei_dsp_blocks_size_image,
    .dsp_blocks = ei_dsp_blocks_image,

    .object_detection = 0,
    .object_detection_count = 0,
    .object_detection_threshold = 0,
    .object_detection_last_layer = EI_CLASSIFIER_LAST_LAYER_UNKNOWN,
    .fomo_output_size = 0,

    .tflite_output_features_count = 2,
    .learning_blocks_size = ei_learning_blocks_size_image,
    .learning_blocks = ei_learning_blocks_image,

    .inferencing_engine = EI_CLASSIFIER_TFLITE,

    .quantized = 1,

    .compiled = 1,

    .sensor = EI_CLASSIFIER_SENSOR_CAMERA,
    .fusion_string = "image",
    .slice_size = (9216/4),
    .slices_per_model_window = 4,

    .has_anomaly = 0,
    .label_count = 2,
    .calibration = ei_calibration_image,
    .categories = ei_classifier_inferencing_categories_image
};

const ei_impulse_t ei_default_impulse = impulse_233515_5;

#endif // _EI_CLASSIFIER_MODEL_METADATA_H_

Merge the files

Create a new directory (merged-impulse for example). Copy the content of one project into this new directory (audio for example). Copy the content of the tflite-model directory from the other project (image) inside the newly created merged-impulse/tflite-model.

The structure of this new directory should look like the following:

> merged-impulse % tree -L 2
.
├── CMakeLists.txt
├── README.txt
├── edge-impulse-sdk
│   ├── CMSIS
│   ├── LICENSE
│   ├── LICENSE-apache-2.0.txt
│   ├── README.md
│   ├── classifier
│   ├── cmake
│   ├── dsp
│   ├── porting
│   ├── sources.txt
│   ├── tensorflow
│   └── third_party
├── model-parameters
│   ├── model_metadata.h
│   └── model_variables.h
└── tflite-model
    ├── trained_model_compiled_audio.cpp
    ├── trained_model_compiled_audio.h
    ├── trained_model_compiled_image.cpp
    ├── trained_model_compiled_image.h
    ├── trained_model_ops_define_audio.h
    └── trained_model_ops_define_image.h

10 directories, 14 files

Merge the variables and structs in model_variables.h

Copy the necessary variables and structs from previously updated image/model_metadata.h file content to the merged-impulse/model_metadata.h.

To do so, include both of these lines in the #include section:

#include "tflite-model/trained_model_compiled_audio.h"
#include "tflite-model/trained_model_compiled_image.h"

The section that should be copied is from const char* ei_classifier_inferencing_categories... to the line before const ei_impulse_t ei_default_impulse = impulse_<ProjectID>_<version>.

Make sure to leave only one const ei_impulse_t ei_default_impulse = impulse_233502_3; this will define which of your impulse is the default one.

Subtract and merge the trained_model_ops_define.h or tflite_resolver.h

Make sure the macros EI_TFLITE_DISABLE_... are a COMBINATION of the ones present in two deployments.

For EON-compiled projects:

E.g. if #define EI_TFLITE_DISABLE_SOFTMAX_IN_U8 1 is present in one deployment and absent in the other, it should be ABSENT in the combined trained_model_ops_define.h.

For non-EON-Compiled projects:

E.g. if resolver.AddFullyConnected(); is present in one deployment and absent in the other, it should be PRESENT in the combined tflite-resolver.h. Remember to change the length of the resolver array if necessary.

In this example, here are the lines to deleted:

diff trained_model_ops_define.h

Transforming clinical data

Transformation blocks take raw data from your and convert the data into a different dataset or files that can be loaded in an Edge Impulse project. You can use transformation blocks to only include certain parts of individual data files, calculate long-running features like a running mean or derivatives, or efficiently generate features with different window lengths. Transformation blocks can be written in any language, and run on the Edge Impulse infrastructure.

No required format for data files

There is no required format for data files. You can upload data in any format, whether it's CSV, Parquet, or a proprietary data format.

Parquet is a columnar storage format that is optimized for reading and writing large datasets. It is particularly useful for data that is stored in S3 buckets, as it can be read in parallel and is highly compressed.

The PPG-DaLiA dataset is a multimodal collection featuring physiological and motion data recorded from 15 subjects.

In this tutorial we build a Python-based transformation block that loads Parquet files, we process the dataset by calculating features and transforming it into a unified schema suitable for machine learning. If you haven't done so, go through first.

1. Prerequisites

You'll need:

  • The .

    • If you receive any warnings that's fine. Run edge-impulse-blocks afterwards to verify that the CLI was installed correctly.

  • PPG-DaLiA CSV files: Download files like ACC.csv, HR.csv, EDA.csv, etc., which contain sensor data.

Transformation blocks use Docker containers, a virtualization technique that lets developers package up an application with all dependencies in a single package. If you want to test your blocks locally you'll also need (this is not a requirement):

  • installed on your machine.

2. Building your first transformation block

To build a transformation block open a command prompt or terminal window, create a new folder, and run:

This will prompt you to log in, and enter the details for your block. E.g.:

Then, create the following files in this directory:

2.1 - Dockerfile

We're building a Python based transformation block. The Dockerfile describes our base image (Python 3.7.5), our dependencies (in requirements.txt) and which script to run (transform.py).

Note: Do not use a WORKDIR under /home! The /home path will be mounted in by Edge Impulse, making your files inaccessible.

ENTRYPOINT vs RUN / CMD

If you use a different programming language, make sure to use ENTRYPOINT to specify the application to execute, rather than RUN or CMD.

2.2 - requirements.txt

This file describes the dependencies for the block. We'll be using pandas and pyarrow to parse the Parquet file, and numpy to do some calculations.

2.3 - transform.py

This file includes the actual application. Transformation blocks are invoked with three parameters (as command line arguments):

  • --in-file or --in-directory - A file (if the block operates on a file), or a directory (if the block operates on a data item) from the organizational dataset. In this case the unified_data.parquet file.

  • --out-directory - Directory to write files to.

  • --hmac-key - You can use this HMAC key to sign the output files. This is not used in this tutorial.

  • --metadata - Key/value pairs containing the metadata for the data item, plus additional metadata about the data item in the dataItemInfo key. E.g.: { "subject": "AAA001", "ei_check": "1", "dataItemInfo": { "id": 101, "dataset": "Human Activity 2022", "bucketName": "edge-impulse-tutorial", "bucketPath": "janjongboom/human_activity/AAA001/", "created": "2022-03-07T09:20:59.772Z", "totalFileCount": 14, "totalFileSize": 6347421 } }

Add the following content. This takes in the Parquet file, groups data by their label, and then calculates the RMS over the X, Y and Z axes of the accelerometer.

Docker

You can also build the container locally via Docker, and test the block. The added benefit is that you don't need any dependencies installed on your local computer, and can thus test that you've included everything that's needed for the block. This requires Docker desktop to be installed.

To build the container and test the block, open a command prompt or terminal window and navigate to the source directory. First, build the container:

Then, run the container (make sure unified_data.parquet is in the same directory):

Seeing the output

This process has generated a new Parquet file in the out/ directory containing the RMS of the X, Y and Z axes. If you inspect the content of the file (e.g. using parquet-tools) you'll see the output:

If you don't have parquet-tools installed, you can install it via:

Then, run:

This will show you the metadata and the columns in the file:

code output block:

3. Pushing the transformation block to Edge Impulse

With the block ready we can push it to your organization. Open a command prompt or terminal window, navigate to the folder you created earlier, and run:

This packages up your folder, sends it to Edge Impulse where it'll be built, and finally is added to your organization.

The transformation block is now available in Edge Impulse under Data transformation > Transformation blocks.

If you make any changes to the block, just re-run edge-impulse-blocks push and the block will be updated.

4. Uploading unified_data.parquet to Edge Impulse

Next, upload the unified_data.parquet file, by going to Data > Add data... > Add data item, setting name as 'Gestures', dataset to 'Transform tutorial', and selecting the Parquet file.

This makes the unified_data.parquet file available from the Data page.

5. Starting the transformation

With the Parquet file in Edge Impulse and the transformation block configured you can now create a new job. Go to Data, and select the Parquet file by setting the filter to dataset = 'Transform tutorial'.

Click the checkbox next to the data item, and select Transform selected (1 file). On the 'Create transformation job' page select 'Import data into Dataset'. Under 'output dataset', select 'Same dataset as source', and under 'Transformation block' select the new transformation block.

Click Start transformation job to start the job. This pulls the data in, starts a transformation job and finally uploads the data back to your dataset. If you have multiple files selected the transformations will also run in parallel.

You can now find the transformed file back in your dataset.

6. Next steps

Transformation blocks are a powerful feature which let you set up a data pipeline to turn raw data into actionable machine learning features. It also gives you a reproducible way of transforming many files at once, and is programmable through the so you can automatically convert new incoming data. If you're interested in transformation blocks or any of the other enterprise features,

🚀

Appendix: Advanced features

Updating metadata from a transformation block

You can update the metadata of blocks directly from a transformation block by creating a ei-metadata.json file in the output directory. The metadata is then applied to the new data item automatically when the transform job finishes. The ei-metadata.json file has the following structure:

Some notes:

  • If action is set to add the metadata keys are added to the data item. If action is set to replace all existing metadata keys are removed.

Environmental variables

Transformation blocks get access to the following environmental variables, which let you authenticate with the Edge Impulse API. This way you don't have to inject these credentials into the block. The variables are:

  • EI_API_KEY - an API key with 'member' privileges for the organization.

  • EI_ORGANIZATION_ID - the organization ID that the block runs in.

  • EI_API_ENDPOINT - the API endpoint (default: https://studio.edgeimpulse.com/v1).

Custom parameters

You can specify custom arguments or parameters to your block by adding a file in the root of your block directory. This file describes all arguments for your training pipeline, and is used to render custom UI elements for each parameter. For example, this parameters file:

Renders the following UI when you run the transformation block:

And the options are passed in as command line arguments to your block:

For more information, and all options see .

$ edge-impulse-blocks init
Edge Impulse Blocks v1.9.0
? What is your user name or e-mail address (edgeimpulse.com)? [email protected]
? What is your password? [hidden]
Attaching block to organization 'Demo Organization'
? Choose a type of block Transformation block
? Choose an option Create a new block
? Enter the name of your block DaLiA Transformation
? Enter the description of your block Process DaLiA data and extract accelerometer features
Creating block with config: {
  name: 'DaLiA Transformation',
  type: 'transform',
  description: 'Processes accelerometer and activity data from the PPG-DaLiA dataset',
  organizationId: 34
}
FROM python:3.7.5-stretch

WORKDIR /app

# Python dependencies
COPY requirements.txt ./
RUN pip3 --no-cache-dir install -r requirements.txt

COPY . ./

ENTRYPOINT [ "python3",  "transform.py" ]
numpy==1.16.4
pandas==0.23.4
pyarrow==0.16.0
import numpy as np
import os
import argparse
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq

# Parse arguments
parser = argparse.ArgumentParser(description='Simple transformation block for single directory PPG-DaLiA data processing')
parser.add_argument('--in-directory', type=str, required=True, help="Path to the directory containing the CSV files")
parser.add_argument('--out-directory', type=str, required=True, help="Path to save the transformed Parquet file")
args = parser.parse_args()

# Check input and output directories
if not os.path.exists(args.in_directory):
    print(f"Data directory {args.in_directory} does not exist.", flush=True)
    exit(1)

if not os.path.exists(args.out_directory):
    os.makedirs(args.out_directory)

# Define paths to the necessary CSV files
acc_file = os.path.join(args.in_directory, 'ACC.csv')
hr_file = os.path.join(args.in_directory, 'HR.csv')
eda_file = os.path.join(args.in_directory, 'EDA.csv')
bvp_file = os.path.join(args.in_directory, 'BVP.csv')
temp_file = os.path.join(args.in_directory, 'TEMP.csv')
activity_file = os.path.join(args.in_directory, 'S1_activity.csv')

# Check if all required files are available
for file_path in [acc_file, hr_file, eda_file, bvp_file, temp_file, activity_file]:
    if not os.path.exists(file_path):
        print(f"Missing file {file_path}. Skipping processing.", flush=True)
        exit(1)

# Load data from CSV files
acc_data = pd.read_csv(acc_file, header=None, skiprows=2, names=['accX', 'accY', 'accZ'])
hr_data = pd.read_csv(hr_file, header=None, skiprows=2, names=['heart_rate'])
eda_data = pd.read_csv(eda_file, header=None, skiprows=2, names=['eda'])
bvp_data = pd.read_csv(bvp_file, header=None, skiprows=2, names=['bvp'])
temp_data = pd.read_csv(temp_file, header=None, skiprows=2, names=['temperature'])

# Load and clean activity labels
activity_labels = pd.read_csv(activity_file, header=None, skiprows=1, names=['activity', 'start_row'])
activity_labels['activity'] = activity_labels['activity'].str.strip()  # Remove leading/trailing whitespace
activity_labels['start_row'] = pd.to_numeric(activity_labels['start_row'], errors='coerce')  # Convert to numeric, setting invalid parsing to NaN
activity_labels = activity_labels.dropna(subset=['start_row']).reset_index(drop=True)  # Remove invalid rows and reset index
activity_labels['start_row'] = activity_labels['start_row'].astype(int)

# Set default activity and map activities to rows based on start_row intervals
acc_data['activity'] = 'NO_ACTIVITY'  # Default activity
for i in range(len(activity_labels) - 1):
    activity = activity_labels.loc[i, 'activity']
    start_row = activity_labels.loc[i, 'start_row']
    end_row = activity_labels.loc[i + 1, 'start_row']
    acc_data.loc[start_row:end_row - 1, 'activity'] = activity

# Handle the last activity to the end of the dataset
last_activity = activity_labels.iloc[-1]['activity']
last_start_row = activity_labels.iloc[-1]['start_row']
acc_data.loc[last_start_row:, 'activity'] = last_activity

# Calculate features
acc_features = {
    'accX_rms': np.sqrt(np.mean(acc_data['accX']**2)),
    'accY_rms': np.sqrt(np.mean(acc_data['accY']**2)),
    'accZ_rms': np.sqrt(np.mean(acc_data['accZ']**2)),
}
hr_mean = hr_data['heart_rate'].mean()
eda_mean = eda_data['eda'].mean()
bvp_mean = bvp_data['bvp'].mean()
temp_mean = temp_data['temperature'].mean()

# Combine features and unique activity labels
features = {
    **acc_features,
    'heart_rate_mean': hr_mean,
    'eda_mean': eda_mean,
    'bvp_mean': bvp_mean,
    'temperature_mean': temp_mean,
    'activity_labels': [activity_labels['activity'].tolist()]  # Nest the list of activities
}

# Convert features to DataFrame and save as Parquet file
features_df = pd.DataFrame([features])
out_file = os.path.join(args.out_directory, 'unified_data.parquet')
table = pa.Table.from_pandas(features_df)
pq.write_table(table, out_file)

print(f'Written features Parquet file: {out_file}', flush=True)
docker build -t ppg-dalia-transform .
[+] Building 1.8s (13/13) FINISHED                                                                                                                                          docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                                                        0.0s
 => => transferring dockerfile: 402B                                                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/ubuntu:20.04                                                                                                                             0.9s
 => [auth] library/ubuntu:pull token for registry-1.docker.io                                                                                                                               0.0s
 => [internal] load .dockerignore                                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                                             0.0s
 => [1/7] FROM docker.io/library/ubuntu:20.04@sha256:8e5c......7555141b                                                                       0.0s
 => [internal] load build context                                                                                                                                                           0.0s
 => => transferring context: 4.75MB                                                                                                                                                         0.0s
 => CACHED [2/7] WORKDIR /app                                                                                                                                                               0.0s
 => CACHED [3/7] RUN apt update && apt install -y python3 python3-distutils wget                                                                                                            0.0s
 => CACHED [4/7] RUN wget https://bootstrap.pypa.io/get-pip.py &&     python3.8 get-pip.py "pip==21.3.1" &&     rm get-pip.py                                                               0.0s
 => CACHED [5/7] COPY requirements.txt ./                                                                                                                                                   0.0s
 => CACHED [6/7] RUN pip3 --no-cache-dir install -r requirements.txt                                                                                                                        0.0s
 => [7/7] COPY . ./                                                                                                                                                                         0.5s
 => exporting to image                                                                                                                                                                      0.3s
 => => exporting layers                                                                                                                                                                     0.3s
 => => writing image sha256:856a1ec5eb879c904.......88e5e48899a                                                                                                0.0s
 => => naming to docker.io/library/ppg-dalia-transform                                                                                                                                      0.0s

View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/rnpnsjzniokmbvx29fj4cs0x3

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview
$ docker run --rm -v $PWD:/data test-org-transform-parquet-dataset --in-file /data/unified_data.parquet --out-directory /data/out
$ pip install parquet-tools
$  parquet-tools inspect out/unified_data.parquet
$
a############ file meta data ############
created_by: parquet-cpp-arrow version 18.0.0-SNAPSHOT
num_columns: 7
num_rows: 1
num_row_groups: 1
format_version: 2.6
serialized_size: 4373


############ Columns ############
accX_rms
accY_rms
accZ_rms
heart_rate_mean
eda_mean
bvp_mean
temperature_mean

############ Column(accX_rms) ############
name: accX_rms
path: accX_rms
max_definition_level: 1
max_repetition_level: 0
physical_type: DOUBLE
logical_type: None
converted_type (legacy): NONE
compression: SNAPPY (space_saved: -4%)

############ Column(accY_rms) ############
name: accY_rms
path: accY_rms
max_definition_level: 1
max_repetition_level: 0
physical_type: DOUBLE
logical_type: None
converted_type (legacy): NONE
compression: SNAPPY (space_saved: -4%)

############ Column(accZ_rms) ############
name: accZ_rms
path: accZ_rms
max_definition_level: 1
max_repetition_level: 0
physical_type: DOUBLE
logical_type: None
converted_type (legacy): NONE
compression: SNAPPY (space_saved: -4%)


Success!
$ edge-impulse-blocks push
Edge Impulse Blocks v1.9.0
Archiving 'tutorial-processing-block'...
Archiving 'tutorial-processing-block' OK (2 KB) /var/folders/3r/fds0qzv914ng4t17nhh5xs5c0000gn/T/ei-transform-block-7812190951a6038c2f442ca02d428c59.tar.gz

Uploading block 'Demo dalia-ppg transformation' to organization 'Moe's Demo Org'...
Uploading block 'Demo dalia-ppg transformation' to organization 'Demo org Inc.' OK

Building transformation block 'Demo dalia-ppg transformation'...
Job started
...
Building transformation block 'Demo dalia-ppg transformation' OK

Your block has been updated, go to https://studio.edgeimpulse.com/organization/34/data to run a new transformation
{
    "version": 1,
    "action": "add",
    "metadata": {
        "some-key": "some-value"
    }
}
[{
    "name": "Bucket",
    "type": "bucket",
    "param": "bucket-name",
    "value": "",
    "help": "The bucket where you're hosting all data"
},
{
    "name": "Bucket prefix",
    "value": "my-test-prefix/",
    "type": "string",
    "param": "bucket-prefix",
    "help": "The prefix in the bucket, where you're hosting the data"
}]
--bucket-name "ei-data-dev" --bucket-prefix "my-test-prefix/"
organizational datasets
synchronizing clinical data with a bucket
Edge Impulse CLI
Docker desktop
Edge Impulse API
let us know!
parameters.json
parameters.json
The transformation block in Edge Impulse
Selecting the transform tutorial dataset
Configuring the transformation job
Dataset transformation running
Running a transformation block with custom parameters
Sample Image
Image with Drop Shadow

Only available on the Enterprise plan

This feature is only available on the Enterprise plan. Review our plans and pricing or sign up for our free Enterprise trial today.

Using the Edge Impulse Python SDK to upload and download data

If you want to upload files directly to an Edge Impulse project, we recommend using the CLI uploader tool. However, sometimes you cannot upload your samples directly, as you might need to convert the files to one of the accepted formats or modify the data prior to model training. Edge Impulse offers data augmentation for some types of projects, but you might want to create your own custom augmentation scheme. Or perhaps you want to generate synthetic data and script the upload process.

The Python SDK offers a set of functions to help you move data into and out of your project. This can be extremely helpful when generating or augmenting your dataset. The following cells demonstrate some of these upload and download functions.

You can find the API documentation for the functions found in this tutorial here.

WARNING: This notebook will add and delete data in your Edge Impulse project, so be careful! We recommend creating a throwaway project when testing this notebook.

Note that you might need to refresh the page with your Edge Impulse project to see the samples appear.

# If you have not done so already, install the following dependencies
!python -m pip install edgeimpulse
import edgeimpulse as ei

You will need to obtain an API key from an Edge Impulse project. Log into edgeimpulse.com and create a new project. Open the project, navigate to Dashboard and click on the Keys tab to view your API keys. Double-click on the API key to highlight it, right-click, and select Copy.

Copy API key from Edge Impulse project

Note that you do not actually need to use the project in the Edge Impulse Studio. We just need the API Key.

Paste that API key string in the ei.API_KEY value in the following cell:

# Settings
ei.API_KEY = "ei_dae2..." # Change this to your Edge Impulse API key

Upload directory

You can upload all files in a directory using the Python SDK. Note that you can set the category, label, and metadata for all files with a single call. If you want to use a different label for each file set label=None in the function call and name your files with <label>.<name>.<ext>. For example, wave.01.csv will have the label wave when uploaded. See here for more information.

The following file formats are allowed: .cbor, .json, .csv, .wav, .jpg, .png, .mp4, .avi.

from datetime import datetime
# Download image files to use as an example dataset
!mkdir -p dataset
!wget -P dataset -q \
  https://raw.githubusercontent.com/edgeimpulse/notebooks/main/.assets/images/capacitor.01.png \
  https://raw.githubusercontent.com/edgeimpulse/notebooks/main/.assets/images/capacitor.02.png
# Upload the entire directory
response = ei.experimental.data.upload_directory(
    directory="dataset",
    category="training",
    label=None, # Will use the prefix before the '.' on each filename for the label
    metadata={
        "date": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        "source": "camera",
    }
)

# Check to make sure there were no failures
assert len(response.fails) == 0, "Could not upload some files"

# Save the sample IDs, as we will need these to retrieve file information and delete samples
ids = []
for sample in response.successes:
    ids.append(sample.sample_id)

# Review the sample IDs and get the associated server-side filename
# Note the lack of extension! Multiple samples on the server can have the same filename.
for id in ids:
    filename = ei.experimental.data.get_filename_by_id(id)
    print(f"Sample ID: {id}, filename: {filename}")

If you head to the Data acquisition page on your project, you should see images in your dataset.

Images uploaded to Edge Impulse project

Download files

You can download samples from your Edge Impulse project if you know the sample IDs. You can get sample IDs by calling the ei.data.get_sample_ids() function, which allows you to filter IDs based on filename, category, and label.

# Get sample IDs for everything in the "training" category
infos = ei.experimental.data.get_sample_ids(category="training")

# The SampleInfo should match what we uploaded earlier
ids = []
for info in infos:
    print(info)
    ids.append(info.sample_id)
# Download samples
samples = ei.experimental.data.download_samples_by_ids(ids)

# Save the downloaded files
for sample in samples:
    with open(sample.filename, "wb") as file:
        file.write(sample.data.read())

# View sample information
for sample in samples:
    print(
        f"filename: {sample.filename}\r\n"
        f"  sample ID: {sample.sample_id}\r\n"
        f"  category: {sample.category}\r\n"
        f"  label: {sample.label}\r\n"
        f"  bounding boxes: {sample.bounding_boxes}\r\n"
        f"  metadata: {sample.metadata}"
    )

Take a look at the files in this directory. You should see the downloaded images. They should match the images in the dataset/ directory, which were the original images that we uploaded.

Delete files

If you know the ID of the sample you would like to delete, you can call the delete_sample_by_id() function. You can also delete all the samples in your project by calling delete_all_samples().

# Delete the samples from the Edge Impulse project that we uploaded earlier
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Take a look at the data in your project. The samples that we uploaded should be gone.

Upload folder for object detection

For object detection, you can put bounding box information (following the Edge Impulse JSON bounding box format) in a file named info.labels in that same directory.

Important! The annotations file must be named exactly info.labels

# Download images and bounding box annotations to use as an example dataset
!mkdir -p dataset
!rm dataset/capacitor.01.png dataset/capacitor.02.png
!wget -P dataset -q \
  https://raw.githubusercontent.com/edgeimpulse/notebooks/main/.assets/images/dog-ball-toy.01.png \
  https://raw.githubusercontent.com/edgeimpulse/notebooks/main/.assets/images/dog-ball-toy.02.png \
  https://raw.githubusercontent.com/edgeimpulse/notebooks/main/.assets/annotations/info.labels
# Upload the entire directory (including the info.labels file)
response = ei.experimental.data.upload_exported_dataset(
    directory="dataset",
)

# Check to make sure there were no failures
assert len(response.fails) == 0, "Could not upload some files"

# Save the sample IDs, as we will need these to retrieve file information and delete samples
ids = []
for sample in response.successes:
    ids.append(sample.sample_id)

If you head to the Data acquisition page on your project, you should see images in your dataset along with the bounding box information.

Images uploaded to Edge Impulse project
# Delete the samples from the Edge Impulse project that we uploaded
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Upload individual CSV files

The Edge Impulse ingestion service accepts CSV files, which we can use to upload raw data. Note that if you configure a CSV template using the CSV Wizard, then the expected format of the CSV file might change. If you do not configure a CSV template, then the ingestion service expects CSV data to be in a particular format. See here for details about the default CSV format.

import csv
import io
import os
# Create example CSV data
sample_data = [
    [
        ["timestamp", "accX", "accY", "accZ"],
        [0, -9.81, 0.03, 0.21],
        [10, -9.83, 0.04, 0.27],
        [20, -9.12, 0.03, 0.23],
        [30, -9.14, 0.01, 0.25],
    ],
    [
        ["timestamp", "accX", "accY", "accZ"],
        [0, -9.56, 5.34, 1.21],
        [10, -9.43, 1.37, 1.27],
        [20, -9.22, -4.03, 1.23],
        [30, -9.50, -0.98, 1.25],
    ],
]

# Write to CSV files
filenames = [
    "001.csv",
    "002.csv"
]
for i, filename in enumerate(filenames):
    file_path = os.path.join("dataset", filename)
    with open(file_path, "w", newline="") as file:
        writer = csv.writer(file)
        writer.writerows(sample_data[i])
# Add metadata to the CSV data
my_samples = [
    {
        "filename": filenames[0],
        "data": open(os.path.join("dataset", filenames[0]), "rb"),
        "category": "training",
        "label": "idle",
        "metadata": {
            "source": "accelerometer",
            "collection site": "desk",
        },
    },
    {
        "filename": filenames[1],
        "data": open(os.path.join("dataset", filenames[1]), "rb"),
        "category": "training",
        "label": "wave",
        "metadata": {
            "source": "accelerometer",
            "collection site": "desk",
        },
    },
]
# Wrap the samples in instances of the Sample class
samples = [ei.experimental.data.Sample(**i) for i in my_samples]

# Upload samples to your project
response = ei.experimental.data.upload_samples(samples)

# Check to make sure there were no failures
assert len(response.fails) == 0, "Could not upload some files"

# Save the sample IDs, as we will need these to retrieve file information and delete samples
ids = []
for sample in response.successes:
    ids.append(sample.sample_id)

If you head to the Data acquisition page on your project, you should see your time series data.

Copy API key from Edge Impulse project
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Upload JSON data directly

Another way to upload data is to encode it in JSON format. See the data acquisition format specificaion for more information on acceptable key/value pairs. Note that at this time, the signature value can be set to 0.

The raw data must be encoded in an IO object. We convert the dictionary objects to a BytesIO object, but you can also read in data from .json files.

import io
import json
# Create two different example data samples
sample_data_1 = {
    "protected": {
        "ver": "v1",
        "alg": "none",
    },
    "signature": 0,
    "payload": {
        "device_name": "ac:87:a3:0a:2d:1b",
        "device_type": "DISCO-L475VG-IOT01A",
        "interval_ms": 10,
        "sensors": [
            { "name": "accX", "units": "m/s2" },
            { "name": "accY", "units": "m/s2" },
            { "name": "accZ", "units": "m/s2" }
        ],
        "values": [
            [ -9.81, 0.03, 0.21 ],
            [ -9.83, 0.04, 0.27 ],
            [ -9.12, 0.03, 0.23 ],
            [ -9.14, 0.01, 0.25 ]
        ]
    }
}
sample_data_2 = {
    "protected": {
        "ver": "v1",
        "alg": "none",
    },
    "signature": 0,
    "payload": {
        "device_name": "ac:87:a3:0a:2d:1b",
        "device_type": "DISCO-L475VG-IOT01A",
        "interval_ms": 10,
        "sensors": [
            { "name": "accX", "units": "m/s2" },
            { "name": "accY", "units": "m/s2" },
            { "name": "accZ", "units": "m/s2" }
        ],
        "values": [
            [ -9.56, 5.34, 1.21 ],
            [ -9.43, 1.37, 1.27 ],
            [ -9.22, -4.03, 1.23 ],
            [ -9.50, -0.98, 1.25 ]
        ]
    }
}
# Provide a filename, category, label, and optional metadata for each sample
my_samples = [
    {
        "filename": "001.json",
        "data": io.BytesIO(json.dumps(sample_data_1).encode('utf-8')),
        "category": "training",
        "label": "idle",
        "metadata": {
            "source": "accelerometer",
            "collection site": "desk",
        },
    },
    {
        "filename": "002.json",
        "data": io.BytesIO(json.dumps(sample_data_2).encode('utf-8')),
        "category": "training",
        "label": "wave",
        "metadata": {
            "source": "accelerometer",
            "collection site": "desk",
        },
    },
]
# Wrap the samples in instances of the Sample class
samples = [ei.data.sample_type.Sample(**i) for i in my_samples]

# Upload samples to your project
response = ei.experimental.data.upload_samples(samples)

# Check to make sure there were no failures
assert len(response.fails) == 0, "Could not upload some files"

# Save the sample IDs, as we will need these to retrieve file information and delete samples
ids = []
for sample in response.successes:
    ids.append(sample.sample_id)

If you head to the Data acquisition page on your project, you should see your time series data.

Copy API key from Edge Impulse project
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Upload NumPy arrays

NumPy is powerful Python library for working with large arrays and matrices. You can upload NumPy arrays directly into your Edge Impulse project. Note that the arrays are required to be in a particular format, and must be uploaded with required metadata (such as a list of labels and the sample rate).

Important! NumPy arrays must be in the shape (Number of samples, number of data points, number of sensors)

If you are working with image data in NumPy, we recommend saving those images as .png or .jpg files and using upload_directory().

import numpy as np
# Create example NumPy array with 2 time series samples
sample_data = np.array(
    [
        [ # Sample 1 ("idle")
            [-9.81, 0.03, 0.21],
            [-9.83, 0.04, 0.27],
            [-9.12, 0.03, 0.23],
            [-9.14, 0.01, 0.25],
        ],
        [ # Sample 2 ("wave")
            [-9.56, 5.34, 1.21],
            [-9.43, 1.37, 1.27],
            [-9.22, -4.03, 1.23],
            [-9.50, -0.98, 1.25],
        ],
    ]
)
# Labels for each sample
labels = ["idle", "wave"]

# Names of the sensors and units for the 3 axes
sensors = [
    { "name": "accX", "units": "m/s2" },
    { "name": "accY", "units": "m/s2" },
    { "name": "accZ", "units": "m/s2" },
]

# Optional metadata for all samples being uploaded
metadata = {
    "source": "accelerometer",
    "collection site": "desk",
}
# Upload samples to your project
response = ei.experimental.data.upload_numpy(
    data=sample_data,
    labels=labels,
    sensors=sensors,
    sample_rate_ms=10,
    metadata=metadata,
    category="training",
)

# Check to make sure there were no failures
assert len(response.fails) == 0, "Could not upload some files"

# Save the sample IDs, as we will need these to retrieve file information and delete samples
ids = []
for sample in response.successes:
    ids.append(sample.sample_id)

If you head to the Data acquisition page on your project, you should see your time series data. Note that the sample names are randomly assigned, so we recommend recording the sample IDs when you upload.

Copy API key from Edge Impulse project
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Upload pandas (and pandas-like) dataframes

pandas is popular Python library for performing data manipulation and analysis. The Edge Impulse library supports a number of ways to upload dataframes. We will go over each format.

Note that several other packages exist that work as drop-in replacements for pandas. You can use these replacements so long as you import that with the name pd. For example, one of:

import pandas as pd
import modin.pandas as pd
import dask.dataframe as pd
import polars as pd
import pandas as pd

The first option is to upload one dataframe for each sample (non-time series)

# Construct one dataframe for each sample (multidimensional, non-time series)
df_1 = pd.DataFrame([[-9.81, 0.03, 0.21]], columns=["accX", "accY", "accZ"])
df_2 = pd.DataFrame([[-9.56, 5.34, 1.21]], columns=["accX", "accY", "accZ"])

# Optional metadata for all samples being uploaded
metadata = {
    "source": "accelerometer",
    "collection site": "desk",
}
# Upload the first sample
ids = []
response = ei.experimental.data.upload_pandas_sample(
    df_1,
    label="One",
    filename="001",
    metadata=metadata,
    category="training",
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)

# Upload the second sample
response = ei.experimental.data.upload_pandas_sample(
    df_2,
    label="Two",
    filename="002",
    metadata=metadata,
    category="training",
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

You can also upload one dataframe for each sample (time series). As with previous examples, we'll assume that the sample rate is 10 ms.

# Create samples (multidimensional, time series)
sample_data_1 = [ # Sample 1 ("idle")
    [-9.81, 0.03, 0.21],
    [-9.83, 0.04, 0.27],
    [-9.12, 0.03, 0.23],
    [-9.14, 0.01, 0.25],
]
sample_data_2 = [ # Sample 1 ("wave")
    [-9.56, 5.34, 1.21],
    [-9.43, 1.37, 1.27],
    [-9.22, -4.03, 1.23],
    [-9.50, -0.98, 1.25],
]
# Construct one dataframe for each sample
df_1 = pd.DataFrame(sample_data_1, columns=["accX", "accY", "accZ"])
df_2 = pd.DataFrame(sample_data_2, columns=["accX", "accY", "accZ"])

# Optional metadata for all samples being uploaded
metadata = {
    "source": "accelerometer",
    "collection site": "desk",
}
# Upload the first sample
ids = []
response = ei.experimental.data.upload_pandas_sample(
    df_1,
    label="Idle",
    filename="001",
    sample_rate_ms=10,
    metadata=metadata,
    category="training",
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)

# Upload the second sample
response = ei.experimental.data.upload_pandas_sample(
    df_2,
    label="Wave",
    filename="002",
    sample_rate_ms=10,
    metadata=metadata,
    category="training",
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

You can upload non-time series data where each sample is a row in the dataframe. Note that you need to provide labels in the rows.

# Construct non-time series data, where each row is a different sample
data = [
    ["desk", "training", "One", -9.81, 0.03, 0.21],
    ["field", "training", "Two", -9.56, 5.34, 1.21],
]
columns = ["loc", "category", "label", "accX", "accY", "accZ"]

# Wrap the data in a DataFrame
df = pd.DataFrame(data, columns=columns)
# Upload non-time series DataFrame (with multiple samples) to the project
ids = []
response = ei.experimental.data.upload_pandas_dataframe(
    df,
    feature_cols=["accX", "accY", "accZ"],
    label_col="label",
    category_col="category",
    metadata_cols=["loc"],
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

A "wide" dataframe is one where each column represents a value in the time series data, and the rows become individual samples. Note that you need to provide labels in the rows.

# Construct time series data, where each row is a different sample
data = [
    ["desk", "training", "idle", 0.8, 0.7, 0.8, 0.9, 0.8, 0.8, 0.7, 0.8],
    ["field", "training", "motion", 0.3, 0.9, 0.4, 0.6, 0.8, 0.9, 0.5, 0.4],
]
columns = ["loc", "category", "label", "0", "1", "2", "3", "4", "5", "6", "7"]

# Wrap the data in a DataFrame
df = pd.DataFrame(data, columns=columns)
# Upload time series DataFrame (with multiple samples) to the project
ids = []
response = ei.experimental.data.upload_pandas_dataframe_wide(
    df,
    label_col="label",
    category_col="category",
    metadata_cols=["loc"],
    data_col_start=3,
    sample_rate_ms=100,
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

A DataFrame can also be divided into "groups" so you can upload multidimensional time series data.

# Create samples
sample_data = [
    ["desk", "sample 1", "training", "idle", 0, -9.81, 0.03, 0.21],
    ["desk", "sample 1", "training", "idle", 0.01, -9.83, 0.04, 0.27],
    ["desk", "sample 1", "training", "idle", 0.02, -9.12, 0.03, 0.23],
    ["desk", "sample 1", "training", "idle", 0.03, -9.14, 0.01, 0.25],
    ["field", "sample 2", "training", "wave", 0, -9.56, 5.34, 1.21],
    ["field", "sample 2", "training", "wave", 0.01, -9.43, 1.37, 1.27],
    ["field", "sample 2", "training", "wave", 0.02, -9.22, -4.03, 1.23],
    ["field", "sample 2", "training", "wave", 0.03, -9.50, -0.98, 1.25],
]
columns = ["loc", "sample_name", "category", "label", "timestamp", "accX", "accY", "accZ"]

# Wrap the data in a DataFrame
df = pd.DataFrame(sample_data, columns=columns)
# Upload time series DataFrame (with multiple samples and multiple dimensions) to the project
ids = []
response = ei.experimental.data.upload_pandas_dataframe_with_group(
    df,
    group_by="sample_name",
    timestamp_col="timestamp",
    feature_cols=["accX", "accY", "accZ"],
    label_col="label",
    category_col="category",
    metadata_cols=["loc"]
)
assert len(response.fails) == 0, "Could not upload some files"
for sample in response.successes:
    ids.append(sample.sample_id)
# Delete the samples from the Edge Impulse project
for id in ids:
    ei.experimental.data.delete_sample_by_id(id)

Developer preview

This feature is a developer preview. Changes and improvements can still be made without prior notice and there are no guarantees that this feature will be fully released in future.

Count objects using FOMO

The Edge Impulse object detection model (FOMO) is effective at classifying objects and very lightweight (can run on MCUs). It does not however have any object persistence between frames. One common use of computer vision is for object counting- in order to achieve this you will need to add in some extra logic when deploying.

This notebook takes you through how to count objects using the linux deployment block (and provides some pointers for how to achieve similar logic other firmware deployment options).

Relevant links:

  • Raw python files for the linux deployment example: https://github.com/edgeimpulse/object-counting-demo

  • An end-to-end demo for on-device deployment of object counting: https://github.com/edgeimpulse/conveyor-counting-data-synthesis-demo

1. Download the linux deployment .eim for your project

To run your model locally, you need to deploy to a linux target in your project. First you need to enable all linux targets. Head to the deployment screen and click "Linux Boards" then in the following pop-up select "show all Linux deployment options on this page":

Enable linux deployment

Then download the linux/mac target which is relevant to your machine:

Download your relevant .eim

Finally, follow the instructions shown as a pop-up to make your .eim file executable (for example for MacOS):

  • Open a terminal window and navigate to the folder where you downloaded this model.

  • Mark the model as executable: chmod +x path-to-model.eim

  • Remove the quarantine flag: xattr -d com.apple.quarantine ./path-to-model.eim

2. Object Detection

Dependencies

Ensure you have these libraries installed before starting:

! pip install edge_impulse_linux
! pip install numpy
! pip install opencv-python

2.1 Run Object Counting on a video file

(see next heading for running on a webcam)

This program will run object detection on an input video file and count the objects going upwards which pass a threshold (TOP_Y). The sensitivity can be tuned with the number of columns (NUM_COLS) and the DETECT_FACTOR which is the factor of width/height of the object used to determine object permanence between frames.

Ensure you have added the relevant paths to your model file and video file:

  • modelfile = '/path/to/modelfile.eim'

  • videofile = '/path/to/video.mp4'

import cv2
import os
import time
import sys, getopt
import numpy as np
from edge_impulse_linux.image import ImageImpulseRunner


modelfile = '/path/to/modelfile.eim'
videofile = '/path/to/video.mp4'


runner = None
# if you don't want to see a video preview, set this to False
show_camera = True
if (sys.platform == 'linux' and not os.environ.get('DISPLAY')):
    show_camera = False
print('MODEL: ' + modelfile)

with ImageImpulseRunner(modelfile) as runner:
    try:
        model_info = runner.init()
        print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"')
        labels = model_info['model_parameters']['labels']
        count = 0
        vidcap = cv2.VideoCapture(videofile)
        sec = 0
        start_time = time.time()

        def getFrame(sec):
            vidcap.set(cv2.CAP_PROP_POS_MSEC,sec*1000)
            hasFrames,image = vidcap.read()
            if hasFrames:
                return image
            else:
                print('Failed to load frame', videofile)
                exit(1)


        img = getFrame(sec)
        
    
        # Define the top of the image and the number of columns
        TOP_Y = 30
        NUM_COLS = 5
        COL_WIDTH = int(vidcap.get(3) / NUM_COLS)
        # Define the factor of the width/height which determines the threshold
        # for detection of the object's movement between frames:
        DETECT_FACTOR = 1.5

        # Initialize variables
        count = [0] * NUM_COLS
        countsum = 0
        previous_blobs = [[] for _ in range(NUM_COLS)]
        
        while img.size != 0:
            # imread returns images in BGR format, so we need to convert to RGB
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

            # get_features_from_image also takes a crop direction arguments in case you don't have square images
            features, cropped = runner.get_features_from_image(img)
            img2 = cropped
            COL_WIDTH = int(np.shape(cropped)[0]/NUM_COLS)

            # the image will be resized and cropped, save a copy of the picture here
            # so you can see what's being passed into the classifier
            cv2.imwrite('debug.jpg', cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))

            res = runner.classify(features)
            # Initialize list of current blobs
            current_blobs = [[] for _ in range(NUM_COLS)]
            
            if "bounding_boxes" in res["result"].keys():
                print('Found %d bounding boxes (%d ms.)' % (len(res["result"]["bounding_boxes"]), res['timing']['dsp'] + res['timing']['classification']))
                for bb in res["result"]["bounding_boxes"]:
                    print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (bb['label'], bb['value'], bb['x'], bb['y'], bb['width'], bb['height']))
                    img2 = cv2.rectangle(cropped, (bb['x'], bb['y']), (bb['x'] + bb['width'], bb['y'] + bb['height']), (255, 0, 0), 1)

                        # Check which column the blob is in
                    col = int(bb['x'] / COL_WIDTH)

                    # Check if blob is within DETECT_FACTOR*h of a blob detected in the previous frame and treat as the same object
                    for blob in previous_blobs[col]:
                        if abs(bb['x'] - blob[0]) < DETECT_FACTOR * (bb['width'] + blob[2]) and abs(bb['y'] - blob[1]) < DETECT_FACTOR * (bb['height'] + blob[3]):
                        # Check this blob has "moved" across the Y threshold
                            if blob[1] >= TOP_Y and bb['y'] < TOP_Y:
                                # Increment count for this column if blob has left the top of the image
                                count[col] += 1
                                countsum += 1
                    # Add current blob to list
                    current_blobs[col].append((bb['x'], bb['y'], bb['width'], bb['height']))



            # Update previous blobs
            previous_blobs = current_blobs

            if (show_camera):
                im2 = cv2.resize(img2, dsize=(800,800))
                cv2.putText(im2, f'{countsum} items passed', (15,750), cv2.FONT_HERSHEY_COMPLEX, 1, (0,255,0), 2)
                cv2.imshow('edgeimpulse', cv2.cvtColor(im2, cv2.COLOR_RGB2BGR))
                print(f'{count}')
                if cv2.waitKey(1) == ord('q'):
                    break

            sec = time.time() - start_time
            sec = round(sec, 2)
            # print("Getting frame at: %.2f sec" % sec)
            img = getFrame(sec)
    finally:
        if (runner):
            print(f'{countsum} Items Left Conveyorbelt')
            runner.stop()

2.2 Run Object Counting on a webcam stream

This program will run object detection on a webcam port and count the objects going upwards which pass a threshold (TOP_Y). The sensitivity can be tuned with the number of columns (NUM_COLS) and the DETECT_FACTOR which is the factor of width/height of the object used to determine object permanence between frames.

Ensure you have added the relevant paths to your model file and video file:

  • modelfile = '/path/to/modelfile.eim'

  • [OPTIONAL] camera_port = '/camera_port'

import cv2
import os
import sys, getopt
import signal
import time
from edge_impulse_linux.image import ImageImpulseRunner

modelfile = '/path/to/modelfile.eim'
# If you have multiple webcams, replace None with the camera port you desire, get_webcams() can help find this
camera_port = None


runner = None
# if you don't want to see a camera preview, set this to False
show_camera = True
if (sys.platform == 'linux' and not os.environ.get('DISPLAY')):
    show_camera = False

def now():
    return round(time.time() * 1000)

def get_webcams():
    port_ids = []
    for port in range(5):
        print("Looking for a camera in port %s:" %port)
        camera = cv2.VideoCapture(port)
        if camera.isOpened():
            ret = camera.read()[0]
            if ret:
                backendName =camera.getBackendName()
                w = camera.get(3)
                h = camera.get(4)
                print("Camera %s (%s x %s) found in port %s " %(backendName,h,w, port))
                port_ids.append(port)
            camera.release()
    return port_ids

def sigint_handler(sig, frame):
    print('Interrupted')
    if (runner):
        runner.stop()
    sys.exit(0)

signal.signal(signal.SIGINT, sigint_handler)


print('MODEL: ' + modelfile)



with ImageImpulseRunner(modelfile) as runner:
    try:
        model_info = runner.init()
        print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"')
        labels = model_info['model_parameters']['labels']
        if camera_port:
            videoCaptureDeviceId = int(args[1])
        else:
            port_ids = get_webcams()
            if len(port_ids) == 0:
                raise Exception('Cannot find any webcams')
            if len(port_ids)> 1:
                raise Exception("Multiple cameras found. Add the camera port ID as a second argument to use to this script")
            videoCaptureDeviceId = int(port_ids[0])

        camera = cv2.VideoCapture(videoCaptureDeviceId)
        ret = camera.read()[0]
        if ret:
            backendName = camera.getBackendName()
            w = camera.get(3)
            h = camera.get(4)
            print("Camera %s (%s x %s) in port %s selected." %(backendName,h,w, videoCaptureDeviceId))
            camera.release()
        else:
            raise Exception("Couldn't initialize selected camera.")

        next_frame = 0 # limit to ~10 fps here
        
        # Define the top of the image and the number of columns
        TOP_Y = 100
        NUM_COLS = 5
        COL_WIDTH = int(w / NUM_COLS)
        # Define the factor of the width/height which determines the threshold
        # for detection of the object's movement between frames:
        DETECT_FACTOR = 1.5

        # Initialize variables
        count = [0] * NUM_COLS
        countsum = 0
        previous_blobs = [[] for _ in range(NUM_COLS)]

        

        for res, img in runner.classifier(videoCaptureDeviceId):
            # Initialize list of current blobs
            current_blobs = [[] for _ in range(NUM_COLS)]
            
            if (next_frame > now()):
                time.sleep((next_frame - now()) / 1000)

            if "bounding_boxes" in res["result"].keys():
                print('Found %d bounding boxes (%d ms.)' % (len(res["result"]["bounding_boxes"]), res['timing']['dsp'] + res['timing']['classification']))
                for bb in res["result"]["bounding_boxes"]:
                    print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (bb['label'], bb['value'], bb['x'], bb['y'], bb['width'], bb['height']))
                    img = cv2.rectangle(img, (bb['x'], bb['y']), (bb['x'] + bb['width'], bb['y'] + bb['height']), (255, 0, 0), 1)

                        # Check which column the blob is in
                    col = int(bb['x'] / COL_WIDTH)
                    # Check if blob is within DETECT_FACTOR*h of a blob detected in the previous frame and treat as the same object
                    for blob in previous_blobs[col]:
                        print(abs(bb['x'] - blob[0]) < DETECT_FACTOR * (bb['width'] + blob[2]))
                        print(abs(bb['y'] - blob[1]) < DETECT_FACTOR * (bb['height'] + blob[3]))
                        if abs(bb['x'] - blob[0]) < DETECT_FACTOR * (bb['width'] + blob[2]) and abs(bb['y'] - blob[1]) < DETECT_FACTOR * (bb['height'] + blob[3]):
                        # Check this blob has "moved" across the Y threshold
                            if blob[1] >= TOP_Y and bb['y'] < TOP_Y:
                                # Increment count for this column if blob has left the top of the image
                                count[col] += 1
                                countsum += 1
                    # Add current blob to list
                    current_blobs[col].append((bb['x'], bb['y'], bb['width'], bb['height']))
                
            # Update previous blobs
            previous_blobs = current_blobs

            if (show_camera):
                im2 = cv2.resize(img, dsize=(800,800))
                cv2.putText(im2, f'{countsum} items passed', (15,750), cv2.FONT_HERSHEY_COMPLEX, 1, (0,255,0), 2)
                cv2.imshow('edgeimpulse', cv2.cvtColor(im2, cv2.COLOR_RGB2BGR))
                print('Found %d bounding boxes (%d ms.)' % (len(res["result"]["bounding_boxes"]), res['timing']['dsp'] + res['timing']['classification']))

                if cv2.waitKey(1) == ord('q'):
                    break

            next_frame = now() + 100
    finally:
        if (runner):
            runner.stop()

3. Deploying to MCU firmware

While running object counting on linux hardware is fairly simple, it would be more useful to be able to deploy this to one of the firmware targets. This method varies per target but broadly speaking it is simple to add the object counting logic into existing firmware.

Here are the main steps:

1. Find and clone the Edge Impulse firmware repository for your target hardware

This can be found on our github pages e.g. https://github.com/edgeimpulse/firmware-arduino-nicla-vision

2. Deploy your model to a C++ library

You'll need to replace the "edge-impulse-sdk", "model-parameters" and "tflite-model" folders within the cloned firmware with the ones you've just downloaded for your model.

3. Find the object detection bounding boxes printout code in your firmware

This will be in a .h or similar file somewhere in the firmware. Likely in the ei_image_nn.h file. It can be found by searching for these lines:

#if EI_CLASSIFIER_OBJECT_DETECTION == 1
        bool bb_found = result.bounding_boxes[0].value > 0;

The following lines must be added into the logic in these files (For code itself see below, diff for clarity). Firstly these variables must be instantiated:

Then this logic must be inserted into the bounding box printing logic here:

Full code example for nicla vision (src/inference/ei_run_camera_impulse.cpp):

/* Edge Impulse ingestion SDK
 * Copyright (c) 2022 EdgeImpulse Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* Include ----------------------------------------------------------------- */
#include "model-parameters/model_metadata.h"
#include "ei_device_lib.h"

#if defined(EI_CLASSIFIER_SENSOR) && EI_CLASSIFIER_SENSOR == EI_CLASSIFIER_SENSOR_CAMERA

#include "edge-impulse-sdk/classifier/ei_run_classifier.h"
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "ei_camera.h"
#include "firmware-sdk/at_base64_lib.h"
#include "firmware-sdk/jpeg/encode_as_jpg.h"
#include "firmware-sdk/ei_device_interface.h"
#include "stdint.h"
#include "ei_device_nicla_vision.h"
#include "ei_run_impulse.h"

#include <ea_malloc.h>

#define DWORD_ALIGN_PTR(a)   ((a & 0x3) ?(((uintptr_t)a + 0x4) & ~(uintptr_t)0x3) : a)
#define ALIGN_PTR(p,a)   ((p & (a-1)) ?(((uintptr_t)p + a) & ~(uintptr_t)(a-1)) : p)

typedef enum {
    INFERENCE_STOPPED,
    INFERENCE_WAITING,
    INFERENCE_SAMPLING,
    INFERENCE_DATA_READY
} inference_state_t;

static inference_state_t state = INFERENCE_STOPPED;
static uint64_t last_inference_ts = 0;

static bool debug_mode = false;
static bool continuous_mode = false;

static uint8_t *snapshot_buf = nullptr;
static uint32_t snapshot_buf_size;

static ei_device_snapshot_resolutions_t snapshot_resolution;
static ei_device_snapshot_resolutions_t fb_resolution;

static bool resize_required = false;
static uint32_t inference_delay;

 // Define the top of the image and the number of columns
static int TOP_Y = 50;
static int NUM_COLS = 5;
static int COL_WIDTH = EI_CLASSIFIER_INPUT_WIDTH / NUM_COLS;
static int MAX_ITEMS = 10;

// Define the factor of the width/height which determines the threshold
// for detection of the object's movement between frames:
static float DETECT_FACTOR = 1.5;

// Initialize variables
std::vector<int> count(NUM_COLS, 0);
int countsum =0;
int notfoundframes = 0;
std::vector<std::vector<ei_impulse_result_bounding_box_t> > previous_blobs(NUM_COLS);

static int ei_camera_get_data(size_t offset, size_t length, float *out_ptr)
{
    // we already have a RGB888 buffer, so recalculate offset into pixel index
    size_t pixel_ix = offset * 3;
    size_t pixels_left = length;
    size_t out_ptr_ix = 0;

    while (pixels_left != 0) {
        out_ptr[out_ptr_ix] = (snapshot_buf[pixel_ix] << 16) + (snapshot_buf[pixel_ix + 1] << 8) + snapshot_buf[pixel_ix + 2];

        // go to the next pixel
        out_ptr_ix++;
        pixel_ix+=3;
        pixels_left--;
    }

    // and done!
    return 0;
}

void ei_run_impulse(void)
{
    switch(state) {
        case INFERENCE_STOPPED:
            // nothing to do
            return;
        case INFERENCE_WAITING:
            if(ei_read_timer_ms() < (last_inference_ts + inference_delay)) {
                return;
            }
            state = INFERENCE_DATA_READY;
            break;
        case INFERENCE_SAMPLING:
        case INFERENCE_DATA_READY:
            if(continuous_mode == true) {
                state = INFERENCE_WAITING;
            }
            break;
        default:
            break;
    }

    snapshot_buf = (uint8_t*)ea_malloc(snapshot_buf_size + 32);
    snapshot_buf = (uint8_t *)ALIGN_PTR((uintptr_t)snapshot_buf, 32);

    // check if allocation was successful
    if(snapshot_buf == nullptr) {
        ei_printf("ERR: Failed to allocate snapshot buffer!\n");
        return;
    }

    EiCameraNiclaVision *camera = static_cast<EiCameraNiclaVision*>(EiCameraNiclaVision::get_camera());

    ei_printf("Taking photo...\n");

    bool isOK = camera->ei_camera_capture_rgb888_packed_big_endian(snapshot_buf, snapshot_buf_size);
    if (!isOK) {
        return;
    }

    if (resize_required) {
        ei::image::processing::crop_and_interpolate_rgb888(
            snapshot_buf,
            fb_resolution.width,
            fb_resolution.height,
            snapshot_buf,
            snapshot_resolution.width,
            snapshot_resolution.height);
    }

    ei::signal_t signal;
    signal.total_length = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT;
    signal.get_data = &ei_camera_get_data;

    // Print framebuffer as JPG during debugging
    if(debug_mode) {
        ei_printf("Begin output\n");

        size_t jpeg_buffer_size = EI_CLASSIFIER_INPUT_WIDTH * EI_CLASSIFIER_INPUT_HEIGHT >= 128 * 128 ?
            8192 * 3 :
            4096 * 4;
        uint8_t *jpeg_buffer = NULL;
        jpeg_buffer = (uint8_t*)ei_malloc(jpeg_buffer_size);
        if (!jpeg_buffer) {
            ei_printf("ERR: Failed to allocate JPG buffer\r\n");
            return;
        }

        size_t out_size;
        int x = encode_rgb888_signal_as_jpg(&signal, EI_CLASSIFIER_INPUT_WIDTH, EI_CLASSIFIER_INPUT_HEIGHT, jpeg_buffer, jpeg_buffer_size, &out_size);
        if (x != 0) {
            ei_printf("Failed to encode frame as JPEG (%d)\n", x);
            return;
        }

        ei_printf("Framebuffer: ");
        base64_encode((char*)jpeg_buffer, out_size, ei_putc);
        ei_printf("\r\n");

        if (jpeg_buffer) {
            ei_free(jpeg_buffer);
        }
    }

    // run the impulse: DSP, neural network and the Anomaly algorithm
    ei_impulse_result_t result = { 0 };

    EI_IMPULSE_ERROR ei_error = run_classifier(&signal, &result, false);
    if (ei_error != EI_IMPULSE_OK) {
        ei_printf("ERR: Failed to run impulse (%d)\n", ei_error);
        ea_free(snapshot_buf);
        return;
    }
    ea_free(snapshot_buf);

    // print the predictions
    ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms., Count: %d ): \n",
                result.timing.dsp, result.timing.classification,result.timing.anomaly, countsum);
    
#if EI_CLASSIFIER_OBJECT_DETECTION == 1
    bool bb_found = result.bounding_boxes[0].value > 0;
    std::vector<std::vector<ei_impulse_result_bounding_box_t> > current_blobs(NUM_COLS);
    for (size_t ix = 0; ix < result.bounding_boxes_count; ix++) {
        auto bb = result.bounding_boxes[ix];
        if (bb.value == 0) {
            continue;
        }
        // Check which column the blob is in
        int col = int(bb.x / COL_WIDTH);
        // Check if blob is within DETECT_FACTOR*h of a blob detected in the previous frame and treat as the same object
        for (auto blob : previous_blobs[col]) {
            if (abs(int(bb.x - blob.x)) < DETECT_FACTOR * (bb.width + blob.width) && abs(int(bb.y - blob.y)) < DETECT_FACTOR * (bb.height + blob.height)) {
                // Check this blob has "moved" across the Y threshold
                if (blob.y >= TOP_Y && bb.y < TOP_Y) {
                    // Increment count for this column if blob has left the top of the image
                    count[col]++;
                    countsum++;
                }
            }
        }
        // Add current blob to list
        current_blobs[col].push_back(bb);
        ei_printf("    %s (%f) [ x: %u, y: %u, width: %u, height: %u ]\n", bb.label, bb.value, bb.x, bb.y, bb.width, bb.height);
    }
    previous_blobs = std::move(current_blobs);
    if (bb_found) { 
        ei_printf("    Count: %d\n",countsum);
        notfoundframes = 0;
    }
    else {
        notfoundframes ++;
        if (notfoundframes == 1){
            ei_printf("    No objects found\n");
        }
        else {
            ei_printf("    Count: %d\n",countsum);
        }
    }
    
#else
    for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        ei_printf("    %s: %.5f\n", result.classification[ix].label,
                                    result.classification[ix].value);
        
    }

#if EI_CLASSIFIER_HAS_ANOMALY == 1
        ei_printf("    anomaly score: %.3f\n", result.anomaly);
#endif
#endif

    if (debug_mode) {
        ei_printf("\r\n----------------------------------\r\n");
        ei_printf("End output\r\n");
    }

    if(continuous_mode == false) {
        ei_printf("Starting inferencing in %d seconds...\n", inference_delay / 1000);
        last_inference_ts = ei_read_timer_ms();
        state = INFERENCE_WAITING;
    }
}

void ei_start_impulse(bool continuous, bool debug, bool use_max_uart_speed)
{
    snapshot_resolution.width = EI_CLASSIFIER_INPUT_WIDTH;
    snapshot_resolution.height = EI_CLASSIFIER_INPUT_HEIGHT;

    debug_mode = debug;
    continuous_mode = (debug) ? true : continuous;

    EiDeviceNiclaVision* dev = static_cast<EiDeviceNiclaVision*>(EiDeviceNiclaVision::get_device());
    EiCameraNiclaVision *camera = static_cast<EiCameraNiclaVision*>(EiCameraNiclaVision::get_camera());

    // check if minimum suitable sensor resolution is the same as 
    // desired snapshot resolution
    // if not we need to resize later
    fb_resolution = camera->search_resolution(snapshot_resolution.width, snapshot_resolution.height);

    if (snapshot_resolution.width != fb_resolution.width || snapshot_resolution.height != fb_resolution.height) {
        resize_required = true;
    }

    if (!camera->init(snapshot_resolution.width, snapshot_resolution.height)) {
        ei_printf("Failed to init camera, check if camera is connected!\n");
        return;
    }

    snapshot_buf_size = fb_resolution.width * fb_resolution.height * 3;

    // summary of inferencing settings (from model_metadata.h)
    ei_printf("Inferencing settings:\n");
    ei_printf("\tImage resolution: %dx%d\n", EI_CLASSIFIER_INPUT_WIDTH, EI_CLASSIFIER_INPUT_HEIGHT);
    ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
    ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));

    if(continuous_mode == true) {
        inference_delay = 0;
        state = INFERENCE_DATA_READY;
    }
    else {
        inference_delay = 2000;
        last_inference_ts = ei_read_timer_ms();
        state = INFERENCE_WAITING;
        ei_printf("Starting inferencing in %d seconds...\n", inference_delay / 1000);
    }

    if (debug_mode) {
        ei_printf("OK\r\n");
        ei_sleep(100);
        dev->set_max_data_output_baudrate();
        ei_sleep(100);
    }

    while(!ei_user_invoke_stop_lib()) {
        ei_run_impulse();
        ei_sleep(1);
    }

    ei_stop_impulse();

    if (debug_mode) {
        ei_printf("\r\nOK\r\n");
        ei_sleep(100);
        dev->set_default_data_output_baudrate();
        ei_sleep(100);
    }

}

void ei_stop_impulse(void)
{
    state = INFERENCE_STOPPED;
}

bool is_inference_running(void)
{
    return (state != INFERENCE_STOPPED);
}

#endif /* defined(EI_CLASSIFIER_SENSOR) && EI_CLASSIFIER_SENSOR == EI_CLASSIFIER_SENSOR_CAMERA */

// AT+RUNIMPULSE

4. Build your firmware locally and flash to your device

Follow the instructions in the README.md file for the firmware repo you have been working in.

5. Run your impulse on device

Use the command below to see on-device inference (follow the local link to see bounding boxes and count output in the browser)

edge-impulse-run-impulse --debug