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、人脸识别模块。
特点
-
手势跟踪游戏控制:利用 cvzone 的 HandDetector 模块跟踪手部动作,控制游戏中的贪吃蛇。
-
实时面部检测和马赛克效果:检测视频中的面部并实时应用马赛克效果,确保游戏过程中的隐私。
-
游戏逻辑实现:包括吃食物、蛇身增长、记分和游戏结束等功能。
实现
库和模块
- 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,因为他们之间一般不会缠绕,强行计算可能出错。
- 接下来就是连接,Head和其他的点,并且判断是否会形成一个多边形,也就是形成闭环。
- 与此同时也会计算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 贪吃蛇