Играем в GTA V c Python. Часть VIII: дальнейшие шаги по созданию беспилотного автомобиля

Предыдущая статья — Играем в GTA V c Python. Часть VII: Беспилотный автомобиль.

Добро пожаловать в восьмую серию нашего цикла заметок по игре в GTA V при помощи методов машинного обучения. С момента выхода первых статей мы получили огромное количество идей от вас, много разного кода и интересных комментариев на нашей странице GitHub. Всем большое спасибо!

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

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

Следующим наиболее частым советом было предложение использовать ПИД-контроллер (PID controller — Пропорционально-интегрально-дифференцирующий контроллер). К сожалению, мы в этом совсем не разбираемся. Мы, конечно, понимаем, что нам нужно как-то детализировать наше движение, но как это реализовать практически — совершенно не представляем. Если у кого-то есть идеи на эту тему, можете поделиться ими на нашей странице GitHub.

Еще одна предложенная кем-то идея будет очень полезна тем, кто боролся с плохой кадровой частотой (FPS). Она заключается в том, что для замедления времени / скорости игры вы можете использовать игровой мод. Например, можно использовать моды с сайта https://www.gta5-mods.com/ под названием Alexander Blade’s Native Trainer или Enhanced Native Trainer. Более подробная информация о них есть в нашем видео. Мы, конечно, не планируем делать пособие по модам GTA V, но обращаем ваше внимание на них, так как с их помощью очень просто создать неоходимое окружение для нашего автомобиля. Мы их используем, чтобы сэкономить время, сделать ясное небо и убрать весь дорожный трафик. Потом их можно использовать и для создания более сложных ситуаций для нашего автомобиля.

Отлично, теперь перейдем к конкретным изменениям в нашем коде. Пользователь под ником «Frannecklp» предложил метод из библиотеки pywin32 для быстрых скриншотов. С его помощью мы удвоили кдровую частоту (FPS), и это крайне полезное изменение. Мы выделили эту функцию в отдельный файл под названием grabscreen.py:

# Done by Frannecklp

import cv2
import numpy as np
import win32gui, win32ui, win32con, win32api

def grab_screen(region=None):

    hwin = win32gui.GetDesktopWindow()

    if region:
            left,top,x2,y2 = region
            width = x2 - left + 1
            height = y2 - top + 1
    else:
        width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
        height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
        left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
        top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)

    hwindc = win32gui.GetWindowDC(hwin)
    srcdc = win32ui.CreateDCFromHandle(hwindc)
    memdc = srcdc.CreateCompatibleDC()
    bmp = win32ui.CreateBitmap()
    bmp.CreateCompatibleBitmap(srcdc, width, height)
    memdc.SelectObject(bmp)
    memdc.BitBlt((0, 0), (width, height), srcdc, (left, top), win32con.SRCCOPY)
    
    signedIntsArray = bmp.GetBitmapBits(True)
    img = np.fromstring(signedIntsArray, dtype='uint8')
    img.shape = (height,width,4)

    srcdc.DeleteDC()
    memdc.DeleteDC()
    win32gui.ReleaseDC(hwin, hwindc)
    win32gui.DeleteObject(bmp.GetHandle())

    return cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)

Файл directkeys.py по-прежнему существует, но функцию по отрисовке линий дорожной полосы мы выделили в отдельный файл (с надеждой, что ее кто-нибудь улучшит!) под названием draw_lanes.py:

import numpy as np
import cv2
import time
import pyautogui
from directkeys import PressKey,ReleaseKey, W, A, S, D
from draw_lanes import draw_lanes
from grabscreen import grab_screen

def roi(img, vertices):
    
    #blank mask:
    mask = np.zeros_like(img)   
    
    #filling pixels inside the polygon defined by "vertices" with the fill color    
    cv2.fillPoly(mask, vertices, 255)
    
    #returning the image only where mask pixels are nonzero
    masked = cv2.bitwise_and(img, mask)
    return masked



def process_img(image):
    original_image = image
    # convert to gray
    processed_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # edge detection
    processed_img =  cv2.Canny(processed_img, threshold1 = 200, threshold2=300)
    
    processed_img = cv2.GaussianBlur(processed_img,(5,5),0)
    
    vertices = np.array([[10,500],[10,300],[300,200],[500,200],[800,300],[800,500],
                         ], np.int32)

    processed_img = roi(processed_img, [vertices])

    # more info: http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_houghlines/py_houghlines.html
    #                                     rho   theta   thresh  min length, max gap:        
    lines = cv2.HoughLinesP(processed_img, 1, np.pi/180, 180,      20,       15)
    m1 = 0
    m2 = 0
    try:
        l1, l2, m1,m2 = draw_lanes(original_image,lines)
        cv2.line(original_image, (l1[0], l1[1]), (l1[2], l1[3]), [0,255,0], 30)
        cv2.line(original_image, (l2[0], l2[1]), (l2[2], l2[3]), [0,255,0], 30)
    except Exception as e:
        print(str(e))
        pass
    try:
        for coords in lines:
            coords = coords[0]
            try:
                cv2.line(processed_img, (coords[0], coords[1]), (coords[2], coords[3]), [255,0,0], 3)
                
                
            except Exception as e:
                print(str(e))
    except Exception as e:
        pass

    return processed_img,original_image, m1, m2

def straight():
    PressKey(W)
    ReleaseKey(A)
    ReleaseKey(D)

def left():
    PressKey(A)
    ReleaseKey(W)
    ReleaseKey(D)
    ReleaseKey(A)

def right():
    PressKey(D)
    ReleaseKey(A)
    ReleaseKey(W)
    ReleaseKey(D)

def slow_ya_roll():
    ReleaseKey(W)
    ReleaseKey(A)
    ReleaseKey(D)




    
def main():
    for i in list(range(4))[::-1]:
        print(i+1)
        time.sleep(1)
    last_time = time.time()

    while True:
        screen = grab_screen(region=(0,40,800,640))
        print('Frame took {} seconds'.format(time.time()-last_time))
        last_time = time.time()
        new_screen,original_image, m1, m2 = process_img(screen)
        #cv2.imshow('window', new_screen)
        cv2.imshow('window2',cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB))

        if m1 < 0 and m2 < 0:
            right()
        elif m1 > 0  and m2 > 0:
            left()
        else:
            straight()

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

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

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

Отлично, и что теперь?

Как мы уже говорили, нам нужны данные. У любой нейроонной сети есть вход и выход. На вход будут подаваться данные с экрана. Мы можем пропускать их через функцию ROI (область нашего интереса), но также можно подумать о возможности загружать весь экран целиком.

А что будет в результате на выходе?

Результатом являются команды по управлению автомобилем. Как уже выше отмечалось, в идеале было бы здорово постороить настоящую ПИД-систему, но на данном этапе у нас ничего для этого нет. Итак, на вход нашей нейронной сети будут подаваться экранные данные, а на выходе должны быть нажатия кнопок W или A и тому подобное. Мы разберем это все подробнее, когда дойдем до этого, но сначала нам нужно самое важное: ДАННЫЕ!

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

Мы займемся этим в нашей следующей статье.

Следующая статья — Играем в GTA V c Python. Часть IX: тренировочные данные для нейронной сети автопилота.