2024艾迈斯欧司朗竞赛-基于tmf8821的手势识别菜单
该项目使用了RP2040、st7789屏幕以及tmf8821传感器,实现了一个可以使用手势交互的菜单界面的设计,它的主要功能为:可以识别上下左右四个方向的挥动以及接近和远离6种手势,向下和向左挥动可使菜单切换到下一个,向上和向右挥动可以使菜单切换到上一个,接近和远离这两种手势分别对应确认和返回两个菜单功能。。
标签
RP2040
手势识别
TMF8821
Pulsar2
更新2025-03-06
116

1、项目介绍

该项目主要使用了RP2040、st7789屏幕以及tmf8821传感器实现了一个可以使用手势交互的菜单界面,可以识别上下左右四个方向的挥动以及接近和远离6种手势,向下和向左挥动可使菜单切换到下一个,向上和向右挥动可以使菜单切换到上一个,接近和远离这两种手势分别对应确认和返回两个菜单功能。

2、硬件介绍

本项目使用到的硬件主要为主控RP2040、st7789屏幕和tmf8821传感器。
主控芯片RP2040与st7789屏幕为搭载在RP2040 Game Kit开发板上的硬件,RP2040 Game Kit是基于树莓派RP2040的嵌入式系统学习平台,USB Type-C供电,采用RP2040作为主控,支持MicroPython、C/C++编程,性能强大。屏幕为240*240分辨率的彩色IPS LCD,SPI接口,控制器为ST7789。
TMF8821传感器是一种直接飞行时间(dToF)传感器,TMF8821采用单个模块化封装,带有相关的 VCSEL(垂直腔面发射激光器)。dToF 设备基于 SPAD、TDC 和直方图技术,可实现 5000 mm 的检测范围。由于它的镜头位于 SPAD 上,它支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。VCSEL 上方的封装内的多透镜阵列 (MLA) 拓宽了 FoI(照明场)。原始数据的所有处理都在片上进行,TMF8821在其 I2C 接口上提供距离信息和置信度值。

3、方案框图与项目设计思路

3.1 方案框图

方案框图如下图所示,以RP2040为核心主控,TMF8821监测手势输入交互,并在彩屏上展示交互界面。


3.2 项目设计思路

3.2.1 手势采集模块

手势采集使用到了艾迈斯欧司朗的dToF传感器TMF8821,对TMF8821初始化、写入RAM固件、设置配置项等就可以将其驱动起来,之后可以读取到到设定区域(如4*4)内各个子区域的距离与置信度,这些距离数据就可以当做手势动作的原始数据。

3.2.2 手势识别模块

从TMF8821获取到手势动作的原始数据后就可以对一段时间内的数据变化进行手势识别了。目前手势识别右两类方法:一是使用AI训练模型进行训练,而是截取一段时间内的原始数据进行一定的逻辑计算进行判断。由于AI模型训练的时间和精力成本较高,本项目采用的第二种方法,缺点是准确度右一定差距,但是作为玩具的话是足够了。

3.2.3 菜单交互模块

为了达到丝滑的菜单界面切换动画,可以移植LVGL来实现。显示接口移植按传统方式移植即可,即先初始化好屏幕后,将打点函数对接到lvgl的显示接口上就行。需要注意的地方是输入设备的移植,往常是使用按键来实现控件焦点的切换,该项目需要将手势识别的结果模拟成按键按下一小会。手势共有6种,上下左右挥动以及接近与远离,本项目将下挥和左挥映射到切换焦点到上一个的按钮,将上挥和右挥映射到切换焦点到下一个按钮,将接近动作映射到确认按钮,将远离动作映射到返回按钮。

4、软件流程图与关键代码介绍

4.1 软件流程图

软件流程图如下图所示,开机首先是初始化,包括RP2040、TMF8821和ST7789的初始化,之后就进入手势检测循环,对从TMF8821中读取到数据进行一定的计算与判断得出当前是否有手势动作,如果检测到手势动作就模拟一次响应按键按下,将按键信号通过lvgl输入设备传输到菜单界面执行相应动作。


4.2 关键代码介绍

4.2.1 TMF8821数据计算与手势识别

