作者/攝影 |
曾吉弘 |
時間 |
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 網站)
以下是實物照片,板子都愈來愈小呢(視力挑戰)
![]()
重要資訊有寫在盒裝背面,當然看原廠網站是最快的。
![]()
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 相機模組需要匯入一些函式庫,請根據以下步驟操作:
- 安裝 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 !
#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();
}
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;
}
相關文章: