text: # Model.py
  # Sample data location /opt/carnd_p3/data/
  # https://d17h27t6h515a5.cloudfront.net/topher/2016/December/584f6edd_data/data.zipunzip 
  
  import os
  import csv
  from scipy import ndimage
  import numpy as np
  import sklearn
  
  # Importing training data 
  PTH_CSV = "data/driving_log.csv"
  PTH_IMG = "data/IMG/"
  #PTH_CSV = "../data_bhvr_cln/driving_log.csv"
  #PTH_IMG = "../data_bhvr_cln/IMG/"
  
  lines=[]
  
  with open(PTH_CSV) as csvfile:
      has_header = csv.Sniffer().has_header(csvfile.read(1024))
      csvfile.seek(0)  # Rewind.
      reader = csv.reader(csvfile)
      if has_header:
          next(reader)  # Skip header row.
      for line in reader:
          lines.append(line)
  #print("Total length of data : {}".format(len(lines)))
  
  from sklearn.model_selection import train_test_split
  from sklearn.utils import shuffle
  train_lines, validation_lines = train_test_split(lines, test_size=0.2)
  
  # http://jeffwen.com/2017/07/14/behavioral_cloning
  
  # Distibuting data to correct the imabalance
  # We shall reduce the data points with zero as the steering angle and increase the other data points 
  # which are in minority.
  # After many trails we rsettle at minimum 80 and maximum 100 for the training data set.
  def  balance_data(data,min_reqd,max_reqd):
      data_output = data.copy()
      
      ## create histogram to know what needs to be added
      steering_angles = np.asarray(data_output[:,3], dtype='float')
      #print(len(np.unique(steering_angles)))
      num_hist, index_hist = np.histogram(steering_angles, np.unique(steering_angles))
      #print(len(num_hist))
      #print(len(index_hist))
      to_be_added = np.empty([1,7])
      to_be_deleted = np.empty([1,1])
      
      for i in range(1, len(num_hist)):
          if num_hist[i-1]<min_reqd:
  
              ## find the index where values fall within the range 
              match_index = np.where((steering_angles>=index_hist[i-1]) & (steering_angles<index_hist[i]))[0]
  
              ## randomly choose up to the minimum needed
              need_to_add = data_output[np.random.choice(match_index,min_reqd-num_hist[i-1]),:]
              
              to_be_added = np.vstack((to_be_added, need_to_add))
  
          elif num_hist[i-1]>max_reqd:
              
              ## find the index where values fall within the range 
              match_index = np.where((steering_angles>=index_hist[i-1]) & (steering_angles<index_hist[i]))[0]
              
              ## randomly choose up to the minimum needed
              to_be_deleted = np.append(to_be_deleted, np.random.choice(match_index,num_hist[i-1]-max_reqd))
  
          
  
      ## delete the randomly selected observations that are overrepresented and append the underrepresented ones
      data_output = np.delete(data_output, to_be_deleted, 0)
      data_output = np.vstack((data_output, to_be_added[1:,:]))
      
      return data_output
  
  # Balance Training data
  train_lines_balanced = balance_data(np.array(train_lines),80,100)
  
  # Checking 
  #print("Total length of balanced Training data : {}".format(len(train_lines_balanced)))
  #print("Total length of balanced Validation data : {}".format(len(validation_lines_balanced)))
  
  # The below generator caused the Udacity GPU to slow down and we lost many hours...
  # def generator(samples, batch_size=32):
  #     num_samples = len(samples)
  #     while 1: # Loop forever so the generator never terminates
  #         shuffle(samples)
  #         for offset in range(0, num_samples, batch_size):
  #             batch_samples = samples[offset:offset+batch_size]
  
  #             images = []
  #             measurements = []
  
  #             for batch_sample in batch_samples:
  #                 #print(batch_sample)
  #                 src_path = batch_sample[0]
  #                 #print(src_path)
  #                 f_name = src_path.split('/')[-1]
  #                 #print(f_name)
  #                 #break
  #                 current_path = PTH_IMG +f_name
  #                 image = ndimage.imread(current_path)
  #                 # Appending original image
  #                 images.append(image)
  #                 measurement = float(line[3])
  #                 measurements.append(measurement)
  #                 #appending flipped image
  #                 images.append(np.fliplr(image))
  #                 measurements.append(-measurement)
  #                 #appending left camera image and steering angle with offset
  #                 src_path = batch_sample[1]
  #                 f_name = src_path.split('/')[-1]
  #                 current_path = PTH_IMG +f_name
  #                 image = ndimage.imread(current_path)
  #                 images.append(image)
  #                 measurements.append(measurement+0.25)
  #                 # appending right camera image and steering angle with offset
  #                 src_path = batch_sample[2]
  #                 f_name = src_path.split('/')[-1]
  #                 current_path = PTH_IMG +f_name
  #                 image = ndimage.imread(current_path)
  #                 images.append(image)
  #                 measurements.append(measurement-0.25)
  
  #             # trim image to only see section with road
  #             X_train = np.array(images)
  #             y_train = np.array(measurements)
  #             yield sklearn.utils.shuffle(X_train, y_train)
  
  # train_generator = generator(train_lines_balanced, batch_size=40)
  # validation_generator = generator(validation_lines, batch_size=40)
  
  # The function to augment the sample data 
  # Offset = 0.4 tunable parameter value reached after many trials
  
  def  generate_data(lines):
      images = []
      measurements = []
  
      for line in lines:
          #print(line)
          src_path = line[0]
          #print(src_path)
          f_name = src_path.split('/')[-1]
          #print(f_name)
          #break
          current_path = PTH_IMG +f_name
          image = ndimage.imread(current_path)
          # Appending original image
          images.append(image)
          measurement = float(line[3])
          measurements.append(measurement)
          #appending flipped image
          images.append(np.fliplr(image))
          measurements.append(-measurement)
          #appending left camera image and steering angle with offset
          src_path = line[1]
          f_name = src_path.split('/')[-1]
          current_path = PTH_IMG +f_name
          image = ndimage.imread(current_path)
          images.append(image)
          measurements.append(measurement+0.4)
          # appending right camera image and steering angle with offset
          src_path = line[2]
          f_name = src_path.split('/')[-1]
          current_path = PTH_IMG +f_name
          image = ndimage.imread(current_path)
          images.append(image)
          measurements.append(measurement-0.4)
  
      
      X_data = np.array(images)
      y_data = np.array(measurements)
      return X_data,y_data
  
  X_train,y_train = generate_data(train_lines_balanced)
  X_valid,y_valid = generate_data(validation_lines)
  
  
  # Creating the model
  
  from keras.models import Sequential, Model
  from keras.layers import Cropping2D,Lambda
  from keras.layers import Dense,Flatten,Dropout,MaxPooling2D
  from keras.layers.convolutional import Conv2D
  from keras.callbacks import ModelCheckpoint
  
  
  
  
  # Adding teh original Nvidia  model to train on the sample data for completing track 1
  
  model = Sequential()
  # trim image to only see section with road
  # Did not help much
  #model.add(Cropping2D(cropping=((50,20), (0,0))))
  
  # preprocessing 
  model.add(Lambda(lambda x: x/127.5-1.0, input_shape=(160,320,3)))
  
  
  model.add(Conv2D(24, 5, 5, activation='elu', subsample=(2, 2)))
  model.add(Conv2D(36, 5, 5, activation='elu', subsample=(2, 2)))
  model.add(Conv2D(48, 5, 5, activation='elu', subsample=(2, 2)))
  model.add(Conv2D(64, 3, 3, activation='elu'))
  model.add(Conv2D(64, 3, 3, activation='elu'))
  model.add(Dropout(0.5))
  model.add(Flatten())
  model.add(Dense(100, activation='elu'))
  model.add(Dense(50, activation='elu'))
  model.add(Dense(10, activation='elu'))
  model.add(Dense(1))
  
  # Adding checkpoint
  
  checkpoint = ModelCheckpoint('model-{epoch:03d}.h5',
                                   monitor='val_loss',
                                   verbose=0,
                                   save_best_only=True,
                                   mode='auto')
  
  model.compile(loss='mse',optimizer='adam')
  
  # model.fit_generator(train_generator, steps_per_epoch=len(train_lines_balanced)*1,\
  #                     validation_data=validation_generator,\
  #                     validation_steps=len(validation_lines)*1, epochs=1,callbacks=[checkpoint])
  
  model.fit(X_train, y_train, validation_data=(X_valid,y_valid), epochs=2, batch_size=40,callbacks=[checkpoint])
  
  # Saving the model
  
  model.save('model.h5')
  