当前位置: 首页 > news >正文

Beetle 树莓派RP2350 - 数字识别

Beetle 树莓派RP2350 - 数字识别

本文介绍了 DFRobot Beetle RP2350 开发板结合 MNIST 数据集和串口通信,实现网页手写数字识别的项目设计。

项目介绍

  • 准备工作:MNIST 数据集和Python库安装等;
  • 模型训练:通过电脑主机训练 MNIST 数据集,获取轻量化权重;
  • 工程代码:包括板端执行代码和网页设计代码;
  • 效果演示:运行程序和网页,完成手写数字识别效果演示。

准备工作

  • 下载 MNIST 数据集文件:MNIST数据集 - GitCode .
  • 电脑主机安装 Python 软件,安装推理所需扩展库,如 numpy ;
  • 开发板上传 *.uf2 固件,用于 MicroPython 开发;

硬件连接

示意图

在这里插入图片描述

RP2350USB to TTLNote
TXD (Pin0)RXDTransmite
RXD (Pin1)TXDReceive
GNDGNDGround

实物图

在这里插入图片描述

模型训练

  • 创建 train_tiny_model.py 文件,添加如下代码
# train_tiny_model.py
import numpy as np
import json
import osdef load_mnist_local():"""直接加载本地的MNIST文件"""# 检查文件是否存在train_images_file = 'train-images.idx3-ubyte'train_labels_file = 'train-labels.idx1-ubyte'test_images_file = 't10k-images.idx3-ubyte'test_labels_file = 't10k-labels.idx1-ubyte'files = [train_images_file, train_labels_file, test_images_file, test_labels_file]missing_files = [f for f in files if not os.path.exists(f)]if missing_files:print(f"❌ 缺少文件: {missing_files}")print("请确保以下文件在当前目录:")print("- train-images.idx3-ubyte")print("- train-labels.idx1-ubyte") print("- t10k-images.idx3-ubyte")print("- t10k-labels.idx1-ubyte")return Noneprint("✅ 找到所有MNIST文件,开始加载...")try:# 加载训练图像with open(train_images_file, 'rb') as f:magic = int.from_bytes(f.read(4), 'big')num_images = int.from_bytes(f.read(4), 'big')rows = int.from_bytes(f.read(4), 'big')cols = int.from_bytes(f.read(4), 'big')x_train = np.frombuffer(f.read(), dtype=np.uint8).reshape(num_images, rows, cols)print(f"训练图像: {x_train.shape}")# 加载训练标签with open(train_labels_file, 'rb') as f:magic = int.from_bytes(f.read(4), 'big')num_labels = int.from_bytes(f.read(4), 'big')y_train = np.frombuffer(f.read(), dtype=np.uint8)print(f"训练标签: {y_train.shape}")# 加载测试图像with open(test_images_file, 'rb') as f:magic = int.from_bytes(f.read(4), 'big')num_images = int.from_bytes(f.read(4), 'big')rows = int.from_bytes(f.read(4), 'big')cols = int.from_bytes(f.read(4), 'big')x_test = np.frombuffer(f.read(), dtype=np.uint8).reshape(num_images, rows, cols)print(f"测试图像: {x_test.shape}")# 加载测试标签with open(test_labels_file, 'rb') as f:magic = int.from_bytes(f.read(4), 'big')num_labels = int.from_bytes(f.read(4), 'big')y_test = np.frombuffer(f.read(), dtype=np.uint8)print(f"测试标签: {y_test.shape}")return (x_train, y_train), (x_test, y_test)except Exception as e:print(f"❌ 文件加载错误: {e}")return Noneclass TinyNN:def __init__(self, input_size=784, hidden_size=8, output_size=10):# 极简模型: 784 -> 8 -> 10np.random.seed(42)self.w1 = np.random.randn(input_size, hidden_size) * 0.1self.b1 = np.zeros(hidden_size)self.w2 = np.random.randn(hidden_size, output_size) * 0.1self.b2 = np.zeros(output_size)def relu(self, x):return np.maximum(0, x)def softmax(self, x):exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))return exp_x / np.sum(exp_x, axis=1, keepdims=True)def forward(self, x):self.layer1 = x @ self.w1 + self.b1self.layer1_act = self.relu(self.layer1)self.output = self.layer1_act @ self.w2 + self.b2return self.softmax(self.output)def predict(self, x):probabilities = self.forward(x)return np.argmax(probabilities, axis=1)def train(self, x, y, learning_rate=0.1, epochs=5):num_samples = x.shape[0]for epoch in range(epochs):# 随机打乱indices = np.random.permutation(len(x))x_shuffled = x[indices]y_shuffled = y[indices]total_loss = 0batches = 0for i in range(0, len(x_shuffled), 128):batch_x = x_shuffled[i:i+128]batch_y = y_shuffled[i:i+128]# 前向传播output = self.forward(batch_x)loss = -np.log(output[np.arange(len(batch_y)), batch_y])total_loss += np.mean(loss)# 简化反向传播output_error = output.copy()output_error[np.arange(len(batch_y)), batch_y] -= 1output_error /= len(batch_y)grad_w2 = self.layer1_act.T @ output_errorgrad_b2 = np.sum(output_error, axis=0)hidden_error = (output_error @ self.w2.T) * (self.layer1_act > 0)grad_w1 = batch_x.T @ hidden_errorgrad_b1 = np.sum(hidden_error, axis=0)self.w2 -= learning_rate * grad_w2self.b2 -= learning_rate * grad_b2self.w1 -= learning_rate * grad_w1self.b1 -= learning_rate * grad_b1batches += 1# 计算精度train_pred = self.predict(x[:1000])train_acc = np.mean(train_pred == y[:1000])test_pred = self.predict(x_test[:1000])test_acc = np.mean(test_pred == y_test[:1000])avg_loss = total_loss / batchesprint(f"Epoch {epoch+1}/{epochs}, 损失: {avg_loss:.4f}, 训练精度: {train_acc:.3f}, 测试精度: {test_acc:.3f}")def save_tiny_weights(model, filename='weights_tiny.json'):"""保存极简模型权重"""# 直接使用列表格式,不保留小数位weights = {'w1': [[round(float(x), 4) for x in row] for row in model.w1.tolist()],'b1': [round(float(x), 4) for x in model.b1.tolist()],'w2': [[round(float(x), 4) for x in row] for row in model.w2.tolist()],'b2': [round(float(x), 4) for x in model.b2.tolist()]}with open(filename, 'w') as f:json.dump(weights, f, separators=(',', ':'))  # 最小化JSON格式size = os.path.getsize(filename)total_params = model.w1.size + model.b1.size + model.w2.size + model.b2.sizeprint(f"✅ 极简模型权重保存到 {filename}")print(f"📊 参数总量: {total_params}")print(f"💾 文件大小: {size} 字节")if __name__ == "__main__":print("=== 训练极简模型 (784->8->10) ===")data = load_mnist_local()if data is None:exit()(x_train, y_train), (x_test, y_test) = datax_train = x_train.reshape(-1, 784).astype(np.float32) / 255.0x_test = x_test.reshape(-1, 784).astype(np.float32) / 255.0# 创建极简模型model = TinyNN(hidden_size=8)  # 只有8个隐藏神经元!print("🚀 开始训练极简模型...")model.train(x_train, y_train, epochs=5)# 最终测试test_pred = []for i in range(0, len(x_test), 100):batch_pred = model.predict(x_test[i:i+100])test_pred.extend(batch_pred)final_acc = np.mean(np.array(test_pred) == y_test[:len(test_pred)])print(f"🎯 最终测试精度: {final_acc:.3f}")# 保存权重save_tiny_weights(model)
  • 保存代码;

  • 终端执行 python train_tiny_model.py 指令,运行程序;

    在这里插入图片描述

  • 生成大小为 48KB 的 weight_tiny.json 权重文件。

