查看原文
其他

利用人工智能给游戏“开挂”,但求一败

工地专业搬砖工 开源中国 2020-09-02

PaddleX是百度飞桨推出的深度学习全流程开发工具,本文以微信小游戏跳一跳为例,体验了PaddleX从数据准备到模型部署的全流程。


本次用到的模型是PaddleX提供的目标检测模型YOLOv3。通过此模型检测跳一跳游戏界面中的小人和跳台,然后估算出小人成功落入跳台所需要的时间,把模型部署到手机模拟器上,即可模拟玩家按屏完成跳一跳,最终实现了自动成功跳跃。


本项目所涉及的代码和文件均放在百度一站式在线开发平台AI Studio上,链接如下:
https://aistudio.baidu.com/aistudio/projectdetail/526100


第一步:理解跳一跳




在跳一跳游戏中,玩家通过按下手机屏幕来控制小人跳起,若准确落入下一个跳台则积分,否则游戏失败。主要有这几个界面:


游戏主界面:


失败后再玩一局界面一:


失败后再玩一局界面二:


玩家需要根据小人与下一个跳台之间的距离,估算出按下屏幕的时长,按下时间越长,小人弹跳地越远。个人直觉估计小人弹跳的距离与按下时长是一种抛物线的关系。所以首先要解决的是如何得到小人与下一个跳台之间的距离,这就要用到PaddleX中的明星产品YOLOv3了。


第二步:制作数据




在使用PaddleX之前,需要制作符合格式的数据集,可以使用LableImg标注工具来制作目标检测数据集。


为了让模型收敛的更好,标注的图片尽可能多一些,本项目中的图片总数不低于150张。根据游戏流程,按照需要标注三个游戏界面的按钮(例如小人、跳台、积分数)。


第三步:模型训练




具体技术实现过程:

安装PaddleX

!pip install paddlex -i https://mirror.baidu.com/pypi/simple

设置使用0号GPU卡(如无GPU,执行以下代码后会使用CPU训练模型)

