
In the previous tutorial, we set up the Arduino UNO Q for headless development using ADB and SSH. Now, we put that foundation to work with our first Edge AI application: Image Classification.
Image classification is the “Hello World” of computer vision and machine learning — given an image, the model assigns it to one of several predefined categories. It is one of the most widely deployed ML tasks at the edge, powering applications from quality inspection in factories to wildlife monitoring in the field.
In this tutorial, we will:
Along the way, we will use the UNO Q’s dual-brain architecture: Python on the MPU for AI inference and an Arduino sketch on the MCU for physical actuation (LEDs, LED matrix) — connected through Bridge RPC.

Before starting this tutorial, make sure you have:
- Completed the Arduino UNO Q Setup Tutorial — UNO Q connected via SSH
- An Edge Impulse account (free at edgeimpulse.com)
- A USB webcam (most USB webcams work — e.g., Logitech C270 or similar)
- A USB hub with power delivery (to connect both the webcam and power to the UNO Q)

| Item | Purpose |
|---|---|
| Arduino UNO Q (2 GB or 4 GB) | Edge AI inference + MCU actuation |
| USB webcam | Image capture (for Part 2 — live inference) |
| USB hub with PD | Connect webcam + power to the single USB-C port |
| (Optional) Toy robot + Periquito | Classification targets for the custom model |
| Tool | Where |
|---|---|
| Edge Impulse Studio | Browser: studio.edgeimpulse.com |
| Edge Impulse Linux CLI + Python SDK | Installed on the UNO Q (in Part 2) |
An editor is not required — every step here works from an SSH terminal. If you’d rather use a full IDE, see VS Code Remote-SSH setup.
At its core, computer vision enables machines to interpret and make decisions based on visual data — essentially mimicking the capability of the human optical system. When we bring ML algorithms into computer vision projects, we supercharge the system’s ability to understand, interpret, and react to visual stimuli.

When discussing computer vision applied to embedded devices, the most common applications are Image Classification and Object Detection:
In this chapter, we cover Image Classification. Object Detection will be covered in the next tutorial.
If you have worked through the TinyML Made Easy e-book, you have already built an image classifier on the Arduino Nicla Vision. Here is how the UNO Q approach differs:
| Aspect | Nicla Vision | Arduino UNO Q |
|---|---|---|
| Camera | Built-in GC2145 (320x240) | External USB webcam (up to 1080p) |
| Data Collection | OpenMV IDE + built-in camera | Edge Impulse Studio (smartphone, webcam, or upload) |
| Model size | MobileNetV2 0.05/0.1 (96x96, INT8) | MobileNetV2 0.35 or larger (96x96 to 320x320) |
| Inference engine | TF Lite for Microcontrollers (C++) | Edge Impulse Linux SDK or App Lab Bricks (Python) |
| RAM for model | ~256 KB SRAM | 2-4 GB LPDDR4X |
| Actuation | Direct GPIO from MCU | MCU via Bridge RPC |
| IDE | OpenMV IDE / Arduino IDE | SSH terminal + arduino-app-cli (VS Code Remote-SSH optional) |
The UNO Q can handle larger, more accurate models and higher-resolution input because inference runs on the Linux MPU with gigabytes of RAM. But the Nicla wins on power consumption (milliwatts vs. watts) and latency for very small models.
The UNO Q ships with a pre-installed image classification example. This example uses an ImageClassification Brick and a WebUI Brick to provide a browser-based interface for uploading an image and receiving classification results.
No webcam is needed for this step.
The pre-installed examples:image-classification app works as follows:
Browser (your PC) UNO Q (MPU / Python)
+-------------------+ +-------------------------+
| | Upload image | |
| Web UI (port | ------------------> | ImageClassification |
| 7000) | (base64 via | Brick |
| | WebSocket) | |
| Shows results | <------------------ | Returns labels + |
| (labels + | Classification | confidence scores |
| confidence) | result | |
+-------------------+ +-------------------------+
Key points:
SSH into the UNO Q (or use the VS Code integrated terminal) and run:
arduino-app-cli app start examples:image-classification
Wait for the app to build and start.

On your host computer, open a web browser and navigate to:
http://<UNO_Q_IP_ADDRESS>:7000
You should see a web interface with an upload button that lets you select an image file from your computer.


Try uploading different images and observe how the model classifies them.
Note: The terminal logs (
arduino-app-cli app logs examples:image-classification) will show the app startup messages but will not show the classification results — those are sent directly to the browser via WebSocket.

