2024艾迈斯欧司朗竞赛-基于RP2040游戏开发板实现手势识别
该项目使用了艾迈斯欧司朗DTOF模块TMF8821 及RP2040游戏机,实现了手势识别的设计,它的主要功能为:实现手势的靠近及远离识别,并通过oled 进行菜单选择。
标签
RP2040
艾迈斯欧司朗
TMF8821
xinshuwei
更新2025-03-06
25

项目介绍

本项目主要使用艾迈斯欧司朗的dtof TMF8821 模块及rp2040 游戏机实现手势识别的功能开发,此次主要实现dtof 模块的mpy驱动,完成驱动适配后,通过获取的数据,进行算法设计,对采集的数据进行分类识别,判断不同的手势,然后通过菜单进行控制显示

硬件介绍

1.rp2040 游戏机

rp2040 game kit 是基于树莓派RP2040的嵌入式系统学习平台,通过type c继续宁供电,采用rp2040作为主控,支持mpy c/c++ arduino csdk 等多种开发方式,性能强大

板上主要功能:

1)主控为ST7789 分辨率 240 *240 彩色IPS LCD 显示屏

2)四向摇杆 +2个轻触按键 一个MMA7660 三轴姿态传感器

3)红外发射及接收

4)蜂鸣器

5) 双排连接器,进行spi iic spi 模拟信号 gpio 扩展

主要功能框图

Fo_bQewUYVRVxvSA5ClIGJ8PyagJ

电路图:

1.TMF8821 dTOF 模块

此功能模块是基于TMF8821 设计的直接飞行时间传感器模块,dTOF 设备基于SPAD TDC 和直方图技术,可实现5m到1cm范围的检测

支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。VCSEL 上方的封装内的多透镜阵列 (MLA) 拓宽了 FoI(照明场)。原始数据的所有处理都在片上进行,TMF8821在其 I2C 接口上提供距离信息和置信度值。


此模块可以和RP2040 管脚匹配,按照下图安装,可进行直接使用

2040 背.png

该芯片通过IIC 进行通讯控制,模块通过3.3V 进行供电,支持低功耗模式


方案框图和项目设计思路介绍

整体硬件框图如下:

image.png

通过硬禾官方的mpy固件,上位机使用thony 进行mpy编程,编写TMF8821 底层驱动,然后通过OLED 屏幕绘制菜单,通过实时采集dtof 模块的采样数据,通过数据分析及机器学习处理,进行不同手势识别分类,并通过不同的手势识别结果,进行屏幕菜单控制。

软件流程图和关键代码介绍

1.dtof 模块初始化流程

image.png

此模式的初始化过程还是比较繁琐的,下载数据通过官方的例程截取到相应的数组,中间状态寄存器检测机制保证功能模块能正常运行及交互定位相关问题,通过测试 TMF8821 是无法使用mode 寄存器进行设置模式的,这个只有TMF8828支持

2.模块测量读取数据过程

image.png

读取过程前需要进行关键参数的设置后,再进行测量,测量前后,通过相应的寄存器命令控制测量的开始和结束,数据读取可以读取多帧或者一帧,看后续进行分类的实时性要求,可进行多次数据一次读取,或者分多次进行读取

我这里spad_map_id 使用的是7 是4x4的矩阵

image.png

对应zone 定义如下图:

image.png


初次获取到数据是连续地址获取,注意官方手册说明,默认采集到的数据是无法直接进行使用的,使用的是zone 1-8 及 10-17

image.png

需进行数据剔除,然后封装成4x4的数据矩阵

    dtof_matrix=[
obj0_dst_list[0:4],
obj0_dst_list[4:8],
obj0_dst_list[9:13],
obj0_dst_list[13:17],
]

数据滤波器:

enable de-scattering

(散射 门限)比如物体从左到右,物体在左侧时,其他sensor 有可能也有有效数据,需要进行过滤,这里直接使用了官方的算法,封装成库


import math

class TMF882X_DESCATTER:
maxScatterDistMm =5000 #5m
threshUQ16 = int( (5<<16)/100)
#/*! The maximum distance at which de-scattering shall be done. */
TMF8XXX_DESCATTER_MAX_DISTANCE_MM =10000 #/* = 10m */
#/*! The descattering resolution. Every object is sorted into an array of this resolution.
#* 2**N-values are recommended, as this this is used as divisor.*/
TMF8XXX_DESCATTER_BIN_WIDTH_MM=8 #/* = 8mm descattering data resolution. */

