一张图片,浏览器上传,Linux 端调用图像分类 Brick,秒级返回"它是什么"。
一、项目简介
Image Classification(图像分类) 是 Arduino UNO Q 官方示例中第一个 AI 应用。它做的事情很简单:在浏览器里上传一张 JPG 或 PNG 图片,UNO Q 板子上的 Linux 芯片运行一个轻量级神经网络模型,判断"整张图片最像什么",然后把若干候选类别连同置信度一起返回页面。
和后面要讲的物体检测(Object Detection)不同,图像分类只回答类别、不标位置。它不会在图片上画框,而是告诉你:“这张图有 85% 的概率是猫,10% 是狗,5% 是老虎”。对想快速上手 UNO Q AI 能力的人来说,这是最合适的起点——代码不到 100 行,却把"网页上传 → AI 推理 → 结果回显"这条完整链路走了一遍。
二、硬件准备
| 硬件 | 数量 |
|---|---|
| Arduino UNO Q(或 VENTUNO Q) | ×1 |
| USB-C® 数据线 | ×1 |
整个项目不需要任何外接传感器或摄像头,图片全靠浏览器上传,推理在板子 Linux 端的模型里完成,光板子 + 一根数据线就能跑。
三、系统架构
这个项目只用到 UNO Q 的 Linux 侧,MCU 侧(点阵屏、GPIO)暂时不参与。整体分三层:
| 层级 | 跑在哪 | 干什么 |
|---|---|---|
| 前端 | 浏览器 | 选图、转 base64、发消息、渲染分类结果表格 |
| 后端 | UNO Q Linux 侧 (Python) | 还原图片、调用分类 Brick、把结果推回页面 |
| AI 模型 | image_classification Brick |
真正的神经网络推理,输出类别 + 置信度 |
通信方式:浏览器 ↔ Python 走 WebUI Brick 提供的 WebSocket,双向 JSON 消息。浏览器发一条 classify_image,Python 回一条 classification_result(成功)或 classification_error(失败)。
四、完整代码
4.1 工程配置 —— app.yaml
一个 UNO Q 工程要使用哪些能力,先在 app.yaml 里声明:
bricks:
- arduino:web_ui
- arduino:image_classification
web_ui 负责浏览器交互(网页、WebSocket、REST),image_classification 负责图像分类推理。少了任意一行,运行时就不会加载对应能力,Python 里写了 ImageClassification() 也会报错。
4.2 Python 后端 —— main.py
# SPDX-FileCopyrightText: Copyright (C) Arduino s.r.l. and/or its affiliated companies
#
# SPDX-License-Identifier: MPL-2.0
"""
Image classification backend.
图像分类后端,负责接收浏览器上传的 base64 图片,
转换为 PIL 图片对象,调用图像分类 Brick,并把结果返回网页。
"""
from arduino.app_utils import App
from arduino.app_bricks.web_ui import WebUI
from arduino.app_bricks.image_classification import ImageClassification
from PIL import Image
import io
import base64
import time
image_classification = ImageClassification() # Reuse one classifier instance for incoming requests (复用同一个分类器实例处理请求)
def on_classify_image(client_id, data):
"""
Handle an image classification request from the browser.
处理浏览器发来的图像分类请求。
Args:
client_id: Web UI client identifier (网页客户端标识)
data: Message payload containing image data and confidence (包含图片数据和阈值的消息数据)
Returns:
None
"""
try:
image_data = data.get('image')
image_type_raw = data.get('image_type')
if image_type_raw:
image_type = image_type_raw.split('/')[-1]
else:
image_type = 'jpeg'
confidence = data.get('confidence', 0.25)
if not image_data:
ui.send_message('classification_error', {'error': 'No image data'})
return
# Convert the browser base64 payload into a PIL image for the Brick
# 将浏览器上传的 base64 数据转换为 Brick 可处理的 PIL 图片
image_bytes = base64.b64decode(image_data)
pil_image = Image.open(io.BytesIO(image_bytes))
start_time = time.time() * 1000
results = image_classification.classify(pil_image, image_type=image_type, confidence=confidence)
diff = time.time() * 1000 - start_time
if results is None:
ui.send_message('classification_error', {'error': 'No results returned'})
return
# Keep the response compact so the frontend can render it directly
# 返回紧凑结构,便于前端直接渲染结果表格
response = {
'success': True,
'results': results,
'processing_time': f"{diff:.2f} ms"
}
ui.send_message('classification_result', response)
except Exception as e:
ui.send_message('classification_error', {'error': str(e)})
ui = WebUI()
ui.on_message('classify_image', on_classify_image)
App.run()
关键代码说明
| 代码 | 干了什么 |
|---|---|
image_classification = ImageClassification() |
在全局创建一个分类器实例,所有请求复用,避免每次上传都重新加载模型 |
data.get('image_type') + split('/')[-1] |
容错处理:前端可能传 image/jpeg 这种 MIME 类型,取 / 后面的 jpeg;没传就用默认值 |
data.get('confidence', 0.25) |
置信度阈值,前端不传时默认 0.25 |
base64.b64decode(image_data) |
把 base64 字符串还原成原始图片字节 |
Image.open(io.BytesIO(image_bytes)) |
把字节包装成流,交给 PIL 解析成可处理的图片对象 |
image_classification.classify(...) |
真正的 AI 推理,返回分类结果字典 |
time.time() * 1000 |
计时起点,乘 1000 转成毫秒 |
ui.on_message('classify_image', ...) |
注册消息:浏览器发 classify_image 时,自动调用这个函数 |
ui.send_message('classification_result', ...) |
把结果推回浏览器,前端据此渲染结果表格 |
except Exception as e |
兜底:任何一步出错,把错误信息发回前端,而不是让程序崩溃 |
五、核心机制解析
5.1 图片是怎么从浏览器"跑"到模型里的
这是整段代码最值得琢磨的地方。浏览器上传图片时,为了能在 WebSocket(文本通道)里传输二进制数据,前端会先把图片转成 base64 字符串——一种用 64 个可打印字符表示任意二进制数据的编码方式。
所以 Python 收到的不是一个文件,而是一大串文本。要交给模型,必须先做两步还原:
base64 字符串 ──b64decode()──> 原始字节 ──Image.open()──> PIL 图片对象
base64.b64decode():把文本还原成字节流;Image.open(io.BytesIO(...)):字节流不能直接给 PIL,先包一层io.BytesIO变成"内存里的文件",PIL 才能识别和解析。
到这一步,图片才从"网页里的数据"变成"Python 里可处理的图像对象",模型才吃得下。
5.2 confidence 阈值不是"准确率"
confidence(置信度)是分类结果里最常见的概念,但它不是模型的准确率,而是一个过滤门槛:
| 阈值 | 结果 | 感受 |
|---|---|---|
| 0.2 | 候选很多 | 可能混入不可靠的分类 |
| 0.5 | 候选适中 | 主流选择 |
| 0.8 | 结果很少 | 只保留模型非常确信的类别 |
阈值越高,返回的类别越少、越"干净",但也可能把正确但置信度略低的答案一并过滤掉。可以拿同一张图分别设 0.2 / 0.5 / 0.8 试一次,观察结果数量变化,很快就能建立体感。
5.3 为什么只创建一次分类器
image_classification = ImageClassification()
这行写在函数外面(全局作用域),意味着分类器在程序启动时只创建一次,之后所有请求都复用同一个实例。原因是神经网络模型的加载和初始化很耗时,如果每次上传图片都新建一个实例,用户每点一次"运行分类"都要重新加载模型,体验会很差。放在全局,加载一次、用到底。
5.4 一条消息进来,两种消息出去
浏览器和 Python 之间用消息名区分不同事件:
| 方向 | 消息名 | 含义 |
|---|---|---|
| 浏览器 → Python | classify_image |
请求分类一张图片 |
| Python → 浏览器 | classification_result |
分类成功,带回结果 |
| Python → 浏览器 | classification_error |
出错,带回错误信息 |
成功和失败用不同的消息名,前端可以分别处理:收到 classification_result 就渲染结果表格,收到 classification_error 就弹错误提示。这种"按事件类型路由"的设计,是 WebSocket 应用的常见做法。
六、运行步骤
- 在 Arduino App Lab 里打开 Image Classification 示例(或导入
image_classification.zip工程) - 点右上角 Run
- 浏览器打开页面:
- USB 有线连接并本机访问 →
http://127.0.0.1:7000 - 局域网访问 →
http://<UNO-Q-IP-ADDRESS>:7000
- USB 有线连接并本机访问 →
- 上传一张 JPG / PNG 图片
- 调整 confidence 阈值
- 点 Run classification,查看分类结果和处理耗时
七、自定义修改指南
想调行为和结果?动 main.py 里这几处就行:
| 参数 | 默认值 | 改法 | 效果 |
|---|---|---|---|
confidence 默认值 |
0.25 | 改成 0.5 或 0.8 | 前端不传阈值时,结果更保守 |
image_type 默认值 |
'jpeg' |
改成 'png' |
前端不传格式时按此处理 |
| 错误提示文案 | 'No image data' |
自定义字符串 | 前端展示更友好的提示 |
想在这个基础上扩展也很自然——比如把分类结果通过 Bridge 发给 MCU 侧,让点阵屏显示"识别到的类别"对应的图案,就能做出一个"看图亮灯"的联动项目。这正是第 5.6 节物体检测和第 6 章综合实践继续深入的方向。
八、总结
Image Classification 代码量很小,但一条完整的 AI 应用链路已经搭起来了:
| 技术点 | 实现方式 |
|---|---|
| 图片传输 | 前端转 base64,WebSocket 文本通道传递 |
| 图片还原 | base64.b64decode() + Image.open() |
| AI 推理 | image_classification.classify(pil_image, image_type, confidence) |
| 性能优化 | 全局单例复用分类器,避免重复加载模型 |
| 结果过滤 | confidence 阈值(非准确率,是门槛) |
| 消息路由 | classify_image / classification_result / classification_error |
| 计时 | time.time() * 1000 毫秒级统计处理耗时 |
| 健壮性 | 空数据、空结果、异常三层防御 |
对想入门 Arduino UNO Q AI 能力 的朋友来说,这是一个门槛低、链路完整、可扩展性强的起点案例。

