目录
简介:
欢迎来到我们的博客!在这里,我们将深入探索如何将PP-TinyPose,一个高效且强大的实时姿态估计模型,应用于创新的体感飙车避障游戏的开发中。这个游戏不仅利用了最新的人体姿态估计技术,使玩家可以通过移动自身来控制游戏角色,而且还设计了一系列刺激的避障挑战,为玩家带来前所未有的游戏体验。
在这篇博客中,我们将详细介绍这款基于PP-TinyPose的体感飙车避障游戏的设计与开发过程,包括如何利用PP-TinyPose进行实时姿态估计,如何将姿态信息转化为游戏操作,以及我们如何设计刺激有趣的避障挑战来提高游戏的吸引力。无论你是游戏开发者,还是对新型互动游戏感兴趣的玩家,或者你只是对人工智能技术的应用感到好奇,我们相信你都会在这篇博客中找到有趣的内容。
游戏介绍和效果展示
左侧是游戏画面,右侧是人物和检测结果。程序检测身上的关键点位置(手腕),并计算偏移角度作为小车移动的方向和速度,并躲避障碍物。
游戏开始的方式采用了非常帅气的双手交叉变身姿势 = w <,撞车会显示这次的游戏时间作为得分,双手再次交叉即可重新开始游戏~
环境准备
运行本项目需要准备:PC(带有CPU即可),USB摄像头(笔记本自带的摄像头也可以)。
部署流程
PP-TinyPose 和 FastDeploy
基于PyQt5的Paddle飙车小游戏
以下代码需要在本地运行,不能再Aistudio上运行的~
整体来说比较简单,这个部分比起与深度学习有关的项目介绍更像是游戏构造讲解,有兴趣的可以康康~
PyQt5
基于PyQt5的一般创建和运行流程如下,简单来说,就是在init里写页面布局,定义行为关系(例如点击按钮会调用哪个函数等等)。
import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap
class Example(QWidget):
def __init__(self):
super().__init__()
# 初始化相关变量、定义界面布局、将相关按钮关联到事件(函数)上
def event1(self):
# 点击第一个按钮所执行的事件(函数)
def event1(self):
# 点击第二个按钮所执行的事件(函数)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
初始化
定义支撑游戏运行的基础信息
def __init__(self):
super().__init__()
# 准备一张底图,后续的所有内容都是在这张图上绘制的。
# 游戏界面卡死在500*500的大小
# 虽然游戏界面卡死在500的大小,但是根据演示,我们需要在图片上贴上车辆的贴图,方便起见,扩大一下图片,这样保证所有的车辆图片都能贴在图片上,之后再进行裁剪
self.map = np.ones([700, 500, 3]).astype('uint8') * 0
# 绘制五车道,让画面更为真实
# 让左右两侧都是绿化带
self.map[:, 0:100, :] = (0, 255, 0)
self.map[:, 400:-1, :] = (0, 255, 0)
# 等间距画五条白线作为车道
for i in [100, 160, 220, 280, 340, 400]:
self.map[:, i-5:i+5, :] = (255, 255, 255)
# 检测的关键点,这里本质上不用赋值成数组,但是因为这个程序我是从贪吃蛇那边复制了部分代码的,所以延续了数组的形式
self.up_key_points = [9,9]# [0,0]
self.down_key_points = [10,10]#[5,6]
# 为了方便起见,我建立了一个叫做car的类,负责管理车辆的位置更新
self.car = car() # 从CarObject 导入 Car
# 一些游戏信息,比如根据关键点可以计算方向盘的角度
self.direction = 0 # 判断角度
self.game_status = 0 # 游戏状态 0 等待开始 1 进行
# 初始化一些内容
self.initModel() # 初始化模型
self.initCamera() # 初始化摄像头
self.initClock() # 初始化时钟
self.initUI() # 初始化界面
模型初始化
通过fastdeploy调用检测模型只需要读取模型所在路径即可,将读取到的模型保存在model变量中,后续通过predict运行
def initModel(self):
self.model = fastdeploy.vision.keypointdetection.PPTinyPose('PP_TinyPose_128x96_infer/model.pdmodel',
'PP_TinyPose_128x96_infer/model.pdiparams',
'PP_TinyPose_128x96_infer/infer_cfg.yml')
初始化界面、时钟、视频
- 界面:本游戏界面非常简单,仅由两个框体占据,所以通过QLabel创建两个框框即可
- 视频:插入摄像头后必须把摄像头唤醒才能在后续推理的时候读取图像信息
- 时钟:视频的本质就是图像,但是我们不可能彻底通过视频的方式处理事情。因此,需要通过QTimer()建立时钟,每隔一段时间唤醒一个自定义函数,这个函数就是我们进行推理,也就游戏控制的主函数了。
def initUI(self):
grid = QGridLayout()
self.setLayout(grid)
self.Game_Box = QLabel() # 定义显示视频的Label
self.Game_Box.setFixedSize(500, 500)
grid.addWidget(self.Game_Box, 0, 0, 20, 20)
self.Game_Box.setMouseTracking(True)
self.Pred_Box = QLabel() # 定义显示视频的Label
self.Pred_Box.setFixedSize(500, 500)
grid.addWidget(self.Pred_Box, 0, 20, 20, 20)
self.setWindowTitle('Paddle Cars')
self.show()
def initCamera(self):
# 开启视频通道
self.camera_id = 0 # 为0时表示视频流来自摄像头
self.camera = cv2.VideoCapture() # 视频流
self.camera.open(self.camera_id)
def initClock(self):
# 通过定时器读取数据
self.flush_clock = QTimer() # 定义定时器,用于控制显示视频的帧率
self.flush_clock.start(30) # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
self.flush_clock.timeout.connect(self.updata_frame) # 若定时器结束,show_frame()
游戏推理和控制
上面已经通过时钟每隔一段时间调用一次self.updata_frame,下面来康康这个游戏主函数入口吧
def updata_frame(self):
# 读取图片,进行模型推理,更新self.direction
self.inferModel()
# 如果当前处于游戏状态,调用car更新车辆的位置信息
if self.game_status == 1:
self.car.update(self.direction)
# 更新画面
current_map = copy.deepcopy(self.map) # 复制底图
current_map = self.car.draw(current_map)
current_map = current_map[100:600,:,:] # 裁剪底图
# 更新画面文字,当没有进入游戏状态,画面需要显示提示文字
self.addText(current_map)
# 展示图片
showPic = QImage(current_map, current_map.shape[1], current_map.shape[0], QImage.Format_BGR888)
self.Game_Box.setPixmap(QPixmap.fromImage(showPic))
# 更新框体展示状态,如果当前在游戏状态,需要不停查看car的is_collision看看是否发生撞车
if self.game_status == 1:
if self.car.is_collision == 1:
self.game_status = 2 # 游戏结束的评分页面
模型推理
def inferModel(self):
# read pic from camera
_, img = self.camera.read() # 从视频流中读取
img = cv2.flip(img, 1) # 摄像头画面反转
img2 = cv2.resize(img, (500, 500)) # 把读到的帧的大小重新设置为 640x480
showPic = QImage(img2, img2.shape[1], img2.shape[0], QImage.Format_BGR888)
self.Pred_Box.setPixmap(QPixmap.fromImage(showPic))
try: # 推理会有失败的情况的,所以通过try可以规避这些问题导致程序崩溃
result = self.model.predict(img) # 推理
# 读取两个识别的关键点坐标
top_y = (result.keypoints[self.up_key_points[0]][1] + result.keypoints[self.up_key_points[1]][1]) / 2
top_x = (result.keypoints[self.up_key_points[0]][0] + result.keypoints[self.up_key_points[1]][0]) / 2
bottom_y = (result.keypoints[self.down_key_points[0]][1] + result.keypoints[self.down_key_points[1]][1]) / 2
bottom_x = (result.keypoints[self.down_key_points[0]][0] + result.keypoints[self.down_key_points[1]][0]) / 2
# 如果当前不在游戏状态,需要判断游戏是否启动;否则计算偏转角度
if self.game_status == 0 or self.game_status == 2:
if abs(top_x-bottom_x) + abs(top_y-bottom_y) < 50:
# 游戏启动,更新画面状态
self.game_status = 1
self.car.__init__()
elif self.game_status == 1:
self.direction = math.acos((top_y - bottom_y) / ((top_x - bottom_x) ** 2 + (top_y - bottom_y) ** 2) ** (1 / 2)) - math.pi/2
# 在右侧显示摄像头画面
img[int(top_y)-10:int(top_y)+10,int(top_x)-10:int(top_x)+10] = [0, 0, 255]
img[int(bottom_y) - 10:int(bottom_y) + 10, int(bottom_x) - 10:int(bottom_x) + 10] = [0, 0, 255]
showPic = QImage(img, img.shape[1], img.shape[0], QImage.Format_BGR888)
self.Pred_Box.setPixmap(QPixmap.fromImage(showPic))
except:
print('infer error')
其他函数
可以看到除了关键的模型推理,还有car的定义,以及addText这样的辅助函数,具体如下~
- addText
def addText(self,img):
if self.game_status == 0:
img[75:350,50:450] = (134, 185, 222)
cv2.putText(img, 'Paddle Cars', (60, 150), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 5)
cv2.putText(img, 'Hold the virtual steering wheel with both hands', (60, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'in the picture to control the car. The game',(60, 210), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'ends when the car crashes. Try to stick to it',(60, 240), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'for a longer time!',(60, 270), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'Move your wrist so that two red dots cross to', (60, 310), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'start!', (60, 340), cv2.FONT_HERSHEY_SIMPLEX, 0.5,(0, 0, 0), 1)
elif self.game_status == 2:
img[75:350,50:450] = (134, 185, 222)
cv2.putText(img, 'Paddle Cars', (60, 150), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 5)
cv2.putText(img, 'YOUR SCORE', (180, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2)
cv2.putText(img, self.car.show_time, (210, 240), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 255, 255), 2)
cv2.putText(img, 'Move your wrist so that two red dots cross to', (60, 310), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
cv2.putText(img, 'start!', (60, 340), cv2.FONT_HERSHEY_SIMPLEX, 0.5,(0, 0, 0), 1)
- car
import random
import cv2
import time
class car(object):
def __init__(self):
# init game object
self.bias = 100 # 真实绘制地图大于实际展示部分,需要适当便宜绘制位点
self.x = 250
self.car = cv2.resize(cv2.imread('./Source/BlueCar.png'),(50, 100))
self.enemy = cv2.resize(cv2.imread('./Source/RedCar.png'),(50, 100))
self.barrier_list = []
self.is_collision = 0 # 撞车标识 0 正常 1 撞车
# 得分计数器
self.time = time.time()
self.now_time = time.time()
self.show_time = "0:0:0"
# 更新车道线
self.COUNT = -50
self.COUNT2 = 250
def update(self, angle = 0): # angle 检测到的方向盘角度
# 更新x信息
self.x -= int(10*angle)
# 更新障碍物
if len(self.barrier_list) == 0 or random.random()<0.02:
# x,y,speed
self.barrier_list.append([int(random.random() * 200 + 150), -50, int(random.random() * 10 + 5)])
# 更新位置
for i in range(len(self.barrier_list)):
self.barrier_list[i][1] += self.barrier_list[i][2]
# 更新game_over status
# TODO: 之后应该通过更灵活的方式实现
for i in range(len(self.barrier_list)):
if abs(self.barrier_list[i][1] - 450) < (self.car.shape[0] + self.enemy.shape[0]) * 0.9 / 2:
if abs(self.barrier_list[i][0] - self.x) < (self.car.shape[1] + self.enemy.shape[1]) * 0.9 / 2:
self.is_collision = 1
# 删除无效数据
self.barrier_list = [barrier for barrier in self.barrier_list if barrier[1]<549]
if self.x < 100: self.x = 100
if self.x > 400: self.x = 400
# 车道线位置更新
self.COUNT += 25
self.COUNT2 += 25
if self.COUNT >= 550: self.COUNT = -50
if self.COUNT2 >= 550: self.COUNT2 = -50
# 时间更新
self.now_time = time.time()
# 绘制
def draw(self, img): # angle 检测到的方向盘角度
# 绘制车道线,给人灵动的感觉
for i in [160, 220, 280, 340]:
img[self.COUNT-20+self.bias:self.COUNT+20+self.bias, i-5:i+5, :] = (0, 0, 0)
img[self.COUNT2 - 20 + self.bias:self.COUNT2 + 20 + self.bias, i - 5:i + 5, :] = (0, 0, 0)
# 绘制自己
draw_car_in_pos(img,self.car,self.x, 450 + self.bias)
# 绘制障碍车
for barrier in self.barrier_list:
draw_car_in_pos(img, self.enemy, barrier[0], barrier[1]+self.bias)
# draw time
# self.now_time在update更新
tmp_time = self.now_time - self.time
hour = int(tmp_time/3600)
minute = int((tmp_time - hour*3600)/60)
second = int(tmp_time - hour*3600 - minute*60)
self.show_time = "{}:{}:{}".format(hour,minute,second)
cv2.putText(img, self.show_time, (5, 50+self.bias), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (128, 128, 128), 2)
return img
def draw_car_in_pos(img,car_img,x,y,mask_id = 0):
# img 画布
# car_img 待贴合图片
# x,y 绘制中心点
h, w, _ = car_img.shape
x1 = int(x-w/2)
x2 = x1+w
y1 = int(y-h/2)
y2 = y1+h
tmp = img[y1:y2,x1:x2]
tmp[car_img!=[0,0,0]]=car_img[car_img!=[0,0,0]]
img[y1:y2,x1:x2] = tmp