Step1: 关于比赛与赛道的介绍

此次比赛的赛道是一根110米长的环形自主计时赛道。赛道四周由木板搭建构成,在赛道中央贴有黑白相间的轨迹。


根据比赛规则,参赛选手可以通过任意方式来实现智能小车的自主完赛。由于赛道不同区域照明不同的关系,要想在这根赛道上让机器人小车基于计算机视觉而非使用距离传感器(例如超声波传感器)来完成比赛可谓挑战不小。


即便如此,实际比赛中我和我的队友还是决定使用中线循迹的策略来完成比赛。我们认为这样可以让机器人小车在行驶过程中避免出现太靠近赛道两侧木板的情况发生,并且小车通过中线循迹的方式能够实现预测性转弯。然而,使用这一策略即意味着我们必须在比赛中让中线时刻出现在小车的视觉范围内,并且得克服现场不同光线环境的变化所带来的小车视觉循迹稳定性这一挑战。

Step2: 软硬件清单

硬件清单:

1. 小车底盘

2. 树莓派3 + 树莓派摄像头

3. Arduino Uno + Arduino原型板

4. 直流电机控制器

5. 3D打印零部件

    - 电池盒

    - 摄像头固定柱

    - 摄像头固定夹

    - 树莓派固定盒

6. 伺服电机

7. 紧急停止按钮开关

8. 电池:用于电机的原装Nikko电池 + 树莓派与Arduino的外部供电电池


软件清单:

1. Arduino:C++

2. 树莓派:Python + Numpy + OpenCV

3. 神经网络训练:Lasagne + Theano

4. Arduino <-> 树莓派通信:自制串口协议

Step3: 程序框图与仿真

如图显示,项目的图像处理和小车控制部分是在Raspberry Pi上完成的。 


树莓派与Arduino之间建立串行协议通信,由后者使用PWM向电机发送指令(方向和速度)。由于Arduino没有提供串口写入的有效方式,为了使两块板卡之间可以进行通信,这里我们使用了一种自制的串行协议,基于单字节写入Arduino方法Serial.write()。协议如下:首先发送一个长度为1个字节(8位)的指令,然后每个字节逐一发送所需的参数。这里我们使用了一些方法来对长度为整数1-4个字节的指令进行编码和解码。例如,当发送一个16位的int时,我们把它分成两个字节(每个8位),并将这些字节存储在一个缓冲区数组中。然后,我们发送缓冲区中的每个字节,并在接收时使用按位操作(移位和屏蔽)来重建16位。


过程中,我们同时考虑到了Arduino有限的缓冲区大小:如果我们在短时间内发送太多的字节,部分信息将会因此而丢失。为了避免这个问题,Arduino通过反馈一条“收到”消息来确认收到每一条指令。这个“收到”消息会增加一个计数器,每当树莓派发送一个指令时,该计数器就会减少。为了向Arduino发送指令,这个计数器必须大于1。 在Python中,它通过信号量来实现。以下是该协议的C++绑定Python绑定,其中包括了交互式命令解析器(在debug的时候会非常有用)。


此外,我们使用Blender和Python创建了一个仿真环境来测试我们的计算机视觉算法以及我们的控制策略。得到的仿真结果相当不错,这是当时的仿真结果录像:

Step4: 图像处理部分

为了控制小车确保其能够沿着赛道前行,我们需要小车能够自行探测线路并能预测转弯,既能够在直线赛道全速行驶同时能够在转弯之前降低速度。上述功能我们通过对摄像头采集的图像进行处理分两步来实现完成,包括在获取探测线路后预测赛道曲率,以及检测确定赛道中心线


预测赛道曲率

为了实现预测转弯并减少计算时间,我们通过摄像头对三个感兴趣区域点(ROI)进行探测捕捉,并对所捕获的图像进行了裁剪。然后,我们将一条线(上图1中的绿线)拟合到所获得的三个点上,并计算此线与参考线(上图1中的蓝线)之间的角度,从而估算出弯道的转弯程度。若此时是一条笔直赛道,则绿色与蓝线会重合。如上图1,右半图为三个感兴趣区域以及所预测的赛道中心(红色圆圈标出)。左半图为针对输入图像所计算出的结果 ,其中绿线为拟合线, 蓝线是对应于一条直线(直赛道)的参考线。


检测确定赛道中心

我们最初的检测方法是使用基于颜色的方法。 基本的想法是找到给定赛道颜色的最大区域,然后计算该区域的几何中心(即赛道中心)。


这里先对“颜色检测法”做一些简单介绍:首先在HSV颜色空间中转换图像,然后根据预定义的阈值计算颜色遮罩,最终所获得的几何中心即为赛道中心。上图2显示的是仿真环境下所获得的结果。 红圈表示找到的赛道中心,绿色线条对应于颜色遮罩的轮廓。现实中,只需要30行Python代码,就能够实现上述:

from __future__ import division

import cv2
import numpy as np
# Input Image
image = cv2.imread("my_image.jpg")
# Convert to HSV color space
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
# Define range of white color in HSV
lower_white = np.array([0, 0, 212])
upper_white = np.array([131, 255, 255])
# Threshold the HSV image
mask = cv2.inRange(hsv, lower_white, upper_white)
# Remove noise
kernel_erode = np.ones((4,4), np.uint8)
eroded_mask = cv2.erode(mask, kernel_erode, iterations=1)
kernel_dilate = np.ones((6,6),np.uint8)
dilated_mask = cv2.dilate(eroded_mask, kernel_dilate, iterations=1)
# Find the different contours
im2, contours, hierarchy = cv2.findContours(dilated_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Sort by area (keep only the biggest one)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:1]
if len(contours) > 0:
    M = cv2.moments(contours[0])
    # Centroid
    cx = int(M['m10']/M['m00'])
    cy = int(M['m01']/M['m00'])
    print("Centroid of the biggest area: ({}, {})".format(cx, cy))
