Outline

  • Step 1: Visualize the Data with its Labels

  • Step 2: Preprocess the Dataset

  • Step 3: Split the Data into Train and Test Set

  • Step 4: Implement the ConvLSTM Approach

    • Step 4.1: Construct the Model

    • Step 4.2: Compile & Train the Model

    • Step 4.3: Plot Model’s Loss & Accuracy Curves

  • Step 5: implement the LRCN Approach

    • Step 5.1: Construct the Model

    • Step 5.2: Compile & Train the Model

    • Step 5.3: Plot Model’s Loss & Accuracy Curves

  • Step 6: Test the Best Performing Model on YouTube videos

Alright, so without further ado, let's get started.

Creating Necessary Environment

  • Create a separate conda environment for the project.

    conda create -n action python=3.7 -y

  • Activate the conda environment.

    conda activate action

  • Install required packages.

    pip install tensorflow opencv-contrib-python youtube-dl moviepy pydot

    pip install git+https://github.com/TahaAnwar/pafy.git#egg=pafy

    pip install numpy matplotlib scikit-learn

Import the Libraries

import os
import cv2
import pafy
import math
import random
import numpy as np
import datetime as dt
import tensorflow as tf
from collections import deque
import matplotlib.pyplot as plt

from moviepy.editor import *
%matplotlib inline

from sklearn.model_selection import train_test_split

from tensorflow.keras.layers import *
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.utils import plot_model

And will set Numpy, Python, and Tensorflow seeds to get consistent results on every execution.

seed_constant = 27
np.random.seed(seed_constant)
random.seed(seed_constant)
tf.random.set_seed(seed_constant)

Step 1: Visualize the Data with its Labels

In the first step, we will visualize the data along with labels to get an idea about what we will be dealing with. We will be using the UCF50 - Action Recognition Dataset, consisting of realistic videos taken from youtube which differentiates this data set from most of the other available action recognition data sets as they are not realistic and are staged by actors. The Dataset contains:

  • 50 Action Categories

  • 25 Groups of Videos per Action Category

  • 133 Average Videos per Action Category

  • 199 Average Number of Frames per Video

  • 320 Average Frames Width per Video

  • 240 Average Frames Height per Video

  • 26 Average Frames Per Seconds per Video

For visualization, we will pick 20 random categories from the dataset and a random video from each selected category and will visualize the first frame of the selected videos with their associated labels written. This way we’ll be able to visualize a subset ( 20 random videos ) of the dataset.

plt.figure(figsize = (20, 20))

# Get the names of all classes/categories in UCF50.
all_classes_names = os.listdir('./UCF50')

# Generate a list of 20 random values. The values will be between 0-50, 
# where 50 is the total number of class in the dataset. 
random_range = random.sample(range(len(all_classes_names)), 20)

# Iterating through all the generated random values.
for counter, random_index in enumerate(random_range, 1):

    # Retrieve a Class Name using the Random Index.
    selected_class_Name = all_classes_names[random_index]

    # Retrieve the list of all the video files present in the randomly selected Class Directory.
    video_files_names_list = os.listdir(f'./UCF50/{selected_class_Name}')

    # Randomly select a video file from the list retrieved from the randomly selected Class Directory.
    selected_video_file_name = random.choice(video_files_names_list)

    # Initialize a VideoCapture object to read from the video File.
    video_reader = cv2.VideoCapture(f'./UCF50/{selected_class_Name}/{selected_video_file_name}')
    
    # Read the first frame of the video file.
    _, bgr_frame = video_reader.read()

    # Release the VideoCapture object. 
    video_reader.release()

    # Convert the frame from BGR into RGB format. 
    rgb_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB)

    # Write the class name on the video frame.
    cv2.putText(rgb_frame, selected_class_Name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 0), 2)
    
    # Display the frame.
    plt.subplot(5, 4, counter);plt.imshow(rgb_frame);plt.axis('off')

Preprocess the Dataset

Next, we will perform some preprocessing on the dataset. First, we will read the video files from the dataset and resize the frames of the videos to a fixed width and height, to reduce the computations and normalized the data to range [0-1] by dividing the pixel values with 255, which makes convergence faster while training the network.

But first, let's initialize some constants.

IMAGE_HEIGHT , IMAGE_WIDTH = 64, 64

# Specify the number of frames of a video that will be fed to the model as one sequence.
SEQUENCE_LENGTH = 20

# Specify the directory containing the UCF50 dataset. 
DATASET_DIR = "./UCF50"

# Specify the list containing the names of the classes used for training. Feel free to choose any set of classes.
CLASSES_LIST = ["Basketball", "JumpRope", "Swing", "HorseRace"]

Note: The IMAGE_HEIGHT, IMAGE_WIDTH and SEQUENCE_LENGTH constants can be increased for better results, although increasing the sequence length is only effective to a certain point, and increasing the values will result in the process being more computationally expensive.

Create a Function to Extract, Resize & Normalize Frames

We will create a function frames_extraction() that will create a list containing the resized and normalized frames of a video whose path is passed to it as an argument. The function will read the video file frame by frame, although not all frames are added to the list as we will only need an evenly distributed sequence length of frames.

def frames_extraction(video_path):
    '''
    This function will extract the required frames from a video after resizing and normalizing them.
    Args:
        video_path: The path of the video in the disk, whose frames are to be extracted.
    Returns:
        frames_list: A list containing the resized and normalized frames of the video.
    '''

    # Declare a list to store video frames.
    frames_list = []
    
    # Read the Video File using the VideoCapture object.
    video_reader = cv2.VideoCapture(video_path)

    # Get the total number of frames in the video.
    video_frames_count = int(video_reader.get(cv2.CAP_PROP_FRAME_COUNT))

    # Calculate the interval after which frames will be added to the list.
    skip_frames_window = max(int(video_frames_count/SEQUENCE_LENGTH), 1)

    # Iterate through the Video Frames.
    for frame_counter in range(SEQUENCE_LENGTH):

        # Set the current frame position of the video.
        video_reader.set(cv2.CAP_PROP_POS_FRAMES, frame_counter * skip_frames_window)

        # Reading the frame from the video. 
        success, frame = video_reader.read() 

        # Check if Video frame is not successfully read then break the loop
        if not success:
            break

        # Resize the Frame to fixed height and width.
        resized_frame = cv2.resize(frame, (IMAGE_HEIGHT, IMAGE_WIDTH))
        
        # Normalize the resized frame by dividing it with 255 so that each pixel value then lies between 0 and 1
        normalized_frame = resized_frame / 255
        
        # Append the normalized frame into the frames list
        frames_list.append(normalized_frame)
    
    # Release the VideoCapture object. 
    video_reader.release()

    # Return the frames list.
    return frames_list

Create a Function for Dataset Creation

Now we will create a function create_dataset() that will iterate through all the classes specified in the CLASSES_LIST constant and will call the function frame_extraction() on every video file of the selected classes and return the frames (features), class index ( labels), and video file path (video_files_paths).

def create_dataset():
    '''
    This function will extract the data of the selected classes and create the required dataset.
    Returns:
        features:          A list containing the extracted frames of the videos.
        labels:            A list containing the indexes of the classes associated with the videos.
        video_files_paths: A list containing the paths of the videos in the disk.
    '''

    # Declared Empty Lists to store the features, labels and video file path values.
    features = []
    labels = []
    video_files_paths = []
    
    # Iterating through all the classes mentioned in the classes list
    for class_index, class_name in enumerate(CLASSES_LIST):
        
        # Display the name of the class whose data is being extracted.
        print(f'Extracting Data of Class: {class_name}')
        
        # Get the list of video files present in the specific class name directory.
        files_list = os.listdir(os.path.join(DATASET_DIR, class_name))
        
        # Iterate through all the files present in the files list.
        for file_name in files_list:
            
            # Get the complete video path.
            video_file_path = os.path.join(DATASET_DIR, class_name, file_name)

            # Extract the frames of the video file.
            frames = frames_extraction(video_file_path)

            # Check if the extracted frames are equal to the SEQUENCE_LENGTH specified above.
            # So ignore the vides having frames less than the SEQUENCE_LENGTH.
            if len(frames) == SEQUENCE_LENGTH:

                # Append the data to their repective lists.
                features.append(frames)
                labels.append(class_index)
                video_files_paths.append(video_file_path)

    # Converting the list to numpy arrays
    features = np.asarray(features)
    labels = np.array(labels)  
    
    # Return the frames, class index, and video file path.
    return features, labels, video_files_paths

Now we will utilize the function create_dataset() created above to extract the data of the selected classes and create the required dataset.

features, labels, video_files_paths = create_dataset()
Extracting Data of Class: Basketball
Extracting Data of Class: JumpRope
Extracting Data of Class: Swing
Extracting Data of Class: HorseRace

Now we will convert labels (class indexes) into one-hot encoded vectors.

one_hot_encoded_labels = to_categorical(labels)

Step 3: Split the Data into Train and Test Set

As of now, we have the required features (a NumPy array containing all the extracted frames of the videos) and one_hot_encoded_labels (also a Numpy array containing all class labels in one hot encoded format). So now, we will split our data to create training and testing sets. We will also shuffle the dataset before the split to avoid any bias and get splits representing the overall distribution of the data.

features_train, features_test, labels_train, labels_test = train_test_split(features, one_hot_encoded_labels,
                                                                            test_size = 0.25, shuffle = True,
                                                                            random_state = seed_constant)

Step 4: Implement the ConvLSTM Approach

In this step, we will implement the first approach by using a combination of ConvLSTM cells. A ConvLSTM cell is a variant of an LSTM network that contains convolutions operations in the network. it is an LSTM with convolution embedded in the architecture, which makes it capable of identifying spatial features of the data while keeping into account the temporal relation.

For video classification, this approach effectively captures the spatial relation in the individual frames and the temporal relation across the different frames. As a result of this convolution structure, the ConvLSTM is capable of taking in 3-dimensional input (width, height, num_of_channels) whereas a simple LSTM only takes in 1-dimensional input hence an LSTM is incompatible for modeling Spatio-temporal data on its own.

You can read the paper Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting by Xingjian Shi (NIPS 2015), to learn more about this architecture.

Step 4.1: Construct the Model

To construct the model, we will use Keras ConvLSTM2D recurrent layers. The ConvLSTM2D layer also takes in the number of filters and kernel size required for applying the convolutional operations. The output of the layers is flattened in the end and is fed to the Dense layer with softmax activation which outputs the probability of each action category.

We will also use MaxPooling3D layers to reduce the dimensions of the frames and avoid unnecessary computations and Dropout layers to prevent overfitting the model on the data. The architecture is a simple one and has a small number of trainable parameters. This is because we are only dealing with a small subset of the dataset which does not require a large-scale model.

def create_convlstm_model():
    '''
    This function will construct the required convlstm model.
    Returns:
        model: It is the required constructed convlstm model.
    '''

    # We will use a Sequential model for model construction
    model = Sequential()

    # Define the Model Architecture.
    ########################################################################################################################
    
    model.add(ConvLSTM2D(filters = 4, kernel_size = (3, 3), activation = 'tanh',data_format = "channels_last",
                         recurrent_dropout=0.2, return_sequences=True, input_shape = (SEQUENCE_LENGTH,
                                                                                      IMAGE_HEIGHT, IMAGE_WIDTH, 3)))
    
    model.add(MaxPooling3D(pool_size=(1, 2, 2), padding='same', data_format='channels_last'))
    model.add(TimeDistributed(Dropout(0.2)))
    
    model.add(ConvLSTM2D(filters = 8, kernel_size = (3, 3), activation = 'tanh', data_format = "channels_last",
                         recurrent_dropout=0.2, return_sequences=True))
    
    model.add(MaxPooling3D(pool_size=(1, 2, 2), padding='same', data_format='channels_last'))
    model.add(TimeDistributed(Dropout(0.2)))
    
    model.add(ConvLSTM2D(filters = 14, kernel_size = (3, 3), activation = 'tanh', data_format = "channels_last",
                         recurrent_dropout=0.2, return_sequences=True))
    
    model.add(MaxPooling3D(pool_size=(1, 2, 2), padding='same', data_format='channels_last'))
    model.add(TimeDistributed(Dropout(0.2)))
    
    model.add(ConvLSTM2D(filters = 16, kernel_size = (3, 3), activation = 'tanh', data_format = "channels_last",
                         recurrent_dropout=0.2, return_sequences=True))
    
    model.add(MaxPooling3D(pool_size=(1, 2, 2), padding='same', data_format='channels_last'))
    #model.add(TimeDistributed(Dropout(0.2)))
    
    model.add(Flatten()) 
    
    model.add(Dense(len(CLASSES_LIST), activation = "softmax"))
    
    ########################################################################################################################
     
    # Display the models summary.
    model.summary()
    
    # Return the constructed convlstm model.
    return model

Now we will utilize the function create_convlstm_model() created above, to construct the required convlstm model.

convlstm_model = create_convlstm_model()

# Display the success message. 
print("Model Created Successfully!")
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv_lstm2d (ConvLSTM2D)    (None, 20, 62, 62, 4)     1024      
                                                                 
 max_pooling3d (MaxPooling3D  (None, 20, 31, 31, 4)    0         
 )                                                               
                                                                 
 time_distributed (TimeDistr  (None, 20, 31, 31, 4)    0         
 ibuted)                                                         
                                                                 
 conv_lstm2d_1 (ConvLSTM2D)  (None, 20, 29, 29, 8)     3488      
                                                                 
 max_pooling3d_1 (MaxPooling  (None, 20, 15, 15, 8)    0         
 3D)                                                             
                                                                 
 time_distributed_1 (TimeDis  (None, 20, 15, 15, 8)    0         
 tributed)                                                       
                                                                 
 conv_lstm2d_2 (ConvLSTM2D)  (None, 20, 13, 13, 14)    11144     
                                                                 
 max_pooling3d_2 (MaxPooling  (None, 20, 7, 7, 14)     0         
 3D)                                                             
                                                                 
 time_distributed_2 (TimeDis  (None, 20, 7, 7, 14)     0         
 tributed)                                                       
                                                                 
 conv_lstm2d_3 (ConvLSTM2D)  (None, 20, 5, 5, 16)      17344     
                                                                 
 max_pooling3d_3 (MaxPooling  (None, 20, 3, 3, 16)     0         
 3D)                                                             
                                                                 
 flatten (Flatten)           (None, 2880)              0         
                                                                 
 dense (Dense)               (None, 4)                 11524     
                                                                 
=================================================================
Total params: 44,524
Trainable params: 44,524
Non-trainable params: 0
_________________________________________________________________
Model Created Successfully!

Check Model’s Structure:

Now we will use the plot_model() function, to check the structure of the constructed model, this is helpful while constructing a complex network and making that the network is created correctly.

plot_model(convlstm_model, to_file = 'convlstm_model_structure_plot.png', show_shapes = True, show_layer_names = True)
You must install pydot (`pip install pydot`) and install graphviz (see instructions at https://graphviz.gitlab.io/download/) for plot_model/model_to_dot to work.

Step 4.2: Compile & Train the Model

Next, we will add an early stopping callback to prevent overfitting and start the training after compiling the model.

#early_stopping_callback = EarlyStopping(monitor = 'val_loss', patience = 10, mode = 'min', restore_best_weights = True)
#callbacks = [early_stopping_callback]

# Compile the model and specify loss function, optimizer and metrics values to the model
convlstm_model.compile(loss = 'categorical_crossentropy', optimizer = 'Adam', metrics = ["accuracy"])

# Start training the model.
convlstm_model_training_history = convlstm_model.fit(x = features_train, y = labels_train, epochs = 50, batch_size = 4,
                                                     shuffle = True, validation_split = 0.2
                                                     )
Epoch 1/50
82/82 [==============================] - 48s 525ms/step - loss: 0.1773 - accuracy: 0.9360 - val_loss: 0.3990 - val_accuracy: 0.8554
Epoch 2/50
82/82 [==============================] - 43s 528ms/step - loss: 0.1852 - accuracy: 0.9329 - val_loss: 0.5882 - val_accuracy: 0.7711
Epoch 3/50
82/82 [==============================] - 43s 521ms/step - loss: 0.1401 - accuracy: 0.9604 - val_loss: 0.4735 - val_accuracy: 0.8675
Epoch 4/50
82/82 [==============================] - 43s 525ms/step - loss: 0.0624 - accuracy: 0.9787 - val_loss: 0.4885 - val_accuracy: 0.8554
Epoch 5/50
82/82 [==============================] - 43s 521ms/step - loss: 0.0669 - accuracy: 0.9817 - val_loss: 0.7081 - val_accuracy: 0.7952
Epoch 6/50
82/82 [==============================] - 43s 525ms/step - loss: 0.0732 - accuracy: 0.9665 - val_loss: 0.4248 - val_accuracy: 0.8675
Epoch 7/50
82/82 [==============================] - 42s 517ms/step - loss: 0.1593 - accuracy: 0.9512 - val_loss: 0.5280 - val_accuracy: 0.8072
Epoch 8/50
82/82 [==============================] - 45s 545ms/step - loss: 0.0735 - accuracy: 0.9817 - val_loss: 0.1993 - val_accuracy: 0.9157
Epoch 9/50
82/82 [==============================] - 44s 533ms/step - loss: 0.0152 - accuracy: 0.9970 - val_loss: 0.2067 - val_accuracy: 0.9157
Epoch 10/50
82/82 [==============================] - 44s 534ms/step - loss: 0.0121 - accuracy: 1.0000 - val_loss: 0.1427 - val_accuracy: 0.9639
Epoch 11/50
82/82 [==============================] - 43s 530ms/step - loss: 0.0075 - accuracy: 1.0000 - val_loss: 0.2228 - val_accuracy: 0.9157
Epoch 12/50
82/82 [==============================] - 43s 524ms/step - loss: 0.0032 - accuracy: 1.0000 - val_loss: 0.1695 - val_accuracy: 0.9398
Epoch 13/50
82/82 [==============================] - 44s 540ms/step - loss: 0.1132 - accuracy: 0.9695 - val_loss: 0.5239 - val_accuracy: 0.8554
Epoch 14/50
82/82 [==============================] - 44s 533ms/step - loss: 0.2671 - accuracy: 0.9055 - val_loss: 0.4620 - val_accuracy: 0.8193
Epoch 15/50
82/82 [==============================] - 44s 535ms/step - loss: 0.0661 - accuracy: 0.9787 - val_loss: 0.4337 - val_accuracy: 0.8675
Epoch 16/50
82/82 [==============================] - 44s 531ms/step - loss: 0.0212 - accuracy: 0.9970 - val_loss: 0.8717 - val_accuracy: 0.7831
Epoch 17/50
82/82 [==============================] - 44s 534ms/step - loss: 0.2264 - accuracy: 0.9329 - val_loss: 0.4955 - val_accuracy: 0.8193
Epoch 18/50
82/82 [==============================] - 44s 537ms/step - loss: 0.0764 - accuracy: 0.9695 - val_loss: 0.5162 - val_accuracy: 0.8193
Epoch 19/50
82/82 [==============================] - 44s 537ms/step - loss: 0.0083 - accuracy: 1.0000 - val_loss: 0.3905 - val_accuracy: 0.8916
Epoch 20/50
82/82 [==============================] - 43s 527ms/step - loss: 0.0048 - accuracy: 1.0000 - val_loss: 0.4184 - val_accuracy: 0.8554
Epoch 21/50
82/82 [==============================] - 44s 540ms/step - loss: 0.0029 - accuracy: 1.0000 - val_loss: 0.3921 - val_accuracy: 0.8554
Epoch 22/50
82/82 [==============================] - 44s 543ms/step - loss: 0.0023 - accuracy: 1.0000 - val_loss: 0.4140 - val_accuracy: 0.8434
Epoch 23/50
82/82 [==============================] - 44s 538ms/step - loss: 0.0016 - accuracy: 1.0000 - val_loss: 0.3960 - val_accuracy: 0.8795
Epoch 24/50
82/82 [==============================] - 45s 555ms/step - loss: 0.0021 - accuracy: 1.0000 - val_loss: 0.4424 - val_accuracy: 0.8434
Epoch 25/50
82/82 [==============================] - 42s 515ms/step - loss: 0.0016 - accuracy: 1.0000 - val_loss: 0.4016 - val_accuracy: 0.8554
Epoch 26/50
82/82 [==============================] - 42s 508ms/step - loss: 0.0024 - accuracy: 1.0000 - val_loss: 0.4445 - val_accuracy: 0.8795
Epoch 27/50
82/82 [==============================] - 44s 532ms/step - loss: 0.0013 - accuracy: 1.0000 - val_loss: 0.3902 - val_accuracy: 0.8795
Epoch 28/50
82/82 [==============================] - 44s 535ms/step - loss: 8.7990e-04 - accuracy: 1.0000 - val_loss: 0.3751 - val_accuracy: 0.9157
Epoch 29/50
82/82 [==============================] - 44s 534ms/step - loss: 0.0012 - accuracy: 1.0000 - val_loss: 0.4122 - val_accuracy: 0.8675
Epoch 30/50
82/82 [==============================] - 44s 540ms/step - loss: 7.2344e-04 - accuracy: 1.0000 - val_loss: 0.3900 - val_accuracy: 0.8795
Epoch 31/50
82/82 [==============================] - 45s 544ms/step - loss: 5.0299e-04 - accuracy: 1.0000 - val_loss: 0.3936 - val_accuracy: 0.8795
Epoch 32/50
82/82 [==============================] - 45s 547ms/step - loss: 4.8611e-04 - accuracy: 1.0000 - val_loss: 0.3832 - val_accuracy: 0.8795
Epoch 33/50
82/82 [==============================] - 43s 530ms/step - loss: 4.6124e-04 - accuracy: 1.0000 - val_loss: 0.4214 - val_accuracy: 0.8675
Epoch 34/50
82/82 [==============================] - 44s 537ms/step - loss: 5.6228e-04 - accuracy: 1.0000 - val_loss: 0.4555 - val_accuracy: 0.8554
Epoch 35/50
82/82 [==============================] - 44s 534ms/step - loss: 5.7521e-04 - accuracy: 1.0000 - val_loss: 0.4253 - val_accuracy: 0.8675
Epoch 36/50
82/82 [==============================] - 42s 516ms/step - loss: 6.2197e-04 - accuracy: 1.0000 - val_loss: 0.3824 - val_accuracy: 0.8916
Epoch 37/50
82/82 [==============================] - 43s 519ms/step - loss: 3.2945e-04 - accuracy: 1.0000 - val_loss: 0.4164 - val_accuracy: 0.8675
Epoch 38/50
82/82 [==============================] - 43s 529ms/step - loss: 3.2070e-04 - accuracy: 1.0000 - val_loss: 0.4198 - val_accuracy: 0.8675
Epoch 39/50
82/82 [==============================] - 44s 536ms/step - loss: 2.7394e-04 - accuracy: 1.0000 - val_loss: 0.4282 - val_accuracy: 0.8675
Epoch 40/50
82/82 [==============================] - 45s 544ms/step - loss: 3.0938e-04 - accuracy: 1.0000 - val_loss: 0.4123 - val_accuracy: 0.8675
Epoch 41/50
82/82 [==============================] - 44s 539ms/step - loss: 2.9413e-04 - accuracy: 1.0000 - val_loss: 0.4503 - val_accuracy: 0.8675
Epoch 42/50
82/82 [==============================] - 44s 531ms/step - loss: 3.4679e-04 - accuracy: 1.0000 - val_loss: 0.4242 - val_accuracy: 0.8675
Epoch 43/50
82/82 [==============================] - 44s 539ms/step - loss: 2.5354e-04 - accuracy: 1.0000 - val_loss: 0.4091 - val_accuracy: 0.8675
Epoch 44/50
82/82 [==============================] - 44s 543ms/step - loss: 1.8511e-04 - accuracy: 1.0000 - val_loss: 0.4019 - val_accuracy: 0.8675
Epoch 45/50
82/82 [==============================] - 44s 534ms/step - loss: 2.9071e-04 - accuracy: 1.0000 - val_loss: 0.4094 - val_accuracy: 0.8795
Epoch 46/50
82/82 [==============================] - 45s 545ms/step - loss: 2.3987e-04 - accuracy: 1.0000 - val_loss: 0.3917 - val_accuracy: 0.9036
Epoch 47/50
82/82 [==============================] - 44s 543ms/step - loss: 2.2292e-04 - accuracy: 1.0000 - val_loss: 0.3978 - val_accuracy: 0.9277
Epoch 48/50
82/82 [==============================] - 45s 552ms/step - loss: 3.2012e-04 - accuracy: 1.0000 - val_loss: 0.4193 - val_accuracy: 0.8675
Epoch 49/50
82/82 [==============================] - 43s 523ms/step - loss: 1.9464e-04 - accuracy: 1.0000 - val_loss: 0.4750 - val_accuracy: 0.8795
Epoch 50/50
82/82 [==============================] - 45s 544ms/step - loss: 1.7188e-04 - accuracy: 1.0000 - val_loss: 0.3900 - val_accuracy: 0.8916

Evaluate the Trained Model

After training, we will evaluate the model on the test set.

model_evaluation_history = convlstm_model.evaluate(features_test, labels_test)
5/5 [==============================] - 1s 154ms/step - loss: 0.5704 - accuracy: 0.8623

Save the Model

Now we will save the model to avoid training it from scratch every time we need the model.

model_evaluation_loss, model_evaluation_accuracy = model_evaluation_history

# Define the string date format.
# Get the current Date and Time in a DateTime Object.
# Convert the DateTime object to string according to the style mentioned in date_time_format string.
date_time_format = '%Y_%m_%d__%H_%M_%S'
current_date_time_dt = dt.datetime.now()
current_date_time_string = dt.datetime.strftime(current_date_time_dt, date_time_format)

# Define a useful name for our model to make it easy for us while navigating through multiple saved models.
model_file_name = f'convlstm_model___Date_Time_{current_date_time_string}___Loss_{model_evaluation_loss}___Accuracy_{model_evaluation_accuracy}.h5'

# Save your Model.
convlstm_model.save(model_file_name)

Step 4.3: Plot Model’s Loss & Accuracy Curves

Now we will create a function plot_metric() to visualize the training and validation metrics. We already have separate metrics from our training and validation steps so now we just have to visualize them.

def plot_metric(model_training_history, metric_name_1, metric_name_2, plot_name):
    '''
    This function will plot the metrics passed to it in a graph.
    Args:
        model_training_history: A history object containing a record of training and validation 
                                loss values and metrics values at successive epochs
        metric_name_1:          The name of the first metric that needs to be plotted in the graph.
        metric_name_2:          The name of the second metric that needs to be plotted in the graph.
        plot_name:              The title of the graph.
    '''
    
    # Get metric values using metric names as identifiers.
    metric_value_1 = model_training_history.history[metric_name_1]
    metric_value_2 = model_training_history.history[metric_name_2]
    
    # Construct a range object which will be used as x-axis (horizontal plane) of the graph.
    epochs = range(len(metric_value_1))

    # Plot the Graph.
    plt.plot(epochs, metric_value_1, 'blue', label = metric_name_1)
    plt.plot(epochs, metric_value_2, 'red', label = metric_name_2)

    # Add title to the plot.
    plt.title(str(plot_name))

    # Add legend to the plot.
    plt.legend()

Now we will utilize the function plot_metric() created above, to visualize and understand the metrics.

plot_metric(convlstm_model_training_history, 'loss', 'val_loss', 'Total Loss vs Total Validation Loss')
plot_metric(convlstm_model_training_history, 'accuracy', 'val_accuracy', 'Total Accuracy vs Total Validation Accuracy') 

Step 5: Implement the LRCN Approach

In this step, we will implement the LRCN Approach by combining Convolution and LSTM layers in a single model. Another similar approach can be to use a CNN model and LSTM model trained separately. The CNN model can be used to extract spatial features from the frames in the video, and for this purpose, a pre-trained model can be used, that can be fine-tuned for the problem. And the LSTM model can then use the features extracted by CNN, to predict the action being performed in the video.

But here, we will implement another approach known as the Long-term Recurrent Convolutional Network (LRCN), which combines CNN and LSTM layers in a single model. The Convolutional layers are used for spatial feature extraction from the frames, and the extracted spatial features are fed to LSTM layer(s) at each time-steps for temporal sequence modeling. This way the network learns spatiotemporal features directly in an end-to-end training, resulting in a robust model.

You can read the paper Long-term Recurrent Convolutional Networks for Visual Recognition and Description by Jeff Donahue (CVPR 2015), to learn more about this architecture.

We will also use TimeDistributed wrapper layer, which allows applying the same layer to every frame of the video independently. So it makes a layer (around which it is wrapped) capable of taking input of shape (no_of_frames, width, height, num_of_channels) if originally the layer's input shape was (width, height, num_of_channels) which is very beneficial as it allows to input the whole video into the model in a single shot.

Step 5.1: Construct the Model

To implement our LRCN architecture, we will use time-distributed Conv2D layers which will be followed by MaxPooling2D and Dropout layers. The feature extracted from the Conv2D layers will be then flattened using the Flatten layer and will be fed to a LSTM layer. The Dense layer with softmax activation will then use the output from the LSTM layer to predict the action being performed.

def create_LRCN_model():
    '''
    This function will construct the required LRCN model.
    Returns:
        model: It is the required constructed LRCN model.
    '''

    # We will use a Sequential model for model construction.
    model = Sequential()
    
    # Define the Model Architecture.
    ########################################################################################################################
    
    model.add(TimeDistributed(Conv2D(16, (3, 3), padding='same',activation = 'relu'),
                              input_shape = (SEQUENCE_LENGTH, IMAGE_HEIGHT, IMAGE_WIDTH, 3)))
    
    model.add(TimeDistributed(MaxPooling2D((4, 4)))) 
    model.add(TimeDistributed(Dropout(0.25)))
    
    model.add(TimeDistributed(Conv2D(32, (3, 3), padding='same',activation = 'relu')))
    model.add(TimeDistributed(MaxPooling2D((4, 4))))
    model.add(TimeDistributed(Dropout(0.25)))
    
    model.add(TimeDistributed(Conv2D(64, (3, 3), padding='same',activation = 'relu')))
    model.add(TimeDistributed(MaxPooling2D((2, 2))))
    model.add(TimeDistributed(Dropout(0.25)))
    
    model.add(TimeDistributed(Conv2D(64, (3, 3), padding='same',activation = 'relu')))
    model.add(TimeDistributed(MaxPooling2D((2, 2))))
    #model.add(TimeDistributed(Dropout(0.25)))
                                      
    model.add(TimeDistributed(Flatten()))
                                      
    model.add(LSTM(32))
                                      
    model.add(Dense(len(CLASSES_LIST), activation = 'softmax'))

    ########################################################################################################################

    # Display the models summary.
    model.summary()
    
    # Return the constructed LRCN model.
    return model

Now we will utilize the function create_LRCN_model() created above to construct the required LRCN model.

LRCN_model = create_LRCN_model()

# Display the success message.
print("Model Created Successfully!")
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 time_distributed_3 (TimeDis  (None, 20, 64, 64, 16)   448       
 tributed)                                                       
                                                                 
 time_distributed_4 (TimeDis  (None, 20, 16, 16, 16)   0         
 tributed)                                                       
                                                                 
 time_distributed_5 (TimeDis  (None, 20, 16, 16, 16)   0         
 tributed)                                                       
                                                                 
 time_distributed_6 (TimeDis  (None, 20, 16, 16, 32)   4640      
 tributed)                                                       
                                                                 
 time_distributed_7 (TimeDis  (None, 20, 4, 4, 32)     0         
 tributed)                                                       
                                                                 
 time_distributed_8 (TimeDis  (None, 20, 4, 4, 32)     0         
 tributed)                                                       
                                                                 
 time_distributed_9 (TimeDis  (None, 20, 4, 4, 64)     18496     
 tributed)                                                       
                                                                 
 time_distributed_10 (TimeDi  (None, 20, 2, 2, 64)     0         
 stributed)                                                      
                                                                 
 time_distributed_11 (TimeDi  (None, 20, 2, 2, 64)     0         
 stributed)                                                      
                                                                 
 time_distributed_12 (TimeDi  (None, 20, 2, 2, 64)     36928     
 stributed)                                                      
                                                                 
 time_distributed_13 (TimeDi  (None, 20, 1, 1, 64)     0         
 stributed)                                                      
                                                                 
 time_distributed_14 (TimeDi  (None, 20, 64)           0         
 stributed)                                                      
                                                                 
 lstm (LSTM)                 (None, 32)                12416     
                                                                 
 dense_1 (Dense)             (None, 4)                 132       
                                                                 
=================================================================
Total params: 73,060
Trainable params: 73,060
Non-trainable params: 0
_________________________________________________________________
Model Created Successfully!

Check Model’s Structure:

Now we will use the plot_model() function to check the structure of the constructed LRCN model. As we had checked for the previous model.

plot_model(LRCN_model, to_file = 'LRCN_model_structure_plot.png', show_shapes = True, show_layer_names = True)

Step 5.2: Compile & Train the Model

After checking the structure, we will compile and start training the model.

#early_stopping_callback = EarlyStopping(monitor = 'val_loss', patience = 15, mode = 'min', restore_best_weights = True)
#, callbacks = [early_stopping_callback]
 
# Compile the model and specify loss function, optimizer and metrics to the model.
LRCN_model.compile(loss = 'categorical_crossentropy', optimizer = 'Adam', metrics = ["accuracy"])

# Start training the model.
LRCN_model_training_history = LRCN_model.fit(x = features_train, y = labels_train, epochs = 70, batch_size = 4 ,
                                             shuffle = True, validation_split = 0.2)
Epoch 1/70
82/82 [==============================] - 3s 21ms/step - loss: 0.0491 - accuracy: 0.9878 - val_loss: 0.3078 - val_accuracy: 0.9398
Epoch 2/70
82/82 [==============================] - 1s 16ms/step - loss: 0.0837 - accuracy: 0.9756 - val_loss: 0.2072 - val_accuracy: 0.9518
Epoch 3/70
82/82 [==============================] - 1s 16ms/step - loss: 0.1060 - accuracy: 0.9665 - val_loss: 0.0990 - val_accuracy: 0.9518
Epoch 4/70
82/82 [==============================] - 1s 16ms/step - loss: 0.0422 - accuracy: 0.9848 - val_loss: 0.0787 - val_accuracy: 0.9639
Epoch 5/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0083 - accuracy: 1.0000 - val_loss: 0.1047 - val_accuracy: 0.9639
Epoch 6/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0032 - accuracy: 1.0000 - val_loss: 0.1349 - val_accuracy: 0.9518
Epoch 7/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0021 - accuracy: 1.0000 - val_loss: 0.1304 - val_accuracy: 0.9518
Epoch 8/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0513 - accuracy: 0.9848 - val_loss: 0.0431 - val_accuracy: 0.9880
Epoch 9/70
82/82 [==============================] - 1s 15ms/step - loss: 0.1271 - accuracy: 0.9634 - val_loss: 0.2294 - val_accuracy: 0.9157
Epoch 10/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0572 - accuracy: 0.9787 - val_loss: 0.2767 - val_accuracy: 0.9518
Epoch 11/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0173 - accuracy: 0.9939 - val_loss: 0.2475 - val_accuracy: 0.9518
Epoch 12/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0589 - accuracy: 0.9878 - val_loss: 0.4520 - val_accuracy: 0.8675
Epoch 13/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0359 - accuracy: 0.9939 - val_loss: 0.2627 - val_accuracy: 0.9518
Epoch 14/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0286 - accuracy: 0.9970 - val_loss: 0.1514 - val_accuracy: 0.9639
Epoch 15/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0730 - accuracy: 0.9726 - val_loss: 0.4235 - val_accuracy: 0.9157
Epoch 16/70
82/82 [==============================] - 1s 15ms/step - loss: 0.1709 - accuracy: 0.9360 - val_loss: 0.3487 - val_accuracy: 0.8916
Epoch 17/70
82/82 [==============================] - 1s 14ms/step - loss: 0.1096 - accuracy: 0.9726 - val_loss: 0.1464 - val_accuracy: 0.9277
Epoch 18/70
82/82 [==============================] - 1s 14ms/step - loss: 0.0161 - accuracy: 0.9939 - val_loss: 0.1094 - val_accuracy: 0.9398
Epoch 19/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0031 - accuracy: 1.0000 - val_loss: 0.0969 - val_accuracy: 0.9518
Epoch 20/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0037 - accuracy: 1.0000 - val_loss: 0.1529 - val_accuracy: 0.9518
Epoch 21/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0029 - accuracy: 1.0000 - val_loss: 0.1425 - val_accuracy: 0.9518
Epoch 22/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0025 - accuracy: 1.0000 - val_loss: 0.1668 - val_accuracy: 0.9518
Epoch 23/70
82/82 [==============================] - 1s 15ms/step - loss: 0.0037 - accuracy: 1.0000 - val_loss: 0.1210 - val_accuracy: 0.9639
Epoch 24/70
82/82 [==============================] - 1s 14ms/step - loss: 0.0014 - accuracy: 1.0000 - val_loss: 0.1450 - val_accuracy: 0.9518
Epoch 25/70
82/82 [==============================] - 1s 14ms/step - loss: 0.0011 - accuracy: 1.0000 - val_loss: 0.1616 - val_accuracy: 0.9518
Epoch 26/70
82/82 [==============================] - 1s 14ms/step - loss: 9.7528e-04 - accuracy: 1.0000 - val_loss: 0.1679 - val_accuracy: 0.9518
Epoch 27/70
82/82 [==============================] - 1s 14ms/step - loss: 8.9347e-04 - accuracy: 1.0000 - val_loss: 0.1660 - val_accuracy: 0.9518
Epoch 28/70
82/82 [==============================] - 1s 14ms/step - loss: 8.2198e-04 - accuracy: 1.0000 - val_loss: 0.1784 - val_accuracy: 0.9518
Epoch 29/70
82/82 [==============================] - 1s 14ms/step - loss: 7.2921e-04 - accuracy: 1.0000 - val_loss: 0.1872 - val_accuracy: 0.9518
Epoch 30/70
82/82 [==============================] - 1s 14ms/step - loss: 6.4949e-04 - accuracy: 1.0000 - val_loss: 0.1900 - val_accuracy: 0.9518
Epoch 31/70
82/82 [==============================] - 1s 14ms/step - loss: 5.8473e-04 - accuracy: 1.0000 - val_loss: 0.1933 - val_accuracy: 0.9518
Epoch 32/70
82/82 [==============================] - 1s 15ms/step - loss: 6.7090e-04 - accuracy: 1.0000 - val_loss: 0.1923 - val_accuracy: 0.9518
Epoch 33/70
82/82 [==============================] - 1s 14ms/step - loss: 4.9970e-04 - accuracy: 1.0000 - val_loss: 0.2070 - val_accuracy: 0.9518
Epoch 34/70
82/82 [==============================] - 1s 14ms/step - loss: 4.7185e-04 - accuracy: 1.0000 - val_loss: 0.2164 - val_accuracy: 0.9518
Epoch 35/70
82/82 [==============================] - 1s 14ms/step - loss: 4.4461e-04 - accuracy: 1.0000 - val_loss: 0.2037 - val_accuracy: 0.9518
Epoch 36/70
82/82 [==============================] - 1s 14ms/step - loss: 4.2033e-04 - accuracy: 1.0000 - val_loss: 0.2329 - val_accuracy: 0.9518
Epoch 37/70
82/82 [==============================] - 1s 14ms/step - loss: 3.7752e-04 - accuracy: 1.0000 - val_loss: 0.2347 - val_accuracy: 0.9518
Epoch 38/70
82/82 [==============================] - 1s 14ms/step - loss: 3.6225e-04 - accuracy: 1.0000 - val_loss: 0.2389 - val_accuracy: 0.9518
Epoch 39/70
82/82 [==============================] - 1s 14ms/step - loss: 3.2556e-04 - accuracy: 1.0000 - val_loss: 0.2449 - val_accuracy: 0.9518
Epoch 40/70
82/82 [==============================] - 1s 14ms/step - loss: 3.2889e-04 - accuracy: 1.0000 - val_loss: 0.2445 - val_accuracy: 0.9518
Epoch 41/70
82/82 [==============================] - 1s 15ms/step - loss: 2.9532e-04 - accuracy: 1.0000 - val_loss: 0.2524 - val_accuracy: 0.9518
Epoch 42/70
82/82 [==============================] - 1s 15ms/step - loss: 3.2840e-04 - accuracy: 1.0000 - val_loss: 0.2588 - val_accuracy: 0.9518
Epoch 43/70
82/82 [==============================] - 1s 15ms/step - loss: 2.7138e-04 - accuracy: 1.0000 - val_loss: 0.2596 - val_accuracy: 0.9518
Epoch 44/70
82/82 [==============================] - 1s 15ms/step - loss: 2.5044e-04 - accuracy: 1.0000 - val_loss: 0.2636 - val_accuracy: 0.9518
Epoch 45/70
82/82 [==============================] - 1s 14ms/step - loss: 2.3714e-04 - accuracy: 1.0000 - val_loss: 0.2630 - val_accuracy: 0.9518
Epoch 46/70
82/82 [==============================] - 1s 14ms/step - loss: 2.2604e-04 - accuracy: 1.0000 - val_loss: 0.2742 - val_accuracy: 0.9518
Epoch 47/70
82/82 [==============================] - 1s 15ms/step - loss: 2.1454e-04 - accuracy: 1.0000 - val_loss: 0.2737 - val_accuracy: 0.9518
Epoch 48/70
82/82 [==============================] - 1s 14ms/step - loss: 1.9916e-04 - accuracy: 1.0000 - val_loss: 0.2794 - val_accuracy: 0.9518
Epoch 49/70
82/82 [==============================] - 1s 14ms/step - loss: 1.8696e-04 - accuracy: 1.0000 - val_loss: 0.2828 - val_accuracy: 0.9518
Epoch 50/70
82/82 [==============================] - 1s 14ms/step - loss: 1.9054e-04 - accuracy: 1.0000 - val_loss: 0.2823 - val_accuracy: 0.9518
Epoch 51/70
82/82 [==============================] - 1s 14ms/step - loss: 1.7845e-04 - accuracy: 1.0000 - val_loss: 0.2749 - val_accuracy: 0.9518
Epoch 52/70
82/82 [==============================] - 1s 15ms/step - loss: 1.6557e-04 - accuracy: 1.0000 - val_loss: 0.2796 - val_accuracy: 0.9518
Epoch 53/70
82/82 [==============================] - 1s 14ms/step - loss: 1.5304e-04 - accuracy: 1.0000 - val_loss: 0.2795 - val_accuracy: 0.9518
Epoch 54/70
82/82 [==============================] - 1s 15ms/step - loss: 1.5462e-04 - accuracy: 1.0000 - val_loss: 0.3114 - val_accuracy: 0.9518
Epoch 55/70
82/82 [==============================] - 1s 14ms/step - loss: 1.5536e-04 - accuracy: 1.0000 - val_loss: 0.2542 - val_accuracy: 0.9518
Epoch 56/70
82/82 [==============================] - 1s 15ms/step - loss: 1.3200e-04 - accuracy: 1.0000 - val_loss: 0.2632 - val_accuracy: 0.9518
Epoch 57/70
82/82 [==============================] - 1s 15ms/step - loss: 1.2617e-04 - accuracy: 1.0000 - val_loss: 0.2696 - val_accuracy: 0.9518
Epoch 58/70
82/82 [==============================] - 1s 15ms/step - loss: 1.2407e-04 - accuracy: 1.0000 - val_loss: 0.2849 - val_accuracy: 0.9518
Epoch 59/70
82/82 [==============================] - 1s 15ms/step - loss: 1.1410e-04 - accuracy: 1.0000 - val_loss: 0.2892 - val_accuracy: 0.9518
Epoch 60/70
82/82 [==============================] - 1s 15ms/step - loss: 1.1018e-04 - accuracy: 1.0000 - val_loss: 0.2923 - val_accuracy: 0.9518
Epoch 61/70
82/82 [==============================] - 1s 14ms/step - loss: 1.0384e-04 - accuracy: 1.0000 - val_loss: 0.2940 - val_accuracy: 0.9518
Epoch 62/70
82/82 [==============================] - 1s 15ms/step - loss: 1.0749e-04 - accuracy: 1.0000 - val_loss: 0.1998 - val_accuracy: 0.9639
Epoch 63/70
82/82 [==============================] - 1s 15ms/step - loss: 9.5134e-05 - accuracy: 1.0000 - val_loss: 0.2080 - val_accuracy: 0.9639
Epoch 64/70
82/82 [==============================] - 1s 14ms/step - loss: 9.5126e-05 - accuracy: 1.0000 - val_loss: 0.2166 - val_accuracy: 0.9639
Epoch 65/70
82/82 [==============================] - 1s 14ms/step - loss: 8.9923e-05 - accuracy: 1.0000 - val_loss: 0.2187 - val_accuracy: 0.9639
Epoch 66/70
82/82 [==============================] - 1s 15ms/step - loss: 9.1264e-05 - accuracy: 1.0000 - val_loss: 0.2349 - val_accuracy: 0.9639
Epoch 67/70
82/82 [==============================] - 1s 14ms/step - loss: 8.2721e-05 - accuracy: 1.0000 - val_loss: 0.2385 - val_accuracy: 0.9639
Epoch 68/70
82/82 [==============================] - 1s 14ms/step - loss: 7.5847e-05 - accuracy: 1.0000 - val_loss: 0.2446 - val_accuracy: 0.9639
Epoch 69/70
82/82 [==============================] - 1s 14ms/step - loss: 1.3082e-04 - accuracy: 1.0000 - val_loss: 0.1858 - val_accuracy: 0.9639
Epoch 70/70
82/82 [==============================] - 1s 15ms/step - loss: 7.6555e-05 - accuracy: 1.0000 - val_loss: 0.2102 - val_accuracy: 0.9639

Evaluating the trained Model

As done for the previous one, we will evaluate the LRCN model on the test set.

model_evaluation_history = LRCN_model.evaluate(features_test, labels_test)
5/5 [==============================] - 0s 36ms/step - loss: 0.2686 - accuracy: 0.9710

Save the Model

After that, we will save the model for future uses using the same technique we had used for the previous model.

model_evaluation_loss, model_evaluation_accuracy = model_evaluation_history

# Define the string date format.
# Get the current Date and Time in a DateTime Object.
# Convert the DateTime object to string according to the style mentioned in date_time_format string.
date_time_format = '%Y_%m_%d__%H_%M_%S'
current_date_time_dt = dt.datetime.now()
current_date_time_string = dt.datetime.strftime(current_date_time_dt, date_time_format)
    
# Define a useful name for our model to make it easy for us while navigating through multiple saved models.
model_file_name = f'LRCN_model___Date_Time_{current_date_time_string}___Loss_{model_evaluation_loss}___Accuracy_{model_evaluation_accuracy}.h5'

# Save the Model.
LRCN_model.save(model_file_name)

Step 5.3: Plot Model’s Loss & Accuracy Curves

Now we will utilize the function plot_metric() we had created above to visualize the training and validation metrics of this model.

plot_metric(LRCN_model_training_history, 'loss', 'val_loss', 'Total Loss vs Total Validation Loss')
plot_metric(LRCN_model_training_history, 'accuracy', 'val_accuracy', 'Total Accuracy vs Total Validation Accuracy')

Step 6: Test the Best Performing Model on YouTube videos

From the results, it seems that the LRCN model performed significantly well for a small number of classes. so in this step, we will put the LRCN model to test on some youtube videos.

Create a Function to Download YouTube Videos:

We will create a function download_youtube_videos() to download the YouTube videos first using pafy library. The library only requires a URL to a video to download it along with its associated metadata like the title of the video.

def download_youtube_videos(youtube_video_url, output_directory):
     '''
    This function downloads the youtube video whose URL is passed to it as an argument.
    Args:
        youtube_video_url: URL of the video that is required to be downloaded.
        output_directory:  The directory path to which the video needs to be stored after downloading.
    Returns:
        title: The title of the downloaded youtube video.
    '''
 
     # Create a video object which contains useful information about the video.
     video = pafy.new(youtube_video_url)
 
     # Retrieve the title of the video.
     title = video.title
 
     # Get the best available quality object for the video.
     video_best = video.getbest()
 
     # Construct the output file path.
     output_file_path = f'{output_directory}/{title}.mp4'
 
     # Download the youtube video at the best available quality and store it to the contructed path.
     video_best.download(filepath = output_file_path, quiet = True)
 
     # Return the video title.
     return title

Download a Test Video:

Now we will utilize the function download_youtube_videos() created above to download a youtube video on which the LRCN model will be tested.

test_videos_directory = 'test_videos'
os.makedirs(test_videos_directory, exist_ok = True)

# Download a YouTube Video.
#url = "https://www.youtube.com/watch?v=8u0qjmHIOcE"
#video_title = pafy.new(url)
video_title = download_youtube_videos('https://www.youtube.com/watch?v=6iTymXuGFhg', test_videos_directory)

# Get the YouTube Video's path we just downloaded.
input_video_file_path = f'{test_videos_directory}/{video_title}.mp4'

print(input_video_file_path)
test_videos/HUGE TREE SWING WITH GOPRO AND CHILDREN! | Sam & Nia.mp4

Create a Function To Perform Action Recognition on Videos

Next, we will create a function predict_on_video() that will simply read a video frame by frame from the path passed in as an argument and will perform action recognition on video and save the results.

def predict_on_video(video_file_path, output_file_path, SEQUENCE_LENGTH):
    '''
    This function will perform action recognition on a video using the LRCN model.
    Args:
    video_file_path:  The path of the video stored in the disk on which the action recognition is to be performed.
    output_file_path: The path where the ouput video with the predicted action being performed overlayed will be stored.
    SEQUENCE_LENGTH:  The fixed number of frames of a video that can be passed to the model as one sequence.
    '''

    # Initialize the VideoCapture object to read from the video file.
    video_reader = cv2.VideoCapture(video_file_path)
    LRCN_MODEL = tf.keras.models.load_model('LRCN_model___Date_Time_2022_07_11__15_32_49___Loss_0.26858294010162354___Accuracy_0.9710144996643066.h5')

    # Get the width and height of the video.
    original_video_width = int(video_reader.get(cv2.CAP_PROP_FRAME_WIDTH))
    original_video_height = int(video_reader.get(cv2.CAP_PROP_FRAME_HEIGHT))

    # Initialize the VideoWriter Object to store the output video in the disk.
    video_writer = cv2.VideoWriter(output_file_path, cv2.VideoWriter_fourcc('M', 'P', '4', 'V'), 
                                   video_reader.get(cv2.CAP_PROP_FPS), (original_video_width, original_video_height))

    # Declare a queue to store video frames.
    frames_queue = deque(maxlen = SEQUENCE_LENGTH)

    # Initialize a variable to store the predicted action being performed in the video.
    predicted_class_name = ''

    # Iterate until the video is accessed successfully.
    while video_reader.isOpened():

        # Read the frame.
        ok, frame = video_reader.read() 
        
        # Check if frame is not read properly then break the loop.
        if not ok:
            break

        # Resize the Frame to fixed Dimensions.
        resized_frame = cv2.resize(frame, (IMAGE_HEIGHT, IMAGE_WIDTH))
        
        # Normalize the resized frame by dividing it with 255 so that each pixel value then lies between 0 and 1.
        normalized_frame = resized_frame / 255

        # Appending the pre-processed frame into the frames list.
        frames_queue.append(normalized_frame)

        # Check if the number of frames in the queue are equal to the fixed sequence length.
        if len(frames_queue) == SEQUENCE_LENGTH:

            # Pass the normalized frames to the model and get the predicted probabilities.
            predicted_labels_probabilities = LRCN_MODEL.predict(np.expand_dims(frames_queue, axis = 0))[0]

            # Get the index of class with highest probability.
            predicted_label = np.argmax(predicted_labels_probabilities)

            # Get the class name using the retrieved index.
            predicted_class_name = CLASSES_LIST[predicted_label]

        # Write predicted class name on top of the frame.
        cv2.putText(frame, predicted_class_name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

        # Write The frame into the disk using the VideoWriter Object.
        video_writer.write(frame)
        
    # Release the VideoCapture and VideoWriter objects.
    video_reader.release()
    video_writer.release()

Perform Action Recognition on the Test Video

Now we will utilize the function predict_on_video() created above to perform action recognition on the test video we had downloaded using the function download_youtube_videos() and display the output video with the predicted action overlayed on it.

output_video_file_path = f'{test_videos_directory}/{video_title}-Output-SeqLen{SEQUENCE_LENGTH}.mp4'

# Perform Action Recognition on the Test Video.
predict_on_video(input_video_file_path, output_video_file_path, SEQUENCE_LENGTH)

# Display the output video.
VideoFileClip(output_video_file_path, audio=False, target_resolution=(300,None)).ipython_display(maxduration=180)

Create a Function To Perform a Single Prediction on Videos

Now let's create a function that will perform a single prediction for the complete videos. We will extract evenly distributed N (SEQUENCE_LENGTH) frames from the entire video and pass them to the LRCN model. This approach is really useful when you are working with videos containing only one activity as it saves unnecessary computations and time in that scenario.

def predict_single_action(video_file_path, SEQUENCE_LENGTH):
    '''
    This function will perform single action recognition prediction on a video using the LRCN model.
    Args:
    video_file_path:  The path of the video stored in the disk on which the action recognition is to be performed.
    SEQUENCE_LENGTH:  The fixed number of frames of a video that can be passed to the model as one sequence.
    '''

    # Initialize the VideoCapture object to read from the video file.
    video_reader = cv2.VideoCapture(video_file_path)

    # Get the width and height of the video.
    original_video_width = int(video_reader.get(cv2.CAP_PROP_FRAME_WIDTH))
    original_video_height = int(video_reader.get(cv2.CAP_PROP_FRAME_HEIGHT))
    
    # Loading Trained LRCN Model.
    LRCN_MODEL = tf.keras.models.load_model('LRCN_model___Date_Time_2022_07_11__15_32_49___Loss_0.26858294010162354___Accuracy_0.9710144996643066.h5')

    # Declare a list to store video frames we will extract.
    frames_list = []
    
    # Initialize a variable to store the predicted action being performed in the video.
    predicted_class_name = ''

    # Get the number of frames in the video.
    video_frames_count = int(video_reader.get(cv2.CAP_PROP_FRAME_COUNT))

    # Calculate the interval after which frames will be added to the list.
    skip_frames_window = max(int(video_frames_count/SEQUENCE_LENGTH),1)

    # Iterating the number of times equal to the fixed length of sequence.
    for frame_counter in range(SEQUENCE_LENGTH):

        # Set the current frame position of the video.
        video_reader.set(cv2.CAP_PROP_POS_FRAMES, frame_counter * skip_frames_window)

        # Read a frame.
        success, frame = video_reader.read() 

        # Check if frame is not read properly then break the loop.
        if not success:
            break

        # Resize the Frame to fixed Dimensions.
        resized_frame = cv2.resize(frame, (IMAGE_HEIGHT, IMAGE_WIDTH))
        
        # Normalize the resized frame by dividing it with 255 so that each pixel value then lies between 0 and 1.
        normalized_frame = resized_frame / 255
        
        # Appending the pre-processed frame into the frames list
        frames_list.append(normalized_frame)

    # Passing the  pre-processed frames to the model and get the predicted probabilities.
    predicted_labels_probabilities = LRCN_MODEL.predict(np.expand_dims(frames_list, axis = 0))[0]

    # Get the index of class with highest probability.
    predicted_label = np.argmax(predicted_labels_probabilities)

    # Get the class name using the retrieved index.
    predicted_class_name = CLASSES_LIST[predicted_label]
    
    # Display the predicted action along with the prediction confidence.
    print(f'Action Predicted: {predicted_class_name}\nConfidence: {predicted_labels_probabilities[predicted_label]}')
        
    # Release the VideoCapture object. 
    video_reader.release()

Perform Single Prediction on a Test Video

Now we will utilize the function predict_single_action() created above to perform a single prediction on a complete youtube test video that we will download using the function download_youtube_videos(), we had created above.

video_title = download_youtube_videos('https://www.youtube.com/watch?v=cnZjjNYsrtw', test_videos_directory)

# Construct tihe nput youtube video path
input_video_file_path = f'{test_videos_directory}/{video_title}.mp4'

# Perform Single Prediction on the Test Video.
predict_single_action(input_video_file_path, SEQUENCE_LENGTH)

# Display the input video.
VideoFileClip(input_video_file_path, audio=False, target_resolution=(300,None)).ipython_display(maxduration=120)
1/1 [==============================] - 6s 6s/step
Action Predicted: Swing
Confidence: 0.9998414516448975
Moviepy - Building video __temp__.mp4.
Moviepy - Writing video __temp__.mp4

                                                                 
Moviepy - Done !
Moviepy - video ready __temp__.mp4