任务要求
- 上位机通过大模型识别手势
- 根据小脚丫核心板上的8个LED灯的变化状态来分别出显示手势识别的上下和左右
- 手势的信息在扩展板的TFTLCD上显示出来
- 小脚丫上三色灯的亮度显示接近传感器感知手势的远近
硬件介绍
小脚丫FPGA套件,这是一块功能很完善的综合FPGA开发套件,主控是一颗型号为MXO2的Lattice FPGA,芯片有4000多个LUT资源,96Kbit的用户闪存、92Kbit的RAM,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、8个7位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,外设很丰富,能满足大部分开发需要,期待能在后面体验其他的功能
电脑主机,跑识别模型并通过串口将结果传给FPGA
项目设计
上位机识别
首先需要考虑如何在上位机利用大模型对手势实现识别。Mediapipe库提供了高效的手关键点检测算法,能够实时捕捉手势动作并解析出对应的手势类型,且Mediapipe使用的python语言有相应的库能很方便地调用摄像头和串口,因此使用python语言通过mediapipe库识别电脑摄像头录制的手势图像,并通过串口将结果通过FPGA。
数据传输
设计串口通信协议,将PC端识别的手势类型编码为固定格式的数据帧。用8个字节表示手势类型,帧头和帧尾添加标志位以确保数据传输的可靠性。FPGA通过串口接收数据并解析协议,提取手势类型。
控制显示
FPGA接收到手势类型后,根据预定义的手势与图像的映射关系,选择合适的图像数据进行显示,其中图像数据以查找表的形式预存至代码中,tftlcd以逐行更新的方式进行扫描,扫描到底部时重新回到顶部进行扫描,从而实现手势变化时图像的切换。
距离显示
距离的显示独立于上述过程,利用I2C通信从rpr0521rs传感器获取距离信息,并映射到三色灯显示。
实现过程
上位机手势识别
利用mediapipe识别手的关节,根据手腕与食指所成向量的方向定义手指的指向,并将结果通过串口发送
import cv2
import mediapipe as mp
import serial
# 初始化MediaPipe Hands模型
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=1, min_detection_confidence=0.7)
mp_drawing = mp.solutions.drawing_utils
# 打开摄像头
cap = cv2.VideoCapture(0)
# 初始化串口通信
# 请根据你的设备修改串口号和波特率
ser = serial.Serial('COM9', 9600, timeout=1)
def recognize_gesture(landmarks):
"""
根据手部关键点判断手势方向
:param landmarks: 手部关键点列表
:return: 手势方向(UP, DOWN, LEFT, RIGHT)
"""
# 获取手腕(0号关键点)和食指指尖(8号关键点)的坐标
wrist = landmarks[6] # (x, y, z)
index_tip = landmarks[8] # (x, y, z)
# 计算食指指尖相对于手腕的位置
dx = index_tip[0] - wrist[0] # 使用索引访问 x 坐标
dy = index_tip[1] - wrist[1] # 使用索引访问 y 坐标
# 判断手势方向
if abs(dx) > abs(dy): # 水平方向
if dx > 0:
return "RIGHT"
else:
return "LEFT"
else: # 垂直方向
if dy > 0:
return "DOWN"
else:
return "UP"
while True:
ret, frame = cap.read()
if not ret:
break
# 将图像从BGR转换为RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 处理图像并获取手部关键点
results = hands.process(frame_rgb)
# 如果检测到手部
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# 绘制手部关键点和连接线
mp_drawing.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)
# 获取关键点坐标
landmarks = []
for landmark in hand_landmarks.landmark:
landmarks.append((landmark.x, landmark.y, landmark.z))
# 判断手势方向
gesture = recognize_gesture(landmarks)
# 在画面中显示手势方向
cv2.putText(frame, f"Gesture: {gesture}", (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 根据手势方向发送相应的数字到串口
if gesture == "UP":
ser.write(b'\x00') # 发送 00000001
elif gesture == "DOWN":
ser.write(b'\x01') # 发送 00000010
elif gesture == "LEFT":
ser.write(b'\x02') # 发送 00000011
elif gesture == "RIGHT":
ser.write(b'\x03') # 发送 00000100
# 显示画面
cv2.imshow("Gesture Recognition", frame)
# 按下 'q' 键退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
ser.close() # 关闭串口
串口状态机
按照串口通信协议对数据进行提取
STATE_IDLE:begin
//闲置状态下对寄存器进行复位
r_rcv_cnt <= 4'd0;
r_data_rcv <= 'd0;
r_parity_check <= 1'b0;
o_rx_done <= 1'b0;
//连续检测到低电平时认为UART传来数据,拉高baud_valid
if(r_flag_rcv_start == 5'b00000)
baud_valid <= 1'b1;
end
STATE_START:begin
if(baud_pulse && sync_uart_rx) //波特率采样脉冲到来时再次检测是否为低电平,如果不为低电平,认为前期误检测,重新进入IDLE状态
baud_valid <= 1'b0;
end
STATE_DATA:begin
if(baud_pulse)
begin
r_data_rcv <= {sync_uart_rx, r_data_rcv[DATA_WIDTH-1 : 1]}; //数据移位存储
r_rcv_cnt <= r_rcv_cnt + 1'b1; //数据位计数
r_parity_check <= r_parity_check + sync_uart_rx; //校验位做加法验证高电平的奇偶
end
end
STATE_PARITY:begin
if(baud_pulse)
begin
//校验检测,正确则o_ld_parity拉高,可输出给led检测,如果闪烁则表示有错误数据发生
if(r_parity_check + sync_uart_rx == PARITY_TYPE)
o_ld_parity <= 1'b1;
else
o_ld_parity <= 1'b0;
end
else
o_ld_parity <= o_ld_parity;
end
STATE_END:begin
if(baud_pulse)
begin
//没有校验位或者校验位正确时才输出数据,否则直接丢弃数据
if(PARITY_ON == 0 || o_ld_parity)
begin
o_uart_data <= r_data_rcv;
o_rx_done <= 1'b1;
end
end
else
begin
o_rx_done <= 1'b0;
end
if(baud_cnt == 16'h0000)
baud_valid <= 1'b0;
end
default:;
endcase
LCD显示控制逻辑
反复读取进行图片的更新
always@(posedge sys_clk_50MHz or negedge sys_rst_n)
if(!sys_rst_n)
show_pic_flag <= 1'b0;
else if(cnt1 == 'd2||show_pic_done)
show_pic_flag <= 1'b1;
else
show_pic_flag <= 1'b0;
rpr0521rs I2C读数据
MAIN:begin
if(cnt_main >= 4'd11) cnt_main <= 4'd4; //写完控制指令后循环读数据
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
4'd0: begin dev_addr <= 7'h38; reg_addr <= 8'h40; reg_data <= 8'h0a; state <= MODE1; end //写入配置
4'd1: begin dev_addr <= 7'h38; reg_addr <= 8'h41; reg_data <= 8'hc6; state <= MODE1; end //写入配置
4'd2: begin dev_addr <= 7'h38; reg_addr <= 8'h42; reg_data <= 8'h02; state <= MODE1; end //写入配置
4'd3: begin dev_addr <= 7'h38; reg_addr <= 8'h43; reg_data <= 8'h01; state <= MODE1; end //写入配置
4'd4: begin state <= DELAY; dat_valid <= 1'b0; end //12ms延时
4'd5: begin dev_addr <= 7'h38; reg_addr <= 8'h44; state <= MODE2; end //读取配置
4'd6: begin prox_dat<= {dat_h,dat_l}; end //读取数据
4'd7: begin dev_addr <= 7'h38; reg_addr <= 8'h46; state <= MODE2; end //读取配置
4'd8: begin ch0_dat <= {dat_h,dat_l}; end //读取数据
4'd9: begin dev_addr <= 7'h38; reg_addr <= 8'h48; state <= MODE2; end //读取配置
4'd10: begin ch1_dat <= {dat_h,dat_l}; end //读取数据
4'd11: begin dat_valid <= 1'b1; end //读取数据
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end
图像获取
使用Image2Lcd获取图像单色数据,转换为对应格式直接存在代码中
功能展示
手指向左
手指向下
手指向右
手指向上
接近传感器演示难以在图片中展示,可以在视频中查看
遇到的难题及解决
首先是板子的LUT不足,无法直接在图片中放太大的图片,因此简单粗暴地将图片压缩至较小的清晰度,顺利解决。
接下来是接近传感器的I2C驱动,之前未接触过相关代码,最后费了很大的功夫也没能实现,最后参考硬禾开源社区的开源I2C驱动才终于实现。
资源占用
活动感想
本次活动学习了verilog的编写,还有I2C模块,tftlcd等一系列模块驱动的实现,收获颇丰。
最后,感谢硬禾学堂举办的寒假练活动为我提供的这次机会,祝硬禾的活动越办越好!