4. 数据采集方法

TinyML手势识别项目的成败,首先取决于数据质量。模型再精巧、训练再充分,如果采集到的IMU数据本身存在混叠、漂移或信息缺失,最终识别准确率也会大打折扣。本章围绕“如何用Arduino Nano 33 BLE Sense开发板正确采集加速度计与陀螺仪数据”展开,重点解决三个核心问题:采样频率如何设定、采样时长如何估算、以及如何在资源受限的MCU上稳定地完成数据记录。我们将从IMU传感器的工作原理出发,逐步深入到寄存器配置、代码实现与数据校验,最终形成一个可复用的采集流程。

硬件层 配置层 数据流层 Arduino Nano 33 BLE Sense nRF52840 Cortex-M4F @64MHz LSM9DS1 IMU 3轴加速度 + 3轴陀螺仪 I2C总线 SCL: D15, SDA: D14, 400kHz USB-UART桥接 串口输出CSV数据 加速度计量程 ±2g / ±4g / ±8g / ±16g 陀螺仪量程 ±245 / ±500 / ±2000 dps 输出数据速率 ODR 加速度: 10-952Hz, 陀螺仪: 14.9-952Hz FIFO缓冲 32级深度,可减少中断频率 原始数据读取 readAccel() + readGyro() 时间戳标记 micros() 精确到微秒 CSV格式化 t,ax,ay,az,gx,gy,gz 串口发送 115200 baud, 每行一条记录 图4-1 IMU数据采集链路:硬件 → 配置 → 数据流

图4-1展示了从开发板硬件到数据输出的完整链路。Arduino Nano 33 BLE Sense板载的LSM9DS1传感器通过I2C总线与主控nRF52840通信,主控读取原始寄存器数据后,经过时间戳标记和格式化,最终通过USB串口发送到PC端保存。整个链路中,采样频率的设定决定了数据的时间分辨率,而采样时长则决定了数据集的覆盖范围。下面我们逐一深入。

4.1 核心原理:IMU采样频率与手势频带的关系

手势识别本质上是对人体运动信号进行分类。人体自然手势的频带范围通常在0.5Hz到10Hz之间,例如挥手约1-2Hz,快速敲击约5-8Hz,而一些突发性动作(如甩腕)可能包含高达15Hz的谐波成分。根据奈奎斯特采样定理,采样频率必须至少为信号最高频率的两倍,才能无失真地重建信号。因此,理论上30Hz的采样率即可覆盖大多数手势。但在实际工程中,我们还需要考虑以下因素:

  • 抗混叠滤波:LSM9DS1内部没有硬件抗混叠滤波器,如果采样率过低,高频噪声会折叠到低频段,污染手势特征。建议采样率至少为手势最高频率的5-10倍。
  • 特征提取需求:TinyML模型通常使用滑动窗口提取统计特征(均值、方差、峰值等),较高的采样率能提供更平滑的波形,减少特征估计的偏差。
  • MCU处理能力:nRF52840主频64MHz,I2C读取一次6轴数据约需1-2ms。若采样率超过500Hz,主控将忙于读取数据而无法执行其他任务。

综合以上因素,对于手势识别项目,推荐采样频率为50Hz-100Hz。50Hz足以捕捉10Hz以内的手势主频,100Hz则能提供更丰富的细节,同时留给MCU充足的处理余量。若手势包含快速冲击成分(如拍手、弹指),可考虑200Hz,但需注意数据量成倍增长。

手势类型典型频带推荐采样率单次采样时长
慢速挥手(打招呼)0.5-2Hz20-50Hz2-3秒
常规手势(滑动、旋转)1-5Hz50-100Hz1-2秒
快速手势(敲击、甩动)3-10Hz100-200Hz0.5-1秒
复合手势(多段动作)0.5-8Hz100Hz2-4秒

采样时长的设定则取决于手势的持续时间和数据增强需求。每个手势样本应包含完整的动作周期,并预留前后各0.2-0.3秒的静止段,以便模型学习“静止→运动→静止”的过渡模式。例如,一个持续1秒的滑动手势,建议采集1.5秒的数据窗口。

4.2 操作流程:从寄存器配置到串口输出

在Arduino环境中,我们使用Arduino_LSM9DS1库来简化IMU的配置。该库封装了底层寄存器操作,但理解其背后的配置逻辑仍然重要。以下是完整的采集流程:

  1. 初始化I2C与传感器:调用IMU.begin(),该函数会检测传感器并设置默认量程(加速度±4g,陀螺仪±245dps)。
  2. 配置量程与ODR:通过IMU.setAccelRange()和IMU.setGyroRange()调整量程。对于手势识别,加速度建议设为±4g或±8g(防止剧烈动作削波),陀螺仪设为±500dps或±2000dps。ODR则通过IMU.setAccelODR()和IMU.setGyroODR()设置,可选值包括10Hz、50Hz、100Hz、200Hz等。
  3. 校准传感器:在采集前,让开发板静止水平放置2秒,计算加速度计和陀螺仪的零偏值,并在后续读数中减去该偏移。
  4. 循环读取数据:在loop()中,使用IMU.readAcceleration()和IMU.readGyroscope()读取6轴数据,同时用micros()记录时间戳。
  5. 格式化输出:将数据拼接为CSV格式字符串,通过Serial.println()发送。波特率设为115200,确保传输速率高于数据产生速率。
#include <Arduino_LSM9DS1.h>

const float accelBias[3] = {0.02, -0.01, 0.03};  // 示例校准值
const float gyroBias[3] = {0.5, -0.3, 0.2};     // 示例校准值

