一、项目介绍
本系统通过TMF8821 dToF传感器捕捉三维手势信息,结合树莓派4B实现深度学习手势识别算法,最终通过Wi-Fi远程控制ESP32-S3开发板,实现可手势控制计算机的HID键盘设备。系统实现了非接触式菜单导航与设备控制功能,具有控制模式多样、高准确率等特点。
二、硬件系统组成
1. TMF8821 ToF传感器
TMF8821是一种直接飞行时间(dToF)传感器,采用单个模块化封装,带有相关的 VCSEL(垂直腔面发射激光器)。基于SPAD、TDC和直方图技术,该传感器可实现5000 mm的检测范围。由于它的镜头位于SPAD上,它支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。原始数据的所有处理都在片上进行,随后在I2C接口上提供距离信息和置信度值。
2. 树莓派4B
本项目采用1GB RAM的树莓派4B运行Ubuntu 20.04 LTS操作系统,负责传感器数据采集、模型推理和系统协调等工作。通过硬件I²C接口(引脚编号3和5)连接TMF8821,通过USB-UART模块连接串口屏,通过Wi-Fi与ESP32-S3开发板建立TCP通讯。
3. ESP32-S3开发板
本项目使用ESP32-S3-DevKitM-1U开发板,搭载双核Xtensa LX7处理器,通过Wi-Fi接收控制指令并模拟USB HID设备。本项目使用Arduino进行ESP32-S3软件开发。
4. 串口屏
本项目使用TJC1612118_011N串口屏,屏幕大小1.8寸,分辨率160x128,工作电压4.5~6.0V。该屏幕通过CH340 USB转串口模块,与树莓派建立连接。
三、系统架构设计
1. 硬件架构
- TMF8821 dToF传感器:通过I²C接口与树莓派4B通信,实时采集手势数据。
- 树莓派4B:作为主控单元,负责数据处理和系统协调,支持SSH远程连接或直接连接显示器。
- ESP32-S3:通过Wi-Fi接收树莓派的控制指令,并模拟USB-HID设备控制电脑。
- 1.8寸串口屏:通过UART接口接收树莓派的指令,显示系统状态和菜单。
2. 软件架构
- 传感器驱动:负责TMF8821传感器的初始化和数据采集。
- 数据处理模块:对传感器数据进行预处理和手势特征提取。
- 控制指令模块:生成并发送控制指令至ESP32-S3和串口屏。
- 用户界面模块:通过串口屏提供用户交互界面,显示系统状态和菜单。
3.数据处理流程
- 树莓派通过I²C接口读取TMF8821传感器的原始数据。
- 数据处理后,树莓派通过UART更新串口屏显示内容,并通过Wi-Fi发送控制指令至ESP32-S3。
- ESP32-S3接收指令后,通过USB-HID接口控制电脑,模拟键盘输入。
4.系统特点
- 模块化设计:各功能模块独立,便于维护和升级。
- 灵活性:树莓派支持SSH远程连接和本地显示器操作,适应不同使用场景。
四、软件实现细节
这是运行于树莓派4B上的手势识别与系统控制等核心代码情况
注:相关代码的执行方式与流程请看附录
1. Driver模块
- 用途:TMF8821传感器底层驱动
- 功能:
- 通过I²C接口读取传感器原始数据
- 解析每个检测区域的深度信息和置信度值
- 实现数据校验和错误处理
- 输出:经过校验的多区域ToF数据,通过pipe传输至Compute.py
- 部分代码:
- 驱动程序使用C++编程,调用i2ctransfer工具收发I²C数据
int get_result_tof_sensor(void){
std::string cmd;
std::stringstream ss;
std::vector<uint8_t> byteArray;
std::string ret_val;
std::string expected_ret_val="0x00 0x00 0xff\n";
while(1){
while(1){
delay_ms(std::chrono::milliseconds(1));
cmd = "i2ctransfer -y -f 1 w1@0x41 0xe1 r1";
ret_val=executeI2CTransfer(cmd);
if(ret_val == "0x23\n"){
delay_ms(std::chrono::milliseconds(1));
cmd = "i2ctransfer -y -f 1 w2@0x41 0xe1 0x23";
executeI2CTransfer(cmd);
delay_ms(std::chrono::milliseconds(2));
break;
}
else if(ret_val == "0x03\n"){
delay_ms(std::chrono::milliseconds(1));
cmd = "i2ctransfer -y -f 1 w2@0x41 0xe1 0x03";
executeI2CTransfer(cmd);
delay_ms(std::chrono::milliseconds(1));
break;
}
}
delay_ms(std::chrono::milliseconds(1));
cmd = "i2ctransfer -y -f 1 w1@0x41 0x20 r132";
ret_val = executeI2CTransfer(cmd);
delay_ms(std::chrono::milliseconds(1));
byteArray = hexStringToUint8Array(ret_val);
printResults(byteArray.data());
}
}
2. Compute.py模块
- 用途:手势识别推理引擎
- 功能:
- 接收Driver模块的ToF数据
- 执行数据预处理(归一化、FFT变换)
- 运行CNN模型进行手势分类
- 生成推理过程调试信息
- 输出:手势类别及每个类别的可能性,通过pipe传输至Execute.py
- CNN模型结构:
- CNN输入数据的预处理:
- 收集24条相邻的TMF8821原始测量数据,每个数据包含18个测距结果及其置信度
- 对这些数据在通道维度上执行FFT,拼接得到的实部、虚部,完成输入数据预处理
- 适当降低手势识别的频率,降低误判的概率,并且在浏览短视频时,上下翻页的动作是低频动作
- CNN代码:
- 基于pytorch编程
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=(3, 3), stride=1, padding=1)
self.bn1 = nn.BatchNorm2d(16)
self.pool1 = nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2))
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3, 3), stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(32)
self.pool2 = nn.MaxPool2d(kernel_size=(2, 2), stride=(2, 2))
self.conv3 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=(3, 3), stride=1, padding=1)
self.bn3 = nn.BatchNorm2d(32)
self.fc1 = nn.Linear(32 * 18 * 6, 32 * 4)
self.fc2 = nn.Linear(32 * 4, 5) #
self.act = nn.LeakyReLU(0.1)
def forward(self, x):
x = self.pool1(self.act(self.bn1(self.conv1(x))))
x = self.pool2(self.act(self.bn2(self.conv2(x))))
x = self.act(self.bn3(self.conv3(x)))
x = x.view(-1, 32 * 18 * 6) # Flatten
x = self.fc1(x)
x = self.fc2(x)
return x
- CNN训练:
- 请参考提交代码中的train.py,默认训练500轮
- 重要说明:我采集的数据集不大,所以训练的模型对手势移动的速度和到传感器的距离有一定要求,泛化性有较大提升空间。如需在新环境部署,可以使用我设计的交互式数据采集程序重新采集数据集并训练。
3. Execute.py模块
- 用途:系统控制与任务执行
- 功能:
- 解析手势识别结果
- 更新1.3寸串口屏显示内容
- 生成ESP32-S3控制指令
- 管理系统状态机
- 输出:串口屏显示更新,ESP32-S3控制指令
- 系统状态机与串口屏显示页面:
- 包含两种可选择的操作模式(模式1、2),后续可添加更多模式
- 利用串口屏厂家的USART HMI软件设计各个页面,与状态机对应
- 部分树莓派4B代码
- 通过/dev/ttyUSB0端口与串口屏通讯
- 通过TCP与ESP32-S3通讯
def read_from_pipe(path):
ser=serial.Serial(port="/dev/ttyUSB0",baudrate=9600,timeout=5)
screen_display = ScreenDisplay(ser)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((SERVER_HOST, SERVER_PORT))
server_socket.listen(5)
print(f"Server listening on {SERVER_HOST}:{SERVER_PORT}")
try:
while True:
client_socket, client_address = server_socket.accept()
print(f"Connection from {client_address}")
try:
while True:
try:
with open(path, 'r') as fifo:
print(f"Reading from named pipe at {path}")
while True:
line = fifo.readline()
if not line:
time.sleep(0.01)
continue
break
except FileNotFoundError:
print(f"Named pipe at {path} does not exist.")
except PermissionError:
print(f"Permission denied when trying to read from named pipe at {path}.")
except Exception as e:
print(f"An error occurred: {e}")
print(line)
line = line.strip()
ret = screen_display.handle_command(line)
if ret is not None:
data = ret
client_socket.sendall(data.encode('utf-8'))
data = client_socket.recv(1024).decode('utf-8')
if not data:
break
print(f"Received: {data.strip()}")
time.sleep(1)
finally:
client_socket.close()
print(f"Closed connection from {client_address}")
except KeyboardInterrupt:
print("Exiting due to keyboard interrupt.")
finally:
server_socket.close()
4. Arduino程序实现
- 用途:ESP32-S3控制程序,实现HID设备模拟功能
- 功能:
- 建立Wi-Fi连接,接收树莓派控制指令
- 模拟键盘和鼠标操作
- 实现基础的人机交互功能
- ESP32-S3的指令解析与执行代码:
void loop()
{
Serial.println("尝试访问服务器");
if (client.connect(serverIP, serverPort)) //尝试访问目标地址
{
Serial.println("访问成功");
while (client.connected() || client.available()) //如果已连接或有收到的未读取的数据
{
if (client.available()) //如果有数据可读取
{
String line = client.readStringUntil('\n'); //读取数据到换行符
Serial.print("读取到数据:");
Serial.println(line);
String answer = "OK!";
if (line == "b0") {
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press('c');
delay(100);
Keyboard.releaseAll();
}
else if (line == "b1") {
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press('v');
delay(100);
Keyboard.releaseAll();
}
else if (line == "b2") {
Mouse.move(0, 0, 2, 0);
}
else if (line == "b3") {
Mouse.move(0, 0, -2, 0);
}
client.write(line.c_str());
}
}
Serial.println("关闭当前连接");
client.stop(); //关闭客户端
}
else
{
Serial.println("访问失败");
client.stop(); //关闭客户端
}
delay(5000);
}
5. DataCollect.py模块
- 用途:实现TMF8821传感器数据的实时采集与处理
- 功能:
- 通过FIFO管道读取传感器原始数据
- 数据解析与格式转换
- 交互式数据采集控制
- 数据存储与分类管理
- 树莓派4B数据收集部分代码:
def collect_data(self, category):
async def collect_data_inner():
async for result_dict in self.processor.get_processed_data():
if category not in self.current_categories:
break
self.collected_data[category].append(result_dict)
print(result_dict)
await asyncio.sleep(0.01) # Collect data every 0.1 seconds
return collect_data_inner()
- 交互式界面:
- 点击按钮后,收集该动作的数据,再次点击停止收集
- 数据文件以动作名+编号的方式存储
五、功能演示
涉及手势动作,不便截图,具体请看演示视频,包括:手势控制上下翻页、手势控制复制粘贴、交互式数据采集等内容。
六、遇到的难题及解决方法
- TMF8821传感器驱动:最初计划采用官方提供的Python驱动程序进行开发,但因软件配置问题未能成功调试。因此,我依据Application Notes(AN001015)自行编写了C++驱动程序。由于对C++环境下的I2C读写操作不够熟悉,我选择调用终端工具i2ctransfer来实现I2C的读写功能。此外,传感器固件直接采用了官方示例中的代码。
- CNN网络架构设计:在调整参数的过程中逐步优化网络性能……
- 多个程序间的通信:本项目将底层驱动、推理运算以及指令下发的功能分布在三个Python文件中执行,以便于项目的开发与管理。然而,这种方式使得直接使用队列等数据结构在多个Python文件之间进行通信变得困难。为了解决这一问题,我们通过管道以文件读写的形式实现了这些文件之间的通信,并将管道文件存储在ramdisk中以加快读写速度。
- 屏幕选型:考虑到使用I2C或SPI协议的屏幕需要额外编写驱动程序并设计可视化界面,这将大大增加工作量。相比之下,串口屏只需通过串口发送控制指令即可完成界面切换,无需编程设计可视化界面,从而显著减少了工程量。
- USB-HID控制:原计划是实现蓝牙键盘和鼠标的控制功能,但在使用Arduino库时遇到了编译问题。因此,我们改为实现USB有线控制方式。
- 程序编写:本项目涉及较多模块,编程工作量比之前几次活动大了许多。因此我使用大模型辅助编程。我负责提供所有的编程思路,功能实现细节由通义千问大模型完成,最后由我进行检查与调试。
七、项目总结
本项目成功开发了一个基于TMF8821 dToF传感器的手势识别系统,实现了非接触式手势识别与控制功能,支持四种基本手势(接近、远离、上挥、下挥)的识别。同时,我们还实现了通过手势控制串口屏页面切换(详情请参见演示视频),以及模拟HID设备(如键盘和鼠标)并通过手势控制上下翻页或执行复制粘贴操作。在这个过程中,我不仅深入学习了TMF8821传感器的I2C驱动编写,而且借助详尽准确的Application Notes,获得了良好的编写体验和丰富的知识积累。
附录:
0.必要的准备
树莓派必须安装i2ctransfer工具和pytorch环境。我使用的树莓派操作系统是ubuntu20.04。
笔记本电脑需要安装MobaXterm,接收DataCollect.py通过X11转发的可视化窗口。其他时候可以使用VSCode等SSH远程开发工具,或者连接HDMI显示器直接在树莓派本地运行调试程序。
1.管道文件存放
创建ramdisk,大小64MB即可,存放pipe文件。通过ramdisk降低文件读写延迟。
2.编译驱动
编译冷启动驱动:g++ -o Driver tmf882x_image.cpp tmf882x_calib.cpp my_i2c_v3.cpp
编译热启动驱动:g++ -o Driver_simple tmf882x_image.cpp tmf882x_calib.cpp my_i2c_v3.cpp 请修改cpp代码,只保留dToF传感器测量部分的代码
3.训练数据收集
在树莓派的项目文件夹执行3行命令:
mkdir dataset
./Driver &
python DataCollect.py
4.启动手势识别系统
运行主程序:(./Driver &) && (OMP_NUM_THREADS=1 python3 Compute.py &) && (python3 Execute.py)
停止主程序的步骤(a~e):
- ctrl-c退出
- 不管屏幕打印的内容,直接输入killall ./Driver并回车
- 执行 ps -ef | grep python 然后kill掉python3 Compute.py
- 执行 sudo fuser -k -n tcp 50037 关闭TCP连接
- 稍等片刻后,可重新运行主程序
5.如果在启动手势识别系统程序后遇到问题,请结束Driver、Compute.py、Execute.py并重新按照操作步骤尝试。