Read through the example’s source to understand how it works. From an SSH terminal:
cat /var/lib/arduino-app-cli/examples/image-classification/python/main.py
Or, if you’re using VS Code Remote-SSH (see chapter 4 §2 for setup), open the folder and browse python/main.py directly:
/var/lib/arduino-app-cli/examples/image-classification/python/main.py.The key components:
# The Bricks handle all the heavy lifting
from arduino.app_bricks.web_ui import WebUI
from arduino.app_bricks.image_classification import ImageClassification
# Initialize the classification brick (loads the model internally)
image_classification = ImageClassification()
# Callback: when the browser sends an image, classify it and return the result
def on_classify_image(client_id, data):
image_data = data.get('image') # Base64-encoded image from browser
image_bytes = base64.b64decode(image_data)
pil_image = Image.open(io.BytesIO(image_bytes))
results = image_classification.classify(pil_image) # Run inference
ui.send_message('classification_result', response) # Send back to browser
# Set up the WebUI and register the callback
ui = WebUI()
ui.on_message('classify_image', on_classify_image)
App.run()
Notice that:
ImageClassification() is a Brick—a prebuilt module that encapsulates the model and inference logic.WebUI() provides the web server and WebSocket communication.App.run() starts the app without a user_loop — it is event-driven (triggered by browser uploads).Bridge.call() — the MCU is not involved.arduino-app-cli app stop examples:image-classification
We will not go into more detail here because there is plenty of documentation on it. But the main steps are:
Examples, go to Classify Images
The pre-installed example demonstrates the Brick-based approach: high-level components that abstract away model loading, image processing, and web communication. This is convenient for quick demos, but it has limitations:
In Part 2, we will build our own classifier that overcomes all of these limitations.
Now we build a custom classifier from scratch. Following the same project used in the TinyML Made Easy e-book, we will train a model to classify three categories:

Build a system that continuously captures frames from a USB webcam, classifies what it sees, and provides physical feedback through the MCU:
USB Webcam -> Python (MPU) -> EI model -> "robot" / "periquito" / "background"
|
Bridge.call("show_result", label, confidence)
|
MCU (Arduino sketch)
-> RGB LEDs (color per class)
-> LED matrix (icon per class)
This is fundamentally different from Part 1: live inference (not static uploads), MCU actuation (not just browser display), and a custom model (not a generic pre-trained one).
We show two deployment paths — you can choose one or try both:
The most crucial step in any ML project is collecting a high-quality dataset.

You can capture images directly from the USB webcam on the UNO Q. For that, create a script, for example: capture_images.py:
import cv2
import os
import time
label = "robot" # Change for each class
output_dir = f"/home/arduino/dataset/{label}"
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
count = 0
print(f"Capturing images for class '{label}'. Press Ctrl+C to stop.")
try:
while count < 60:
ret, frame = cap.read()
if ret:
filename = f"{output_dir}/{label}_{count:03d}.jpg"
cv2.imwrite(filename, frame)
print(f"Saved: {filename}")
count += 1
time.sleep(0.5)
except KeyboardInterrupt:
pass
cap.release()
print(f"Captured {count} images for '{label}'.")
Install OpenCV if needed:
# On Debian/Ubuntu systems, you need to install the python3-venv package using the following command.
sudo apt install python3.13-venv
# 1) criar o env (ainda NÃO existe a pasta cv2-env)
python3.13 -m venv ~/envs/cv2-env
# 2) ativar o env
source ~/envs/cv2-env/bin/activate
# 3) instalar OpenCV (melhor a versão headless)
pip install --upgrade pip
pip install opencv-python-headless
# 4) run the script:
python3 capture_images.py
The images will be stored on the Uno-Q (dataset/robot, periquito, etc.). Copy them to your computer with
scp(scp -r arduino@<UNO_Q_IP>:~/path/to/dataset ./dataset) or by dragging them in VS Code’s file explorer, then upload them to Edge Impulse Studio via Data acquisition > Upload data.
If you already have images from the TinyML Made Easy e-book, upload them to Edge Impulse Studio via Data acquisition > Upload data, selecting the correct label for each batch.
Tip: More diverse training data produces more robust models.
In Edge Impulse Studio, go to Data acquisition and verify your data is correctly labeled and balanced.

You can clone a similar project: NICLA-Vision_Image_Classification
robot, periquito, background.
Review the accuracy, confusion matrix, and on-device performance.