TMF8821数据计算与手势识别代码如下所示,循环读取tmf8821,由于在初始化是将tmf8821测量频率设置为了10Hz,所以能读取到测量数据的周期为100ms。由于传感器的位置不同读取到的距离数值也不同,直接根据距离数值直接判断手势不太直观,我在处理数据时做了一个差分帧的操作,即每读取到一帧数据就与上一帧做一个相减操作,这样无论传感器在什么位置,只要传感器稳定下来,差分帧数据都会稳定在0左右。当差分帧数据绝对值之和大于一定数值时则说明检测到的动作,需要根据数据进行判断手势了。在判断手势时我是先判断的接近与远离手势,如果是这两个手势,差分帧各个像素的数据会有一个差不多的变化,所以只需判断差分帧各个像素数据的标准差小于一定值即可判断当前手势为接近或者远离,具体是接近还是远离则根据差分帧数值的正负来判断。当检测到不是接近与远离手势时则需要判断是否是上下左右这四种手势了,这四个方向的判断实际就是判断差分帧数据的变化方向,所以我对相邻的两个差分帧再进行了一次差分。再使用这个二次差分帧进行特征判断,特征值最高的且高于一定值即认为是相应的手势。将手势状态保存在hand_moving_stat变量中,在lvgl的输入设备扫描中使用get_hand_moving_stat()函数获取相应手势状态。

static int is_moved(void)
{
uint16_t sum = 0;
for(uint8_t i=0; i<16; i++){
if(distance_dif[i]>0)sum+=distance_dif[i];
else sum -= distance_dif[i];
}
printf("sum=%d\n",sum);
return sum>200 ? 0 : -1;
}

static int check_closer_away(void)
{
int sum = 0;
for(uint8_t i=0; i<16; i++){
sum+=distance_dif[i];
}
int avg = sum>>4;
int letter = 0;
int temp = 0;
for(uint8_t i=0; i<16; i++){
temp = distance_dif[i] - avg;
if(temp>0)letter += temp;
else letter -= temp;
}
printf("avg = %d, letter=%d\n",avg,letter);
uint32_t time_now = get_milliseconds();
if(letter > 100)return -1;
else{
if(time_now-time_last_move>1000){
time_last_move = get_milliseconds();
if(avg>0)return 1;
else return 2;
}
else return -1;
}
}

static int calc_dir_feature(int8_t* feature)
{
int sum = 0;
for(uint8_t i=0; i<16; i++){
sum += distance_dif[i] * feature[i];
}
return sum;
}

static int check_dir(void)
{
int dif[16] = {0};
memcpy(distance_dif_prv,distance_dif_cur,sizeof(distance_dif_cur));
memcpy(distance_dif_cur,distance_dif,sizeof(distance_dif));
for(uint8_t i=0; i<16; i++){
dif[i] = distance_dif_cur[i] - distance_dif_prv[i];
}
int feature[4] = {0};
feature[0] = calc_dir_feature(dir_left_feature);
feature[1] = calc_dir_feature(dir_right_feature);
feature[2] = calc_dir_feature(dir_up_feature);
feature[3] = calc_dir_feature(dir_down_feature);
printf("feature left:%d, right:%d, up:%d, down:%d\n",feature[0],feature[1],feature[2],feature[3]);
int max=0;
int max_index=0;
for(uint8_t i=0; i<4; i++){
if(feature[i]>max){
max = feature[i];
max_index = i;
}
}
uint32_t time_now = get_milliseconds();
if(max>400){
if(time_now-time_last_move>1000){
time_last_move = get_milliseconds();
return max_index+1;
}
}
return -1;
}

void tmf8821_distance_print(int* d)
{
printf("\n%d,%d,%d,%d\n",d[0],d[1],d[2],d[3]);
printf("%d,%d,%d,%d\n",d[4],d[5],d[6],d[7]);
printf("%d,%d,%d,%d\n",d[8],d[9],d[10],d[11]);
printf("%d,%d,%d,%d\n",d[12],d[13],d[14],d[15]);
}

void tmf8821_distance_update(uint8_t* data)
{
memcpy(distance_prv,distance_cur,sizeof(distance_cur));
for(uint8_t i=0; i<8; i++){
if(data[24+i*3]==0xff)
distance_cur[i] = data[25+i*3] + data[26+i*3]*256;
if(data[51+i*3]==0xff)
distance_cur[i+8] = data[52+i*3] + data[53+i*3]*256;
}
for(uint8_t i=0; i<16; i++){
distance_dif[i] = distance_cur[i] - distance_prv[i];
}
tmf8821_distance_print(distance_dif);
if(is_moved()==0){
time_dif_prv = time_dif_cur;
time_dif_cur = get_milliseconds();
printf("time:%d",time_dif_cur);
if(time_dif_cur-time_dif_prv<1000){
int ret = check_closer_away();
if(ret<0)
{
ret = check_dir();
switch(ret){
case 1:
printf("moving left\n");
hand_moving_stat = 1;
break;
case 2:
printf("moving right\n");
hand_moving_stat = 2;
break;
case 3:
printf("moving up\n");
hand_moving_stat = 3;
break;
case 4:
printf("moving down\n");
hand_moving_stat = 4;
break;
default:
hand_moving_stat = 0;
break;
}
}
else{
if(ret == 2){
hand_moving_stat = 5;
printf("hand is getting closer\n");
}
else{
hand_moving_stat = 6;
printf("hand is getting far away\n");
}
}
}
}
else{
hand_moving_stat = 0;
}
}

