ARTICLE DETAIL

资讯详情

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

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

从零搭建衣物面料识别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(高度 29mm)

F1 1356

F2 6629

F3 5307

F4 8129

F5 20413

F6 16435

F7 9821

F8 8120

NIR 1062

CLEAR 16240

F1 1342

F2 6594

F3 5255

F4 8020

F5 20176

F6 16316

F7 9683

F8 8089

NIR 1005

CLEAR 15382

F1 1335

F2 6502

F3 5178

F4 7951

F5 19843

F6 16022

F7 9568

F8 7886

NIR 1078

CLEAR 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_size=0.2 分出来只有3条) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 4. 训练决策树(限制深度,防止过拟合) clf = DecisionTreeClassifier(max_depth=3, random_state=42) 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_names=features))

生成的决策树规则如下:

|--- 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: cotton

3. 模型转换为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 特征值 (F2+F3)/(F5+F6) * @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 阶段,应该不会再深入开发下去了。当然也欢迎大家一起交流。

返回列表