大型生存类游戏自动代玩人工智能[3] -- 目标识别(新)
自从上次断更以后已经过去3年,是之前几篇不断收到的点赞、评论、私信支持着博主(既然明日之后这个游戏现在还活着)索性就继续更新下去吧 [doge][doge][doge]
一、目标检测算法更新
人工智能领域的算法更新迭代速度非常之快,上一篇中使用的目标检测模型yolov3已然过时,目前yolo算法已经更新至第5代,在准确度、速度、稳定性、易用性上已经远远超越之前。本篇就与时俱进,使用最新的yolov5s6模型来重构我们的目标检测任务。虽然说是重构,但实际代码量和操作步骤减少了非常多。
yolov5继承了各代yolo的传统,出厂即提供多种不同大小的模型,这次v5版本更是直接一口气发布了4个模型(s, m, l, x),而截至目前已经更新到10个模型之多(n, s, m, l, x, n6, s6, m6, l6, x6),简直选择困难症犯了。这里不需要纠结,越大的模型肯定越准确同时帧率相对越低,所以直接选一个最低帧率需求下自己硬件能满足的模型就好,训练和部署的步骤都是一样的跟模型选择无关,对博主来说yolov5s6就是最适合的模型。
Model | size (pixels) | mAPval 0.5:0.95 | mAPval 0.5 | Speed CPU b1 (ms) | Speed V100 b1 (ms) | Speed V100 b32 (ms) | params (M) | FLOPs @640 (B) |
---|---|---|---|---|---|---|---|---|
YOLOv5n | 640 | 28.0 | 45.7 | 45 | 6.3 | 0.6 | 1.9 | 4.5 |
YOLOv5s | 640 | 37.4 | 56.8 | 98 | 6.4 | 0.9 | 7.2 | 16.5 |
YOLOv5m | 640 | 45.4 | 64.1 | 224 | 8.2 | 1.7 | 21.2 | 49.0 |
YOLOv5l | 640 | 49.0 | 67.3 | 430 | 10.1 | 2.7 | 46.5 | 109.1 |
YOLOv5x | 640 | 50.7 | 68.9 | 766 | 12.1 | 4.8 | 86.7 | 205.7 |
YOLOv5n6 | 1280 | 36.0 | 54.4 | 153 | 8.1 | 2.1 | 3.2 | 4.6 |
YOLOv5s6 | 1280 | 44.8 | 63.7 | 385 | 8.2 | 3.6 | 12.6 | 16.8 |
YOLOv5m6 | 1280 | 51.3 | 69.3 | 887 | 11.1 | 6.8 | 35.7 | 50.0 |
YOLOv5l6 | 1280 | 53.7 | 71.3 | 1784 | 15.8 | 10.5 | 76.8 | 111.4 |
YOLOv5x6 | 1280 | 55.0 | 72.7 | 3136 | 26.2 | 19.4 | 140.7 | 209.8 |
二、YOLOv5模型训练
1. 数据准备
训练数据主要指图像和标记,YOLO各个版本的算法都采用统一的标记格式,即标准化过后的中心点标记,这里yolov5跟v3需要的训练数据是完全一样的。如果已经有了之前训练v3的数据即可零转换马上训练v5的模型,如果还没有数据那么数据标注的步骤也是和原来一样的,这里不再赘述(详见上一篇中的“创建自定义数据集”章节)。
2. 训练模型
首先下载yolov5的源码然后安装必要的运行库,官方是使用了近几年大火的pytorch取代了原本的darknet,如果有英伟达显卡就可以安装pytorch的cuda版本,没有就安装cpu版本。博主用的30系显卡必须安装cuda11.0以上的pytorch版本才能正常使用gpu加速。
git clone https://github.com/ultralytics/yolov5
cd yolov5
pip install -r requirements.txt
安装好以后先准备一下 .yaml
格式的训练配置文件,主要设置数据集的路径 path
、训练数据路径 train
、验证数据路径 val
、和可选的测试数据路径 test
,以及我们目标总共分类的数目 nc
还有类名 names
,其中 train
, val
, test
都是相对 path
的路径。我们把这个配置文件命名为 bot.yaml
然后放到主目录下的 data/
文件夹,配置文件内容如下
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: datasets/qrsl # dataset root dir
train: images # train images (relative to 'path')
val: images # val images (relative to 'path')
test: # test images (optional)
# Classes
nc: 11 # number of classes
names: ['tree', 'stone', 'herb', 'wolf', 'zombie', 'dear', 'bear', 'helicopter', 'berry', 'mushroom', 'cole'] # class names
接下来我们把标记好的数据集放到上面配置文件指定的位置 datasets/qrsl
,图片位于 datasets/qrsl/images
,标记位于 datasets/qrsl/labels
,最终目录结构如下
一切准备就绪后在命令行中输入一行代码即可开始训练
python train.py --img 1280 --batch 1 --epochs 100 --data bot.yaml --weights yolov5s6.pt
使用 --img
来设定模型输入的图像尺寸,--batch
设定训练的批量大小,--epochs
设定最大的训练次数,--data
设定训练配置文件,--weights
设定预训练模型。
这里输入尺寸可以根据实际应用场景的目标大小来设置,一般越大的输入尺寸能检测到的小目标就越精确但是耗时会成指数增加。越大的图像尺寸训练时消耗的cpu和gpu内存也会更多,如果训练集很大的话非常容易导致内存不足而无法运行,这时候就需要调节批量大小来使数据顺利的载入训练。博主在训练的时候就遇到了cpu内存不足的情况,所以设置 --batch 1
每次只输入一张图片才可以开始训练。当然越大的批数量可以使训练收敛的更快而且模型的健壮性会更好,所以还是根据自己硬件来选择一个最大的批量大小。
显示如下的进度条就说明训练已经开始了,每个epoch分别进行一次训练和一次验证,系统会自动保存一个最近的和一个最佳的checkpoint到 runs/train/exp*/weights
路径,中途停止以后下次训练参数设置比如 --weight runs/trian/exp*/weights/last.pt
就会从上次的checkpoint继续,完成到最后一个epoch以后就自动生成优化过的模型,模型参数会变少(主要裁掉了batchnorm等推理时无用的层)但也无法继续训练。
可以从打印的信息看到比较丰富的训练指标数据,一些比较重要的指标:
- box – 标记框的loss
- obj – 单目标的loss
- cls – 分类的loss
- P – 精确率
- R – 召回率
- mAP@.5 – 置信度50%以上的平均精度
- mAP@.5:.95 – 置信度50%~95%的平均精度
精确率反应的是识别出的框的正确率,召回率反应的是有多少正确的框被识别出来,像我们这个任务可以允许一些目标没被识别出来,但是被识别出来的目标一定要准确,所以也就是精确率对我们来说更重要。mAP反应了所有类别的综合精度,要是不想看其他的数据光看这个准没错。
训练指标达到如下的程度基本就是一个比较合格的模型了
这里因为我的数据量偏少只有700多张图,就没有划分训练和验证集,验证集也是训练过的,所以看起来的数据都很虚高,这存在过拟合的风险,不过对我们的任务影响不大,毕竟训练截取的场景就是我们实际应用的场景。一般比较健壮的模型最好需要每个类1000张,每个类10000个标记,然后划分10%左右的数据进行验证,训练数据包含5%左右的无标记图像用来减少False Positive,但是对于我们这个任务不太有必要。
所有训练都完成后在对应的runs/train/exp*/weights
路径找到 best.pt
文件作为我们最终拿去预测的最佳模型。
三、Pytorch推理预测YOLOv5
1. 引入库
直接用pytorch的hub包进行预测是非常简单的,只需要opencv,pytorch和numpy三个常见的库
import cv2
import torch
import numpy as np
2. 加载模型
直接使用 torch.hub
包来加载yolov5源码和我们自己训练的模型,device='0'
用来指定运行在gpu上,如果没有gpu就不用这个参数。
yolov5 = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt', device='0')
2. 读图预测
用opencv来读取一张没有训练过的图片,然后输入到我们的模型中进行推理,最后输出结果转成numpy格式的标记框数组,代码非常少。
img = cv2.imread('scrsht/1652240462.png')
img = img.copy()[:,:,::-1] # BGR to RGB
results = yolov5(img,size=1280)
bboxes = np.array(results.xyxy[0].cpu())
3. 可视化
使用opencv画出所有识别出的框
def drawBBox(image,bboxes):
for bbox in bboxes:
x0,y0,x1,y1 = int(bbox[0]),int(bbox[1]),int(bbox[2]),int(bbox[3])
cv2.rectangle(image, (x0, y0), (x1, y1), (255,0,0), 2)
return image
img = drawBBox(img.copy(),bboxes)
cv2.imshow("", img)
cv2.waitKey()
四、实时识别
以上只是测试单张图片,在上面代码的基础上结合高速截屏库 mss
和多线程库 threading
可以实现实时识别游戏窗口。使用 win32gui
库来定位窗口在桌面的具体位置坐标。
import mss
import cv2
import os
import threading
import time
import torch
import numpy as np
from win32gui import FindWindow, GetWindowRect
yolov5 = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt', device='0')
yolov5.conf = 0.6
yolov5.iou = 0.4
COLORS = [
(0, 0, 255), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 255, 255),
(255, 0, 255), (192, 192, 192), (128, 128, 128), (128, 0, 0),
(128, 128, 0), (0, 128, 0), (128, 0, 128), (0, 128, 128), (0, 0, 128)]
LABELS = ['tree','stone','herb','wolf','zombie','dear','bear','helicopter','berry','mushroom','cole']
img_src = np.zeros((1280,720,3),np.uint8)
def getScreenshot():
id = FindWindow(None, "明日之后 - MuMu模拟器")
x0,y0,x1,y1 = GetWindowRect(id)
mtop,mbot = 30,50
monitor = {"left": x0, "top": y0, "width": x1-x0, "height": y1-y0}
img_src = np.array(mss.mss().grab(monitor))
img_src = img_src[:,:,:3]
img_src = img_src[mtop:-mbot]
return img_src, [x0,y0,x1,y1,mtop,mbot]
def getMonitor():
global img_src
while True:
# last_time = time.time()
img_src, _ = getScreenshot()
#print("fps: {}".format(1 / (time.time() - last_time+0.000000001)))
def yolov5Detect():
cv2.namedWindow("",cv2.WINDOW_NORMAL)
cv2.resizeWindow("",960,540)
cv2.moveWindow("",1560,0)
global img_src
while True:
img = img_src.copy()
bboxes = getDetection(img)
img = drawBBox(img,bboxes)
cv2.imshow("", img)
if cv2.waitKey(1) & 0xFF == ord("q"):
cv2.destroyAllWindows()
break
def getLargestBox(bboxes,type):
largest = -1
bbox_largest = np.array([])
for bbox in bboxes:
if LABELS[int(bbox[5])] in type:
x0,y0,x1,y1 = int(bbox[0]),int(bbox[1]),int(bbox[2]),int(bbox[3])
area = (x1-x0)*(y1-y0)
if area > largest:
largest = area
bbox_largest = bbox
return bbox_largest
def drawBBox(image, bboxes):
for bbox in bboxes:
conf = bbox[4]
classID = int(bbox[5])
if conf > yolov5.conf and classID==0:
x0,y0,x1,y1 = int(bbox[0]),int(bbox[1]),int(bbox[2]),int(bbox[3])
color = [int(c) for c in COLORS[classID]]
cv2.rectangle(image, (x0, y0), (x1, y1), color, 3)
text = "{}: {:.2f}".format(LABELS[classID], conf)
cv2.putText(image, text, (max(0,x0), max(0,y0-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
return image
def getDetection(img):
bboxes = np.array(yolov5(img[:,:,::-1],size=1280).xyxy[0].cpu())
return bboxes
if __name__ == '__main__':
t1 = threading.Thread(target=getMonitor,args=())
t1.start()
t2 = threading.Thread(target=yolov5Detect,args=())
t2.start()
最终的运行效果在这个视频展示
https://www.bilibili.com/video/BV15B4y197Vo/
因为相比三年前的渣配置,这次鸟枪换大炮,在1280x720的原生分辨率下也能达到40~50fps了,整体准确率和稳定性上升了一个档次。