工程代码

Thonny IDE 新建文件,添加如下代码

# main.py
import json
import math
import time
from machine import UART, Pinclass FixedTinyNN:def __init__(self, weights_file='weights_tiny.json'):self.load_weights(weights_file)def load_weights(self, filename):try:with open(filename, 'r') as f:weights = json.load(f)self.w1 = weights['w1']self.b1 = weights['b1']self.w2 = weights['w2']self.b2 = weights['b2']print("✅ 权重加载成功")except Exception as e:print(f"❌ 权重加载失败: {e}")self._create_backup_weights()def _create_backup_weights(self):# 简化的备份权重self.w1 = [[0.01] * 8 for _ in range(784)]self.b1 = [0.0] * 8self.w2 = [[0.1 if j == i % 8 else 0.0 for i in range(10)] for j in range(8)]self.b2 = [0.0] * 10print("🔄 使用备用权重")def predict(self, pixels):"""预测手写数字"""if len(pixels) != 784:return 0, [0.1] * 10# 像素归一化normalized = [p / 255.0 for p in pixels]# 第一层: 784 -> 8hidden = [0.0] * 8for j in range(8):total = 0.0for i in range(784):total += normalized[i] * self.w1[i][j]hidden[j] = max(0.0, total + self.b1[j])# 第二层: 8 -> 10output = [0.0] * 10for k in range(10):total = 0.0for j in range(8):total += hidden[j] * self.w2[j][k]output[k] = total + self.b2[k]# Softmaxmax_val = max(output)exp_sum = sum(math.exp(o - max_val) for o in output)probabilities = [math.exp(o - max_val) / exp_sum for o in output]prediction = probabilities.index(max(probabilities))return prediction, probabilitiesdef safe_int_convert(value, default=0):"""安全地将值转换为整数"""try:# 移除可能的空白字符和非数字字符cleaned = value.strip()if not cleaned:return defaultreturn int(cleaned)except (ValueError, TypeError):return defaultdef parse_pixel_data(data_line):"""安全地解析像素数据"""pixels = []# 移除可能的"pixels:"前缀if data_line.startswith('pixels:'):data_line = data_line[7:]# 分割数据parts = data_line.split(',')for part in parts:pixel_value = safe_int_convert(part)pixels.append(pixel_value)# 如果已经收集了784个像素,就停止if len(pixels) >= 784:break# 如果像素数量不足,用0填充while len(pixels) < 784:pixels.append(0)return pixels[:784]  # 确保正好784个像素def main():# 初始化UARTuart = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))uart.init(baudrate=115200, bits=8, parity=None, stop=1, timeout=100)# 初始化模型model = FixedTinyNN('weights_tiny.json')print("🎯 修复版数字识别系统就绪")print("📡 等待UART数据输入...")buffer = ""error_count = 0success_count = 0while True:try:# 读取UART数据if uart.any():data = uart.read()if data:try:buffer += data.decode('utf-8')except UnicodeDecodeError:# 处理解码错误buffer = ""error_count += 1if error_count % 10 == 0:print(f"⚠️  解码错误计数: {error_count}")continue# 处理完整的行while '\n' in buffer:line_end = buffer.find('\n')line = buffer[:line_end].strip()buffer = buffer[line_end + 1:]if not line:continueprint(f"📨 收到数据: {line[:60]}{'...' if len(line) > 60 else ''}")# 检查是否为像素数据(包含逗号)if ',' in line:try:# 安全解析像素数据pixels = parse_pixel_data(line)if len(pixels) == 784:# 进行预测start_time = time.ticks_ms()prediction, probabilities = model.predict(pixels)confidence = max(probabilities)inference_time = time.ticks_diff(time.ticks_ms(), start_time)# 发送结果result_lines = [f"识别结果: 数字 {prediction}",f"置信度: {confidence:.1%}",f"推理时间: {inference_time}ms",f"RESULT:{prediction}:{confidence:.3f}"]for result_line in result_lines:uart.write(result_line + '\n')success_count += 1print(f"✅ 识别成功: 数字{prediction}, 置信度{confidence:.1%}, 时间{inference_time}ms")print(f"📊 成功计数: {success_count}")else:uart.write(f"错误: 像素数量不正确 ({len(pixels)}/784)\n")print(f"❌ 像素数量错误: {len(pixels)}/784")except Exception as e:error_count += 1uart.write(f"错误: 数据处理失败 - {str(e)[:50]}\n")print(f"❌ 处理错误 #{error_count}: {e}")# 处理命令elif line == "test":# 测试命令test_pixels = [0] * 784# 创建一个简单的数字1测试图像for i in range(28):test_pixels[i * 28 + 12] = 200test_pixels[i * 28 + 13] = 255test_pixels[i * 28 + 14] = 200prediction, probabilities = model.predict(test_pixels)confidence = max(probabilities)uart.write(f"测试结果: 数字 {prediction} (应该为1)\n")uart.write(f"测试置信度: {confidence:.1%}\n")print(f"🧪 测试完成: 数字{prediction}, 置信度{confidence:.1%}")elif line == "info":# 信息命令info_lines = ["系统信息:","- 模型: 极简神经网络 (784->8->10)","- 通信: UART0 (GPIO0/1)","- 波特率: 115200",f"- 成功识别: {success_count} 次",f"- 错误计数: {error_count} 次"]for info_line in info_lines:uart.write(info_line + '\n')elif line == "reset":# 重置计数success_count = 0error_count = 0uart.write("计数器已重置\n")print("🔄 计数器重置")elif line in ["help", "?"]:# 帮助命令help_lines = ["可用命令:","- 像素数据: 784个逗号分隔的数字 (0-255)","- 'test': 运行测试识别","- 'info': 显示系统信息", "- 'reset': 重置计数器","- 'help': 显示此帮助"]for help_line in help_lines:uart.write(help_line + '\n')else:uart.write(f"未知命令: {line}\n")uart.write("输入 'help' 查看可用命令\n")except KeyboardInterrupt:print("\n👋 程序退出")breakexcept Exception as e:error_count += 1print(f"💥 系统错误 #{error_count}: {e}")time.sleep(1)  # 错误时暂停一下# 短暂休眠以减少CPU使用time.sleep(0.01)if __name__ == "__main__":main()

