手势识别实现贪吃蛇

本文介绍了如何使用Python、OpenCV和cvzone库创建一个交互式贪吃蛇游戏,通过手势控制蛇的移动,并在实时视频中应用面部马赛克保护隐私。游戏逻辑涉及手部追踪、蛇的移动、得分系统和游戏结束条件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SnakeGame


前言

Author:yuansen zhang 

Email:<2493884567@qq.com>

Date:12/18/2023

本课程设计环境安装方式

确保您配置pip、conda等,其外python版本可以选择3.7版本,但是推荐3.8

conda create -n testx python=3.8

conda activate testx

pip install opencv-python matplotlib absl-py protobuf pyyaml

pip uninstall mediapipe

pip install mediapipe

pip install cvzone

或者一键安装可以定制化您的环境,提供了安装样本
pip(conda) install -r requirement.txt

在这里插入图片描述

课程设计

使用手势跟踪和面部马赛克的贪吃蛇游戏:Python OpenCV 项目

此项目是一个互动式贪吃蛇游戏,结合了手势追踪来控制游戏和实时面部检测与马赛克效果。它使用了 Python、OpenCV 和 cvzone 库。

概览

  • 目标:通过手部动作控制贪吃蛇游戏,并在实时视频中对检测到的面部应用马赛克效果。
  • 技术:Python、OpenCV、cvzone、手部追踪模块、NumPy、人脸识别模块。

特点

  1. 手势跟踪游戏控制:利用 cvzone 的 HandDetector 模块跟踪手部动作,控制游戏中的贪吃蛇。

  2. 实时面部检测和马赛克效果:检测视频中的面部并实时应用马赛克效果,确保游戏过程中的隐私。

  3. 游戏逻辑实现:包括吃食物、蛇身增长、记分和游戏结束等功能。

实现

库和模块
  • OpenCV (cv2):用于图像处理和视频处理。
  • cvzone:用于手部追踪和叠加图像。
  • NumPy:用于数值操作。
  • HandDetector:来自 cvzone,用于检测和跟踪手部动作。
关键函数
  • apply_mosaic():在图像的指定区域应用马赛克效果。
  • SnakeGameClass:封装游戏逻辑和渲染的类。
  • update():更新游戏状态,包括蛇的移动、食物消耗和碰撞检测。
SnakeGameClass成员变量

在编写这个类的时候,我们需要有成员变量,在这里,我们需要最基础的几个参数如下

在这里插入图片描述

  • List Of Points 坐标表

  • List Of Distances 距离表

  • Current Length 当前长度

  • Total Length 允许的总长度

  • Head 头部结点坐标

  • Previous Head 前一个头部结点坐标

在代码中,具体的变量为

  • points 坐标表
  • lengths 距离表
  • currentLength 当前长度
  • allowedLength 允许的总长度
  • previousHead 前一个头部结点坐标
  • imgFood 食物的图片文件
  • hFood 食物图片高度
  • wFood 食物图片宽度
  • foodPoint 初始化食物坐标
  • randomFoodLocation() 随机生成食物的坐标
  • score 分数
  • gameOver 游戏是否结束
Length Reduction 收缩长度
# Length Reduction 收缩长度
        if self.currentLength > self.allowedLength:
            for i, length in enumerate(self.lengths):
                self.currentLength -= length
                self.lengths.pop(i)
                self.points.pop(i)
                if self.currentLength < self.allowedLength:
                    break

在这里插入图片描述

每一次更新完成之后,都会检查当前长度是否大于允许的总长度。

假如当前长度大于允许的总长度,就会依次去掉末尾的那个结点,直到当前长度在允许的总长度范围内。

Check if snake ate the food 是否吃了食物
# Check if snake ate the food 是否吃了食物
    rx, ry = self.foodPoint
    # if rx < cx < rx + self.wFood and ry < cy < ry + self.hFood:
    #     print("ate")
    if rx - self.wFood // 2 < cx < rx + self.wFood // 2 and \
        ry - self.hFood // 2 < cy < ry + self.hFood // 2:
            self.randomFoodLocation()
            self.allowedLength += 50
            self.score += 1
            # print(self.score)

每次都会检查食指的坐标是否在食物的区域内,如果在这个区域内,则会重新生成食物的坐标,并且让分数加一和让允许的长度增加一定长度。

Draw Snake 画蛇
# Draw Snake 画蛇
if self.points:
    for i, point in enumerate(self.points):
        if i != 0:
            self.points[i-1] = tuple(self.points[i-1])  # 转换数据格式22
            self.points[i] = tuple(self.points[i])  # 转换数据格式22
            cv2.line(imgMain, self.points[i - 1], self.points[i], (0, 0, 255), 20)
            # 对列表最后一个点也就是蛇头画为紫色点
            self.points[-1] = tuple(self.points[-1])  # 转换数据格式22
            cv2.circle(imgMain, self.points[-1], 20, (200, 0, 200), cv2.FILLED)

