十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于 ESP-DL 的触摸板手写数字识别:从触摸感知、模型训练到 ESP32 设备端推理

基于 ESP-DL 的触摸板手写数字识别:从触摸感知、模型训练到 ESP32 设备端推理 基于 ESP-DL 的触摸板手写数字识别从触摸感知、模型训练到 ESP32 设备端推理【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution本篇技术指南以 esp-iot-solution 仓库中的 Touch Digit Recognition 文档 为主线完整讲解基于电容触摸板的 0~9 手写数字识别方案包括 6×7 触摸通道阵列的感知原理与定位算法、自定义数据集采集、基于 PyTorch 的 CNN 模型训练、经 ESP-PPQ 量化导出以及最终在 ESP32 设备上的模型加载与实时推理。读者阅读后可以掌握从硬件触摸数据到端侧 AI 推理的全链路工程实现并可直接基于仓库中的 touchpad_digit_recognition 示例 进行二次开发。触摸感知原理与数据采集触摸板硬件与触摸原理本示例采用ESP_Touch_Kit_Touchpad触摸板作为输入设备该触摸板由6 列 × 7 行共 13 个触摸通道6 列 7 行组成。手指在触摸板上滑动时会改变经过通道的电容值通过监测这些电容值的变化即可推断手指的位置。图ESP Touch Kit 物理触摸板6×7 触摸通道阵列在示例工程中13 个触摸通道映射到 ESP32 的 GPIO 1~13行列通道的划分定义在 main/touch_digit.cpp 中#define CHANNEL_NUM 13 #define CHANNEL_LIST {1,2,3,4,5,6,7,8,9,10,11,12,13} #define ROW_CHANNEL_INDEX {4,6,8,7,10,9,12} #define COL_CHANNEL_INDEX {2,1,3,5,13,11}底层通过touch_sensor_lowlevel新版 Touch 低层驱动创建并启动 13 个触摸通道并注册中断回调在通道状态变化时读取全部 13 个通道的原始数据static void state_cb(uint32_t channel, touch_lowlevel_state_t state, void *state_values, void *arg) { QueueHandle_t data_queue (QueueHandle_t)arg; data_array_t data_array {0}; for (int i 0; i CHANNEL_NUM; i) { touch_sensor_lowlevel_get_data(i 1, data_array[i]); } xQueueSendFromISR(data_queue, data_array, NULL); }原始数据通过 FreeRTOS 队列送入touch_digit_task处理从而把中断上下文与数据处理逻辑解耦队列容量 10见 touch_digit.cpp 中的touch_digit_init。检测算法归一化与双通道定位由于硬件制造差异每个触摸通道的最大/最小触发值各不相同因此原始电容值需要归一化。整个检测算法分为以下几步记录极值归一化在手指滑过触摸板的过程中逐通道记录最大值与最小值。示例中通过touch_dight_begin_normalize()复位各通道极值、touch_dight_end_normalize()结束采集并保存归一化期间持续调用touch_digit_normalize()更新max/min。对应TouchChannel类的实现见 main/include/touch_channel.h归一化公式为normalized_data double((data - min) * 1.0f / (max - min));结果被裁剪到 [0, 1] 区间。找出变化率最大的两个相邻通道手指移动时电容变化率最高的通道及其相邻通道次高变化率即可构成一对相邻通道。示例代码在行、列两个方向分别找到max_row[0]/max_row[1]与max_col[0]/max_col[1]若最大值落在边界通道则强制取唯一相邻通道。比值插值计算亚通道精度坐标利用两个相邻通道的归一化值通过比值公式确定手指在该方向相对两通道中心点的偏移$$x \frac{F_a - F_b}{F_a F_b}$$对应源码中的compute_positionstatic double compute_position(int position_a, int position_b, double value_a, double value_b) { if (value_a value_b 0) { return 0; } return (position_a * value_a position_b * value_b) / (value_a value_b); }组合两个方向的相对坐标得到手指位置。示例中定义PRECISION 5将每两个相邻通道之间再细分 5 个精度点从而把 7×6 的通道阵列插值放大为30×25 的图像分辨率(7-1)*5 30行、(6-1)*5 25列见 main/include/touch_image.h 中TouchImage的构造函数坐标计算为int _x max_row[0] * PRECISION round(error * (PRECISION - 1)); int _y max_col[0] * PRECISION round(error * (PRECISION - 1));设置触发阈值判断手指是否在板面上示例中归一化最大值低于0.15即判定为未触摸if (max_value 0.15) { return false; }手指抬起结束书写touch_digit_task内维护一个三态状态机WAIT_WRITE → BEGIN_WRITE → END_WRITE检测到触摸后开始落笔写点连续IDEL_CNT_MAX 90次采样未检测到触摸即判定抬笔此时将 30×25 的位图TouchImage::data打包送入推理队列xImageQueue随后清空画布回到等待状态同时用*字符在串口控制台打印出手写的笔画图像。自定义数据集采集在实际场景中触摸板上绘制的数字与 MNIST 数据集中的手写数字在视觉分布上有明显差异直接使用 MNIST 训练的模型在真实触摸输入上表现不佳。因此示例通过上位机采集触摸板绘制的数字图像构建自定义数据集用于训练。图基于触摸板绘制输入采集的真实数据集样例0~9需要说明的是插值后的手写数据图像尺寸为30×25即TouchImage的row_length × col_length这正是后续模型输入1×25×30的来源。示例工程还提供了 pack_model.py 等配套脚本用于模型文件打包。若需要通过上位机采集原始数据可将 main/touch_digit.cpp 中的宏REALLY_DATA_PRINT置为1即可按逗号分隔输出 13 通道原始数据。模型训练与部署模型结构设计基于 PyTorch 框架构建适用于触摸手写数字识别的卷积神经网络输入为单通道1×25×30图像输出 10 个类别对应数字 0~9。网络由 3 个卷积块Conv2d ReLU前两个带MaxPool2d、一个Flatten层和两个全连接层组成并在全连接层后加入Dropout(p0.5)防止过拟合class Net(torch.nn.Module): def __init__(self): super(Net, self).__init__() self.model torch.nn.Sequential( torch.nn.Conv2d(in_channels1, out_channels16, kernel_size3, stride1, padding1), torch.nn.ReLU(), torch.nn.MaxPool2d(kernel_size2, stride2), torch.nn.Conv2d(in_channels16, out_channels32, kernel_size3, stride1, padding1), torch.nn.ReLU(), torch.nn.MaxPool2d(kernel_size2, stride2), torch.nn.Conv2d(in_channels32, out_channels64, kernel_size3, stride1, padding1), torch.nn.ReLU(), torch.nn.Flatten(), torch.nn.Linear(in_features7 * 6 * 64, out_features256), torch.nn.ReLU(), torch.nn.Dropout(p0.5), torch.nn.Linear(in_features256, out_features10), torch.nn.Softmax(dim1) ) def forward(self, x): output self.model(x) return output网络经过两次步长为 2 的MaxPool2d后特征图尺寸由25×30缩小到7×6向上取整后的特征空间因此全连接层输入维度为7 * 6 * 64。数据加载与预处理数据集按数字分文件夹组织在dataset/extra目录下每个数字一个子文件夹以数字命名。预处理使用transforms.Compose依次完成灰度化、随机旋转/平移增强、张量化和归一化import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms transform transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.RandomAffine(degrees10, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) dataset datasets.ImageFolder(root./dataset/extra, transformtransform) train_size int(0.8 * len(dataset)) test_size len(dataset) - train_size train_dataset, test_dataset random_split(dataset, [train_size, test_size]) train_loader DataLoader(datasettrain_dataset, batch_size32, shuffleTrue) test_loader DataLoader(datasettest_dataset, batch_size32, shuffleFalse)整个数据集通过ImageFolder加载后按8:2划分训练集与测试集DataLoader以batch_size32构造批量加载器。注意RandomAffine(degrees10, translate(0.1, 0.1))提供了 ±10° 旋转与 ±10% 平移的数据增强以提升模型的泛化能力。训练参数配置训练使用交叉熵损失函数与 Adam 优化器初始学习率为0.001。设备优先使用 GPUcuda:0无 GPU 时回退到 CPUdevice cuda:0 if torch.cuda.is_available() else cpu model Net().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)训练过程与模型保存训练共 100 个 epoch每个 epoch 先用训练集更新参数并统计损失与准确率再用测试集评估泛化性能def train_epoch(model, train_loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / len(train_loader) epoch_acc 100 * correct / total return epoch_loss, epoch_acc def test_epoch(model, test_loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in test_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) running_loss loss.item() _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / len(test_loader) epoch_acc 100 * correct / total return epoch_loss, epoch_acc num_epochs 100 train_acc_array [] test_acc_array [] for epoch in range(num_epochs): train_loss, train_acc train_epoch(model, train_loader, criterion, optimizer, device) test_loss, test_acc test_epoch(model, test_loader, criterion, device) print(fEpoch [{epoch 1}/{num_epochs}], fTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, fTest Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%) train_acc_array.append(train_acc) test_acc_array.append(test_acc) torch.save(model.state_dict(), ./models/final_model.pth)训练完成后模型参数被保存为./models/final_model.pth该权重文件将作为后续 ESP-PPQ 量化部署的输入。训练过程中训练集与测试集准确率曲线的变化趋势如下图训练过程中训练集与测试集准确率随 epoch 的变化曲线模型量化与部署ESP-PPQ环境配置ESP-PPQ是基于ppq的量化工具。安装前需先卸载原生ppq再通过 git 安装ESP-PPQpip uninstall ppq pip install githttps://github.com/espressif/esp-ppq.git量化导出脚本量化流程的完整说明可参考 esp-dl 仓库中的模型量化与导出教程。若需导出面向 ESP32-P4 的模型将TARGET设置为esp32p4即可。下面是本示例在 PC 端完成量化并导出.espdl模型的完整脚本import torch from PIL import Image from ppq.api import espdl_quantize_torch from torch.utils.data import Dataset from torch.utils.data import random_split from torchvision import transforms, datasets DEVICE cpu class FeatureOnlyDataset(Dataset): def __init__(self, original_dataset): self.features [] for item in original_dataset: self.features.append(item[0]) def __len__(self): return len(self.features) def __getitem__(self, idx): return self.features[idx] def collate_fn2(batch): features torch.stack(batch) return features.to(DEVICE) if __name__ __main__: BATCH_SIZE 32 INPUT_SHAPE [1, 25, 30] TARGET esp32s3 NUM_OF_BITS 8 ESPDL_MODEL_PATH ./s3/touch_recognition.espdl transform transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) dataset datasets.ImageFolder(root../dataset/extra, transformtransform) train_size int(0.8 * len(dataset)) test_size len(dataset) - train_size train_dataset, test_dataset random_split(dataset, [train_size, test_size]) image Image.open(../dataset/extra/9/20250225_140331.png).convert(L) input_tensor transform(image).unsqueeze(0) print(input_tensor) feature_only_test_data FeatureOnlyDataset(test_dataset) testDataLoader torch.utils.data.DataLoader(datasetfeature_only_test_data, batch_sizeBATCH_SIZE, shuffleFalse, collate_fncollate_fn2) model Net().to(DEVICE) model.load_state_dict(torch.load(./final_model.pth, map_locationDEVICE)) model.eval() quant_ppq_graph espdl_quantize_torch( modelmodel, espdl_export_fileESPDL_MODEL_PATH, calib_dataloadertestDataLoader, calib_steps8, input_shape[1] INPUT_SHAPE, inputs[input_tensor], targetTARGET, num_of_bitsNUM_OF_BITS, deviceDEVICE, error_reportTrue, skip_exportFalse, export_test_valuesTrue, verbose1, dispatching_overrideNone )量化校验与测试数据导出为了便于模型调试ESP-DL 支持在量化过程中注入测试数据并在 PC 端直接查看推理结果。上述流程将image示例中为数字 9 的图像传入espdl_quantize_torch进行测试模型转换完成后该测试数据的推理结果会保存在*.info后缀的文件中。从输出可见第 10 个类别索引 9的概率接近 1.0与测试样本“9”一致test outputs value: %23, shape: [1, 10], exponents: [0], value: array([9.85415445e-34, 1.92874989e-22, 7.46892081e-43, 1.60381094e-28, 3.22134028e-27, 1.05306175e-20, 4.07960022e-41, 1.42516404e-21, 2.38026637e-26, 1.00000000e00, 0.00000000e00, 0.00000000e00], dtypefloat32)重要提示在模型量化与部署过程中请务必将torch.utils.data.DataLoader中的shuffle参数设为False否则校准数据顺序不确定会导致量化结果不可复现。量化产物touch_recognition.espdl已随示例工程发布分别位于 models/esp32s3/touch_recognition.espdl 与 models/esp32p4/touch_recognition.espdl可直接烧录使用。设备端推理模型加载与运行设备端模型加载与推理的具体实现可参考 esp-dl 关于加载测试模型与运行模型的教程。在本示例中核心类TouchDigitRecognition见 main/touch_digit_recognition.cpp封装了完整流程通过dl::Model(model_name, fbs::MODEL_LOCATION_IN_FLASH_PARTITION)从 flash 分区加载模型并调用minimize()最小化内存占用从模型输入张量读取exponent据此计算输入量化缩放系数m_input_scale 1.0f / DL_SCALE(input_tensor-exponent)推理输入缓冲区通过heap_caps_calloc(..., MALLOC_CAP_SPIRAM)分配在 PSRAM 中以缓解内部 RAM 压力。predict()的调用链为预处理 → 构造输入张量 →m_model-run(inputs)→ 后处理取argmaxint TouchDigitRecognition::predict(const uint8_t* input_data) { m_preprocessor-process(input_data, m_quant_buffer); std::mapstd::string, dl::TensorBase* inputs; auto model_inputs m_model-get_inputs(); for (auto input_pair : model_inputs) { dl::TensorBase* tensor new dl::TensorBase( {1, static_castint(m_feature_size)}, m_quant_buffer, input_pair.second-exponent, input_pair.second-dtype, false, MALLOC_CAP_SPIRAM ); inputs.emplace(input_pair.first, tensor); } m_model-run(inputs); for (auto input : inputs) { delete input.second; } return m_postprocessor-process(); }后处理DataPostprocessor::process()在模型输出的 10 个类别概率中取最大值下标即为识别结果并通过ESP_LOGI打印float *output_ptr (float *)m_model_output-data; int size m_model_output-get_size(); auto max_iter std::max_element(output_ptr, output_ptr size); int max_index std::distance(output_ptr, max_iter); ESP_LOGI(TAG, Predict result: %d, max_index); return max_index;输入数据预处理的关键点需要特别注意的是触摸驱动上报的按/抬状态为1/0按下写点置 1、空白为 0而模型输入是归一化后的图像数据PC 端训练时使用Normalize((0.5,), (0.5,))对应值域约 [-1, 1]。因此在喂入模型前必须对触摸驱动数据做一次映射0 映射为 -11 映射为 1再按输入张量的量化系数转成 int8for (size_t i 0; i m_feature_size; i) { int8_t value (input_data[i] 0 ? -1 : 1); quant_buffer[i] dl::quantizeint8_t((float)value, m_input_scale); }这正是DataPreprocessor::process()的实现见 main/touch_digit_recognition.cpp它保证了 PC 端训练/量化时使用的数据分布与设备端一致是识别准确率的关键前提。运行流程与交互设计完整示例工程位于 examples/ai/esp_dl/touchpad_digit_recognition工程中还包含 CH455 数码管驱动组件components/digital_tube用于显示识别结果。交互与运行流程见 main/main.cpp如下初始化 NVS用于保存各通道归一化的 max/min 校准数据初始化 GPIO0 按键按下按键进入/退出归一化校准模式通过touch_dight_begin_normalize()/touch_dight_end_normalize()控制校准值持久化保存touch_digit_init()启动时自动加载touch_digit_init()创建触摸低层驱动、图像队列与两个 FreeRTOS 任务触摸采样任务 推理任务数码管初始化I2C0SDAGPIO37SCLGPIO38。启动阶段串口日志会打印每个通道校准得到的max/min值随后进入等待书写状态在触摸板上书写数字并抬笔后控制台会以*字符打印 30×25 的位图并输出识别结果I (735) touch_lowlevel: Touch sensor lowlevel (v0.6.0) configured with 13 channels max:19678 min:8823 max:20068 min:8889 ... I (998) MemoryManagerGreedy: Maximum memory size: 15632 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * I (6524) TouchDigitRecognition: Predict result: 2上例中触摸板绘制的笔画被还原为 30×25 位图模型正确预测结果为数字2同时该结果会同步显示在数码管上。小结本方案在 esp-iot-solution 中形成了一条完整的“数据采集 → 模型训练 → 量化部署 → 设备端推理”流水线触摸感知利用 6×7 触摸通道阵列 极值归一化 相邻通道比值插值将低分辨率通道数据放大为 30×25 的图像数据与训练基于真实触摸板采集的自定义数据集而非 MNIST通过 PyTorch 训练三层卷积 CNN量化部署通过 ESP-PPQ 将 PyTorch 模型量化为 8bit 的.espdl模型并支持 PC 端注入测试数据校验量化精度端侧推理基于 ESP-DL 加载 flash 分区中的模型完成 1/0 → ±1 的数据映射与 int8 量化后实时推理结果通过数码管与串口呈现。读者可以在此基础上继续深入查看 touch_channel.h 理解通道归一化实现参考 touch_image.h 理解插值位图的数据组织或参考 README.md 了解示例输出与整体流程。【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表