"""
# 1- Imports
"""
import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt 
import seaborn as sns
import scipy as sp
import tensorflow as tf 
import tensorflow_hub as hub
import tensorflow_datasets as tfds
import pathlib
import os
from sklearn.metrics import accuracy_score
import PIL
import requests
from io import BytesIO
import urllib
import cv2
from skimage import io
"""
# 2- ImageDataGenerator
"""
training_dataset_path = pathlib.Path(r"../input/cats-vs-dogs/data/train")
validation_dataset_path = pathlib.Path(r"../input/cats-vs-dogs/data/validation")
img_height = 224
img_width = 224


training_data_gen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1/255.0,
                                                                   rotation_range=40,
                                                                   zoom_range=0.2,
                                                                   width_shift_range=0.2,
                                                                   height_shift_range=0.2,
                                                                   shear_range=0.2,
                                                                   horizontal_flip=True,
                                                                   vertical_flip=True)

training_generator = training_data_gen.flow_from_directory(training_dataset_path,
                                                          target_size=(224, 224),
                                                          batch_size=32,
                                                          class_mode='binary',
                                                          seed=101)
#------------------------------------------------------------------------------------------------------
val_data_gen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1/255.0)
validation_generator = val_data_gen.flow_from_directory(validation_dataset_path,
                                                        target_size=(224, 224),
                                                        batch_size=32,
                                                        class_mode='binary',
                                                        seed=101)

"""
# 3- Building the model
"""
my_model = tf.keras.models.Sequential()

my_model.add(tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)))
my_model.add(tf.keras.layers.MaxPooling2D(2))


my_model.add(tf.keras.layers.Conv2D(64, (3, 3), activation='relu'))
my_model.add(tf.keras.layers.MaxPooling2D(2))


my_model.add(tf.keras.layers.Conv2D(128, (3, 3), activation='relu'))
my_model.add(tf.keras.layers.MaxPooling2D(2))


my_model.add(tf.keras.layers.Conv2D(128, (3, 3), activation='relu'))
my_model.add(tf.keras.layers.MaxPooling2D(2))

my_model.add(tf.keras.layers.Conv2D(256, (3, 3), activation='relu'))
my_model.add(tf.keras.layers.MaxPooling2D(2))


my_model.add(tf.keras.layers.GlobalAveragePooling2D())

my_model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
my_model.summary()
my_model.compile(loss='binary_crossentropy',
                 optimizer=tf.keras.optimizers.SGD(),
                 metrics=['acc'])
history = my_model.fit(training_generator, steps_per_epoch=500,
                       validation_data=validation_generator, 
                       validation_steps=281, epochs=15) 
my_model.layers
"""
# 4- Generate the Class Activation Map (CAM)
"""
"""
Class Activation Map (CAM): is a matrix that shows what parts of the image the model was paying attension to when deciding what class assign to the image. its a map of which pixels of the image sent activation that we use to determine the class of the object 
"""
"""
to generate the class activation map, we want to get the features detected in the last convolution layer and see which ones are most active when generating the output probabilities.
"""
validation_generator.class_indices
# final convolution layer
print(my_model.layers[-3].name)

# global average pooling layer
print(my_model.layers[-2].name)

# output of the classifier
print(my_model.layers[-1].name)
"""
### create your CAM model
"""
# same as previous model but with an additional output
cam_model  = tf.keras.Model(inputs=my_model.input,outputs=(my_model.layers[-3].output,my_model.layers[-1].output))
cam_model.summary()
"""
Use the CAM model to predict on the test set, so that it generates the features and the predicted probability for each class (results).
"""
# get the features and results of the test images using the newly created model
features, results = cam_model.predict(validation_generator)

# shape of the features
print("features shape: ", features.shape)
print("results shape: ", results.shape)
"""
we can generate the CAM by getting the dot product of the class activation features and the class activation weights.

we will need the weights from the Global Average Pooling layer (GAP) to calculate the activations of each feature given a particular class.

- Note that you'll get the weights from the dense layer that follows the global average pooling layer.

- The last conv2D layer has (h,w,depth) of (5 x 5 x 256), so there are 256 features.

- The global average pooling layer collapses the h,w,f (5 x 5 x 256) into a dense layer of 256 neurons (1 neuron per feature).

- The activations from the global average pooling layer get passed to the last dense layer.

- The last dense layer assigns weights to each of those 256 features (for each of the 10 classes), 

- So the weights of the last dense layer (which immmediately follows the global average pooling layer) are referred to in this context as the "weights of the global average pooling layer".

For each of the 10 classes, there are 256 features, so there are 256 feature weights, one weight per feature.
"""
# these are the weights going into the softmax layer
last_dense_layer = my_model.layers[-1]

# get the weights list.  index 0 contains the weights, index 1 contains the biases
gap_weights_l = last_dense_layer.get_weights()

