前言
在项目中遇到了用户交互问题,设备需要用户选择面料,但是一般用户并不认识面料材质,于是希望开发能够自动识别面料材质的功能。在寻找解决方案时发现不同物质吸收的光谱不同,这一原理被用在了面料检测上,目前已有有高校论文证明了技术可行,于是希望搭建一个简单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: 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 特征值 (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 阶段,应该不会再深入开发下去了。当然也欢迎大家一起交流。