使用 Python 和 OpenCV 制作反應(yīng)游戲
共 19902字,需瀏覽 40分鐘
·
2024-04-19 10:20
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)
在本文中,將向你展示如何使用 OpenCV 在 Python 中制作一個(gè)反應(yīng)游戲,你可以動(dòng)手來玩。
你可能已經(jīng)熟悉 OpenCV,OpenCV 基本上允許進(jìn)行各種圖像處理。
你可以在下面的視頻中看到最終結(jié)果,并且可以在此處獲取文件:https://github.com/Goncalo-Chambel/ReactionGame
盡管這可能看起來很復(fù)雜(取決于你的專業(yè)知識(shí)),但在我們看來,這是一個(gè)相當(dāng)簡(jiǎn)單但很有趣的項(xiàng)目。你基本上可以用 200 行代碼創(chuàng)建一個(gè)游戲(這代碼量很少了!)。
我們將把任務(wù)分成幾個(gè)部分:設(shè)置+手部檢測(cè)、主要游戲機(jī)制、創(chuàng)建實(shí)際游戲和最后潤(rùn)色。
第 1 步:設(shè)置 + 手部檢測(cè)
這個(gè)項(xiàng)目的主要目標(biāo)是創(chuàng)建一個(gè)反應(yīng)游戲,其中圓圈會(huì)隨機(jī)出現(xiàn)在屏幕上,你必須用你的手盡可能快地“觸摸”它們。
因此,第一個(gè)步驟是讓程序訪問你的網(wǎng)絡(luò)攝像頭。
為此,我們將使用 OpenCV 庫(kù),為此我們只需添加一行import cv2。就這么簡(jiǎn)單,但如果你還沒有安裝,你必須先安裝它。
在此處添加了此項(xiàng)目的要求:https://github.com/Goncalo-Chambel/ReactionGame/blob/main/requirements.txt
因此你可以通過在命令行中鍵入pip install -r requirements.txt來安裝所有這些要求。
cv2 庫(kù)有很多功能,但讓我們一步一步來。第一個(gè)目標(biāo)是告訴 Python 從網(wǎng)絡(luò)攝像頭讀取數(shù)據(jù)并將其顯示在屏幕上。這可以通過使用函數(shù)cv2.VideoCapture()來完成
import cv2
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
現(xiàn)在cap變量包含對(duì)你的網(wǎng)絡(luò)攝像頭的引用。然后在我們的主文件中,我們可以創(chuàng)建一個(gè)無限循環(huán),每次迭代都會(huì)顯示網(wǎng)絡(luò)攝像頭捕獲的當(dāng)前圖像。
import cv2
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # set width of window
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # set height of window
while True:
ret, frame = cap.read()
cv2.imshow("Reaction Game", frame)
k = cv2.waitKey(1) & 0xFF
if k == ord('q'):
break
有了這幾行代碼,我們應(yīng)該有一個(gè)程序,這個(gè)程序可以簡(jiǎn)單地打開一個(gè)新窗口(名為“反應(yīng)游戲”),大小為 1280 x 720 像素,帶有網(wǎng)絡(luò)攝像頭的視頻源。我還添加了最后幾行代碼,以便你可以關(guān)閉窗口并按“q”退出程序。
現(xiàn)在我們可以繼續(xù)檢測(cè)手了。創(chuàng)建一個(gè)算法來檢測(cè)視頻中的手是一項(xiàng)復(fù)雜的任務(wù),但幸運(yùn)的是我們不必重新發(fā)明輪子。有一個(gè)非常好的庫(kù)可以為我們做這件事,叫做 CV Zone。
CV Zone 允許各種與對(duì)象檢測(cè)相關(guān)的項(xiàng)目,但我們主要對(duì) HandDetector 模型感興趣。該模型使我們能夠獲得有關(guān)視頻中被跟蹤手的重要信息,例如它們的中心位置或邊界框。我們初始化模型的方式如下:
from cvzone.HandTrackingModule import HandDetector
detector = HandDetector(detectionCon=0.8, maxHands=2)
其中detectionCon是置信區(qū)間(從0到1),表明你希望模型跟蹤手的精度。更高的值意味著模型更確信被跟蹤的手就是手,但也可能使模型“錯(cuò)過手”,因?yàn)樗鼪]有信心相信它們是手。maxHands參數(shù)僅限制模型一次可以跟蹤的手的數(shù)量。
然后在我們的主循環(huán)中,我們只調(diào)用函數(shù)detector.findHands()來獲取有關(guān)被跟蹤手的信息。
while True:
ret, frame = cap.read()
hands, frame = detector.findHands(frame, flipType=False)
cv2.imshow("Reaction Game", frame)
輸出:
如你所見,該算法非常擅長(zhǎng)跟蹤手部(包括手指位置)。請(qǐng)注意,根據(jù)你的規(guī)格,該程序可能看起來有點(diǎn)“不穩(wěn)定”。此外,根據(jù)你使用的相機(jī),你可能不需要翻轉(zhuǎn)圖像,只需檢查標(biāo)簽“左手”是否真的對(duì)應(yīng)于你的左手。
最后,在屏幕上顯示所有這些信息可能會(huì)太混亂,因此我們可以告訴檢測(cè)器不要通過書寫來繪制這些信息
hands = detector.findHands(frame, flipType=False, draw=False)
請(qǐng)注意,當(dāng)我們包含參數(shù)draw=False時(shí),detector僅返回手信息(與返回幀相反)。
第 2 步:主要游戲機(jī)制
我們現(xiàn)在可以研究我們游戲的基本機(jī)制。基本想法是在屏幕上隨機(jī)生成圓圈并檢測(cè)手是否在觸摸它。
這里有兩個(gè)主要部分,畫圈和檢查碰撞。
畫圈
這是一個(gè)相對(duì)簡(jiǎn)單的步驟,因?yàn)?em>cv2有一個(gè)內(nèi)置函數(shù)cv2.circle(),可以在屏幕上繪制一個(gè)圓形,該函數(shù)將我們正在繪制的圖像、屏幕上的位置(以像素為單位)、圓的半徑(以像素為單位)、顏色(以BGR) 和線條粗細(xì)作為輸入。所以假設(shè)我們想在屏幕中間放置一個(gè)紅色圓圈,厚度為2,半徑為 50 像素,我們會(huì)這樣做:
while True:
ret, frame = cap.read()
cv2.circle(frame,(int(width/2),int(heigh/2)), 50, (0,0,255), 2)
cv2.imshow("Reaction Game", frame)
請(qǐng)注意,此函數(shù)不返回任何內(nèi)容,它會(huì)自動(dòng)更新frame變量。此外,如果你想要一個(gè)實(shí)心圓而不僅僅是輪廓,你可以將粗細(xì)設(shè)置為 -1。
在繼續(xù)在屏幕上創(chuàng)建隨機(jī)圓圈之前,讓我們先創(chuàng)建自己的Circle類。如果我們想在只處理一個(gè)變量的同時(shí)訪問圓的多個(gè)屬性,這將很方便。這是一個(gè)非常簡(jiǎn)單的類,現(xiàn)在讓我們添加一個(gè)構(gòu)造函數(shù)和一個(gè)draw()方法
class Circle:
def __init__(self, coordinates, radius, color, thickness):
self.coordinates = coordinates
self.radius = radius
self.color = color
self.thickness = thickness
def draw(self, _frame):
cv2.circle(_frame, self.coordinates, self.radius, self.color, self.thickness)
現(xiàn)在如果我們想像之前那樣畫一個(gè)圓圈,我們會(huì)這樣做:
target = Circle((int(width/2),int(heigh/2)), 50, (0, 0,255), 2)
while True:
ret, frame = cap.read()
target.draw(frame)
cv2.imshow("Reaction Game", frame)
現(xiàn)在可能看起來不是很有用,但以后會(huì)有所幫助。
在繼續(xù)討論交集方法本身之前,我們首先需要一些東西來檢查交集。我們已經(jīng)有了一種在屏幕上創(chuàng)建目標(biāo)的方法,但我們需要將其與某些東西進(jìn)行比較。很明顯,有些東西會(huì)是我們的手,但具體是手的哪一部分?
我們可以使用邊界框,但我覺得這可能太容易了。我們還可以使用手指的位置,例如食指,但這似乎不太直觀。我發(fā)現(xiàn)我認(rèn)為效果最好的方法是使用手的中心位置。
我們可以在手的中心位置創(chuàng)建另一個(gè)圓圈,這樣我們只需要檢查一個(gè)圓圈是否與另一個(gè)圓圈相交。
首先我們需要一種方法在手的中心位置創(chuàng)建一個(gè)圓圈。變量hands(detector.findHands()函數(shù)的輸出)是一個(gè)列表,其中每個(gè)項(xiàng)目都是一個(gè)字典,其中包含有關(guān)被跟蹤的手的信息。這個(gè)字典有 4 個(gè)鍵:
-
lmList:21 個(gè)地標(biāo)的位置列表(以像素為單位) -
bbox : 邊界框的坐標(biāo)和大小(以像素為單位) -
center : 中心位置的坐標(biāo),以像素為單位 -
type:左手或右手
從這 4 個(gè)鍵中,我們感興趣的是中心,所以為了得到中心位置,我們這么做:
while True:
ret, frame = cap.read()
hands = detector.findHands(frame, flipType=False, draw=False)
if hands:
for i in range(len(hands)):
hand_position = hands[i]["center"]
我們首先檢查是否檢測(cè)到任何手,如果是,我們可以訪問中心位置,但只需指定鍵“center”。
現(xiàn)在我們有了創(chuàng)建一個(gè)圓圈的方法,我們可以在每只手的中心創(chuàng)建一個(gè)圓圈并通過添加這兩條線來繪制它
hand_circle = Circle(hand_position, hand_radius, (0, 0, 255), 1)
hand_circle.draw(frame)
這會(huì)在每個(gè)被跟蹤手的中心位置繪制一個(gè)紅色圓圈(未填充)。注意,手的中心位置不是手掌中心,而是所有地標(biāo)位置的平均位置。你可以通過合上手來測(cè)試一下,你可以看到中心位置向你手的下部移動(dòng)。
檢查碰撞
我們需要一種方法來檢查玩家是否擊中了目標(biāo)。這將是我們游戲的主要機(jī)制。
Cv2 沒有任何檢查兩個(gè)對(duì)象是否相交的函數(shù),但由于我們處理的是圓,所以這個(gè)任務(wù)變得非常簡(jiǎn)單。我們只需要檢查圓心之間的距離是否小于或等于半徑之和。讓我們看一下下面的例子
相交算法
希望這說明了相交算法。由于我們已經(jīng)知道目標(biāo)半徑和手圓半徑,我們只需要計(jì)算距離d。并且有一個(gè)非常簡(jiǎn)單的數(shù)學(xué)公式,給定兩個(gè)點(diǎn),計(jì)算它們之間的距離。
該距離公式是:
我們所要做的就是在代碼中創(chuàng)建該函數(shù)。為了方便起見,我在Circle類中創(chuàng)建了這個(gè)函數(shù),如下所示
def check_intersection(self, other_coordinates, other_radius):
distance = math.sqrt(math.pow(other_coordinates[0] - self.coordinates[0], 2) + math.pow(
other_coordinates[1] - self.coordinates[1], 2))
if distance <= self.radius + other_radius:
return True
else:
return False
該函數(shù)考慮了我們上面討論的所有內(nèi)容并返回一個(gè)布爾值,指示一個(gè)圓是否與另一個(gè)圓相交。
我們可以通過在主循環(huán)中添加幾行來測(cè)試它
if hands:
for i in range(len(hands)):
hand_position = hands[i]["center"]
hand_circle = Circle(hand_position,hand_radius,(0,0,255),1)
if target.check_intersection(hand_circle.coordinates, hand_circle.radius):
# is intersecting
hand_circle.color = (0, 255, 0)
else:
# not intersection
hand_circle.color = (0, 0, 255)
hand_circle.draw(frame)
你現(xiàn)在可以看到,如果我“觸摸”目標(biāo),我的手圈會(huì)變成綠色,否則就是紅色
第 3 步:創(chuàng)建實(shí)際游戲
因此,創(chuàng)建我們實(shí)際游戲的第一步是,一旦我們擊中當(dāng)前目標(biāo),就能夠在隨機(jī)位置選擇一個(gè)新目標(biāo)。為此,我們可以創(chuàng)建一個(gè)這樣的函數(shù)
def create_random_target(current_target_pos=[]):
if current_target_pos:
possible_x = []
x_limit = [target_radius + border_size + 15, width - target_radius - border_size - 15]
y_limit = [target_radius + border_size + 15, height - target_radius - border_size - 15]
for i in range(x_limit[0], x_limit[1]):
if i + 200 < current_target_pos[0] or i - 200 > current_target_pos[0]:
possible_x.append(i)
possible_y = []
for i in range(y_limit[0], y_limit[1]):
if i + 200 < current_target_pos[1] or i - 200 > current_target_pos[1]:
possible_y.append(i)
if not possible_x:
possible_x = range(x_limit[0], x_limit[1])
if not possible_y:
possible_y = range(y_limit[0], y_limit[1])
else:
possible_x = range(target_radius + border_size, width - target_radius - border_size)
possible_y = range(target_radius + border_size, height - target_radius - border_size)
# pick a random coordinate
random_x = random.choice(possible_x)
random_y = random.choice(possible_y)
# pick a random color
random_color = [random.randint(0, 255), random.randint(0, 255), random.randint(0, 256)]
_target = Circle([random_x, random_y], target_radius, random_color, -1)
return _target
這個(gè)函數(shù)有很多事情要做,所以讓我們分解一下。
第一部分是我們?cè)O(shè)置新目標(biāo)可以采用的寬度和高度的可能值。我們通過首先設(shè)置變量x_limit和y_limit來做到這一點(diǎn),顧名思義,這些變量限制了可以放置目標(biāo)的位置。這是為了避免我們最終得到一個(gè)部分在屏幕外的目標(biāo)。
你可能已經(jīng)注意到有一個(gè)新變量,border_size我們還沒有討論過,但我稍后會(huì)討論它。
然后,對(duì)于每個(gè)維度(寬度和高度),我們運(yùn)行一個(gè) for 循環(huán),用可能的位置填充數(shù)組possible_x和possible_y。請(qǐng)注意,我加入了一個(gè)限制,為了讓游戲更具挑戰(zhàn)性,新目標(biāo)必須與當(dāng)前目標(biāo)相距至少 282 像素(寬度為 200 像素,高度為 200 像素))
之后,只需從possible_x和possible_y中選擇一個(gè)隨機(jī)值,分配一個(gè)隨機(jī)顏色,然后返回新的圓圈。
現(xiàn)在我們可以在主循環(huán)中使用這個(gè)函數(shù)
if hands:
for i in range(len(hands)):
hand_position = hands[i]["center"]
hand_circle = Circle(hand_position,hand_radius,(0,0,255),1)
hand_circle.draw(frame)
if target.check_intersection(hand_circle.coordinates, hand_circle.radius):
# is intersecting
hit_target = True
break;
if hit_target:
target_count += 1
target = create_random_target(target.coordinates)
hit_target = False
如你所見,此代碼可能非常耗時(shí),因?yàn)槊看挝覀円獎(jiǎng)?chuàng)建新目標(biāo)時(shí),我們都會(huì)遍歷屏幕的幾乎每個(gè)像素。如果這個(gè)函數(shù)會(huì)減慢你的游戲速度,只需注釋掉你有 for 循環(huán)的行并使用以下代碼來代替
possible_x = range(target_radius + border_size, width -
target_radius - border_size)
possible_y = range(target_radius + border_size, height - target_radius - border_size)
這將產(chǎn)生相同的影響,除了我們不再有創(chuàng)建遠(yuǎn)離當(dāng)前目標(biāo)的新目標(biāo)的限制。
有了這段代碼,游戲就完成了!你現(xiàn)在可以不停地玩游戲。但當(dāng)然,我們會(huì)改進(jìn)它。
可以改進(jìn)這款游戲的眾多方法之一是賦予它一種感覺或緊迫感。基本上,我們需要一種方法來激勵(lì)玩家盡快達(dá)到目標(biāo)。一個(gè)很好的方法是添加一個(gè)計(jì)時(shí)器。
為了使用計(jì)時(shí)器,我們首先必須有一種方法來跟蹤經(jīng)過的時(shí)間。我們可以通過time庫(kù)做到這一點(diǎn)
import time
t_start = time.time()
while True:
elapsed_time = time.time() - t_start
print(elapsed_time)
這應(yīng)該輸出我們自循環(huán)開始以來經(jīng)過的時(shí)間。有了這個(gè),我們現(xiàn)在可以限制玩家玩游戲的時(shí)間,迫使玩家盡可能快地獲得更好的分?jǐn)?shù)。我們還需要一種方法讓玩家知道他打得有多好,所以我們將在圖像中添加一條消息,使用函數(shù)cv2.putText()顯示得分
if elapsed_time >= max_time:
is_playing = False
final_message = "Time's up! You hit " + str(target_count) + " targets in " + str(max_time) + " seconds"
frame = cv2.putText(frame, final_message, object_title_pos, cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 255), 2)
我還添加了標(biāo)志is_playing以在計(jì)時(shí)器結(jié)束后阻止目標(biāo)出現(xiàn)。現(xiàn)在我們可以玩游戲并嘗試每次提高我們的分?jǐn)?shù),我們甚至可以與其他人競(jìng)爭(zhēng)!
我們還可以用兩種不同的方式玩我們的游戲。事實(shí)上,現(xiàn)在我們已經(jīng)設(shè)置了運(yùn)行時(shí)間的游戲,但我們也可以讓游戲運(yùn)行目標(biāo)。我的意思是,與其嘗試在給定時(shí)間內(nèi)擊中盡可能多的目標(biāo),我們可以看到我們可以多快擊中給定數(shù)量的目標(biāo)。
我們只需要添加一些東西,即控制我們正在玩的游戲類型的變量,我們還需要一種方法來檢查目標(biāo)計(jì)數(shù)何時(shí)達(dá)到最大值
play_for_time = True
play_for_targets = not play_for_time
...
while True:
if hit_target:
target_count += 1
target = create_random_target(target.coordinates)
if play_for_time and target_count == max_targets:
is_playing = False
final_message = "Congrats! You hit " + str(target_count) + " targets in " + "{:.2f}".format(elapsed_time) + " seconds"
現(xiàn)在我們可以用兩種不同的方式玩我們的游戲了!
第 4 步:最后潤(rùn)色
我們現(xiàn)在擁有的游戲非常基礎(chǔ),有無數(shù)種方法可以改進(jìn)它,但是在本節(jié)中,我將與你分享一些我也實(shí)現(xiàn)的其他功能。
保存/加載高分
不必記住上次獲得的分?jǐn)?shù),你可以創(chuàng)建一個(gè)讀取(和寫入)當(dāng)前高分的方法。
為此,我們需要兩個(gè)函數(shù)來加載和保存我們的高分
import pickle
def load_highscore(is_time):
try:
if is_time:
with open('high_score_time.dat', 'rb') as file:
score = pickle.load(file)
else:
with open('high_score_targets.dat', 'rb') as file:
score = pickle.load(file)
except:
score = 0
return score
def save_highscore(score, is_time):
if is_time:
with open('high_score_time.dat', 'wb') as file:
pickle.dump(score, file)
else:
with open('high_score_targets.dat', 'wb') as file:
pickle.dump(score, file)
我們正在使用pickle將當(dāng)前的高分保存并加載到文件中。要將其合并到我們當(dāng)前的代碼中,我們只需要檢查我們得到的當(dāng)前分?jǐn)?shù)是否比我們的高分更好,如果是,則更新高分
highscore_targets = load_highscore(False)
while True:
if hit_target:
target_count += 1
target = create_random_target(target.coordinates)
if play_for_time and target_count == max_targets:
is_playing = False
final_message = "Congrats! You hit " + str(target_count) + " targets in " + "{:.2f}".format(elapsed_time) + " seconds"
if target_count > highscore_targets or highscore_targets==0:
save_highscore(target_count, False)
highscore_message = "New highscore!!"
else:
highscore_message = "Best score: " + str(highscore_targets)
添加當(dāng)前時(shí)間和分?jǐn)?shù)
現(xiàn)在我們無法知道我們擊中了多少個(gè)目標(biāo),或者我們玩了多長(zhǎng)時(shí)間,所以為了解決這個(gè)問題,我們可以在頂部角落添加兩個(gè)文本框來顯示當(dāng)前目標(biāo)計(jì)數(shù)和當(dāng)前時(shí)間
score_text_pos = (width - 150, border_size + 30)
time_text_pos = (border_size + 15, border_size + 30)
while True:
...
frame = cv2.putText(frame, "Total: " + str(target_count), score_text_pos, cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 255), 2)
frame = cv2.putText(frame, "Time left: " + "{:.2f}".format(elapsed_time), time_text_pos, cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 255), 2)
添加邊框
最后,我之前提到過border_size這個(gè)變量,這個(gè)變量表示我們希望邊框的像素?cái)?shù)。我們可以使用函數(shù)cv2.copyMakeBorder()創(chuàng)建邊框
while True:
frame = cv2.copyMakeBorder(frame, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=[0, 0, 0])
GitHub 存儲(chǔ)庫(kù)上提供的文件還有更多功能:https://github.com/Goncalo-Chambel/ReactionGame
下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程
在「小白學(xué)視覺」公眾號(hào)后臺(tái)回復(fù):擴(kuò)展模塊中文教程,即可下載全網(wǎng)第一份OpenCV擴(kuò)展模塊教程中文版,涵蓋擴(kuò)展模塊安裝、SFM算法、立體視覺、目標(biāo)跟蹤、生物視覺、超分辨率處理等二十多章內(nèi)容。
下載2:Python視覺實(shí)戰(zhàn)項(xiàng)目52講
在「小白學(xué)視覺」公眾號(hào)后臺(tái)回復(fù):Python視覺實(shí)戰(zhàn)項(xiàng)目,即可下載包括圖像分割、口罩檢測(cè)、車道線檢測(cè)、車輛計(jì)數(shù)、添加眼線、車牌識(shí)別、字符識(shí)別、情緒檢測(cè)、文本內(nèi)容提取、面部識(shí)別等31個(gè)視覺實(shí)戰(zhàn)項(xiàng)目,助力快速學(xué)校計(jì)算機(jī)視覺。
下載3:OpenCV實(shí)戰(zhàn)項(xiàng)目20講
在「小白學(xué)視覺」公眾號(hào)后臺(tái)回復(fù):OpenCV實(shí)戰(zhàn)項(xiàng)目20講,即可下載含有20個(gè)基于OpenCV實(shí)現(xiàn)20個(gè)實(shí)戰(zhàn)項(xiàng)目,實(shí)現(xiàn)OpenCV學(xué)習(xí)進(jìn)階。
交流群
歡迎加入公眾號(hào)讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動(dòng)駕駛、計(jì)算攝影、檢測(cè)、分割、識(shí)別、醫(yī)學(xué)影像、GAN、算法競(jìng)賽等微信群(以后會(huì)逐漸細(xì)分),請(qǐng)掃描下面微信號(hào)加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請(qǐng)按照格式備注,否則不予通過。添加成功后會(huì)根據(jù)研究方向邀請(qǐng)進(jìn)入相關(guān)微信群。請(qǐng)勿在群內(nèi)發(fā)送廣告,否則會(huì)請(qǐng)出群,謝謝理解~
