使用 Python 和 OpenCV 制作反应游戏
共 19902字,需浏览 40分钟
·
2024-04-19 10:20
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
在本文中,将向你展示如何使用 OpenCV 在 Python 中制作一个反应游戏,你可以动手来玩。
你可能已经熟悉 OpenCV,OpenCV 基本上允许进行各种图像处理。
你可以在下面的视频中看到最终结果,并且可以在此处获取文件:https://github.com/Goncalo-Chambel/ReactionGame
尽管这可能看起来很复杂(取决于你的专业知识),但在我们看来,这是一个相当简单但很有趣的项目。你基本上可以用 200 行代码创建一个游戏(这代码量很少了!)。
我们将把任务分成几个部分:设置+手部检测、主要游戏机制、创建实际游戏和最后润色。
第 1 步:设置 + 手部检测
这个项目的主要目标是创建一个反应游戏,其中圆圈会随机出现在屏幕上,你必须用你的手尽可能快地“触摸”它们。
因此,第一个步骤是让程序访问你的网络摄像头。
为此,我们将使用 OpenCV 库,为此我们只需添加一行import cv2
。就这么简单,但如果你还没有安装,你必须先安装它。
在此处添加了此项目的要求:https://github.com/Goncalo-Chambel/ReactionGame/blob/main/requirements.txt
因此你可以通过在命令行中键入pip install -r requirements.txt
来安装所有这些要求。
cv2 库有很多功能,但让我们一步一步来。第一个目标是告诉 Python 从网络摄像头读取数据并将其显示在屏幕上。这可以通过使用函数cv2.VideoCapture()
来完成
import cv2
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
现在cap
变量包含对你的网络摄像头的引用。然后在我们的主文件中,我们可以创建一个无限循环,每次迭代都会显示网络摄像头捕获的当前图像。
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
有了这几行代码,我们应该有一个程序,这个程序可以简单地打开一个新窗口(名为“反应游戏”),大小为 1280 x 720 像素,带有网络摄像头的视频源。我还添加了最后几行代码,以便你可以关闭窗口并按“q”退出程序。
现在我们可以继续检测手了。创建一个算法来检测视频中的手是一项复杂的任务,但幸运的是我们不必重新发明轮子。有一个非常好的库可以为我们做这件事,叫做 CV Zone。
CV Zone 允许各种与对象检测相关的项目,但我们主要对 HandDetector 模型感兴趣。该模型使我们能够获得有关视频中被跟踪手的重要信息,例如它们的中心位置或边界框。我们初始化模型的方式如下:
from cvzone.HandTrackingModule import HandDetector
detector = HandDetector(detectionCon=0.8, maxHands=2)
其中detectionCon
是置信区间(从0到1),表明你希望模型跟踪手的精度。更高的值意味着模型更确信被跟踪的手就是手,但也可能使模型“错过手”,因为它没有信心相信它们是手。maxHands
参数仅限制模型一次可以跟踪的手的数量。
然后在我们的主循环中,我们只调用函数detector.findHands()
来获取有关被跟踪手的信息。
while True:
ret, frame = cap.read()
hands, frame = detector.findHands(frame, flipType=False)
cv2.imshow("Reaction Game", frame)
输出:
如你所见,该算法非常擅长跟踪手部(包括手指位置)。请注意,根据你的规格,该程序可能看起来有点“不稳定”。此外,根据你使用的相机,你可能不需要翻转图像,只需检查标签“左手”是否真的对应于你的左手。
最后,在屏幕上显示所有这些信息可能会太混乱,因此我们可以告诉检测器不要通过书写来绘制这些信息
hands = detector.findHands(frame, flipType=False, draw=False)
请注意,当我们包含参数draw=False
时,detector
仅返回手信息(与返回帧相反)。
第 2 步:主要游戏机制
我们现在可以研究我们游戏的基本机制。基本想法是在屏幕上随机生成圆圈并检测手是否在触摸它。
这里有两个主要部分,画圈和检查碰撞。
画圈
这是一个相对简单的步骤,因为cv2有一个内置函数cv2.circle()
,可以在屏幕上绘制一个圆形,该函数将我们正在绘制的图像、屏幕上的位置(以像素为单位)、圆的半径(以像素为单位)、颜色(以BGR) 和线条粗细作为输入。所以假设我们想在屏幕中间放置一个红色圆圈,厚度为2,半径为 50 像素,我们会这样做:
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)
请注意,此函数不返回任何内容,它会自动更新frame
变量。此外,如果你想要一个实心圆而不仅仅是轮廓,你可以将粗细设置为 -1。
在继续在屏幕上创建随机圆圈之前,让我们先创建自己的Circle类。如果我们想在只处理一个变量的同时访问圆的多个属性,这将很方便。这是一个非常简单的类,现在让我们添加一个构造函数和一个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)
现在如果我们想像之前那样画一个圆圈,我们会这样做:
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)
现在可能看起来不是很有用,但以后会有所帮助。
在继续讨论交集方法本身之前,我们首先需要一些东西来检查交集。我们已经有了一种在屏幕上创建目标的方法,但我们需要将其与某些东西进行比较。很明显,有些东西会是我们的手,但具体是手的哪一部分?
我们可以使用边界框,但我觉得这可能太容易了。我们还可以使用手指的位置,例如食指,但这似乎不太直观。我发现我认为效果最好的方法是使用手的中心位置。
我们可以在手的中心位置创建另一个圆圈,这样我们只需要检查一个圆圈是否与另一个圆圈相交。
首先我们需要一种方法在手的中心位置创建一个圆圈。变量hands
(detector.findHands()
函数的输出)是一个列表,其中每个项目都是一个字典,其中包含有关被跟踪的手的信息。这个字典有 4 个键:
-
lmList:21 个地标的位置列表(以像素为单位) -
bbox : 边界框的坐标和大小(以像素为单位) -
center : 中心位置的坐标,以像素为单位 -
type:左手或右手
从这 4 个键中,我们感兴趣的是中心,所以为了得到中心位置,我们这么做:
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"]
我们首先检查是否检测到任何手,如果是,我们可以访问中心位置,但只需指定键“center”。
现在我们有了创建一个圆圈的方法,我们可以在每只手的中心创建一个圆圈并通过添加这两条线来绘制它
hand_circle = Circle(hand_position, hand_radius, (0, 0, 255), 1)
hand_circle.draw(frame)
这会在每个被跟踪手的中心位置绘制一个红色圆圈(未填充)。注意,手的中心位置不是手掌中心,而是所有地标位置的平均位置。你可以通过合上手来测试一下,你可以看到中心位置向你手的下部移动。
检查碰撞
我们需要一种方法来检查玩家是否击中了目标。这将是我们游戏的主要机制。
Cv2 没有任何检查两个对象是否相交的函数,但由于我们处理的是圆,所以这个任务变得非常简单。我们只需要检查圆心之间的距离是否小于或等于半径之和。让我们看一下下面的例子
相交算法
希望这说明了相交算法。由于我们已经知道目标半径和手圆半径,我们只需要计算距离d。并且有一个非常简单的数学公式,给定两个点,计算它们之间的距离。
该距离公式是:
我们所要做的就是在代码中创建该函数。为了方便起见,我在Circle类中创建了这个函数,如下所示
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
该函数考虑了我们上面讨论的所有内容并返回一个布尔值,指示一个圆是否与另一个圆相交。
我们可以通过在主循环中添加几行来测试它
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)
你现在可以看到,如果我“触摸”目标,我的手圈会变成绿色,否则就是红色
第 3 步:创建实际游戏
因此,创建我们实际游戏的第一步是,一旦我们击中当前目标,就能够在随机位置选择一个新目标。为此,我们可以创建一个这样的函数
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
这个函数有很多事情要做,所以让我们分解一下。
第一部分是我们设置新目标可以采用的宽度和高度的可能值。我们通过首先设置变量x_limit
和y_limit
来做到这一点,顾名思义,这些变量限制了可以放置目标的位置。这是为了避免我们最终得到一个部分在屏幕外的目标。
你可能已经注意到有一个新变量,border_size
我们还没有讨论过,但我稍后会讨论它。
然后,对于每个维度(宽度和高度),我们运行一个 for 循环,用可能的位置填充数组possible_x
和possible_y
。请注意,我加入了一个限制,为了让游戏更具挑战性,新目标必须与当前目标相距至少 282 像素(宽度为 200 像素,高度为 200 像素))
之后,只需从possible_x
和possible_y
中选择一个随机值,分配一个随机颜色,然后返回新的圆圈。
现在我们可以在主循环中使用这个函数
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
如你所见,此代码可能非常耗时,因为每次我们要创建新目标时,我们都会遍历屏幕的几乎每个像素。如果这个函数会减慢你的游戏速度,只需注释掉你有 for 循环的行并使用以下代码来代替
possible_x = range(target_radius + border_size, width -
target_radius - border_size)
possible_y = range(target_radius + border_size, height - target_radius - border_size)
这将产生相同的影响,除了我们不再有创建远离当前目标的新目标的限制。
有了这段代码,游戏就完成了!你现在可以不停地玩游戏。但当然,我们会改进它。
可以改进这款游戏的众多方法之一是赋予它一种感觉或紧迫感。基本上,我们需要一种方法来激励玩家尽快达到目标。一个很好的方法是添加一个计时器。
为了使用计时器,我们首先必须有一种方法来跟踪经过的时间。我们可以通过time
库做到这一点
import time
t_start = time.time()
while True:
elapsed_time = time.time() - t_start
print(elapsed_time)
这应该输出我们自循环开始以来经过的时间。有了这个,我们现在可以限制玩家玩游戏的时间,迫使玩家尽可能快地获得更好的分数。我们还需要一种方法让玩家知道他打得有多好,所以我们将在图像中添加一条消息,使用函数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)
我还添加了标志is_playing
以在计时器结束后阻止目标出现。现在我们可以玩游戏并尝试每次提高我们的分数,我们甚至可以与其他人竞争!
我们还可以用两种不同的方式玩我们的游戏。事实上,现在我们已经设置了运行时间的游戏,但我们也可以让游戏运行目标。我的意思是,与其尝试在给定时间内击中尽可能多的目标,我们可以看到我们可以多快击中给定数量的目标。
我们只需要添加一些东西,即控制我们正在玩的游戏类型的变量,我们还需要一种方法来检查目标计数何时达到最大值
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"
现在我们可以用两种不同的方式玩我们的游戏了!
第 4 步:最后润色
我们现在拥有的游戏非常基础,有无数种方法可以改进它,但是在本节中,我将与你分享一些我也实现的其他功能。
保存/加载高分
不必记住上次获得的分数,你可以创建一个读取(和写入)当前高分的方法。
为此,我们需要两个函数来加载和保存我们的高分
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将当前的高分保存并加载到文件中。要将其合并到我们当前的代码中,我们只需要检查我们得到的当前分数是否比我们的高分更好,如果是,则更新高分
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)
添加当前时间和分数
现在我们无法知道我们击中了多少个目标,或者我们玩了多长时间,所以为了解决这个问题,我们可以在顶部角落添加两个文本框来显示当前目标计数和当前时间
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
这个变量,这个变量表示我们希望边框的像素数。我们可以使用函数cv2.copyMakeBorder()
创建边框
while True:
frame = cv2.copyMakeBorder(frame, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=[0, 0, 0])
GitHub 存储库上提供的文件还有更多功能:https://github.com/Goncalo-Chambel/ReactionGame
下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。
下载2:Python视觉实战项目52讲 在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。
下载3:OpenCV实战项目20讲 在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~