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

作者/攝影 曾吉弘
時間 1.5 小時
難度

★★★☆☆

材料表
  • Arduino Nano 33 BLE Sense
  • OV7670 相機模組
  • 母/母杜邦線材
  • PC / NB (作業系統不限),用於執行 Processing IDE / Arduino IDE / Teachable Machine(網頁開啟)

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

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

說是即時,但都在 Arduino 上執行了,當然不可能快到哪裡去,圖片也是黑白的,這都是針對 Arduino 的運算能力來考量,且 Arduino Nano 33 BLE Sense 與 OV7670 相機模組這兩個買起來也快接近 Raspberry Pi 3 了。另外,ESP32cam 搭配 tensorflow lite 很早就能做到深度學習視覺分類應用,但用 teachable machine 可以自行訓練所要目標,也是不錯的選擇。老話一句,看您的專案需求來決定使用哪些軟硬體喔!

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

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

以下操作步驟根據 teachable Machine 網站說明 https://github.com/googlecreativelab/teachablemachine-community/blob/master/snippets/markdown/tiny_image/GettingStarted.md

硬體 

Arduino Nano 33 BLE Sense / Nano 33 BLE

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

(照片來自 Arduino 網站)

 

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

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

Ov7670 相機模組

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

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

 

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

0v7670 相機模組腳位名稱 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 程式

#include <Arduino.h>
#include <Arduino_OV767X.h>

#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 程式

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;
}

 

相關文章:

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *