游乐游手机版
首页/AI热点日报/热点详情

Teachable Machine嵌入式神经网络视觉分类技术

类型:热点整理2026-07-08
本文介绍使用谷歌可教学机器学习平台训练神经网络,并部署至ArduinoNano33BLESense开发板,结合OV7670摄像头实现实时灰阶图像分类。具体步骤:采集96×96像素灰阶图像,导出TensorFlowLite微控制器模型,在Arduino上运行推理,通过串口输出识别结果及置信度。

本教程将循序渐进地指导你利用 Google Teachable Machine 训练一个轻量级神经网络,并将其成功部署至 Arduino 开发板,从而实现基于 OV7670 摄像头的实时灰阶图像分类。即使你没有深度学习基础,也只需跟随操作步骤即可轻松完成。

硬件准备

Arduino Nano 33 BLE Sense / Nano 33 BLE

目前官方明确仅支持此款板子,若使用其他型号编译时将会报错(官方未来可能会扩展兼容性)。详细技术规格请查阅官方页面:Arduino Nano 33 BLE Sense

实物照片如下(板子越来越小,考验视力):

重要信息印在盒装背面,当然看官网最快:

OV7670 相机模块

本教程选用 OmniVision 推出的 OV7670 摄像头模块,通过将其与 Arduino 连接,你可直接在 Teachable Machine 中采集灰度图像用于训练模型。详细规格参见官方数据手册:OV7670 数据手册

接线步骤

请使用母对母杜邦线,严格按照下表进行连接:

连接完成后的实物图:

温馨提示:接线前务必断开电源,并多次核对引脚编号。建议使用不同颜色的杜邦线来区分各功能引脚,以免接错。

软件环境搭建

Arduino IDE 安装与库配置

首先需要安装 Arduino IDE(本教程以 1.8.5 版本为例)。接着按照以下步骤添加必需的库:

  1. 安装 Arduino_TensorFlowLite 库:在 Arduino IDE 菜单栏点击 Tools → Manage Libraries,搜索 Arduino_TensorFlowLite,选取 2.4.0-ALPHA 及以上版本,并点击安装按钮。
  2. 安装 Arduino_OV767X 库:同样在库管理器中搜索 Arduino_OV767X 并点击安装。

Processing IDE 安装与库配置

Processing 作为桥梁,负责连接 Arduino 与 Teachable Machine 平台。请下载 Processing 3.X 版,可通过下方链接获取:下载地址

安装完成后,启动 Processing IDE,依次点击 Sketch → Add Library → Manage Libraries,搜索并安装以下两个库:

  • ControlP5
  • Websockets

Teachable Machine 操作步骤

Teachable Machine 内置的嵌入式模型是标准图像分类网络经精简后的迷你版本,专为微控制器(如 Arduino)优化。在开始使用 Teachable Machine 之前,请确保上述硬件和软件设置均已正确完成。

1. 下载并烧录 TMUploader Arduino 程序

首先下载 TMUploader 压缩包,解压后使用 Arduino IDE 打开包含的 .ino 文件。
在工具菜单中选择 Arduino Nano 33 BLE,并正确配置 COM 端口号。
该程序的功能是将 Arduino 摄像头采集到的图像数据发送至 Processing。
代码下载链接:TMUploader 代码库

2. 运行 TMConnector Processing 程序

接下来下载 TMConnector 压缩包,解压后在 Processing IDE 中打开对应的 .pde 文件。点击工具栏上的运行按钮(Play),程序将弹出如下窗口,显示可用的 COM 端口及连接状态:

在窗口列表中选择你的 Arduino 端口(如不确定是哪个,可对照 Arduino IDE 中显示的端口号)。连接成功后,Processing 窗口会实时显示摄像头画面。

温馨提示:若画面停滞或全黑,请检查 OV7670 接线是否牢固且正确。若画面模糊,可尝试旋转摄像头模块前端的镜头环以调节焦距。

3. 在 Teachable Machine 网站中连接设备

访问 Teachable Machine 官网,创建一个新的 Image Project。点击左侧的 Device 选项,再点击 Attempt to connect to device,若所有配置无误,你将看到 OV7670 摄像头传来的实时图像。

收集资料与训练模型

后续步骤与标准的 Teachable Machine 训练流程基本相同:

  • 将摄像头对准待识别的目标物体,点击 Webcam 下的 Add Sample 按钮采集图像(采集的图像尺寸固定为 96×96 灰度图)。
  • 即便你通过 Upload 上传彩色图片,训练后的模型依然只接受单色(灰度)输入。
  • 重要提示:建议使用同一块 OV7670 摄像头模块完成训练与测试(即“原厂考照”原则),以确保模型在实际部署时获得最佳识别效果。

训练过程非常迅速。完成后点击右上角 Export Model 按钮,在弹出的对话框中选择 Tensorflow Lite 选项,并勾选下方的 Tensorflow Lite for Microcontrollers,最后点击 Download myModel 下载模型。

常见问题:如果下载的压缩包文件名并非 converted_tinyml.zip,请检查导出选项是否正确——务必勾选 Tensorflow Lite for Microcontrollers 复选框。

解压后,你会在 converted_tinyml 文件夹中找到模型相关文件:

在 Arduino 上运行模型

首先关闭所有 Processing 程序(因为它们占用了 COM 端口),同时确保 Arduino 仍与电脑保持连接。在 Arduino IDE 中新建项目(或沿用之前的 TMUploader 模板),将导出的模型文件(通常为 model.hmodel_data.cpp)添加到项目文件夹中,并编写用于调用模型进行推理的代码(可参考官方示例)。然后编译并上传至 Arduino 开发板。

上传完成后,打开 Arduino IDE 的 Serial Monitor,你将看到每帧图像的分类结果及其置信度(置信度数值范围为 -128 至 127)。实时分类效果可参考本文引言部分的演示视频。

常见问题与解答

  • Q:为什么 Processing 连接后没有画面?
    A:请检查 OV7670 的接线是否准确,尤其注意时钟线和数据线的连接。同时确认 Processing 中已选择了正确的 COM 端口。
  • Q:训练后的模型文件如何添加到 Arduino 项目?
    A:解压 converted_tinyml.zip 后,将 model.cppmodel.h 文件复制到 Arduino 项目文件夹下,并在主程序中通过 #include "model.h" 包含头文件。
  • Q:为什么 Serial Monitor 显示的置信度总是很低?
    A:可能是训练样本数量不足或物体背景过于复杂。建议为每个分类类别收集至少 50 张图像,并尽可能在与训练时相似的光照环境下进行测试。
  • Q:能否使用 ESP32-CAM 替代?
    A:ESP32-CAM 配合 TensorFlow Lite 也可以实现类似功能,但本教程专门针对 Arduino Nano 33 BLE Sense 编写,请根据实际项目需求选择合适的硬件平台。

附录:完整代码

以下是本教程所涉及的 Arduino 程序(TMUploader)与 Processing 程序(TMConnector)的完整代码,你可以直接复制并用于你的项目中。

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

TMConnector Processing 程序

importprocessing.serial.*;
importja va.nio.ByteBuffer;
importja va.nio.ByteOrder;
importwebsockets.*;
importja vax.xml.bind.DatatypeConverter;
importcontrolP5.*;
importja va.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;
}

至此,你已经完成了所有配置,可以开始使用 Arduino 进行嵌入式视觉分类任务了!请根据具体项目需求选择合适的硬件和软件方案。祝你制作顺利!

来源:https://m.elecfans.com/article/1965072.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。