前言
闪耀优俊少女突然堂堂复活,也可以把搁置的AI养马项目重新捡起来了。之前虽然做出了手动养马的脚本,但是接入AI的效果并不理想。通过OCR识别的准确度始终有问题,通过手动编码来处理和识别各种状态也让逻辑变的十分臃肿,不利于AI接入。
这次希望利用之前的经验,从头开始设计一个大部分由AI驱动的养马脚本。
架构设计
图像输入(第三方) -> 状态识别(模型) -> 信息处理(模型) -> 决策(模型) -> 点击结果(第三方)
之前是脚本是通过特定区域的图像查找来判断当前处于哪个界面的,这就需要游戏跑到对应的界面再停下来截图去处理,非常麻烦。这次希望可以通过后台截图无感知玩游戏的同时,通过大量的数据进行状态的归类和判断。
图像输入
截图部分参考了LmeSzinc大佬制作的碧蓝航线脚本实现,使用了DroidCast插件。
数据采集
想要训练模型,第一步肯定是收集数据。先写了一个脚本每秒截图一次并保存画面。
import logging
from base import adb, screen, util
from datetime import datetime
from pathlib import Path
import cv2
import time
log = logging.getLogger('monitor')
def screenshot():
image = screen.screenshot()
image_id = datetime.now().strftime("%Y%m%d_%H%M%S")
image_path = Path("data/ocr/images") / f"{image_id}.png"
cv2.imwrite(str(image_path), util.to_bgr(image))
log.info(f"截图保存到 {image_path}")
return image
adb.init("127.0.0.1:16416")
screen.width = 720
screen.height = 1280
screen.init()
while True:
screenshot()
time.sleep(1)
几局游戏下来,就有了几千张图片数据,于是迫不及待准备试试状态识别。
数据标注
刚开始准备老老实实选用监督模型进行训练,自然少不了标注数据。好在数据量不算大,花了点时间把训练界面、准备比赛界面和其他界面区分开了,就先写一个判断是否是训练界面的模型试试吧。
数据预处理
对于游戏来说,几乎整个屏幕区域都有可能显示有用的信息,具体的信息位置是根据界面决定的,因此在状态识别模型中我很难想到能对界面进行什么处理操作,只是简单的压缩了下分辨率:
transform = transforms.Compose([
transforms.Resize((320, 180)), # 将1280x720缩放到320x180
transforms.ToTensor(),
])
训练模型
根据AI的建议创建了一个卷积神经网络(CNN)模型:
- 输入层:接收3通道RGB图像(320x180像素)
- 卷积层第一个卷积块:3通道→32通道,3x3卷积核
- 卷积层第二个卷积块:32通道→64通道,3x3卷积核
- 卷积层第三个卷积块:64通道→128通道,3x3卷积核
- 全连接层:将特征图展平后(128×40×22)映射到256维
- 输出层:256维映射到3个类别
class StateClassifier(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
self.fc1 = nn.Linear(128 * 40 * 22, 256)
self.fc2 = nn.Linear(256, 3)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = self.pool(self.relu(self.conv3(x)))
x = x.view(-1, 128 * 40 * 22)
x = self.dropout(self.relu(self.fc1(x)))
x = self.fc2(x)
return x
验证模型
没想到的是只用1epoch就已经有了99%的准确度,于是优化了下数据采集的代码,在截图后通过模型判断并自动分类:
while True:
try:
image = util.to_bgr(screen.screenshot())
state, confidence = predict_image(image)
logger.info(f"预测状态: {state}, 置信度: {confidence:.2%}")
image_id = datetime.now().strftime("%Y%m%d_%H%M%S")
image_dir = Path("data/predict") / f"{state}"
image_dir.mkdir(parents=True, exist_ok=True)
if confidence < 0.9:
track_dir = image_dir / "track"
track_dir.mkdir(parents=True, exist_ok=True)
image_path = track_dir / f"{image_id}_{state}_{confidence:.2f}.png"
else:
image_path = image_dir / f"{image_id}_{state}_{confidence:.2f}.png"
cv2.imwrite(str(image_path), image)
time.sleep(1)
又玩了几局游戏,分类基本很准确,只有准备比赛界面因为数据量太少容易判断失误,准备了更多数据后重新训练就彻底没问题了。
下一步
如果要让AI接管大部分游戏画面的操作,那么状态必须划分的很细(每个界面都是一个单独的状态去执行不同的逻辑)。这样所需的数据量如果要我一个个去分类有点太痛苦了,于是准备试试无监督学习能否帮我自动进行分类。