前言

本文讲解如何在游戏欧卡2中使用深度学习框架实现无人驾驶,为什么选择欧卡2?虽然ROS中也有真实汽车的仿真环境但是世界场景过于简单,而游戏欧卡2完全是80%的程度还原了欧洲交通以及城市道路、农村道路、高速路等,车辆以及环境和物理引擎都比较完善,最重要的是所有的交通规则都在游戏里复现了,所以在游戏中研究算法也是不错的选择。

项目地址

github地址:觉得不错点个star呗,持续更新ing

效果

原理

首先分析问题,实现自动驾驶无非就是需要做两个控制:
  1. 一个是车辆的油门控制
  2. 一个是方向盘的角度控制
根据我个人的开车经验,就可以把自动驾驶简化成一个简单的系统,输入是人眼看到的前方道路,输出是手打方向盘的角度和脚踩刹车的力度。当然这里是做了简化处理,暂时不考虑后视镜视角并且将车速的控制改成刹车力度的控制(这里主要是因为游戏的原因,速度会自动加到最大速,所以用刹车力度来控制速度)。 分析完了就得到下面的系统: 系统

实现

ok,分析明白了问题就解决问题的过程了,从左往右解决问题。

拿到车前视角图像

游戏里有一点不好就是一般不提供视觉数据的接口以及游戏中传感器数据的接口等,不过没关系,车前视角我们可以使用录屏的方式来获取,话不多说直接上源码:
from PIL import ImageGrab
import threading
import numpy as np
import cv2
import time
import os

class myRecord:
    def __init__(self,width,height):
        if not os.path.exists("config.txt"):
            os.system(r"touch {}".format("config.txt"))
        f = open('config.txt',"r")
        line = f.readline()
        if len(line) != 0:
            print(line.split(" "))
            self.left =  int(line.split(" ")[0])
            self.top = int(line.split(" ")[1])
            self.right = int(line.split(" ")[2])
            self.bottom = int(line.split(" ")[3])
        else:
            self.left, self.right, self.top, self.bottom = 0,width,0,height
        f.close()
        self.width = width
        self.height = height
        self.recordThread = threading.Thread(target=self.record)
        self.recordThread.start()

    def record(self):
        cv2.namedWindow('image')
        cv2.namedWindow('tool')
        cv2.createTrackbar('left', 'tool', self.left, self.width, self.left_callback)
        cv2.createTrackbar('top', 'tool', self.top, self.height, self.top_callback)
        cv2.createTrackbar('right', 'tool', self.right , self.width, self.right_callback)
        cv2.createTrackbar('bottom', 'tool', self.bottom, self.height, self.bottom_callback)
        lastTime = time.time()
        write_flag = 0
        while True:
            im = ImageGrab.grab((self.left,self.top,self.right,self.bottom))  # 获得当前屏幕
            imm = cv2.cvtColor(np.array(im), cv2.COLOR_RGB2BGR)  # 转为opencv的BGR格式
            scale = 1.5
            show = cv2.resize(imm,(int(self.right/scale - self.left/scale),int(self.bottom/scale - self.top/scale)))
            cv2.putText(show, "delay:" + str(round(time.time() - lastTime,2)) + "s", (50, 300), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 2)
            lastTime = time.time()
            cv2.imshow('image', show)  # 显示

            if write_flag == 1:
                if not os.path.exists("record"):
                    os.makedirs("record")
                cv2.imwrite("record/" + str(time.time()) + ".jpg",imm)
            key = cv2.waitKey(1) & 0xFF
            if key == ord('q'):  # q键推出
                break
            elif key == ord('s'):  # q键推出
                print("开始录制")
                write_flag = 1

        cv2.destroyAllWindows()

    def left_callback(self,x):
        self.left = x
        if self.left >= self.right:
            self.left = self.right - 5
        f = open('config.txt',"w")
        f.write("{} {} {} {}".format(self.left,self.top,self.right,self.bottom))
        f.close()

    def right_callback(self,x):
        self.right = self.width - x
        if self.left >= self.right:
            self.right = self.left + 5
        f = open('config.txt',"w")
        f.write("{} {} {} {}".format(self.left,self.top,self.right,self.bottom))
        f.close()

    def top_callback(self, x):
        self.top = x
        if self.top >= self.bottom:
            self.top = self.bottom - 5
        f = open('config.txt',"w")
        f.write("{} {} {} {}".format(self.left,self.top,self.right,self.bottom))
        f.close()

    def bottom_callback(self, x):
        self.bottom = self.height - x
        if self.top >= self.bottom:
            self.bottom = self.top + 5
        f = open('config.txt',"w")
        f.write("{} {} {} {}".format(self.left,self.top,self.right,self.bottom))
        f.close()


if __name__ == '__main__':
    window = ImageGrab.grab()  # 获得当前屏幕,存窗口大小
    width,height = window.size
    r = myRecord(width,height)
这个代码直接保存本地使用python运行即可,效果如下: 代码没啥好说的,使用PIL库抓取本地电脑桌面图片,然后使用opencv的Trackbar修改截取长宽。

黑盒实现

