之前提到了如果要做到自动采集游戏中的资源,首先第一步就是要从复杂的游戏场景中识别出需要采集的资源。虽然游戏场景是三维立体的,但是显示在屏幕上的画面仍然是二维的图像,所以基本上所有的图像识别和处理算法都适用于这个项目。前面也分析过,由于游戏场景的元素多样并且受日夜更替及天气变化的影响,若简单使用传统的图像处理是无法准确识别需要采集的资源的,然而目前大火的深度学习却非常适合这个任务。
简单来说,我们事先采样游戏中的各种物体作为一个训练的样本,然后用这个训练集来训练基于深度神经网络的目标识别模型,然后将训练好的模型应用到我们的游戏中,如果训练集质量够高,理论上是可以准确识别任何训练过的物体的。
而目前开源的目标检测算法就有好几种,比较好用的就是SSD和YOLO,而YOLO当属时下最流行的目标检测开源算法,所以我也尝试了一下YOLO的效果,确实简单易用而且速度和准确率都令人满意。
YOLO (You Only Look Once) 是近几年大火的目标识别算法,以其速度和准确率兼备而闻名。目前YOLO已经升级至了第三代YOLOv3了,而其每一代又有为移动设备定制的轻量(tiny)版本,所以总共加起来有 7-8 个版本了。官网上有所有版本以及他们的速度和准确率的对比。总体来说,tiny版本会有数倍的速度提升,但是准确率略有下降,v2版本比v3更轻量但是v3的准确率更高。
由于我电脑的配置有限(没有合适的GPU),所以运行v3标准版会很吃力,所以综合各方面,YOLOv3-tiny版本是值得尝试的,从官网给的数据来看,YOLOv3-tiny在GPU的加持下最高可以达到 220FPS,可以说是非常的快了。
现在已经有好几个框架适配了YOLO目标检测,包括最流行的Tensorflow, Pytorch还有老一点的Caffe框架都可以实现YOLO的部署。但是在尝试过Tensorflow版的YOLO以后,由于只有CPU,可以说速度是非常的慢以至于无法应用到我们的任务中。所以我转用了OpenCV来部署,发现操作简单而且只用CPU的速度也不会太慢,所以这篇博客主要就以OpenCV下如何部署YOLO,包括如何使用YOLO的专用模型Darknet来训练和测试自己创建的数据集。
基本上所有的机器学习方法的首要前提是需要提供数据集来训练我们的神经网络,要么是已经采集好而且开源的数据集(如手写数字MINST集、ImagNet数据集、COCO数据集等等…),要么就需要自己采集数据创建一个自己的训练集。
比较好用的图片标记工具OpenLabeling可以帮助我们快速制作自己的数据集。
首先下载OpenLabeling压缩包:https://github.com/Cartucho/OpenLabeling/archive/master.zip
点进main/
文件夹
复制我们需要标记的图片到input/
文件夹
在class_list.txt
里面填入我们所有的目标类别 class (这里我总共有11个class)
然后运行主程序:
python main.py
等待一分钟以后会弹出一个窗口,里面会显示有我们input/
文件夹里所有的训练图片。
然后我们就可以开始标记图片了,鼠标左键单击框选我们的目标,右键删除标记。我们可以使用w
和s
键切换11个标签,a
和d
键切换下一个或上一个图片。
标记好的 label 会自动储存在 output/
文件夹,这个程序提供两种格式对应两个文件夹,而我们需要的是YOLO的格式:
[category number] [object center in X] [object center in Y] [object width in X] [object width in Y]
每一张标记号的训练图片都会对应相同名字的.txt
文件作为目标的label单独保存,所以我们还需要一个train.txt
和test.txt
来汇总这些单独的文件以便读取。我们可以直接用python来帮助我们生成这两个汇总文件,代码如下(参照了这里):
import glob, os
# Current directory
current_dir = ## set the absolute path of your training image directory
# Directory where the data will reside, relative to 'darknet.exe'
path_data = 'bot/'
# Percentage of images to be used for the test set
percentage_test = 10;
# Create and/or truncate train.txt and test.txt
file_train = open('train.txt', 'w')
file_test = open('test.txt', 'w')
# Populate train.txt and test.txt
counter = 1
index_test = round(100 / percentage_test)
for pathAndFilename in glob.iglob(os.path.join(current_dir, "*.jpg")):
title, ext = os.path.splitext(os.path.basename(pathAndFilename))
if counter == index_test:
counter = 1
file_test.write(path_data + 'images/' + title + '.jpg' + "\n")
else:
file_train.write(path_data + 'images/' + title + '.jpg' + "\n")
counter = counter + 1
(记得将current_dir
改为训练集的绝对路径)
运行完这段代码以后,在当前文件夹会生成train.txt
和test.txt
两个文件。
准确标记好我们的数据集以后,就可以开始训练我们的神经网络了,YOLO的作者专门为这个模型搞了一个Darknet,非常易于训练。
首先要安装Darknet,拷贝他们的github:
git clone https://github.com/pjreddie/darknet
然后进入 darknet/
文件夹进行编译:
cd darknet
make
(如果想要使用GPU来训练,我们可以修改darknet/
文件夹下的Makefile
的第一行为GPU = 1
,然后再进行上面一步)
下一步就是下载在ImageNet预训练过的网络参数weight,选择YOLOv3-tiny版本的:
wget https://pjreddie.com/media/files/yolov3-tiny.weights
若要使用YOLO来预测图片中的物体,我们需要一个网络参数文件.weight
和一个网络配置文件.cfg
。YOLOv3-tiny的.cfg
文件已经包含在了cfg/
文件夹了,所以我们可以直接开始测试,放上一张data/
文件夹里的测试图进行测试:
./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg
结果显示成功识别了图片中的物体(狗、自行车、皮卡),但是预测的confidence有一点低,如果是YOLOv3标准版会更高,不过我们可以通过添加更多的训练图像来提高我们的confidence,所以这个问题不用过多担心。
下面就可以开始我们的训练了,要在Darknet框架下训练YOLO模型,我们总共需要以下文件:
yolov3-tiny.conv.15
yolov3-tiny-bot.cfg
bot.data
bot.names
train.txt
test.txt
我们统一将这些文件放到命名为bot/
的文件夹里。
yolov3-tiny.conv.15
已经预训练过的网络可以使用迁移学习(Transfer Learning)来减少训练时间。简单来说我们只使用前面15层的卷积层来提取图像特征,这15层是不用训练的,我们着重训练后面几层全连接层来让我们自己的数据和对应的标签对接。我们可以用下面的代码来提取原先网络的前15层的参数weight,然后生成一个yolov3-tiny.conv.15
文件:
./darknet partial cfg/yolov3-tiny.cfg yolov3-tiny.weights yolov3-tiny.conv.15 15
yolov3-tiny-bot.cfg
我们可以复制原来的yolov3-tiny.cfg
文件将其命名为yolov3-tiny-bot.cfg
,在这个文件里的以下几行进行改动:
第3行:batch = 64
第4行:subdivision = 8
第127行和第171行:filters = 48
第135行和第177行:classes = 11
这些设置代表着我们用每批batch
64张图片来训练一次,然后这64张图片会被进一步subdivision
分成8份以减少GPU的VRAM需求(这个subdivision
主要是由GPU的性能决定的,如果你的GPU很强那么可以适当减少到4或2)。然后我们对应我们的数据设定classes
的数量为11,filters
的值就为(classes
+ 5)*3 = 48。
bot.data
文件里填入以下内容:
classes = 11
train = bot/train.txt
valid = bot/test.txt
names = bot/bot.names
backup = backup/
train.txt
和test.txt
就是前面我们生成的两个汇总文件,而backup
就是我们训练过的weight自动保存的路径。
bot.names
文件里填入每个class的名称,与之前的class_list.txt
一样共11类:
tree
stone
herb
wolf
zombie
dear
bear
helicopter
berry
mushroom
cole
所有这些文件都统一放到bot/
文件夹以后,就可以开始我们的训练了,输入以下代码:
./darknet detector train bot/bot.data cfg/yolov3-tiny-bot.cfg yolov3-tiny.conv.15
如果没有出错的话,训练就自动开始了。一般来讲训练的时间会取决于你的硬件配置以及你数据集的大小,正常的话你需要大概每个class至少有300个数据才可以有一个比较准确的结果。结果会显示在每个batch结束时,一般着重看averge loss也就是avg
参数,avg
值越小准确率就越高。Darknet不会自动停止训练,当你观察到avg
值比较小时(比如0.3),你就可以手动停止训练了。当然也可以设置到第几个iteration时就终止训练。训练时网络的参数weight会自动保存至backup
设置的路径,默认是每100个iteration保存一次weight直到第1000个iteration为止就到10000才保存一次。
现在可供使用开源框架五花八门(如tensorflow、pytorch、caffe等等…),而比较方便并且效率很高的OpenCV是更好的部署工具,因为最新的OpenCV版本已经顺应时代加入了深度神经网络dnn模块了,这个dnn模块也正好支持Darknet下的YOLO模型,所以这里我们主要讲如何用OpenCV来部署我们训练好的目标检测模型。
首先还是导入必要的工具包:
import numpy as np
import time
import cv2
import os
然后设置confidence和non-maxima suppression的阈值,这里我们用较低的阈值来允许更多的识别:
min_confidence = 0.3
nm_threshold = 0.3
在OpenCV中部署YOLO我们需要以下三个文件,这些文件都是我们刚才用Darknet训练好我们的网络得到的:
bot.names
yolov3-tiny-bot_19000.weights
(19000表示第19000个iteration)yolov3-tiny-bot.cfg
我们可以把这些文件统一放到命名为yolo-config/
的文件夹里,然后我们就可以导入这些文件的路径:
labelsPath = os.path.sep.join(['yolo-config', "bot.names"])
weightsPath = os.path.sep.join(['yolo-config', "yolov3-tiny-bot_19000.weights"])
configPath = os.path.sep.join(['yolo-config', "yolov3-tiny-bot.cfg"])
然后就是将这些路径导入到OpenCV的dnn模块中:
LABELS = open(labelsPath).read().strip().split("\n")
COLORS = np.random.randint(0, 255, size=(len(LABELS), 3), dtype="uint8")
net = cv2.dnn.readNetFromDarknet(configPath, weightsPath)
接下来就是如何进行预测了,我们可以定义一个函数yolo_detect(image)
来进行一次预测,这里我借鉴了这篇博客:
def yolo_detect(image):
(H, W) = image.shape[:2]
# determine only the *output* layer names that we need from YOLO
ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
# construct a blob from the input image and then perform a forward
# pass of the YOLO object detector, giving us our bounding boxes and
# associated probabilities
blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
start = time.time()
layerOutputs = net.forward(ln)
end = time.time()
# show timing information on YOLO
print("[INFO] YOLO took {:.6f} seconds".format(end - start))
# initialize our lists of detected bounding boxes, confidences, and
# class IDs, respectively
boxes = []
positions = []
confidences = []
classIDs = []
# loop over each of the layer outputs
for output in layerOutputs:
# loop over each of the detections
for detection in output:
# extract the class ID and confidence (i.e., probability) of
# the current object detection
scores = detection[5:]
classID = np.argmax(scores)
confidence = scores[classID]
# filter out weak predictions by ensuring the detected
# probability is greater than the minimum probability
if confidence > min_confidence:
# scale the bounding box coordinates back relative to the
# size of the image, keeping in mind that YOLO actually
# returns the center (x, y)-coordinates of the bounding
# box followed by the boxes' width and height
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")
# use the center (x, y)-coordinates to derive the top and
# and left corner of the bounding box
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))
# update our list of bounding box coordinates, confidences,
# and class IDs
boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)
# apply non-maxima suppression to suppress weak, overlapping bounding
# boxes
idxs = cv2.dnn.NMSBoxes(boxes, confidences, min_confidence, nm_threshold)
# ensure at least one detection exists
flag = 0
if len(idxs) > 0:
# loop over the indexes we are keeping
for i in idxs.flatten():
# extract the bounding box coordinates
(x, y) = (boxes[i][0], boxes[i][1])
(w, h) = (boxes[i][2], boxes[i][3])
flag = 1
# draw a bounding box rectangle and label on the image
color = [int(c) for c in COLORS[classIDs[i]]]
cv2.rectangle(image, (x, y), (x + w, y + h), color, 2)
position = [(x+w/2) - (W/2), (H/2) - (y+h/2)]
positions.append(position)
text = "{}: {:.4f} {}".format(LABELS[classIDs[i]], confidences[i], position)
cv2.putText(image, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
return image, boxes, positions, confidences, classIDs
这个函数需要一个被转换为numpy数组的图片image
(在我这里就是游戏截图)作为输入,然后经过我们自己训练过的YOLO模型以后,返回带有bbox
的图片以及识别出的物体的框boxes
、位置positions
、置信度confidence
还有标签classID
。
最后我们就可以测试我们训练的模型了:
from PIL import Image
im = Image.open('1559271181.jpg')
im_array = np.array(im)
im_array = cv2.cvtColor(im_array, cv2.COLOR_BGR2RGB)
image_detection, boxes, positions, confidences, classIDs = yolo_detect(im_array)
cv2.imshow('detection',image_detection)
cv2.waitKey(0)
cv2.destroyAllWindows()
将im
替换为测试图,然后结果就可以通过OpenCV显示出来了,下面是一些游戏截图的测试结果。
最后就是结合之前的屏幕监控来完成对游戏中资源的实时识别,只需要在前面代码的基础上加上我们新创建的yolo_detect(image)
函数即可:
# coding=gbk
import numpy as np
import cv2
from PIL import ImageGrab
from win32gui import FindWindow, GetWindowRect
from yolo import yolo_detect
while True:
window_name = "明日之后 - MuMu模拟器"
id = FindWindow(None, window_name)
bbox = GetWindowRect(id)
image_array = np.array(ImageGrab.grab(bbox=bbox))
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
image_array, boxes, positions, confidences, classIDs = yolo_detect(image_array)
cv2.imshow('screenshot',image_array)
if cv2.waitKey(25) & 0xFF == ord('q'):
cv2.destroyAllWindows()
break
最终结果可以查看这个视频:
https://v.youku.com/v_show/id_XNDIyMDYzMzQ3Ng==.html
由于电脑配置原因,视频里的测试只能做到2~3FPS,但是对于我们的项目来说已经足够用了。