void tmf8821_scan(void)
{
uint8_t tmf8821_result[132] = {0};
static uint32_t time_to_wait_tmf8821 = 0;
if(try_to_wait(&time_to_wait_tmf8821, 20)==0){
if(tmf8821_measure_is_ready()==0){
memset(tmf8821_result,0,sizeof(tmf8821_result));
//memset(result_str,0,sizeof(result_str));
tmf8821_measure_result_read(tmf8821_result);
tmf8821_distance_update(tmf8821_result);
// tmf8821_distance_print(distance_dif);
}
}
}

4.2.2 手势识别结果接入lvgl

将手势识别结果传入lvgl的代码如下所示,使用get_hand_moving_stat()函数获取到手势识别结果后,根据结果将按键映射到相应按键即可,可以看到我这里是将下挥和左挥都映射到切换焦点到上一个的按钮,将上挥和右挥都映射到切换焦点到下一个按钮,将接近动作映射到确认按钮,而远离动作则是在其他地方单独检测并映射到返回按钮。

static void keypad_read(lv_indev_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;

/*Get the current x and y coordinates*/
mouse_get_xy(&data->point.x, &data->point.y);

/*Get whether the a key is pressed and save the pressed key*/
char act_key = get_hand_moving_stat();
if(act_key != 0) {
data->state = LV_INDEV_STATE_PRESSED;

/*Translate the keys to LVGL control characters according to your key definitions*/
switch(act_key) {
case 1:
act_key = LV_KEY_NEXT;
break;
case 2:
act_key = LV_KEY_PREV;
break;
case 3:
act_key = LV_KEY_PREV;
break;
case 4:
act_key = LV_KEY_NEXT;
break;
case 5:
act_key = LV_KEY_ENTER;
break;
break;
}

last_key = act_key;
}
else {
data->state = LV_INDEV_STATE_RELEASED;
}
data->key = last_key;
}

4.2.3 主菜单界面

主菜单界面代码如下所示,首先创建5个图片按钮用作APP图标,然后生成5个标签用作APP名称,并在回调函数中将切换到焦点的APP图标按钮放大用作提示当前选择的是那个APP,默认焦点在第一个按钮上。在回调函数中触发确认事件时将界面切换到相应APP。

