Opencv项目实战:17 贪吃蛇游戏

时间:2022-12-15 19:55:04

目录

0、项目介绍

1、效果展示

2、项目搭建

3、项目代码展示与讲解

4、项目资源

5、项目总结


0、项目介绍

这次是一个有意思的计算机视觉游戏——贪吃蛇,我们以食指为蛇头,不断的移动我们的手指,当吃到甜甜圈的时候,蛇身增长,当食指停止或触碰蛇身时,游戏结束。点击'r'将重新开始游戏。

1、效果展示

Opencv项目实战:17 贪吃蛇游戏

制作成GIF图片的效果有点卡,实际的效果还是很好的。

2、项目搭建

————17 Gluttonous snake

        ——Donut.png

        ——snake.py

3、项目代码展示与讲解

#snake.py

import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector

cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)

detector = HandDetector(detectionCon=0.8, maxHands=1)


class SnakeGameClass:
    def __init__(self, pathFood):
        self.points = []  # 蛇的所有点
        self.lengths = []  # 每个点之间的距离
        self.currentLength = 0  # 蛇的总长度
        self.allowedLength = 150  # 总共允许长度
        self.previousHead = 0, 0  # 起点

        self.imgFood = cv2.imread(pathFood, cv2.IMREAD_UNCHANGED)  # 读取甜甜圈的位置路径
        self.hFood, self.wFood, _ = self.imgFood.shape  # 得到甜甜圈图像的长、宽
        self.foodPoint = 0, 0  # 初始化甜甜圈的位置

        self.randomFoodLocation()  # 随机分布甜甜圈的位置   

        self.score = 0  # 吃得甜甜圈的分数
        self.gameOver = False # gameover

    def randomFoodLocation(self):
        self.foodPoint = random.randint(100, 1000), random.randint(100, 600)

    def update(self, imgMain, currentHead):

        if self.gameOver:
            cvzone.putTextRect(imgMain, "Game Over", [300, 400],
                               scale=7, thickness=5, offset=20)
            cvzone.putTextRect(imgMain, f'Your Score: {self.score}', [300, 550],
                               scale=7, 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

            # Check if snake ate the Food
            rx, ry = self.foodPoint
            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
            if self.points:
                for i, point in enumerate(self.points):
                    if i != 0:
                        cv2.line(imgMain, self.points[i - 1], self.points[i], (0, 0, 255), 20)
                cv2.circle(imgMain, self.points[-1], 20, (0, 255, 0), cv2.FILLED)

            # Add Food
            imgMain = cvzone.overlayPNG(imgMain, self.imgFood,
                                        (rx - self.wFood // 2, ry - self.hFood // 2))

            cvzone.putTextRect(imgMain, f'Score: {self.score}', [50, 80],
                               scale=3, thickness=3, offset=10)

            # Check for Collision
            pts = np.array(self.points[:-2], np.int32)
            pts = pts.reshape((-1, 1, 2))
            cv2.polylines(imgMain, [pts], False, (0, 255, 0), 3)
            minDist = cv2.pointPolygonTest(pts, (cx, cy), True)

            if -1 <= minDist <= 1:
                print("Hit")
                self.gameOver = True
                self.points = []  # all points of the snake
                self.lengths = []  # distance between each point
                self.currentLength = 0  # total length of the snake
                self.allowedLength = 150  # total allowed Length
                self.previousHead = 0, 0  # previous head point
                self.randomFoodLocation()

        return imgMain


game = SnakeGameClass("Donut.png")

while True:
    success, img = cap.read()
    img = cv2.flip(img, 1)
    hands, img = detector.findHands(img, flipType=False)

    if hands:
        lmList = hands[0]['lmList']
        pointIndex = lmList[8][0:2]
        img = game.update(img, pointIndex)
    cv2.imshow("Image", img)
    key = cv2.waitKey(1)
    if key == ord('r'):

        game.gameOver = False

    elif key==27:break

首先,打开摄像头的窗口得比较的大,不仅为了方便测试,而且窗口小了也会影响我们的手势识别,然后建立SnakeGameClass类,其下的变量名用途请查看注释。

其中,先看while True里面,cv2.flip函数是用窗口画面翻转的,输入一个正值,便会绕y轴翻转,这样能够保证窗口画面中的动作与我们的想法相同,接下来获得我们的手指landmark,即是获得目前蛇的头部landmark,之后完成我们的游戏更新部分。

其次,来看randomFoodLocation()函数,可以用于随机生成甜甜圈所在位置,我们为边缘留了些位置,避免手势识别失误和容易出界的问题。此时它并不具备更新位置的能力(只是生成了一个随机数位置),我们用update函数来完成主要的功能。我们想要实现的一个效果是游戏结束时,我需要得到一个提示,你不能再继续下去了,就如上面的GIF图片中,当我手势被识别时,会有一个提示(用putTextRcet完成),否则就会进行正常的游戏操作。

正常的游戏操作,我希望有这几个方面,甜甜圈覆盖窗口画面实现更新、蛇头与蛇身相撞就会gameover、手势一旦识别必须不停移动、吃掉一个甜甜圈增长一节蛇身等。

  • Length Reduction:

        一旦蛇的总长度超过了我们允许的长度,我们就需要对其进行缩减,这里使用了enumerate函数对列表进行了迭代,返回了一个索引和值,方便减去对应的距离。

  • Check if snake ate the Food:

        只要蛇头进入了甜甜圈的范围内,即可认为是吃到了甜甜圈,接下来就可以完成食物位置的更新,蛇身的加长,score得分。

  • Draw Snake:

        画蛇的初始态。

  • Add Food:

        在窗口画面中覆盖上甜甜圈,可以使用cvzone.overlayPNG函数。

  • Check for Collision:

        [pts]为多边形曲线阵列,在这里我们将会绘制一个多边形,并用cv2.pointPolygonTest来返回除最后两个点外与蛇头最近的距离,因为有tickness的原因,所以我们无需纠结于minDist一定小于等于0。

4、项目资源

GitHub:17 Gluttonous snake

5、项目总结

最近本来在准备期末考试的,所以很久没写博客了,但由于疫情原因提前返乡回家了,这学期的考试要得等到明年啦。想要添加按钮、声音、毒药这些的,没有精力搞了,有兴趣的大家可以试试。

Opencv项目实战:17 贪吃蛇游戏