【Arduino UNO Q】 边缘 AI 视觉部署方案:手写数字识别
本文介绍了 Arduino UNO Q 开发板结合 OpenCV 和 MNIST 数据集实现手写数字识别的项目设计。
项目介绍
Arduino UNO Q 开发板结合 OpenCV 与 MNIST 数据集实现手写数字识别。
- 准备工作:硬件连接、软件更新、环境搭建、模型下载等;
- MNIST:使用 OpenCV 和 MNIST 数据集实现手写数字识别;
- 网页设计:设计 Web 手写数字面板,实现网页端实时手写数字识别。
准备工作
包括硬件连接、系统安装、软件更新等。
详见:Arduino UNO Q 介绍、环境搭建、工程测试 .
硬件连接
这里采用 SSH 远程控制,使用 Type-C 数据线供电并 WiFi 联网即可。

软件更新
更新软件包
bash
sudo apt update
sudo apt upgrade
环境搭建
-
安装 OpenCV 和 opencv-data 软件包;
bashsudo apt install python3-opencv sudo apt install opencv-data -
执行如下指令验证
bash
python3 -c "import cv2,sys;print(cv2.__version__,sys.version)"
终端打印 opencv 版本号。
模型下载
下载预训练的 MNIST 数字识别 ./model/mnist-12.onnx 模型
bash
wget https://github.com/onnx/models/blob/main/validated/vision/classification/mnist/model/mnist-12.onnx
详见:mnist | github .
MNIST
使用 OpenCV 自带的 DNN 工具和 MNIST 数据集预训练模型,加载目标手写数字图片并打印识别结果。
流程图
开始
读取图片
加载模型
检测数字
打印识别结果
弹窗显示
按键退出
结束
代码
终端执行 touch mnist_demo.py 新建程序文件,添加如下代码
python
#!/usr/bin/env python3
import cv2, numpy as np, time, sys
model = './model/mnist-12.onnx'
img_p = sys.argv[1] if len(sys.argv) > 1 else './img/num2.jpg'
# ---------- 载入模型 ----------
net = cv2.dnn.readNetFromONNX(model)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
# ---------- 读图并预处理 ----------
img_bgr = cv2.imread(img_p)
if img_bgr is None:
raise FileNotFoundError(img_p)
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) # 用于预处理
img_gray = cv2.bitwise_not(img_gray)
img28 = cv2.resize(img_gray, (28, 28))
blob = img28.astype(np.float32) / 255.0
blob = blob.reshape(1, 1, 28, 28)
# ---------- 推理 ----------
t0 = time.time()
net.setInput(blob)
logits = net.forward().squeeze()
exp = np.exp(logits - np.max(logits))
pred = exp / np.sum(exp) # shape (10,)
digit = int(np.argmax(pred))
confidence = float(pred[digit])
cost = (time.time() - t0) * 1000
print(f'识别结果: {digit} (置信度: {confidence:.3f}) 耗时: {cost:.1f} ms')
# ---------- 可视化 ----------
show = img_bgr.copy()
# 阈值(白字黑底)
_, bin_img = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 数字轮廓
cnts, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if cnts:
# 取最大轮廓(防噪点)
cnt = max(cnts, key=cv2.contourArea)
x, y, w_box, h_box = cv2.boundingRect(cnt)
# 画框
cv2.rectangle(show, (x, y), (x + w_box, y + h_box), (0, 255, 0), 3)
# ---------- 标签 ----------
text = f'{digit}'
font, scale, thick = cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
(tw, th), _ = cv2.getTextSize(text, font, scale, thick)
cv2.rectangle(show, (x, y - th - 6), (x + tw + 4, y), (0, 255, 0), cv2.FILLED)
cv2.putText(show, text, (x + 2, y - 4), font, scale, (0, 0, 255), thick, cv2.LINE_AA)
cv2.namedWindow("MNIST", cv2.WINDOW_NORMAL)
cv2.imshow('MNIST', show)
cv2.waitKey(0)
保存代码。
效果
-
终端运行指令
python3 mnist_demo.py --image_path ./img/nums.jpg,打印结果
-
弹窗显示结果,按任意键关闭窗口;
效果如下


