ESP-DL 端侧自学习整图分类示例:基于原型网络的免重训练本地分类方案
2026/9/19 23:57:27 网站建设 项目流程
  • 物联网
  • 嵌入式
  • 驱动开发
  • 硬件开发

【免费下载链接】esp-iot-solution

Espressif IoT Library. IoT Device Drivers, Documentations and Solutions.

项目地址:https://gitcode.com/GitHub_Trending/es/esp-iot-solution
点击查看免费下载

导读

本文围绕 esp-iot-solution 仓库中的 self_learning_classification 示例,系统讲解如何在 ESP32-P4 / ESP32-S31 上基于 ESP-DL 框架实现端侧、离线、免重训练的整图分类。该方案通过 MobileNetV2 特征提取器把图像映射为特征向量,由用户直接在设备上录入样本、存储特征原型,再以最近邻匹配完成分类,无需云端参与。读完本文,你将掌握该示例的硬件/软件要求、构建烧录流程、交互操作方式,以及SelfLearningClassifier的源码级实现原理与调优手段,可直接将其复用到固定视角、固定背景的物体/类别识别场景中。

示例定位:免重训练的自学习分类

传统嵌入式视觉分类需要离线收集数据集、训练模型、量化并重新部署固件,迭代周期长。而本示例采用原型网络(Prototype Network)思路,把“训练”搬到设备端:

  • 预训练好的 MobileNetV2 只负责把输入图像编码成 1280 维的特征向量(见 imagenet_cls.cpp);
  • 用户点击“Record”按钮,即可把当前画面作为某类别的样本录入设备;
  • 点击“Recog”按钮,设备将当前画面的特征向量与已录入的特征原型逐一比对,输出类别与置信度。

整个过程不需要重训练模型、不需要云端服务,分类依据完全来自设备本地记录的样本特征,因此特别适合相机视角与背景相对固定的场景,例如固定工位上的物体/类别识别、固定拍摄区域内的物品区分。

原文档特别强调,为了获得最佳识别效果,录入(enroll)与识别(recog)之间的环境条件应尽量保持一致,包括:

  • 光照强度与色温(illumination/color temperature);
  • 相机曝光与白平衡设置(exposure/white balance);
  • 物体距离与尺度(object distance/scale);
  • 摆放角度(placement angle)。

工作原理:从分类头到特征向量

本示例的核心思想是“去掉分类头,只留特征”。在模型量化部署阶段,官方教程对应的 quantize_torch_model.py 脚本 中,MobileNetV2 的分类层被替换为恒等映射,使网络只输出特征向量:

model = torchvision.models.mobilenet.mobilenet_v2( weights=MobileNet_V2_Weights.IMAGENET1K_V1 ) model.classifier = torch.nn.Identity()

这样做的好处是:特征向量是“通用描述符”,不绑定任何预定义类别,因此可以在设备端任意新建类别、动态扩展。后续识别时,只需将当前图像的特征向量与已录入的特征向量做距离比较,即可完成本地分类。模型量化与部署的整体流程遵循 How to deploy MobileNetV2 教程的步骤。

在仓库中,量化部署后的模型文件位于 components/imagenet_cls/model/p4/imagenet_cls_mobilenetv2_s8_v1.espdl,由 imagenet_cls.cpp 封装为MobileNetV2FeatureExtractor类加载执行。从运行日志可知,该模型的特征维度为 1280

I (2703) self_learning: SelfLearningClassifier created, feature dim: 1280, max prototypes/class: 7

特征提取器的预处理细节

imagenet_cls.cpp 中创建模型时,同步配置了 ImageNet 风格的标准化参数:

