拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从零搭建衣物面料识别Demo:ESP32-S3 + AS7341光谱采集 + 决策树部署

前言在项目中遇到了用户交互问题设备需要用户选择面料但是一般用户并不认识面料材质于是希望开发能够自动识别面料材质的功能。在寻找解决方案时发现不同物质吸收的光谱不同这一原理被用在了面料检测上目前已有有高校论文证明了技术可行于是希望搭建一个简单demo进行实验验证。该文章讲述了如何搭建实验系统如何收集采集数据制作数据库如何采用机器学习决策树模型对数据进行训练如何将模型算法转换为C语言代码部署在 ESP32-S3 上。由于精力有限无法完善该项目仅以此文记录我的开发过程想要实现完整闭环还需读者在模型算法的部署上自己摸索。一、硬件介绍1. 开发板实验中使用的是物联网开发常用的 ESP32-S3 系列开发板型号为ESP32-S3-CAM因为项目中需要用到摄像头所以采用这款开发板搭建实验系统用普通的 ESP32-S3-N8R2 开发板也可以。2. 光谱传感器目前市面上比较流行的光谱传感器模块为 AS7341该模块价格不到30元模块自带一颗白色 LED拥有11个采集通道能够采集 350nm~1000nm 波长范围的光谱F1~F8为可见光通道NIR 为近红外光通道CLEAR 为不加滤镜所采集的总光强最后一个通道能够采集环境光的闪烁频率。3. 红外光源红外光源采用的是监控补光用的 940nm 黑灯发出的光肉眼不可见需要将模块上的光敏传感器用黑胶带遮住 LED 才能工作。4. 系统搭建找一个手机支架将 AS7341 和红外光源用热熔胶固定在之间上需要注意 AS7341 模块自带的白光 LED 在工作时发热比较严重容易使热熔胶松动因此尽量不要让白光 LED 长时间工作只需要在采集数据时打开采集结束时关闭。二、AS7341 开发驱动1. 项目功能介绍在github上有大佬开源的AS7341驱动项目在 readme.txt 文件介绍了该如何使用函数接口。将 esp-as7341 文件夹移植到自己项目中我的项目功能为uart_service_task线程负责串口接收指令command_execution_task线程负责执行当串口接收到任务执行指令后由 command_execution_task 进行一次数据采集。由于本文重点讲解 AS7341 部分所以串口、消息队列部分略过读者可自行设计任务逻辑。2. 设计驱动函数项目中我自定义了 I2C 初始化和 AS7341 采集任务函数在 app_main() 中初始化 AS7341 模块后每次采集只需要调用一次 i2c0_as7341_task() 即可。#include stdio.h #include stdlib.h #include string.h #include assert.h #include freertos/FreeRTOS.h #include freertos/task.h #include driver/i2c_master.h #include esp_log.h #include as7341.h #include command.h // 宏定义 #define APP_TAG AS7341_TASK // 定义 I2C 总线配置 #define I2C_MASTER_NUM I2C_NUM_0 #define I2C_MASTER_SCL_IO GPIO_NUM_38 #define I2C_MASTER_SDA_IO GPIO_NUM_39 #define I2C_MASTER_FREQ_HZ 100000 // 100 kHz #define I2C0_TASK_SAMPLING_RATE pdMS_TO_TICKS(1000) /*! Sampling period: 1 second */ #define LED_DRIVE_STRENGTH AS7341_LED_DRIVE_STRENGTH_100MA /*! LED drive strength: 12 mA */ // 全局句柄 static i2c_master_bus_handle_t i2c0_bus_hdl NULL; // I2C 总线句柄AS7341 库使用自定义类型 static as7341_handle_t dev_hdl NULL; static bool led_enabled false; // LED 状态标志 // 任务函数 static void i2c0_as7341_task() { as7341_enable_led(dev_hdl, LED_DRIVE_STRENGTH); vTaskDelay(1000 / portTICK_PERIOD_MS); // 延迟1秒 // 3. 任务主循环 ESP_LOGI(APP_TAG, ######################## AS7341 - START #########################); // ---------- 光谱数据采集 ---------- as7341_channels_spectral_data_t adc_data; esp_err_t ret as7341_get_spectral_measurements(dev_hdl, adc_data); if (ret ! ESP_OK) { ESP_LOGE(APP_TAG, Spectral measurement failed: %s, esp_err_to_name(ret)); // 此处可考虑跳过本次转换继续下一循环 } else { // 打印原始 ADC 值 printf( F1 %d\n, adc_data.f1); printf( F2 %d\n, adc_data.f2); printf( F3 %d\n, adc_data.f3); printf( F4 %d\n, adc_data.f4); printf( F5 %d\n, adc_data.f5); printf( F6 %d\n, adc_data.f6); printf( F7 %d\n, adc_data.f7); printf( F8 %d\n, adc_data.f8); printf( NIR %d\n, adc_data.nir); printf( CLEAR %d\n, adc_data.clear); // ---------- 转换为基本计数浮点数 ---------- as7341_channels_basic_counts_data_t basic_counts_data; ret as7341_get_basic_counts(dev_hdl, adc_data, basic_counts_data); if (ret ! ESP_OK) { ESP_LOGE(APP_TAG, Basic counts conversion failed: %s, esp_err_to_name(ret)); } else { printf( F1 %.2f\n, basic_counts_data.f1); printf( F2 %.2f\n, basic_counts_data.f2); printf( F3 %.2f\n, basic_counts_data.f3); printf( F4 %.2f\n, basic_counts_data.f4); printf( F5 %.2f\n, basic_counts_data.f5); printf( F6 %.2f\n, basic_counts_data.f6); printf( F7 %.2f\n, basic_counts_data.f7); printf( F8 %.2f\n, basic_counts_data.f8); printf( NIR %.2f\n, basic_counts_data.nir); printf( CLEAR %.2f\n, basic_counts_data.clear); } } as7341_disable_led(dev_hdl); ESP_LOGI(APP_TAG, ######################## AS7341 - END ###########################); } // I2C 总线初始化 static esp_err_t as7341_device_init(void) { // 1. 配置总线参数 i2c_master_bus_config_t bus_cfg { .i2c_port I2C_MASTER_NUM, .sda_io_num I2C_MASTER_SDA_IO, .scl_io_num I2C_MASTER_SCL_IO, .clk_source I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt 7, .flags.enable_internal_pullup true, // 启用内部上拉根据硬件情况 }; // 2. 创建总线并获取句柄 esp_err_t ret i2c_new_master_bus(bus_cfg, i2c0_bus_hdl); if (ret ! ESP_OK) { ESP_LOGE(APP_TAG, I2C bus creation failed: %s, esp_err_to_name(ret)); return ret; } ESP_LOGI(APP_TAG, I2C bus created successfully); // 3. 初始化 AS7341 设备 as7341_config_t dev_cfg I2C_AS7341_CONFIG_DEFAULT; ret as7341_init(i2c0_bus_hdl, dev_cfg, dev_hdl); if (ret ! ESP_OK || dev_hdl NULL) { ESP_LOGE(APP_TAG, AS7341 initialization failed: %s, esp_err_to_name(ret)); assert(dev_hdl ! NULL); // 可改为 vTaskDelete(NULL) 或重启 return ret; } ESP_LOGI(APP_TAG, AS7341 initialized successfully); return ret; }3. 注意事项1修改 LED 驱动在一开始调用函数 as7341_enable_led(dev_hdl, LED_DRIVE_STRENGTH); 时无法点亮板载 LED 是因为函数 as7341_enable_led() 源码在写入寄存器时没有将寄存器CFG0 (0xA9) 置 1详见 AS7341 Datasheet.pdf 25页。代码修改如下在 as7341.c 文件中esp_err_t as7341_enable_led(as7341_handle_t handle, as7341_led_drive_strengths_t led_drive_strength) { as7341_config_register_t config; as7341_led_register_t led; /* validate arguments */ ESP_ARG_CHECK( handle ); /* attempt to enable low register bank */ ESP_RETURN_ON_ERROR( as7341_enable_lo_register_bank(handle), TAG, enable low register bank for enable LED failed ); /* attempt to read configuration register */ ESP_RETURN_ON_ERROR( as7341_get_config_register(handle, config), TAG, read configuration register for enable LED failed ); /* attempt to write to led register*/ // as7341_get_led_register 该函数内部会动切换为高寄存器组 ESP_RETURN_ON_ERROR( as7341_get_led_register(handle, led), TAG, read led register for enable LED failed ); /* enable led */ config.bits.led_ldr_control_enabled true; led.bits.led_drive_strength led_drive_strength; led.bits.led_ldr_enabled true; /////////////////////////////////////////////////////////////// // 驱动少加了这一句使得在设置寄存器之前没有切换为低寄存器组导致写入失败因为调用 as7341_get_led_register 会自动切换为高寄存器组 ESP_RETURN_ON_ERROR( as7341_enable_lo_register_bank(handle), TAG, re-enable low bank before config write ); /////////////////////////////////////////////////////////////// /* attempt to write configuration register */ ESP_RETURN_ON_ERROR( as7341_set_config_register(handle, config), TAG, write configuration register for enable LED failed ); /* attempt to write to led register*/ ESP_RETURN_ON_ERROR( as7341_set_led_register(handle, led), TAG, write led register for enable LED failed ); /* attempt to enable high register bank */ ESP_RETURN_ON_ERROR( as7341_enable_hi_register_bank(handle), TAG, enable high register bank for enable LED failed ); return ESP_OK; }2调节 LED 亮度在调用函数 as7341_enable_led() 时输入的第二个参数可以调节板载 LED 的亮度官方给出的亮度调节示例如下图所示根据其规律可以总结出12mA~256mA之间对应的二进制数值。在 as7341.h 文件中我添加了100mA、110mA、200mA对应的参数为了找到合适的亮度经过多次测试我将 LED 电流大小固定为100mA防止光照太强导致通道采集数据溢出光照太弱导致采集数据区分度不明显。typedef enum as7341_led_drive_strengths_e {// 规律4 2n AS7341_LED_DRIVE_STRENGTH_4MA (0b0000000), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_6MA (0b0000001), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_8MA (0b0000010), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_10MA (0b0000011), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_12MA (0b0000100), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_100MA (0b0110000), /*! as7341 (default) */ AS7341_LED_DRIVE_STRENGTH_110MA (0b0110101), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_200MA (0b1100010), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_256MA (0b1111110), /*! as7341 */ AS7341_LED_DRIVE_STRENGTH_258MA (0b1111111), /*! as7341 */ } as7341_led_drive_strengths_t;三、实验设计1. 准备材料我准备了涤纶、涤棉、棉、涤麻、白色亚麻共五种布料每次采集时保证传感器距离面料之间的高度为 28~30mm 之间每种面料采集三次每次采集位置都不同。2. 数据采集第一组为涤纶的采集结果由于本文只是记录操作过程所以仅展示部分数据。采样点1高度 30mm采样点2高度 30mm采样点3高度 29mmF1 1356F2 6629F3 5307F4 8129F5 20413F6 16435F7 9821F8 8120NIR 1062CLEAR 16240F1 1342F2 6594F3 5255F4 8020F5 20176F6 16316F7 9683F8 8089NIR 1005CLEAR 15382F1 1335F2 6502F3 5178F4 7951F5 19843F6 16022F7 9568F8 7886NIR 1078CLEAR 16245将全部数据绘制为点线图一眼能够区分出涤纶最上面三条曲线、涤麻最下面三条曲线但是涤棉、棉、亚麻这三种不容易进行区分。3. 制作数据集由于这个小实验的数据量并不多于是我选择手动制作.csv文件。新建excel文件将自己所需要的数据制作成表格然后另存为.csv格式。四、Python 决策树模型训练由于本实验需要训练的特征数量并不多且特征与标签是线性关系为了快速验证可行性所以该项目使用的是决策树模型并且决策树的本质是 if-else更加便于转换为 C 语言部署在 ESP32-S3 上而且不依赖任何机器学习解释器和库移植起来非常简单。1. 入门学习软件使用的是 PyCharm训练数据使用的是 Scikit-learn Python机器学习库B站上的 入门视频链接 给大家放这里只需要学习前6讲把 python 环境配好、编程软件安装好、Scikit-learn 所需的所有依赖库安装好就行。2. 模型训练将制作的 .csv 格式文件放在项目根目录下执行下述代码会打印决策树规则。import pandas as pd from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score # 1. 加载数据 df pd.read_csv(datatest.csv) # 2. 定义特征和标签 # 推荐使用这4个核心特征你之前验证过的 features [clear, f5f4, f7f5, blue_yellow] X df[features] y df[label] # 3. 划分训练集和测试集因为数据少test_size0.2 分出来只有3条 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 4. 训练决策树限制深度防止过拟合 clf DecisionTreeClassifier(max_depth3, random_state42) clf.fit(X_train, y_train) # 5. 评估 y_pred clf.predict(X_test) print(f测试集准确率: {accuracy_score(y_test, y_pred):.2%}) # 6. 打印决策树规则这就是你要部署到ESP32的代码 from sklearn.tree import export_text print(\n决策树规则) print(export_text(clf, feature_namesfeatures))生成的决策树规则如下|--- f7f5 0.52 | |--- f7f5 0.49 | | |--- blue_yellow 0.32 | | | |--- class: linen | | |--- blue_yellow 0.32 | | | |--- class: polyester | |--- f7f5 0.49 | | |--- clear 10738.00 | | | |--- class: poly_linen | | |--- clear 10738.00 | | | |--- class: poly_cotton |--- f7f5 0.52 | |--- class: cotton3. 模型转换为C语言将生成的规则复制给DeepSeek要求他转换为C语言代码即可生成一个判断面料材质的函数。typedef enum { MATERIAL_UNKNOWN 0, MATERIAL_POLYESTER, MATERIAL_COTTON, MATERIAL_LINEN, MATERIAL_POLY_COTTON, MATERIAL_POLY_LINEN } fabric_material_t; /** * brief 根据决策树规则预测材质 * param f7f5 特征值 F7/F5 * param blue_yellow 特征值 (F2F3)/(F5F6) * param clear 原始ADC总光强值 * return 材质枚举 */ fabric_material_t predict_material(float f7f5, float blue_yellow, float clear) { // 第一层判断根据 Python 导出的规则 if (f7f5 0.52) { // 进入左子树非棉材质分支 if (f7f5 0.49) { // 高光泽区涤纶、亚麻 if (blue_yellow 0.32) { return MATERIAL_LINEN; // 亚麻 } else { return MATERIAL_POLYESTER; // 涤纶 } } else { // 中等光泽区混纺 if (clear 10738.00) { return MATERIAL_POLY_LINEN; // 涤麻 } else { return MATERIAL_POLY_COTTON; // 涤棉 } } } else { // 右子树粗糙表面棉 return MATERIAL_COTTON; } }结语本文仅用于我工作过程中的记录只是把自己的想法进行快速验证其专业程度远远不够当然若能为大家提供一些参考给大家避坑我觉得也是非常有价值的。由于公司项目延期该方案仅仅停留在 demo 阶段应该不会再深入开发下去了。当然也欢迎大家一起交流。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门