---
bibliography: image_classification.bib
---

# CV on Nicla Vision {.unnumbered}

![*DALL·E 3 Prompt: Cartoon in a 1950s style featuring a compact electronic device with a camera module placed on a wooden table. The screen displays blue robots on one side and green periquitos on the other. LED lights on the device indicate classifications, while characters in retro clothing observe with interest.*](images/jpg/img_class_ini.jpg){fig-align="center" width="6.5in"}

## Introduction

As we initiate our studies into embedded machine learning or TinyML, it's impossible to overlook the transformative impact of Computer Vision (CV) and Artificial Intelligence (AI) in our lives. These two intertwined disciplines redefine what machines can perceive and accomplish, from autonomous vehicles and robotics to healthcare and surveillance.

More and more, we are facing an artificial intelligence (AI) revolution where, as stated by Gartner, **Edge AI** has a very high impact potential, and **it is for now**!

![](images/jpg/image2.jpg){fig-align="center" width="4.729166666666667in"}

In the "bullseye" of the Radar is the *Edge Computer Vision*, and when we talk about Machine Learning (ML) applied to vision, the first thing that comes to mind is **Image Classification**, a kind of ML "Hello World"!

This exercise will explore a computer vision project utilizing Convolutional Neural Networks (CNNs) for real-time image classification. Leveraging TensorFlow's robust ecosystem, we'll implement a pre-trained MobileNet model and adapt it for edge deployment. The focus will be on optimizing the model to run efficiently on resource-constrained hardware without sacrificing accuracy.

We'll employ techniques like quantization and pruning to reduce the computational load. By the end of this tutorial, you'll have a working prototype capable of classifying images in real-time, all running on a low-power embedded system based on the Arduino Nicla Vision board.

## Computer Vision

At its core, computer vision aims to enable machines to interpret and make decisions based on visual data from the world, essentially mimicking the capability of the human optical system. Conversely, AI is a broader field encompassing machine learning, natural language processing, and robotics, among other technologies. When you bring AI algorithms into computer vision projects, you supercharge the system's ability to understand, interpret, and react to visual stimuli.

When discussing Computer Vision projects applied to embedded devices, the most common applications that come to mind are *Image Classification* and *Object Detection*.

![](images/jpg/image15.jpg){fig-align="center" width="6.5in"}

Both models can be implemented on tiny devices like the Arduino Nicla Vision and used on real projects. In this chapter, we will cover Image Classification.

## Image Classification Project Goal

The first step in any ML project is to define the goal. In this case, it is to detect and classify two specific objects present in one image. For this project, we will use two small toys: a *robot* and a small Brazilian parrot (named *Periquito*). Also, we will collect images of a *background* where those two objects are absent.

![](images/jpg/image36.jpg){fig-align="center" width="6.5in"}

## Data Collection

Once you have defined your Machine Learning project goal, the next and most crucial step is the dataset collection. You can use the Edge Impulse Studio, the OpenMV IDE we installed, or even your phone for the image capture. Here, we will use the OpenMV IDE for that.

### Collecting Dataset with OpenMV IDE

First, create in your computer a folder where your data will be saved, for example, "data." Next, on the OpenMV IDE, go to `Tools > Dataset Editor` and select `New Dataset` to start the dataset collection:

![](images/png/image29.png){fig-align="center" width="6.291666666666667in"}

The IDE will ask you to open the file where your data will be saved and choose the "data" folder that was created. Note that new icons will appear on the Left panel.

![](images/png/image46.png){fig-align="center" width="0.9583333333333334in"}

Using the upper icon (1), enter with the first class name, for example, "periquito":

![](images/png/image22.png){fig-align="center" width="3.25in"}

Running the `dataset_capture_script.py` and clicking on the camera icon (2), will start capturing images:

![](images/png/image43.png){fig-align="center" width="6.5in"}

Repeat the same procedure with the other classes

![](images/jpg/image6.jpg){fig-align="center" width="6.5in"}

> We suggest around 60 images from each category. Try to capture different angles, backgrounds, and light conditions.

The stored images use a QVGA frame size of 320x240 and the RGB565 (color pixel format).

After capturing your dataset, close the Dataset Editor Tool on the `Tools > Dataset Editor`.

On your computer, you will end with a dataset that contains three classes: *periquito,* *robot*, and *background*.

![](images/png/image20.png){fig-align="center" width="6.5in"}

You should return to *Edge Impulse Studio* and upload the dataset to your project.

## Training the model with Edge Impulse Studio

We will use the Edge Impulse Studio for training our model. Enter your account credentials and create a new project:

![](images/png/image45.png){fig-align="center" width="6.5in"}

