4. 手势识别模型部署
任务目标与实现范围
本案例完成一个可部署到STM32的轻量级手势识别闭环:使用公开UCI HAR数据集(加速度计+陀螺仪,6类手势/活动),训练一个1D-CNN模型,转换为TensorFlow Lite格式,并给出STM32端C代码推理框架。核心目标是验证“传感器数据→模型推理→手势分类”的完整链路,而非实现完整嵌入式UI。
范围限定:本案例聚焦模型训练、TFLite转换和STM32部署的最小可复现实现。不包含实时数据采集驱动(使用静态数组模拟传感器输入),不包含RTOS或复杂外设管理。手势类别限定为6类:WALKING, WALKING_UPSTAIRS, WALKING_DOWNSTAIRS, SITTING, STANDING, LAYING。
环境依赖
- Python 3.9+,TensorFlow 2.13+,numpy,pandas,scikit-learn
- STM32CubeIDE 1.13+,STM32F407VG开发板(或任意Cortex-M4,≥128KB RAM)
- TensorFlow Lite for Microcontrollers C++库(tflite-micro)
1. 模型训练与TFLite转换(Python)
以下代码完成数据加载、1D-CNN训练、模型导出为TFLite,并生成C头文件model.h。训练在PC上执行,输出gesture_model.tflite和model.h。
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 1. 加载UCI HAR数据集(假设已下载并解压至 ./UCI_HAR_Dataset)
def load_uci_har():
X_train = np.loadtxt('UCI_HAR_Dataset/train/X_train.txt')
y_train = np.loadtxt('UCI_HAR_Dataset/train/y_train.txt')
X_test = np.loadtxt('UCI_HAR_Dataset/test/X_test.txt')
y_test = np.loadtxt('UCI_HAR_Dataset/test/y_test.txt')
return X_train, y_train, X_test, y_test
X_train, y_train, X_test, y_test = load_uci_har()
# 2. 标准化(基于训练集)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 3. 重塑为序列格式:每个样本128个时间步,每个时间步9个特征(3轴加速度+3轴陀螺仪+3轴总加速度)
# UCI HAR原始数据:561个特征,按固定窗口顺序排列,这里取前128*9=1152个特征作为演示
# 注意:实际完整使用需按官方特征顺序切分,此处为演示简化
n_steps, n_features = 128, 9
X_train_seq = X_train[:, :n_steps*n_features].reshape(-1, n_steps, n_features)
X_test_seq = X_test[:, :n_steps*n_features].reshape(-1, n_steps, n_features)
# 标签从1-6转为0-5
y_train -= 1
y_test -= 1
# 4. 构建1D-CNN模型(轻量级,适合MCU)
model = tf.keras.Sequential([
tf.keras.layers.Input(shape=(n_steps, n_features)),
tf.keras.layers.Conv1D(16, 3, activation='relu'),
tf.keras.layers.MaxPooling1D(2),
tf.keras.layers.Conv1D(16, 3, activation='relu'),
tf.keras.layers.GlobalAveragePooling1D(),
tf.keras.layers.Dense(6, activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.summary()
# 5. 训练(少量epochs用于演示,实际需更多)
history = model.fit(X_train_seq, y_train, epochs=5, batch_size=32, validation_split=0.1, verbose=1)
# 6. 评估
test_loss, test_acc = model.evaluate(X_test_seq, y_test, verbose=0)
print(f'Test accuracy: {test_acc:.4f}') # 仅用于参考,非部署验证
# 7. 转换为TFLite并量化(int8)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = lambda: (X_test_seq[i:i+1].astype(np.float32) for i in range(100))
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
# 8. 保存模型和C头文件
with open('gesture_model.tflite', 'wb') as f:
f.write(tflite_model)
# 生成model.h(用于STM32)
c_array = ', '.join(f'0x{b:02x}' for b in tflite_model)
with open('model.h', 'w') as f:
f.write(f'const unsigned char model_tflite[] = {{{c_array}}};\n')
f.write(f'const unsigned int model_tflite_len = {len(tflite_model)};\n')
print("TFLite model saved: gesture_model.tflite")
print("C header saved: model.h")
2. STM32部署代码(C++)
以下代码在STM32上执行推理。使用静态数组模拟传感器输入(取自测试集前20个样本),通过tflite-micro加载模型并输出分类结果。需将model.h加入工程,并链接tflite-micro库。
#include "model.h"
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
// 模拟传感器输入:从测试集提取的20个样本(每个样本128*9=1152个float)
// 实际项目中替换为实时加速度计数据
const float simulated_input[20][1152] = { /* 从Python导出前20个测试样本 */ };
void setup() {
Serial.begin(115200);
while(!Serial);
Serial.println("Gesture recognition demo");
// 加载模型
static tflite::AllOpsResolver resolver;
static const tflite::Model* model = tflite::GetModel(model_tflite);
static tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, kTensorArenaSize);
// 分配内存
TfLiteStatus allocate_status = interpreter.AllocateTensors();
if (allocate_status != kTfLiteOk) {
Serial.println("AllocateTensors failed");
while(1);
}
// 获取输入输出张量
TfLiteTensor* input = interpreter.input(0);
TfLiteTensor* output = interpreter.output(0);
// 对20个模拟样本进行推理
for (int i = 0; i < 20; i++) {
// 输入为int8,需要量化参数
float input_scale = input->params.scale;
int input_zero_point = input->params.zero_point;
// 将float数据转为int8
for (int j = 0; j < 1152; j++) {
input->data.int8[j] = (int8_t)(simulated_input[i][j] / input_scale + input_zero_point);
}
// 运行推理
if (interpreter.Invoke() != kTfLiteOk) {
Serial.println("Invoke failed");
return;
}
// 获取输出(int8)
float output_scale = output->params.scale;
int output_zero_point = output->params.zero_point;
// 找到最大分数对应的类别
int max_idx = 0;
int8_t max_val = output->data.int8[0];
for (int c = 1; c < 6; c++) {
if (output->data.int8[c] > max_val) {
max_val = output->data.int8[c];
max_idx = c;
}
}
// 打印结果(类别索引0-5)
Serial.print("Sample "); Serial.print(i);
Serial.print(" -> Predicted class: "); Serial.println(max_idx);
}
}
void loop() {
// 空循环
}
// 全局张量内存池
constexpr int kTensorArenaSize = 8 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
3. 运行与验证方法
PC端验证(模型正确性)
- 运行Python脚本,观察训练输出。若测试准确率>0.8(本演示仅5 epochs,可能较低,但应>0.5),说明模型有效。
- 生成
model.h后,可编写Python脚本加载gesture_model.tflite,对测试集前20个样本推理,记录预测类别。
STM32端验证
- 将
model.h加入STM32CubeIDE工程,并配置tflite-micro库。 - 将
simulated_input数组填充为与PC端相同的20个测试样本(从Python导出)。 - 编译烧录后,打开串口监视器(115200波特率),观察输出。
- 通过标准:串口打印的20个预测类别与PC端TFLite推理结果完全一致(误差为0)。若一致,说明部署成功。
注意:由于量化误差,STM32端结果可能与PC端float模型略有差异,但应与PC端TFLite int8模型完全一致。若不一致,检查输入数据转换和内存对齐。
4. 常见问题与调试建议
- 内存不足:增大
kTensorArenaSize至16KB或32KB,或减小模型(减少滤波器数量)。 - 输出全为0:检查输入int8转换是否正确,确认
input_scale和zero_point来自实际模型。 - 串口无输出:确认
model.h正确包含,且tflite-micro库已添加。 - 模拟数据导出:在Python训练脚本中,使用
np.savetxt导出前20个测试样本为CSV,再复制到C数组。
5. 预期输出示例(非实测)
若PC端TFLite推理前20个样本的类别为:[0, 1, 1, 0, 2, 2, 3, 3, 4, 4, 5, 5, 0, 1, 2, 3, 4, 5, 0, 1],则STM32串口应打印相同序列。此序列仅为示例,实际取决于训练结果。