m_image_preprocessor = new dl::image::ImagePreprocessor(m_model, {123.675, 116.28, 103.53}, // mean {58.395, 57.12, 57.375}); // std

每次推理时按preprocess → model run → output assign三步执行(imagenet_cls.cpp),并借助DL_LOG_INFER_LATENCY_*宏统计各阶段耗时。此外,模型在构造后调用了m_model->minimize()删除推理不需要的中间变量,日志中会出现对应提示:

W (2701) dl::Model: Minimize() will delete variables not used in model inference, which will make it impossible to test or debug the model.

软硬件要求

支持的芯片与 ESP-IDF 版本

目标芯片ESP-IDF 要求
ESP32-P4release/v5.5 及更高版本
ESP32-S31需要 ESP-IDF master 分支,并以预览目标方式配置:idf.py --preview set-target esp32s31

开发环境按 ESP-IDF 编程指南搭建,建议先完成“Build Your First Project”示例,确认工具链正常后再构建本工程。对应目标的默认配置保存在仓库的 sdkconfig.defaults.esp32p4 与 sdkconfig.defaults.esp32s31 中。

开发板与硬件连接

本示例支持以下两种开发板:

  • ESP32-S31-Korvo:板载 DVP 摄像头、800×480 RGB LCD 以及 GT1151 触摸面板;
  • ESP32-P4-Function-EV-Board:搭配 MIPI-CSI 摄像头(SC2336)与 1024×600 MIPI-DSI LCD(EK79007)。

将摄像头和 LCD 连接到开发板后,用 USB-C 线连接USB-UART端口到 PC,即可同时完成供电与串口日志输出。从运行日志中可以看到典型的外设初始化序列(README.md 中的示例输出):MIPI DSI PHY 上电、EK79007 面板初始化、GT911 触摸芯片探测、SC2336 传感器识别以及 MIPI-CSI 视频流建立。

构建、烧录与监视

在终端中执行(将PORT替换为开发板实际串口名):

idf.py -p PORT flash monitor
  • 该命令会依次完成编译、烧录并打开串口监视器;
  • 查看串口输出;
  • 退出串口监视器请按Ctrl-]

本工程共用的配置项定义在 sdkconfig.defaults 中,包括:16MB 外部 Flash、自定义分区表(partitions.csv,分区表偏移 0x9000)、开启 PSRAM 及 PSRAM XIP(CONFIG_SPIRAM_XIP_FROM_PSRAM=y)、FreeRTOS 时钟节拍 1000Hz、LVGL Montserrat 20/30 号字体等。其中CONFIG_SPIRAM=yCONFIG_SPIRAM_XIP_FROM_PSRAM=y对端侧视觉应用至关重要——大尺寸模型与特征缓存依赖 PSRAM 空间。

运行与交互:录入与识别

示例默认预留5 个类别用于识别。程序启动后,LCD 上实时显示摄像头画面(由display_task以约 50fps 的节拍刷新,见 app_classifier.cpp)。

界面交互通过三个控件完成(app_classifier.cpp):

控件作用
ui_ClassRoller滚轮选择当前要录入的类别 ID
ui_RecordButton点击后将当前画面作为样本录入到选中类别
ui_RecogButton点击后对当前画面执行识别并显示结果

事件驱动的工作流程

App 层使用 FreeRTOS 事件组把 UI 点击与识别任务解耦(app_classifier.cpp):

  1. 按下Record→ 设置RECORD_BIT事件位;
  2. 按下Recog→ 设置RECOG_BIT事件位;
  3. recognition_task在独立的 CPU 核心上阻塞等待事件(xEventGroupWaitBits),收到事件后从摄像头取一帧 RGB565 图像,构造dl::image::img_t交给分类器的enroll()predict()

识别结果会叠加显示在预览画面上:当class_id >= 0时显示Class %d %.0f%%(类别号与置信度),否则显示Unknown

录入的样本越多,识别准确率越高。类别识别仅在置信度大于等于80%(默认阈值,定义于 self_learning_classifier.cpp)时才被接受,否则视为无有效类别匹配(class_id = -1)。

分类器源码解析:SelfLearningClassifier

分类核心类 SelfLearningClassifier 是一个基于原型网络的 few-shot 自学习分类器,对外暴露三个核心接口:

SelfLearningClassifier(const char *model_name, int max_prototypes = 7); void enroll(const dl::image::img_t &img, int class_id); PredictResult predict(const dl::image::img_t &img);

其中PredictResult结构体包含:

struct PredictResult { int class_id; ///< 预测类别 ID,-1 表示未知 float distance; ///< 到最近原型的距离 float confidence; ///< 置信度(0~100%) };

录入流程(enroll)

enroll() 的执行链路为:

  1. 调用特征提取器run()得到 1280 维特征向量;
  2. 对特征做L2 归一化normalize,使用dl::math::sqrt_newton计算范数,见 self_learning_classifier.cpp);
  3. 若该class_id首次出现,则创建新类别(日志打印Created new class %d);
  4. 通过add_prototype()将特征加入类别原型集合。

原型集合的管理策略

每个类别最多保存max_prototypes(默认7)个特征原型。当样本数未超过上限时直接追加;超过上限后,add_prototype()(self_learning_classifier.cpp)采用多样性替换策略

  • 找到与该新特征距离最近的原型;
  • 若新特征与最近原型的距离大于当前类内平均距离的 0.6 倍(min_dist > cat->avg_intra_dist * 0.6f),说明新样本携带了额外信息,则替换该原型,从而控制原型数量、提升类内覆盖度;
  • 否则丢弃新样本,不改变原型集合。

每次录入后都会重算类内平均距离avg_intra_dist(所有原型两两欧氏距离的均值),该指标在替换决策与日志观测中都很有用。从日志可以看到录入过程中avg_intra_dist随样本数增长而上升并趋于稳定:

I (7869) self_learning: Created new class 0 I (7869) self_learning: Enrolled to class 0: 1/7 prototypes, 1 total samples, avg_intra_dist=0.0000 I (9384) self_learning: Enrolled to class 0: 2/7 prototypes, 2 total samples, avg_intra_dist=0.3002 I (10791) self_learning: Enrolled to class 0: 3/7 prototypes, 3 total samples, avg_intra_dist=0.4023 ... I (17778) self_learning: Enrolled to class 0: 7/7 prototypes, 7 total samples, avg_intra_dist=0.5137

注意原型数量达到上限(7/7)后,total samples仍会继续累加,但原型集合保持不变或按替换策略更新。

识别流程(predict)

predict()) 的执行逻辑为:

  1. 提取并归一化当前图像的特征向量;
  2. 遍历所有类别的所有原型,计算欧氏距离,同时记录最近距离次近距离(用于置信度计算);
  3. 用最近距离计算相似度分数:similarity_score = (1 - distance² / 2) × 100,并裁剪到 0~100 区间(self_learning_classifier.cpp)。由于特征已 L2 归一化,该式本质上是把余弦相似度映射为百分比置信度;
  4. 若置信度 ≥ 80%,返回对应类别;否则返回class_id = -1(Unknown)。

内存管理细节

从源码可见实现者对内存分配有精细考量(self_learning_classifier.cpp):

  • 原型指针数组使用heap_caps_calloc(..., MALLOC_CAP_INTERNAL)分配在内部 RAM,保证访问速度;
  • 每个特征原型本体使用heap_caps_malloc(..., MALLOC_CAP_SPIRAM)分配在 PSRAM,因为 1280 维 float 数组(约 5KB/个)乘以多个类别后总量可观,放在 PSRAM 更合理;
  • 析构函数与free_category()会完整释放所有原型与指针数组,避免内存泄漏。

应用入口

程序入口 self_learning_classification.cpp 的app_main()依次完成:

app_lcd_init(); auto cam = new Camera(VIDEO_PIX_FMT_RGB565, 4, V4L2_MEMORY_MMAP, false); auto classifier = new SelfLearningClassifier("imagenet_cls.espdl"); auto app = new AppClassifier(cam, classifier); app->start();

摄像头以 RGB565 格式、4 个 MMAP 帧缓冲采集画面;Camera类封装了 V4L2 视频采集、帧缓冲队列、PPA 硬件后处理(字节交换/旋转/镜像)等能力(app_camera.hpp);AppClassifier::start()将显示任务与识别任务分别固定到两个 CPU 核心,实现流水线并行。

性能指标

README 中给出了本示例实测的延迟数据:

  • 平均类别录入时间(class enrollment):750 ms—— 主要消耗在特征提取推理;
  • 平均距离计算时间(distance computation):1 ms—— 特征向量比对极快,因为每类最多 7 个原型、默认 5 类,最坏也只需几十次 1280 维向量点积。