print("gap_weights_l index 0 contains weights ", gap_weights_l[0].shape)
print("gap_weights_l index 1 contains biases ", gap_weights_l[1].shape)

# shows the number of features per class, and the total number of classes
# Store the weights
gap_weights = gap_weights_l[0]

print(f"There are {gap_weights.shape[0]} feature weights and {gap_weights.shape[1]} classes.")
def Make_a_prediction_on_an_image_from_the_web(url):
    
    response = requests.get(url)
    img = PIL.Image.open(BytesIO(response.content))
    img = img.resize((224, 224))
    img_array = np.array(img)
    img_array_exp = np.expand_dims(img_array, axis=0)

    features_web, results_web = cam_model.predict(img_array_exp)
    
    # takes the features of the chosen image
    features_for_img = features_web[0,:,:,:]
    # get the class with the highest output probability
    prediction_prob = results_web[0]

    if prediction_prob >= 0.5: 
        prediction = "Dog"
        prob = prediction_prob[0]
    else:
        prediction = "Cat"
        prob = 1 - prediction_prob[0]

    # get the gap weights at the predicted class
    class_activation_weights = gap_weights[:,0]

    # upsample the features to the image's original size (224 x 224)
    class_activation_features = sp.ndimage.zoom(features_for_img, (224/5, 224/5, 1), order=2)

    # compute the intensity of each feature in the CAM
    cam_output  = np.dot(class_activation_features,class_activation_weights)
    

    print('Predicted Class = ' +str(prediction)+ ', Probability = ' + str(prob))

    #--------------------------------------------
    
    fig=plt.figure(figsize=(12, 7))
    
    fig.add_subplot(2,2,1)
    plt.imshow(cam_output, cmap='Reds')
    plt.colorbar()
    
    fig.add_subplot(2,2,2)
    plt.imshow(img_array)
    
    fig.add_subplot(2,2,3)
    #plt.figure(figsize = (7,7))
    plt.imshow(cam_output, cmap='Reds') #, alpha=1.5)
    plt.imshow(img_array, alpha=0.5)
    plt.axis('off')
    
url_1 = "https://upload.wikimedia.org/wikipedia/commons/1/18/Dog_Breeds.jpg"
Make_a_prediction_on_an_image_from_the_web(url_1)
url_2 = "https://i.pinimg.com/originals/cd/94/f0/cd94f0089700a1c01407d9f409e2d9b6.jpg"
Make_a_prediction_on_an_image_from_the_web(url_2)
url_3 = "https://upload.wikimedia.org/wikipedia/commons/7/74/Golden_Retrievers_dark_and_light.jpg"
Make_a_prediction_on_an_image_from_the_web(url_3)
url_4 = "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/cats-that-look-like-tigers-bengal-1562690663.jpg"
Make_a_prediction_on_an_image_from_the_web(url_4)
"""
# 5- Visualizing intermediate activations
"""
"""
**Have you ever asked about the responce map what looks like?**

After the training of the Convolutional network, if we pass an image to it we can extract the feature maps that produced
by the proceesing of this image using network layers. 

Each layer contain learnable filters that trained to detect certain featire from the input of the layer. The layer may contain 
any number of filters 32, 64, ... each of them detect certain feature and produce a feature map, so a layer that have 
64 filter will produce 64 feature map. 

in this notebook we will see the feature maps produced by each convolutional and pooling layer in a simple network we will train. 


**The code referance: Deep Learning with Python by François Chollet**
"""
"""
Neural networks are black boxes where we can not extract the learning represntations in a form that the human can understand, 
but it in not true for ConvNets, Where we can for example visualize intermediate convnet outputs, convnet filters, and heatmaps of class activation in an image. 
"""
"""
Visualizing intermediate convnet outputs is the process of displaying the feature maps that output by various convolution and pooling layers in the netweork given an input. 
"""
"""
We will start by making our image data generator for the training, validation, test sets then structuring a ConvNet, it's a simple and traditional network, series of Conolutional and pooling layers then at the top we add Dense layers (The classifier)
"""
from tensorflow.keras.preprocessing import image


