【CAVEDU講堂】Teachable Machine 嵌入式神經網路 – Arduino 也可以做視覺分類!

作者/圖片來源:CAVEDU 教育團隊

Google Teachable Machine最近推出了新的神經網路匯出方案,需要使用Arduino Nano 33 BLE Sense 搭配 OV7670相機模組,就可以讓Arduino透過匯出的TensorFlow Lite檔案來做到邊緣裝置端的「即時」影像分類。

說是即時,但都在Arduino上執行了,當然不可能快到哪裡去,圖片也是黑白的,這都是針對Arduino的運算能力來考量,且Arduino Nano 33 BLE Sense與OV7670相機模組這兩個買起來也快接近Raspberry Pi 3了。

另外,ESP32-CAM搭配TensorFlow Lite很早就能做到深度學習視覺分類應用,但用Teachable Machine可以自行訓練所要目標,也是不錯的選擇。老話一句,看您的專案需求來決定使用哪些軟硬體喔!

本文會帶您完成相關的軟硬體環境設定,並操作Teachable Machine透過相機模組來搜集照片、訓練神經網路,最後匯出檔案給 Arduino執行即時影像(灰階)分類!別說這麼多了,先看影片!

手邊有設備的朋友歡迎跟著這一篇文章做做看,也歡迎與我們分享成果喔。教學中會用到Processing來呈現辨識結果,也歡迎從阿吉老師的Processing小教室來學習Processing的應用喔~

以下操作步驟根據Teachable Machine 網站說明

硬體

Arduino Nano 33 BLE Sense / Nano 33 BLE

目前指定只能用這片板子,其他板子編譯會有問題,看看之後有沒有機會在別的板子上執行囉,詳細規格請參考原廠網站

(圖片來源: Arduino 網站

以下是實物照片,板子都愈來愈小呢(視力挑戰)

重要資訊有寫在盒裝背面,當然看原廠網站是最快的。

OV7670 相機模組

由OmniVision推出的相機模組,本範例會把它接在Arduino上,並直接從Teachable Machine來擷取黑白影像作為訓練資料集。

規格請點我,實體照片如下。

接下來是大工程,使用母母杜邦線並根據下表完成接線,請細心完成囉。

OV7670 相機模組腳位名稱 Arduino 腳位名稱
3.3v 3.3v
GND GND (所有GND都可使用)
SCL/SIOC A5
SDA/SIOD A4
VS/VSYNC D8
HS/HREF A1
PCLK A0
MCLK/XCLK D9
D7 D4
D6 D6
D5 D5
D4 D3
D3 D2
D2 D0 / RX
D1 D1 / TX
D0 D10

完成如下圖。

軟體 – Arduino IDE

請先取得Arduino IDE,我使用Arduino 1.8.5。OV7670相機模組需要匯入一些函式庫,請根據以下步驟操作:

  1. 安裝Arduino_TensorFlowLite函式庫:Arduino IDE,請開啟 Tools -> Manage Libraries,並搜尋 Arduino_TensorFlowLite,請選擇Version 2.4.0-ALPHA之後的版本,點選安裝。
  2. 安裝Arduino_OV767X 函式庫:搜尋 Arduino_OV767X並安裝。

軟體 – Processing

Processing是用來連接Arduino與Teachable Machine。請先下載 Processing IDE 3.X 版本。下載好Processing IDE之後,請開啟 Sketch -> Add Library -> Manage Libraries,並搜尋ControlP5與Websockets,點選安裝就完成了。

軟體 – Teachable Machine

根據網站說明,Embedded model是標準影像分類神經網路模型的迷你版,因此可在微控制器上運行。

這應該是最簡單的地方啦,但在操作TM之前要先完成上述的軟硬體設定。完成之後請根據以下步驟操作:

  1. 下載TMUploader Arduino Sketch,解壓縮之後於Arduino IDE開啟同名的.ino檔。板子類型要選擇Arduino Nano 33,COM port也要正確設定否則將無法燒錄。本程式負責把Arduino所拍攝的影像送往Processing。
  2. 下載TMConnector Processing Sketch,解壓縮之後於 Arduino IDE 開啟同名的 .pde 檔。點選左上角的執行(Play)鍵,會看到如下的畫面,並列出可用的 COM port 與連線狀態。
  3. 請由畫面中來選擇您的 Arduino,如果列出很多裝置不知道怎麼選的話,可由 Arduino IDE 中來交叉比對。順利的話就會在 Processing 執行畫面中看到相機的即時預覽畫面。
    如果畫面停頓或是沒有畫面,請檢查接線是否都接對了。如果畫面有更新但是模糊,請轉動相機模組前端圓環來調整焦距。
  4. 回到Teachable Machine網站,新增一個Image Project專案。先點選Device,再點選 [Attempt to connect to device] 選項,順利的話應該就可以看到OV7670的畫面了。

收集資料與訓練

接下來的步驟就一樣了,請用您的照相機來蒐集想要訓練的圖片吧,圖片格式為 96 x 96 灰階。請用相機對準想要辨識的物體,從 [webcam] 選項來收集照片。請注意,即便用 [Upload] 選項去上傳彩色照片,訓練完的模型一樣只能接受單色(灰階)輸入。請盡量讓資料收集與後續測試時使用同一個相機模組(原場考照的概念~)

訓練完成(很快)之後,於 Teachable Machine 右上角點選 [Export Model],於彈出畫面中選擇 Tensorflow Lite 並勾選下方的 Tensorflow Lite for Microcontrollers ,最後點選 [Download my model] 就好了!轉檔需要稍等一下(有可能要幾分鐘),完成就會下載一個 converted_tinyml.zip,檔名如果不對,就代表之前的選項選錯了喔。

解壓縮可以看到converted_tinyml相關內容。

執行於 Arduino

關閉所有 Processing app,因為我們暫時不需要收集照片了,且這樣占住 COM port 而無法上傳 Arduino 程式。上傳完成,請開啟 Arduino IDE 的 Serial Monitor,就會看到每一個畫面的辨識結果與信心指數 (-128 to 127),請回顧本文一開始的執行影片就知道囉,happy making!

TMUploader Arduino 程式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include
#include

#include "ImageProvider.h"


void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
      digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
    delay(400);                       // wait for a second
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
    delay(400);                       // wait for a second
      digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
    delay(400);                       // wait for a second
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
    delay(400);                       // wait for a second
 
  Serial.begin(9600);
  while (!Serial);

     
}