保存代码。

网页设计

电脑端新建 index.html 文件,添加如下代码

<!DOCTYPE html>
<html>
<head><title>手写数字识别</title><meta charset="utf-8"><style>body {font-family: Arial, sans-serif;max-width: 800px;margin: 0 auto;padding: 20px;background-color: #f5f5f5;}.container {background: white;padding: 20px;border-radius: 10px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}h1 {color: #333;text-align: center;}.canvas-container {text-align: center;margin: 20px 0;}canvas {border: 2px solid #333;border-radius: 5px;cursor: crosshair;background-color: black;}.controls {text-align: center;margin: 20px 0;}button {background-color: #4CAF50;border: none;color: white;padding: 10px 20px;text-align: center;text-decoration: none;display: inline-block;font-size: 16px;margin: 4px 2px;cursor: pointer;border-radius: 5px;transition: background-color 0.3s;}button:hover {background-color: #45a049;}button.clear {background-color: #f44336;}button.clear:hover {background-color: #da190b;}button.connect {background-color: #2196F3;}button.connect:hover {background-color: #1976D2;}.result {margin-top: 20px;padding: 20px;border-radius: 10px;background: linear-gradient(135deg, #e8f5e8, #c8e6c9);border-left: 5px solid #4CAF50;text-align: center;}.result.hidden {display: none;}.status {padding: 12px;margin: 10px 0;border-radius: 5px;background-color: #e3f2fd;border-left: 4px solid #2196F3;}.status.error {background-color: #ffebee;border-left-color: #f44336;}.log {margin-top: 20px;padding: 15px;background-color: #fafafa;border-radius: 5px;max-height: 250px;overflow-y: auto;font-family: 'Courier New', monospace;font-size: 13px;border: 1px solid #ddd;}.prediction-number {font-size: 72px;font-weight: bold;color: #2e7d32;margin: 20px 0;text-shadow: 2px 2px 4px rgba(0,0,0,0.1);}.confidence {font-size: 20px;color: #555;margin: 10px 0;}.details {font-size: 14px;color: #777;margin-top: 10px;}</style>
</head>
<body><div class="container"><h1>🖊️ 手写数字识别</h1><p style="text-align: center; color: #666;">网页版 | 串口通信 | 神经网络</p><div class="canvas-container"><canvas id="canvas" width="280" height="280"></canvas></div><div class="controls"><button onclick="clearCanvas()">🧹 清除画布</button><button onclick="recognize()">🔍 识别数字</button><button onclick="runTest()">🧪 运行测试</button><button class="connect" onclick="connectSerial()">🔌 连接串口</button><button onclick="clearLog()">📋 清除日志</button></div><div id="status" class="status">⚡ 请点击"连接串口"开始使用</div><div id="result" class="result hidden"><h3>🎯 识别结果</h3><div class="prediction-number" id="prediction">?</div><div class="confidence" id="confidence">置信度: 计算中...</div><div class="details" id="details">简化神经网络 (784→8→10)</div></div><div class="log"><div style="display: flex; justify-content: between; align-items: center;"><strong>📊 通信日志</strong><span id="stats" style="font-size: 12px; color: #666;">成功: 0 | 失败: 0</span></div><div id="log" style="margin-top: 10px;"></div></div></div><script>// 全局变量const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');let isDrawing = false;let port = null;let writer = null;let reader = null;let serialBuffer = '';let successCount = 0;let errorCount = 0;// 初始化画布function initCanvas() {ctx.fillStyle = 'black';ctx.fillRect(0, 0, canvas.width, canvas.height);ctx.strokeStyle = 'white';ctx.lineWidth = 15;ctx.lineCap = 'round';ctx.lineJoin = 'round';}// 绘图功能canvas.addEventListener('mousedown', startDrawing);canvas.addEventListener('mousemove', draw);canvas.addEventListener('mouseup', stopDrawing);canvas.addEventListener('mouseout', stopDrawing);// 触摸屏支持canvas.addEventListener('touchstart', handleTouch);canvas.addEventListener('touchmove', handleTouch);canvas.addEventListener('touchend', stopDrawing);function handleTouch(e) {e.preventDefault();if (e.touches.length > 0) {const touch = e.touches[0];const mouseEvent = new MouseEvent(e.type === 'touchstart' ? 'mousedown' : 'mousemove', {clientX: touch.clientX,clientY: touch.clientY});canvas.dispatchEvent(mouseEvent);}}function startDrawing(e) {isDrawing = true;draw(e);}function draw(e) {if (!isDrawing) return;const rect = canvas.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;ctx.lineTo(x, y);ctx.stroke();ctx.beginPath();ctx.moveTo(x, y);}function stopDrawing() {isDrawing = false;ctx.beginPath();}function clearCanvas() {initCanvas();hideResult();addLog("画布已清除");}function clearLog() {document.getElementById('log').innerHTML = '';addLog("日志已清除");}// 串口连接async function connectSerial() {try {if (!('serial' in navigator)) {updateStatus('❌ 浏览器不支持Web Serial API', true);return;}updateStatus('🔌 正在连接串口...');port = await navigator.serial.requestPort();await port.open({ baudRate: 115200,dataBits: 8,stopBits: 1,parity: 'none'});writer = port.writable.getWriter();reader = port.readable.getReader();updateStatus('✅ 串口连接成功! 请在画布上手写数字并点击识别');addLog("串口连接已建立", "success");// 开始读取数据readSerialData();} catch (error) {updateStatus(`❌ 连接失败: ${error}`, true);addLog(`连接错误: ${error}`, "error");}}// 读取串口数据(改进版本)async function readSerialData() {try {while (port.readable) {const { value, done } = await reader.read();if (done) {break;}if (value) {const text = new TextDecoder().decode(value);serialBuffer += text;processSerialBuffer();}}} catch (error) {if (error.name !== 'InterruptedError') {updateStatus(`❌ 读取错误: ${error}`, true);addLog(`读取错误: ${error}`, "error");errorCount++;updateStats();}}}// 处理串口缓冲区(改进版本)function processSerialBuffer() {const lines = serialBuffer.split('\n');// 保留最后一行(可能不完整)serialBuffer = lines.pop() || '';for (let line of lines) {line = line.trim();if (!line) continue;addLog(`${line}`, "receive");processReceivedLine(line);}}// 处理接收到的每一行数据(改进版本)function processReceivedLine(line) {// 优先处理RESULT格式if (line.startsWith('RESULT:')) {const parts = line.split(':');if (parts.length >= 3) {const prediction = parts[1];const confidence = parseFloat(parts[2]);if (!isNaN(confidence)) {showResult(prediction, confidence);successCount++;updateStats();return;}}}// 处理其他格式的结果if (line.includes('识别结果:') || line.includes('数字:')) {const match = line.match(/[数字:\s]*(\d)/);if (match) {const prediction = match[1];// 查找置信度const confidenceMatch = line.match(/置信度[:\s]*([\d.]+)/);const confidence = confidenceMatch ? parseFloat(confidenceMatch[1]) : 0.8;showResult(prediction, confidence);successCount++;updateStats();return;}}// 测试结果if (line.includes('测试结果:')) {updateStatus(line);return;}// 错误信息if (line.includes('错误:')) {updateStatus(line, true);errorCount++;updateStats();return;}// 其他信息性消息if (line.includes('识别完成') || line.includes('测试完成')) {updateStatus(line);}}// 识别数字async function recognize() {if (!writer) {updateStatus('❌ 请先连接串口', true);return;}const imageData = preprocessImage();if (!imageData) {updateStatus('❌ 请先在画布上手写数字', true);return;}updateStatus('🔄 正在识别中,请稍候...');showLoading();try {addLog(`→ 发送像素数据 (784个值)`, "send");await writer.write(new TextEncoder().encode(imageData + '\n'));} catch (error) {updateStatus(`❌ 发送失败: ${error}`, true);addLog(`发送错误: ${error}`, "error");errorCount++;updateStats();hideLoading();}}// 运行测试async function runTest() {if (!writer) {updateStatus('❌ 请先连接串口', true);return;}updateStatus('🧪 运行测试中...');try {addLog('→ 发送测试命令', "send");await writer.write(new TextEncoder().encode('test\n'));} catch (error) {updateStatus(`❌ 测试发送失败: ${error}`, true);addLog(`测试发送错误: ${error}`, "error");errorCount++;updateStats();}}// 图像预处理function preprocessImage() {const tempCanvas = document.createElement('canvas');const tempCtx = tempCanvas.getContext('2d');tempCanvas.width = 28;tempCanvas.height = 28;// 缩放并转换为灰度tempCtx.drawImage(canvas, 0, 0, 28, 28);const imageData = tempCtx.getImageData(0, 0, 28, 28);// 提取亮度值const pixels = [];for (let i = 0; i < imageData.data.length; i += 4) {const brightness = imageData.data[i]; // R通道pixels.push(brightness);}// 检查是否真的有内容const hasContent = pixels.some(pixel => pixel > 10);if (!hasContent) {return null;}return pixels.join(',');}// 显示结果(改进版本)function showResult(prediction, confidence) {const resultDiv = document.getElementById('result');const predictionDiv = document.getElementById('prediction');const confidenceDiv = document.getElementById('confidence');const detailsDiv = document.getElementById('details');// 更新显示内容predictionDiv.textContent = prediction;confidenceDiv.textContent = `置信度: ${(confidence * 100).toFixed(1)}%`;detailsDiv.textContent = `识别时间: ${new Date().toLocaleTimeString()}`;// 显示结果区域(使用class而不是style)resultDiv.classList.remove('hidden');// 更新状态updateStatus(`✅ 识别完成: 数字 ${prediction} (置信度 ${(confidence * 100).toFixed(1)}%)`);// 添加成功日志addLog(`识别成功: 数字 ${prediction}, 置信度 ${(confidence * 100).toFixed(1)}%`, "success");// 可选:3秒后自动隐藏状态消息setTimeout(() => {if (document.getElementById('status').textContent.includes('识别完成')) {updateStatus('✅ 系统就绪,可以继续识别');}}, 3000);}function showLoading() {const predictionDiv = document.getElementById('prediction');const confidenceDiv = document.getElementById('confidence');predictionDiv.textContent = '...';confidenceDiv.textContent = '计算中...';document.getElementById('result').classList.remove('hidden');}function hideResult() {document.getElementById('result').classList.add('hidden');}function hideLoading() {hideResult();}function updateStatus(message, isError = false) {const statusDiv = document.getElementById('status');statusDiv.textContent = message;statusDiv.className = isError ? 'status error' : 'status';}function addLog(message, type = "info") {const logDiv = document.getElementById('log');const timestamp = new Date().toLocaleTimeString();const color = type === "error" ? "color: #d32f2f;" : type === "success" ? "color: #388e3c;" :type === "send" ? "color: #1976d2;" :type === "receive" ? "color: #7b1fa2;" : "color: #666;";logDiv.innerHTML += `<div style="${color}">[${timestamp}] ${message}</div>`;logDiv.scrollTop = logDiv.scrollHeight;}function updateStats() {document.getElementById('stats').textContent = `成功: ${successCount} | 失败: ${errorCount}`;}// 页面卸载时清理window.addEventListener('beforeunload', async () => {if (writer) {try {writer.releaseLock();} catch (e) {}}if (reader) {try {reader.releaseLock();} catch (e) {}}if (port) {try {await port.close();} catch (e) {}}});// 初始化initCanvas();updateStats();updateStatus('🎯 请点击"连接串口",然后在画布上手写数字0-9');// 键盘快捷键document.addEventListener('keydown', (e) => {if (e.key === 'r' || e.key === 'R') {recognize();} else if (e.key === 'c' || e.key === 'C') {clearCanvas();} else if (e.key === 't' || e.key === 'T') {runTest();}});</script>
</body>
</html>

保存代码。

效果

在这里插入图片描述

更多效果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

动态演示

在这里插入图片描述

总结

本文介绍了 DFRobot Beetle RP2350 开发板结合 MNIST 数据集和串口通信,实现网页手写数字识别的项目设计,为 RP2350 在人工智能领域的开发设计和产品应用提供了参考。

http://www.dtcms.com/a/587440.html

相关文章:

  • 网站建设公司现在还挣钱吗公司注册地址异常如何处理
  • JSP 深度解析:从运行机制读懂 Java Web 的 “初代顶流“ 待定
  • 时尚类网站建设国外移动网站设计
  • 俄罗斯女孩制作论文网站京东网站设计风格
  • 网站建设厃金手指花总十一移动端优化
  • 建站公司现状佛山专业网站建设公司哪家好
  • Java装箱与拆箱完全指南:从原理到实战
  • 厦门网站建设推广wordpress+知更鸟+下载
  • 淄博网站开发招聘jsp网站开发遇到问题
  • 江苏网站设计公司电话wordpress step 2
  • 模板网站首页设计优化seo网站西安
  • 如何自己开发微网站最新cms
  • 非洲在建最大光储项目光伏组件安装工作全面启动
  • 懒人手机网站网站空间控制面板软件
  • 网站怎么添加广告代码网站一直不被百度收录
  • 如何建设自己企业网站山东企业网站建设费用
  • 坪山网站建设行业现状wordpress百度自动
  • 东莞海边网站建设工作室南宁网络公司联系方式
  • 嘉兴 网站制作有做喜糖的网站吗
  • 公司网站建设p开发企业小程序开发
  • WordPress知更鸟主题怎样安装seo专业优化公司
  • 花都网站建设公司php大流量网站开发规范
  • 同心食品厂网站建设项目任务分解优化seo教程技术
  • 政务网站建设目标和核心功能总部在深圳的互联网公司
  • 浏览器缓存策略
  • 北京网络公司网站莱芜信息平台
  • 重庆企业公司网站建设公司网站域名怎么注册
  • 阿里云网站托管南宁市网站开发公司
  • discuz仿搜索网站企业网站建设ppt介绍
  • 跑腿网站建设哪里搜索引擎优化好