基于YoloV5的游戏自瞄框架


本项目仅限于学习交流不可商用,不可用于非法用途(包括但不限于:用于制作游戏外挂等)。

一开始是觉得不应该讲得太详细,但实际上不管是原理还是代码,在网上都是一找一大堆了,真有心也能拿去用了,还是记录一下。

实现一个AI自瞄,基本是三步走,屏幕截图——目标检测——鼠标移动,只有目标检测这一步需要用到AI,接下来各个部分分开讲一下。

首先是屏幕截图,如果确定要用Pytorch作为框架的话,那么Python的截图方法主要是三种:

第一种是用PIL或者Pillow进行图像处理,这两个库是大部分Python图像处理类的项目都会有的库,优点是很方便,但是效率不怎么样。

第二种是用pyautogui,这个库是自动化图形界面库,除了截图以外还能够控制鼠标,但是可能是为了跨平台使用,pyautogui的运行速度也很差,在我的电脑上截一张图需要0.02s左右,额,我跑一次Yolo才0.04秒,这都占一半时间了

第三种是用win32api,这个是微软给的应用程序编程接口,可以进行截图,缺点是代码比较长,不能跨平台,但是应该也没人会用Linux,Mac打游戏吧。

除此之外还有一些其他方法,比方说OBS提供了Python的api用于构建脚本,这个效率也很高,毕竟广泛用于直播推流,但是OBS是写在C平台上的,如果是用TensorFlow会比较方便。

在GitHub上有一个快速截图的库:d3dshot,至少在我测试下来,这个速度应该是最快的,但是缺点是一旦切屏出去了,截图就会出问题,没找出问题在哪。

这样看来最适合的截图库就只剩下了win32api和d3dshot,时间上差不多,但是win32api似乎BUG更少。

键盘事件监听

在开始之前我们需要监听键盘的动作,这一部分通过pynput实现:

mode = False

def on_press(key):
    global mode
    if key == keyboard.Key.f11:
        mode = not mode

def on_release(key):
    """松开按键时执行。"""
    # if key == keyboard.Key.esc:
    #     pass
    pass

if __name__ == '__main__':
    listener = keyboard.Listener(
        on_press=on_press,
        on_release=on_release)
    listener.start()

这样就可以通过f11来进行开启或关闭,当然也可以改成别的按键,因为python貌似并没有哪个库支持鼠标侧键的读取,所以可以在鼠标驱动写一个简单的按键触发。

屏幕截取

def window_prepare():
    hwnd = 0  # 窗口的编号,0号表示当前活跃窗口
    # 根据窗口句柄获取窗口的设备上下文DC(Divice Context)
    hwnd_dc = win32gui.GetWindowDC(hwnd)
    # 根据窗口的DC获取mfcDC
    mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
    # mfcDC创建可兼容的DC
    save_dc = mfc_dc.CreateCompatibleDC()
    # 创建bigmap准备保存图片
    save_bit_map = win32ui.CreateBitmap()
    # 获取监控器信息
    width = 640
    height = 360
    # 为bitmap开辟空间
    save_bit_map.CreateCompatibleBitmap(mfc_dc, width, height)
    return save_bit_map, save_dc, mfc_dc, width, height


def window_capture(save_bit_map, save_dc, mfc_dc, width, height):
    # 高度saveDC,将截图保存到saveBitmap中
    save_dc.SelectObject(save_bit_map)
    # 截取从左上角(0,0)长宽为(w,h)的图片
    # saveDC.BitBlt((0, 0), (w, h), mfcDC, (0, 0), win32con.SRCCOPY)
    save_dc.BitBlt((0, 0), (width, height), mfc_dc, (640, 360), win32con.SRCCOPY)
    signed_ints_array = save_bit_map.GetBitmapBits(True)
    img = np.frombuffer(signed_ints_array, dtype='uint8')
    img.shape = (height, width, 4)
    img0 = img[:, :, :3]
    return img0

程序初始化阶段先运行window_prepare,之后通过键盘控制是否循环触发window_capture,捕获屏幕中央640x360的空间,进行检测,移动鼠标。(我这里的分辨率是1920x1080)

检测的部分跟之前的行人检测的demo是一致的,这里就不提了。总之是把截图丢给YoloV5,跑出结果后把坐标返回。

行人检测demo | 莉莉娅! (diduseemyelk.github.io)

当然我们这里还有一个小问题,YoloV5跑完之后的结果坐标默认是按照置信度进行排序的,但很显然,如果有多个目标的时候,一般采取就近原则,谁离准心近,就瞄哪,所以还有一个小函数计算哪一组坐标离准心最近。

def closest(self, boxes):
	distance_list = []
    for (x1, y1, x2, y2, label, conf) in boxes:
		mid_x = (x1 + x2) / 2
        mid_y = (y1 + y2) / 2
        distance = (mid_x - 960)**2 + (mid_y - 540)**2
        distance_list.append(distance)
	value = min(distance_list)
	index = distance_list.index(value)
	return index

鼠标移动

鼠标的移动也有很多库可以用,还是跟上面一样,pyautogui的效率很低,我很难想象只是移动一下鼠标的操作,在pyautogui上都需要0.02s,GitHub上有一个mouse的第三方库,用这个库实现的话耗时是检测不到的。

具体代码我们连着前面两部分一起讲

def aiming(save_bit_map, save_dc, mfc_dc, width, height):
    # 读取每帧图片
    im = window_capture(save_bit_map, save_dc, mfc_dc, width, height)
    bboxes = detector.detect(im)
    # 如果画面中有bbox,即detector探测到待检测对象
    if len(bboxes) > 0:
        box_index = detector.closest(bboxes)
        check_point_x = int(bboxes[box_index][0] + ((bboxes[box_index][2] - bboxes[box_index][0]) * 0.5)) + 640
        check_point_y = int(bboxes[box_index][1] + ((bboxes[box_index][3] - bboxes[box_index][1]) * 0.5)) + 360
        move_x = int(1.4 * (check_point_x - 960))
        move_y = int(1.4 * (check_point_y - 540))
        mouse.move(move_x, move_y, absolute=False, duration=0)
        pass
    else:
        pass

check_point_x和check_point_y是目标检测的中心点,这个点是离准心最近的点。但是值得注意的是,并不是直接把这个参数扔进去move就可以了,因为这种第一人称射击游戏,他都不需要记录你鼠标的位置,只需要记录你鼠标相对位移,并根据你的DPI和鼠标灵敏度进行转向即可。虽然这个目标离你的准心差了可能200个像素点,但转动和平移是两件事情,如何保证你转动过后目标恰好就在你的准心上呢?这就需要用到球面理论去进行计算了。

但是在游戏里,人物模型是有大小的,对于很远的人物,Yolo识别不出来,对于很近的人物,人物HitBox又足够大,即便第一时间没有瞄到最中央,仍然能够命中,因此很大程度上,我们不需要百分百精确地计算出move_x和move_y,完全可以利用中间的线性区域作线性拟合,找到一个经验常数,能够从check_point_x和check_point_y生成出误差较小的move_x和move_y即可,不同的分辨率可能会有所不同,我这分辨率取1.4差不多是比较稳定的。最后用mouse.move函数进行鼠标操作,这里的duration是指延迟,就是执行这个鼠标操作的耗时,如果取0.1的话其实会看起来比较丝滑。

ok最后附上一段bot测试吧。


文章作者: LYC
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LYC !
评论
  目录