网页设计
包括前端网页和后端 flask 服务器设计。
代码
终端执行 touch mnist_web.py 新建程序文件,并添加如下代码
python
#!/usr/bin/env python3
"""
手写数字识别网页服务器
- 模型:mnist-12.onnx
- 推理:OpenCV-DNN(CPU)
"""
import cv2, numpy as np, base64, time
from flask import Flask, render_template_string, request, jsonify
app = Flask(__name__)
MODEL = './model/mnist-12.onnx'
net = cv2.dnn.readNetFromONNX(MODEL)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
# confidence
def softmax(x):
x = x - np.max(x)
return np.exp(x) / np.sum(np.exp(x))
# 解码前端画布 base64
def decode_canvas(data_url):
header, encoded = data_url.split(',', 1)
img = cv2.imdecode(np.frombuffer(base64.b64decode(encoded), np.uint8),
cv2.IMREAD_GRAYSCALE)
return img
@app.route('/')
def index():
with open('./web/index.html') as f:
return render_template_string(f.read())
@app.route('/predict', methods=['POST'])
def predict():
t0 = time.time()
img = decode_canvas(request.json['image'])
img = cv2.bitwise_not(img)
img = cv2.resize(img, (28, 28))
blob = cv2.dnn.blobFromImage(img, 1/255.0, (28, 28), swapRB=False)
# 发送至 Web
net.setInput(blob)
logits = net.forward()[0, :]
prob = softmax(logits)
digit = int(np.argmax(prob))
conf = float(prob[digit])
cost = (time.time() - t0) * 1000
return jsonify({'digit': digit, 'conf': round(conf, 3), 'time': round(cost, 1)})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=False)
保存代码。
网页设计
新建网页文件 ./web/index.html 添加代码如下
python
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Arduino UNO Q MNIST</title>
<style>
body{font-family:Arial,Helvetica,sans-serif;text-align:center;background:#f2f2f2}
canvas{border:1px solid #999;display:block;margin:10px auto;background:#fff}
button{font-size:18px;margin:6px;padding:6px 14px}
#res{color:#d44;font-size:20px;font-weight:bold}
</style>
</head>
<body>
<h2>Arduino UNO Q MNIST</h2>
<canvas id="c" width="280" height="280"></canvas>
<div>
<button id="clear">Clear</button>
<button id="go">Recognize</button>
</div>
<p id="res">Waiting...</p>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
ctx.fillStyle='#fff'; ctx.fillRect(0,0,280,280);
ctx.lineWidth=20; ctx.lineCap='round'; ctx.strokeStyle='#000';
let drawing=false;
function pos(e){const r=canvas.getBoundingClientRect();return [e.clientX-r.left,e.clientY-r.top];}
canvas.onmousedown=e=>{drawing=true;ctx.beginPath();ctx.moveTo(...pos(e));};
canvas.onmousemove=e=>{if(drawing){ctx.lineTo(...pos(e));ctx.stroke();}};
canvas.onmouseup=()=>drawing=false;
document.getElementById('clear').onclick=()=>{
ctx.fillStyle='#fff';ctx.fillRect(0,0,280,280);document.getElementById('res').textContent='Waiting...';
};
document.getElementById('go').onclick=async()=>{
const tmp=document.createElement('canvas');
tmp.width=tmp.height=28;
const tctx=tmp.getContext('2d');
tctx.drawImage(canvas,0,0,28,28);
const dataURL=tmp.toDataURL('image/png');
const r=await fetch('/predict',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({image:dataURL})
}).then(r=>r.json());
document.getElementById('res').textContent=
`Result:${r.digit} (Confidence: ${(r.conf*100).toFixed(1)}%) Time: ${r.time} ms`;
};
</script>
</body>
</html>
保存代码。
效果
终端执行指令 python3 mnist_web.py 输出网页服务器地址等信息

浏览器中输入网址 http://192.168.31.109:8080 打开数字识别网页界面;
- 使用鼠标或触摸屏在空白面板上手写数字;
- 点击
Recognize按钮,下方立即显示识别结果; - 点击
Clear按钮清空手写板;

更多测试效果



总结
本文介绍了 Arduino UNO Q 开发板结合 OpenCV 与 MNIST 数据集实现手写数字识别,包括OpenCV 部署、流程图、关键代码和工程测试,为相关产品在边缘 AI 视觉应用的快速开发设计提供了参考。