目录
在这个教程中,我们将使用人工智能(AI)和飞行时间(Time of Flight)来创建一个石头剪刀布游戏。
我们的目标是展示如何使用NanoEdge AI Studio和Arduino IDE来创建任何你能想到的、使用AI技术的项目。
NanoEdge AI Studio是由STMicroelectronics开发的一款工具,专为嵌入式系统用户设计,帮助他们获取一个AI库,以便将其嵌入到他们的项目中,并且只使用他们自己的数据。
通过一个简单且逐步的过程,我们将收集与你用例相关的数据,并使用这个工具以最小的努力获取最佳模型。
NanoEdge AI Studio库与任何Cortex M都兼容,从4.4版本开始,该工具能够编译出可以直接导入到Arduino IDE中的库。
1 目标
- 从头开始使用TOF数据和AI创建一个石头剪刀布游戏。
- 通过这个例子,学习如何使用NanoEdge AI Studio轻松地将AI集成到未来的任何项目中。
2 需要的软硬件
硬件:
- 一块Arduino GIGA R1开发板
- 一块Arduino GiGA显示屏扩展板
- 一个ST飞行时间传感器:X-nucleo-53L5A1
- 一根micro USB线,用于将Arduino开发板连接到你的台式电脑
软件:
- 为开发板编程,你可以使用Arduino网络编辑器或安装Arduino IDE。在后续部分,我们将为你提供有关如何设置这些软件的更多详细信息。
- 为了创建并获取石头剪刀布手势识别的AI模型,你需要NanoEdge AI Studio v4.4或更高版本。
注意:对于Windows用户,我们建议使用Arduino IDE v1.8.19版本。请不要使用Microsoft应用商店的版本。
或更高版本。
注意:对于Windows用户,我们建议使用Arduino IDE v1.8.19版本。请不要使用Microsoft应用商店的版本。
语法说明
标题文本样式列表图片链接目录代码片表格注脚注释自定义列表LaTeX 数学公式插入甘特图插入UML图插入Mermaid流程图插入Flowchart流程图插入类图快捷键
目录复制
2.1 硬件设置
关于设置和组装,有一点需要注意。你需要将显示屏插接在GIGA R1开发板的顶部,并将TOF扩展板放在开发板的下面,像这样:
重要提示:我们需要修改TOF和Arduino板之间的I2C通信引脚,以避免与显示屏发生冲突:
- 将一根跳线从SDA1引脚连接到20SDA引脚。
- 将另一根跳线从SCL1引脚连接到21SCL引脚。
3 NanoEdge AI Studio
对于与NanoEdge AI Studio相关的任何部分,你可以查阅相关文档以获取更多信息:NanoEdge AI Studio Docs
3.1 安装
首先,我们需要安装NanoEdge AI Studio。
下载链接
然后:
- 填写完表格后,会发送一封包含使用工作室许可证的电子邮件。
- 等待下载完成,然后启动.exe文件开始安装。
- 安装完成后,输入许可证,这样就完成了!
3.2 创建一个Project
在NanoEdge AI Studio中,有四种项目可供选择,每种项目服务于不同的目的:
- 异常检测(AD):用于检测正常行为和异常行为。可以在板上直接进行再训练。
- 一类分类(1c):创建模型以检测正常和异常行为,但仅使用正常数据。(在无法收集异常样本的情况下)
- N类分类(Nc):创建模型以将数据分类为你定义的多个类别。
- 外推(Ex):简而言之,就是回归。根据输入数据预测一个值而不是类别(例如速度或温度)。
在我们的案例中,我们想要创建一个能够识别三种手势(剪刀、石头和布)的人工智能,所以我们点击N类分类 > 创建新项目。
在项目设置中:
- 输入项目名称
- 如果需要,定义RAM和FLASH限制
- 点击选择目标,然后转到ARDUINO BOARD选项卡并选择GIGA R1 WiFi
- 在传感器类型中,输入GENERIC,并将轴数设置为1。
然后点击保存并下一步
注意:我们正在处理64个值(8x8)的TOF矩阵,但每次测量都是独立进行的。这就是为什么我们使用一个轴的原因。
3.3 数据收集
在NanoEdge AI Studio的信号部分,我们需要导入四个数据集:
- 无:当我们没有操作时
- 纸
- 剪刀
- 石头
TOF可以收集8x8大小的矩阵数据,因此我们的数据集中的每个信号大小都是64。然后,我们需要收集数据以创建四个数据集,每个数据集包含每种手势的各种示例。
数据收集的Arduino代码:为了收集数据,请在Arduino IDE中创建一个新项目,并复制以下代码:
#include <Wire.h>
#include <SparkFun_VL53L5CX_Library.h> //http://librarymanager/All#SparkFun_VL53L5CX
SparkFun_VL53L5CX myImager;
VL53L5CX_ResultsData measurementData; // Result data class structure, 1356 bytes of RAM
int imageResolution = 0; //Used to pretty print output
float neai_buffer[64];
void setup() {
Serial.begin(115200);
delay(100);
Wire.begin(); //This resets to 100kHz I2C
Wire.setClock(400000); //Sensor has max I2C freq of 400kHz
if (myImager.begin() == false)
{
Serial.println(F("Sensor not found - check your wiring. Freezing"));
while (1);
}
myImager.setResolution(8*8); //Enable all 64 pads
myImager.setRangingFrequency(15); //Ranging frequency = 15Hz
imageResolution = myImager.getResolution(); //Query sensor for current resolution - either 4x4 or 8x8
myImager.startRanging();
}
void loop() {
if (myImager.isDataReady() == true)
{
if (myImager.getRangingData(&measurementData)) //Read distance data into array
{
for(int i = 0 ; i < imageResolution ; i++) {
neai_buffer[i] = (float)measurementData.distance_mm[i];
}
for(int i = 0 ; i < imageResolution ; i++) {
Serial.print(measurementData.distance_mm[i]);
Serial.print(" ");
}
Serial.println();
}
}
}
我们需要向项目中添加两个库:
- Wire.h 用于I2C通信:点击Sketch > Include Library > Wire
- SparkFun_VL53L5CX_Library:用于使用TOF:点击Sketch > Include Library > Manage Library > SparkFun_VL53L5CX_Library,然后点击安装。
准备好后,点击验证符号来编译代码,然后点击向右的箭头将代码烧录到板上。请确保板已事先连接到电脑。
请确保选择了正确的COM端口。点击Tools > Port进行选择。如果板已插入,您应该能看到其名称显示。
回到NanoEdge:在步骤SIGNALS中:
- 点击ADD SIGNAL
- 点击FROM SERIAL
- 确保选择了正确的COM端口
- 保持波特率不变
- 点击最大行数并输入500
- 点击START/STOP开始记录数据
- 完成后,点击CONTINUE,然后点击IMPORT
非常重要:
不要像玩石头剪刀布那样多次记录数据,而是在捕捉器下方不同位置连续记录一个手势(上、下、左、右等)。例如,在做剪刀手势的同时,在收集500个信号时,不要离开捕捉器的视线。
我们确实只需要与该类对应的数据。如果您模拟玩游戏来记录数据,您将得到没有手势的数据(因为您不在捕捉器的视线内),然后是对应类别的数据。
请确保不要太靠近TOF,因为它将只能看到一个覆盖整个矩阵的大物体。
3.4 寻找最佳模型
进入BENCHMARK步骤。
在这一步中,NanoEdge AI Studio将为您的数据、模型和参数寻找最佳预处理方式,以便找到最适合您用例的最佳组合。
完成后,您将在项目结束时能够将找到的组合编译为AI库,导入到Arduino IDE中。
- 点击NEW BENCHMARK
- 选择之前收集的四个类别
- 点击START
studio会显示几个指标:
- 平衡准确度,它是每个类别良好分类的加权平均值
- RAM和FLASH需求
- 分数:这个指标考虑了找到模型的性能和大小。
基准测试所需的时间与使用的缓冲区大小及其数量密切相关。基准测试开始时改进速度很快,然后趋于缓慢,以便在最后找到最优化的库。因此,当您对结果满意时(超过95%是一个很好的参考),您可以停止基准测试。
3.5 验证找到的模型
验证和仿真步骤旨在确保找到的库确实是最佳的。*要实现这一点,建议测试几个最佳库与新数据,并确保所选的库是最佳的。
注意:为了收集用于验证的新数据集,您可以返回到信号步骤,通过串行导入新数据集,并下载它们以在验证中使用:
- 转到验证步骤
- 选择1到10个库
- 点击NEW EXPERIMENT
- 为4个类别导入新数据集。
- 点击START
您还可以使用仿真器对某个库进行进一步的测试(如果需要),并使用串行连接进行实时测试,例如快速演示:
3.6 获取Arduino库↑
要获取包含模型和将其添加到您的Arduino代码中的功能的AI库:
- 转到编译步骤
- 重要提示:对于GIGA R1 WIFI,请确保未选中Float abi
- 点击COMPILE LIBRARY
- 输出是一个.zip文件,稍后我们将直接将其导入到Arduino IDE中。
4 Arduino IDE
4.1 库设置↑
从NanoEdge AI Studio获取包含AI库的zip文件后,点击File > New(文件 > 新建)创建一个新项目。
要使这个项目能够运行,我们需要几个库。
- 点击Sketch > Include Library > Wire来包含用于I2C通信的Wire.h库(默认已安装)。
要添加一个标准库,请点击Sketch > Include Library > Manage Library并安装以下库: - ArduinoGraphics:用于屏幕显示。
- SparkFun_VL53L5CX_Library:用于使用TOF。
要添加NanoEdge AI Studio库:
- 在Arduino IDE中点击Sketch > Include Library > Add .ZIP Library…(草图 > 包含库 > 添加.ZIP库…)。
- 找到NanoEdge AI Studio编译后提供的.zip文件并解压。
- 在Arduino文件夹中找到之前解压的内容并导入.zip文件。
我们还需要添加incbin.h:
- 您可以在以下位置找到incbin.h:incbin.h
- 将其复制并粘贴到包含项目.ino文件的文件夹中。
最后,我们需要选择GIGA R1 WIFI作为我们的开发板:
- 点击Tools > Board: “您的实际开发板” > Boards Manager…(工具 > 开发板:“您的实际开发板” > 开发板管理器…)。
- 搜索并安装Arduino Mbed OS Giga Boards。
- 然后返回Tools > Board: “您的实际开发板” > Arduino Mbed OS Giga Boards > Arduino Giga R1。
注意:在选择开发板时,Arduino_H7_Video.h会自动添加,您无需手动添加。
4.2 代码
以下代码负责三个主要部分:
- 从TOF收集数据。
- 每次从TOF收集数据时,使用NanoEdge AI库来检测正在播放的手势。
- 加载并显示与NanoEdge AI Studio检测到的手势相对应的图像。
以下是代码:
#include "Arduino_H7_Video.h"
#include "ArduinoGraphics.h"
#include "incbin.h"
#include <Wire.h>
#include <SparkFun_VL53L5CX_Library.h> //http://librarymanager/All#SparkFun_VL53L5CX
#include "NanoEdgeAI.h"
#include "knowledge.h"
// Online image converter: https://lvgl.io/tools/imageconverter (Output format: Binary RGB565)
//#define DATALOG
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 480
#define SIGN_WIDTH 150
#define SIGN_HEIGHT 200
#define LEFT_SIGN_X 115
#define RIGHT_SIGN_X 530
#define SIGN_Y 200
#define INCBIN_PREFIX
INCBIN(backgnd, "YOUR_PATH/backgnd.bin");
INCBIN(rock, "YOUR_PATH/rock.bin");
INCBIN(paper, "YOUR_PATH/paper.bin");
INCBIN(scissors, "YOUR_PATH/scissors.bin");
void signs_wheel(void);
void signs_result(uint16_t neaiclass);
uint16_t mostFrequent(uint16_t arr[], int n);
Arduino_H7_Video Display(SCREEN_WIDTH, SCREEN_HEIGHT, GigaDisplayShield);
Image img_backgnd(ENCODING_RGB16, (uint8_t *) backgndData, SCREEN_WIDTH, SCREEN_HEIGHT);
Image img_rock(ENCODING_RGB16, (uint8_t *) rockData, SIGN_WIDTH, SIGN_HEIGHT);
Image img_paper(ENCODING_RGB16, (uint8_t *) paperData, SIGN_WIDTH, SIGN_HEIGHT);
Image img_scissors(ENCODING_RGB16, (uint8_t *) scissorsData, SIGN_WIDTH, SIGN_HEIGHT);
Image img_classes[CLASS_NUMBER - 1] = {img_paper, img_rock, img_scissors};
SparkFun_VL53L5CX myImager;
VL53L5CX_ResultsData measurementData; // Result data class structure, 1356 bytes of RAM
int imageResolution = 0; //Used to pretty print output
int imageWidth = 0; //Used to pretty print output
float neai_buffer[DATA_INPUT_USER];
float output_buffer[CLASS_NUMBER]; // Buffer of class probabilities
uint16_t neai_class = 0;
uint16_t previous_neai_class = 0;
int class_index = 0;
uint16_t neai_class_array[10] = {0};
void setup() {
randomSeed(analogRead(0));
Display.begin();
neai_classification_init(knowledge);
Serial.begin(115200);
delay(100);
Wire.begin(); //This resets to 100kHz I2C
Wire.setClock(400000); //Sensor has max I2C freq of 400kHz
if (myImager.begin() == false)
{
Serial.println(F("Sensor not found - check your wiring. Freezing"));
while (1);
}
myImager.setResolution(8*8); //Enable all 64 pads
myImager.setRangingFrequency(15); //Ranging frequency = 15Hz
imageResolution = myImager.getResolution(); //Query sensor for current resolution - either 4x4 or 8x8
imageWidth = sqrt(imageResolution); //Calculate printing width
myImager.startRanging();
Display.beginDraw();
Display.image(img_backgnd, (Display.width() - img_backgnd.width())/2, (Display.height() - img_backgnd.height())/2);
Display.endDraw();
delay(500);
}
void loop() {
if (myImager.isDataReady() == true)
{
if (myImager.getRangingData(&measurementData)) //Read distance data into array
{
for(int i = 0 ; i < DATA_INPUT_USER ; i++) {
neai_buffer[i] = (float)measurementData.distance_mm[i];
}
#ifdef DATALOG
for(int i = 0 ; i < DATA_INPUT_USER ; i++) {
Serial.print(measurementData.distance_mm[i]);
Serial.print(" ");
}
Serial.println();
#else
neai_classification(neai_buffer, output_buffer, &neai_class);
if(class_index < 10) {
neai_class_array[class_index] = neai_class;
Serial.print(F("class_index "));
Serial.print(class_index);
Serial.print(F(" = class"));
Serial.println(neai_class);
class_index++;
} else {
neai_class = mostFrequent(neai_class_array, 10);
Serial.print(F("Most frequent class = "));
Serial.println(neai_class);
class_index = 0;
if (neai_class == 4 && previous_neai_class != 4) // EMPTY
{
previous_neai_class = neai_class;
Display.beginDraw();
Display.image(img_backgnd, (Display.width() - img_backgnd.width())/2, (Display.height() - img_backgnd.height())/2);
Display.endDraw();
Serial.println(F("Empty class detected!"));
}
else if (neai_class == 1 && previous_neai_class != 1) // PAPER
{
previous_neai_class = neai_class;
Serial.println(F("Paper class detected!"));
signs_wheel();
signs_result(neai_class);
}
else if (neai_class == 3 && previous_neai_class != 3) // SCISSORS
{
previous_neai_class = neai_class;
Serial.println(F("SCISSORS class detected!"));
signs_wheel();
signs_result(neai_class);
}
else if (neai_class == 2 && previous_neai_class != 2) // ROCK
{
previous_neai_class = neai_class;
Serial.println(F("Rock class detected!"));
signs_wheel();
signs_result(neai_class);
}
}
#endif
}
}
}
void signs_wheel(void)
{
for(int i = 0 ; i < 10 ; i++) {
Display.beginDraw();
Display.image(img_backgnd, (Display.width() - img_backgnd.width())/2, (Display.height() - img_backgnd.height())/2);
Display.image(img_SCISSORS, LEFT_SIGN_X, SIGN_Y);
if(i % (CLASS_NUMBER - 1) == 0) {
Display.image(img_rock, RIGHT_SIGN_X, SIGN_Y);
} else if(i % (CLASS_NUMBER - 1) == 1) {
Display.image(img_paper, RIGHT_SIGN_X, SIGN_Y);
} else {
Display.image(img_SCISSORS, RIGHT_SIGN_X, SIGN_Y);
}
Display.endDraw();
}
}
void signs_result(uint16_t neaiclass)
{
Display.beginDraw();
Display.image(img_backgnd, (Display.width() - img_backgnd.width())/2, (Display.height() - img_backgnd.height())/2);
int random_img = random(0, CLASS_NUMBER - 1);
if(random_img == neaiclass - 1) {
Display.fill(255, 127, 127);
Display.rect(LEFT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
Display.rect(RIGHT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
} else if(random_img == (neaiclass == 1) ? 2 : (neaiclass == 2) ? 0 : 1) {
Display.fill(255, 0, 0);
Display.rect(LEFT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
Display.fill(0, 255, 0);
Display.rect(RIGHT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
} else {
Display.fill(0, 255, 0);
Display.rect(LEFT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
Display.fill(255, 0, 0);
Display.rect(RIGHT_SIGN_X - 10, SIGN_Y - 10, SIGN_WIDTH + 20, SIGN_HEIGHT + 20);
}
Display.image(img_classes[neaiclass - 1], LEFT_SIGN_X, SIGN_Y);
Display.image(img_classes[random_img], RIGHT_SIGN_X, SIGN_Y);
Display.endDraw();
delay(1000);
}
uint16_t mostFrequent(uint16_t arr[], int n)
{
int count = 1, tempCount;
uint16_t temp = 0,i = 0,j = 0;
//Get first element
uint16_t popular = arr[0];
for (i = 0; i < (n- 1); i++)
{
temp = arr[i];
tempCount = 0;
for (j = 1; j < n; j++)
{
if (temp == arr[j])
tempCount++;
}
if (tempCount > count)
{
popular = temp;
count = tempCount;
}
}
return popular;
}
在能够烧录代码之前,我们需要做一些准备工作。
4.3 屏幕显示
我们将在屏幕上显示背景以及SHIFUMI(剪刀、石头、布)游戏的手势。您可以下载以下图像:
背景图像:
- Rock sign:
- Scissors sign:
- Paper sign:
我们还需要使用以下网站将这些图像转换为bin文件: - 在线图像转换器 - BMP、JPG或PNG转C数组或二进制 | LVGL
- 选择图像。
- 将输出格式更改为二进制RGB565。
- 点击转换。
接下来:
获取所有二进制图像,并将它们复制到项目文件夹中。您可以创建一个名为images的文件夹(例如),然后将它们放入其中。
在代码中,更新图像路径。您可能需要添加图像的完整路径。
4.4 NanoEdge库的使用↑
关于NanoEdge AI库的使用,它非常简单:
- 我们在setup()函数中使用neai_classification_init(knowledge)来加载在基准测试期间获得知识的模型。
- 我们使用neai_classification(neai_buffer, output_buffer, &neai_class)函数来进行检测。此函数需要三个我们创建的变量作为输入:
float neai_buffer[64]:用于检测的输入数据,即TOF数据。
float output_buffer[CLASS_NUMBER]:一个大小为4的输出数组,包含输入信号属于每个类别的概率。
uint16_t neai_class = 0:我们用来获取检测到的类别的变量。它对应于具有最高概率的类别。
就这么简单!
5 演示设置
如果您想要重现这个演示设置,以下是所使用的资源:
4x Brackets 20.stl
M3尼龙12mm螺丝和M3尼龙螺母
支撑物: