2025寒假练 - 基于小脚丫FPGA实现手势识别控制LED显示
该项目使用了STEP Baseboard4.0底板+STEP MXO2 LPC核心板,Python语言,verilog语言,实现了手势识别系统的设计,它的主要功能为:通过mediapipe和PC摄像头对手势进行识别,并在FPGA的TFTLCD上以作出相应的显示。
标签
FPGA
张佳宁
更新2025-03-17
复旦大学
70

任务要求

  • 上位机通过大模型识别手势
  • 根据小脚丫核心板上的8个LED灯的变化状态来分别出显示手势识别的上下和左右
  • 手势的信息在扩展板的TFTLCD上显示出来
  • 小脚丫上三色灯的亮度显示接近传感器感知手势的远近

硬件介绍

小脚丫FPGA套件,这是一块功能很完善的综合FPGA开发套件,主控是一颗型号为MXO2的Lattice FPGA,芯片有4000多个LUT资源,96Kbit的用户闪存、92Kbit的RAM,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、8个7位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,外设很丰富,能满足大部分开发需要,期待能在后面体验其他的功能

FheM1anVmLBxLX0YIiIxNkkAIWf6

电脑主机,跑识别模型并通过串口将结果传给FPGA

项目设计

上位机识别

首先需要考虑如何在上位机利用大模型对手势实现识别。Mediapipe库提供了高效的手关键点检测算法,能够实时捕捉手势动作并解析出对应的手势类型,且Mediapipe使用的python语言有相应的库能很方便地调用摄像头和串口,因此使用python语言通过mediapipe库识别电脑摄像头录制的手势图像,并通过串口将结果通过FPGA。

数据传输

设计串口通信协议,将PC端识别的手势类型编码为固定格式的数据帧。用8个字节表示手势类型,帧头和帧尾添加标志位以确保数据传输的可靠性。FPGA通过串口接收数据并解析协议,提取手势类型。

控制显示

FPGA接收到手势类型后,根据预定义的手势与图像的映射关系,选择合适的图像数据进行显示,其中图像数据以查找表的形式预存至代码中,tftlcd以逐行更新的方式进行扫描,扫描到底部时重新回到顶部进行扫描,从而实现手势变化时图像的切换。

距离显示

距离的显示独立于上述过程,利用I2C通信从rpr0521rs传感器获取距离信息,并映射到三色灯显示。

PlantUML diagram

实现过程

上位机手势识别

利用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获取图像单色数据,转换为对应格式直接存在代码中

image.png

功能展示

手指向左

76d11ff1ff6a4b73973377cca17b9a6.jpg

手指向下

aa60834a679427db221c46ebd991308.jpg

手指向右

7b88f055fed8a3cd26fa779fe79177e.jpg

手指向上

be95e63bac3e876c19cd15edc4cf116.jpg

接近传感器演示难以在图片中展示,可以在视频中查看

遇到的难题及解决

首先是板子的LUT不足,无法直接在图片中放太大的图片,因此简单粗暴地将图片压缩至较小的清晰度,顺利解决。

接下来是接近传感器的I2C驱动,之前未接触过相关代码,最后费了很大的功夫也没能实现,最后参考硬禾开源社区的开源I2C驱动才终于实现。

资源占用

image.png

活动感想

本次活动学习了verilog的编写,还有I2C模块,tftlcd等一系列模块驱动的实现,收获颇丰。

最后,感谢硬禾学堂举办的寒假练活动为我提供的这次机会,祝硬禾的活动越办越好!

附件下载
gesture_impl1.jed
gesture.zip
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号