"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">Libraries And Utilities</h3>

"""
import os
import tensorflow                as tf
import pandas                    as pd
import numpy                     as np
import seaborn                   as sns
import cv2                       as cv
import matplotlib.pyplot         as plt
from tqdm.notebook               import tqdm
from sklearn.cluster             import DBSCAN
from sklearn.manifold            import TSNE
from tensorflow.keras            import layers
from sklearn.model_selection     import train_test_split
from tensorflow.keras.models     import Sequential
from tensorflow.keras.models     import Model
from tensorflow.keras.layers     import Dense, Reshape, Flatten, Dropout, Input
from tensorflow.keras.layers     import Conv2D, Conv2DTranspose, LeakyReLU
from tensorflow.keras.optimizers import Adam


plt.rc('figure',figsize=(16,8))
sns.set_context('paper',font_scale=1.5)
def set_seed(seed=31415):
    np.random.seed(seed)
    tf.random.set_seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    os.environ['TF_DETERMINISTIC_OPS'] = '1'
set_seed()
print(tf.__version__)
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">Data Loading and Global Variables</h3>

"""
#GLOBAL VARS
FOLDER_PATH     = '/kaggle/input/6000-children-and-teen-book-covers/BookCovers/'
N_IMAGES        = 6232
LATENT_DIM      = 1024
N_BATCHES       = 128
N_EPOCHS        = 350
BATCH_PER_EPOCH = N_IMAGES // N_BATCHES

# detect and init the TPU
tpu          = tf.distribute.cluster_resolver.TPUClusterResolver.connect()

# instantiate a distribution strategy
tpu_strategy = tf.distribute.experimental.TPUStrategy(tpu)

#Loading Images and Normalizing between 0 and 1
images       = np.array([cv.resize(plt.imread(FOLDER_PATH+i),(128,128),interpolation = cv.INTER_AREA)/ 255.0 for i in os.listdir(FOLDER_PATH)])
_, axs  = plt.subplots(4, 4, figsize=(12, 12))
axs     = axs.flatten()

for img, ax in zip(np.random.choice(np.arange(1,len(images)),4*4), axs):
    ax.imshow(images[img])
    
plt.tight_layout()
plt.show()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">Creating an Autoencoder</h3>

"""
"""
**What is an autoencoder** - An autoencoder is a type of artificial neural network used to learn efficient codings of unlabeled data (unsupervised learning).The encoding is validated and refined by attempting to regenerate the input from the encoding. The autoencoder learns a representation (encoding) for a set of data, typically for dimensionality reduction, by training the network to ignore insignificant data (“noise”).

Variants exist, aiming to force the learned representations to assume useful properties. Examples are regularized autoencoders (Sparse, Denoising and Contractive), which are effective in learning representations for subsequent classification tasks, and Variational autoencoders, with applications as generative models.Autoencoders are applied to many problems, from facial recognition to acquiring the meaning of words. (Wikipedia.com)


<img src=https://i.ibb.co/WWStX76/enbocedrdecoderarc.png alt="AEA" class="center">
"""
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">The Encoder</h3>

"""
"""
<img src=https://i.ibb.co/2d2PQNn/Fig-A1-The-standard-VGG-16-network-architecture-as-proposed-in-32-Note-that-only.png alt="AEA" class="center">
"""
with tpu_strategy.scope() as tpu:

    Encoder = Sequential(name='Encoder')

    Encoder.add( tf.keras.applications.VGG16(input_shape=(128,128,3),
                                               include_top=False,
                                               weights='imagenet'))
    Encoder.layers[0].trainable = False

    Encoder.add(Flatten())
    Encoder.add(Dense(LATENT_DIM, activation='sigmoid'))
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">The Decoder</h3>

"""
"""


