Traffic sign detection - Udacity’s selfdriving car nanodegree— deep learning series 3

Dhanoop Karunakaran
Intro to Artificial Intelligence
6 min readJun 6, 2018

1. Introduction

Convolutional Neural Network (CNN) is a powerful tool in computer vision and self driving cars. Traffic sign detection is one of the major task in self-driving as it gives the input of what sign is in the image to decision making. I have done this project as part of Udacity’s self-driving car engineer course and all the credit goes to them. If you would like to get a brief introduction on CNN, please visit my previous article in this series.

2. Dataset

We have been provided with training, validation, and testing dataset in pickle format. Each dataset contains number of images and it’s label. We can use below code to load the data from pickle.

# Load pickled data
import pickle
import pandas as pd
import numpy as np

# TODO: Fill this in based on where you saved the training and testing data

training_file = 'traffic-signs-data/train.p'
validation_file= 'traffic-signs-data/valid.p'
testing_file = 'traffic-signs-data/test.p'

with open(training_file, mode='rb') as f:
train = pickle.load(f)
with open(validation_file, mode='rb') as f:
valid = pickle.load(f)
with open(testing_file, mode='rb') as f:
test = pickle.load(f)

X_train, y_train = train['features'], train['labels']
X_valid, y_valid = valid['features'], valid['labels']
X_test, y_test = test['features'], test['labels']

The pickled data is a dictionary with 4 key/value pairs:

  • 'features' is a 4D array containing raw pixel data of the traffic sign images, (num examples, width, height, channels).
  • 'labels' is a 1D array containing the label/class id of the traffic sign. The file signnames.csv contains id -> name mappings for each id.
  • 'sizes' is a list containing tuples, (width, height) representing the original width and height the image.
  • 'coords' is a list containing tuples, (x1, y1, x2, y2) representing coordinates of a bounding box around the sign in the image. THESE COORDINATES ASSUME THE ORIGINAL IMAGE. THE PICKLED DATA CONTAINS RESIZED VERSIONS (32 by 32) OF THESE IMAGES.

Here is Summary statistics of the traffic signs data set we have:

Number of training examples = 34799
Number of validation examples = 4410
Number of testing examples = 12630
Image data shape = (32, 32, 3)
Number of classes = 43
Example dataset

3. Pre-processing

Before starting the training processs, dataset needs to have basic preprocessing using normalisation, grayscale etc. I found out normalisation itself gives very good output result and did not use other preprocessing techniques.

I did a reshuffling of the data so that it can increase the random nature of the datset.

from sklearn.utils import shuffleX_train, y_train = shuffle(X_train, y_train)
X_valid, y_valid = shuffle(X_valid, y_valid)
X_test, y_test = shuffle(X_test, y_test)

Then did the normalisation to make sure the image data has been normalized so that the data has mean zero and equal variance.

#Nomralisation
X_train = (X_train-X_train.mean())/(np.max(X_train)-np.min(X_train))
X_valid = (X_valid-X_valid.mean())/(np.max(X_valid)-np.min(X_valid))
X_test = (X_test-X_test.mean())/(np.max(X_test)-np.min(X_test))

Image before and after normalisation are displayed here.

Before:

After:

4. Model architecture

The above picture is the architecture of LeNet-5 which is considered as one of the first Convolutional Neural Network(CNN). We are using LeNet-5 for the traffic sign detection project.

Pseudo-code of the architecture as follows:

Input

The LeNet architecture accepts a 32x32x3 image as input

Architecture

Layer 1: Convolutional. The output shape should be 28x28x6.

Activation. Your choice of activation function.

Pooling. The output shape should be 14x14x6.

Layer 2: Convolutional. The output shape should be 10x10x16.

Activation. Your choice of activation function.

Pooling. The output shape should be 5x5x16.

Flatten. Flatten the output shape of the final pooling layer such that it’s 1D instead of 3D. The easiest way to do is by using tf.contrib.layers.flatten, which is already imported for you.

Layer 3: Fully Connected. This should have 120 outputs.

Activation. Your choice of activation function.

Layer 4: Fully Connected. This should have 84 outputs.

Activation. Your choice of activation function.

Layer 5: Fully Connected (Logits). This should have 43 outputs.

Actual code implementation is below:

def LeNet(x): 

# Layer 1: Convolutional. Input = 32x32x3. Output = 28x28x6.
conv1_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 3, 6), mean = 0, stddev = 0.1))
conv1_b = tf.Variable(tf.zeros(6))
conv1 = tf.nn.conv2d(x, conv1_W, strides=[1, 1, 1, 1], padding='VALID') + conv1_b

# Activation 1.
conv1 = tf.nn.relu(conv1)
# Pooling. Input = 28x28x6. Output = 14x14x6.
conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID')