#/*! The descatter filter length. */
TMF8XXX_DESCATTER_FILTER_LENGTH = int ( TMF8XXX_DESCATTER_MAX_DISTANCE_MM / TMF8XXX_DESCATTER_BIN_WIDTH_MM )
CONF_BREAKPOINT = 40
EXP_GROWTH_RATE = 1.053676
bindat = [0]*TMF8XXX_DESCATTER_FILTER_LENGTH
def __init__(self,maxDistMm=5000,thresh=6):
self.maxScatterDistMm = maxDistMm
if thresh<=99:
self.threshUQ16 =int( (thresh<<16)/100)
else:
self.threshUQ16 = int((99<<16)/100)
def mapConfidence (self,conf):
exp_conf = 0
if conf < self.CONF_BREAKPOINT:
exp_conf = conf
else:
#exponential de-mapping
steps = conf - self.CONF_BREAKPOINT
exp_conf = math.pow(self.EXP_GROWTH_RATE,steps)*self.CONF_BREAKPOINT
return int(exp_conf)
def descatterReset(self):
self.bindat = [0]*self.TMF8XXX_DESCATTER_FILTER_LENGTH
def descatterIsScatteringPeak(self,conf,dist):
if dist >= self.TMF8XXX_DESCATTER_MAX_DISTANCE_MM:
return 0
index = int(dist/self.TMF8XXX_DESCATTER_BIN_WIDTH_MM)
return (conf<=self.bindat[index])
def descatteraddObjectPeak(self,conf,dist):
if conf == 0:
return
firstIdx = (dist - self.maxScatterDistMm)/self.TMF8XXX_DESCATTER_BIN_WIDTH_MM
lastIdx = (dist - self.maxScatterDistMm)/self.TMF8XXX_DESCATTER_BIN_WIDTH_MM
#/* This is a linear threshold. Change if your metric is different.*/
scaledVal = (conf*self.threshUQ16+ (1<<15) )>>16
#Clip to filter length
if firstIdx < 0:
firstIdx = 0
if lastIdx > (self.TMF8XXX_DESCATTER_FILTER_LENGTH-1):
lastIdx = self.TMF8XXX_DESCATTER_FILTER_LENGTH-1
for i in range (firstIdx,lastIdx):
if self.bindat[i] <scaledVal:
self.bindat[i] = scaledVal

数据分类:

手势的远近通过 16个点的平均距离 进行判定

avg_dist = sum(sum(row) for row in matrix) /16 #平均距离

手势左右挥动通过进行矩阵的质心,判断其运动轨迹进行决定:

def calculate_centroid(matrix):
total_weight = 0.0
weighted_x = 0.0
weighted_y = 0.0

# 遍历矩阵的每个元素(假设矩阵为4x4)
for i in range(4): # 行索引(对应y坐标)
for j in range(4): # 列索引(对应x坐标)
value = matrix[i][j]
total_weight += value
weighted_x += j * value # 列索引j对应x方向
weighted_y += i * value # 行索引i对应y方向

# 处理除零错误(如果所有值为0
if total_weight == 0:
return (0.0, 0.0) # 或抛出异常

centroid_x = weighted_x / total_weight
centroid_y = weighted_y / total_weight
return (centroid_x, centroid_y)

数据通过连续帧进行数据缓存,算取多个点的平均距离和水平方向的质心

远近手势的判定

def det_away_near():
global dist_buf
global window_size
if len(dist_buf) < window_size:
return "none"
trend =0
for i in range(1,len(dist_buf)):
trend = trend + dist_buf[i] - dist_buf[i-1]
DISTANCE_THRESHOLD =200
if trend > DISTANCE_THRESHOLD:
#print("远离")
return "away"
elif trend <-DISTANCE_THRESHOLD:
#print("靠近")
return "near"
else:
return "none"
#print("远近值"+str(trend*100))

左右手势的判定:

def det_swipe():
global cx_buf
global window_size
if len(cx_buf)< window_size:
return "none"
x_start = cx_buf[0]
x_end = cx_buf[-1]
delta_x = x_end - x_start
delta_x = delta_x*100
X_MOVE_THRESHOLD=16

if delta_x>X_MOVE_THRESHOLD:
#print("向左")
return "swipe_left"
elif delta_x < -X_MOVE_THRESHOLD:
#print("向右")
return "swipe_right"
else:
return "none"

最终根据远近的判定 更改界面的菜单选择

    if read_away_near=="near":
display.text(font2, ">".format(dst), 20,100)
display.text(font2, " ".format(dst),20,140)
elif read_away_near=="away":
display.text(font2, " ".format(dst), 20,100)
display.text(font2, ">".format(dst),20,140)
display.text(font2, "up".format(dst), 40,100)
display.text(font2, "down".format(dst), 40,140)


功能展示图及说明

微信截图_20250301020520.png

当手势判断为靠近时,选择up菜单

当手势判断为远离时,选择down菜单

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

1.由于不能使用rp2040 的arduino库,此次主要是进行mpy 驱动移植,在移植过程中总会出现设置失败的情况,后来通过官方的驱动手册定位到检测状态寄存器需要通过超时机制多次检测才可以,后来修改后才能驱动成功

对本次竞赛的心得体会(包括意见或建议)

1.感谢硬禾和艾迈斯欧司朗提供此次机会,此次针对bsp移植和 手势识别算法设计挑战还是蛮大的,此次收获也很多,梳理了dtof 工作远离及编程控制等相关知识

2.手势识别的过程,目前只能挥动和 远离靠近分别识别,只通过均值没法确定挥手过程,后续需要添加方差等数据进行区分,或者采用机器学习 方式后续优化下识别算法

3.后续建议多次直播答疑下,一次直播感觉有点少,此次实践工作难度还是很大的



附件下载
tmf882x_iic.py
tmf8821 基于iic 驱动程序
tmf8821_filter.py
录取数据滤波类
tmf8821_test.py
运行主程序,依赖rp2040默认 lcd驱动及固件
团队介绍
苏州工程师一枚
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号