On Training, the model reached a high accuraccy (with a estimated latency of 100ms). With models with smaller alphas (0.35, for example), we can get faster inferences with similar accuraccy. you should test the better parameters for you project.
Go to Model testing. On settings, enable Int8 and click Classify all to validate on the test dataset.

This path uses the Brick architecture with your custom model. Quick to set up, but limited to static image uploads via the browser.
We can copy an example using CLI as below
cp -r /var/lib/arduino-app-cli/examples/image-classification/ ~/ArduinoApps/my-classifier-bricks
Or directly on the Arduino App Lab, giving it a new name. The project will be copied to My Apps area.

My Apps area, go to the Bricks/Image Classification, select AI models and Train new AI model

When the project is built, the .eim file will be downloded directly to your computer and a Pop-up window will appear, with a button Go to Arduino. When you do it, the model will be sent to App Lab.

Note that you can test your model, downloading it to your mobile of PC, using the Bar code availabel on the deploy page.
At the AI Models area of your project, the model will be available, download it to the Uno-Q and after that select it.

On the project folder (~/ArduinoApps/my-classifier-bricks) , the Brick configuration in app.yaml is automatically updated to point to your custom model.:
name: My Classifier (Custom Images)
description: Custom Image classification in the browser using a web-based interface.
ports: []
bricks:
- arduino:image_classification:
model: ei-model-947334-1
- arduino:web_ui: {}
icon: 📊
The model (model.eim), will be saved on /home/arduino/.arduino-bricks/models/custom-ei/ei-model-947334-1
cd ~/ArduinoApps/my-classifier-bricks
arduino-app-cli app start .
Open http://<UNO_Q_IP>:7000 in your browser. Upload images of your robot, periquito, or background — the classifier now uses your custom model.

Limitation: This approach still uses static image uploads via the browser. For live webcam inference with MCU actuation, proceed to Path B.
But, before it, let’s change the main.c file to show the info about our new model and to print the inference result.
First, stop the app:
arduino-app-cli app stop .
On the main.c file, enter with the code:
from arduino.app_utils import App
from arduino.app_bricks.web_ui import WebUI
from arduino.app_bricks.image_classification import ImageClassification
from PIL import Image
import io
import base64
import time
image_classification = ImageClassification()
def print_model_info(image_classification):
info = image_classification.get_model_info()
if info is not None:
print("Model info:")
for attr in dir(info):
if not attr.startswith("_") and not callable(getattr(info, attr)):
print(f" {attr}: {getattr(info, attr)}")
else:
print("Failed to retrieve model info.")
def on_classify_image(client_id, data):
"""Callback function to handle image classification requests."""
try:
image_data = data.get('image')
image_type_raw = data.get('image_type')
if image_type_raw:
image_type = image_type_raw.split('/')[-1]
else:
image_type = 'jpeg'
confidence = data.get('confidence', 0.25)
if not image_data:
ui.send_message('classification_error', {'error': 'No image data'})
return
image_bytes = base64.b64decode(image_data)
pil_image = Image.open(io.BytesIO(image_bytes))
start_time = time.time() * 1000
results = image_classification.classify(pil_image, image_type=image_type, \
confidence=confidence)
print(f"\nInference: {results}")
diff = time.time() * 1000 - start_time
print(f"Latency: {diff:.2f} ms")
if results is None:
ui.send_message('classification_error', {'error': 'No results returned'})
return
response = {
'success': True,
'results': results,
'processing_time': f"{diff:.2f} ms"
}
ui.send_message('classification_result', response)
except Exception as e:
ui.send_message('classification_error', {'error': str(e)})
print_model_info(image_classification)
ui = WebUI()
ui.on_message('classify_image', on_classify_image)
App.run()
Run the app:
arduino-app-cli app start .
Open a second terminal, go to the project folder and run:
cd ~/ArduinoApps/my-classifier-bricks
arduino-app-cli app logs . --follow

We can see that our custom classification model receives images of 160x160 and that our labels are:
labels: ['background', 'periquito', 'robot']
The latency of this model is around 444 ms, or a little over 2 FPS.
If we need a faster model, we can train a new model using a small image (as 96x96) or a reduced model, with an Alpha as 0.35 for example.
This path gives you full control: live webcam capture, continuous classification, and Bridge communication to the MCU.
On The Arduino App Lab My Apps, click on the upper righ button Create new app

And name it, for example: Image Classification on Camera.
Go to Bricks and click on Add Brick.

A list of available Bricks will appear, Scroll down untill Video Image Classification and Add brick.

