从零搭建衣物面料识别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 阶段,应该不会再深入开发下去了。当然也欢迎大家一起交流。
更多推荐

所有评论(0)