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;
}
编辑:黄飞
评论
查看更多