As you did on the previous section, once the brick is installed, go to the AI Models tab and select Uno-Q Image. Classification, deployed on the last section.

Note that the app. yaml is automatically created when the model is selected, pointing to the customized model:
name: Image Classification on Camera
description: ""
ports: []
bricks:
- arduino:video_image_classification:
model: ei-model-947334-1
icon: 😀
The file main.c is a generic file on which should be defined for our model.
The most important part of the code is the VideoImageClassification class
class VideoImageClassification(camera: BaseCamera | None,
confidence: float,
debounce_sec: float)
This is a module for image classification on a live video stream using a specified machine learning model. It provides a way to react to detected classes over a video stream, invoking registered actions in real-time.
Here is the complete code:
from arduino.app_utils import App
from arduino.app_bricks.video_imageclassification import VideoImageClassification
# Create a classification stream with default confidence threshold (0.5)
classification_stream = VideoImageClassification(confidence=0.5)
# Callback when "periquito" is detected
def periquito_detected():
print("Detected periquito!")
# Callback when "robot" is detected
def robot_detected():
print("Detected robot!")
# Subscribe to your specific labels
classification_stream.on_detect("periquito", periquito_detected)
classification_stream.on_detect("robot", robot_detected)
# Optional: callback for all classifications (useful for debugging)
def all_detected(results):
# results is a list of dicts like {"label": "periquito", "confidence": 0.85}
print("Classification results:", results)
classification_stream.on_detect_all(all_detected)
# Run the app
App.run()
Run the app:
cd ~/ArduinoApps/image-classification-on-camera
arduino-app-cli app start .
Open a second terminal, go to the project folder and run:
cd ~/ArduinoApps/image-classification-on-camera
arduino-app-cli app logs . --follow

In the previous section, we got live image classification running via the VideoImageClassification Brick, with detection callbacks printing results to the log. Now, we add the MCU side — using Bridge RPC to drive the LED matrix, based on what the camera sees.
USB Webcam -> VideoImageClassification Brick -> on_detect("robot", callback)
-> on_detect("periquito", callback)
|
Bridge.call("show_result", label)
|
MCU (Arduino sketch)
-> LED Matrix; "simple face" = robot
-> LED Matrix: "bird" = periquito
-> LED Matrix: "empty" = background/unknown
Our project from Section 9 (image-classification-on-camera) currently has no sketch/ folder — the MCU is not involved. We need to add one.
In VS Code (or via terminal), create the sketch directory (if it does not alheady exist) :
cd ~/ArduinoApps/image-classification-on-camera
mkdir -p sketch
Here, we use matrix.renderBitmap(frame, 8, 13) to render an 8×13 matrix, and we include the Arduino_LED_Matrix.h library in sketch.ino. The patterns are simple pixel art — you can refine them later using the Arduino LED Matrix Editor (adjusting for 13 columns instead of 12).
Create (or modify) sketch/sketch.ino:
#include "Arduino_RouterBridge.h"
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
// 8x13 patterns for each class
// Robot icon (simple face)
uint8_t robot_frame[8][13] = {
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,1,0,0,0,0,0,0,0,0,0,1,0},
{0,1,0,1,1,0,0,0,1,1,0,1,0},
{0,1,0,1,1,0,0,0,1,1,0,1,0},
{0,1,0,0,0,0,0,0,0,0,0,1,0},
{0,1,0,0,1,1,1,1,1,0,0,1,0},
{0,1,0,0,0,0,0,0,0,0,0,1,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0}
};
// Bird icon (simple periquito)
uint8_t bird_frame[8][13] = {
{0,0,0,0,0,1,1,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,0,0,0,0,0},
{0,0,0,1,1,0,1,1,1,0,0,0,0},
{0,1,1,1,1,1,1,1,0,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,1,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,0,0,1,0,1,0,0,0,0,0},
{0,0,0,0,0,1,0,1,0,0,0,0,0}
};
// Empty frame (background / nothing detected)
uint8_t empty_frame[8][13] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
};
void setup() {
matrix.begin();
matrix.renderBitmap(empty_frame, 8, 13);
Bridge.begin();
Bridge.provide("show_result", show_result);
}
void loop() {
}
void show_result(String label) {
if (label == "robot") {
matrix.renderBitmap(robot_frame, 8, 13);
} else if (label == "periquito") {
matrix.renderBitmap(bird_frame, 8, 13);
} else {
matrix.renderBitmap(empty_frame, 8, 13);
}
}
Create (or modify) sketch/sketch.yaml:
profiles:
default:
platforms:
- platform: arduino:zephyr
libraries:
- Arduino_RouterBridge (0.3.0)
- dependency: Arduino_RPClite (0.2.1)
- dependency: ArxContainer (0.7.0)
- dependency: ArxTypeTraits (0.3.2)
- dependency: DebugLog (0.8.4)
- dependency: MsgPack (0.4.2)
default_profile: default
Modify python/main.py to send classification results to the MCU via Bridge. In the previous example, we created abd use separate on_detect callbacks. Here we will use only on_detect_all and pick the highest-confidence result from there. This removes the individual on_detect("periquito", ...) / on_detect("robot", ...) callbacks entirely and relies on a single on_detect_all that always picks the top class. The current_label check ensures we only send a Bridge call when the result actually changes.
Also lowered the confidence to 0.3 so that “background” detections (which sometimes have lower confidence) still come through. You can tune this after testing.
from arduino.app_utils import App, Bridge
from arduino.app_bricks.video_imageclassification import VideoImageClassification
classification_stream = VideoImageClassification(confidence=0.3)
current_label = ""
# Priority order: robot and periquito take precedence over background
PRIORITY = {"robot": 2, "periquito": 1, "background": 0}
def all_detected(results):
global current_label
print(f"Raw results: {results}")
if results:
# Pick the highest-priority label from the results
label = max(results, key=lambda r: PRIORITY.get(r, -1))
else:
label = "background"
if label != current_label:
current_label = label
Bridge.call("show_result", label)
print(f"==> {label}")
classification_stream.on_detect_all(all_detected)
App.run()
Note: We use a
current_labelvariable to avoid sending redundant Bridge calls every frame. The LED Matrix only changes when the detected class actually changes.
Your project should now look like this:
image-classification-on-camera/
├── app.yaml
├── python/
│ └── main.py
└── sketch/
├── sketch.ino
└── sketch.yaml
The app.yaml should already point to your custom model from Section 9:
name: Image Classification on Camera
description: ""
ports: []
bricks:
- arduino:video_image_classification:
model: ei-model-947334-1
icon: 😀
Stop any running app first:
arduino-app-cli app stop .
Start the updated project:
arduino-app-cli app start .
Note: The first run after adding the sketch will take longer, as the system compiles the Arduino code for the MCU.
Monitor the logs:
arduino-app-cli app logs . --follow
Now place objects in front of the camera and observe:

arduino-app-cli app stop .
The Arduino sketch can be extended with additional feedback:
External LEDs:
#include "Arduino_RouterBridge.h"
// Use digital pins for external LEDs (or replace with correct RGB LED macros)
#define MY_LED_BLUE 2
#define MY_LED_GREEN 3
#define MY_LED_RED 4
void setup() {
pinMode(MY_LED_RED, OUTPUT);
pinMode(MY_LED_GREEN, OUTPUT);
pinMode(MY_LED_BLUE, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(MY_LED_RED, LOW);
digitalWrite(MY_LED_GREEN, LOW);
digitalWrite(MY_LED_BLUE, LOW);
digitalWrite(LED_BUILTIN, HIGH);
Bridge.begin();
Bridge.provide("show_result", show_result);
}
void loop() {}
void show_result(String label) {
digitalWrite(MY_LED_RED, LOW);
digitalWrite(MY_LED_GREEN, LOW);
digitalWrite(MY_LED_BLUE, LOW);
if (label == "robot") {
digitalWrite(MY_LED_BLUE, HIGH);
} else if (label == "periquito") {
digitalWrite(MY_LED_GREEN, HIGH);
} else {
digitalWrite(MY_LED_RED, HIGH);
}
}
Buzzer Feedback:
#define BUZZER_PIN 3
void show_result(String label) {
// LEDs as before...
if (label == "robot") tone(BUZZER_PIN, 1000, 200);
if (label == "periquito") tone(BUZZER_PIN, 2000, 200);
}
Servo Motor:
#include <Servo.h>
Servo myServo;
void show_result(String label) {
if (label == "robot") myServo.write(0);
else if (label == "periquito") myServo.write(180);
else myServo.write(90);
}
From the logs captured in the previous sections, we can observe the inference performance:
| Model | Input Size | Inference (approx.) | Accuracy |
|---|---|---|---|
| MobileNetV2 0.1 | 96×96 | ~30–50 ms | Good |
| MobileNetV2 0.35 | 96×96 | ~60–100 ms | Better |
| MobileNetV2 0.35 | 160×160 | ~100–200 ms | Very Good |
| MobileNetV2 1.0 | 160×160 | ~400–500 ms | Best |
If you need faster inference (e.g., for responsive LED feedback), retrain with a smaller model (MobileNetV2 0.35 at 96×96). You should test different configurations to find the best balance for your project.
| Aspect | Path A (Image Classification Brick) | Path B (Video Image Classification Brick) |
|---|---|---|
| Input | Static images (browser upload) | Live webcam (continuous) |
| Latency | Per-upload (user-triggered) | Continuous (model-dependent FPS) |
| MCU actuation | Not included (browser only) | Yes, via Bridge RPC |
| Web UI | Built-in (upload + results) | No built-in UI (add separately) |
| Best for | Quick testing, demos | Real-time applications |
| Metric | Nicla Vision (MCU) | UNO Q (MPU) |
|---|---|---|
| Model | MobileNetV2 0.05 (96×96, INT8) | MobileNetV2 0.35–1.0 (96×96 to 160×160) |
| Inference time | ~100–200 ms | ~50–500 ms (model-dependent) |
| Accuracy (3 classes) | ~85–90% | ~90–97% |
| Power during inference | ~100 mW | ~3–6 W (*) |
| Framework | TF Lite Micro (C++) | App Lab Bricks (Python) |
| Camera | Built-in | External USB webcam |
| MCU actuation | Direct GPIO | Bridge RPC |
(*) Practical total estimate Under live image classification (using the USB camera and USB hub), a realistic total power consumption profile is: • Board (Uno‑Q): ~2–3.5 W when CPU/NPU is busy. • USB camera: ~0.5–1.25 W. • Powered hub: ~0.5–1 W. → Total: ~3–6 W from the wall (5 V, ~0.6–1.2 A) for a typical one‑camera, one‑hub setup.
The UNO Q can run larger, more accurate models but uses significantly more power. The Nicla Vision is ideal for battery-powered, always-on applications; the UNO Q excels when accuracy matters and you need the AI result to drive real-time hardware actions.
Add a fourth class: Collect images of a new object, retrain the model in Edge Impulse, redeploy to the UNO Q, and update the Python callbacks and Arduino sketch. How does adding more classes affect accuracy?
Experiment with model size: Train the same dataset with MobileNetV2 0.1 (96×96) and MobileNetV2 1.0 (160×160). Deploy both to the UNO Q and compare inference time, accuracy, and LED responsiveness. Create a table with your results.
Log to CSV: Modify main.py to log each detection event (timestamp, label) to a CSV file on the UNO Q. After running for 5 minutes, download the CSV and plot detection frequency per class.
LED Matrix icons: Design custom 8×13 LED matrix patterns for each class (robot, periquito, background) and display them on the UNO Q’s built-in LED matrix via the Arduino sketch.
Cross-platform comparison: If you have a Nicla Vision, deploy a 96×96 MobileNetV2 0.1 model to both platforms. Compare inference time, accuracy, power consumption, and development workflow.
Debounce tuning: Experiment with the debounce_sec parameter in VideoImageClassification(confidence=0.5, debounce_sec=1.0). How does increasing the debounce time affect the responsiveness vs. stability of the LED feedback?
In this tutorial, we progressed through three levels of image classification on the Arduino UNO Q:
ImageClassification Brick handles static images (great for testing); the VideoImageClassification Brick handles live camera streams (essential for real applications).In the next tutorial, we will extend our computer vision skills to Object Detection — using FOMO and YOLO to detect not just what is in the image, but also where it is. This opens the door to applications in tracking, counting, and spatial awareness.
| Resource | URL |
|---|---|
| Edge Impulse Studio | https://studio.edgeimpulse.com |
| EI Arduino UNO Q Docs | https://docs.edgeimpulse.com/hardware/boards/arduino-uno-q |
| EI App Lab Deployment | https://docs.edgeimpulse.com/hardware/deployments/run-arduino-app-lab |
| App Lab Bricks Documentation | https://docs.arduino.cc/software/app-lab/tutorials/bricks |
| TinyML Made Easy (Nicla Vision) | https://mjrovai.github.io/TinyML_Made_Easy_NiclaV_eBook/ |
| Arduino App Lab CLI | https://docs.arduino.cc/software/app-lab/tutorials/cli |
| UNO Q Setup Tutorial | Previous chapter |
Tutorial created for IESTI05 — Edge AI ML System Engineering, UNIFEI. Licensed under CC-BY-SA 4.0.