使用Nanoedge AI Studio在Arduino上开发石头剪刀布项目

在这里插入图片描述
在这个教程中,我们将使用人工智能(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尼龙螺母
支撑物:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值