diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7d4154 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Event-based Vision meets Deep Learning on Steering Prediction for Self-driving Cars + +This repository contains a deep learning approach to unlock the potential of event cameras on the prediction of a vehicles's steering angle. + +#### Citing + +If you use this code in an academic context, please cite the following publication: + +Paper: [Event-based vision meets deep learning on steering prediction for self-driving cars](http://rpg.ifi.uzh.ch/docs/CVPR18_Maqueda.pdf) + +Video: [YouTube](https://www.youtube.com/watch?v=_r_bsjkJTHA&feature=youtu.be) + +``` +@inproceedings{maqueda_2018, + title={Event-based vision meets deep learning on steering prediction for self-driving cars}, + author={Maqueda, Ana I and Loquercio, Antonio and Gallego, Guillermo and Garc{\i}a, Narciso and Scaramuzza, Davide}, + booktitle={Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition}, + pages={5419--5427}, + year={2018} +} + +``` + +## Introduction + +Steering angle prediction with standard cameras is not robust to scenes characterized by high dynamic range (HDR), motion blur, and low light. Event cameras, however, are bioinspired sensors that are able to solve all three problems at once. They output a stream of asynchronous events that are generated by moving edges in the scene. Their natural response to motion, and their advantages over traditional cameras (very high temporal resolution, very high dynamic range, and low latency) make them a perfect fit for the steering prediction task, which is addressed by a DL-based solution from a regression viewpoint. + + +### Model + +A series of ResNet architectures, i.e., ResNet18 and ResNet50, have been deployed to carry out the steering prediction task. They are used as feature extractors, considering only their convolutional layers. Next, a global average pooling (GAP) layer is used to encode the image features into a vectorized descriptor that feeds a fully-connected (FC) layer (256-dimensional for ResNet18 and 1024-dimensional for ResNet50). This FC layer is followed by a ReLU non-linearity, and the final 1-dimensional FC layer to output the predicted steering angle. + +![architecture](images/architecture.png) + + +### Data + +In order to learn steering angles from event images, the publicly available [DAVIS Driving Dataset 2017 (DDD17)](https://docs.google.com/document/d/1HM0CSmjO8nOpUeTvmPjopcBcVCk7KXvLUuiZFS6TWSg/pub) has been used. It provides approximately 12 hours of annotated driving recordings collected by a car under different road, weather, and illumination conditions. The dataset includes asynchronous events as well as synchronous grayscale frames. + +![architecture](images/input_data.png) + + +## Running the code + +### Software requirements + +This code has been tested on Ubuntu 14.04, and on Python 3.4. + +Dependencies: +- Tensorflow +- Keras 2.1.4 +- NumPy +- OpenCV +- scikit-learn +- Python gflags + + +### Data preparation + +Please follow the instructions from the [DDD17 site](https://docs.google.com/document/d/1HM0CSmjO8nOpUeTvmPjopcBcVCk7KXvLUuiZFS6TWSg/pub), to download the dataset and visualize the HDF5 file contents. After that, you should get the following structure: + +``` +DDD17/ + run1_test/ + run2/ + run3/ + run4/ + run5/ +``` + +Authors also provide some [code](https://code.ini.uzh.ch/jbinas/ddd17-utils) for viewing and exporting the data. Download the repository and copy the files within the ```data_preprocessing``` directory. + +Asynchronous events are converted into synchronous event frames by pixel-wise accumulation over a constant time interval, using separate channels for positive and negative events. To prepare the data in the format required by our implementation, follow these steps: + + +#### 1. Accumulate events + +Run ```data_preprocessing/reduce_to_frame_based.py``` to reduce data to frame-based representation. The output is another HDF5 file, containing the frame-based data as a result of accumulating the events every other ```binsize``` seconds. The created HDF5 file will contain two new fields: +- **dvs_frame**: event frames (a 4-tensor, with number_of_frames x width x height x 2 elements). +- **aps_frame**: grayscales frames (a 3-tensor, with number_of_frames x width x height). + +``` +python data_preprocessing/reduce_to_frame_based.py --binsize 0.050 --update_prog_every 10 --keep_events 1 --source_folder ../DDD17 --dest_folder ../DDD17/DAVIS_50ms +``` + +Note: the ```reduce_to_frame_based.py``` script is the original ```export.py``` provided by the authors, which has been modified in order to compute several HDF5 files from a source directory, and save positive and negative event frames by separately. + + + +#### 2. Split recordings + +Run ```data_preprocessing/split_recordings.py``` to split the recordings into consecutive and non-overlapping short sequences of a few seconds each. Subsets of these sequences are used for training and testing, respectively. In particular, we set training sequences to 40 sec, and testing sequences to 20 sec. + +``` +python data_preprocessing/split_recordings.py --source_folder ./DDD17/DAVIS_50ms --rewrite 1 --train_per 40 --test_per 20 +``` + +Note: the ```split_recordings.py``` is the original ```prepare_cnn_data.py``` provided by the authors, which has been modified in order to compute several HDF5 files from a source directory, and avoid frame pre-processing. + + + +#### 3. Compute percentiles + +Run ```data_preprocessing/compute_percentiles.py``` to compute some percentiles from DVS/event frames in order to remove outliers, and normalize them. + +``` +python data_preprocessing/compute_percentiles.py --source_folder ./DDD17/DAVIS_50ms --inf_pos_percentile 0.0 --sup_pos_percentile 0.9998 --inf_neg_percentile 0.0 --sup_neg_percentile 0.9998 +``` + + +#### 4. Export CNN data + +Run ```data_preprocessing/export_cnn_data.py``` to export DVS/event frames, APS/grayscale frames, difference of grayscale frames (APS diff) in PNG format, and text files with steering angles form the HDF5 files to be used by the network. + +``` +python export_cnn_data.py --source_folder ./DDD17/DAVIS_50ms +``` diff --git a/cnn.py b/cnn.py new file mode 100644 index 0000000..3933b3d --- /dev/null +++ b/cnn.py @@ -0,0 +1,204 @@ +import tensorflow as tf +import numpy as np +import os +import sys +import gflags + +from keras.callbacks import ModelCheckpoint +from keras import backend as K +import keras + +import logz +import cnn_models +import utils +import log_utils +from common_flags import FLAGS +from constants import TRAIN_PHASE + + + +def getModel(img_width, img_height, img_channels, output_dim, weights_path): + """ + Initialize model. + + # Arguments + img_width: Target image widht. + img_height: Target image height. + img_channels: Target image channels. + output_dim: Dimension of model output. + weights_path: Path to pre-trained model. + + # Returns + model: A Model instance. + """ + if FLAGS.imagenet_init: + model = cnn_models.resnet50(img_width, + img_height, img_channels, output_dim) + else: + model = cnn_models.resnet50_random_init(img_width, + img_height, img_channels, output_dim) + + + if weights_path: + #try: + model.load_weights(weights_path) + print("Loaded model from {}".format(weights_path)) + #except: + # print("Impossible to find weight path. Returning untrained model") + + return model + + +def trainModel(train_data_generator, val_data_generator, model, initial_epoch): + """ + Model training. + + # Arguments + train_data_generator: Training data generated batch by batch. + val_data_generator: Validation data generated batch by batch. + model: Target image channels. + initial_epoch: Dimension of model output. + """ + + # Initialize number of samples for hard-mining + model.k_mse = tf.Variable(FLAGS.batch_size, trainable=False, name='k_mse', dtype=tf.int32) + + # Configure training process + optimizer = keras.optimizers.Adam(lr=FLAGS.initial_lr, decay=1e-4) + model.compile(loss=[utils.hard_mining_mse(model.k_mse)], optimizer=optimizer, + metrics=[utils.steering_loss, utils.pred_std]) + + # Save model with the lowest validation loss + weights_path = os.path.join(FLAGS.experiment_rootdir, 'weights_{epoch:03d}.h5') + writeBestModel = ModelCheckpoint(filepath=weights_path, monitor='val_steering_loss', + save_best_only=True, save_weights_only=True) + + # Save model every 'log_rate' epochs. + # Save training and validation losses. + logz.configure_output_dir(FLAGS.experiment_rootdir) + saveModelAndLoss = log_utils.MyCallback(filepath=FLAGS.experiment_rootdir, + period=FLAGS.log_rate, + batch_size=FLAGS.batch_size, + factor=FLAGS.lr_scale_factor) + + # Train model + steps_per_epoch = np.minimum(int(np.ceil( + train_data_generator.samples / FLAGS.batch_size)), 2000) + validation_steps = int(np.ceil(val_data_generator.samples / FLAGS.batch_size))-1 + + model.fit_generator(train_data_generator, + epochs=FLAGS.epochs, steps_per_epoch = steps_per_epoch, + callbacks=[writeBestModel, saveModelAndLoss], + validation_data=val_data_generator, + validation_steps = validation_steps, + initial_epoch=initial_epoch) + + +def _main(): + + # Set random seed + if FLAGS.random_seed: + seed = np.random.randint(0,2*31-1) + else: + seed = 5 + np.random.seed(seed) + tf.set_random_seed(seed) + + K.set_learning_phase(TRAIN_PHASE) + + # Create the experiment rootdir if not already there + if not os.path.exists(FLAGS.experiment_rootdir): + os.makedirs(FLAGS.experiment_rootdir) + + # Input image dimensions + img_width, img_height = FLAGS.img_width, FLAGS.img_height + + # Cropped image dimensions + crop_img_width, crop_img_height = FLAGS.crop_img_width, FLAGS.crop_img_height + + # Output dimension (one for steering) + output_dim = 1 + + # Input image channels + # - DVS frames: 2 channels (first one for positive even, second one for negative events) + # - APS frames: 1 channel (grayscale images) + # - APS DIFF frames: 1 channel (log(I_1) - log(I_0)) + if FLAGS.frame_mode == 'dvs': + img_channels = 3 + else: + img_channels = 3 + + + # Generate training data with real-time augmentation + if FLAGS.frame_mode == 'dvs': + train_datagen = utils.DroneDataGenerator() + elif FLAGS.frame_mode == 'aps': + train_datagen = utils.DroneDataGenerator(rotation_range = 0.2, + rescale = 1./255, + width_shift_range = 0.2, + height_shift_range=0.2) + else: + train_datagen = utils.DroneDataGenerator(rotation_range = 0.2, + width_shift_range = 0.2, + height_shift_range=0.2) + + train_generator = train_datagen.flow_from_directory(FLAGS.train_dir, + is_training=True, + shuffle = True, + frame_mode = FLAGS.frame_mode, + target_size=(img_height, img_width), + crop_size=(crop_img_height, crop_img_width), + batch_size = FLAGS.batch_size) + + # Generate validation data with real-time augmentation + if FLAGS.frame_mode == 'dvs' or FLAGS.frame_mode == 'aps_diff': + val_datagen = utils.DroneDataGenerator() + else: + val_datagen = utils.DroneDataGenerator(rescale = 1./255) + + val_generator = val_datagen.flow_from_directory(FLAGS.val_dir, + shuffle = False, + frame_mode = FLAGS.frame_mode, + target_size=(img_height, img_width), + crop_size=(crop_img_height, crop_img_width), + batch_size = FLAGS.batch_size) + # output dim + assert train_generator.output_dim == val_generator.output_dim, \ + " Not macthing output dimensions." + output_dim = train_generator.output_dim + + # Weights to restore + weights_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.weights_fname) + initial_epoch = 0 + if not FLAGS.restore_model: + # In this case weights will start from random + weights_path = None + else: + # In this case weigths will start from the specified model + initial_epoch = FLAGS.initial_epoch + + # Define model + model = getModel(img_width, img_height, img_channels, + output_dim, weights_path) + + # Serialize model into json + json_model_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.json_model_fname) + utils.modelToJson(model, json_model_path) + + # Train model + trainModel(train_generator, val_generator, model, initial_epoch) + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/cnn_models.py b/cnn_models.py new file mode 100644 index 0000000..7aa9276 --- /dev/null +++ b/cnn_models.py @@ -0,0 +1,321 @@ +import keras +from keras.models import Model, Sequential +from keras.layers import Dense, Dropout, Activation, Flatten, Input +from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D +from keras.layers.merge import add +from keras import regularizers +from keras.applications import ResNet50, VGG16 + +regular_constant=0 + + +def resnet8(img_width, img_height, img_channels, output_dim): + """ + Define model architecture. + + # Arguments + img_width: Target image widht. + img_height: Target image height. + img_channels: Target image channels. + output_dim: Dimension of model output. + + # Returns + model: A Model instance. + """ + + # Input + img_input = Input(shape=(img_height, img_width, img_channels)) + + x1 = Conv2D(32, (5, 5), strides=[2,2], padding='same')(img_input) + x1 = MaxPooling2D(pool_size=(3, 3), strides=[2,2])(x1) + + # First residual block + x2 = keras.layers.normalization.BatchNormalization()(x1) + x2 = Activation('relu')(x2) + x2 = Conv2D(32, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-5))(x2) + + x2 = keras.layers.normalization.BatchNormalization()(x2) + x2 = Activation('relu')(x2) + x2 = Conv2D(32, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-5))(x2) + + x1 = Conv2D(32, (1, 1), strides=[2,2], padding='same')(x1) + x3 = add([x1, x2]) + + # Second residual block + x4 = keras.layers.normalization.BatchNormalization()(x3) + x4 = Activation('relu')(x4) + x4 = Conv2D(64, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-4))(x4) + + x4 = keras.layers.normalization.BatchNormalization()(x4) + x4 = Activation('relu')(x4) + x4 = Conv2D(64, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-4))(x4) + + x3 = Conv2D(64, (1, 1), strides=[2,2], padding='same')(x3) + x5 = add([x3, x4]) + + # Third residual block + x6 = keras.layers.normalization.BatchNormalization()(x5) + x6 = Activation('relu')(x6) + x6 = Conv2D(128, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-4))(x6) + + x6 = keras.layers.normalization.BatchNormalization()(x6) + x6 = Activation('relu')(x6) + x6 = Conv2D(128, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(1e-4))(x6) + + x5 = Conv2D(128, (1, 1), strides=[2,2], padding='same')(x5) + x7 = add([x5, x6]) + + x = Flatten()(x7) + x = Activation('relu')(x) + x = Dropout(0.5)(x) + x = Dense(output_dim)(x) + + # Define steering-collision model + model = Model(inputs=[img_input], outputs=[x]) + #print(model.summary()) + + return model + + + +def resnet50(img_width, img_height, img_channels, output_dim): + + img_input = Input(shape=(img_height, img_width, img_channels)) + + base_model = ResNet50(input_tensor=img_input, + weights='imagenet', include_top=False) + x = base_model.output + x = GlobalAveragePooling2D()(x) + + x = Dense(1024, activation='relu')(x) + + # Steering channel + output = Dense(output_dim)(x) + + model = Model(inputs=[img_input], outputs=[output]) + #print(model.summary()) + + return model + + +def resnet50_random_init(img_width, img_height, img_channels, output_dim): + + img_input = Input(shape=(img_height, img_width, img_channels)) + + base_model = ResNet50(input_tensor=img_input, + weights=None, include_top=False) + x = base_model.output + x = GlobalAveragePooling2D()(x) + + x = Dense(1024, activation='relu')(x) + + # Steering channel + output = Dense(output_dim)(x) + + model = Model(inputs=[img_input], outputs=[output]) + #print(model.summary()) + + return model + + +def resnet18(img_width, img_height, img_channels, output_dim): + """ + Define model architecture. + + # Arguments + img_width: Target image widht. + img_height: Target image height. + img_channels: Target image channels. + output_dim: Dimension of model output. + + # Returns + model: A Model instance. + """ + + # Input + img_input = Input(shape=(img_height, img_width, img_channels)) + + x1 = Conv2D(32, (5, 5), strides=[2,2], padding='same')(img_input) + x1 = MaxPooling2D(pool_size=(3, 3), strides=[2,2])(x1) + + # First residual block + x2 = keras.layers.normalization.BatchNormalization()(x1) + x2 = Activation('relu')(x2) + x2 = Conv2D(32, (3, 3), strides=[1,1], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x2) + + x2 = keras.layers.normalization.BatchNormalization()(x2) + x2 = Activation('relu')(x2) + x2 = Conv2D(32, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x2) + + x3 = add([x1, x2]) + + # Second residual block + x4 = keras.layers.normalization.BatchNormalization()(x3) + x4 = Activation('relu')(x4) + x4 = Conv2D(64, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x4) + + x4 = keras.layers.normalization.BatchNormalization()(x4) + x4 = Activation('relu')(x4) + x4 = Conv2D(64, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x4) + + x3 = Conv2D(64, (1, 1), strides=[2,2], padding='same')(x3) + x5 = add([x3, x4]) + + # Third residual block + x6 = keras.layers.normalization.BatchNormalization()(x5) + x6 = Activation('relu')(x6) + x6 = Conv2D(64, (3, 3), strides=[1,1], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x6) + + x6 = keras.layers.normalization.BatchNormalization()(x6) + x6 = Activation('relu')(x6) + x6 = Conv2D(64, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x6) + + x7 = add([x5, x6]) + + # Fourth residual block + x8 = keras.layers.normalization.BatchNormalization()(x7) + x8 = Activation('relu')(x8) + x8 = Conv2D(128, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x8) + + x8 = keras.layers.normalization.BatchNormalization()(x8) + x8 = Activation('relu')(x8) + x8 = Conv2D(128, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x8) + + x7 = Conv2D(128, (1, 1), strides=[2,2], padding='same')(x7) + x9 = add([x7, x8]) + + # Fifth residual block + x10 = keras.layers.normalization.BatchNormalization()(x9) + x10 = Activation('relu')(x10) + x10 = Conv2D(128, (3, 3), strides=[1,1], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x10) + + x10 = keras.layers.normalization.BatchNormalization()(x10) + x10 = Activation('relu')(x10) + x10 = Conv2D(128, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x10) + + x11 = add([x9, x10]) + + # Sixth residual block + x12 = keras.layers.normalization.BatchNormalization()(x11) + x12 = Activation('relu')(x12) + x12 = Conv2D(256, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x12) + + x12 = keras.layers.normalization.BatchNormalization()(x12) + x12 = Activation('relu')(x12) + x12 = Conv2D(256, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x12) + + x11 = Conv2D(256, (1, 1), strides=[2,2], padding='same')(x11) + x13 = add([x11, x12]) + + # Seventh residual block + x14 = keras.layers.normalization.BatchNormalization()(x13) + x14 = Activation('relu')(x14) + x14 = Conv2D(256, (3, 3), strides=[1,1], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x14) + + x14 = keras.layers.normalization.BatchNormalization()(x14) + x14 = Activation('relu')(x14) + x14 = Conv2D(256, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x14) + + x15 = add([x13, x14]) + + # Eigth residual block + x16 = keras.layers.normalization.BatchNormalization()(x15) + x16 = Activation('relu')(x16) + x16 = Conv2D(512, (3, 3), strides=[2,2], padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x16) + + x16 = keras.layers.normalization.BatchNormalization()(x16) + x16 = Activation('relu')(x16) + x16 = Conv2D(512, (3, 3), padding='same', + kernel_initializer="he_normal", + kernel_regularizer=regularizers.l2(regular_constant))(x16) + + x15 = Conv2D(512, (1, 1), strides=[2,2], padding='same')(x15) + x17 = add([x15, x16]) + + x = GlobalAveragePooling2D()(x17) + + x = Dense(256)(x) + x = Activation('relu')(x) + x = Dropout(0.5)(x) + x = Dense(output_dim)(x) + + # Define steering-collision model + model = Model(inputs=[img_input], outputs=[x]) + print(model.summary()) + + return model + + +def nvidia_net(img_width, img_height, img_channels, output_dim): + img_input = Input(shape=(img_height, img_width, img_channels)) + + x = Conv2D(24, (5,5), strides=[2,2], padding='same')(img_input) + x = Activation('relu')(x) + + x = Conv2D(36, (5,5), strides=[2,2], padding='same')(x) + x = Activation('relu')(x) + + x = Conv2D(48, (5,5), strides=[2,2], padding='same')(x) + x = Activation('relu')(x) + + x = Conv2D(64, (3,3), strides=[1,1], padding='same')(x) + x = Activation('relu')(x) + + x = Conv2D(64, (3,3), strides=[1,1], padding='same')(x) + x = Activation('relu')(x) + + x = Flatten()(x) + x = Dense(100)(x) + x = Dense(50)(x) + x = Dense(10)(x) + x = Dense(output_dim)(x) + + # Define steering-collision model + model = Model(inputs=[img_input], outputs=[x]) + print(model.summary()) + + return model + + diff --git a/common_flags.py b/common_flags.py new file mode 100644 index 0000000..e706da9 --- /dev/null +++ b/common_flags.py @@ -0,0 +1,53 @@ +import gflags + + + +FLAGS = gflags.FLAGS + +# Random seed +gflags.DEFINE_bool('random_seed', True, 'Random seed') + +# Input +gflags.DEFINE_integer('img_width', 200, 'Target Image Width') +gflags.DEFINE_integer('img_height', 200, 'Target Image Height') + +gflags.DEFINE_integer('crop_img_width', 200, 'Cropped image widht') +gflags.DEFINE_integer('crop_img_height', 200, 'Cropped image height') + +gflags.DEFINE_string('frame_mode', "dvs", 'Load mode for images, either ' + 'dvs, aps or aps_diff') +gflags.DEFINE_string('visual_mode', "red_blue", 'Mode for video visualization, either ' + 'red_blue or grayscale') + +# Training +gflags.DEFINE_integer('batch_size', 32, 'Batch size in training and evaluation') +gflags.DEFINE_integer('epochs', 100, 'Number of epochs for training') +gflags.DEFINE_integer('log_rate', 10, 'Logging rate for full model (epochs)') +gflags.DEFINE_integer('initial_epoch', 0, 'Initial epoch to start training') +gflags.DEFINE_float('initial_lr', 1e-4, 'Initial learning rate for adam') +gflags.DEFINE_float('lr_scale_factor', 0.5, 'Reducer factor for learning rate ' + 'when loss stagnates.') +gflags.DEFINE_bool('imagenet_init', True, 'Initialization from imagenet weights') + +# Files +gflags.DEFINE_string('experiment_rootdir', "./model", 'Folder ' + ' containing all the logs, model weights and results') +gflags.DEFINE_string('train_dir', "../training", 'Folder containing' + ' training experiments') +gflags.DEFINE_string('val_dir', "../validation", 'Folder containing' + ' validation experiments') +gflags.DEFINE_string('test_dir', "../testing", 'Folder containing' + ' testing experiments') +gflags.DEFINE_string('video_dir', "../video_1", 'Folder containing' + ' only one experiment to be processed') +gflags.DEFINE_string('exp_name', "exp_1", 'Name of the experiment' + ' to be processed') + +# Model +gflags.DEFINE_bool('restore_model', False, 'Whether to restore a trained' + ' model for training') +gflags.DEFINE_string('weights_fname', "model_weights.h5", '(Relative) ' + 'filename of model weights') +gflags.DEFINE_string('json_model_fname', "model_struct.json", + 'Model struct json serialization, filename') + diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..beefa19 --- /dev/null +++ b/constants.py @@ -0,0 +1,2 @@ +TEST_PHASE = 0 +TRAIN_PHASE = 1 \ No newline at end of file diff --git a/data_preprocessing/compute_percentiles.py b/data_preprocessing/compute_percentiles.py new file mode 100644 index 0000000..87bde48 --- /dev/null +++ b/data_preprocessing/compute_percentiles.py @@ -0,0 +1,122 @@ +''' + +Compute some percentiles, in particular the median, an inferior percentile, and a superior percentile, from DVS data +in order to remove outliers and normalize event frames. These values are saved in a txt file. + +Plot the histogram of positive and negative events. + +''' + + +import h5py +import numpy as np +import os +import argparse +import glob +import collections +import math +import matplotlib.pyplot as plt +from keras.utils.generic_utils import Progbar + + +def compute_percentiles(all_events, inf, sup): + """ + Compute the median, and an inferior and a superior percentiles defined by inf and sup, respectively. + """ + # Accumulated sum + prior_sum = np.cumsum(all_events[:, 1]) + + # Total number of events + n_values = prior_sum[-1] + + # Position of the percentiles in the accumulated sum + median_pos = math.ceil(0.5*n_values) + inf_pos = math.ceil(inf*n_values) + sup_pos = math.ceil(sup*n_values) + + # Index of the percentiles in the counter + idx_median = np.array(np.where((prior_sum >= median_pos) != False))[0][0] + idx_inf = np.array(np.where((prior_sum >= inf_pos) != False))[0][0] + idx_sup = np.array(np.where((prior_sum >= sup_pos) != False))[0][0] + + # Get the values from the counter + median = all_events[idx_median, 0] + p_inf = all_events[idx_inf, 0] + p_sup = all_events[idx_sup, 0] + + # Return median, inferior and superior percentiles + return median, p_inf, p_sup + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--source_folder', help='Path to frame-based hdf5 files.') + parser.add_argument('--inf_pos_percentile', help='Inferior percentile for positive events.') + parser.add_argument('--sup_pos_percentile', help='Superior percentile for positive events.') + parser.add_argument('--inf_neg_percentile', help='Inferior percentile for negative events.') + parser.add_argument('--sup_neg_percentile', help='Superior percentile for negative events.') + args = parser.parse_args() + + # Initialize counters for positive and negative events + all_pos_events = collections.Counter([]) + all_neg_events = collections.Counter([]) + + # For every recording/hdf5 file + recordings = glob.glob(args.source_folder + '/*.hdf5') + prog_bar = Progbar(target=len(recordings)) + j = 0 + for rec in recordings: + + # Get the data + f_in = h5py.File(rec, 'r') + for key in f_in.keys(): + key = str(key) + + # Read DVS frames + if key == 'dvs_frame': + dvs_frames = f_in[key].value + + # For every dvs frame in the recording + for i in range(dvs_frames.shape[0]): + pos_img = dvs_frames[i, :, :, 0] + pos_events = pos_img.flatten() + neg_img = dvs_frames[i, :, :, 1] + neg_events = neg_img.flatten() + + # Count positive and negative events + counter_pos = collections.Counter(pos_events[pos_events > 0]) + counter_neg = collections.Counter(neg_events[neg_events > 0]) + + # Update counters with events from new images + if i == 0: + all_pos_events = counter_pos + all_neg_events = counter_neg + else: + all_pos_events = all_pos_events + counter_pos + all_neg_events = all_neg_events + counter_neg + + f_in.close() + prog_bar.update(j) + j += 1 + + # Sort the counters according to the number of events (not frequency) + all_pos_events = np.array(sorted(all_pos_events.items())) + all_neg_events = np.array(sorted(all_neg_events.items())) + + # Plot histogram of positive and negative events + plt.hist(all_pos_events[:, 0], weights=all_pos_events[:, 1], bins=all_pos_events.shape[0], + alpha=0.5, label='Positive', color='b') + plt.hist(-1*all_neg_events[:, 0], weights=all_neg_events[:, 1], bins=all_neg_events.shape[0], + alpha=0.5, label='Negative', color='r') + plt.legend(fontsize=10) + plt.savefig(os.path.join(args.source_folder, 'events.png'), bbox_inches='tight') + + # Compute and save percentiles for positive and negative events + pos_median, pos_inf, pos_sup = compute_percentiles(all_pos_events, args.inf_pos_percentile, args.sup_pos_percentile) + neg_median, neg_inf, neg_sup = compute_percentiles(all_neg_events, args.inf_neg_percentile, args.sup_neg_percentile) + print("pos_median = {}, pos_inf = {}, pos_sup = {}," + "neg_median = {}, neg_inf = {}, neg_sup = {}".format(pos_median, pos_inf, pos_sup, neg_median, neg_inf, neg_sup)) + np.savetxt(os.path.join(args.source_folder, 'percentiles.txt'), + [pos_median, pos_inf, pos_sup, neg_median, neg_inf, neg_sup], + delimiter=',', header='pos_median, pos_inf, pos_sup, neg_median, neg_inf, neg_sup') + diff --git a/data_preprocessing/export_cnn_data.py b/data_preprocessing/export_cnn_data.py new file mode 100644 index 0000000..8839a04 --- /dev/null +++ b/data_preprocessing/export_cnn_data.py @@ -0,0 +1,143 @@ +''' +Export DVS frames, APS frames, APS diff frames (difference of grayscale frames), and steering angles to be used by +the networks. +''' + +import h5py +import numpy as np +import cv2 +import os +import argparse +import glob +from itertools import groupby +from operator import itemgetter + + +def split_sequence(data): + sequences = [] + for k, g in groupby(enumerate(data), lambda (i, x): i-x): + sequences.append(map(itemgetter(1), g)) + return sequences + + +def export_data(f_in, idxs, out_path, pos_inf, pos_sup, neg_inf, neg_sup): + + # Non-image data + data = np.zeros((len(idxs), 4)) + + for key in f_in.keys(): + key = str(key) + + # Export DVS frames + if key == 'dvs_frame': + dvs_path = os.path.join(out_path, 'dvs') + os.makedirs(dvs_path) + + images = f_in[key].value[idxs] + for i in range(images.shape[0]): + new_img = np.zeros((images.shape[1], images.shape[2], 3), dtype=np.uint8) + event_img = images[i] + + # Positive events to channel 0 + pos_img = event_img[:,:,0] + index_p = pos_img > 0 + pos_img[index_p] = np.clip(pos_img[index_p], pos_inf, pos_sup) + new_img[:,:,0] = pos_img + + # Negative events to channel 1 + neg_img = event_img[:,:,1] + index_n = neg_img > 0 + neg_img[index_n] = np.clip(neg_img[index_n], neg_inf, neg_sup) + new_img[:,:,-1] = neg_img + + # Save DVS frame + img_name = "frame_" + str(i).zfill(5) + ".png" + cv2.imwrite(os.path.join(dvs_path, img_name),new_img) + + # Export APS frames and APS diff frames (difference of grayscale frames) + elif key == 'aps_frame': + aps_path = os.path.join(out_path, 'aps') + os.makedirs(aps_path) + + aps_diff_path = os.path.join(out_path, 'aps_diff') + os.makedirs(aps_diff_path) + + images = f_in[key].value[idxs] + images = np.asarray(images, dtype = np.uint8) + for i in range(images.shape[0]): + + # Save APS frames + img_name = "frame_" + str(i).zfill(5) + ".png" + cv2.imwrite(os.path.join(aps_path, img_name),images[i,:,:]) + + # Save APS diff frames + if i > 0: + new_img = np.zeros((images.shape[1], images.shape[2], 3), dtype=np.uint8) + new_img[:,:,0] = images[i-1,:,:] + new_img[:,:,-1] = images[i,:,:] + cv2.imwrite(os.path.join(aps_diff_path, img_name),new_img) + + + # Steering, torque, engine speed, vehicle speed associated to DVS and APS frames + elif key == 'steering_wheel_angle': + steer = f_in[key].value[idxs] + data[:,0] = steer + elif key == 'torque_at_transmission': + torque = f_in[key].value[idxs] + data[:,1] = torque + elif key == 'engine_speed': + eng_speed = f_in[key].value[idxs] + data[:,2] = eng_speed + elif key == 'vehicle_speed': + veh_speed = f_in[key].value[idxs] + data[:,3] = veh_speed + + # Save steering angles + txt_name = os.path.join(out_path, 'sync_steering.txt') + np.savetxt(txt_name, data, delimiter=',', header='steering, torque, engine_velocity, vehicle_velocity') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--source_folder', help='Path to frame-based hdf5 files.') + args = parser.parse_args() + + # Load percentiles + try: + percentiles = np.loadtxt(os.path.join(args.source_folder, 'percentiles.txt'), usecols=0, skiprows=1) + except: + raise IOError("Percentiles file not found") + pos_inf = percentiles[1] # Inferior percentile for positive events + pos_sup = percentiles[2] # Superior percentile for positive events + neg_inf = percentiles[4] # Inferior percentile for negative events + neg_sup = percentiles[5] # Superior percentile for negative events + + # For every recording/hdf5 file + recordings = glob.glob(args.source_folder + '/*.hdf5') + for rec in recordings: + f_in = h5py.File(rec, 'r') + + # Name of the experiment + exp_name = rec.split('.')[-2] + exp_name = exp_name.split('/')[-1] + + # Get training sequences + train_idxs = np.ndarray.tolist(f_in['train_idxs'].value) + train_sequences = split_sequence(train_idxs) + for i, train_seq in enumerate(train_sequences): + output_path = os.path.join(args.source_folder, 'training', exp_name + str(i)) + if not os.path.exists(output_path): + os.makedirs(output_path) + export_data(f_in, train_seq, output_path, pos_inf, pos_sup, neg_inf, neg_sup) + + # Get testing sequences + test_idxs = np.ndarray.tolist(f_in['test_idxs'].value) + test_sequences = split_sequence(test_idxs) + for j, test_seq in enumerate(test_sequences): + output_path = os.path.join(args.source_folder, 'testing', exp_name + str(j)) + if not os.path.exists(output_path): + os.makedirs(output_path) + export_data(f_in, test_seq, output_path, pos_inf, pos_sup, neg_inf, neg_sup) + + f_in.close() + diff --git a/data_preprocessing/reduce_to_frame_based.py b/data_preprocessing/reduce_to_frame_based.py new file mode 100644 index 0000000..c5800cf --- /dev/null +++ b/data_preprocessing/reduce_to_frame_based.py @@ -0,0 +1,266 @@ +''' + +Original file: export.py +Author: J. Binas , 2017 + +This software is released under the +GNU LESSER GENERAL PUBLIC LICENSE Version 3. + +------------------------------------------------------------------------------- + +Modified in order to compute several hdf5 files from a directory, and save +positive and negative event frames by separately. + +'source_folder' and 'dest_folder' must be different to not overwrite the +original hdf5 files from DDD17. You could want to generate frame-based +data again but with a different time interval. + +------------------------------------------------------------------------------- + +Creates another hdf5 file containing frame-based data as a result of accumulating +the events every other binsize seconds. + +''' + + +from __future__ import print_function +import os, time, argparse +import Queue +import numpy as np +import glob +from copy import deepcopy +from view import HDF5Stream, MergedStream +from datasets import HDF5 +from interfaces.caer import DVS_SHAPE, unpack_data + + +export_data_vi = { + 'steering_wheel_angle', + 'brake_pedal_status', + 'accelerator_pedal_position', + 'engine_speed', + 'vehicle_speed', + 'windshield_wiper_status', + 'headlamp_status', + 'transmission_gear_position', + 'torque_at_transmission', + 'fuel_level', + 'high_beam_status', + 'ignition_status', + #'lateral_acceleration', + 'latitude', + 'longitude', + #'longitudinal_acceleration', + 'odometer', + 'parking_brake_status', + #'fine_odometer_since_restart', + 'fuel_consumed_since_restart', + } + +export_data_dvs = { + 'dvs_frame', + 'aps_frame', + } + +export_data = export_data_vi.union(export_data_dvs) + + +def filter_frame(d): + ''' + receives 8 bit frame, + needs to return unsigned 8 bit img + ''' + # add custom filters here... + # d['data'] = my_filter(d['data']) + frame8 = (d['data'] / 256).astype(np.uint8) + return frame8 + +def get_progress_bar(): + try: + from tqdm import tqdm + except ImportError: + print("\n\nNOTE: For an enhanced progress bar, try 'pip install tqdm'\n\n") + class pbar(): + position=0 + def close(self): pass + def update(self, increment): + self.position += increment + print('\r{}s done...'.format(self.position)), + def tqdm(*args, **kwargs): + return pbar() + return tqdm(total=(tstop-tstart)/1e6, unit_scale=True) + + +def raster_evts(data): + _histrange = [(0, v) for v in DVS_SHAPE] + pol_on = data[:,3] == 1 + pol_off = pol_on == False + # Positive-event frame + img_on, _, _ = np.histogram2d( + data[pol_on, 2], data[pol_on, 1], + bins=DVS_SHAPE, range=_histrange) + # Negative-event frame + img_off, _, _ = np.histogram2d( + data[pol_off, 2], data[pol_off, 1], + bins=DVS_SHAPE, range=_histrange) + img_on = np.expand_dims(img_on, axis=-1) + img_off = np.expand_dims(img_off, axis=-1) + # First channel of a DVS frame contains the positve events, and the second + # one contains the negative events. + dvs_img = np.concatenate((img_on, img_off), axis=-1) + return dvs_img.astype(np.int16) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--tstart', type=int, default=0, help='Starting time in seconds') + parser.add_argument('--tstop', type=int, help='Stopping time in seconds') + parser.add_argument('--binsize', type=float, default=0.1, help='If positive, events will be binned into time-slices' + 'of BINSIZE seconds. If negative, frames will be' + 'generated every abs(BINSIZE) events.') + parser.add_argument('--update_prog_every', type=float, default=0.01, help='Progress bar update interval (in' + 'percentage points).') + parser.add_argument('--export_aps', type=int, default=1, help='Whether to generate APS/grayscale frames') + parser.add_argument('--export_dvs', type=int, default=1, help='Whether to generate DVS/event frames') + parser.add_argument('--source_folder', help='Path to HDF5 files.') + parser.add_argument('--dest_folder', help='Path to where saving the new HDF5 files.') + args = parser.parse_args() + + recordings = glob.glob(args.source_folder + '/*') + + # For every recording/hdf5 file + for rec in recordings: + + f_in = HDF5Stream(rec, export_data_vi.union({'dvs'})) + m = MergedStream(f_in) + + fixed_dt = args.binsize > 0 + tstart = int(m.tmin + 1e6 * args.tstart) + tstop = m.tmin + 1e6 * args.tstop if args.tstop is not None else m.tmax + print(tstart, tstop) + m.search(tstart) + + print('recording duration', (m.tmax - m.tmin) * 1e-6, 's') + + # Create output file + dtypes = {k: float for k in export_data.union({'timestamp'})} + if args.export_aps: + dtypes['aps_frame'] = (np.uint8, DVS_SHAPE) + if args.export_dvs: + dtypes['dvs_frame'] = (np.int16, DVS_SHAPE+(2,)) + + if not os.path.exists(args.dest_folder): + os.makedirs(args.dest_folder) + outfile = os.path.join(args.dest_folder, rec.split('/')[-1]) + f_out = HDF5(outfile, dtypes, mode='w', chunksize=32, compression='gzip') + + current_row = {k: 0 for k in dtypes} + if args.export_aps: + current_row['aps_frame'] = np.zeros(DVS_SHAPE, dtype=np.uint8) + if args.export_dvs: + current_row['dvs_frame'] = np.zeros(DVS_SHAPE+(2,), dtype=np.int16) + + pbar = get_progress_bar() + sys_ts, t_pre, t_offset, ev_count, pbar_next = 0, 0, 0, 0, 0 + while m.has_data and sys_ts <= tstop*1e-6: + try: + sys_ts, d = m.get() + except Queue.Empty: + # Continue while waiting for queue to fill up + continue + if not d: + # Skip unused data + continue + if d['etype'] == 'special_event': + unpack_data(d) + if any(d['data'] == 0): + d['etype'] = 'timestamp_reset' + current_row['timestamp'] = d['timestamp'] + if d['etype'] == 'timestamp_reset': + print('ts reset detected, setting offset', current_row['timestamp']) + t_offset += current_row['timestamp'] + continue + if d['etype'] in export_data_vi: + current_row[d['etype']] = d['data'] + continue + if d['etype'] == 'frame_event' and args.export_aps: + if t_pre == 0: + print('resetting t_pre (current frame)') + t_pre = d['timestamp'] + t_offset + while fixed_dt and t_pre + args.binsize < d['timestamp'] + t_offset: + # SYSTEM Timestamp version: + current_row['timestamp'] = (sys_ts - tstart * 1e-6) + f_out.save(deepcopy(current_row)) + current_row['dvs_frame'][:,:,:] = 0 + current_row['timestamp'] = t_pre + t_pre += args.binsize + if not fixed_dt: + current_row['timestamp'] = d['timestamp'] + t_offset + current_row['aps_frame'] = filter_frame(unpack_data(d)) + current_row['timestamp'] = t_pre + continue + if d['etype'] == 'polarity_event' and args.export_dvs: + unpack_data(d) + times = d['data'][:,0] * 1e-6 + t_offset + num_evts = d['data'].shape[0] + if t_pre == 0: + print('resetting t_pre (current pol)') + t_pre = times[0] + offset = 0 + if fixed_dt: + # fixed time interval bin mode + num_samples = np.ceil((times[-1] - t_pre) / args.binsize) + for _ in xrange(int(num_samples)): + # take n events + n = (times[offset:] < t_pre + args.binsize).sum() + sel = slice(offset, offset + n) + current_row['dvs_frame'] += raster_evts(d['data'][sel]) + offset += n + # save if we're in the middle of a packet, otherwise + # wait for more data + if sel.stop < num_evts: + # SYSTEM Timestamp version: + current_row['timestamp'] = (sys_ts - tstart * 1e-6) + #current_row['timestamp'] = t_pre + f_out.save(deepcopy(current_row)) + current_row['dvs_frame'][:,:,:] = 0 + t_pre += args.binsize + else: + # fixed event count mode + num_samples = np.ceil(-float(num_evts + ev_count)/args.binsize) + for _ in xrange(int(num_samples)): + n = min(int(-args.binsize - ev_count), num_evts - offset) + sel = slice(offset, offset + n) + current_row['dvs_frame'] += raster_evts(d['data'][sel]) + if sel.stop > sel.start: + current_row['timestamp'] = times[sel].mean() + offset += n + ev_count += n + if ev_count == -args.binsize: + # SYSTEM Timestamp version: + current_row['timestamp'] = (sys_ts - tstart * 1e-6) + f_out.save(deepcopy(current_row)) + current_row['dvs_frame'][:,:,:] = 0 + ev_count = 0 + pbar_curr = int((sys_ts - tstart * 1e-6) / args.update_prog_every) + if pbar_curr > pbar_next: + pbar.update(args.update_prog_every) + pbar_next = pbar_curr + pbar.close() + print('[DEBUG] sys_ts/tstop', sys_ts, tstop*1e-6) + m.exit.set() + f_out.exit.set() + f_out.join() + while not m.done.is_set(): + print('[DEBUG] waiting for merger') + time.sleep(1) + print('[DEBUG] merger done') + f_in.join() + print('[DEBUG] stream joined') + m.join() + print('[DEBUG] merger joined') + filesize = os.path.getsize(outfile) + print('Finished. Wrote {:.1f}MiB to {}.'.format(filesize/1024**2, outfile)) + + time.sleep(1) + os._exit(0) diff --git a/data_preprocessing/split_recordings.py b/data_preprocessing/split_recordings.py new file mode 100644 index 0000000..cff35ea --- /dev/null +++ b/data_preprocessing/split_recordings.py @@ -0,0 +1,46 @@ +''' + +Original file: prepare_cnn_data.py + +------------------------------------------------------------------------------------------ + +Modified in order to: + a) compute several hdf5 files from a source directory. + b) avoid frame pre-processing. + +------------------------------------------------------------------------------------------ + +Splits the recordings into short sequences of a few seconds each. Subsets of these +sequences are used for training and testing, respectively. + +''' + +from __future__ import print_function +import h5py +import os, sys, argparse +import glob + +from hdf5_deeplearn_utils import build_train_test_split + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--source_folder', help='Path to frame-based hdf5 files.') + parser.add_argument('--rewrite', default=0, type=int, help='Whether to overwrite training and testing information.') + parser.add_argument('--train_length', default=5*60, type=float, help='Length of training sequences in seconds.') + parser.add_argument('--test_length', default=2*60, type=float, help='Length of testing sequences in seconds') + args = parser.parse_args() + + # For every recording/hdf5 file + recordings = glob.glob(args.source_folder + '/*.hdf5') + for rec in recordings: + # Get the data + dataset = h5py.File(rec, 'a') + + print('Calculating train/test split...') + sys.stdout.flush() + build_train_test_split(dataset, train_div=args.train_length, test_div=args.test_length, force=args.rewrite) + + print('Done. Preprocessing complete.') + filesize = os.path.getsize(rec) + print('Final size: {:.1f}MiB to {}.'.format(filesize/1024**2, rec)) diff --git a/evaluation.py b/evaluation.py new file mode 100644 index 0000000..fba84be --- /dev/null +++ b/evaluation.py @@ -0,0 +1,216 @@ +import gflags +import numpy as np +import os +import sys +import json +from unipath import Path + +from keras import backend as K +import tensorflow as tf + +import utils +from constants import TEST_PHASE +from common_flags import FLAGS + + +# Functions to evaluate steering prediction + +def explained_variance_1d(ypred,y): + """ + Var[ypred - y] / var[y]. + https://www.quora.com/What-is-the-meaning-proportion-of-variance-explained-in-linear-regression + """ + assert y.ndim == 1 and ypred.ndim == 1 + vary = np.var(y) + return np.nan if vary==0 else 1 - np.var(y-ypred)/vary + + +def compute_explained_variance(predictions, real_values): + """ + Computes the explained variance of prediction for each + steering and the average of them + """ + assert np.all(predictions.shape == real_values.shape) + ex_variance = explained_variance_1d(predictions, + real_values) + print("EVA = {}".format(ex_variance)) + return ex_variance + + +def compute_sq_residuals(predictions, real_values): + assert np.all(predictions.shape == real_values.shape) + sq_res = np.square(predictions - real_values) + sr = np.mean(sq_res, axis = -1) + print("MSE = {}".format(sr)) + return sq_res + + +def compute_rmse(predictions, real_values): + assert np.all(predictions.shape == real_values.shape) + mse = np.mean(np.square(predictions - real_values), axis=0) + rmse = np.sqrt(mse) + print("RMSE = {}".format(rmse)) + return rmse + + +def compute_highest_regression_errors(predictions, real_values): + """ + Compute the indexes with highest error + """ + n_errors = 5 + assert np.all(predictions.shape == real_values.shape) + sq_res = np.sqrt(np.square(predictions - real_values)) + highest_errors = np.sort(sq_res, axis=None)[-n_errors:] + print("=============") + print("Highest errors") + print(highest_errors) + print("=============") + return highest_errors + + +def random_regression_baseline(real_values): + mean = np.mean(real_values) + std = np.std(real_values) + return np.random.normal(loc=mean, scale=abs(std), size=real_values.shape) + + +def constant_baseline(real_values): + mean = np.mean(real_values) + return mean * np.ones_like(real_values) + + +def evaluate_regression(predictions, real_values): + results = {} + results['evas'] = compute_explained_variance(predictions, real_values) + results['rmse'] = compute_rmse(predictions, real_values).tolist() + results['h_error'] = compute_highest_regression_errors(predictions, real_values).tolist() + return results + + +def _main(): + + # Set testing mode (dropout/batchnormalization) + K.set_learning_phase(TEST_PHASE) + + seed = 5 + np.random.seed(seed) + tf.set_random_seed(seed) + + # Generate testing data + if FLAGS.frame_mode == 'dvs' or FLAGS.frame_mode == 'aps_diff': + test_datagen = utils.DroneDataGenerator() + else: + test_datagen = utils.DroneDataGenerator(rescale = 1./255) + + test_generator = test_datagen.flow_from_directory(FLAGS.test_dir, + shuffle=False, + frame_mode = FLAGS.frame_mode, + target_size=(FLAGS.img_height, FLAGS.img_width), + crop_size=(FLAGS.crop_img_height, FLAGS.crop_img_width), + batch_size = FLAGS.batch_size) + + # Load json and create model + json_model_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.json_model_fname) + model = utils.jsonToModel(json_model_path) + + # Load weights + weights_load_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.weights_fname) + try: + model.load_weights(weights_load_path) + print("Loaded model from {}".format(weights_load_path)) + except IOError as e: + print("Impossible to find weight path. Returning untrained model") + + # Compile model + model.compile(loss='mse', optimizer='sgd') + + ## Get predictions and ground truth + n_samples = test_generator.samples + nb_batches = int(np.ceil(n_samples / FLAGS.batch_size))-1 + + predictions, ground_truth = utils.compute_predictions_and_gt( + model, test_generator, nb_batches, verbose = 1) + + + print('----------------------------------') + print('Prediction std is {}'.format(np.std(predictions))) + print('----------------------------------') + + # Transformed predictions (network output) + u_dict = {'trasformed_predicted': predictions, + 'transfomed_constant': np.ones_like(ground_truth) * np.mean(ground_truth)} + + # Evaluate transformed predictions (won't be saved) + results_dict = {} + for name, pred in u_dict.items(): + print("------------------------") + print("Evaluating {}".format(name)) + evaluation = evaluate_regression(pred, ground_truth) + print("------------------------") + results_dict[name] = [evaluation] + + # Steering boundaries seen in data + json_dict_fname = os.path.join( + Path(os.path.realpath(FLAGS.test_dir)).parent, + 'scalers_dict.json') + + with open(json_dict_fname, 'r') as f: + scalers_dict = json.load(f) + + mins = np.array(scalers_dict['mins']) + maxs = np.array(scalers_dict['maxs']) + + # Range of the transformed data + min_bound = -1.0 + max_bound = 1.0 + + + # Undo transformation for predicitons (only for steering) + pred_std = (predictions - min_bound)/(max_bound - min_bound) + pred_steer = pred_std*(maxs - mins) + mins + #pred_steer = np.expand_dims(pred_steer, axis = -1) + + # Undo transformation for ground-truth (only for steering) + gt_std = (ground_truth - min_bound)/(max_bound - min_bound) + steer_gt = gt_std*(maxs - mins) + mins + #steer_gt = np.expand_dims(gt_steer, axis=-1) + + # Compute random and constant baselines for steerings + random_steerings = random_regression_baseline(steer_gt).ravel() + constant_steerings = constant_baseline(steer_gt).ravel() + + # Create dictionary of baselines + baseline_dict = {'predicted': pred_steer, + 'random': random_steerings, + 'constant': constant_steerings} + + # Evaluate detransformed predictions: EVA, residuals, and highest errors + results_dict = {} + for name, pred in baseline_dict.items(): + print("------------------------") + print("Evaluating {}".format(name)) + evaluation = evaluate_regression(pred, steer_gt) + print("------------------------") + results_dict[name] = [evaluation] + + utils.write_to_file(results_dict, os.path.join(FLAGS.experiment_rootdir, 'test_results.json')) + + # Write predicted and real steerings + steer_dict = {'pred_steerings': pred_steer.tolist(), + 'real_steerings': steer_gt.tolist()} + utils.write_to_file(steer_dict, os.path.join(FLAGS.experiment_rootdir, + 'predicted_and_real_steerings.json')) + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/hyperparam_selection/lr_sweep.sh b/hyperparam_selection/lr_sweep.sh new file mode 100755 index 0000000..ca23b8e --- /dev/null +++ b/hyperparam_selection/lr_sweep.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +TRAIN_DIR=/media/HHD/ana/dvs_steering_learning/DAVIS_small_new/training/ +VAL_DIR=/media/HHD/ana/dvs_steering_learning/DAVIS_small_new/validation/ +TEST_DIR=/media/HHD/ana/dvs_steering_learning/DAVIS_small_new/testing/ + +N_exp=5 +min_lr=0.00005 +max_lr=0.0005 + +step=$(bc -l <<< "($max_lr - $min_lr) /($N_exp - 1)") + + +for lr in `seq $min_lr $step $max_lr` +do + exp_rootdir=/media/HHD/ana/dvs_steering_learning/models/hyper_test/expr_lr_$lr + + # Train + python3.4 ../cnn.py --experiment_rootdir=$exp_rootdir \ + --train_dir=$TRAIN_DIR --val_dir=$VAL_DIR --frame_mode=dvs \ + --initial_lr=$lr --epochs=30 --norandom_seed + + # Test + python3.4 ../evaluation.py --experiment_rootdir=$exp_rootdir --test_dir=$TEST_DIR \ + --frame_mode=dvs --weights_fname=model_weights_29.h5 + +done diff --git a/images/architecture.png b/images/architecture.png new file mode 100755 index 0000000..92274f9 Binary files /dev/null and b/images/architecture.png differ diff --git a/images/input_data.png b/images/input_data.png new file mode 100644 index 0000000..723f751 Binary files /dev/null and b/images/input_data.png differ diff --git a/img_utils.py b/img_utils.py new file mode 100644 index 0000000..7ae2912 --- /dev/null +++ b/img_utils.py @@ -0,0 +1,108 @@ +import cv2 +import numpy as np + + +def load_img(path, frame_mode, percentiles=None, target_size=None, crop_size=None): + """ + Load an image. + + # Arguments + path: Path to image file. + percentiles: some percentiles, in particular the median, an inferior percentil, + and a superior percentil for both positive and negative events in order + to remove outliers and normalize DVS images. Array containing [pos_median, + pos_inf, pos_sup, neg_median, neg_inf, neg_sup]. + target_size: Either `None` (default to original size) + or tuple of ints `(img_height, img_width)`. + crop_size: Either `None` (default to original size) + or tuple of ints `(img_height, img_width)`. + dvs: Boolean, whether to load the image as DVS. + + # Returns + Image as numpy array. + """ + + + + # Read input image + img = cv2.imread(path) + + if frame_mode == 'dvs': + + # Extract percentiles of interest to normalize between 0 and 1 + pos_sup = percentiles[2] # Superior percentile for positive events + neg_sup = percentiles[5] # Superior percentile for negative events + + if crop_size: + img = image_crop(img, crop_size[0], crop_size[1]) + + # Extract positive-event image + pos_events = img[:,:,0] + norm_pos_img = pos_events/pos_sup + norm_pos_img = np.expand_dims(norm_pos_img, axis=-1) + + # Extract negative-event image + neg_events = img[:,:,-1] + norm_neg_img = neg_events/neg_sup + norm_neg_img = np.expand_dims(norm_neg_img, axis=-1) + + + #input_img = np.concatenate((norm_pos_img, norm_neg_img), axis=-1) + + input_img = (norm_pos_img - norm_neg_img) + input_img = np.repeat(input_img, 3, axis=2) + + elif frame_mode == 'aps': + if len(img.shape) != 3: + #img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) + + if crop_size: + img = image_crop(img, crop_size[0], crop_size[1]) + + if target_size: + if (img.shape[0], img.shape[1]) != target_size: + img = cv2.resize(img, target_size) + + #input_img = img.reshape((img.shape[0], img.shape[1], 1)) + input_img = img + + else: + max_diff = np.log(255 + 1e-3) - np.log(0 + 1e-3) + #min_diff = np.log(0 + 1e-3) - np.log(255 + 1e-3) + + if crop_size: + img = image_crop(img, crop_size[0], crop_size[1]) + + if target_size: + if (img.shape[0], img.shape[1]) != target_size: + img = cv2.resize(img, target_size) + + input_img = (np.log(cv2.cvtColor(img[:,:,-1], cv2.COLOR_GRAY2RGB) + 1e-3)\ + - np.log(cv2.cvtColor(img[:,:,0], cv2.COLOR_GRAY2RGB) + 1e-3))/max_diff + #input_img = (np.log(img[:,:,-1] + 1e-3) - np.log(img[:,:,0] + 1e-3))/max_diff + #input_img = input_img.reshape((input_img.shape[0], input_img.shape[1], 1)) + + + + return np.asarray(input_img, dtype=np.float32) + + + +def image_crop(img, crop_heigth=200, crop_width=346): + """ + Crop the input image centered in width and starting from the top + in height to remove the hood and dashboard of the car. + + # Arguments: + crop_width: Width of the crop. + crop_heigth: Height of the crop. + + # Returns: + Cropped image. + """ + half_the_width = int(img.shape[1] / 2) + img = img[0:crop_heigth, + half_the_width - int(crop_width / 2): + half_the_width + int(crop_width / 2)] + return img diff --git a/log_utils.py b/log_utils.py new file mode 100644 index 0000000..7b4c8a1 --- /dev/null +++ b/log_utils.py @@ -0,0 +1,59 @@ +import logz +import numpy as np + +import keras +from keras import backend as K + +MIN_LR=0.00001 + + + +class MyCallback(keras.callbacks.Callback): + """ + Customized callback class. + + # Arguments + filepath: Path to save model. + period: Frequency in epochs with which model is saved. + batch_size: Number of images per batch. + """ + + def __init__(self, filepath, period, batch_size, factor = 1.0): + self.filepath = filepath + self.period = period + self.batch_size = batch_size + self.factor = factor + self.min_lr = MIN_LR + + def on_epoch_end(self, epoch, logs={}): + + # Save training and validation losses + logz.log_tabular('steering_loss', logs.get('steering_loss')) + logz.log_tabular('val_steering_loss', logs.get('val_steering_loss')) + logz.dump_tabular() + + # Save model every 'period' epochs + if (epoch+1) % self.period == 0: + filename = self.filepath + '/model_weights_' + str(epoch) + '.h5' + print("Saved model at {}".format(filename)) + self.model.save_weights(filename, overwrite=True) + + # Reduce lr in critical conditions + std_pred = logs.get('pred_std') + if std_pred < 0.05: + current_lr = K.get_value(self.model.optimizer.lr) + if not hasattr(self.model.optimizer, 'lr'): + raise ValueError('Optimizer must have a "lr" attribute.') + + new_lr = np.maximum(current_lr * self.factor, self.min_lr) + if not isinstance(new_lr, (float, np.float32, np.float64)): + raise ValueError('The output of the "schedule" function ' + 'should be float.') + K.set_value(self.model.optimizer.lr, new_lr) + print("Reduced learing rate!\n") + + # Hard mining + sess = K.get_session() + mse_function = self.batch_size-(self.batch_size-10)*( + np.maximum(0.0,1.0-np.exp(-1.0/30.0*(epoch-30.0)))) + self.model.k_mse.load(int(np.round(mse_function)), sess) diff --git a/logz.py b/logz.py new file mode 100644 index 0000000..da79332 --- /dev/null +++ b/logz.py @@ -0,0 +1,104 @@ +""" +Some simple logging functionality, inspired by rllab's logging. +Assumes that each diagnostic gets logged each iteration + +Call logz.configure_output_dir() to start logging to a +tab-separated-values file (some_folder_name/log.txt) + +To load the learning curves, you can do, for example + +A = np.genfromtxt('/tmp/expt_1468984536/log.txt',delimiter='\t',dtype=None, names=True) +A['EpRewMean'] + +""" + +import os.path as osp +import time +import os +import subprocess +import atexit + + +color2num = dict( + gray=30, + red=31, + green=32, + yellow=33, + blue=34, + magenta=35, + cyan=36, + white=37, + crimson=38 +) + + +def colorize(string, color, bold=False, highlight=False): + attr = [] + num = color2num[color] + if highlight: num += 10 + attr.append(str(num)) + if bold: attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string) + + +class G: + output_dir = None + output_file = None + first_row = True + log_headers = [] + log_current_row = {} + + +def configure_output_dir(d=None): + """ + Set output directory to d, or to /tmp/somerandomnumber if d is None + """ + G.output_dir = d or "/tmp/experiments/%i"%int(time.time()) + #assert not osp.exists(G.output_dir), "Log dir %s already exists! Delete it first or use a different dir"%G.output_dir + if not osp.exists(G.output_dir): + os.makedirs(G.output_dir) + G.output_file = open(osp.join(G.output_dir, "log.txt"), 'w') + atexit.register(G.output_file.close) + try: + cmd = "cd %s && git diff > %s 2>/dev/null"%(osp.dirname(__file__), osp.join(G.output_dir, "a.diff")) + subprocess.check_call(cmd, shell=True) # Save git diff to experiment directory + except subprocess.CalledProcessError: + print("configure_output_dir: not storing the git diff, probably because you're not in a git repo") + print(colorize("Logging data to %s"%G.output_file.name, 'green', bold=True)) + + +def log_tabular(key, val): + """ + Log a value of some diagnostic + Call this once for each diagnostic quantity, each iteration + """ + if G.first_row: + G.log_headers.append(key) + else: + assert key in G.log_headers, "Trying to introduce a new key %s that you didn't include in the first iteration"%key + assert key not in G.log_current_row, "You already set %s this iteration. Maybe you forgot to call dump_tabular()"%key + G.log_current_row[key] = val + + +def dump_tabular(): + """ + Write all of the diagnostics from the current iteration + """ + vals = [] + print("-"*37) + for key in G.log_headers: + val = G.log_current_row.get(key, "") + if hasattr(val, "__float__"): valstr = "%8.3g"%val + else: valstr = val + print("| %15s | %15s |"%(key, valstr)) + vals.append(val) + print("-"*37) + if G.output_file is not None: + if G.first_row: + G.output_file.write("\t".join(G.log_headers)) + G.output_file.write("\n") + G.output_file.write("\t".join(map(str,vals))) + G.output_file.write("\n") + G.output_file.flush() + G.log_current_row.clear() + G.first_row=False \ No newline at end of file diff --git a/plot_hyper_results.py b/plot_hyper_results.py new file mode 100644 index 0000000..ed0e20f --- /dev/null +++ b/plot_hyper_results.py @@ -0,0 +1,58 @@ +import os +import sys +import gflags +import json +import glob +import numpy as np +import matplotlib.pyplot as plt + +FLAGS = gflags.FLAGS + +gflags.DEFINE_string('exp_dir', "./model", 'Folder ' + ' containing all the learning-rate experiments') + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + + evas = [] + rmses = [] + lrs = [] + + + experiments = glob.glob(FLAGS.exp_dir + '/expr*') + for exp_name in experiments: + file_name = os.path.join(exp_name, 'test_results.json') + + lr = file_name.split('/')[-2] + lr = float(lr.split('_')[-1]) + lrs.append(lr) + + with open(file_name, 'r') as f: + results_dict = json.load(f) + + predicted_dict = results_dict['predicted'] + + evas.append(predicted_dict[0]['evas'][0]) + rmses.append(predicted_dict[0]['rmse']) + + evas = np.array(evas) + rmses = np.array(rmses) + lrs = np.array(lrs) + + plt.subplot(2, 1, 1) + plt.stem(lrs, evas) + plt.title('EVA') + plt.subplot(2, 1, 2) + plt.stem(lrs, rmses) + plt.title('RMSE') + plt.savefig(os.path.join(FLAGS.exp_dir, 'evas_rmses.png')) + + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file diff --git a/plot_loss.py b/plot_loss.py new file mode 100644 index 0000000..d095778 --- /dev/null +++ b/plot_loss.py @@ -0,0 +1,62 @@ +import os +import sys +import numpy as np +import gflags +import matplotlib.pyplot as plt +import matplotlib +matplotlib.style.use('ggplot') + +from common_flags import FLAGS + +gflags.DEFINE_string("exp_root_2", "../training/", "Folder where to take the second experiment") + +def smooth(y, box_pts): + box = np.ones(box_pts)/box_pts + y_smooth = np.convolve(y, box, mode='valid') + return y_smooth + + +def _main(): + + # Read log file + log_files = [os.path.join(FLAGS.experiment_rootdir, "log.txt"), + os.path.join(FLAGS.exp_root_2, "log.txt")] + logs = [] + for log_file in log_files: + try: + logs.append(np.genfromtxt(log_file, delimiter='\t',dtype=None, names=True)) + except: + raise IOError("Log file not found") + + train_loss_1 = logs[0]['steering_loss'][:130] + train_loss_1 = smooth(train_loss_1, 10) + train_loss_2 = logs[1]['steering_loss'][:130] + train_loss_2 = smooth(train_loss_2, 10) + timesteps = list(range(train_loss_1.shape[0])) + + # Plot losses + fig = plt.figure(1, figsize=(17,8)) + ax = fig.add_subplot(111) + ax.plot(timesteps, train_loss_2, 'r', timesteps, train_loss_1, 'b', linewidth=7) + plt.legend(["Random Initialization", "ImageNet Initialization"], fontsize=40) + plt.ylabel('Loss', size=45) + plt.xlabel('Epoch', size=45) + plt.yscale('log') + plt.tick_params(labelsize=35) + #plt.ylim((0,0.03)) + plt.savefig(os.path.join(FLAGS.experiment_rootdir, "log.png"), bbox_inches='tight') + + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/plot_results.py b/plot_results.py new file mode 100644 index 0000000..9525404 --- /dev/null +++ b/plot_results.py @@ -0,0 +1,56 @@ +import os +import sys +import numpy as np +import json +import matplotlib.pyplot as plt +import gflags + +from common_flags import FLAGS + + + +def make_and_save_histograms(pred_steerings, real_steerings, + img_name = "histograms.png"): + """ + Plot and save histograms from predicted steerings and real steerings. + + # Arguments + pred_steerings: List of predicted steerings. + real_steerings: List of real steerings. + img_name: Name of the png file to save the figure. + """ + pred_steerings = np.array(pred_steerings) + real_steerings = np.array(real_steerings) + max_h = np.maximum(np.max(pred_steerings), np.max(real_steerings)) + min_h = np.minimum(np.min(pred_steerings), np.min(real_steerings)) + bins = np.linspace(min_h, max_h, num=50) + plt.hist(pred_steerings, bins=bins, alpha=0.5, label='Predicted', color='b') + plt.hist(real_steerings, bins=bins, alpha=0.5, label='Real', color='r') + plt.title('Predicted vs. real steering angles') + plt.legend(fontsize=10) + plt.savefig(img_name, bbox_inches='tight') + + +def _main(): + + # Compute histograms from predicted and real steerings + fname_steer = os.path.join(FLAGS.experiment_rootdir, 'predicted_and_real_steerings.json') + with open(fname_steer,'r') as f: + results_dict = json.load(f) + make_and_save_histograms(results_dict['pred_steerings'], results_dict['real_steerings'], + os.path.join(FLAGS.experiment_rootdir, "histograms.png")) + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file diff --git a/process_new_video.py b/process_new_video.py new file mode 100644 index 0000000..a6c4f23 --- /dev/null +++ b/process_new_video.py @@ -0,0 +1,120 @@ +""" +Processes a new video sequence to predict the steering angle for each frame. +DroneDataGenerator is used to generate data from the new sequence, so that +'video_dir' must contain a single experiment with the same structure as the +training, validation and testing data: +name_of_experiment/ + exp_1/ + dvs/ + aps/ + aps_diff/ + sync_steering + +If the sequence does not have groundtruth, you must create it because DroneDataGenerator +expects a txt file. For simplicity, just create sync_steering.txt with as many zeros +as images in the sequence. +""" + + +import gflags +import numpy as np +import os +import sys +from unipath import Path +import json + +from keras import backend as K + +import utils +from constants import TEST_PHASE +from common_flags import FLAGS + + +def _main(): + + # Set testing mode (dropout/batchnormalization) + K.set_learning_phase(TEST_PHASE) + + # Generate data + if FLAGS.frame_mode == 'dvs' or FLAGS.frame_mode == 'aps_diff': + test_datagen = utils.DroneDataGenerator() + else: + test_datagen = utils.DroneDataGenerator(rescale = 1./255) + + test_generator = test_datagen.flow_from_directory(FLAGS.test_dir, + shuffle=False, + frame_mode = FLAGS.frame_mode, + target_size=(FLAGS.img_height, FLAGS.img_width), + crop_size=(FLAGS.crop_img_height, FLAGS.crop_img_width), + batch_size = FLAGS.batch_size) + + # Load json and create model + json_model_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.json_model_fname) + model = utils.jsonToModel(json_model_path) + + # Load weights + weights_load_path = os.path.join(FLAGS.experiment_rootdir, FLAGS.weights_fname) + try: + model.load_weights(weights_load_path) + print("Loaded model from {}".format(weights_load_path)) + except IOError as e: + print("Impossible to find weight path. Returning untrained model") + + + # Compile model + model.compile(loss='mse', optimizer='sgd') + + # Get predictions and ground truth + n_samples = test_generator.samples + nb_batches = int(np.ceil(n_samples / FLAGS.batch_size)) + + predictions, ground_truth = utils.compute_predictions_and_gt( + model, test_generator, nb_batches, verbose = 1) + + + # Steering boundaries seen in data + json_dict_fname = os.path.join( + Path(os.path.realpath(FLAGS.test_dir)).parent, + 'scalers_dict.json') + + with open(json_dict_fname, 'r') as f: + scalers_dict = json.load(f) + + mins = np.array(scalers_dict['mins']) + maxs = np.array(scalers_dict['maxs']) + + # Range of the transformed data + min_bound = -1.0 + max_bound = 1.0 + + # Undo transformation for predicitons (only for steering) + pred_std = (predictions[:,0] - min_bound)/(max_bound - min_bound) + pred_steer = pred_std*(maxs[0] - mins[0]) + mins[0] + pred_steer = np.expand_dims(pred_steer, axis = -1) + + # Undo transformation for ground-truth (only for steering) + gt_std = (ground_truth[:,0] - min_bound)/(max_bound - min_bound) + gt_steer = gt_std*(maxs[0] - mins[0]) + mins[0] + steer_gt = np.expand_dims(gt_steer, axis=-1) + + + # Write predicted and real steerings + steer_dict = {'pred_steerings': pred_steer.tolist(), + 'real_steerings': steer_gt.tolist()} + utils.write_to_file(steer_dict, os.path.join(FLAGS.test_dir, + 'predicted_and_real_steerings_' + FLAGS.frame_mode + '.json')) + + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d952599 --- /dev/null +++ b/utils.py @@ -0,0 +1,479 @@ +import re +import os +import numpy as np +import tensorflow as tf +import json +from unipath import Path +from sklearn.preprocessing import MinMaxScaler + +from keras import backend as K +from keras.preprocessing.image import Iterator +from keras.preprocessing.image import ImageDataGenerator +from keras.utils.generic_utils import Progbar +from keras.models import model_from_json + +import img_utils + + +offset = 6 # This is more or less 1/3s in the future! + + +class DroneDataGenerator(ImageDataGenerator): + """ + Generate minibatches of images and labels with real-time augmentation. + + The only function that changes w.r.t. parent class is the flow that + generates data. This function needed in fact adaptation for different + directory structure and labels. All the remaining functions remain + unchanged. + + For an example usage, see the evaluate.py script + """ + def flow_from_directory(self, directory, frame_mode='dvs', is_training=False, + target_size=(224,224), crop_size=(None,None), batch_size=32, + shuffle=True, seed=None, follow_links=False): + return DroneDirectoryIterator( + directory, self, frame_mode=frame_mode, is_training=is_training, + target_size=target_size, crop_size=crop_size, + batch_size=batch_size, shuffle=shuffle, seed=seed, + follow_links=follow_links) + + + +class DroneDirectoryIterator(Iterator): + """ + Class for managing data loading.of images and labels + We assume that the folder structure is: + root_folder/ + folder_1/ + dvs/ aps/ aps_diff/ + sync_steering + folder_2/ + dvs/ aps/ aps_diff/ + sync_steering + . + . + folder_n/ + dvs/ aps/ aps_diff/ + sync_steering + + # Arguments + directory: Path to the root directory to read data from. + image_data_generator: Image Generator. + frame_mode: One of `"dvs"`, `"aps"`. Frame mode to read images. + target_size: tuple of integers, dimensions to resize input images to. + crop_size: tuple of integers, dimensions to crop input images. + batch_size: The desired batch size + shuffle: Whether to shuffle data or not + seed : numpy seed to shuffle data + follow_links: Bool, whether to follow symbolic links or not + + # TODO: Add functionality to save images to have a look at the augmentation + """ + def __init__(self, directory, image_data_generator,frame_mode='dvs', + target_size=(224,224), crop_size = (None,None), is_training=False, + batch_size=32, shuffle=True, seed=None, follow_links=False): + self.directory = os.path.realpath(directory) + self.image_data_generator = image_data_generator + self.target_size = tuple(target_size) + self.is_training = is_training + self.crop_size = tuple(crop_size) + self.follow_links = follow_links + if frame_mode not in {'dvs', 'aps', 'aps_diff'}: + raise ValueError('Invalid frame mode:', frame_mode, + '; expected "dvs", "aps", or "aps_diff".') + self.frame_mode = frame_mode + + # Input image channels + # - DVS frames: 2 channels (first one for positive even, second one for negative events) + # - APS frames: 1 channel (grayscale images) + # - APS DIFF frames: 1 channel (log(I_1) - log(I_0)) + if self.frame_mode == 'dvs': + img_channels = 3 + else: + img_channels = 3 + + # TODO: if no target size is provided, it shoudl read image dimension + self.image_shape = self.target_size + (img_channels,) + + # First count how many experiments are out there + self.samples = 0 + + experiments = [] + for subdir in sorted(os.listdir(directory)): + if os.path.isdir(os.path.join(directory, subdir)): + experiments.append(subdir) + self.num_experiments = len(experiments) + self.formats = {'png', 'jpg'} + + # Idea = associate each filename with corresponding ground truths + # (multiple predictions) + self.filenames = [] + self.outputs = [] + self.dump_outputs = [] + + for subdir in experiments: + subpath = os.path.join(directory, subdir) + try: + self._decode_experiment_dir(subpath) + except: + continue + if self.samples == 0: + raise IOError("Did not find any data") + + # Conversion of list into array + self.outputs = np.array(self.outputs, dtype= K.floatx()) + self.outputs = np.expand_dims(self.outputs, axis=-1) + + self.dump_outputs = np.array(self.dump_outputs, dtype= K.floatx()) + self.dump_outputs = np.expand_dims(self.dump_outputs, axis=-1) + + # Output dimension + self.output_dim = self.outputs.shape[-1] + + # Steering normalization + self.outputs = self._output_normalization(self.outputs) + self.dump_outputs = self._output_normalization(self.dump_outputs) + + print('Found {} images belonging to {} experiments.'.format( + self.samples, self.num_experiments)) + + super(DroneDirectoryIterator, self).__init__(self.samples, + batch_size, shuffle, seed) + + if self.frame_mode == 'dvs': + # Load percentiles for positive and negative event normalization + try: + self.event_percentiles = np.loadtxt(os.path.join(Path(self.directory).parent, + 'percentiles.txt'), usecols=0, + skiprows=1) + except: + raise IOError("Percentiles file not found") + else: + self.event_percentiles = None + + + def _recursive_list(self, subpath): + return sorted(os.walk(subpath, followlinks=self.follow_links), + key=lambda tpl: tpl[0]) + + + def _decode_experiment_dir(self, dir_subpath): + # Load steerings from the experiment dir + steerings_filename = os.path.join(dir_subpath, "sync_steering.txt") + try: + outputs = np.loadtxt(steerings_filename, delimiter=',', + skiprows=1) + except: + raise IOError("Steering file not found") + + # Steering angle is not predicted for the first APS DIFF frame + if self.frame_mode == 'aps_diff': + outputs = outputs[1:] + + # Now fetch all images in the image subdir + if self.frame_mode == 'dvs': + image_dir_path = os.path.join(dir_subpath, "dvs") + elif self.frame_mode == 'aps': + image_dir_path = os.path.join(dir_subpath, "aps") + else: + image_dir_path = os.path.join(dir_subpath, "aps_diff") + + for root, _, files in self._recursive_list(image_dir_path): + sorted_files = sorted(files, + key = lambda fname: int(re.search(r'\d+',fname).group())) + for frame_number, fname in enumerate(sorted_files): + is_valid = False + gt_number = frame_number + offset + for extension in self.formats: + if (gt_number >= outputs.shape[0]): + break + + + if fname.lower().endswith('.' + extension): + # Filter those images whose velocity is under 23 km/h (for training) + if self.is_training: + if np.abs(outputs[frame_number][3]) < 2.3e1: + break + else: + is_valid = True + # Filter those images whose velocity is under 15 km/h (for evaluation) + else: + if np.abs(outputs[frame_number][3]) < 1.5e1: + break + else: + is_valid = True + + # Filter 30% of images whose steering is under 5 (only for training) + if self.is_training: + if np.abs(outputs[gt_number][0]) < 5.0: + if np.random.random() > 0.3: + is_valid=False + break + else: + break + + if is_valid: + absolute_path = os.path.join(root, fname) + self.filenames.append(os.path.relpath(absolute_path, + self.directory)) + self.outputs.append(outputs[gt_number, 0]) + self.dump_outputs.append(outputs[frame_number, 0]) + self.samples += 1 + + def _output_normalization(self, outputs): + """ + Normalize input array between -1 and 1. + + # Arguments + array: input array. + directory: + + # Returns + array: normalized array. + """ + out_path = Path(self.directory).parent + dict_path = os.path.join(out_path, 'scalers_dict.json') + + if self.is_training: + means = np.mean(outputs) + stds = np.std(outputs) + # 3sigma clipping + outputs = np.clip(outputs, means-3*stds, means+3*stds) + + # Scaling of all values + scaler = MinMaxScaler((-1.0,1.0)) + outputs = scaler.fit_transform(outputs) + + out_dict = {} + out_dict['means'] = means.tolist() + out_dict['stds'] = stds.tolist() + out_dict['mins'] = scaler.data_min_.tolist() + out_dict['maxs'] = scaler.data_max_.tolist() + + # Save dictionary for later testing + with open(dict_path, 'w') as f: + json.dump(out_dict, f) + + else: + # Read dictionary + with open(dict_path,'r') as f: + train_dict = json.load(f) + + # 3sigma clipping + means = train_dict['means'] + stds = train_dict['stds'] + outputs = np.clip(outputs,means-3*stds, means+3*stds) + + # Scaling of all values + mins = np.array(train_dict['mins']) + maxs = np.array(train_dict['maxs']) + + # Range of the transformed data + min_bound = -1.0 + max_bound = 1.0 + + outputs = (outputs - mins) / (maxs - mins) + outputs = outputs * (max_bound - min_bound) + min_bound + + + + return outputs + + def _get_batches_of_transformed_samples(self, index_array): + current_batch_size = index_array.shape[0] + # Image transformation is not under thread lock, so it can be done in + # parallel + batch_x = np.zeros((current_batch_size,) + self.image_shape, + dtype=K.floatx()) + + batch_outputs = np.zeros((current_batch_size, self.output_dim), + dtype=K.floatx()) + + # build batch of image data + for i, j in enumerate(index_array): + fname = self.filenames[j] + x = img_utils.load_img(os.path.join(self.directory, fname), + percentiles=self.event_percentiles, + frame_mode=self.frame_mode, + target_size=self.target_size, + crop_size=self.crop_size) + x = self.image_data_generator.random_transform(x) + x = self.image_data_generator.standardize(x) + batch_x[i] = x + + # Now build batch of steerings + batch_outputs = np.array(self.outputs[index_array], dtype=K.floatx()) + return batch_x, batch_outputs + + + def next(self): + """ + Public function to fetch next batch + # Returns + The next batch of images and commands. + """ + with self.lock: + index_array = next(self.index_generator) + + return self._get_batches_of_transformed_samples(index_array) + + +def compute_predictions_and_gt(model, generator, steps, + max_q_size=10, + pickle_safe=False, verbose=0): + """ + Generate predictions and associated ground truth + for the input samples from a data generator. + The generator should return the same kind of data as accepted by + `predict_on_batch`. + Function adapted from keras `predict_generator`. + + # Arguments + generator: Generator yielding batches of input samples. + steps: Total number of steps (batches of samples) + to yield from `generator` before stopping. + max_q_size: Maximum size for the generator queue. + pickle_safe: If `True`, use process based threading. + Note that because + this implementation relies on multiprocessing, + you should not pass + non picklable arguments to the generator + as they can't be passed + easily to children processes. + verbose: verbosity mode, 0 or 1. + + # Returns + Numpy array(s) of predictions and associated ground truth. + + # Raises + ValueError: In case the generator yields + data in an invalid format. + """ + steps_done = 0 + all_outs = [] + all_steerings = [] + + if verbose == 1: + progbar = Progbar(target=steps) + + while steps_done < steps: + generator_output = next(generator) + + if isinstance(generator_output, tuple): + if len(generator_output) == 2: + x, gt_steer = generator_output + elif len(generator_output) == 3: + x, gt_steer, _ = generator_output + else: + raise ValueError('output of generator should be ' + 'a tuple `(x, y, sample_weight)` ' + 'or `(x, y)`. Found: ' + + str(generator_output)) + else: + raise ValueError('Output not valid for current evaluation') + + outs = model.predict_on_batch(x) + #outs = gt_steer + + if not isinstance(outs, list): + outs = [outs] + if not isinstance(gt_steer, list): + gt_steer = [gt_steer] + + if not all_outs: + for out in outs: + # Len of this list is related to the number of + # outputs per model(1 in our case) + all_outs.append([]) + + if not all_steerings: + # Len of list related to the number of gt_steerings + # per model (1 in our case ) + for steer in gt_steer: + all_steerings.append([]) + + + for i, out in enumerate(outs): + all_outs[i].append(out) + + for i, steer in enumerate(gt_steer): + all_steerings[i].append(steer) + + steps_done += 1 + if verbose == 1: + progbar.update(steps_done) + + if steps_done == 1: + return [out for out in all_outs], [steer for steer in all_steerings] + else: + return np.squeeze(np.array([np.concatenate(out) for out in all_outs])), \ + np.squeeze(np.array([np.concatenate(steer) for steer in all_steerings])) + + +def hard_mining_mse(k): + """ + Compute MSE for steering evaluation and hard-mining for the current batch. + + # Arguments + k: number of samples for hard-mining. + + # Returns + custom_mse: average MSE for the current batch. + """ + + def custom_mse(y_true, y_pred): + + # Steering loss + l_steer = K.square(y_pred - y_true) + l_steer = tf.squeeze(l_steer, squeeze_dims=-1) + + # Hard mining + k_min = tf.minimum(k, tf.shape(l_steer)[0]) + _, indices = tf.nn.top_k(l_steer, k=k_min) + max_l_steer = tf.gather(l_steer, indices) + hard_l_steer = tf.divide(tf.reduce_sum(max_l_steer), tf.cast(k,tf.float32)) + + return hard_l_steer + + return custom_mse + + +def steering_loss(y_true, y_pred): + return tf.reduce_mean(K.square(y_pred - y_true)) + +def pred_std(y_true, y_pred): + _, var = tf.nn.moments(y_pred, axes=[0]) + return tf.sqrt(var) + + +def modelToJson(model, json_model_path): + """ + Serialize model into json. + """ + model_json = model.to_json() + + with open(json_model_path,"w") as f: + f.write(model_json) + + + +def jsonToModel(json_model_path): + """ + Serialize json into model. + """ + with open(json_model_path, 'r') as json_file: + loaded_model_json = json_file.read() + + model = model_from_json(loaded_model_json) + return model + + + +def write_to_file(dictionary, fname): + """ + Writes everything is in a dictionary in json model. + """ + with open(fname, "w") as f: + json.dump(dictionary,f) + print("Written file {}".format(fname)) diff --git a/viewer.py b/viewer.py new file mode 100644 index 0000000..cce8681 --- /dev/null +++ b/viewer.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Jun 14 15:49:08 2017 +@author: ana +""" + +''' +Results video generator Udacity Challenge 2 +Original By: Comma.ai Revd: Chris Gundling +''' + +import cv2 +import glob +import sys +import os +import numpy as np +import json +import gflags +import re + +from common_flags import FLAGS + + +def process_dvs_as_grayscale(img, climit=[-100,100]): + pos_img = (10*img[:,:,0]).astype('float32') + neg_img = (10*img[:,:,-1]).astype('float32') + gray_img = pos_img - neg_img + gray_img = (np.clip(gray_img, climit[0], climit[1]).astype('float32')+127).astype('uint8') + gray_img = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2RGB) + + return gray_img + + +def process_dvs_as_rb(img, constant, climit=[0,255]): + img[:,:,0] = constant*img[:,:,0] + img[:,:,-1] = constant*img[:,:,-1] + img = np.clip(img, climit[0], climit[1]).astype('uint8') + return img + + + +def get_data(exp_dir, img_height, img_width, img_channels, frame_mode, visual_mode): + + # Read images + img_files = [os.path.basename(x) for x in glob.glob(exp_dir + "/" + frame_mode + "/*")] + test_x = np.zeros((len(img_files),img_height, img_width, img_channels)) + sorted_files = sorted(img_files, + key = lambda fname: int(re.search(r'\d+',fname).group())) + for i,fname in enumerate(sorted_files): + img = cv2.imread(os.path.join(exp_dir, frame_mode, fname)) + if frame_mode=='dvs': + if visual_mode == 'grayscale': + img = process_dvs_as_grayscale(img) + else: + img = process_dvs_as_rb(img) + + elif frame_mode=='aps': + if len(img.shape) == 2: + img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) + + else: + input_img = (np.log(img[:,:,-1] + 1e-3) - np.log(img[:,:,0] + 1e-3)) + img = cv2.cvtColor(input_img, cv2.COLOR_GRAY2RGB) + + test_x[i] = img + return test_x + + +def plot_steering(img, pred_steer, real_steer): + c, r = (173, 130), 65 #center, radius + + # Draw circle + cv2.circle(img, c, r, (255, 255, 255), 1, lineType=cv2.LINE_AA) + cv2.line(img, (c[0]-r+5, c[1]), (c[0]-r, c[1]), (255, 255, 255), 1, lineType=cv2.LINE_AA) + cv2.line(img, (c[0]+r-5, c[1]), (c[0]+r, c[1]), (255, 255, 255), 1, lineType=cv2.LINE_AA) + cv2.line(img, (c[0], c[1]-r+5), (c[0], c[1]-r), (255, 255, 255), 1, lineType=cv2.LINE_AA) + cv2.line(img, (c[0], c[1]+r-5), (c[0], c[1]+r), (255, 255, 255), 1, lineType=cv2.LINE_AA) + + # Draw real steering + real_rad = + real_steer / 180. * np.pi + np.pi / 2 + t = (c[0] + int(np.cos(real_rad) * r), c[1] - int(np.sin(real_rad) * r)) + cv2.line(img, c, t, (255, 255, 255), 2, lineType=cv2.LINE_AA) + cv2.putText(img, 'GT', (c[0]-r-60, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1,lineType=cv2.LINE_AA) + cv2.putText(img, '%0.1f deg' % real_steer, (c[0]-r-60, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1,lineType=cv2.LINE_AA) + + # Draw predicted steering + pred_rad = + pred_steer / 180. * np.pi + np.pi / 2 + t = (c[0] + int(np.cos(pred_rad) * r), c[1] - int(np.sin(pred_rad) * r)) + cv2.line(img, c, t, (0,255,0), 2, lineType=cv2.LINE_AA) + + if FLAGS.frame_mode == 'dvs': + cv2.putText(img, 'DVS', (c[0]+35, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) + elif FLAGS.frame_mode =='aps': + cv2.putText(img, 'APS', (c[0]+35, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) + else: + cv2.putText(img, 'APS_DIFF', (c[0]+35, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) + + cv2.putText(img, '%0.1f deg' % pred_steer, (c[0]+35, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) + +# if FLAGS.frame_mode == 'dvs': +# # Draw predicted steering with DVS +# dvs_pred_rad = + dvs_steer / 180. * np.pi + np.pi / 2 +# t = (c[0] + int(np.cos(dvs_pred_rad) * r), c[1] - int(np.sin(dvs_pred_rad) * r)) +# cv2.line(img, c, t, (0,255,0), 2, lineType=cv2.LINE_AA) +# cv2.putText(img, 'DVS', (c[0]+35, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) +# cv2.putText(img, '%0.1f deg' % dvs_steer, (c[0]+35, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) +# +# elif FLAGS.frame_mode =='aps': +# # Draw predicted steering with APS +# aps_pred_rad = + 0 / 180. * np.pi + np.pi / 2 +# t = (c[0] + int(np.cos(aps_pred_rad) * r), c[1] - int(np.sin(aps_pred_rad) * r)) +# cv2.line(img, c, t, (0,255,0), 2, lineType=cv2.LINE_AA) +# cv2.putText(img, 'APS', (c[0]+35, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) +# cv2.putText(img, '%0.1f deg' % 0, (c[0]+35, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,0), 1,lineType=cv2.LINE_AA) +# +# else: +# # Draw predicted steering with DVS +# dvs_pred_rad = + dvs_steer / 180. * np.pi + np.pi / 2 +# t = (c[0] + int(np.cos(dvs_pred_rad) * r), c[1] - int(np.sin(dvs_pred_rad) * r)) +# cv2.line(img, c, t, (0,0,255), 2, lineType=cv2.LINE_AA) +# cv2.putText(img, 'DVS', (c[0]-30, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1,lineType=cv2.LINE_AA) +# cv2.putText(img, '%0.1f deg' % dvs_steer, (c[0]-30, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1,lineType=cv2.LINE_AA) +# +# # Draw predicted steering with APS +# aps_pred_rad = + 0 / 180. * np.pi + np.pi / 2 +# t = (c[0] + int(np.cos(aps_pred_rad) * r), c[1] - int(np.sin(aps_pred_rad) * r)) +# cv2.line(img, c, t, (0,0,255), 2, lineType=cv2.LINE_AA) +# cv2.putText(img, 'APS', (c[0]+r, c[1]-r-40), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1,lineType=cv2.LINE_AA) +# cv2.putText(img, '%0.1f deg' % 0, (c[0]+r, c[1]-r-20), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1,lineType=cv2.LINE_AA) + + return img + + + +def _main(): + + # Path to images + exp_dir = os.path.join(FLAGS.test_dir, 'exp_1') + + # Read ground truth + steerings_filename = os.path.join(exp_dir, "sync_steering.txt") + try: + gt = np.loadtxt(steerings_filename, delimiter=',', skiprows=1) + except: + raise IOError("Steering file not found") + + + # Prepare steering data + fname_steer = os.path.join(FLAGS.test_dir, 'predicted_and_real_steerings.json') + with open(fname_steer,'r') as f: + dict_steerings = json.load(f) + pred_steerings = np.array(dict_steerings['pred_steerings']) + real_steerings = np.array(dict_steerings['real_steerings']) + n_predictions = pred_steerings.shape[0] + + + # Prepare images + img_height, img_width, img_channels = 260, 346, 3 + + # Always visualize APS frames + aps_images = get_data(exp_dir, img_height, img_width, img_channels, 'aps', + FLAGS.visual_mode) + aps_images = aps_images[-n_predictions:,:,:,:] + print('APS data shape:', aps_images.shape) + + if FLAGS.frame_mode == 'dvs': + # Prepare DVS images + dvs_images = get_data(exp_dir, img_height, img_width, img_channels, FLAGS.frame_mode, + FLAGS.visual_mode) + dvs_images = dvs_images[-n_predictions:,:,:,:] + num_images = dvs_images.shape[0] + print('DVS data shape:', dvs_images.shape) + + elif FLAGS.frame_mode == 'aps_diff': + # Prepare APS images + aps_diff_images = get_data(exp_dir, img_height, img_width, img_channels, FLAGS.frame_mode, + FLAGS.visual_mode) + aps_diff_images = aps_diff_images[-n_predictions:,:,:,:] + num_images = aps_diff_images.shape[0] + print('APS data shape:', aps_diff_images.shape) + + + # Run through all images + for i in range(num_images): + + # Check if velocity is 0 + if np.abs(gt[i][3]) >= 2.30e1: + pred_steer = float(pred_steerings[i]) + real_steer = float(real_steerings[i]) + else: + if i==0: + pred_steer = 0 + real_steer = 0 + else: + pred_steer = float(pred_steerings[i-1]) + real_steer = float(real_steerings[i-1]) + + + # Show DVS and APS jointly + if FLAGS.frame_mode == 'dvs': + dvs = dvs_images[i] + aps = aps_images[i] + aps_steer = plot_steering(aps, pred_steer, real_steer) + output_img = np.concatenate((aps_steer, dvs), axis=1) + output_path = os.path.join(FLAGS.test_dir, "dvs_video") + + # Show APS only + elif FLAGS.frame_mode == 'aps': + # Draw predicted steering in APS frame + aps = aps_images[i] + output_img = plot_steering(aps, pred_steer, real_steer) + output_path = os.path.join(FLAGS.test_dir, "aps_video") + + # Show APS_DIFF and APS jointly + else: + aps_diff = aps_diff_images[i] + aps = aps_images[i] + aps_steer = plot_steering(aps, pred_steer, real_steer) + output_img = np.concatenate((aps_steer, aps_diff), axis=1) + output_path = os.path.join(FLAGS.test_dir, "aps_diff_video") + + # Save frame as png + if not os.path.exists(output_path): + os.makedirs(output_path) + img_name = "frame_" + str(i).zfill(5) + ".png" + cv2.imwrite(os.path.join(output_path, img_name),output_img) + + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv) diff --git a/visualization.py b/visualization.py new file mode 100644 index 0000000..66b761e --- /dev/null +++ b/visualization.py @@ -0,0 +1,172 @@ +import numpy as np +import cv2 +import gflags +import os +from vis.visualization import visualize_cam, overlay +from keras.utils.generic_utils import Progbar +import sys +import tensorflow as tf +from unipath import Path + +from common_flags import FLAGS +import img_utils +import utils +import re + +from viewer import process_dvs_as_rb + +gflags.DEFINE_string("input_imgs_dir", "","Input images directory") +gflags.DEFINE_string("output_dir", "", "Directory where to write images") + + +modifiers = [None] + + +def recursive_list(subpath): + return sorted(os.walk(subpath), key=lambda tpl: tpl[0]) + + +def load_fnames(dir_subpath, frame_mode): + # Steering angle is not predicted for the first APS DIFF frame + steerings_filename = os.path.join(dir_subpath, "sync_steering.txt") + try: + outputs = np.loadtxt(steerings_filename, delimiter=',', + skiprows=1) + except: + raise IOError("GT files not found") + + filenames = [] + + if frame_mode == 'aps_diff': + outputs = outputs[1:] + + # Now fetch all images in the image subdir + if frame_mode == 'dvs': + image_dir_path = os.path.join(dir_subpath, "dvs") + elif frame_mode == 'aps': + image_dir_path = os.path.join(dir_subpath, "aps") + else: + image_dir_path = os.path.join(dir_subpath, "aps_diff") + + for root, _, files in recursive_list(image_dir_path): + sorted_files = sorted(files, + key = lambda fname: int(re.search(r'\d+',fname).group())) + for frame_number, fname in enumerate(sorted_files): + is_valid = False + for extension in {'png'}: + if fname.lower().endswith('.' + extension): + if np.abs(outputs[frame_number][3]) < 2.30e1: + break + else: + is_valid = True + break + + if is_valid: + absolute_path = os.path.join(root, fname) + filenames.append(absolute_path) + + print("Found {} filenames to analyze".format(len(filenames))) + assert len(filenames) > 0, "No filenames found" + return filenames + + +def visualize_dvs_img(fname, target_size=None, crop_size=None): + img = cv2.imread(fname) + + if crop_size: + img = img_utils.image_crop(img, crop_size[0], crop_size[1]) + + if target_size: + if (img.shape[0], img.shape[1]) != target_size: + img = cv2.resize(img, target_size) + + img = process_dvs_as_rb(img, constant=30) + + return img + + +def read_percentiles(frame_mode): + if frame_mode == 'dvs': + # Load percentiles for positive and negative event normalization + try: + percentiles = np.loadtxt(os.path.join(Path(FLAGS.train_dir).parent, + 'percentiles.txt'), usecols=0, + skiprows=1) + except: + raise IOError("Percentiles file not found") + else: + percentiles = None + + return percentiles + +def _main(): + + # Load json and create model + json_model_path = os.path.join(FLAGS.experiment_rootdir, + FLAGS.json_model_fname) + model = utils.jsonToModel(json_model_path) + + # Check that output dir actually exists + if not os.path.exists(FLAGS.output_dir): + os.makedirs(FLAGS.output_dir) + + # Load weights + weights_load_path = os.path.join(FLAGS.experiment_rootdir, + FLAGS.weights_fname) + try: + model.load_weights(weights_load_path) + print("Loaded model from {}".format(weights_load_path)) + except: + print("Impossible to find weight path. Returning untrained model") + + target_size = (FLAGS.img_width, FLAGS.img_height) + crop_size = (FLAGS.crop_img_width, FLAGS.crop_img_height) + frame_mode = FLAGS.frame_mode + + # Initialize number of samples for hard-mining + model.k_mse = tf.Variable(FLAGS.batch_size, trainable=False, name='k_mse', dtype=tf.int32) + + model.compile(loss=[utils.hard_mining_mse(model.k_mse)], + optimizer='adam', decay=1e-4, lr=FLAGS.initial_lr, + metrics=[utils.steering_loss, utils.pred_std]) + + filenames = load_fnames(FLAGS.input_imgs_dir, frame_mode) + percentiles = read_percentiles(frame_mode) + + + progbar = Progbar(target=len(filenames)) + for n, fname in enumerate(filenames): + + img = img_utils.load_img(fname, frame_mode, percentiles, + target_size, crop_size) + if frame_mode == 'dvs': + colored = visualize_dvs_img(fname, target_size, crop_size) + else: + colored = cv2.imread(fname, 3) + + if frame_mode == 'aps': + img = np.asarray(img / 255.0, dtype = np.float32) + for i, modifier in enumerate(modifiers): + heatmap = visualize_cam(model, layer_idx=-1, filter_indices=0, + seed_input=img, grad_modifier=modifier) + # Overlay is used to alpha blend heatmap onto img + result_fname = os.path.join(FLAGS.output_dir, + os.path.basename(fname)) + new_img = cv2.cvtColor(overlay(colored, heatmap, alpha=0.6), + cv2.COLOR_RGB2BGR) + cv2.imwrite(result_fname, new_img) + + progbar.update(n) + +def main(argv): + # Utility main to load flags + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError: + print ('Usage: %s ARGS\\n%s' % (sys.argv[0], FLAGS)) + sys.exit(1) + _main() + + +if __name__ == "__main__": + main(sys.argv)