給 Arduino 外掛一顆 AI 腦:使用離線版 Teachable Machine 和 JavaScript / p5.js / Johnny Five 從 Chrome 瀏覽器控制 Arduino 開發板

注意:本文介紹的東西目前測試,只能在 Windows 平台運作。我沒有 Mac 所以未測 macOS 環境。

2017 年時,Google 推出了一個網站 Teachable Machine,用意在用親合的瀏覽器介面展示人工智慧/機器學習模型是如何訓練和辨識目標的。2.0 版網站則在 2019 年底上線,可以辨識影像、聲音和人體姿態,而且可以下載它背後使用的 Tensorflow 模型。也就是說,你可以用這網站當成一個快速訓練 AI 的工具,然後把模型用在自己的工具中。

Teachable Machine 使用所謂的遷移式學習(Transfer Learning),也就是它其實是在用事先訓練好的模型,但你可以指定自己的辨識樣本給它。這使得 Teachable Machine 所需的「訓練」時間非常短,而且效果仍然不錯,只是客製化的空間也就不大。但和許多正規的 AI 工具相比,Teachable Machine 高親和力的介面仍是很有吸引力的。

在這篇文,我們要探討的則是如何更進一步,讓 Teachable Machine 的用處從瀏覽器走入真實世界。

從瀏覽器連結真實世界

Photo by Marvin Meyer on Unsplash

其實已經有不少人在用 Teachable Machine 用於教育用途,也有人寫了各種將它銜接到某種平台或工具的版本。小朋友最常學(而且經常學到膩)的 Scratch 3 就有 Teachable Machine 外掛。

幾年前我在試玩大陸的 MakeBlock(一個把 Arduino、Scratch 等一堆東西包裝在一起的編輯器)時,裡面就有第一代 Teachable Machine 的離線版。然後該編輯器也有個功能可以從 Scratch 有線遙控 Arduino 開發板,這表示我能利用 Teachable Machine 來控制真實世界的裝置!

這種與真實世界互動的可能性,使得這些好玩的人工智慧工具有了更多可能性。不過是一直到後來,才知道 MakeBlock 裡面用了一個叫 Johnny Five 的工具。

Teachable Machine 2.0 推出後,官方網站也有一個連接 Arduino 開發板的糖果分類器,但對於怎麼實作的講得也不是很清楚。

去年研究過後,我曾弄了一個使用 p5.serialport 來對序列埠輸出辨識結果的專案。Teachable Machine 在下載模型時會顯示一些在 JavaScript 執行模型的範例,其中一種是在 p5.js 互動式平台上透過 ml5.js 函式庫來跑,而剛好它們有做一個實驗性的序列埠輸出工具。如果把 p5.js 和 ml5.js 函式庫拷貝下來,那麼我也可以透過本機伺服器來跑專案。

以上做法的好處是任何開發板都可以用,但你得另外在開發板上寫個程式判讀序列埠資料,而且要另外開一個 p5.serialcontrol 工具程式來讓 Teachable Machine 跟開發板「連線」。而若要修改開發板程式,就得把工具程式開開關關,以免佔用序列埠而上傳不了草稿碼,有點麻煩。

不過,現在我們倒有了個新的解決方式:直接從 JavaScript 草稿碼來控制開發板。

Johnny Five:用 JavaScript 控制 Arduino

忘了當初是從哪邊聽說 Johnny Five 套件,其的原理是在 Arduino 上安裝叫做 StandardFirmataPlus 的韌體,然後從電腦上以 JavaScript 程式來遙控它(它得透過 Node.js 執行,這是個能當成本地端或伺服端 JavaScript 編譯器的環境)。這意味著開發板必須接在電腦上而不能獨立運作;但可以用 JS 控制外部硬體這件事,還是令許多 JS 使用者備感驚奇。

Johnny Five 這名字是蝦咪東東來著?它其實是 80 年代下半老科幻喜劇片《霹靂五號》的主角機器人,本來是軍用機器人,被雷打到後產生了自我意識和逃出來。小時候在電視上看過。雖然老實說,這跟 JavaScript 或 Arduino 其實也沒啥關係(笑)

Johnny Five 支援大部分的經典 Arduino 開發板(Uno、Nano、Leonardo、Micro、Mega 等),可控制這些板子上的一系列功能,當中也不乏一些感測器跟顯示螢幕之類的裝置。既然都是 JavaScript,那麼我應該能將它和 Teachable Machine 銜接起來,這樣就不必再透過額外的連線工具了吧!

問題在於,此套件需要透過 Node.js 執行(該套件會用 Node.js 的方式安裝在專案內),而前面 Teachable Machine 的 p5.js 範例專案都是將 .js 原始檔內嵌在 HTML 網頁中,透過瀏覽器來執行。這樣的話我可能得大幅改寫專案,從 JavaScript 的角度來產生網站,我自己也不確定能否做到。有沒有更簡單的解法呢?

