(计算机视觉实战) 自动驾驶 python实现
拿到数据之后,应该先对数据进行合理的分析了解数据。首先了解数据可以分为一下两个方面,查看数据是否平衡,查看数据最大值与最最小值是否在正常范围内,这可以在一定的程度上减少异常值的出现。数据是在行车模拟骑上收集的。它包含了三个摄像头从左中右三个角度捕捉到的行车图像的存储位置以及对应的方向盘转动角度等信息。首先先对steering属性进行一些分析。将数据使用pandas读入,可取出该特征,并对该特征的用
1. 项目介绍
该练手小项目,根据英伟达的论文end to end learning for self-driving cars论文的基本思想设计。该小项目使用模拟器收集捕获的路面图像以及方向盘转动的角度作为为网络的训练数据,训练一个神经网络。训练好的网络可以根据路面的情况自动的输出方向盘的角度,并带领小汽车行驶。论文中提及,在采集数据的时候,使用的是三个摄像头,分别位于车头的中间,左边以及右边。但是在测试的时候,则只用一个摄像头。模拟器收集的数据和论文中的类似。每一个时刻模拟器捕获三张图像和中间摄像头对应的方向盘的转动角度,三张图像分别对应左中右摄像头。
上图是论文中描述的系统结构。
模拟器如下所示:
上图是模拟器界面。
整个小项目分为五个阶段:
- 数据理解:了解数据组成和分布。
- 数据清洗:平衡数据等。
- 设计hypothesis:设计多种可行的网路结构
- 训练模型
- 测试模型
2. 数据理解
首先从第一步开始。拿到数据之后,应该先对数据进行合理的分析并了解数据。首先,了解数据可以分为一下两个方面,查看数据是否平衡,查看数据最大值与最最小值是否在正常范围内,这可以在一定的程度上减少异常值的出现。数据是在行车模拟器上收集的。它包含了三个摄像头从左中右三个角度捕捉到的行车图像的存储位置以及中间摄像头对应的方向盘转动角度等信息。
将数据使用pandas读入,取出方向盘角度对应的列,并对该特征的用直方图画出该特征的分布。
分布图的X轴表示的是方向盘转动的角度,而y轴表示的是这个角度,在收集到的数据中所出现的频率。可以发现转动角度为0的出现的次数是远远大于其余的角度的。这也是数据不平衡的一个表现。 我们暂且可以先将方向盘的转动分成往左,往右,和不转动。它们分别对应角度小于0,角度大于0,和角度等于0。我们可以查看一下它们具体在数据集中出现了多少次,以及最大值与最小值
print('maximum steering:{} and minimum steering:{}'.format(img_data_steering.max(),img_data_steering.min()))
print('steering larger than zero: {}'.format(len(img_data_steering[img_data_steering>0])))
print('steering smaller than zero: {}'.format(len(img_data_steering[img_data_steering<0])))
print('steering equals zero: {}'.format(len(img_data_steering[img_data_steering==0])))
'''
output:
maximum steering:1.0 and minimum steering:-0.9426954
steering larger than zero: 1900
steering smaller than zero: 1775
steering equals zero: 4361
'''
3. 数据清洗
0度出现的次数已经是其他两个方向的2倍还要多,所以可以考虑先对0度做欠采样,也就是减少采集它的频率。或者随机减少它在数据集中的数量。
def zero_degree_discard(degree_list,discard_rate):
zero_degree = np.where(degree_list==0.0)[0]#取出转动角度为0的数据
len_preserved = int(len(zero_degree) * discard_rate) #需要保存的长度
return np.random.choice(zero_degree,size=len_preserved,replace=False) #返回保留之后的数据
4. data_generator与图像增强
在处理完数值类数据之后,我们可以开始处理图像。
因为训练数据很大,而且需要对每一张图像进行不同的图像增强操作,如果所有的操作都在内存中操作,势必会对内存带来很大的压力。所以对于这样庞大的训练数量,可以使用生成器的方式来为网络提供相应的输入,每一次只取一批训练数据在内存中进行图像增强的操作,可以减少内存消耗。那么生成器的思路大致如下。首先训练和测试都需要数据,但是对数据的要求不太一样,当训练模型的时候,需要对输入的图像进行相应的处理,而相反在测试模型的时候,并不需要对图像做任何的修改。所以可以将这个生成器定义成两个模式并定义参数training用于区分。当生成器用于训练的时候,也就是training参数为True的时候,首先确定的就是输入的数据一定要是平衡的。根据前面对数据的分析,我们知道对于中间摄像头所捕捉到的图像而言,方向盘的转动角度有很大一部分都是0度。所以为了让模型学习到这个规律,在绝大多数的情况下都输出0度。我们需要合理的删除一些方向盘角度为0的训练数据,使得数据集平衡。基本的数据满足了要求之后,我们需要定义两个空的ndarray用于存放每一批数据,并将它们返回给网络。当所有的数据都被使用过一次之后,需要将数据重新打乱一次,这样做的目的也是为了让网络看到更多不同的可能性。此外,为了跟踪每一个batch之后,数据读到哪里了,需要定义一个counter,每训练一个批次,counter就加上一个批次的大小,下一个批次的读取就从上一个批次的终止处开始,直到和训练数据一样大,意味着所有的数据都被遍历了。每一张训练数据中的图像和角度,都会用一个data_transformation 的函数来进行处理,这个函数整合了所有需要的图像增强和变换操作,(下面会展开描述)。 该函数会返回处理之后的图像和角度,为什么角度也需要处理?因为图像的翻转会影响到方向盘转动的方向。得到处理好的图像和角度之后,将它们分别存放到事先定义好的两个ndarray中,并将它们组合成一个元组并yield回去即可。 该生成器的代码如下:
def data_generator(self,x,y,shape,img_dir, batch_size = 128, training=True, discard_rate = 0.85):
'''
input:
x_train: imgs catured by cameras at the front of car
y_train: steering radius
:return:
'''
# counter = 0 # when value of counter equals the length of x and y, data is reshuffled
# x_batch = np.empty((batch_size,*shape)) # processed imgs are stored here
# y_batch = np.empty((batch_size,1))
if training: # imgs need to be processed if they are prepared for training
#discard redundent y and coresponding imgs
x,y = shuffle(np.array(x),np.array(y))
y_discarded_index = self.discard_zero_angle(y,discard_rate)
y_new = np.delete(y,y_discarded_index,axis=0)
x_new = np.delete(x,y_discarded_index,axis=0)
else:
y_new = y
x_new = x
# print('length of x:{}'.format(len(x_new)))
# print('length of y:{}'.format(len(y_new)))
while True : # generator should be able to produce data every time it is called.
counter = 0 # when value of counter equals the length of x and y, data is reshuffled
x_batch = np.empty((batch_size, *shape)) # processed imgs are stored here
y_batch = np.empty((batch_size, 1))
for index_sample in range(batch_size):
# print(index_sample)
# print('counter:{}'.format(counter))
if training:
img_name,img_angle = x_new[index_sample + counter],y_new[index_sample + counter] # get img name
# img_ = self.read_img(img_name,img_dir) #get the real image
img_transformed,degree = self.data_transformation(img_name,img_angle,img_dir) # transform image to the required standard and degree
# img_cliped = self.region_of_interest(img_transformed,shape) #
img_standard = self.img_standardisation(img_transformed)
x_batch[index_sample,:,:,:] = img_standard #meet the format of CNN
# print('x_batch:'.format(x_batch))
y_batch[index_sample] = degree
# print('y_batch:'.format(y_batch))
else:
x_batch[index_sample,:,:,:] = self.read_img(img_name)
y_batch[index_sample] = img_angle
if counter > len(x_new): # meaning out of bound
counter = 0
x, y = shuffle(x, y)
y_discarded_index = self.discard_zero_angle(y, discard_rate)
y_new = np.delete(y, y_discarded_index)
x_new = np.delete(x, y_discarded_index)
counter += batch_size
yield (x_batch,y_batch)
上述代码中的data_transformation函数主要的目的是为了对训练数据进行实时增强。比如,亮度的调节,图像翻转等。这一切的目的都是为了丰富数据集,让网络看到更多的情况。在data_transformation的代码如下:
def data_transformation(self,img_name,degree,img_dir):
img_name, degree = self.left_and_right_swap(img_name, degree) #img_name is returned
img_ = self.read_img(img_name,img_dir) # read real img #
img_ = self.random_brightness(img_) # alter brightness
img_,degree = self.img_flip(img_,degree) # flip img
return (img_,degree)
该函数接受三个参数,分别是图片名,方向盘转动的角度,以及文件所在的路径。然后在该函数中将图片读出,并做处理。
收集到的数据中只有中间摄像头所捕获的图像有对应的方向盘转动角度,其余的左边和右边的摄像头捕捉到的画面都没有对应的角度。为了让能够增加训练数据的多样性。可以使用中间摄像头的画面以及其方向角度来推测左边和右边的摄像头捕捉的画面所对应的方向盘的角度。该问题可以用以下思路建模。假设中间摄像头方向盘的角度是S,那么左边摄像头捕捉到的画面所对应的方向盘的角度是 s ′ = t a n ( S ) + 1 4 s^{'}=tan(S) + \frac{1}{4} s′=tan(S)+41,所以同理可推,同理右边的角度和左边的正好相反,为 s ′ = t a n ( S ) + 1 4 s^{'}=tan(S) + \frac{1}{4} s′=tan(S)+41。所以由此,我们可以根据一张中间摄像头捕获的画面,随机计算同一时刻左或右边摄像头捕获画面所对应的方向盘角度。 实现的代码如下:
def generate_left_right_steer(self,img_name,degree,corr=1.0/4):
flag = np.random.choice(['L','R','C']) #
if flag =='L':
img_name = img_name.replace('center','left')
degree = math.tan(degree) + corr
return img_name,degree
if flag =='R':
img_name = img_name.replace('center', 'right')
degree = math.tan(degree) - corr
return img_name, degree
if flag == 'C' : return img_name,degree
上述步骤完成之后,就可以对实实在在的图像进行处理了,处理的步骤是随机调整亮度,以及图像翻转。首先函数random_brightness的作用是更改收集到的图片的亮度,在一定程度增加了数据的多样性。对于自动驾驶而言,合理的调整图片的亮度,可以模拟在不同时间段的行车情况,也在一定程度上平衡数据。对于调整图片亮度而言,我们需要将图片从RGB色彩空间转换到HSV色彩空间。简单的来说,HSV色彩空间中,HS主要包含的是图片的颜色信息,而V通道主要包含的是图片的亮度。所以,想要更改图片的亮度,一个可行的思路是将目标照片转换至HSV色彩空间之后,将其三个通道拆分开来,然后对给V通道乘上一个合理的强度。H和S两个通道保持不变。最后将H,S和改动之后的V通道合并起来,并重新更改回RGB色彩空间。这样一来,图片的亮度就会根据我们所给定的强度而发生相应的改变。根据上述思路可以得到:
def random_brightness(img,factor):
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) #将BRG转换成RGB
img = cv2.cvtColor(img,cv2.COLOR_RGB2HSV) #将RGB转换成HSV
h,s,v = cv2.split(img) #将HSV三个通道拆开
v = v * factor #更改V
v = v.astype('uint8') #CV2中图片的表示用的是8位整形
img = cv2.cvtColor(cv2.merge((h,s,v)),cv2.COLOR_HSV2RGB) #重组图片
return img
下图是当参数factor的大小为0.2到时候,可以明显地看出,整张图片的亮度明显下降。使用这样的技巧将可以很轻松的模拟不同时段的行车情况。
将返回的图片继续做随机翻转处理。需要注意的是,图像翻转了之后,方向盘的角度也应该随之进行翻转。处理完成之后返回处理完成的图片和角度。图像翻转的函数是这样定义的:
def img_flip(self,img,angle):
flag = np.random.choice([0,1])
if flag == 0:
return (img,angle) # flag == 0, return the original img and angle
else:
img_ = cv2.flip(img,1)
return (img_,-angle)
之后并对图像做归一化处理,将所有像素点的值压缩到一个固定的范围中。当然这里可以做更多的操作,比如只保留图像中对训练有用的部分,比如车道。将其余的部分删去。因为严格来讲图片中其余的部分,例如天空,树木等对训练自动驾驶而言并没有实质性的帮助,根据论文,这些图像中的元素并不会被激活函数激活,所以删除将他们从训练脸数据中删去也不会影响训练的效果。在做完上述这一系列的操作之后,便可以将处理完成的图像和角度,分别加入到x_batch和y_batch当中了,当x_batch和y_batch的长度,等于实现定义好的batch_size的时候,便可以将其返回做为一个批次的网络输入了。
5. 网络结构
对于网络模型而言,所使用的的模型是根据论文做了一些相应的改动,但是基本结构都是很相似的。其原理和一般的回归网络很像:使用CNN提取图像特征,在这个小项目中,道路部分的特征将会被激活。之后只用FC网络回归相应的角度。网络的设计尝试了很多种不同结构,以下只是一种可行的hypothesis。
def autonomous_model(self,input_shape):
input_ = Input(input_shape,name='input_shape')
conv_1 = Conv2D(24,(3,3),activation='relu',name='conv_1')(input_)
drop_out = Dropout(rate=0.5)(conv_1)
max_out_1 = MaxPooling2D(pool_size=(3,3),name = 'maxpooling_1')(drop_out)
conv_2 = Conv2D(36,(3,3),activation='relu',name = 'conv_2')(max_out_1)
max_out_2 = MaxPooling2D(pool_size=(3,3),name='maxpooling_2')(conv_2)
conv_3 = Conv2D(48,(3,3),activation='relu',name='conv_3')(max_out_2)
max_out_3 = MaxPooling2D(pool_size=(3,3),name='maxpooling_3')(conv_3)
conv_4 = Conv2D(64,(3,3),activation='relu',name = 'conv_4')(max_out_3)
flatten_ = Flatten()(conv_4)
Dense_1 = Dense(256,activation='relu')(flatten_)
Dropout_2 = Dropout(rate=0.5)(Dense_1)
Dense_2 = Dense(128,activation='relu',kernel_regularizer=l1(0.002))(Dropout_2)
Dense_3 = Dense(64,activation='relu')(Dense_2)
Dense_4 = Dense(1,activation='linear',name='Dense_4',activity_regularizer=l1(0.002))(Dense_3)
optimizer = Adam(lr=0.00001)
my_model = Model(inputs=input_,outputs = Dense_4)
my_model.compile(loss='mean_squared_error',optimizer=optimizer,metrics=['mse'])
return my_model
然后调用类中定义好的train方法,即可对网络进行训练。
测试训练好的模型之前,需要与模拟器建立连接,让模型与模拟器之间可以相互通信。可用flask框架建立连接,但是在这之前,一定要检查好flask,socketio等版本是否相互兼容,版本是与模拟器兼容。如果出现迟迟连接不上的问题,可以首先考虑上述问题。确定版本无误后只需简单的使用flask的模板,即可连接。有趣的是,虽然网络并没有预测油门的大小,但是我们可以根据转向的大小来改变返回给模拟器油门的大小,当转向的角度超出阈值的时候(个别时候网络输出异常),可以减慢汽车的速度。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)