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 了。另外,ESP32cam 搭配 tensorflow lite 很早就能做到深度學習視覺分類應用,但用 teachable machine 可以自行訓練所要目標,也是不錯的選擇。老話一句,看您的項目需求來決定使用哪些軟硬件喔!
本文會帶您完成相關的軟硬件環境設定,并操作? Teachable Machine 透過相機模塊來搜集照片、訓練神經網絡,最后導出檔案給 Arduino 執行實時影像(灰階)分類!
以下操作步驟根據 teachable Machine 網站說明
https://github.com/googlecreativelab/teachablemachine-community/blob/master/snippets/markdown/tiny_image/GettingStarted.md
硬件
Arduino Nano 33 BLE Sense / Nano 33 BLE
目前指定只能用這片板子,其他板子編譯會有問題,看看之后有沒有機會在別的板子上執行啰,詳細規格請參考原廠網站。
https://store-usa.arduino.cc/products/arduino-nano-33-ble-sense
以下是實物照片,板子都愈來愈小呢(視力挑戰)
重要信息有寫在盒裝背面,當然看原廠網站是最快的。
https://store-usa.arduino.cc/products/arduino-nano-33-ble-sense
Ov7670 相機模塊
由 OmniVision 推出的相機模塊,本范例會把它接在Arduino上,并直接從 Teachable Machine 來擷取黑白影像作為訓練數據集。
規格請看這里。
http://web.mit.edu/6.111/www/f2016/tools/OV7670_2006.pdf
實體照片如下
接下來是大工程,使用母母杜邦線并根據下表完成接線,請細心完成啰。
完成如下圖
軟件– 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 與 TeachableMachine。請先下載 Processing IDE 3.X 版本。
https://processing.org/download/?PHPSESSID=8e6890fd30e3476408b69f203c217284
下載好 Processing IDE 之后,請開啟 Sketch -> Add Library -> Manage Libraries,并搜尋ControlP5 與 Websockets,點選安裝就完成了
軟件– Teachable Machine
根據網站說明,embedded model 是標準影像分類神經網絡模型的迷你版,因此可在微控制器上運行。
這應該是最簡單的地方啦,但在操作 TM 之前要先完成上述的軟硬件設定。完成之后請根據以下步驟操作:
1.下載 TMUploader ArduinoSketch,解壓縮之后于Arduino IDE 開啟同名的 .ino 檔。板子類型要選擇 Arduino Nano 33,COM port 也要正確設定否則將無法刻錄。本程序負責把 Arduino 所拍攝的影像送往 Processing。
https://github.com/googlecreativelab/teachablemachine-community/tree/master/snippets/markdown/tiny_image/tiny_templates/TMUploader
2.下載 TMConnectorProcessing Sketch, 解壓縮之后于 Arduino IDE 開啟同名的 .pde 檔。點選左上角的執行(Play)鍵,會看到如下的畫面,并列出可用的 COM port 與聯機狀態。
https://github.com/googlecreativelab/teachablemachine-community/tree/master/snippets/markdown/tiny_image/tiny_templates/TMConnector
3.請由畫面中來選擇您的 Arduino,如果列出很多裝置不知道怎么選的話,可由 Arduino IDE 中來交叉比對。順利的話就會在 Processing 執行畫面中看到相機的實時預覽畫面。如果畫面停頓或是沒有畫面,請檢查接線是否都接對了。如果畫面有更新但是模糊,請轉動相機模塊前端圓環來調整焦距。
4.回到 Teachable Machine 網站,新增一個 ImageProject 專案。先點選 Device,再點選 [Attempt to connect to device] 選項,順利的話應該就可以看到 OV7670的畫面了。
收集資料與訓練
接下來的步驟就一樣了,請用您的照相機來搜集想要訓練的圖片吧,圖片格式為 96 x 96 灰階。請用相機對準想要辨識的物體,從 [webcam] 選項來收集照片。請注意,即便用 [Upload] 選項去上傳彩色照片,訓練完的模型一樣只能接受單色(灰階)輸入。請盡量讓數據收集與后續測試時使用同一個相機模塊 (原場考照的概念~)
訓練完成(很快)之后,于 Teachable Machine 右上角點選 [Export Model],于彈出畫面中選擇 Tensorflow Lite 并勾選下方的 Tensorflow Lite for Microcontrollers ,最后點選 [Download myModel] 就好了!轉檔需要稍等一下(有可能要幾分鐘),完成就會下載一個 converted_tinyml.zip,檔名如果不對,就代表之前的選項選錯了喔
解壓縮可以看到 converted_tinyml 相關內容
執行于 Arduino
關閉所有 Processing app,因為我們暫時不需要收集照片了,且這樣占住 COM port 而無法上傳 Arduino 程序。上傳完成,請開啟 Arduino IDE 的 Serial Monitor,就會看到每一個畫面的辨識結果與信心指數 (-128 to 127),請回顧本文一開始的執行影片就知道啰,happy making !
TMUploader Arduino 程序
#include
#include
#include"ImageProvider.h"
voidsetup() {
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);?? // turn the LED on (HIGH is the voltagelevel)
delay(400);?????????????????????? // wait for a second
digitalWrite(LED_BUILTIN, LOW);??? // turn the LED off by making the voltageLOW
delay(400);?????????????????????? // wait for a second
digitalWrite(LED_BUILTIN, HIGH);?? // turn the LED on (HIGH is the voltagelevel)
delay(400);???? ??????????????????// wait for a second
digitalWrite(LED_BUILTIN, LOW);??? // turn the LED off by making the voltageLOW
delay(400);?????????????????????? // wait for a second
Serial.begin(9600);
while (!Serial);
}
constint kNumCols = 96;
constint kNumRows = 96;
constint kNumChannels = 1;
constint bytesPerFrame = kNumCols * kNumRows;
// QVGA: 320x240 X 2 bytes per pixel (RGB565)
uint8_tdata[kNumCols * kNumRows * kNumChannels];
voidflushCap() {
for (int i = 0; i < kNumCols * kNumRows *kNumChannels; i++) {
data[i] = 0;
}
}
voidloop() {
//? Serial.println(000"creatingimage");
GetImage(kNumCols, kNumRows, kNumChannels,data);
//? Serial.println("got image");
Serial.write(data, bytesPerFrame);
//? flushCap();
}
TMConnectorProcessing 程序
importprocessing.serial.*;
importjava.nio.ByteBuffer;
importjava.nio.ByteOrder;
importwebsockets.*;
importjavax.xml.bind.DatatypeConverter;
importcontrolP5.*;
importjava.util.*;
SerialmyPort;
WebsocketServerws;
// mustmatch resolution used in the sketch
finalint cameraWidth = 96;
finalint cameraHeight = 96;
finalint cameraBytesPerPixel = 1;
finalint bytesPerFrame = cameraWidth * cameraHeight * cameraBytesPerPixel;
PImagemyImage;
byte[] frameBuffer= new byte[bytesPerFrame];
String[]portNames;
ControlP5cp5;
ScrollableListportsList;
booleanclientConnected = false;
voidsetup()
{
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();
}
voiddraw()
{?
background(240);
image(myImage, 0, 0, 224, 224);
drawConnectionStatus();
}
voiddrawConnectionStatus() {
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);
}
voidportSelect(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);
}
}
booleanstringFilter(String s) {
return (!s.startsWith("/dev/tty"));
}
intlastFrame = -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;
}
voidserialEvent(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);
}
voidwebSocketServerEvent(String msg) {
if (msg.equals("tm-connected"))clientConnected = true;
}
編輯:黃飛
?
評論
查看更多