> Here, you can clone a similar project: [NICLA-Vision_Image_Classification](https://studio.edgeimpulse.com/public/273858/latest).

## Dataset

Using the EI Studio (or *Studio*), we will go over four main steps to have our model ready for use on the Nicla Vision board: Dataset, Impulse, Tests, and Deploy (on the Edge Device, in this case, the NiclaV).

![](images/jpg/image41.jpg){fig-align="center" width="6.5in"}

Regarding the Dataset, it is essential to point out that our Original Dataset, captured with the OpenMV IDE, will be split into *Training*, *Validation*, and *Test*. The Test Set will be divided from the beginning, and a part will reserved to be used only in the Test phase after training. The Validation Set will be used during training.

![](images/jpg/image7.jpg){fig-align="center" width="6.5in"}

On Studio, go to the Data acquisition tab, and on the UPLOAD DATA section, upload the chosen categories files from your computer:

![](images/png/image39.png){fig-align="center" width="6.5in"}

Leave to the Studio the splitting of the original dataset into *train and test* and choose the label about that specific data:

![](images/png/image30.png){fig-align="center" width="6.5in"}

Repeat the procedure for all three classes. At the end, you should see your "raw data" in the Studio:

![](images/png/image11.png){fig-align="center" width="6.5in"}

The Studio allows you to explore your data, showing a complete view of all the data in your project. You can clear, inspect, or change labels by clicking on individual data items. In our case, a very simple project, the data seems OK.

![](images/png/image44.png){fig-align="center" width="6.5in"}

## The Impulse Design

In this phase, we should define how to:

-   Pre-process our data, which consists of resizing the individual images and determining the `color depth` to use (be it RGB or Grayscale) and

-   Specify a Model, in this case, it will be the `Transfer Learning (Images)` to fine-tune a pre-trained MobileNet V2 image classification model on our data. This method performs well even with relatively small image datasets (around 150 images in our case).

![](images/jpg/image23.jpg){fig-align="center" width="6.5in"}

Transfer Learning with MobileNet offers a streamlined approach to model training, which is especially beneficial for resource-constrained environments and projects with limited labeled data. MobileNet, known for its lightweight architecture, is a pre-trained model that has already learned valuable features from a large dataset (ImageNet).

![](images/jpg/image9.jpg){fig-align="center" width="6.5in"}

By leveraging these learned features, you can train a new model for your specific task with fewer data and computational resources and yet achieve competitive accuracy.

![](images/jpg/image32.jpg){fig-align="center" width="6.5in"}

This approach significantly reduces training time and computational cost, making it ideal for quick prototyping and deployment on embedded devices where efficiency is paramount.

Go to the Impulse Design Tab and create the *impulse*, defining an image size of 96x96 and squashing them (squared form, without cropping). Select Image and Transfer Learning blocks. Save the Impulse.

![](images/png/image16.png){fig-align="center" width="6.5in"}

### Image Pre-Processing

All the input QVGA/RGB565 images will be converted to 27,640 features (96x96x3).

![](images/png/image17.png){fig-align="center" width="6.5in"}

Press \[Save parameters\] and Generate all features:

![](images/png/image5.png){fig-align="center" width="6.5in"}

### Model Design

In 2007, Google introduced [[MobileNetV1]{.underline}](https://research.googleblog.com/2017/06/mobilenets-open-source-models-for.html), a family of general-purpose computer vision neural networks designed with mobile devices in mind to support classification, detection, and more. MobileNets are small, low-latency, low-power models parameterized to meet the resource constraints of various use cases. in 2018, Google launched [MobileNetV2: Inverted Residuals and Linear Bottlenecks](https://arxiv.org/abs/1801.04381).

MobileNet V1 and MobileNet V2 aim at mobile efficiency and embedded vision applications but differ in architectural complexity and performance. While both use depthwise separable convolutions to reduce the computational cost, MobileNet V2 introduces Inverted Residual Blocks and Linear Bottlenecks to enhance performance. These new features allow V2 to capture more complex features using fewer parameters, making it computationally more efficient and generally more accurate than its predecessor. Additionally, V2 employs a non-linear activation in the intermediate expansion layer. It still uses a linear activation for the bottleneck layer, a design choice found to preserve important information through the network. MobileNet V2 offers an optimized architecture for higher accuracy and efficiency and will be used in this project.

Although the base MobileNet architecture is already tiny and has low latency, many times, a specific use case or application may require the model to be even smaller and faster. MobileNets introduces a straightforward parameter α (alpha) called width multiplier to construct these smaller, less computationally expensive models. The role of the width multiplier α is that of thinning a network uniformly at each layer.

Edge Impulse Studio can use both MobileNetV1 (96x96 images) and V2 (96x96 or 160x160 images), with several different **α** values (from 0.05 to 1.0). For example, you will get the highest accuracy with V2, 160x160 images, and α=1.0. Of course, there is a trade-off. The higher the accuracy, the more memory (around 1.3MB RAM and 2.6MB ROM) will be needed to run the model, implying more latency. The smaller footprint will be obtained at the other extreme with MobileNetV1 and α=0.10 (around 53.2K RAM and 101K ROM).

![](images/jpg/image27.jpg){fig-align="center" width="6.5in"}

We will use **MobileNetV2 96x96 0.1** for this project, with an estimated memory cost of 265.3 KB in RAM. This model should be OK for the Nicla Vision with 1MB of SRAM. On the Transfer Learning Tab, select this model:

![](images/png/image24.png){fig-align="center" width="6.5in"}

## Model Training

Another valuable technique to be used with Deep Learning is **Data Augmentation**. Data augmentation is a method to improve the accuracy of machine learning models by creating additional artificial data. A data augmentation system makes small, random changes to your training data during the training process (such as flipping, cropping, or rotating the images).

Looking under the hood, here you can see how Edge Impulse implements a data Augmentation policy on your data:

``` python
# Implements the data augmentation policy
def augment_image(image, label):
    # Flips the image randomly
    image = tf.image.random_flip_left_right(image)

    # Increase the image size, then randomly crop it down to
    # the original dimensions
    resize_factor = random.uniform(1, 1.2)
    new_height = math.floor(resize_factor * INPUT_SHAPE[0])
    new_width = math.floor(resize_factor * INPUT_SHAPE[1])
    image = tf.image.resize_with_crop_or_pad(image, new_height, new_width)
    image = tf.image.random_crop(image, size=INPUT_SHAPE)

    # Vary the brightness of the image
    image = tf.image.random_brightness(image, max_delta=0.2)

    return image, label
```

Exposure to these variations during training can help prevent your model from taking shortcuts by "memorizing" superficial clues in your training data, meaning it may better reflect the deep underlying patterns in your dataset.

The final layer of our model will have 12 neurons with a 15% dropout for overfitting prevention. Here is the Training result:

![](images/jpg/image31.jpg){fig-align="center" width="6.5in"}

The result is excellent, with 77ms of latency, which should result in 13fps (frames per second) during inference.

## Model Testing

![](images/jpg/image10.jpg){fig-align="center" width="6.5in"}

Now, you should take the data set aside at the start of the project and run the trained model using it as input:

![](images/png/image34.png){fig-align="center" width="3.1041666666666665in"}

The result is, again, excellent.

![](images/png/image12.png){fig-align="center" width="6.5in"}

## Deploying the model

At this point, we can deploy the trained model as.tflite and use the OpenMV IDE to run it using MicroPython, or we can deploy it as a C/C++ or an Arduino library.

![](images/jpg/image28.jpg){fig-align="center" width="6.5in"}

### Arduino Library

First, Let's deploy it as an Arduino Library:

![](images/png/image48.png){fig-align="center" width="6.5in"}

You should install the library as.zip on the Arduino IDE and run the sketch *nicla_vision_camera.ino* available in Examples under your library name.

> Note that Arduino Nicla Vision has, by default, 512KB of RAM allocated for the M7 core and an additional 244KB on the M4 address space. In the code, this allocation was changed to 288 kB to guarantee that the model will run on the device (`malloc_addblock((void*)0x30000000, 288 * 1024);`).

The result is good, with 86ms of measured latency.

![](images/jpg/image25.jpg){fig-align="center" width="6.5in"}

Here is a short video showing the inference results: {{< video https://youtu.be/bZPZZJblU-o width="480" height="270" center >}}

### OpenMV

It is possible to deploy the trained model to be used with OpenMV in two ways: as a library and as a firmware.

Three files are generated as a library: the trained.tflite model, a list with labels, and a simple MicroPython script that can make inferences using the model.

![](images/png/image26.png){fig-align="center" width="6.5in"}

Running this model as a *.tflite* directly in the Nicla was impossible. So, we can sacrifice the accuracy using a smaller model or deploy the model as an OpenMV Firmware (FW). Choosing FW, the Edge Impulse Studio generates optimized models, libraries, and frameworks needed to make the inference. Let's explore this option.

Select `OpenMV Firmware` on the `Deploy Tab` and press `[Build]`.

![](images/png/image3.png){fig-align="center" width="6.5in"}

On your computer, you will find a ZIP file. Open it:

![](images/png/image33.png){fig-align="center" width="6.5in"}

Use the Bootloader tool on the OpenMV IDE to load the FW on your board:

![](images/jpg/image35.jpg){fig-align="center" width="6.5in"}

Select the appropriate file (.bin for Nicla-Vision):

![](images/png/image8.png){fig-align="center" width="6.5in"}

After the download is finished, press OK:

![](images/png/image40.png){fig-align="center" width="3.875in"}

If a message says that the FW is outdated, DO NOT UPGRADE. Select \[NO\].

![](images/png/image42.png){fig-align="center" width="4.572916666666667in"}

Now, open the script **ei_image_classification.py** that was downloaded from the Studio and the.bin file for the Nicla.

![](images/png/image14.png){fig-align="center" width="6.5in"}

Run it. Pointing the camera to the objects we want to classify, the inference result will be displayed on the Serial Terminal.

![](images/png/image37.png){fig-align="center" width="6.5in"}

#### Changing the Code to add labels

The code provided by Edge Impulse can be modified so that we can see, for test reasons, the inference result directly on the image displayed on the OpenMV IDE.

[[Upload the code from GitHub,]{.underline}](https://github.com/Mjrovai/Arduino_Nicla_Vision/blob/main/Micropython/nicla_image_classification.py) or modify it as below:

``` python
# Marcelo Rovai - NICLA Vision - Image Classification
# Adapted from Edge Impulse - OpenMV Image Classification Example
# @24Aug23

import sensor, image, time, os, tf, uos, gc

sensor.reset()                         # Reset and initialize the sensor.
sensor.set_pixformat(sensor.RGB565)    # Set pxl fmt to RGB565 (or GRAYSCALE)
sensor.set_framesize(sensor.QVGA)      # Set frame size to QVGA (320x240)
sensor.set_windowing((240, 240))       # Set 240x240 window.
sensor.skip_frames(time=2000)          # Let the camera adjust.

net = None
labels = None

try:
    # Load built in model
    labels, net = tf.load_builtin_model('trained')
except Exception as e:
    raise Exception(e)

clock = time.clock()
while(True):
    clock.tick()  # Starts tracking elapsed time.

    img = sensor.snapshot()

    # default settings just do one detection
    for obj in net.classify(img, 
                            min_scale=1.0, 
                            scale_mul=0.8, 
                            x_overlap=0.5, 
                            y_overlap=0.5):
        fps = clock.fps()
        lat = clock.avg()

        print("**********\nPrediction:")
        img.draw_rectangle(obj.rect())
        # This combines the labels and confidence values into a list of tuples
        predictions_list = list(zip(labels, obj.output()))

        max_val = predictions_list[0][1]
        max_lbl = 'background'
        for i in range(len(predictions_list)):
            val = predictions_list[i][1]
            lbl = predictions_list[i][0]

            if val > max_val:
                max_val = val
                max_lbl = lbl

    # Print label with the highest probability
    if max_val < 0.5:
        max_lbl = 'uncertain'
    print("{} with a prob of {:.2f}".format(max_lbl, max_val))
    print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))

    # Draw label with highest probability to image viewer
    img.draw_string(
        10, 10,
        max_lbl + "\n{:.2f}".format(max_val),
        mono_space = False,
        scale=2
        )
```

Here you can see the result:

![](images/jpg/image47.jpg){fig-align="center" width="6.5in"}

Note that the latency (136 ms) is almost double of what we got directly with the Arduino IDE. This is because we are using the IDE as an interface and also the time to wait for the camera to be ready. If we start the clock just before the inference:

![](images/jpg/image13.jpg){fig-align="center" width="6.5in"}

The latency will drop to only 71 ms.

![](images/jpg/image1.jpg){fig-align="center" width="3.5520833333333335in"}

> The NiclaV runs about half as fast when connected to the IDE. The FPS should increase once disconnected.

#### Post-Processing with LEDs

When working with embedded machine learning, we are looking for devices that can continually proceed with the inference and result, taking some action directly on the physical world and not displaying the result on a connected computer. To simulate this, we will light up a different LED for each possible inference result.

![](images/jpg/image38.jpg){fig-align="center" width="6.5in"}

To accomplish that, we should [[upload the code from GitHub]{.underline}](https://github.com/Mjrovai/Arduino_Nicla_Vision/blob/main/Micropython/nicla_image_classification_LED.py) or change the last code to include the LEDs:

``` python
# Marcelo Rovai - NICLA Vision - Image Classification with LEDs
# Adapted from Edge Impulse - OpenMV Image Classification Example
# @24Aug23

import sensor, image, time, os, tf, uos, gc, pyb

ledRed = pyb.LED(1)
ledGre = pyb.LED(2)
ledBlu = pyb.LED(3)

sensor.reset()                         # Reset and initialize the sensor.
sensor.set_pixformat(sensor.RGB565)    # Set pixl fmt to RGB565 (or GRAYSCALE)
sensor.set_framesize(sensor.QVGA)      # Set frame size to QVGA (320x240)
sensor.set_windowing((240, 240))       # Set 240x240 window.
sensor.skip_frames(time=2000)          # Let the camera adjust.

net = None
labels = None

ledRed.off()
ledGre.off()
ledBlu.off()

try:
    # Load built in model
    labels, net = tf.load_builtin_model('trained')
except Exception as e:
    raise Exception(e)

clock = time.clock()


def setLEDs(max_lbl):

    if max_lbl == 'uncertain':
        ledRed.on()
        ledGre.off()
        ledBlu.off()

    if max_lbl == 'periquito':
        ledRed.off()
        ledGre.on()
        ledBlu.off()

    if max_lbl == 'robot':
        ledRed.off()
        ledGre.off()
        ledBlu.on()

    if max_lbl == 'background':
        ledRed.off()
        ledGre.off()
        ledBlu.off()


while(True):
    img = sensor.snapshot()
    clock.tick()  # Starts tracking elapsed time.

    # default settings just do one detection.
    for obj in net.classify(img, 
                            min_scale=1.0, 
                            scale_mul=0.8, 
                            x_overlap=0.5, 
                            y_overlap=0.5):
        fps = clock.fps()
        lat = clock.avg()

        print("**********\nPrediction:")
        img.draw_rectangle(obj.rect())
        # This combines the labels and confidence values into a list of tuples
        predictions_list = list(zip(labels, obj.output()))

        max_val = predictions_list[0][1]
        max_lbl = 'background'
        for i in range(len(predictions_list)):
            val = predictions_list[i][1]
            lbl = predictions_list[i][0]

            if val > max_val:
                max_val = val
                max_lbl = lbl

    # Print label and turn on LED with the highest probability
    if max_val < 0.8:
        max_lbl = 'uncertain'

    setLEDs(max_lbl)

    print("{} with a prob of {:.2f}".format(max_lbl, max_val))
    print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))

    # Draw label with highest probability to image viewer
    img.draw_string(
        10, 10,
        max_lbl + "\n{:.2f}".format(max_val),
        mono_space = False,
        scale=2
        )
```

Now, each time that a class scores a result greater than 0.8, the correspondent LED will be lit:

-   Led Red 0n: Uncertain (no class is over 0.8)

-   Led Green 0n: Periquito \> 0.8

-   Led Blue 0n: Robot \> 0.8

-   All LEDs Off: Background \> 0.8

Here is the result:

![](images/jpg/image18.jpg){fig-align="center" width="6.5in"}

In more detail

![](images/jpg/image21.jpg){fig-align="center" width="6.5in"}

## Image Classification (non-official) Benchmark

Several development boards can be used for embedded machine learning (TinyML), and the most common ones for Computer Vision applications (consuming low energy), are the ESP32 CAM, the Seeed XIAO ESP32S3 Sense, the Arduino Nicla Vison, and the Arduino Portenta.

![](images/jpg/image19.jpg){fig-align="center" width="6.5in"}

Catching the opportunity, the same trained model was deployed on the ESP-CAM, the XIAO, and the Portenta (in this one, the model was trained again, using grayscaled images to be compatible with its camera). Here is the result, deploying the models as Arduino's Library:

![](images/jpg/image4.jpg){fig-align="center" width="6.5in"}

## Conclusion

Before we finish, consider that Computer Vision is more than just image classification. For example, you can develop Edge Machine Learning projects around vision in several areas, such as:

-   **Autonomous Vehicles**: Use sensor fusion, lidar data, and computer vision algorithms to navigate and make decisions.

-   **Healthcare**: Automated diagnosis of diseases through MRI, X-ray, and CT scan image analysis

-   **Retail**: Automated checkout systems that identify products as they pass through a scanner.

-   **Security and Surveillance**: Facial recognition, anomaly detection, and object tracking in real-time video feeds.

-   **Augmented Reality**: Object detection and classification to overlay digital information in the real world.

-   **Industrial Automation**: Visual inspection of products, predictive maintenance, and robot and drone guidance.

-   **Agriculture**: Drone-based crop monitoring and automated harvesting.

-   **Natural Language Processing**: Image captioning and visual question answering.

-   **Gesture Recognition**: For gaming, sign language translation, and human-machine interaction.

-   **Content Recommendation**: Image-based recommendation systems in e-commerce.
