Python利用手勢識別實(shí)現(xiàn)貪吃蛇游戲
一、前言
想必大家都玩過貪吃蛇的游戲吧:通過操縱蛇的移動方向能夠讓蛇吃到隨機(jī)出現(xiàn)的食物,吃到的食物越多,蛇就會變得越長,但如果不小心撞到了自己,那么蛇就會死亡,game over!! 我們玩過的貪吃蛇游戲一般都是在手機(jī)或者游戲機(jī)上進(jìn)行的,通過方向鍵操縱蛇的移動,那么我們是否可以直接使用一個(gè)攝像頭捕捉我們的手勢動作,并用手的移動來代表貪吃蛇的移動呢?當(dāng)然可以,今天我就和大家一起完成這個(gè)游戲的設(shè)計(jì)并愉快的玩耍。
Let's Start!
二、項(xiàng)目介紹
1、游戲的操作方式
貪吃蛇游戲人盡皆知,計(jì)算機(jī)視覺鮮為人知,計(jì)算機(jī)視覺+貪吃蛇游戲會帶給人們更多的參與感以及新鮮度,本次這個(gè)項(xiàng)目就是主要使用手勢識別來完成貪吃蛇這個(gè)簡單的游戲。在這個(gè)游戲中,電腦通過攝像頭捕捉到我們的手勢并判別是否進(jìn)行移動,玩家移動手去操縱貪吃蛇得到屏幕中隨機(jī)出現(xiàn)的食物,每得到一個(gè)食物,就會算作一分,Score 就會加1并顯示在畫面中,當(dāng)玩家在操作的過程中不小心使得蛇的頭部和身體相撞,那么就會顯示GameOver! 按下 ‘r’ 鍵可以重新開始游戲。