"P5.J5":Johnny Five for p5.js

Photo by Brett Jordan on Unsplash

無獨有偶,我找到了有人弄的一個函式庫叫 p5.j5,基本是就是把 Johnny Five 的功能打包成一個相容於 p5.js 的 .js 函式庫,而且可以透過 web serial 或 web usb 來連接裝置。這函式庫甚至有多塞一些功能,如 oled.js(控制 I2C/SPI OLED 螢幕)及 node-pixel(控制 NeoPixel 彩燈)。

雖然做出這套件的仁兄沒給多少說明文件,但測試過後發現是可以運作的。這表示我們只要撰寫並修改單一一個 JavaScript 草稿碼,就可以實現用 Teachable Machine 控制外部裝置的目的了。而且修改後不必重啟伺服器,重新整理即可再次測試!

其實 p5.j5 還支援 WebUSB,跟 Web Serial 很像,但燒了指定的韌體後發現無法作用(p5.j5 能偵測到裝置,但不會連線),所以就放棄不用啦。

以下我們就來看如何實現 Teachable Machine + Johnny Five 的離線版 AI,並用它來控制 Arduino 開發板。

訓練並下載 Tensorflow.js 視覺辨識模型

有點好笑的是,離線的第一步是需要上線,在網路上訓練一個模型。打開 Teachable Machine V2 網站,點 Get Started 開始:

點 Image Project:

接著點 Standard image model:

點第一個分類的 Webcam 來啟動你的攝影機:

Teachable Machine 的介面非常方便,只要按下 Hold to Record 就能快速記錄影像樣本(你可按分類框右上角的三個點來編輯)。每一個 Class 是模型要辨識的一個分類。習慣上,我第一個影像分類會是背景(代表沒有東西),從第二個分類開始才是要辨識的物體。記得給分類取個有意義的名稱,這也稱為是分類的標籤(label):

樣本越多越好,而且盡量變化,比如離鏡頭有不同距離、轉動和傾斜,甚至確保每種分類之間的差異夠大、背景夠一致等等,這些都能增加正確辨識的機會。(這其實是個絕佳的教育示範 — — AI 模型只會根據你給它的範例來學習。如果樣本無法反映真實情境會看到的東西,預測效果自然會打折扣。)

而所有分類都錄好樣本後,就按分類框右邊的 Train Model(訓練模型)來開始訓練:

訓練過程很快,沒多久就完成了。這時最右方會出現預覽區,你可以現場驗證模型的運作效果,視情況決定要不要重新訓練。覺得滿意之後,就按預覽框上方的 Export Model(匯出模型):

畫面會問你要下載 Tensorflow 模型的哪種版本,預設是 Tensorflow.js。點下面的 Download 來下載模型:

這就是我們等一下會用到的視覺辨識模型檔。

下載範例專案並安裝本地伺服器

載點:https://github.com/alankrantas/TeachableMachine-p5js-serialport/raw/master/teachable-machine-image-recognition-p5js-johnnyfive-web-serial.zip

把裡面的子資料夾「teachable-machine-image-recognition-p5js-johnnyfive-web-serial」的內容放到電腦中某處即可。如前所述,這專案內已經包含所有的 JavaScript 函式庫。接著將剛才下載的 model.json、metadata.json 和 weights.bin 丟進專案的 image_model 子目錄。

這個專案本身是一個網站,需要伺服器才能運作。在本機開伺服器的方式有很多,最簡單的方法之一是安裝 Python 3

準備 Arduino 開發板

Photo by Sahand Babali on Unsplash

網路上教學文很多,我就不解釋 Arduino IDE 的詳細操作了,不過由於 Johnny Five 支援的都是經典的 Arduino 開發板,所以其實也不需要什麼額外設定:

  1. 工具開發板 → 選擇你的板子
  2. 選擇板子的序列埠
  3. 檔案 範例FirmataStandardFirmataPlus,把這個範例編譯上傳到板子。如果真的找不到的話,也可從這裡 copy。

本篇我們用一個 Arduino Nano 示範(記得在「處理器」要選 ATmega328p (Old Bootloader);Uno 則不需更改。若是 Arduino Pro Micro,選 Leonardo 即可。)

我們在 D2~D5 接四個 LED 燈和 220Ω 電阻,每一個燈用來代表一個分類:

調整範例專案

在執行專案之前,我們要先告訴程式分類的標籤有哪些,以及對到的 LED 是在哪個腳位。用編輯器(比如 VS Code 或記事本)打開專案下的 sketch.js,開頭會有這兩段常數宣告:

const labels = [
'Class 1',
'Class 2',
'Class 3'
];
const ledPins = [
2, // pin 2 for Class 1
3, // pin 3 for Class 2
4 // pin 4 for Class 3
];

