Играем в GTA V с Python. Часть I: чтение игровых фреймов при помощи OpenCV

Когда только вышла платформа OpenAI’s Universe и появилось множество статей с описанием того, что на ней может быть запущена даже GTA V, было очень интересно следить за всем этим. Однако потом, по непонятным причинам, GTA V была полностью исключена из списка возможных игр.

Хотя данная идея казалась крайне заманчивой, о ней пришлось на время забыть. Но затем мы решили вернуться к этому вопросу еще раз и подумать, а нужна ли вообще для подобных задач платформа Open AI. Конечно, для игр попроще, где можно тренироваться на нескольких тысячах итераций за один раз, это вполне реально, но вот для игр вроде GTA V это не вариант.

На самом деле не вполне очевидно: почему именно GTA V?

Grand Theft Auto (GTA) V по многим причинам является великолепным окружением для целого ряда практических задач моделирования. Это огромный мир с бесконечным количеством вещей, которые вы можете делать. Но давайте просто подумаем о самом простом: беспилотные автомобили. В GTA V мы можем использовать моды для контроля времени дня, погоды, скорости, происходящего при аварии и так далее — просто всего на свете. Это полностью настраиваемая среда.

Некоторые наши пособия тщательно спланированы, некоторые частично, а какие-то — абсолютно нет. Вот эта статья не планировалась совершенно, и мы просто стараемся работать над данной задачей. Конечно, мы понимаем, что не у всех есть GTA V, но мы считаем, что если вы играете в аналогичные игры и выполняете схожие задачи, то данный опыт может быть вам полезен. Поскольку, чтобы все заработало. вам придется самостоятельно настроить многоие вещи, эта серия статей не совсем для новичков.

Наша основная цель — это просто создать своего рода беспилотный автомобиль. Для этого подойдет любая игра с полосами движения и автомобилями. Метод, который мы будем использовать для доступа к игре, должен быть практически универсальным. Причем чем проще игра, тем проще это будет сделать. Такие штуки, как солнечные блики в GTA V, делают задачу компьютерного зрения гораздо сложней, но и намного реалистичней.

Также с таким подходом можно работать и с другими играми. Думаю, мы можем научить AI играть, просто показывая ему, как мы играем, передавая эту информацию в сверточную нейронную сеть, которая потом сама начнет играть.

Вот наши предварительные мысли. Несмотря на то, что у нас нет пока законченного решаная на Python, мы можем:

  1. Получить доступ к кадрам экрана.
  2. Имитировать нажатия клавиш (библиотеки sendkeys, pyautogui и вероятно некоторые другие).

Этого должно хватить для большинства элементарных задач, но как насчет глубокого обучения? На самом деле единственное, что нам может еще понадобиться, это то, что также сможет регистрировать различные события из игрового мира. Но большинство игр воспроизводятся почти полностью визуально, а мы уже можем успешно работать с этим, а также можем отслеживать положение мыши и нажатия клавиш. Именно это позволяет нам приступить к глубокому обучению.

Вряд ли на нашей дороге не будет ям и ухабов, но мы уверены, что задача решаема и из нее может выйти большой, интересный проект. Наша главная проблема в том, что мы должны все обрабатывать в реальном времени и достаточно быстро. Но мы думаем, что справимся, во всяком случае, стоит попробовать.

Так как это достаточно большой проект, нам надо его несколько структурировать и разбить на шаги, иначе мы можем запутаться. Мы думаем, что сначала надо сделать некий жестокий минимум. Итак, вот наши первоочередные цели:

  1. Получить доступ к игровому экрану с более-менее приличной кадровой частотой (FPS). Все, что больше 5-ти, нам пойдет. Это не очень приятно для глаз, но мы всегда можем смотреть на реальный игровой экран.
  2. Посылать клавиатурные команды на игровой экран. Мы уверены, что это можно сделать очень просто, но все равно надо убедиться.
  3. Попробовать посылать команды джойстика, если это возможно (особенно учитывая переключение скоростей и повороты).
  4. Использовать библиотеку OpenCV для обработки игровых фреймов, не слишком при этом затруднив обработку кадровой частоты.
  5. Создать простой беспилотный автомобиль, способный двигаться среди полос движения в простых условиях (чистое небо, день, нет дождя и нет трафика).

Итак, шаг первый. Как же нам в действительности получить доступ к экрану? Мы не сомневаемся, что это возможно, но вот как — пока не знаем. Но для этого есть Google! Там мы нашли несоклько ответов, остановимся вот на этом. Но в нем, кажется ошибка при импорте: ImageGrab у них это часть PIL.

import numpy as np
import ImageGrab
import cv2

while(True):
    printscreen_pil =  ImageGrab.grab()
    printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype=uint8)\
    .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3)) 
    cv2.imshow('window',printscreen_numpy)
    if cv2.waitKey(25) & 0xFF == ord('q'):
        cv2.destroyAllWindows()
        break 

Результат:

ImportError                          Traceback (most recent call last)
<ipython-input-3-00f897cb4216> in <module>()
      1 import numpy as np
----> 2 import ImageGrab
      3 import cv2
      4 
      5 while(True):

Ок, уговорили, и правда ImageGrab — это часть PIL.

import numpy as np
from PIL import ImageGrab
import cv2

while(True):
    printscreen_pil =  ImageGrab.grab()
    printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype=uint8)\
    .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3)) 
    cv2.imshow('window',printscreen_numpy)
    if cv2.waitKey(25) & 0xFF == ord('q'):
        cv2.destroyAllWindows()
        break 

Результат:

NameError                            Traceback (most recent call last)
<ipython-input-4-545ecbe36422> in <module>()
      5 while(True):
      6     printscreen_pil =  ImageGrab.grab()
----> 7     printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype=uint8)    .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3))
      8     cv2.imshow('window',printscreen_numpy)
      9     if cv2.waitKey(25) & 0xFF == ord('q'):

NameError: name 'uint8' is not defined

Опять проблемы! Параметр dtype должен быть строкового вида. Они там вообще свой код запускали?!

import numpy as np
from PIL import ImageGrab
import cv2

def screen_record(): 
    while True:
        printscreen_pil =  ImageGrab.grab()
        printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype='uint8')\
        .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3)) 
        cv2.imshow('window',printscreen_numpy)
        if cv2.waitKey(25) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            break 

Отлично, в некоторой степени работает. Но слишком большой размер и слишком медленно. Давайте поработаем с размером изображения.

Примечание переводчика: модуль ImageGrab работает только в Windows и Mac OS. Для работы в Linux замените строчку from PIL import ImageGrab на import pyscreenshot as ImageGrab. Не забыв, конечно, предварительно установить у себя pyscreenshot (можно при помощи менеджера pip). Правда, надо сказать, что GTA V работает только под Windows (и конечно, на игровых приставках), но под Linux тоже есть много игр, где ездят автомобили.

import numpy as np
from PIL import ImageGrab
import cv2


def screen_record(): 
    while True:
        # 800x600 windowed mode
        printscreen_pil =  ImageGrab.grab(bbox=(0,40,800,640))
        printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype='uint8')\
        .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3))
        cv2.imshow('window',cv2.cvtColor(printscreen_numpy, cv2.COLOR_BGR2RGB))
        if cv2.waitKey(25) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            break
         

Отлично, с размером решили вопрос, но все еще проблемы со скоростью: у нас только 2-3 фрейма в секунду. Давайте посмотрим, почему.

import numpy as np
from PIL import ImageGrab
import cv2
import time

def screen_record(): 
    last_time = time.time()
    while True:
        # 800x600 windowed mode
        printscreen_pil =  ImageGrab.grab(bbox=(0,40,800,640))
        printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype='uint8')\
        .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3))
        print('loop took {} seconds'.format(time.time()-last_time))
        last_time = time.time()

    ##    cv2.imshow('window',cv2.cvtColor(printscreen_numpy, cv2.COLOR_BGR2RGB))
    ##    if cv2.waitKey(25) & 0xFF == ord('q'):
    ##        cv2.destroyAllWindows()
    ##        break 

По-прежнему 2-3 фрейма в секунду, стало быть, дело не в imshow.

import numpy as np
from PIL import ImageGrab
import cv2
import time

def screen_record(): 
    last_time = time.time()
    while True:
        # 800x600 windowed mode
        printscreen_pil =  ImageGrab.grab(bbox=(0,40,800,640))
    ##    printscreen_numpy =   np.array(printscreen_pil.getdata(),dtype='uint8')\
    ##    .reshape((printscreen_pil.size[1],printscreen_pil.size[0],3))
        print('loop took {} seconds'.format(time.time()-last_time))
        last_time = time.time()
    ##    
    ##    cv2.imshow('window',cv2.cvtColor(printscreen_numpy, cv2.COLOR_BGR2RGB))
    ##    if cv2.waitKey(25) & 0xFF == ord('q'):
    ##        cv2.destroyAllWindows()
    ##        break

О, вот это уже кое-что:

loop took 0.05849909782409668 seconds
loop took 0.044053077697753906 seconds
loop took 0.04760456085205078 seconds
loop took 0.04805493354797363 seconds
loop took 0.05989837646484375 seconds

Теперь для метода imshow библиотеки OpenCV нам в действительности нужен массив numpy. Что если вместо выполнения всех этих методов .getdata и reshape мы просто преобразуем ImageGrab.grab (bbox = (0,40,800,640)) в массив numpy? Зачем менять форму? Это уже тот размер, который нам нужен, и возможно, метод .getdata нам не потребуется.

import numpy as np
from PIL import ImageGrab
import cv2
import time

def screen_record(): 
    last_time = time.time()
    while(True):
        # 800x600 windowed mode
        printscreen =  np.array(ImageGrab.grab(bbox=(0,40,800,640)))
        print('loop took {} seconds'.format(time.time()-last_time))
        last_time = time.time()
        cv2.imshow('window',cv2.cvtColor(printscreen, cv2.COLOR_BGR2RGB))
        if cv2.waitKey(25) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            break 

Отлично, это дает нам 12-13 фреймов в секунду. Не бог весть что, но для нашей работы пойдет.

Размер параметра bbox был выбран исходя из разрешения GTA V в оконном режиме.

Продолжение — Играем в GTA V c Python. Часть II: основы OpenCV.