import matplotlib
matplotlib.use('Agg'
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
import paddlex as pdx

#把制作好的数据集解压到相应子目录
!rm data/game -r
# !cat game.zip.* > game.zip
!unzip game.zip
!mv game data/
# !rm game.zip

划分训练集、验证集、测试集:

import os
import zipfile
import xml.etree.ElementTree as ET
import re
import numpy as np

lables = os.listdir("data/game/lable")
print("lables:",len(lables))
trains = os.listdir("data/game/train1")
print("trains:",len(trains))
_lable = []
ratio = 0.8
offset = int(len(lables)*ratio)
np.random.shuffle(lables)
path = "data/game/"
with open(path + "train_list.txt","w") as f:
    for lable in lables[:offset]:
        if lable.split(".")[0] + ".jpg" in trains:
            f.writelines("train1/" + lable.split(".")[0] + ".jpg" + " " + "lable/" + lable +"\n")
            tree = ET.parse(path + "lable/" + lable)
            root = tree.getroot()
            objs = root.findall("object")
            for obj in objs:
                if obj.find("name").text not in _lable:
                    _lable.append(obj.find("name").text)
            tree.find("path").text = path + "lable/" + lable
            tree.write(path + "lable/" + lable)

with open(path + "test_list.txt","w") as f:
    for lable in lables[offset:]:
        if lable.split(".")[0] + ".jpg" in trains:
            f.writelines("train1/" + lable.split(".")[0] + ".jpg" + " " + "lable/" + lable +"\n")
            tree = ET.parse(path + "lable/" + lable)
            root = tree.getroot()
            objs = root.findall("object")
            for obj in objs:
                tree.find("path").text = path + "lable/" + lable
                tree.write(path + "lable/" + lable)
                if obj.find("name").text not in _lable:
                    _lable.append(obj.find("name").text)

with open(path + "val_list.txt","w") as f:
    for lable in lables[offset:]:
        if lable.split(".")[0] + ".jpg" in trains:
            f.writelines("train1/" + lable.split(".")[0] + ".jpg" + " " + "lable/" + lable +"\n")

print(_lable)
with open(path + "lable.txt","w") as f:
    for lable in _lable:
        f.writelines(lable+"\n")

定义训练和验证时的transforms:

from paddlex.det import transforms
train_transforms = transforms.Compose([
    transforms.MixupImage(mixup_epoch=250),
    transforms.RandomDistort(),
    transforms.RandomExpand(),
    transforms.RandomCrop(),
    transforms.Resize(target_size=608, interp='RANDOM'),
    transforms.RandomHorizontalFlip(),
    transforms.Normalize(),
])

eval_transforms = transforms.Compose([
    transforms.Resize(target_size=608, interp='CUBIC'),
    transforms.Normalize(),
])

定义训练和验证所用到的数据集

train_dataset = pdx.datasets.VOCDetection(
    data_dir='data/game',
    file_list='data/game/train_list.txt',
    label_list='data/game/lable.txt',
    transforms=train_transforms,
    shuffle=True)
eval_dataset = pdx.datasets.VOCDetection(
    data_dir='data/game',
    file_list='data/game/val_list.txt',
    label_list='data/game/lable.txt',
    transforms=eval_transforms)

初始化模型,并进行训练。由于backbone选择的是DarkNet53,且batch_size设置值较大,CPU下跑不动,所以一定要用GPU

num_classes = len(train_dataset.labels)
model = pdx.det.YOLOv3(num_classes=num_classes, backbone='DarkNet53')
model.train(
    num_epochs=300,
    train_dataset=train_dataset,
    train_batch_size=12,
    eval_dataset=eval_dataset,
    learning_rate=0.000125,
    lr_decay_epochs=[210240],
    save_interval_epochs=20,
    save_dir='output/yolov3_darknet53',
    use_vdl=True)

# 训练完了,就可以用来检测小人与下一个跳台分别在什么地方了。

# 设置使用0号GPU卡(如无GPU,执行此代码后会使用CPU训练模型)

import matplotlib
matplotlib.use('Agg'
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
import paddlex as pdx

# 加载模型文件夹路径,这里修改为训练时模型保存的路径:

base_path = "output/best_model"
model = pdx.load_model(base_path)


第四步:获取屏幕画面




获取屏幕画面,可以是摄像头,也可以是安桌模拟器。这里讲一下如何获取手机屏幕:首先,安装安卓模拟器,或者adb,推荐大家安装360手机助手,这样就可以在电脑上获取手机屏幕了。


并在电脑上操控手机:


获取屏幕画面:

import win32gui
import win32api
from PIL import ImageGrab
import cv2
import time
import numpy as np
classname = None
titlename = "360手机演示"
#获取句柄
hwnd = win32gui.FindWindow(classname, titlename)
print("获取窗口:",hwnd)
#获取窗口左上角和右下角坐标
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
print(left, top, right, bottom)

def getScreen():
    n = time.time()
    left, top, right, bottom = win32gui.GetWindowRect(hwnd)
    img = ImageGrab.grab(bbox=(left, top, right, bottom))
    img = np.array(img.getdata(), np.uint8).reshape(img.size[1], img.size[0], 3)
    #img = img[...,::-1]
    cv2.imwrite("response.jpg", img)
getScreen()


第五步:目标检测




想办法把获取的屏幕画面传递给模型,在画面中检测出小人和跳台的位置。

filename = "response.jpg"
result = model.predict("temp/" +filename)
pdx.det.visualize("temp/" +filename, result, threshold=0.5, save_dir='./output/yolov3_mobilenetv1')
#result的格式是
# [{'category_id': 1, 
# 'bbox': [164.57411193847656, 248.15550231933594, 34.76348876953125, 16.626205444335938], 
# 'score': 0.972798228263855, 
# 'category': 'next'}]

获取屏幕时,360演示窗口要在桌面上保持为可见状态,ImageGrab.grab获取的就是桌面截屏。

在保存截取的屏幕后,用训练好的模型对截屏进行目标检测。此处需注意,训练集是由摄像头获取的,摄像头获取时设置的是横屏,而模拟器截屏设置的是竖屏,所以需要把模拟器截屏转换成横屏


惊喜点:发现模型意外的可靠,本来还以为横屏和竖屏两种模式下的数据格式差别大,会导致识别能力下降,结果完全没有影响,转换后直接预测就可以了,这说明模型泛化能力很强。


第六步:估算按屏时间




#获取小人脚底坐标与下一个跳台中心的坐标,计算距离d
#计算距离d和时间t的之间函数关系,
#假定小人的路径是抛物线,与发射角和时间都相关
#当按下时间为0时,发射方向为垂直向上的,此时发射角为0度
#按下时间越长,发射角越接近0度,所以构造以下函数
#发射角aa = Pi/2 - Pi/2*(a/(c*t + 1.0) + (1-a))
#发射起始速度V与按下时间成正比,V = z*t
#Vx = V*cos(aa),Vy= V*sin(aa)
#空中飞行时间为T = 2*Vy/g
#跳跃距离为Vx*T = V*cos(aa)* 2*V*sin(aa)/g= z^2 * t^2 *sin(aa)*cos(aa) /g =b *t^2 * sin(2*aa)
#
#这部分其实应该用深度学习来找这个函数,这个以后再弄
import math
def d2t(a,b,c,d):
    #返回按下的时间长度,感觉是单调函数,所以二分法试一下
    t0,t1 = 0.0,500.0
    for _ in range(100):
        t = t1/2.0 + t0/2.0
        aa = (3.1415926*(a/(c*t+1.0)+1-a) - 3.1415926/2)
        d0 = b * t**2 *math.sin(aa)
        if (d - d0)**2 < 0.000001:
            return t
        if d > d0:
            t0 = t1/2.0 + t0/2.0
        else:
            t1 = t1/2.0 + t0/2.0
    return t
#手工调试这组参数
a,b,c = 0.254203.0
#最后就是在屏幕上对游戏进行操作:
#需要用到autopy库
!pip install autopy
import autopy
def toggle(x,y,t):
#在x,y位置按下t时长,模拟在手机上的按下操作
autopy.mouse.move(x ,y)
autopy.mouse.toggle(None,True)
time.sleep(t)
autopy.mouse.toggle(None,False)


第七步:总结



因为跳一跳游戏本身实在太慢了,优化整个游戏流程比较耗时,目前最高积分仅100多分。后续还会持续优化,感兴趣的同学可以持续关注本项目。

参考链接

本项目代码和文件均放在百度一站式在线开发平台AI Studio上,链接如下:
https://aistudio.baidu.com/aistudio/projectdetail/526100

如果您想详细了解更多PaddleX的相关内容,请参阅以下文档。
PaddleX项目地址:
GitHub:
https://github.com/PaddlePaddle/PaddleX
GitHub:
https://gitee.com/paddlepaddle/PaddleX

官网地址:
https://www.paddlepaddle.org.cn

飞桨开源框架项目地址:
的GitHub:
https://github.com/PaddlePaddle/Paddle
Gitee: 
https://gitee.com/paddlepaddle/Paddle

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存