# Layer 2: Convolutional. Input = 14x14x6. Output = 10x10x16.
conv2_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 6, 16), mean = 0, stddev = 0.1))
conv2_b = tf.Variable(tf.zeros(16))
conv2 = tf.nn.conv2d(conv1, conv2_W, strides=[1, 1, 1, 1], padding='VALID') + conv2_b

# Activation 2.
conv2 = tf.nn.relu(conv2)
# Pooling. Input = 10x10x16. Output = 5x5x16.
conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID')

# Flatten. Input = 5x5x16. Output = 400.
flattened = flatten(conv2)

#Matrix multiplication
#input: 1x400
#weight: 400x120
#Matrix multiplication(dot product rule)
#output = 1x400 * 400*120 => 1x120

# Layer 3: Fully Connected. Input = 400. Output = 120.
fullyc1_W = tf.Variable(tf.truncated_normal(shape=(400, 120), mean = 0, stddev = 0.1))
fullyc1_b = tf.Variable(tf.zeros(120))
fullyc1 = tf.matmul(flattened, fullyc1_W) + fullyc1_b

# Full connected layer activation 1.
fullyc1 = tf.nn.relu(fullyc1)

# Layer 4: Fully Connected. Input = 120. Output = 84.
fullyc2_W = tf.Variable(tf.truncated_normal(shape=(120, 84), mean = 0, stddev = 0.1))
fullyc2_b = tf.Variable(tf.zeros(84))
fullyc2 = tf.matmul(fullyc1, fullyc2_W) + fullyc2_b

# Full connected layer activation 2.
fullyc2 = tf.nn.relu(fullyc2)

# Layer 5: Fully Connected. Input = 84. Output = 43.
fullyc3_W = tf.Variable(tf.truncated_normal(shape=(84, 43), mean = 0, stddev = 0.1))
fullyc3_b = tf.Variable(tf.zeros(43))
logits = tf.matmul(fullyc2, fullyc3_W) + fullyc3_b

return logits

5. Train & Evaluate

To train the model, I used following hyperparameter after several trial and error method.

learning_rate = 0.001epochs = 40batch_size = 64

Lenet model gives the logits and cross entropy. Then loss operation give the error compared to actual result and predicted result. Finally Adam optimiser is used for optimisation.

logits = LeNet(x)
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=one_hot_y, logits=logits)
loss_operation = tf.reduce_mean(cross_entropy)
optimizer = tf.train.AdamOptimizer(learning_rate = learning_rate)
training_operation = optimizer.minimize(loss_operation)

The above steps do the forward and backward pass and doing this on iterative manner will reduce the error at the end.

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
num_examples = len(X_train)

print("Training...")
print()
for i in range(epochs):
X_train, y_train = shuffle(X_train, y_train)
for offset in range(0, num_examples, batch_size):
end = offset + batch_size
batch_x, batch_y = X_train[offset:end], y_train[offset:end]
sess.run(training_operation, feed_dict={x: batch_x, y: batch_y})

valid_loss, valid_accuracy = evaluate(X_valid, y_valid)
print("Epoch {}, Validation loss = {:.3f}, Validation Accuracy = {:.3f}".format(i+1, valid_loss, valid_accuracy))
print()

saver1.save(sess, './classifier')
print("Model saved")

The above code is the typical way of running the training in Tensorflow. Then evaluate function added below get called to check the validation accuracy at each epoch.

def evaluate(X_data, y_data):
num_examples = len(X_data)
total_accuracy = 0
total_loss = 0
sess = tf.get_default_session()
for offset in range(0, num_examples, batch_size):
batch_x, batch_y = X_data[offset:offset+batch_size], y_data[offset:offset+batch_size]
loss, accuracy = sess.run([loss_operation, accuracy_operation], feed_dict={x: batch_x, y: batch_y})
total_accuracy += (accuracy * len(batch_x))
total_loss += (loss * len(batch_x))
return total_loss/num_examples, total_accuracy/num_examples

7. Testing

Once the training is done, we can run the model against test dataset to check the final accuracy. In this case, trained model is able to correctly guess 6 of the 6 traffic signs, which gives an accuracy of 100%.

The result of the prediction as follows:

The above table gives the indication that, if we provide an image of traffic signs, then it can predict it accurately. I have only provided the label on the image columns instead of image due to this article space constraints.

If you would like to see the full code in action, please visit my github repo.

If you like my write up, follow me on Github, Linkedin, and/or Medium profile.

Deep learning series

  1. Deep learning series 1- Intro to deep learning
  2. Deep learning series 2 — simple image classification using deep learning

Reference

  1. Udacity’s self-driving car engineer nanodegree

--

--