2、開發(fā)的過程中的注意事項(xiàng)
(1) 圖像的左右問題
由于我們是使用手勢來進(jìn)行控制蛇的移動的,但攝像頭的畫面顯示的是別人的視角,所以這和玩家的左右意識剛好是相反的,因此我們要將攝像頭讀取到的畫面進(jìn)行一個(gè)左右的翻轉(zhuǎn)。原理上說就是將左右的像素點(diǎn)位置進(jìn)行一個(gè)調(diào)換,但在 Python 中可以使用一個(gè) cv2.flip( ) 函數(shù)就可以實(shí)現(xiàn)鏡像翻轉(zhuǎn)了。
(2) 攝像頭的畫面尺寸問題
通
過攝像頭得到的圖像我們需要在上面進(jìn)行游戲,因此畫面過小會導(dǎo)致游戲空間不足,在最開始可以對畫面的大小進(jìn)行一個(gè)預(yù)處理,設(shè)定一個(gè)較為合理的大小,最后得到的畫面玩游戲時(shí)才不會顯得局促。通過函數(shù) cap.set(3, m) cap.set(4, n) 可以實(shí)現(xiàn)對畫面的寬和高的設(shè)定。
本項(xiàng)目中還會存在一些其他的注意事項(xiàng),比如判斷碰撞,判斷獲得食物等,我會在后面的項(xiàng)目過程中再加以介紹。
三、游戲的實(shí)現(xiàn)要點(diǎn)
1、選擇第三方庫
一些使用到的第三方庫:
import math import random import cvzone import cv2 import numpy as np from cvzone.HandTrackingModule import HandDetector
在本次項(xiàng)目中,我們主要使用到以上的幾個(gè)庫,其中使用 random 庫來隨機(jī)選擇像素點(diǎn)來放置食物甜甜圈,使用 cvzone 中的手部識別來進(jìn)行玩家手勢的檢測,使用 cv2 來進(jìn)行一些基礎(chǔ)的圖像操作,其他的一些庫也各有用處,后面一一介紹。
2、找到關(guān)鍵點(diǎn)并標(biāo)記
在本次游戲中我們是選擇了一只手作為目標(biāo)節(jié)點(diǎn),所以當(dāng)我們檢測到畫面中出現(xiàn)手部時(shí)需要對其中的關(guān)鍵點(diǎn)進(jìn)行標(biāo)記,而這個(gè)關(guān)鍵點(diǎn)恰好是我們的貪吃蛇的頭部,由于我們是調(diào)用的第三方庫,而該庫可以對手部進(jìn)行3D的標(biāo)記,但我們只需要 x,y 兩個(gè)坐標(biāo)值就可以了,主要使用以下函數(shù)進(jìn)行手部關(guān)鍵節(jié)點(diǎn)的標(biāo)記:
#檢測到第一個(gè)手,并標(biāo)記手部位置
if hands:
lmList = hands[0]['lmList']
pointIndex = lmList[8][0:2] #第八個(gè)坐標(biāo)點(diǎn)的 x, y值,其中 z 值不被包括在里面
cv2.circle(img, pointIndex, 20, (200, 0, 200), cv2.FILLED) #在關(guān)鍵點(diǎn)處繪制一個(gè)圓點(diǎn)并進(jìn)行填充(此處只是示范,后面會更改)3、創(chuàng)建一個(gè)類來保存關(guān)于游戲的所有功能
我們需要實(shí)現(xiàn)的游戲是很多功能結(jié)合起來完成的,如果想要使用函數(shù)來實(shí)現(xiàn)這些功能,那么將會非常麻煩,當(dāng)我們使用 class 來完成時(shí),由于很多東西都保存在同一個(gè)類中,將會降低難度。在這個(gè) class 中我們將會創(chuàng)建很多重要的列表來存儲我們用得到的一些關(guān)鍵點(diǎn),比如貪吃蛇的身上的所有的點(diǎn)、貪吃蛇的長度、蛇的總體距離、食物的放置、得分等:
class SnakeGameClass:
def __init__(self, pathFood):
self.points = [] #貪吃蛇身上所有點(diǎn)
self.lengths = [] #點(diǎn)與點(diǎn)之間的距離
self.currentLength = 0 #當(dāng)下蛇的長度
self.allowedLength = 50 #最大允許長度(閾值)
self.previousHead = 0, 0 #手部關(guān)鍵點(diǎn)之后的第一個(gè)點(diǎn)
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 = False4、定義函數(shù)進(jìn)行不斷更新
隨著我們的手部的移動,貪吃蛇的長度以及位置都會發(fā)生變化,所以我們需要?jiǎng)?chuàng)建一個(gè)函數(shù)來不斷進(jìn)行更新,滿足變化的需求(該部分也是在前面創(chuàng)建的大類里面完成的):
def update(self, imgMain, currentHead):
#游戲結(jié)束,打印文本
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
#長度縮小
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
#檢查貪吃蛇是否已經(jīng)觸碰到食物
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)
#使用線條繪制貪吃蛇
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)
#顯示食物
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)
#檢測是否碰撞
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 = [] #蛇身上所有的點(diǎn)
self.lengths = [] #不同點(diǎn)之間的距離
self.currentLength = 0 #當(dāng)前蛇的長度
self.allowedLength = 50 #最大允許長度
self.previousHead = 0, 0 #先前的蛇的頭部
self.randomFoodLocation()
return imgMain在這個(gè)更新的函數(shù)中,我們需要判斷很多東西,比如貪吃蛇是否觸碰到食物(如果觸碰到食物我們就要增加蛇的長度并累積得分)、當(dāng)前長度是否超過所允許的最大長度(當(dāng)前長度小于最大長度就不必要進(jìn)行更改了,但如果當(dāng)前長度大于最大長度,則需要進(jìn)行縮短)、貪吃蛇是否發(fā)生碰撞(通過關(guān)鍵節(jié)點(diǎn)之間的距離判斷貪吃蛇是否發(fā)生了碰撞,如果發(fā)生了碰撞,則進(jìn)入 gameover 模塊,如果沒有,繼續(xù)游戲)等,都解釋在上面的代碼中了。
主要是通過上面定義的 class 我們就能實(shí)現(xiàn)當(dāng)前的貪吃蛇游戲了。
四、總體代碼
本次小游戲我是在b站看到教程并一步步復(fù)現(xiàn)出來的,大家感興趣可以嘗試一下,當(dāng)然按照慣例整體代碼會貼在下面:
"""
Author:XiaoMa
CSDN Address:一馬歸一碼
"""
import math
import random
import cvzone
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector
cap = cv2.VideoCapture(0)
#設(shè)置畫面的尺寸大小,過小的話導(dǎo)致貪吃蛇活動不開
cap.set(3, 1280)
cap.set(4, 720)
detector = HandDetector(detectionCon=0.8, maxHands=1)
class SnakeGameClass:
def __init__(self, pathFood):
self.points = [] #貪吃蛇身上所有點(diǎn)
self.lengths = [] #每一個(gè)點(diǎn)之間的距離
self.currentLength = 0 #當(dāng)下蛇的長度
self.allowedLength = 50 #最大允許長度(閾值)
self.previousHead = 0, 0 #手部關(guān)鍵點(diǎn)之后的第一個(gè)點(diǎn)
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
def randomFoodLocation(self):
self.foodPoint = random.randint(100, 1000), random.randint(100, 600)
def update(self, imgMain, currentHead):
#游戲結(jié)束,打印文本
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
#長度縮小
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
#檢查貪吃蛇是否已經(jīng)觸碰到食物
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)
#使用線條繪制貪吃蛇
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)
#顯示食物
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)
#檢測是否碰撞
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 = [] #蛇身上所有的點(diǎn)
self.lengths = [] #不同點(diǎn)之間的距離
self.currentLength = 0 #當(dāng)前蛇的長度
self.allowedLength = 50 #最大允許長度
self.previousHead = 0, 0 #先前的蛇的頭部
self.randomFoodLocation()
return imgMain
game = SnakeGameClass("Donut.png")
while True:
success, img = cap.read()
img = cv2.flip(img, 1) #鏡像翻轉(zhuǎn)
hands, img = detector.findHands(img, flipType=False)
#檢測到第一個(gè)手,并標(biāo)記手部位置
if hands:
lmList = hands[0]['lmList']
pointIndex = lmList[8][0:2] #第八個(gè)坐標(biāo)點(diǎn)的 x, y值,其中 z 值不被包括在里面
#cv2.circle(img, pointIndex, 20, (200, 0, 200), cv2.FILLED) #在關(guān)鍵點(diǎn)處繪制一個(gè)圓點(diǎn)并進(jìn)行填充(此處只是示范,后面會更改)
img = game.update(img, pointIndex)
cv2.imshow("Image", img)
key = cv2.waitKey(1)
#按下‘r'重新開始游戲
if key == ord('r'):
game.gameOver = False至于需要使用到的甜甜圈的圖案,可以網(wǎng)上找一個(gè)合適的大小的圖案進(jìn)行替代即可。