const int kNumCols = 96;
const int kNumRows = 96;
const int kNumChannels = 1;
const int bytesPerFrame = kNumCols * kNumRows;

 // QVGA: 320x240 X 2 bytes per pixel (RGB565)
uint8_t data[kNumCols * kNumRows * kNumChannels];

void flushCap() {
  for (int i = 0; i < kNumCols * kNumRows * kNumChannels; i++) {
    data[i] = 0;
  }
}

void loop() {
//  Serial.println(000"creating image");
  GetImage(kNumCols, kNumRows, kNumChannels, data);
//  Serial.println("got image");
  Serial.write(data, bytesPerFrame);
//  flushCap();
}

TMConnector Processing 程式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import processing.serial.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import websockets.*;
import javax.xml.bind.DatatypeConverter;
import controlP5.*;
import java.util.*;

Serial myPort;
WebsocketServer ws;

// must match resolution used in the sketch
final int cameraWidth = 96;
final int cameraHeight = 96;
final int cameraBytesPerPixel = 1;
final int bytesPerFrame = cameraWidth * cameraHeight * cameraBytesPerPixel;

PImage myImage;
byte[] frameBuffer = new byte[bytesPerFrame];
String[] portNames;
ControlP5 cp5;
ScrollableList portsList;
boolean clientConnected = false;

void setup()
{
  size(448, 224);
  pixelDensity(displayDensity());
  frameRate(30);
  cp5 = new ControlP5(this);
  portNames = Serial.list();
  portNames = filteredPorts(portNames);
  ws = new WebsocketServer(this, 8889, "/");
  portsList = cp5.addScrollableList("portSelect")
    .setPosition(235, 10)
    .setSize(200, 220)
    .setBarHeight(40)
    .setItemHeight(40)
    .addItems(portNames);

  portsList.close();
  // wait for full frame of bytes
  //myPort.buffer(bytesPerFrame);  
  //myPort = new Serial(this, "COM5", 9600);
  //myPort = new Serial(this, "/dev/ttyACM0", 9600);
  //myPort = new Serial(this, "/dev/cu.usbmodem14201", 9600);  

  myImage = createImage(cameraWidth, cameraHeight, RGB);
  noStroke();
}

void draw()
{  
  background(240);
  image(myImage, 0, 0, 224, 224);

  drawConnectionStatus();
}

void drawConnectionStatus() {
  fill(0);
  textAlign(RIGHT, CENTER);
  if (!clientConnected) {
    text("Not Connected to TM", 410, 100);
    fill(255, 0, 0);
  } else {
    text("Connected to TM", 410, 100);
    fill(0, 255, 0);
  }
  ellipse(430, 102, 10, 10);
}
void portSelect(int n) {
  String selectedPortName = (String) cp5.get(ScrollableList.class, "portSelect").getItem(n).get("text");

  try {
    myPort = new Serial(this, selectedPortName, 9600);
    myPort.buffer(bytesPerFrame);
  }
  catch (Exception e) {
    println(e);
  }
}


boolean stringFilter(String s) {
  return (!s.startsWith("/dev/tty"));
}
int lastFrame = -1;
String [] filteredPorts(String[] ports) {
  int n = 0;
  for (String portName : ports) if (stringFilter(portName)) n++;
  String[] retArray = new String[n];
  n = 0;
  for (String portName : ports) if (stringFilter(portName)) retArray[n++] = portName;
  return retArray;
}

void serialEvent(Serial myPort) {
  // read the saw bytes in
  myPort.readBytes(frameBuffer);
  //println(frameBuffer);

  // access raw bytes via byte buffer
  ByteBuffer bb = ByteBuffer.wrap(frameBuffer);
  bb.order(ByteOrder.BIG_ENDIAN);
  int i = 0;
 
  while (bb.hasRemaining()) {
    //0xFF & to treat byte as unsigned.
    int r = (int) (bb.get() & 0xFF);
    myImage.pixels[i] = color(r, r, r);
    i++;
    //println("adding pixels");
  }
  if (lastFrame == -1) {
    lastFrame = millis();
  }
  else {
    int frameTime = millis() - lastFrame;
    print("fps: ");
    println(frameTime);
    lastFrame = millis();
  }
 
  myImage.updatePixels();
  myPort.clear();
  String data = DatatypeConverter.printBase64Binary(frameBuffer);
  ws.sendMessage(data);
}

void webSocketServerEvent(String msg) {
  if (msg.equals("tm-connected")) clientConnected = true;
}

相關文章

Processing 互動裝置藝術結合 Arduino
CAVEDU blog Teachable machine 相關文章

(本文經CAVEDU同意轉載,原文連結;責任編輯:謝涵如)

CAVEDU 教育團隊

Author: CAVEDU 教育團隊

CAVEDU 教育團隊是由一群對教育充滿熱情的大孩子所組成的機器人科學教育團隊。致力推動國內機器人教育。

Share This Post On

發表

跳至工具列