labels 陣列的內容就是每個分類的標籤(照訓練時的順序),而 ledPins 則是對應的腳位號碼。這兩個陣列要一樣長,不然可能會產生錯誤。

以本篇的例子而言,我將之修改如下:

const labels = [
'Agfa Vista 400',
'Kodak Ultramax 400',
'Fujifilm Superia 400',
'Lomography 800'

];
const ledPins = [
2,
3,
4,
5

];

如果你不熟 JavaScript,[ ] 之間的每一行就是一個項目,除了最末項目以外都得在結尾加逗號。標籤是字串,所以要用 ‘ 或 “ 括起來。

由於背景(”Background”)不需要控制 LED,所以這裡不需要放它。簡單來說,程式會讀取模型的辨識結果,然後根據 labels 陣列內的字串去找到對應的 LED 並點亮它:

辨識結果:Fujifilm Superia 400這個字串出現在 labels 的索引 2ledPins[2] 是腳位 4, 故點亮該 LED, 其它的關掉

因此如果辨識出來只是背景的話,就沒有燈會亮了。

啟動本地伺服器

Photo by JESHOOTS.COM on Unsplash

前面我們安裝了 Python,我們可以用它來啟動一個本地伺服器。在 Windows 上打開命令提示字元,切到專案目錄後輸入

python -m http.server

接著到瀏覽器打開 http://localhost:8000 就行了。

體驗辨識效果

伺服器啟動後,攝影機應該會啟動、並能看到辨識模型已經在作用了。點畫面下方的 Authorize Serial Device(授權序列埠裝置):

確定 Arduino 開發板有接上,點選你的裝置並按連線

你會看到開發板的 Rx 燈開始閃爍,表示它正在接收 Johnny Five 傳送的訊號。

影片中我是在專案底下用 Node.js 的 npm 裝了一個 http-server 伺服器套件,不過後來覺得裝 Python 應該還是比較簡單。

我後來也有在 64 位元 Ubuntu 20.04 測試過,Teachable Machine 模型可以跑,但 p5.j5 無法跟裝置連線。所以目前就只有 Windows 平台能運作了。至於我的樹莓派 3B+ 連網頁都開不太起來,自然是直接出局。

(稍微)解釋 JavaScript 程式碼

現在我們來回頭看一下專案中的主程式 sketch.js。只要搞懂程式的結構,又對 JavaScript 有一點了解的話,你就能自己修改。

// Teachable Machine 分類名稱
const labels = [
'Agfa Vista 400',
'Kodak Ultramax 400',
'Fujifilm Superia 400',
'Lomography 800'
];
// 對應分類的 LED 腳位
const ledPins = [
2,
3,
4,
5
];
// 全域變數
let classifier,
video,
flippedVideo,
label = '',
conf = 0,
boardConnected = false,
leds = [];
// p5.js 預載入資源
function preload() {
// 要 p5.j5 顯示板子連線功能並等待連線
loadBoard();
// 載入 Teachable Machine 影像辨識模型
classifier = ml5.imageClassifier('./image_model/model.json');
}
// p5.js 起始設定 (在 preload 之後執行, 等同於 Arduino 的 setup())
function setup() {
// 在板子連線時設定 LED 陣列
p5.j5.events.on('boardReady', () => {
ledPins.forEach(pin => {
leds.push(new five.Led(pin));
});
boardConnected = true;
});
// 設定 p5.js 畫布
createCanvas(320, 260);
video = createCapture(VIDEO);
video.size(320, 240);
video.hide();
// 從攝影機擷取影像並辨識
flippedVideo = ml5.flipImage(video);
classifyVideo();
}
// p5.js 重複執行的函式 (等同於 Arduino 的 loop())
function draw() {
// 將擷取的畫面繪製到畫布上
background(0);
image(flippedVideo, 0, 0);
fill(255);
textSize(16);
textAlign(CENTER);
// 在畫面下方顯示分類結果及信賴度
text(`Result: ${label} (${conf} %)`, width / 2, height - 4);
// 控制開發板
setBoard();
}
// 辨識影像
function classifyVideo() {
flippedVideo = ml5.flipImage(video)
classifier.classify(flippedVideo, gotResult);
flippedVideo.remove();
}
// 處理影像辨識結果
function gotResult(error, results) {
if (error) {
console.error(error);
return;
}
// 讀取辨識信賴度最高的分類名稱 (標籤) 及信賴度
label = String(results[0].label);
conf = Math.round(Number(results[0].confidence) * 10000) / 100;
console.log(`Result: ${label} (${conf} %)`);
// 繼續下一次影像辨識
classifyVideo();
}
// 控制開發板
function setBoard() {
// 確保板子已經連線
if (boardConnected) {
// 從 labels 尋找符合辨識結果之元素的索引
let labelIndex = labels.indexOf(label);
// 走訪每一個 LED 物件, 符合索引且信賴度夠高時點亮之, 否則熄滅
leds.forEach((led, index) => {
if (led) {
if (labelIndex == index && conf >= 95) {
led.on();
} else {
led.off();
}
}
});
}
}