依次把坐标表用线连起来,但是第一个坐标,也就是Head会更大一点,颜色也会不同。

Draw Food 画食物
# Draw Food 画食物
# rx, ry = self.foodPoint
imgMain = cvzone.overlayPNG(imgMain, self.imgFood,(rx - self.wFood // 2, ry - self.hFood // 2))

一层一层地覆盖图像,达到更新图片的目的。

Check for Collision 检查是否缠绕
# Check for Collision 检查是否缠绕
pts = np.array(self.points[:-2], np.int32)
pts = pts.reshape((-1, 1, 2))  # 重塑为一个行数未知但只有一列且每个元素有2个子元素的矩阵
cv2.polylines(imgMain, [pts], False, (0, 200, 0), 3)
# 第三个参数是False,我们得到的是不闭合的线
minDist = cv2.pointPolygonTest(pts, (cx, cy), True)
# print(minDist)
# 参数True表示输出该像素点到轮廓最近距离


if -1 <= minDist <= 1:
print("Hit")
self.gameOver = True
self.points = []  # 蛇身上所有的点
self.lengths = []  # 每个点之间的长度
self.currentLength = 0  # 蛇的总长
self.allowedLength = 150  # 蛇允许的总长度
self.previousHead = 0, 0  # 第二个头结点
self.randomFoodLocation()

这不是最好的方法,但是是最简单的方法。

在这里插入图片描述

首先忽略掉前两个点Head 和 Previous Head,因为他们之间一般不会缠绕,强行计算可能出错。

  1. 接下来就是连接,Head和其他的点,并且判断是否会形成一个多边形,也就是形成闭环。
  2. 与此同时也会计算Head与各个点之间的最小距离,如果距离的绝对值无限接近于0,则代表接触,形成闭环。

形成闭环,代表着蛇的身体已经缠绕,游戏结束。

randomFoodLocation()
def randomFoodLocation(self):
        self.foodPoint = random.randint(self.wFood//2, 640 - self.wFood//2), random.randint(self.hFood//2, 480 - self.hFood//2)

生成食物的随机坐标。

游戏主体循环

  • 从本地摄像头捕捉视频。

    #当然了这句话是在循环外面
    cap = cv2.VideoCapture(0) #0为自己的摄像头
    success, img = cap.read() #将视频处理为帧
    
  • 翻转图像以获得镜像视图。

    # 左手是左手,右手是右手,映射正确
     hands, img = detector.findHands(img, flipType=False)
    # 我们知道人在视频里面肯本身正好要相反在,这样做在目的要在控制蛇头在时候可以正确的操作
    #主要目的要为了得到 hands 这个标签,方便接下来显示贪吃蛇画面,
    #如果检测不到手的存在就不会显示贪吃蛇的画面,这样有利于展示马赛克的效果
    
  • 检测和跟踪手部动作。

    if hands:
        lmList = hands[0]['lmList']     # hands是由N个字典组成的列表
        pointIndex = lmList[8][0:2]     # 只要食指指尖的x和y坐标
        pointThumb = lmList[4][0:2]  # 大拇指指尖
        distance = math.hypot(pointIndex[0] - pointThumb[0], pointIndex[1] - pointThumb[1])
        pointIndex = tuple(pointIndex)  #转换数据格式
        # print(type(img))
        # cv2.circle(img, pointIndex, 20, (200, 0, 200), cv2.FILLED)
        img = game.update(img,pointIndex)
    
    

    这个代码重点要解释hands是一个怎么样的数组,所以这里插入一张图片

在这里插入图片描述

我们并不需要对于hands这个数组究本溯源,我们通过查看 https://github.com/fandhikazhr/handDetector?tab=readme-ov-file(cv2的手势识别) 这个仓库中在代码或者说明文档,就可以得知我们只需要调用hands[0][‘lmList’] 这个数组就可以得到上图我们关注的点位了

那么,显然我们的想法要用食指来控制蛇头,那么我们只需要关注8这个点位,其实每个点位之间亦有距离这个概念,所以hands这个模块将这段距离细分给它的up节点,所以用**[8][0:2]**来表示我们的食指位置

  • 根据手的位置更新游戏状态。
    def update(self, imgMain, currentHead):     # 实例方法
        
        #为了实现重玩功能
        if self.gameOver:
            cvzone.putTextRect(imgMain, "Game Over", [300, 400],
                               scale=3, thickness=5, offset=20)
            cvzone.putTextRect(imgMain, f'Your Score:{self.score}', [250, 300],
                               scale=3, thickness=5, offset=20)
        else:
            px, py = self.previousHead
            cx, cy = currentHead
    
            self.points.append([cx, cy])             # 添加蛇的点列表节点
            distance = math.hypot(cx - px, cy - py)  # 两点之间的距离
            self.lengths.append(distance)            # 添加蛇的距离列表内容
            self.currentLength += distance
            self.previousHead = cx, cy
            # Length Reduction 收缩长度
            if self.currentLength > self.allowedLength:
                for i, length in enumerate(self.lengths):
                    self.currentLength -= length
                    self.lengths.pop(i)
                    self.points.pop(i)
                    if self.currentLength < self.allowedLength:
                        break
        
    
    

在这里插入图片描述

  • 检测面部并应用马赛克效果。

    def apply_mosaic(img, top_left, bottom_right, mosaic_size):
    (x1, y1), (x2, y2) = top_left, bottom_right
    roi = img[y1:y2, x1:x2]
    # 计算每个小块的大小
    w, h = (x2 - x1) // mosaic_size, (y2 - y1) // mosaic_size
    for i in range(0, x2 - x1, w):
        for j in range(0, y2 - y1, h):
            rect = [i + x1, j + y1, w, h]
            # 获取每个小块的颜色值
            color = img[j + y1 + h // 2, i + x1 + w // 2]
            # 填充小块
            cv2.rectangle(img, (rect[0], rect[1]), (rect[0] + rect[2], rect[1] + rect[3]), color.tolist(), -1)
    return img
    

    面部识别就是简单的调库,在之前的学习也反复的用过了多次,这里就不解释这个库函数。

    然后说一下这个马赛克的操作是怎么实现的,就是根据将识别脸部的区域,分成若干小块,然后遍历这个区域然后把所有的小块统一的扩散为相同的颜色

  • 显示更新的图像和游戏状态。

    cv2.imshow("Image",img)
    
  • 当手指捏合或按下 ‘r’ 键时重置游戏。

    pointIndex = lmList[8][0:2]     # 只要食指指尖的x和y坐标
    pointThumb = lmList[4][0:2]  # 大拇指指尖
    distance = math.hypot(pointIndex[0] - pointThumb[0], pointIndex[1] - pointThumb[1])
    
    if distance < 40:  # 如果食指和大拇指的距离小于40像素,则重置游戏
        key = ord('r')  
    
    if key == ord('r'):
        game.gameOver = False
    

    这个部分纯粹为了增加可玩性,每次按下 r 太过于麻烦,换得敲下回车,所以设想了如果给出一个 OK 的手势,能够继续游戏就好了。因为如果手指一直不动,蛇会一直向前走
    然后我们可以通过计算食指和大拇指的距离来计算是否在 OK 的状态,如果小于40,那么就重置游戏。

  • 按下ESC退出摄像头调用并释放资源

    elif key == 27:
        #释放内存
        cv2.destroyAllWindows(0)
        #释放摄像头
        cap.release()
        break
    
程序运行界面

在这里插入图片描述
在这里插入图片描述

遇到的问题

  • 摄像头:硬件不支持调参,无法进行set

    #解决办法1:
    cv2.namedWindow("Image", cv2.WINDOW_NORMAL)
    cv2.setWindowProperty("Image", cv2.WND_PROP_FULLSCREEN, cv2.WND_PROP_FULLSCREEN)
    #缺点:
    #这个方法实际上和cv2.resize()的效果是无二的,就是对画面进行了一个拉扯,画面依然是糊的
    
    #解决办法2:
    cv2.resize()
    #缺点同1
    
    #解决办法3:
    #手机上传视频
    #优点:
    #画面清晰,像素点可定义高和宽
    #缺点:
    #无法演示贪吃蛇部分,因为食物刷新无法预测,如果将食物的刷新位置写死,去录视频这也不现实,不可能完美的去吃食物;而且可能这个过程会gameover,无法预测OK手势
    
  • CPU占用率高

    #因为使用到的模块是基于CPU,好处是不使用CUDA对我这个A卡用户非常友好
    #坏处是在一些架构非常清朝的CPU(如:学校机房),调用这个模块会非常慢
    
  • 马赛克算法影响了Hit

      #把马赛克算法放到Hit检测的后面:
      # 为每个检测到的人脸应用马赛克
      for (x, y, w, h) in faces:
          img = apply_mosaic(img, (x, y), (x + w, y + h), 15)
    

心得体会

  • cv2是一个非常好玩的库,里面有非常多的模版,我们可以在这个基础上加上自己的一些想法,来实现一些有意思的功能。
  • OpenCV课程的内容有点多,大部分是讲了一些cv2库内的功能是通过算法怎么来实现的,但是作为初学者,我们还是要知道大概的库函数(参数,返回值,功能)这些基本的知识,方便我们在以后开发的时候能够轻松一点
  • 随着AI热潮来袭,作为一名计算机学生要善用AI工具,提高开发效率。

参考文献

[1] OpenCV面向Python,李立宗著,电子工业出版社,2019

[2] https://github.com/fandhikazhr/handDetector?tab=readme-ov-file 手势识别

[3] https://www.youtube.com/watch?v=w26Ze6lP02Y 贪吃蛇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值