若需要更快的端到端推理,可以将 MobileNetV2 替换为其他更轻量的分类/主干网络(backbone),进一步压缩录入与识别延迟。

模型选择与部署配置

components/imagenet_cls/Kconfig 提供了两组配置选项:

  • 默认模型FLASH_IMAGENET_CLS_MOBILENETV2_S8_V1(默认开启),对应imagenet_cls_mobilenetv2_s8_v1模型;
  • 模型存放位置
    • flash_rodata(默认):模型以 rodata 形式链接进固件(imagenet_cls.cpp 通过_binary_..._start符号引用);
    • flash_partition:模型放在独立 Flash 分区(分区名imagenet_cls)。

构建时可通过idf.py menuconfig调整上述选项。若启用CONFIG_SPIRAM_RODATACONFIG_SPIRAM_XIP_FROM_PSRAM,模型会被拷贝到 PSRAM 执行,日志中会有对应提示:

W (2626) FbsLoader: CONFIG_SPIRAM_RODATA or CONFIG_SPIRAM_XIP_FROM_PSRAM option is on, fbs model is copied to PSRAM.

完整的运行日志解读

README.md 提供了一份完整的启动与交互日志,可观察到几个关键阶段:

  1. 启动阶段:bootloader 加载固件(ESP-IDF v5.5.2-737-...)、检测 16MB Flash 与 32MB PSRAM、初始化双核应用;
  2. 外设阶段:LVGL 任务启动、MIPI DSI/EK79007 LCD 初始化(1024×600)、GT911 触摸探测、SC2336 摄像头识别(1280×720)、MIPI-CSI 视频流建立;
  3. 模型加载阶段FbsLoader将 flatbuffers 模型加载(必要时拷贝到 PSRAM),dl::Model::Minimize()精简模型;
  4. 分类器就绪:打印feature dim: 1280, max prototypes/class: 7
  5. 交互阶段:依次录入类别 0、类别 1 的多个样本(原型数、总样本数、类内平均距离逐步更新),最后识别输出:
I (42464) self_learning: Predict: best_class=1, dist=0.3092, confidence=95.2%

该行说明当前画面与类别 1 的某个原型距离为 0.3092,置信度 95.2%,超过 80% 阈值,判定为类别 1。

调优建议与注意事项

  1. 置信度阈值:默认 80%(confidence >= 80.0f,见 self_learning_classifier.cpp)。误识别偏高时可以提高阈值,漏识别偏高时可以适当降低;
  2. 原型数量max_prototypes默认 7(构造函数第二个参数),可通过构造函数传入调整。原型越多对类内变化容忍度越高,但内存与比对耗时也随之增加;
  3. 类别数量:示例默认 5 个类别,识别时会遍历所有类别的全部原型做最近邻匹配,类别越多单次距离计算时间越长(当前约 1ms,余量充足);
  4. 环境一致性:录入与识别阶段的光照、曝光、白平衡、距离与角度尽量一致,这是本方案准确率的决定性因素;
  5. 样本多样性:每个类别多录入不同角度/位置的样本,让原型覆盖类内变化,可显著提升识别鲁棒性;
  6. 硬件选型:若追求更低延迟,可参考“性能”一节替换更轻量的 backbone 模型,并结合 sdkconfig.defaults 中的 PSRAM XIP 配置保持推理带宽。

小结

self_learning_classification 示例为嵌入式端侧提供了一条“零重训练、即录即用”的分类路径:以去分类头的 MobileNetV2 作为通用特征提取器,设备端录入样本即生成特征原型,识别时以归一化欧氏距离 + 相似度置信度完成最近邻分类。其核心价值在于把“样本采集—特征存储—类别扩展”全部下沉到本地,非常适合固定视角、固定背景的工业工位识别与设备端视觉交互场景,也为基于 ESP-DL 的原型网络类应用提供了清晰的可复用代码骨架(SelfLearningClassifier)。

  • 物联网
  • 嵌入式
  • 驱动开发
  • 硬件开发

【免费下载链接】esp-iot-solution

Espressif IoT Library. IoT Device Drivers, Documentations and Solutions.

项目地址:https://gitcode.com/GitHub_Trending/es/esp-iot-solution
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询