AI玩游戏的一点尝试(1)—— 架构设计与初步状态识别

108 阅读4分钟

前言

闪耀优俊少女突然堂堂复活,也可以把搁置的AI养马项目重新捡起来了。之前虽然做出了手动养马的脚本,但是接入AI的效果并不理想。通过OCR识别的准确度始终有问题,通过手动编码来处理和识别各种状态也让逻辑变的十分臃肿,不利于AI接入。

这次希望利用之前的经验,从头开始设计一个大部分由AI驱动的养马脚本。

架构设计

图像输入(第三方) -> 状态识别(模型) -> 信息处理(模型) -> 决策(模型) -> 点击结果(第三方)

之前是脚本是通过特定区域的图像查找来判断当前处于哪个界面的,这就需要游戏跑到对应的界面再停下来截图去处理,非常麻烦。这次希望可以通过后台截图无感知玩游戏的同时,通过大量的数据进行状态的归类和判断。

图像输入

github.com/LmeSzinc/Az…

截图部分参考了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)

几局游戏下来,就有了几千张图片数据,于是迫不及待准备试试状态识别。

image.png

数据标注

刚开始准备老老实实选用监督模型进行训练,自然少不了标注数据。好在数据量不算大,花了点时间把训练界面、准备比赛界面和其他界面区分开了,就先写一个判断是否是训练界面的模型试试吧。

image.png

数据预处理

对于游戏来说,几乎整个屏幕区域都有可能显示有用的信息,具体的信息位置是根据界面决定的,因此在状态识别模型中我很难想到能对界面进行什么处理操作,只是简单的压缩了下分辨率:

    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接管大部分游戏画面的操作,那么状态必须划分的很细(每个界面都是一个单独的状态去执行不同的逻辑)。这样所需的数据量如果要我一个个去分类有点太痛苦了,于是准备试试无监督学习能否帮我自动进行分类。