任务介绍
本项目实现了dToF传感器光电设计竞赛的自由命题,使用官方提供的dToF传感器和RP2040 Game Kit开发板,实现了基于手势识别与PyQt上位机的PC万能控制器。
硬件平台
首先介绍本次用到的开发板:RP2040 Game Kit,这是一块基于RP2040微控制器的游戏机开发板,它板载了LCD、蜂鸣器、摇杆、按键和姿态传感器等常用外设,并且预留了扩展排针以及Debug接口,可玩性非常高,因此它在硬禾项目中的出场率非常高,本次也将作为主控来驱动活动的传感器。
接下来是本次活动的核心器件:艾迈斯欧司朗dToF传感器模块。它是基于 TMF8821 设计的直接飞行时间 (dToF) 传感器模块,支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。这次TMF8821 dToF传感器模块与RP2040 Game Kit管脚匹配,插上直接可以使用,虽然官方也放开可以使用其他开发板来作为主控,但Game Kit已经足够使用,本次我会使用官方提供的两个原装设备,做一个基于手势识别与PC上位机的PC万能控制器。
任务分析与实现
这次主办方出了四种任务,侧重于传感器的各种应用场景
- 任务一是角度测算,计算传感器平面与桌面的夹角;
- 任务二是手势识别,通过识别结果控制屏幕上的菜单;
- 任务三是液体种类识别,通过测量折射率区分液体种类;
- 最后是自由命题。
这次我选择了自由命题。和任务二的手势识别很像,因为我感觉相比小屏幕上的菜单。用手势识别的结果来控制电脑更有实用价值,比赛结束后板卡和传感器不用吃灰,可以直接投入日常使用。
方案框图:
一、硬件感知层
- ToF传感器模块
- 采用TMF882X激光测距芯片,通过I2C总线(引脚16/17)获取9通道距离数据
- 工作参数:50ms采样周期,200+置信度阈值,有效检测范围30-120cm
- 输出3x3距离矩阵,每个单元存储毫米级精度测量值
- 显示输出模块
- 240x240 TFT显示屏,采用SPI接口驱动
- 方向指示:使用黄色三角显示手势识别方向
- 文本显示区域固定在屏幕底部,使用20号字体并预留10像素边距
二、数据处理层
- 数据预处理管道
- 双缓冲机制:prevDistanceMatrix/distanceMatrix存储连续两帧数据
- 一阶差分:逐元素相减生成3x3变化量矩阵
- 数据校验:通道索引有效性检查,过滤异常值
- 核心识别算法
- 特征提取:计算行差(row0_sum-row2_sum)和列差(col0_sum-col2_sum)
- 双重阈值:
- 动态比例阈值:maxVal/minVal > 1.5(判断手势存在)
- 绝对差值阈值:行/列差绝对值 > 100mm(识别运动方向)
- 决策逻辑:优先水平方向判断,正差左移/负差右移;垂直方向次优判断
- 跳帧机制:识别成功后跳过下一处理周期
三、人机交互层
- 本地显示
- 图形引擎:基于TFT_eSPI库实现,采用直接写屏模式
- 串口通信协议
- 数据格式:ASCII文本行,带UNICODE方向符号
- 有效数据:"↑ UP\n"、"↓ DOWN\n"等
- 无效数据:"NONE\n"
- 传输参数:115200bps波特率,8N1格式,硬件流控关闭
四、上位机联动层
- 数据解析模块
- 关键词匹配:识别包含UP/DOWN/LEFT/RIGHT的字符串
- 有效性校验:仅接受带方向符号的标准格式数据
- 系统集成
- 热键映射:固定组合键触发(如Ctrl+Alt+方向键)
- 状态反馈:通过系统托盘图标显示连接状态
- 配置存储:Windows注册表保存端口和快捷键设置
代码详解
下位机整体软件流程图:
本次项目涉及到了几个关键技术,接下来结合相关代码来进行讲解:
- 手势识别算法: 基于TMF882X dToF传感器采集3x3空间距离矩阵,采用帧间差分法计算动态变化量。通过极值比阈值(MAX/MIN>1.5)过滤噪声,最后执行手势方向检测算法。
- 帧间差分法
- 使用变化量代替原始值进行判断,可以提高对不同传感器位置的适应性。
void updateDistanceMatrix() {
// 增强型数据保存(带溢出保护)
static bool firstUpdate = true;
if(firstUpdate) {
memset(prevDistanceMatrix, 0, sizeof(prevDistanceMatrix));
firstUpdate = false;
}
// 使用memcpy替代循环拷贝
memcpy(prevDistanceMatrix, distanceMatrix, sizeof(distanceMatrix));
// 带数据校验的更新
for (int i = 0; i < myResults.num_results; ++i) {
if(myResults.results[i].confidence >= 200) {
int channel = myResults.results[i].channel - 1;
if(channel < 0 || channel >= 9) continue; // 新增通道校验
int row = channel / 3;
int col = channel % 3;
distanceMatrix[row][col] = myResults.results[i].distance_mm;
}
}
}
// 计算差分数据
int diffMatrix[3][3] = {0};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
diffMatrix[i][j] = distanceMatrix[i][j] - prevDistanceMatrix[i][j];
}
}
- 手势存在判断:
- 对于整个手势识别系统的工作过程中,不存在手势才是常态。因此我们需要首先判断当前数据是否存在手势,随后在进行进一步的手势判断,这样可以极大的节省资源和算力。本项目我们使用极值比进行判断,当比值超过1.5时,认为存在手势。
- 方向判断算法:
- 以手势从上向下挥动为例,当手势进入传感器范围时,距离矩阵的第一行数据会明显变化;当手势离开时,最后一行的数据也会明显变化,我们通过阈值检测这一变化,就可以得到手势方向。其他方向的检测也同理。
// 手势参数
enum GestureDirection {
NONE,
UP,
DOWN,
LEFT,
RIGHT
};
GestureDirection getGestureDirection(int diffMatrix[3][3]) {
int rowSums[3] = {0};
int colSums[3] = {0};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
rowSums[i] += diffMatrix[i][j];
colSums[j] += diffMatrix[i][j];
}
}
// 计算各方向差值
int rowDiff = rowSums[0] - rowSums[2]; // 行差(左-右)
int colDiff = colSums[0] - colSums[2]; // 列差(上-下)
int absRow = abs(rowDiff);
int absCol = abs(colDiff);
// 候选方向存储
struct {
int value;
GestureDirection positive;
GestureDirection negative;
} directions[2] = {
{absRow, LEFT, RIGHT}, // 行差值对应左右
{absCol, UP, DOWN} // 列差值对应上下
};
GestureDirection finalDir = NONE;
int maxDiff = 0;
// 遍历所有可能方向
for (int i = 0; i < 2; i++) {
if (directions[i].value > DIRECTION_THRESHOLD) {
if (directions[i].value > maxDiff) {
maxDiff = directions[i].value;
finalDir = (i == 0 ? (rowDiff > 0 ? directions[i].positive : directions[i].negative)
: (colDiff > 0 ? directions[i].positive : directions[i].negative));
}
}
}
return finalDir;
}
PyQt5 GUI与串口编程
采用PyQt5框架构建跨平台应用,通过QSerialPort实现自适应串口通信。使用Windows注册表持久化存储快捷键配置,结合pyautogui库实现系统级热键注入,支持毫秒级响应延迟。
- 多线程通信架构
class SerialWorker(QObject):
dataReceived = pyqtSignal(str)
def __init__(self):
super().__init__()
self.serial = QSerialPort()
self.serial.readyRead.connect(self._readData)
def _readData(self):
while self.serial.canReadLine():
line = self.serial.readLine().data().decode()
self.dataReceived.emit(line.strip())
- 专用工作线程避免界面冻结
- 信号槽机制实现线程间通信
- 自动重连功能(每秒检测端口状态)
- 数据解析
def processData(data):
patterns = {
r'.*UP.*': 'UP',
r'.*DOWN.*': 'DOWN',
r'.*LEFT.*': 'LEFT',
r'.*RIGHT.*': 'RIGHT'
}
for pattern, cmd in patterns.items():
if re.search(pattern, data, re.IGNORECASE):
return cmd
return None
- 自定义快捷键管理
class HotkeyManager:
def __init__(self):
self.mapping = self._loadConfig()
def _loadConfig(self):
return {
'UP': 'ctrl+alt+up',
'DOWN': 'ctrl+alt+down',
'LEFT': 'ctrl+alt+left',
'RIGHT': 'ctrl+alt+right'
}
def trigger(self, direction):
if combo := self.mapping.get(direction):
pyautogui.hotkey(*combo.split('+'))
上位机整体软件流程图:
效果展示
上位机GUI
开发板状态
遇到的难题与解决办法
重复检测与跳帧机制
我们知道,手势从上向下滑动时,会连续触发两次检测,因为我们的算法是根据实时数据计算的,就会先检测到第一行矩阵的变化,然后检测到第三行矩阵的变化,连续输出两次相反的结果。因此,我们抑制第二次输出,这里我们会用到跳帧机制:现在如果检测到了手势,那么就先停止下一次的数据处理与判断。
活动感想
本次大赛是我第一次接触dToF传感器,在本次手势识别算法的开发过程中,我深刻体会到嵌入式系统中数据驱动开发的魅力。通过3x3距离矩阵的实时处理,完成了从原始信号到有效手势的转化,这个过程让我对嵌入式系统中的工程算法设计有了新的认知,并最终完成了一个可以投入日常使用的手势识别控制器,非常有成就感。
感谢硬禾科技和艾迈斯欧司朗联合举办的竞赛,祝硬禾的活动越办越好!