前言
闪耀优俊少女突然堂堂复活,也可以把搁置的AI养马项目重新捡起来了。之前虽然做出了手动养马的脚本,但是接入AI的效果并不理想。通过OCR识别的准确度始终有问题,通过手动编码来处理和识别各种状态也让逻辑变的十分臃肿,不利于AI接入。
这次希望利用之前的经验,从头开始设计一个大部分由AI驱动的养马脚本。
架构设计
图像输入(第三方) -> 状态识别(模型) -> 信息处理(模型) -> 决策(模型) -> 点击结果(第三方)
之前是脚本是通过特定区域的图像查找来判断当前处于哪个界面的,这就需要游戏跑到对应的界面再停下来截图去处理,非常麻烦。这次希望可以通过后台截图无感知玩游戏的同时,通过大量的数据进行状态的归类和判断。
图像输入
截图部分参考了LmeSzinc大佬制作的碧蓝航线脚本实现,使用了DroidCast插件。
数据采集
想要训练模型,第一步肯定是收集数据。先写了一个脚本每秒截图一次并保存画面。
import loggingfrom base import adb, screen, utilfrom datetime import datetimefrom pathlib import Pathimport cv2import timelog = 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 imageadb.init("127.0.0.1:16416")screen.width = 720screen.height = 1280screen.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%的准确度,于是优化了下数据采集的代码,在截图后通过模型判断并自动分类:
import loggingimport torchfrom torchvision import transformsfrom PIL import Imageimport timefrom datetime import datetimefrom pathlib import Pathimport cv2from base import adb, screen, utilfrom train_state_classifier import StateClassifierfrom config import NORMALIZE_MEAN, NORMALIZE_STDlogger = logging.getLogger('predict_state')logger.setLevel(logging.INFO)device = torch.device('cuda')model = StateClassifier().to(device)model.load_state_dict(torch.load('data/state_classifier.pth', map_location=device))model.eval()transform = transforms.Compose([ transforms.Resize((320, 180)), transforms.ToTensor(), transforms.Normalize(mean=NORMALIZE_MEAN, std=NORMALIZE_STD)])def predict_image(image): image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) image = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(image) probabilities = torch.softmax(outputs, dim=1)[0] state = torch.argmax(outputs, dim=1).item() return state, probabilities[state].item()adb.init("127.0.0.1:16416")screen.width = 720screen.height = 1280screen.init()logger.info("开始预测游戏状态")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) except KeyboardInterrupt: logger.info("停止预测") break except Exception as e: logger.error(f"预测出错: {str(e)}") time.sleep(1)
又玩了几局游戏,分类基本很准确,只有准备比赛界面因为数据量太少容易判断失误,准备了更多数据后重新训练就彻底没问题了。
下一步
如果要让AI接管大部分游戏画面的操作,那么状态必须划分的很细(每个界面都是一个单独的状态去执行不同的逻辑)。这样所需的数据量如果要我一个个去分类有点太痛苦了,于是准备试试无监督学习能否帮我自动进行分类。