黑盒是项目的核心部分,传统的做法可以像恩智浦智能车一样通过噪声过滤,霍夫变换,二值化等等操作提取车道线然后用PID算法将车辆归正到跑道中心,这种做法相比于深度学习的方法有个很明显的缺陷就是不够鲁棒,无论你算法多么优秀你在真实的无人驾驶场景都不能覆盖到所有的道路情况。因此深度学习的方法优势就体现出来了,只要网络设计合理,数据集丰富,他就可以拥有很强的泛化能力,而且相对于设计复杂的传统算法来适配一个一个的情况,深度学习的方法简单了许多。 接下来就是黑盒的网络设计,处理图像数据我们知道必然会使用卷积神经网络,然后因为我们的输出是两个数字(刹车力度和方向盘角度),所以我们需要在最后面接上全连接层。 网络结构如下:
def cnn_model(image):

    conv1 = fluid.layers.conv2d(input=image, num_filters=24, filter_size=5, stride=2, act='relu')
    conv2 = fluid.layers.conv2d(input=conv1, num_filters=32, filter_size=5, stride=2, act='relu')
    conv3 = fluid.layers.conv2d(input=conv2, num_filters=64, filter_size=5, stride=2, act='relu')
    conv4 = fluid.layers.conv2d(input=conv3, num_filters=64, filter_size=3, stride=2, act='relu')
    conv5 = fluid.layers.conv2d(input=conv4, num_filters=64, filter_size=3, stride=1, act='relu')
    fc1 = fluid.layers.fc(input=conv5, size=100, act=None)
    drop_fc1 = fluid.layers.dropout(fc1, dropout_prob=0.1)
    fc2 = fluid.layers.fc(input=drop_fc1, size=50, act=None)
    drop_fc2 = fluid.layers.dropout(fc2, dropout_prob=0.1)
    predict = fluid.layers.fc(input=drop_fc2, size=2, act=None)
    return predict
注意这里的输入和输出,这个网络的输入是(120,120)的图片,会对传入的图片做预处理resize成网络需要的大小,网络的输出是2,因为有刹车力度和方向盘角度两个。 定义好了网络结构接下来就是定义数据维度配置优化器定义损失函数数据预处理等等。 定义数据维度:
image = fluid.layers.data(name='image', shape=[3, crop_size, crop_size], dtype='float32')
label = fluid.layers.data(name='label', shape=[2], dtype='float32')
损失函数:
cost = fluid.layers.square_error_cost(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
定义优化方法:
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.001)
opts = optimizer.minimize(avg_cost)
完整代码参考项目中的selfDriverInEuroTruck\trainPart\Train_Model.py文件。

数据集制作

数据集是深度学习中重要的一部分也是很无聊的一部分,在游戏中获取数据集更是繁琐至极。本文采用vjoy虚拟摇杆技术,使用python的vjoy库模拟虚拟摇杆来控制游戏中的车辆,并且使用python编写全局按键监听脚本(大部分按键监听都是需要鼠标在Terminal窗口获取光标才能监听按键)来给虚拟摇杆赋值模拟手柄操作。 数据集录制脚本:github地址 下载到本地右键运行即可: 一定要按键u和i控制方向才会记录方向数据,按键G是记录刹车力度同时刹车,运行结束按键B保存录制结果,直接退出脚本是不会保存录制的log的。 数据集样式:

控制驾驶的代码

import pyvjoy
j = pyvjoy.VJoyDevice(1)
def control(ang,brake):
    if ang > 60:
        ang = 60
    if ang < -60:
        ang = -60
    global j
    x = ang / 180 + 0.5
    j.data.wAxisX = int(x * 32767)
    j.data.wAxisY = int(brake * 32767) # 刹车力度,0-1,0时无刹车90km/h速度运行
    j.data.wAxisZ = 0
    j.update()
完整代码参考:github地址

运行项目

项目文件介绍

  • selfDriverInBetaSimulator文件夹:在MIT仿真软件UDACITY上实现的无人驾驶代码。
  • selfDriverInEuroTruck文件夹:在欧卡2游戏里实现无人驾驶的代码。

欧卡2无人驾驶快速使用教程

step1

安装software文件夹下的vJoySetup.exe,双击运行即可,一直默认安装。

step2

修改欧卡2的控制器,在选项->控制器->设置键盘加vjoy。

step3

运行autoDriver.py脚本开始自动驾驶。 !!!重要?
脚本运行后会有opencv的窗口出现,需要调整你的游戏界面使得窗口内的图像是你的车窗视角

欧卡2无人驾驶完整训练加数据集准备教程

step1

安装software文件夹下的vJoySetup.exe,双击运行即可,一直默认安装。

step2

修改欧卡2的控制器,在选项->控制器->设置键盘加vjoy。

step3

运行recordData.py脚本录制训练数据: !!!重要?
按键‘O’开始录制 按键‘B’退出录制并写入log 未开始录制时按键a d控制方向盘 开启录制后按键u i控制方向盘

step4

运行trainPart/ouka2_makelist.py文件生成train_data.txt和test_data.txt。

step5

运行trainPart/Train_Model.py开始训练,训练结束会自动保存在model_infer文件夹中。

step6

运行autoDriver.py脚本开始自动驾驶。 !!!重要?
脚本运行后会有opencv的窗口出现,需要调整你的游戏界面使得窗口内的图像是你的车窗视角

后续优化

  • 准备使用paddleseg库做一个城市景物的分割当作数据的预处理,然后使用这个预处理的数据来训练方向盘角度和刹车力度。这么做的优点是可以识别车辆和道路的可行区域并且路径上的干扰可以过滤掉。目前已经完成了模型的部署准备制作数据集开始训练。示例如下:

项目地址

github地址:觉得不错点个star呗,持续更新ing

效果