else:
    print("No Centroid Found")


然而,这一方法最主要的缺点是在赛道不同区域环境光变化下所带来的检测不稳定性。我们尝试了直方图均衡法来克服这个问题,但发现效果并不理想,并且后者的计算量很大。


最后,我们决定将机器学习应用到这一项目中来检测赛道中心,即我们希望通过训练一个模型来从所获得的图像输入中预测出赛道中心的准确位置。我选择使用神经网络,因为这是我最熟悉的方法,并且使用纯粹的numpy和python代码其实很容易实现上述想法。

Step5: 机器学习部分

“回归问题”概述

在监督式学习模式下,当我们标记数据时,我们的目标是预测所输入数据的标签(例如,预测图像中是否包含猫或狗)。 而在我们的这一项目中,我们想要预测来自摄像头所输入图像的赛道中心的坐标。为简化问题,针对所输入的图像区域,我们仅预测赛道中心的x坐标(沿着宽度),也就是说这里我们假定中心位于裁剪图像高度的一半处。


为了更好地评估我们的模型,我们选择均方误差(MSE)作为目标:我们取预测的x坐标与真正的赛道中心之间的平方误差,并在所有的训练样本上取平均值。


图像标记

在小车远程控制模式下录制赛道视频后,我们手动标记了3000张图像(在约25分钟内完成,即1张标签/秒)。 为了达到目的,我们创建了自己的标记工具:每个训练图像逐一显示,对每一张图像我们点击白色赛道的中心,然后必须按任意键才能进入下一张图像。


预处理和数据增强

在将我们的学习算法应用到数据之前,我们先要完成以下几步:首先,我们需要调整所输入图像的大小以减小输入尺寸(减少4倍),从而大大减少学习参数的数量。这样可以简化问题,加快训练和预测时间。此外,为避免学习问题的产生并加快训练,需要对数据进行规范化。 在我们的例子中,我们将输入图像统一在[-1,1]范围内,并将输出(赛道中心的预测x坐标)缩放为[0,1]。预处理脚本如下:

def preprocessImage(image, width, height):
    """
    Preprocessing script to convert image into neural net input array
    :param image: (cv2 RBG image)
    :param width: (int)
    :param height: (int)
    :return: (numpy array)
    """
    image = cv2.resize(image, (width, height), interpolation=cv2.INTER_LINEAR)
    x = image.flatten()  # create a big 1D-array
    # Normalize
    x = x / 255. # values in [0, 1]
    x -= 0.5 # values in [-0.5, 0.5]
    x *= 2 # values in [-1, 1]
    return x

最后,为了增加训练样本的数量,我们将图像垂直翻转,以快速且简单的方式将训练集的大小乘以2。


神经网络架构

我们所使用的是一个前馈神经网络,由两个隐藏层组成,分别具有8个和4个单位。实际过程中,我们尝试了不同的架构,包括CNN(卷积神经网络),但我们发现前馈神经网络架构取得了最好的效果,可以以超过60 FPS的速度实时运行!


超参数的选择

我们选择的超参数包括网络架构,学习率,小批量大小...等。为了验证超参数的选择,我们将数据集分成3个子集:一个训练集(60%),一个验证集(20%)和一个测试集(20%)。 我们保持模型在验证集上的最低误差,并使用测试集估计我们的泛化误差。

————————————————————————————


最终,我们用了不到20分钟的时间,在8核笔记本电脑的CPU上对这一神经网络进行了训练。


训练数据包括7600+张标记的图片。下载后,会有两个文件夹,分别是:

· input_images/ (小车远程控制模式下所获取的原始图像)

· label_regions/ (输入图像的标记区域)


每张图片的命名规则为:“x_center"-"y_center"_"id".jpg,例如:

· 0-28_452-453r0.jpg => center = (0, 28) | id = "452-453r0"

· 6-22_691-23sept1506162644_2073r2.jpg => center = (6, 22) | id = "691-23sept1506162644_2073r2"


Step6: 小车循迹的控制

当完成了图像处理部分并计算出偏离中线的误差之后,我们就需要通过调整小车的前进方向和速度来纠正误差。


为此,我们使用了经典PID控制来实现赛道循迹,并通过两个主要因素来进行速度调节:当前偏离赛道中心的误差值,以及弯道的弯曲程度。同一线曲率下,误差越大,速度越低。


我们的控制策略的可以简单概括为两行代码:

command = Kp * e + Kd * (de/dt)  # where "e" is the error
speed = MIN_SPEED * h + (1 - h) * MAX_SPEED

# WHERE h = 0 if it is a straight line and 1 if it's a sharp turn
#      MAX_SPEED depends on the error "e" in the same manner,
#      that is to say the bigger the error, the lower the MAX_SPEED


此外,比赛中我们发现弯道预测有进一步的优化空间,所以我们决定做一个移动平均值,以此来提高控制的稳定性。

Step7: 项目小结

最终,通过使用上述策略,我们在142支参赛队中取得了第4名的好成绩。


点击左侧“下载代码”,可以下载到项目的开源代码。希望可以帮助有兴趣的朋友复现我们的项目,或者从中获取到一些项目制作的灵感。

评论