到此這篇關(guān)于Python利用手勢識別實(shí)現(xiàn)貪吃蛇游戲的文章就介紹到這了,更多相關(guān)Python貪吃蛇游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python爬蟲進(jìn)階之爬取某視頻并下載的實(shí)現(xiàn)
這篇文章主要介紹了Python爬蟲進(jìn)階之爬取某視頻并下載的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
flask框架配置mysql數(shù)據(jù)庫操作詳解
這篇文章主要介紹了flask框架配置mysql數(shù)據(jù)庫操作,結(jié)合實(shí)例形式詳細(xì)分析了flask框架配置mysql數(shù)據(jù)庫及連接訪問等相關(guān)操作技巧,需要的朋友可以參考下2019-11-11
python使用rstrip函數(shù)刪除字符串末位字符
rstrip函數(shù)用于刪除字符串末位指定字符,默認(rèn)為空白符,這篇文章主要介紹了python使用rstrip函數(shù)刪除字符串末位字符的方法,需要的朋友可以參考下2023-04-04
Python實(shí)現(xiàn)序列化及csv文件讀取
這篇文章主要介紹了Python實(shí)現(xiàn)序列化及csv文件讀取,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
conda?install?nb_conda失敗原因分析及解決
這篇文章主要給大家介紹了關(guān)于conda?install?nb_conda失敗原因分析及解決方法,conda install nb_conda顯示錯(cuò)誤的原因可能有很多,具體原因取決于你的系統(tǒng)環(huán)境和安裝的conda版本,需要的朋友可以參考下2023-11-11
OpenCV根據(jù)面積篩選連通域?qū)W習(xí)示例
這篇文章主要為大家介紹了OpenCV根據(jù)面積篩選連通域?qū)W習(xí)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