void setup() {
  Serial.begin(115200);
  while (!Serial);
  
  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
  
  // 配置量程与ODR
  IMU.setAccelRange(IMU_ACCEL_RANGE_4G);
  IMU.setGyroRange(IMU_GYRO_RANGE_500DPS);
  IMU.setAccelODR(100);  // 100Hz
  IMU.setGyroODR(100);   // 100Hz
  
  Serial.println("t,ax,ay,az,gx,gy,gz");
}

void loop() {
  float ax, ay, az, gx, gy, gz;
  
  if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
    unsigned long t = micros();
    
    IMU.readAcceleration(ax, ay, az);
    IMU.readGyroscope(gx, gy, gz);
    
    // 减去零偏
    ax -= accelBias[0]; ay -= accelBias[1]; az -= accelBias[2];
    gx -= gyroBias[0]; gy -= gyroBias[1]; gz -= gyroBias[2];
    
    // 输出CSV行
    Serial.print(t);
    Serial.print(",");
    Serial.print(ax, 4); Serial.print(",");
    Serial.print(ay, 4); Serial.print(",");
    Serial.print(az, 4); Serial.print(",");
    Serial.print(gx, 4); Serial.print(",");
    Serial.print(gy, 4); Serial.print(",");
    Serial.println(gz, 4);
  }
}

上述代码中,accelerationAvailable()和gyroscopeAvailable()用于检查FIFO中是否有新数据。当ODR设置为100Hz时,每10ms会有一个新样本,MCU有充足时间完成读取和输出。注意,micros()返回的是微秒级时间戳,在100Hz采样下,相邻时间戳间隔约为10000微秒,可用于后续数据同步和插值。

4.3 案例拆解:采集“挥拳”手势的完整过程

假设我们需要为“挥拳”手势(快速向前出拳并收回)采集训练数据。该手势持续约0.8秒,包含明显的加速度冲击(峰值可达±6g)和角速度旋转(峰值约300dps)。我们按照以下步骤操作:

场景设定:采集环境为室内办公桌旁,开发板通过USB线连接笔记本电脑,使用Arduino IDE的串口监视器或Python脚本保存数据。目标采集50个正样本和50个负样本(其他随机动作)。

第一步:参数选择。根据手势特征,加速度量程设为±8g(避免削波),陀螺仪量程设为±500dps(足够覆盖旋转速度)。采样频率设为100Hz,采样时长设为1.5秒(包含0.3秒前静止、0.8秒动作、0.4秒后静止)。

第二步:编写采集脚本。在PC端使用Python的pyserial库读取串口数据,当检测到用户按下键盘空格键时开始记录,持续1.5秒后自动停止并保存为CSV文件。脚本同时显示实时波形,便于观察数据质量。

import serial
import time
import csv

ser = serial.Serial('COM3', 115200, timeout=1)
input("Press Enter to start...")
start_time = time.time()
duration = 1.5  # 秒
samples = []

while time.time() - start_time < duration:
    line = ser.readline().decode().strip()
    if line:
        samples.append(line.split(','))

with open('punch_001.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['t', 'ax', 'ay', 'az', 'gx', 'gy', 'gz'])
    writer.writerows(samples)
print(f"Saved {len(samples)} samples")

第三步:执行采集与质量检查。采集过程中,观察Python打印的波形图,确认:① 静止段加速度模值接近1g(重力加速度);② 动作段加速度峰值未触及量程上限;③ 时间戳间隔均匀(约10ms±2ms)。若发现数据异常,立即停止并调整。

第四步:数据标注与存储。每个样本文件以“手势类别_编号”命名,如punch_001.csv、random_001.csv。同时记录采集时的环境信息(如佩戴位置、动作方向)到元数据文件,便于后续分析。

结果与复盘:共采集50个正样本和50个负样本,每个样本约150行数据。检查发现,有3个正样本的峰值加速度超过±8g导致削波,重新采集后解决。另有2个负样本包含明显的抖动噪声,分析原因是采集时手部未完全放松,后续在采集指导中增加了“保持自然放松”的提示。

4.4 常见误区与总结复盘

在数据采集环节,初学者常犯以下错误,需要特别注意:

误区一:采样率越高越好。过高的采样率(如500Hz)会导致数据量爆炸,且MCU可能因I2C读取耗时过长而丢失样本。实际上,100Hz已能满足绝大多数手势识别需求。若确需高频,应使用DMA或FIFO中断方式。

误区二:忽略传感器校准。未校准的加速度计可能有±0.1g的零偏,陀螺仪零偏可达±5dps。这些偏差在静止段会表现为非零值,导致模型学习到错误的“静止”模式。务必在每次采集前执行2秒静止校准。

误区三:采样时长不足。手势动作前后的过渡段(静止→运动→静止)对模型识别至关重要。若只采集动作核心段,模型将无法区分“开始动作”和“动作中”的状态。

经验建议:① 在采集脚本中加入实时数据质量监控,包括时间戳间隔、量程溢出检测、静止段方差等指标;② 每个手势类别至少采集30个样本,且在不同时间段(上午/下午)和不同佩戴位置(手腕/手背)采集,增加数据多样性;③ 保存原始数据的同时,记录采集参数(采样率、量程、校准值)到JSON配置文件,确保数据可复现。

总结本章,数据采集是TinyML手势识别的基础工程。通过合理设置采样频率(50-100Hz)和采样时长(1-2秒),配合正确的量程选择和校准流程,我们能够获得高质量的训练数据。下一阶段,我们将基于这些数据构建特征工程和模型训练流程。