木子呢 发表于 2024-11-15 12:07:26

TinyML电子鼻E-nose:使用MEMS气体传感器和edge impulse平台

电子鼻(Electronic Nose)是一种模拟人类嗅觉系统的仪器,它能够检测和识别气味。

电子鼻是由多种不同的气体传感器组成,通过tinyML训练过后,可识别到不同气味的特征。

在这个项目中,我使用DFRobot mems系列气体传感器和Edge Impulse平台开发了一个能够识别饮料和水果气味的电子鼻。在这过程中,我也实验了水果F·B维护的预测实验。完成了识别饮料和水果气味的实验后,我也进行了一系列的探索实验,看这个电子鼻是否能识别其他的味道,希望了解这个电子鼻的能力边界。

通过这个项目,我们验证了,使用 DFRobot mems 系列气体传感器的电子鼻可以分辨饮料和水果,且有一定程度上进行预防性水果F·B的能力,进一步的也发现在一定的条件下,电子鼻可以识别多种气味。希望本项目能够探索电子鼻技术的潜力,并为未来开发更广泛的气味识别应用奠定基础。
## 设计过程
为了完成电子鼻的功能,我考虑从硬件上和软件上对其进行考虑和设计。在进行一系列的调研后,设计方案和内容如下。
### 硬件
- (https://www.dfrobot.com.cn/goods-3048.html)
- (https://www.dfrobot.com.cn/goods-3681.html)
- (https://www.dfrobot.com.cn/goods-3317.html)
- (https://www.dfrobot.com.cn/goods-504.html)
- (https://www.dfrobot.com.cn/goods-3401.html)
- (https://www.dfrobot.com.cn/goods-3293.html)
- (https://www.dfrobot.com.cn/goods-1734.html)
- [迷你面包板](https://www.dfrobot.com.cn/goods-351.html)
- (https://www.dfrobot.com.cn/goods-1405.html)
- [铝制散热器散热风扇 (LattePanda V1)](https://www.dfrobot.com.cn/goods-1381.html)

电子鼻的传感器是整个项目中的重要组成部分。

**DFRobotFermion MEMS系列气体传感器**利用MEMS工艺在Si基衬底上制作微热板,所使用的气敏材料是在清洁空气中电导率较低的金属氧化物半导体材料。当环境空气中有被检测气体存在时传感器电导率发生变化,该气体的浓度越高,传感器的电导率就越高。该系列传感器结构坚固,使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出信号。它的小尺寸和低功耗也使得它很容易与其他设备进行集成。

为了实现电子鼻的功能,我需要采集气味数据并进行分析。因此,我选择了DFRobot MEMS系列气体传感器来测量气味的成分并输出模拟信号。为了同时采集这10个MEMS传感器的信号,我使用了MEGA2560作为主控,并特意制作了一个mega2560扩展板,用于安装组合传感器阵列。这个阵列可以检测传感器对不同气味的响应情况并找出主要响应的传感器。同时,我还使用了两个风扇来让密封空间中的气体对流,以便气味更均匀地分布在空间里。

为了储存传感器数据,我还配备了串口记录模块,使得MEGA2560主控可以脱离电脑直接存储传感器数据。这样可以保证数据的准确性和可靠性。

图 1 实验装置示意图

后期经过实验后找到相对应的主要传感器后设计的基于 ESP32 的电子鼻系统


图 2 基于 ESP32 的电子鼻系统

### 二、软件平台
为了简化软件的开发难度,我选用了Edge Impulse 这款边缘 AI 平台。
Edge Impulse是一款为企业团队打造创新产品的边缘AI平台。该平台可以优化模型并轻松地部署到任何边缘设备上。它是专门处理真实世界传感器数据的平台,可以加速产品开发,同时最小化风险。
由于该平台((http://))简单易用,相关内容在网站上有详细介绍,在此就不在赘述。

### 三、饮料气味分析
网上很多的团队已经使用电子鼻对饮料进行检测,效果不错,因此我们先对饮料气味进行检测,检验电子鼻的基本能力。
#### 数据收集和预处理
在收集数据方面,我选择了葡萄汁和可乐这两种常见的饮料作为实验对象。我首先将每种饮料放入一个封闭的容器中静置十分钟,然后将气体传感器放入容器并开始测量气味数据。我测量了每种饮料多次,并将每次测量的数据保存在电脑上,将得到的csv数据文件命名为气味的名称,将每一行数据打上label。



可以从两个图表对比中,看到部分气体传感器对于两种气味的输出有明显差异。


图7 气味数据分布图



#### 训练模型
我使用Edge Impulse平台来训练模型。将数据上传到平台上,并使用标记功能将每个数据集标记为“葡萄汁”或“可乐”。通过特征相关性排序(各类特征相对于其他所有类别的重要性),可以筛选出对饮料气味分析来说更重要的特征。



#### 实验结果
经过训练和测试,我得到了一个较准确的模型,它可以自动识别和分类葡萄汁和可乐的气味。模型分类的准确率达到了91.67%,表明可以有效地区分这两种饮料的气味。


图 12 模型准确率



根据实验,我发现我设计的电子鼻可以顺利了识别饮料的区别,该结论和大部分电子鼻团队得出的结果类似。
### 四、水果fubai气味分析
用MEMS系列传感器制作的电子鼻可以在嵌入式设备上长时间运行,采集水果散发的气味,使用机器学习技术对水果F·B时产生的气味进行分析和识别,可以帮助我们及时检测水果的fubai情况,从而减少浪费和损失。
#### 主要应用场景
- 评估食品质量
- 水果店
- 冰箱食物气味检测

#### 数据收集和预处理
在收集数据方面,我选择了芒果这种常见的水果作为实验对象。实验物品有略带青色的芒果和开始腐烂的芒果,首先将传感器预热五分钟,然后将实验物品放入容器并开始测量气味数据,每次测量后让风扇清洗容器五分钟,等待容器中的气体恢复洁净的状态。我将每次测量的数据保存成CSV文件,并将文件命名为气味的名称,每一行数据打上label。


图 13 传感器数据空间分布图



#### 训练模型
使用平台的EON Tuner根据导入的传感器数据可以自动训练模型,右上角可以选择目标主控,模型会根据目标主控的类型调整模型参数和能力。


图 15 训练完成的模型列表



可以看到EON Tuner训练出的模型准确率不错,但可能存在过拟合的情况


图 16 模型准确率



#### 实验结果
将测试集导入模型中,查看最终得分。

图 17 模型测试结果



#### 水果fubai维护管理(以Firebeetle esp32-E为例)
久置的芒果会很快fubai,表面出现黑斑。我使用MEMS系列CH4,CO,Odor,CH2O传感器组合搭配进行为时两天的芒果气味情况检测。ESP32-E主控搭配ADS1115模块和RTC模块,每过半小时读取一次传感器数据,在开始监测前,传感器已经预热了半小时。将表面无明显黑斑的新鲜芒果放入气室。由于夜间气温稍微降低,整体的数据曲线有一些下降。可以看到在过了约8个小时后,传感器的数据有明显上升,芒果表面也逐渐出现小黑点。


图 18 芒果气味数据趋势图




Esp32代码:
```
#include <DFRobot_ADS1115.h>

    #include "GravityRtc.h"
    #include "Wire.h"

DFRobot_ADS1115 ads(&Wire);
GravityRtc rtc;   //RTC Initialization
unsigned long start_time;// 保存程序开始时的时间

void setup(void)
{
Serial.begin(115200);
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");
ads.setAddr_ADS1115(ADS1115_IIC_ADDRESS0);   // 0x48
ads.setGain(eGAIN_TWOTHIRDS);   // 2/3x gain
ads.setMode(eMODE_SINGLE);       // single-shot mode
ads.setRate(eRATE_128);          // 128SPS (default)
ads.setOSMode(eOSMODE_SINGLE);   // Set to start a single-conversion
ads.init();
delay(20);
rtc.setup();
start_time = millis();// 记录程序开始时的时间
//Serial.print("Elapsedtime(ms):,");
//Serial.print("\t");
Serial.print("CH4,");
Serial.print("\t");
Serial.print("CO,");
Serial.print("\t");
Serial.print("Odor");
Serial.print("\t");
Serial.println("CH2O");
}

void loop(void)
{
rtc.read();
if((rtc.minute % 30) == 0 && rtc.second == 1){
    for(byte i = 1; i < 5; i = i + 1){
      if (ads.checkADS1115()){
      int16_t adc0, adc1,adc2, adc3;
      adc0 = ads.readVoltage(0);
      adc1 = ads.readVoltage(1);
      adc2 = ads.readVoltage(2);
      adc3 = ads.readVoltage(3);

      float sensorValue0 = adc0;
      float sensorValue1 = adc1;
      float sensorValue2 = adc2;
      float sensorValue3 = adc3;
   
      Serial.print(String(sensorValue0));
      Serial.print("\t");
      Serial.print(String(sensorValue1));
      Serial.print("\t");
      Serial.print(String(sensorValue2));
      Serial.print("\t");
      Serial.println(String(sensorValue3));
   
      delay(1000);   
      }

      }
   
}

}
```

### 五、气味探索
为了进一步了解电子鼻的能力,我们进行了一些其他物品的测试。主要是对一些特定气味的实验,意图了解其分辨能力以及局限。
#### 1. 酸:柠檬、食醋
分析:对于不同类型的酸味,传感器的响应情况不同。


图 21 柠檬气味数据图


图 22 醋气味数据图





#### 腥:海鲜
预热5分钟后,盒子中放入虾,多次采集数据。


图 23 实验过程



分析:H2S 传感器有明显响应,且响应曲线相似。


图 24 虾气味数据图


#### 3. 臭:蒜
预热5分钟后,盒子中放入新鲜大蒜。多次采集数据(10分钟)。


图 24 实验过程



分析:Odor(口气传感器) 和 VOC(检测挥发性有机化合物) 对蒜味有明显响应。


图 25 大蒜气味数据图



#### 4. 自然:薄荷
薄荷是一种散发味道且人能感知的植物,经过测试发现传感器对表面无破损,挥发性不强的草本植物感应较弱,暂时不能分辨。


图 26 薄荷气味数据图



### 六、模型部署与验证
经过对历史数据的检验和分析,我们得到了传感器的能力和局限的了解。为了让设备可以实现本地的判断,我们需要将该电子鼻模型进行优化,并部署在本地,实现本地的推理能力。我们将模型部署到ESP32-E和验证的过程记录如下。
**2024.10更新:添加对ESP32S3的硬件支持,使用教程详情请看视频链接,代码请看文章末尾。(视频:https://www.bilibili.com/video/BV1wdDSYjE9k/)
#### 配置环境(以windows系统为例)
1. 在电脑上安装(https://www.python.org/) 。
2. 在电脑上安装(https://nodejs.org/en/) v14 或更高版本。
- 对于 Windows 用户,在提示时安装附加的 Node.js 工具(在较新版本中称为 Native Modules 工具)。
1. 通过以下命令安装 CLI 工具:`npm install -g edge-impulse-cli --force`现在这些工具应该已经在你的 PATH 中可用了。

#### 收集证据
FireBeetle ESP32-E主控,搭配DFRobot I2C ADS1115 16位AD转换模块,可对模拟量信号进行精确的采集与转换。
代码:
```
#include <DFRobot_ADS1115.h>

DFRobot_ADS1115 ads(&Wire);
unsigned long start_time;// 保存程序开始时的时间

void setup(void)
{
Serial.begin(115200);
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");
ads.setAddr_ADS1115(ADS1115_IIC_ADDRESS0);   // 0x48
ads.setGain(eGAIN_TWOTHIRDS);   // 2/3x gain
ads.setMode(eMODE_SINGLE);       // single-shot mode
ads.setRate(eRATE_128);          // 128SPS (default)
ads.setOSMode(eOSMODE_SINGLE);   // Set to start a single-conversion
ads.init();
start_time = millis();// 记录程序开始时的时间
//Serial.print("Elapsedtime(ms):,");
//Serial.print("\t");
Serial.print("CH4,");
Serial.print("\t");
Serial.print("CO,");
Serial.print("\t");
Serial.print("Odor");
Serial.print("\t");
Serial.println("CH2O");
}

void loop(void)
{
      if (ads.checkADS1115())
    {
      int16_t adc0, adc1,adc2, adc3;
      adc0 = ads.readVoltage(0);
      adc1 = ads.readVoltage(1);
      adc2 = ads.readVoltage(2);
      adc3 = ads.readVoltage(3);
      unsigned long current_time = millis();// 获取当前时间
      unsigned long elapsed_time = current_time - start_time;// 计算自程序开始以来经过的毫秒数

      float sensorValue0 = adc0;
      float sensorValue1 = adc1;
      float sensorValue2 = adc2;
      float sensorValue3 = adc3;
   
      Serial.print(String(sensorValue0));
      Serial.print("\t");
      Serial.print(String(sensorValue1));
      Serial.print("\t");
      Serial.print(String(sensorValue2));
      Serial.print("\t");
      Serial.println(String(sensorValue3));
   
      delay(1000);   
      }

}
```
打开WINDOWS POWERSHELL,输入$ edge-impulse-data-forwarder --frequency 1
填入传感器名称,即可连接到平台。


图 27

在平台列表里的Devices中可以看到ESP32-E设备在线。


图 28

选中平台列表中的Data acquisition,填好label,点击start sampling可以开始收集数据。在神经网络中训练集和测试集的比例可以根据具体情况而定,但是一般情况下,我们会将数据集划分为训练集、测试集。其中训练集用于训练模型,而测试集则用于评估模型的性能,即模型对未知数据的预测能力。


图29
Create impulse

图 30

选择Esp32作为目标主控,可以让生成的模型更符合目标主控的算力。


图 31

选择用8位整数(int8)来表示机器学习模型的输出,可以显著减少在低功耗边缘设备上运行模型所需的内存和计算资源,而此类设备通常具有有限的计算资源。Int8数据格式仅使用1个字节来表示每个值,而float32使用4个字节。这意味着使用int8可以将模型大小减少4倍。此外,一些硬件加速器针对int8数据类型进行了优化,可以提供更快,更高效的模型执行。但是,与float32相比,使用int8可能会降低模型输出的精度,因此需要在选择适当的数据类型时进行权衡。


图 32

读取ESP32的数据,使用live classification预测种类。


图 33

经过以上处理后,适用于 ESP32 的模型就准备好了,下面我们将进行模型的验证。

#### 模型验证<以Firebeetle esp32-E,ArduinoIDE为例>
1. ArduinoIDE加载zip库
2. 将传感器的一次数据填入features[],运行stactic buffer代码.

```
/* Edge Impulse ingestion SDK
* Copyright (c) 2022 EdgeImpulse Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

/* Includes ---------------------------------------------------------------- */
#include <esp32_fruit_only_inferencing.h>

static const float features[] = {
1356.7017, 162.0838, 1648.8528, 381.1971, 1560.0804, 186.4678, 1897.0874, 438.4866, 1355.7012, 162.0838, 1649.8533, 381.1971, 1226.0417, 146.5643, 1492.6686, 344.7962
    // copy raw features here (for example from the 'Live classification' page)
    // see https://docs.edgeimpulse.com/docs/running-your-impulse-arduino
};

/**
* @brief      Copy raw feature data in out_ptr
*             Function called by inference library
*
* @paramoffset   The offset
* @paramlength   The length
* @param      out_ptrThe out pointer
*
* @return   0
*/
int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) {
    memcpy(out_ptr, features + offset, length * sizeof(float));
    return 0;
}

void print_inference_result(ei_impulse_result_t result);

/**
* @brief      Arduino setup function
*/
void setup()
{
    // put your setup code here, to run once:
    Serial.begin(115200);
    // comment out the below line to cancel the wait for USB connection (needed for native USB)
    while (!Serial);
    Serial.println("Edge Impulse Inferencing Demo");
}

/**
* @brief      Arduino main function
*/
void loop()
{
    ei_printf("Edge Impulse standalone inferencing (Arduino)\n");

    if (sizeof(features) / sizeof(float) != EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
      ei_printf("The size of your 'features' array is not correct. Expected %lu items, but had %lu\n",
            EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, sizeof(features) / sizeof(float));
      delay(1000);
      return;
    }

    ei_impulse_result_t result = { 0 };

    // the features are stored into flash, and we don't want to load everything into RAM
    signal_t features_signal;
    features_signal.total_length = sizeof(features) / sizeof(features);
    features_signal.get_data = &raw_feature_get_data;

    // invoke the impulse
    EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false /* debug */);
    if (res != EI_IMPULSE_OK) {
      ei_printf("ERR: Failed to run classifier (%d)\n", res);
      return;
    }

    // print inference return code
    ei_printf("run_classifier returned: %d\r\n", res);
    print_inference_result(result);

    delay(1000);
}

void print_inference_result(ei_impulse_result_t result) {

    // Print how long it took to perform inference
    ei_printf("Timing: DSP %d ms, inference %d ms, anomaly %d ms\r\n",
            result.timing.dsp,
            result.timing.classification,
            result.timing.anomaly);

    // Print the prediction results (object detection)
#if EI_CLASSIFIER_OBJECT_DETECTION == 1
    ei_printf("Object detection bounding boxes:\r\n");
    for (uint32_t i = 0; i < result.bounding_boxes_count; i++) {
      ei_impulse_result_bounding_box_t bb = result.bounding_boxes;
      if (bb.value == 0) {
            continue;
      }
      ei_printf("%s (%f) [ x: %u, y: %u, width: %u, height: %u ]\r\n",
                bb.label,
                bb.value,
                bb.x,
                bb.y,
                bb.width,
                bb.height);
    }

    // Print the prediction results (classification)
#else
    ei_printf("Predictions:\r\n");
    for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
      ei_printf("%s: ", ei_classifier_inferencing_categories);
      ei_printf("%.5f\r\n", result.classification.value);
    }
#endif

    // Print anomaly result (if it exists)
#if EI_CLASSIFIER_HAS_ANOMALY == 1
    ei_printf("Anomaly prediction: %.3f\r\n", result.anomaly);
#endif

}
```
代码占用esp32内存情况:
```
Sketch uses 332537 bytes (25%) of program storage space. The maximum is 1310720 bytes.
Global variables use 24160 bytes (7%) of dynamic memory, leaving 303520 bytes for local variables. The maximum is 327680 bytes.
```

3.使用live classification多次收集空气数据,新鲜芒果和F·B芒果的数据与edge impulse运行结果比对情况:

图 34
可以看到经过处理的模型可以再 esp32 上正常运行,且模型识别率也达到了 88.16%,虽然较计算机上的识别率较低,但仍然可以在一定程度上完成基本的识别。
该电子鼻在单片机的基础上实现水果F·B的识别,为今后生鲜存储的管理和降本增效提供了有力的工具和思路指导。
## 总结
这是一个电子鼻项目,使用DFRobot mems系列气体传感器和Edge Impulse平台,可以感知饮料和水果的气味。在该项目中,进行了预测实验,探究电子鼻是否可以检测到不同物品气味,同时也研究了芒果的保质期。实验结果表明,DFRobot mems系列气体传感器电子鼻可以区分饮料和水果。此外,该项目为开发更广泛的气味识别应用奠定了基础。这个项目还有一些可扩展和改进的地方,比如可以使用更多种类的饮料或其他物品来进行气味识别测试,再将模型固件落实到主控中。同时也可以从实验中看出,该系列MEMS传感器不适合闻挥发性弱的草本植物,温湿度的变化可能会引起一定的数据漂移。
代码仓库:(https://github.com/polamaxu/smartnose)

Edge Impulse项目:
- 饮料 (http://)
- 水果 (http://)

## FAQ
1. Q: Windows环境下配置环境出错?
A:(http://)
2. Q:缺失C:\Users\DFRobot\AppData\Roaming\npm\node_modules\edge-impulse-cli\build\cli\daemon.js?
A:更改daemon.js文件位置。
3. Q: 报错+CategoryInfo : SecurityError: (:) [],ParentContainsErrorRecordException+FullyQualifiedErrorId : UnauthorizedAccess?
A:以管理员身份打开PowerShell 输入 set-executionpolicy remotesigned

页: [1]
查看完整版本: TinyML电子鼻E-nose:使用MEMS气体传感器和edge impulse平台