07.Arduino UNO Q 图像分类——浏览器上传图片与 AI 识别

一张图片,浏览器上传,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()

:magnifying_glass_tilted_left: 关键代码说明

代码 干了什么
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 应用的常见做法。


六、运行步骤

  1. 在 Arduino App Lab 里打开 Image Classification 示例(或导入 image_classification.zip 工程)
  2. 点右上角 Run
  3. 浏览器打开页面:
    • USB 有线连接并本机访问 → http://127.0.0.1:7000
    • 局域网访问 → http://<UNO-Q-IP-ADDRESS>:7000
  4. 上传一张 JPG / PNG 图片
  5. 调整 confidence 阈值
  6. 点 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 能力 的朋友来说,这是一个门槛低、链路完整、可扩展性强的起点案例。

1 个赞