由上可見和 Johnny Five 有關的程式,第一部分在 setup() 的 ‘boardReady’ 事件程序,用來初始化相關的物件,接著就是在我們的自訂函式 setBoard() 中根據分類結果來控制 LED 燈。

Photo by Paul Esch-Laurent on Unsplash

p5.j5 會在 JavaScript 環境內建立一個 five 全域物件,包含 Johnny Five 支援的所有東西。問題在於,在使用者完成板子連線之前,five 內的板子以及相關物件是不存在的(傳回 undefined),這時嘗試存取它們就會出錯。所以最保險的方式是先用 if 檢查一下。

若有修改程式,重開網站時記得按 Ctrl + F5 強迫重新整理。而要想除錯的話,既然是在瀏覽器內執行,你可以使用 Chrome 的開發人員工具(選單更多工具開發人員工具),這個介面也可以讓你看到程式碼中 console.log() 印出的辨識結果。

下面是我在開發人員工具的命令列介面查詢連線後的 five 物件,可見它有以下內容:

{Accelerometer: ƒ, Animation: ƒ, Altimeter: ƒ, Barometer: ƒ, Board: ƒ, …}
Accelerometer: class m
Altimeter: class l
Animation: class l
Barometer: class a
Board: class E
Boards: class y
Button: class d
Buttons: class p
Collection: class n
Color: class c
Compass: class c
ESC: class p
ESCs: class f
Expander: class p
Fn: {debounce: ƒ, cloneDeep: ƒ, toFixed: ƒ, map: ƒ, scale: ƒ, …}
GPS: class d
Gyro: class u
Hygrometer: class u
IMU: class v
Joystick: class u
Keypad: class f
LCD: class p
Led: class d
LedControl: class o
Leds: class o
Light: class u
Luxmeter: class u
Magnetometer: class c
Motion: class h
Motor: class m
Motors: class v
Multi: class v
Orientation: class a
Piezo: class h
Pin: class l
Ping: class p
Pins: class c
Proximity: class p
ReflectanceArray: class extends
Relay: class a
Relays: class l
Repl: class o
SIP: class v
Sensor: class l
Sensors: class h
Servo: class d
Servos: class f
ShiftRegister: class a
Sonar: class p
Stepper: class c
Switch: class a
Switches: class l
Thermometer: class E
Touchpad: class f
board: E {_events: {…}, _eventsCount: 1, _maxListeners: undefined, io: p, repl: false, …}
events: a {_events: {…}, _eventsCount: 1, _maxListeners: undefined}
firmata: class p
handleElementInit: ƒ (t,e={})
handleSerialElementInit: ƒ (t,e={})
io: p {_events: {…}, _eventsCount: 4, _maxListeners: undefined, isReady: true, MODES: {…}, …}
nodeLed: {Backpack: ƒ, Matrix8x8: ƒ, Matrix8x16: ƒ, AlphaNum4: ƒ, SevenSegment: ƒ, …}
nodePixel: {COLOR_ORDER: {…}, FORWARD: 32, BACKWARD: 0, Strip: ƒ}
oledJS: ƒ t(t,i,n)
serial: class extends
tharp: {ikSolvers: {…}, Chain: ƒ, Robot: ƒ}
usbSerial: ƒ o(t)
__proto__: Object

尾聲

從前面可以看到,p5.j5 提供了許多功能,這些可以在 Johnny Five 官網 API 查詢其 JavaScript 操作方式,少數其他額外工具則可參考 p5.j5 的連結。我不想在這裡深入寫太多,因為你實際上想拿視覺辨識做什麼,這是因人而異的。你可以用伺服馬達做一個簡單的門控機構,接上繼電器來控制電燈,或者接個直流馬達開關風扇等等,組合是無窮的。而有了這個離線版的網站後,你就能把它攜帶到其他電腦執行(假設攝影機拍攝的背景能維持差不多的話)。

若想套用到 Teachable Machine 的聲音或姿態辨識,網站在訓練好模型後都會產生 p5.js 範例程式碼,你可以用它們來修改本文的範例(基本上就是使用的 ml5.js API 有點不同而已)。

若你想更新網站中使用的 JavaScript 函式庫,你可以在下面的連結找到目前最新版:

p5.min.js

p5.j5.min.js

ml5.min.jsml5.min.js.map

Photo by Ian Stauffer on Unsplash

I just like to write weird stuff that have very little to do with my actual work. My normal blog is https://krantasblog.blogspot.com.

I just like to write weird stuff that have very little to do with my actual work. My normal blog is https://krantasblog.blogspot.com.