通过yolov5实现自动Bangdream

本文最后更新于:2023年10月4日 晚上

通过yolov5实现自动Bangdream

起因是某天晚上睡觉前在打邦邦,想再磕10点火,结果不小心点到最大并按了确认,最后盯着90火哭笑不得。

自带的10次auto打完后就在想如果能继续自动打就好了,遂有了做这个的想法。

叠个甲:因为本人对Python并不熟,所以写的代码可能会很难看。

1、构思

首先肯定想到的便是图像识别+模拟点击,图像识别这块因为之前也了解过yolo的相关知识,所以直接选择了yolov5

然后就是怎么抓取游戏然后模拟点击了,一开始想的是平板/手机usb到电脑上,然后使用scrapy进行画面捕捉,但是实验了一下后发现这样做还是存在一定的延迟的,而且音游对延迟还是挺敏感的,遂放弃了这个想法。

如果需要做到没有延迟的话便是在电脑上开模拟器并进行画面捕捉,同时我希望模拟器在后台时也能捕捉到画面,毕竟如果需要常驻前台的话,自动打游戏也就没啥意义了。同时为了尽可能降低图像识别需要的时间,将模拟器分辨率设置为了1280x720。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def grab_window_win32():
window = gw.getWindowsWithTitle('MuMu模拟器12')[0]
hwnd = window._hWnd
width = 1280
height = 780

hwnd_dc = win32gui.GetWindowDC(hwnd)
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
save_dc = mfc_dc.CreateCompatibleDC()

save_bit_map = win32ui.CreateBitmap()
save_bit_map.CreateCompatibleBitmap(mfc_dc, width, height)

save_dc.SelectObject(save_bit_map)
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (0, 0), win32con.SRCCOPY)

bmp_info = save_bit_map.GetInfo()
bmp_str = save_bit_map.GetBitmapBits(True)

img = np.fromstring(bmp_str, dtype=np.uint8)
img.shape = (height, width, 4)

cv2_img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)

win32gui.DeleteObject(save_bit_map.GetHandle())
save_dc.DeleteDC()
mfc_dc.DeleteDC()
win32gui.ReleaseDC(hwnd, hwnd_dc)

return cv2_img

搞定画面捕捉后,需要考虑的就是,如何判断音游中note的点击/长按等类型。其实这个一开始想的很简单,把不同note标注好后扔进去训练就完事,但是细想后就会发现:note是判断出来了,但是依然不知道该什么时候去点击这个note。所以其实需要训练的是note接触到判定线的那一帧的画面:

识别到后进行点击/长按…即可。

2、训练

想法有了后便是进行枯燥的训练,因为只是做一个测试,或者说想法的验证,我先只是训练了:点击、普通绿条、粉键三种note,其中绿条又分为:开始长按和结束的拿起尾判,基本上可以覆盖绝大多数easy的铺面了。

首先是收集尽可能多的游戏录屏,我是在手机上录制的90fps游戏画面(因为想要尽可能精准地获取到note到判定线的那一帧画面),然后用ffmpeg分割为图片:

1
ffmpeg -i 1_v.mp4 -r 90 -f image2 1_m_data-%05d.jpeg

随后挑选出有用的图片,再使用labelimg进行标注。注意我们直接使用yolov5格式就行,免得再去转换一次。

标注完成后,我们拥有了4种类型的数据,然后就需要对数据进行分类。但是因为标注的时候是混标的,相当于一个文件夹下有着四种标注的数据,我们需要再自己写个小代码,去把这些数据均分为训练集&验证集(测试集是可选的),比例大概在9:1。

标注训练的过程是什么样的就不再赘述了,我这里主要记录(流水账)一下我的心路历程。

随后就是训练:

1
python train.py --weights yolov5s.pt  --cfg models/yolov5s.yaml --data data/bangdream.yaml --epoch 200 --batch-size 8 --img 640

我用的4070显卡,数据量不多的情况下,跑了200的epoch也没花多久,具体设置建议都使用默认的。

3、运行

模型训练好后可以先找点视频验证:

1
python detect.py --weights runs/train/exp/weights/best.pt --source data/3_m.mp4 --data data/bangdream.yaml

确认模型没问题后,就可以开始着手剩余代码的编写了。

如1中所说,我是采用了截屏的方式,这样其实还是有一些延迟的,所以游戏的流速最好调到最低,经测试基本easy曲都能fc。

因为代码实在有点丑,就只说一些思路:

其实我们只需要知道note在屏幕的具体哪个位置就可以了,我们直接修改detect.py代码,让其通过我们的截图来识别对象位置,并且拿到坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
for i, det in enumerate(pred):  # per image
im0 = img0.copy()
s = ' '
s += '%gx%g ' % im.shape[2:] # print string
gn = torch.tensor(img0.shape)[[1, 0, 1, 0]] # normalization gain whwh
imc = img0 # for save_crop

if len(det):
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()
# Print results
for c in det[:, 5].unique():
n = (det[:, 5] == c).sum() # detections per class
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # add to string

# Write results
for *xyxy, conf, cls in reversed(det):
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh

x_point = int((xyxy[0] + xyxy[2]) / 2)
y_point = int((xyxy[1] + xyxy[3]) / 2) - 72


if 1001 < x_point < 1200:
handle_press_2(x_point, y_point, s, 'u')
if 920 < x_point < 1000:
handle_press_2(x_point, y_point, s, 'y')
if 750 < x_point < 900:
handle_press_2(x_point, y_point, s, 't')
if 670 < x_point < 720:
handle_press_2(x_point, y_point, s, 'r')
if 450 < x_point < 560:
handle_press_2(x_point, y_point, s, 'e')
if 300 < x_point < 450:
handle_press_2(x_point, y_point, s, 'w')
if 180 < x_point < 300:
handle_press_2(x_point, y_point, s, 'q')

如上,我们就可以拿到识别到的note的中心点的x、y坐标,并通过ADB去点击即可。同时因为判定线的Y轴其实是固定的,所以我们还可以通过y轴坐标去微调一下点击的延迟,从而做到更为精确的点击(这步比较多余)。

然后就是绿条的处理,我们可以通过x坐标得知目前在哪条落线中,比如:1、2、3、4…因为我一开始是用mumu模拟器的键盘输入完成的点击而不是ADB,所以偷懒用了之前的设置。

在判定到绿条的时候,通过x坐标判定具体的位置,长按屏幕并写入map中,然后在绿条结束时判定map并结束长按,就可以完成对绿条的处理了。

4、结束

这个是一个测试视频:https://www.bilibili.com/video/BV1cV411P7Sp

可以看到easy下还是有比较精确的识别和点击的,识别耗时基本在8ms左右。

整个代码的完成度比较低,比如没有处理会变化位置的绿条、ADB长按屏幕等处理还不够优雅等…不过也基本到了一个可玩的程度。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!