def visualize (image_path):
    
    #take image path and load the image
    img = image.load_img(image_path, target_size=(224, 224))
    #trasform the image to array
    img_tensor = image.img_to_array(img)
    img_tensor = np.expand_dims(img_tensor, axis=0)
    #scaling the image
    img_tensor /= 255.0

    # model that takes the input image and return the activations for the
    # conv and pool layers in the trained model 
    layer_outputs = [layer.output for layer in my_model.layers[:10]]
    activation_model = tf.keras.models.Model(inputs=my_model.input, outputs=layer_outputs)
    
    activation = activation_model.predict(img_tensor)
    
    layer_names = []

    for layer in my_model.layers[:10]:
        layer_names.append(layer.name)

    images_per_row = 16

    for layer_name, layer_activation in zip(layer_names, activation):
        n_features = layer_activation.shape[-1]

        size = layer_activation.shape[1]

        n_cols = n_features // images_per_row
        display_grid = np.zeros((size * n_cols, images_per_row * size))

        for col in range(n_cols):
            for row in range(images_per_row):
                channel_image = layer_activation[0,
                                                :, :,
                                                col * images_per_row + row]

                channel_image -= channel_image.mean()
                channel_image /= channel_image.std()
                channel_image *= 64
                channel_image += 128
                channel_image = np.clip(channel_image, 0, 255).astype('uint8')
                display_grid[col * size : (col + 1) * size, 
                            row * size : (row + 1) * size] = channel_image

        scale = 1. / size
        plt.figure(figsize=(scale * display_grid.shape[1],
                            scale * display_grid.shape[0]))

        plt.title(layer_name)
        plt.grid(False)
        plt.imshow(display_grid, aspect='auto', cmap='viridis')   
cat_img_path = r"../input/cats-vs-dogs/data/validation/cats/cat.10088.jpg"
visualize (cat_img_path)
dog_img_path = r"../input/cats-vs-dogs/data/validation/dogs/dog.10068.jpg"
visualize (dog_img_path)
"""
The network act like Information distillation pipeline where irrelevant information is filtered out and the useful information is refined. As we can see as we go deeper in the network activations become less visuallyinterpretable.
"""
"""
# 6- Saliency

"""
"""
- Saliency map: is a representation of every pixel in the image in way that make sense for our particular context. 
    
- it's define the pixels in the image that end up having an impact on the classification of the image
"""
# the_class is 0 for cats and 1 for dogs
def veiw_saliency_map_for_binary_classification_model(url, the_class):
    
    #The image needs to be preprocessed before being fed to the model
    
    # read the image
    img = io.imread(url)

    # resize to 300x300 and normalize pixel values to be in the range [0, 1]
    img = cv2.resize(img, (300, 300)) / 255.0

    # add a batch dimension in front
    image = np.expand_dims(img, axis=0)
    #------------------------------------------------------------------------
    # compute the gradient of the loss with respect to the input image,
    # this should tell us how the loss value changes with respect to
    # a small change in input image pixels
    
    expected_output = tf.constant(the_class, shape=(1, 1))
    with tf.GradientTape() as tape:
        # cast image to float
        inputs = tf.cast(image, tf.float32)

        # watch the input pixels
        tape.watch(inputs)

        # generate the predictions
        predictions = my_model(inputs)

        # get the loss
        loss = tf.keras.losses.binary_crossentropy(
            expected_output, predictions
        )

    # get the gradient with respect to the inputs
    gradients = tape.gradient(loss, inputs)
    # The gradient tells us how much the categorical loss changes when the input change,
    #also it can indicate when certain parts of the input image have a
    #significat effect on the categorical prediction
    #------------------------------------------------------------------------
    # reduce the RGB image to grayscale
    grayscale_tensor = tf.reduce_sum(tf.abs(gradients), axis=-1)

    # normalize the pixel values to be in the range [0, 255].
    # the max value in the grayscale tensor will be pushed to 255.
    # the min value will be pushed to 0.
    normalized_tensor = tf.cast(
        255
        * (grayscale_tensor - tf.reduce_min(grayscale_tensor))
        / (tf.reduce_max(grayscale_tensor) - tf.reduce_min(grayscale_tensor)),
        tf.uint8,
    )

    # remove the channel dimension to make the tensor a 2d tensor
    normalized_tensor = tf.squeeze(normalized_tensor)
    #------------------------------------------------------------------------
    gradient_color = cv2.applyColorMap(normalized_tensor.numpy(), cv2.COLORMAP_HOT)
    gradient_color = gradient_color / 255.0
    super_imposed = cv2.addWeighted(img, 0.5, gradient_color, 0.5, 0.0)

    plt.figure(figsize=(8, 8))
    plt.imshow(super_imposed)
    plt.axis('off')
    plt.show()
url = "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/cats-that-look-like-tigers-bengal-1562690663.jpg"
veiw_saliency_map_for_binary_classification_model(url, 0)
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Dog_for_Senior_Dog_Food_Diet_Wikipedia_Page.jpg/1200px-Dog_for_Senior_Dog_Food_Diet_Wikipedia_Page.jpg"
veiw_saliency_map_for_binary_classification_model(url, 1)
"""
So in the image above we can see that the model use the dog eyes nose and mouth to identify is class 
"""
"""
# 7- Thank you 
"""
"""
Thank you for reading, I hope you enjoyed and benefited from it.

If you have any questions or notes please leave it in the commont section.

If you like this notebook please press upvote and thanks again.
"""