void ui_main_screen_init(void)
{
ui_main = lv_obj_create(NULL);
lv_obj_clear_flag(ui_main, LV_OBJ_FLAG_SCROLLABLE); /// Flags
lv_obj_set_style_bg_img_src(ui_main, &bg, LV_PART_MAIN | LV_STATE_DEFAULT);

g_main_indev_group = lv_group_create();
lv_indev_set_group(indev, g_main_indev_group);

label_app_name = lv_label_create(ui_main);
lv_obj_align(label_app_name, LV_ALIGN_BOTTOM_MID, 0, -24);
lv_obj_set_style_text_color(label_app_name,lv_color_make(255,0,0),LV_PART_MAIN);
lv_obj_set_style_text_font(label_app_name,&lv_font_montserrat_20,LV_PART_MAIN);
lv_label_set_text(label_app_name,app_name[0]);

container = lv_obj_create(ui_main);
lv_obj_set_size(container, LV_PCT(100), LV_PCT(100));
lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_scroll_snap_x(container, LV_SCROLL_SNAP_CENTER);
lv_obj_set_style_bg_opa(container, LV_OPA_0, LV_PART_MAIN);
lv_obj_set_style_bg_color(container, lv_color_black(), LV_PART_MAIN);
lv_obj_set_style_border_width(container, 0, LV_PART_MAIN);
lv_obj_set_style_pad_column(container, 30, LV_PART_MAIN); //图标之间的间隙
lv_obj_center(container);

//生成演示按钮
for (int i = 0; i < MAX_APP_NUM; i++)
{
lv_obj_t* btn = lv_btn_create(container);
lv_obj_set_size(btn, 80, 80);
lv_obj_set_style_bg_img_src(btn,btn_image[i],LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_add_event_cb(btn, button_event_cb, LV_EVENT_ALL, NULL);//只处理获取到焦点、按下的消息LV_EVENT_FOCUSED
//lv_obj_add_event_cb(btn, button_event_cb, LV_EVENT_PRESSED, NULL);

lv_group_add_obj(g_main_indev_group, btn);
}
lv_obj_scroll_to_view(lv_obj_get_child(container, 0), LV_ANIM_ON);
lv_obj_set_size(lv_obj_get_child(container, 1), 64, 64);
lv_obj_set_style_bg_opa(lv_obj_get_child(container, 1), LV_OPA_80, LV_PART_MAIN);
}

/**
* @brief 处理按钮事件的回调函数
* @param event
*/
static void button_event_cb(lv_event_t* event)
{
lv_obj_t* current_btn = lv_event_get_current_target(event);
uint32_t current_btn_index = lv_obj_get_index(current_btn);
uint32_t btn_cnt = lv_obj_get_child_cnt(container);
lv_event_code_t code = lv_event_get_code(event);
if (code == LV_EVENT_FOCUSED)
{
lv_obj_set_size(current_btn, 100, 100);
lv_obj_set_style_bg_opa(current_btn, LV_OPA_100, LV_PART_MAIN);

lv_label_set_text(label_app_name,app_name[current_btn_index]);
if(current_btn_index==0){
if(btn_cnt!=1){
lv_obj_set_size(lv_obj_get_child(container, 1), 64, 64);
lv_obj_set_style_bg_opa(lv_obj_get_child(container, 1), LV_OPA_80, LV_PART_MAIN);
}
}
else if(current_btn_index==MAX_APP_NUM-1){
lv_obj_set_size(lv_obj_get_child(container, current_btn_index-1), 64, 64);
lv_obj_set_style_bg_opa(lv_obj_get_child(container, current_btn_index - 1), LV_OPA_80, LV_PART_MAIN);
}
else{
lv_obj_set_size(lv_obj_get_child(container, current_btn_index - 1), 64, 64);
lv_obj_set_size(lv_obj_get_child(container, current_btn_index + 1), 64, 64);
lv_obj_set_style_bg_opa(lv_obj_get_child(container, current_btn_index - 1), LV_OPA_80, LV_PART_MAIN);
lv_obj_set_style_bg_opa(lv_obj_get_child(container, current_btn_index + 1), LV_OPA_80, LV_PART_MAIN);
}
}
else if (code == LV_EVENT_PRESSED)
{
/*启动相应任务*/
page = current_btn_index+1;
lv_scr_load_anim(ui_array[current_btn_index], LV_SCR_LOAD_ANIM_OVER_TOP, 500, 0, false);
}
}

void btn_return(void)
{
lv_scr_load_anim(ui_main, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false);
lv_indev_set_group(indev, g_main_indev_group);
page = 0;
}

5、功能展示图及说明

由于本项目功能为动作与动画,所以此处展示的是串口中的结果输出。

5.1 接近与远离手势

接近与远离的手势识别结果的串口数据输出如下图所示,可以看到数据符合上一章中的检测逻辑,差分帧各个像素的数据有一个差不多的变化,只需判断差分帧各个像素数据的标准差小于一定值即可判断当前手势为接近或者远离,均值为负数说明是接近,正数说明是远离。

5.2 上下左右挥动手势

上下左右挥动手势识别结果的串口数据输出如下图所示,可以看到数据也是符合上一章中的检测逻辑的。

6、总结

6.1 项目中遇到的难题和解决方法

本项目中遇到的难题主要有两个,一是tmf8821的驱动,由于厂家提供的代码示例只有Arduino的,且代码包装程度比较高,不太适合用作参考,最终只能根据官方的主机通讯手册与寄存器手册编写驱动了。另一个难题就是手势识别逻辑了,其实有想过使用AI模型训练,但一方面4*4的规格数据量有点小,另一方面使用AI模型训练有点大材小用了,最终还是直接使用原始距离数据进行一定的逻辑运算进行判断了。

6.2 心得体会

经过本次竞赛又学会了一种新的传感器使用经验,Tof传感器测距精确且稳定,不容易受到外界环境干扰。之前只是听说过在小型四轴上做定高功能时经常使用,现在也是实际体验了一下,为以后的其他项目开发又积累了一份经验。

附件下载
tmf8821.rar
项目源码,基于CSDK,压缩包内不包含PICO-SDK,需要在环境变量中配置SDK路径才能正常编译
app.uf2
项目可直接下载的固件
团队介绍
个人团队
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号