3、数据采集与预处理:触摸数据格式解析、噪声滤波、数据归一化
好,咱们进入第三章。说实话,这一章是整门课里最「脏」最累的活,但也是决定你算法能不能跑起来的关键。我见过太多人,模型调得飞起,一上硬件数据全是乱的,直接翻车。嗯,咱们今天就把这块硬骨头啃下来。
3.1 触摸数据格式解析——你得先看懂硬件在说什么
手环的触摸屏,说白了就是一块电容感应阵列。手指点上去,电容值变化,芯片把这种变化转成坐标和压力值。但不同厂商、不同型号的芯片,吐出来的数据格式千奇百怪。
我个人习惯,拿到一个新硬件,第一件事就是看它的数据手册。别急着写代码,先搞清楚这几个关键字段:
- 坐标格式:是绝对坐标还是相对坐标?一般是绝对坐标,范围可能是0-240,0-240,也可能是0-1023。
- 触摸点数:支持单点还是多点?手环通常只支持单点或两点。
- 数据包结构:一帧数据有多少字节?每个字节代表什么?
- 校验方式:有没有CRC或累加和校验?
举个例子,我手头这块FT6236触摸芯片,它的数据包格式是这样的:
// FT6236 触摸数据包格式(I2C读取)
// 寄存器地址 0x02 开始,读取 6 个字节
// Byte 0: 触摸状态 (0x00=无触摸, 0x01=有触摸)
// Byte 1: 触摸点1 X坐标高8位
// Byte 2: 触摸点1 X坐标低8位 (实际只用低4位)
// Byte 3: 触摸点1 Y坐标高8位
// Byte 4: 触摸点1 Y坐标低8位 (实际只用低4位)
// Byte 5: 触摸点1 触摸ID + 事件标志
// 解析代码示例
uint8_t touch_data[6];
uint16_t x, y;
x = ((uint16_t)touch_data[1] & 0x0F) << 8 | touch_data[2];
y = ((uint16_t)touch_data[3] & 0x0F) << 8 | touch_data[4];
3.2 噪声滤波——把脏数据洗干净
原始触摸数据,说白了就是一堆带毛刺的信号。手指按下去,电容值不是稳定的,会有抖动。环境电磁干扰、电源噪声,都会让坐标跳来跳去。你想想看,如果直接把这种数据送进手势识别算法,那结果肯定是一团糟。
我常用的两种滤波方法:中值滤波和均值滤波。它们各有各的脾气。
3.2.1 中值滤波——对付「野点」的利器
中值滤波的原理很简单:取连续N个采样值,排序后取中间那个。它能很好地干掉那些突然跳变的「野点」,而且不会模糊边缘。
为什么说它适合触摸数据?因为触摸手势的轨迹是连续的,偶尔会因为静电或干扰出现一个离谱的坐标点。中值滤波能把这个点直接扔掉,不影响前后数据。
// 中值滤波实现(窗口大小=5)
#define MEDIAN_WINDOW_SIZE 5
uint16_t median_filter(uint16_t new_value, uint16_t *buffer) {
uint16_t temp[MEDIAN_WINDOW_SIZE];
uint16_t i, j;
// 滑动窗口:新数据入队,旧数据出队
for(i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
buffer[i] = buffer[i+1];
}
buffer[MEDIAN_WINDOW_SIZE - 1] = new_value;
// 拷贝并排序
for(i = 0; i < MEDIAN_WINDOW_SIZE; i++) {
temp[i] = buffer[i];
}
// 冒泡排序(窗口小,效率可以接受)
for(i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
for(j = 0; j < MEDIAN_WINDOW_SIZE - 1 - i; j++) {
if(temp[j] > temp[j+1]) {
uint16_t t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
return temp[MEDIAN_WINDOW_SIZE / 2]; // 返回中值
}
3.2.2 均值滤波——平滑轨迹的好帮手
均值滤波,就是把连续N个值加起来求平均。它能有效抑制高频噪声,让轨迹变得平滑。但代价是,它会模糊掉手势的细节,比如快速点击的尖峰。
我个人习惯,在需要平滑滑动轨迹时用均值滤波,在需要保留点击动作时用中值滤波。有时候也会组合使用:先中值滤波去野点,再均值滤波平滑。
// 均值滤波实现(窗口大小=4)
#define AVG_WINDOW_SIZE 4
uint16_t avg_filter(uint16_t new_value, uint16_t *buffer, uint8_t *index) {
uint32_t sum = 0;
uint8_t i;
buffer[*index] = new_value;
*index = (*index + 1) % AVG_WINDOW_SIZE;
for(i = 0; i < AVG_WINDOW_SIZE; i++) {
sum += buffer[i];
}
return (uint16_t)(sum / AVG_WINDOW_SIZE);
}
3.3 数据归一化——让算法「看懂」数据
归一化,说白了就是把不同量纲的数据映射到同一个范围。触摸屏的坐标可能是0-240,也可能是0-1023。手势识别算法通常期望输入在0-1或者-1到1之间。
为什么需要归一化?
- 消除量纲影响:不同硬件坐标范围不同,归一化后算法可以通用。
- 加速收敛:神经网络等算法,输入数据范围一致时,训练收敛更快。
- 避免数值溢出:嵌入式MCU的浮点运算能力有限,归一化到定点数范围可以避免溢出。
常用的归一化方法有两种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Min-Max归一化 | x' = (x - min) / (max - min) | 坐标值范围已知且固定 |
| Z-Score归一化 | x' = (x - μ) / σ | 数据分布未知,需要自适应 |
在手环这种资源受限的设备上,我强烈建议用Min-Max归一化。因为坐标范围是硬件决定的,是固定的。比如屏幕分辨率是240x240,那min=0,max=239,直接算就行,不需要统计均值和方差。
// Min-Max归一化(定点数实现,Q8格式)
// 假设触摸屏分辨率 240x240
#define TOUCH_X_MIN 0
#define TOUCH_X_MAX 239
#define TOUCH_Y_MIN 0
#define TOUCH_Y_MAX 239
// 归一化到 0-255(8位定点数)
uint8_t normalize_x(uint16_t x) {
// x' = (x - min) * 255 / (max - min)
return (uint8_t)((uint32_t)(x - TOUCH_X_MIN) * 255 / (TOUCH_X_MAX - TOUCH_X_MIN));
}
uint8_t normalize_y(uint16_t y) {
return (uint8_t)((uint32_t)(y - TOUCH_Y_MIN) * 255 / (TOUCH_Y_MAX - TOUCH_Y_MIN));
}
3.4 实战中的坑与避坑指南
嗯,说了这么多理论,咱们来点实际的。我在项目里踩过的坑,今天全抖出来。
- 坑一:数据采集频率不稳定。触摸芯片的采样率可能不是固定的,有时候会因为I2C总线被占用而丢帧。我建议在数据采集时加上时间戳,或者用定时器固定采样间隔。
- 坑二:滤波窗口的边界处理。刚上电时,滤波缓冲区全是0,前几个滤波结果会偏小。我曾经因为这个,导致手势识别一开始总是误判。解决办法:用第一个有效值填充整个缓冲区。
- 坑三:归一化参数写死。不同批次的手环,触摸屏的坐标范围可能有细微差异。我建议在出厂校准时,动态读取触摸屏的边界值,而不是写死在代码里。
核心总结:
- 解析触摸数据前,先读懂数据手册的位定义。
- 中值滤波去野点,均值滤波平滑轨迹,组合使用效果更佳。
- 归一化用Min-Max,映射到0-255,省内存省时间。
- 边界处理和动态校准,能避免很多线上问题。
好了,这一章的内容就到这儿。数据预处理这块,说白了就是「垃圾进,垃圾出」。你前面处理得越干净,后面的手势识别算法就越省心。下一章,咱们开始讲特征提取,那才是真正考验算法功底的地方。
公众号:蓝海资料掘金营,微信deep3321