<img src=https://i.ibb.co/WF3qZmH/generator.png alt="AEA" class="center">
"""
with tpu_strategy.scope() as tpu:

    Decoder = Sequential(name='Decoder')

    Decoder.add(Dense(256*16*16, input_dim=LATENT_DIM)) 
    Decoder.add(LeakyReLU(alpha=0.2))
    Decoder.add(Reshape((16, 16, 256)))

    Decoder.add(Conv2DTranspose(128, (4,4), strides=(2,2), padding='same'))
    Decoder.add(LeakyReLU(alpha=0.2))

    Decoder.add(Conv2DTranspose(128, (4,4), strides=(2,2), padding='same'))
    Decoder.add(LeakyReLU(alpha=0.2))

    Decoder.add(Conv2DTranspose(128, (4,4), strides=(2,2), padding='same'))
    Decoder.add(LeakyReLU(alpha=0.2))


    Decoder.add(Conv2D(3, (8,8), activation='sigmoid', padding='same'))
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">Assembled Autoencoder</h3>

"""
with tpu_strategy.scope() as tpu:

    autoencoder = Sequential(name='autoencoder')
    autoencoder.add(Encoder) 
    autoencoder.add(Decoder)
    
    # Loss functtion
    def ssim_loss(y_true, y_pred):
        SSIM = tf.reduce_mean(tf.image.ssim(y_true, y_pred, 1.0))
        return (1-SSIM)/2
    
    #Optimizer
    opt = Adam(lr=0.0002, beta_1=0.8)

    
    autoencoder.compile(loss=ssim_loss, optimizer=opt)
autoencoder.summary()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">Training and Evaluating the Autoencoder</h3>

"""
autoencoder.fit(images, images,
                epochs=150,
                batch_size=16,
                shuffle=True,verbose=0)
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">Original Sample of Images</h3>

"""
predictions = autoencoder.predict(images)
R=np.random.choice(np.arange(1,len(images)),4*4)
predictions = autoencoder.predict(images)

_, axs = plt.subplots(4, 4, figsize=(12, 12))
axs = axs.flatten()
for img, ax in zip(R, axs):
    ax.imshow(images[img])
plt.tight_layout()
plt.show()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">Reconstructed Sample of Images</h3>

"""
_, axs = plt.subplots(4, 4, figsize=(12, 12))
axs = axs.flatten()
for img, ax in zip(R, axs):
    ax.imshow(predictions[img])
plt.tight_layout()
plt.show()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">Extracting Image Latent Representation Via Our Encoder</h3>

"""
#Extracting Both Parts of Our Autoencoder
decoder = autoencoder.get_layer('Decoder')
encoder = autoencoder.get_layer('Encoder')

#Converting Images into a Latent Representation
latent_representation = encoder.predict(images,verbose=1)
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">Exploring the Latent Dimension</h3>

"""
tsne = TSNE(n_components= 2)
r2 = tsne.fit_transform(latent_representation)
dbs = DBSCAN(eps=0.05,min_samples = 2)
r2_df = pd.DataFrame({'d1':r2[:,0],'d2':r2[:,1]})
dbs.fit(r2)
r2_df['cluster'] = dbs.labels_
import plotly.express as ex

ex.scatter(r2_df,x='d1',y='d2',color='cluster',title='Distribution of Images in Reduced Latent Dimension')
"""
**Note**: After reducing the dimensionality of our latent representation, we can clearly see clusters within our data that we can interpret as similar images.
DBSCAN clustering was applied in order to extract the cluster of highest density with the hypothesis that those will be books from the same series or simply of the same theme.
"""
indecies = r2_df[r2_df.cluster == r2_df.cluster.value_counts().index[1]].index.to_list()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:150%;text-align:center;border-radius: 15px 50px;">Resulting Image From Sampled Cluster</h3>

"""
_, axs = plt.subplots(5, 4, figsize=(12, 12))
axs = axs.flatten()
for img, ax in zip(indecies, axs):
    ax.imshow(images[img])
plt.tight_layout()
plt.show()
"""
<h3 style="background-color:orange;font-family:newtimeroman;font-size:200%;text-align:center;border-radius: 15px 50px;">"Walking" in The Latent Dimension</h3>


**Note**: In this part, as a small fun experiment, we will select two random pictures and take a "walk" starting at the position of the first image in the latent dimension and going towards the second image, such a "walk" is achieved via linear interpolation.
"""
R2=np.random.choice(np.arange(1,len(images)),2)
img1 = images[R2[0]]
img2 = images[R2[1]]
plt.subplot(1,2,1)
plt.title('img1')
plt.imshow(img1)
plt.subplot(1,2,2)
plt.title('img2')
plt.imshow(img2)
img1 = latent_representation[R2[0]]
img2 = latent_representation[R2[1]]

interpolated = np.array([alpha*img1 + (1-alpha)*img2 for alpha in np.arange(0,1,0.1)])
examples = decoder.predict(interpolated)


plt.figure(figsize=(20,11))
for i in range(10):
    plt.title(f'Transition {i+1}')
    plt.subplot(2, 5, 1 + i)
    plt.axis('off')
    plt.imshow(examples[i])
plt.show()