# Versão da Linguagem Python
from platform import python_version
print('Versão da Linguagem Python Usada Neste Jupyter Notebook:', python_version())

# Instala o TensorFlow
#!pip install -q tensorflow==2.5

# Imports
import sklearn
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from pathlib import Path
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from tensorflow.keras.layers.experimental.preprocessing import RandomFlip
from tensorflow.keras.layers.experimental.preprocessing import RandomRotation
from tensorflow.keras.layers.experimental.preprocessing import RandomZoom
from tensorflow.keras.applications import EfficientNetB3
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.metrics import Precision
from tensorflow.keras.metrics import Recall

# Seed para reprodutibilidade
tf.random.set_seed(4)

"""
Explanation: <font color='blue'>Data Science Academy - Python Fundamentos - Capítulo 12</font>
Download: http://github.com/dsacademybr
Mini-Projeto 4 - Inteligência Artificial na Agricultura
ATENÇÃO
Este Mini-Projeto é um bônus com nível intermediário/avançado e o que apresentaremos aqui é apenas uma demonstração. Os conceitos necessários para executar este Mini-Projeto são estudados em detalhes na <a href="https://www.datascienceacademy.com.br/bundle/formacao-inteligencia-artificial">Formação Inteligência Artificial</a> e <a href="https://www.datascienceacademy.com.br/bundle/formacao-inteligencia-artificial-aplicada-a-medicina">Formação Inteligência Artificial Aplicada à Medicina</a> aqui na DSA.

Definição do Problema
Acesse o manual em pdf no Capítulo 12 do curso.
Fonte de Dados
Acesse o manual em pdf no Capítulo 12 do curso.
Instalando e Carregando Pacotes
End of explanation
"""


# Diretório atual
diretorio_atual = Path.cwd()
print(diretorio_atual)

# Caminho para os dados de treino
caminho_dados_treino = Path("fruits-360/Training")

# Caminho para os dados de teste
caminho_dados_teste = Path("fruits-360/Test")

# Listando o conteúdo da pasta
imagens_treino = list(caminho_dados_treino.glob("*/*"))

# Visualiza uma amostra da lista
imagens_treino[925:936]

# Expressão lambda que extrai apenas o valor com o caminho de cada imagem
imagens_treino = list(map(lambda x: str(x), imagens_treino))

# Visualiza uma amostra da lista
imagens_treino[925:936]

# Total de imagens de treino
len(imagens_treino)

"""
Explanation: Carregando os Dados (Imagens)
End of explanation
"""


# Função que obtém o label de cada imagem
def extrai_label(caminho_imagem):
    return caminho_imagem.split("/")[-2]

# Aplica a função
imagens_treino_labels = list(map(lambda x: extrai_label(x), imagens_treino))

# Visualiza uma amostra
imagens_treino_labels[840:846]

"""
Explanation: Pré-Processamento dos Dados
End of explanation
"""


# Cria o objeto
encoder = LabelEncoder()

# Aplica o fit_transform
imagens_treino_labels = encoder.fit_transform(imagens_treino_labels)

# Visualiza uma amostra
imagens_treino_labels[840:846]

# Aplicamos One-Hot-Encoding nos labels
imagens_treino_labels = tf.keras.utils.to_categorical(imagens_treino_labels)

# Visualiza uma amostra
imagens_treino_labels[840:846]

# Dividimos os dados de treino em duas amostras, treino e validação
X_treino, X_valid, y_treino, y_valid = train_test_split(imagens_treino, imagens_treino_labels)

X_treino[15:18]

y_treino[15:18]

"""
Explanation: Label encoding (convertendo string para valor numérico)
End of explanation
"""


# Redimensionamento de todas as imagens para 224 x 224
img_size = 224
resize = tf.keras.Sequential([tf.keras.layers.experimental.preprocessing.Resizing(img_size, img_size)])

# Cria o objeto para dataset augmentation
data_augmentation = tf.keras.Sequential([RandomFlip("horizontal"),
                                         RandomRotation(0.2),
                                         RandomZoom(height_factor = (-0.3,-0.2)) ])

"""
Explanation: Dataset Augmentation
End of explanation
"""


# Hiperparâmnetros
batch_size = 32
autotune = tf.data.experimental.AUTOTUNE

# Função para carregar e transformar as imagens
def carrega_transforma(image, label):
    image = tf.io.read_file(image)
    image = tf.io.decode_jpeg(image, channels = 3)
    return image, label

# Função para preparar os dados noo formato do TensorFlow
def prepara_dataset(path, labels, train = True):

    # Prepara os dados
    image_paths = tf.convert_to_tensor(path)
    labels = tf.convert_to_tensor(labels)
    image_dataset = tf.data.Dataset.from_tensor_slices(image_paths)
    label_dataset = tf.data.Dataset.from_tensor_slices(labels)
    dataset = tf.data.Dataset.zip((image_dataset, label_dataset))
    dataset = dataset.map(lambda image, label: carrega_transforma(image, label)) 
    dataset = dataset.map(lambda image, label: (resize(image), label), num_parallel_calls = autotune)
    dataset = dataset.shuffle(1000)
    dataset = dataset.batch(batch_size)

    # Se train = True aplica dataset augmentation
    if train:
        dataset = dataset.map(lambda image, label: (data_augmentation(image), label), num_parallel_calls = autotune)
  
    # Se train = False repete sobre o dataset e retorna
    dataset = dataset.repeat()

    return dataset

# Cria o dataset de treino
dataset_treino = prepara_dataset(X_treino, y_treino)

# Shape
imagem, label = next(iter(dataset_treino))
print(imagem.shape)
print(label.shape)

# Vamos visualizar uma imagem e um label
print(encoder.inverse_transform(np.argmax(label, axis = 1))[0])
plt.imshow((imagem[0].numpy()/255).reshape(224,224,3))

# Cria o dataset de validação
dataset_valid = prepara_dataset(X_valid, y_valid, train = False)

# Shape
imagem, label = next(iter(dataset_valid))
print(imagem.shape) 
print(label.shape)

"""
Explanation: Preparando os Dados
End of explanation
"""


# Carregando um modelo pré-treinado
modelo_pre = EfficientNetB3(input_shape = (224,224,3), include_top = False)

# Adicionando nossas próprias camadas ao modelo_pre
modelo = tf.keras.Sequential([modelo_pre,
                              tf.keras.layers.GlobalAveragePooling2D(),
                              tf.keras.layers.Dense(131, activation = 'softmax')])

# Sumário do modelo
modelo.summary()

# Hiperparâmetros
lr = 0.001
beta1 = 0.9
beta2 = 0.999
ep = 1e-07

# Compilação do modelo
modelo.compile(optimizer = Adam(learning_rate = lr, 
                                beta_1 = beta1, 
                                beta_2 = beta2, 
                                epsilon = ep),
               loss = 'categorical_crossentropy',
               metrics = ['accuracy', Precision(name = 'precision'), Recall(name = 'recall')])

"""
Explanation: Construção do Modelo
End of explanation
"""


%%time
history = modelo.fit(dataset_treino,
                     steps_per_epoch = len(X_treino)//batch_size,
                     epochs = 1,
                     validation_data = dataset_valid,
                     validation_steps = len(y_treino)//batch_size)

"""
Explanation: Vamos treinar o modelo por apenas uma época e verificar as métricas.

Nota: Se você treinar o modelo em um computador sem GPU o tempo de treinamento será bastante alto, provavelmente de muitas horas. Pratique a paciência e aguarde.
End of explanation
"""


# Não precisamos mais do modelo_pre
modelo.layers[0].trainable = False

# Checkpoint
checkpoint = tf.keras.callbacks.ModelCheckpoint("modelo/melhor_modelo.h5", 
                                                verbose = 1, 
                                                save_best = True, 
                                                save_weights_only = True)

# Early stop
early_stop = tf.keras.callbacks.EarlyStopping(patience = 4) 

# Sumário
modelo.summary()

%%time
history = modelo.fit(dataset_treino,
                     steps_per_epoch = len(X_treino)//batch_size,
                     epochs = 6,
                     validation_data = dataset_valid,
                     validation_steps = len(y_treino)//batch_size,
                     callbacks = [checkpoint, early_stop])

"""
Explanation: Vamos treinar o modelo por mais 6 épocas a fim de melhorar a performance e aplicar algumas técnicas para evitar overfitting.
End of explanation
"""


# Para carregar os pesos, precisamos descongelar as camadas
modelo.layers[0].trainable = True

# Carrega os pesos do ponto de verificação e reavalie
modelo.load_weights("modelo/melhor_modelo.h5")

"""
Explanation: Avaliação do Modelo
End of explanation
"""


# Carregando e preparando os dados de teste
camninho_imagens_teste = list(caminho_dados_teste.glob("*/*"))
imagens_teste = list(map(lambda x: str(x), camninho_imagens_teste))
imagens_teste_labels = list(map(lambda x: extrai_label(x), imagens_teste))
imagens_teste_labels = encoder.fit_transform(imagens_teste_labels)
imagens_teste_labels = tf.keras.utils.to_categorical(imagens_teste_labels)
test_image_paths = tf.convert_to_tensor(imagens_teste)
test_image_labels = tf.convert_to_tensor(imagens_teste_labels)

# Função para decode das imagens
def decode_imagens(image, label):
    image = tf.io.read_file(image)
    image = tf.io.decode_jpeg(image, channels = 3)
    image = tf.image.resize(image, [224,224], method = "bilinear")
    return image, label

# Cria o dataset de teste
dataset_teste = (tf.data.Dataset
                 .from_tensor_slices((imagens_teste, imagens_teste_labels))
                 .map(decode_imagens)
                 .batch(batch_size))

# Shape
imagem, label = next(iter(dataset_teste))
print(imagem.shape)
print(label.shape)

# Visualiza uma imagem de teste
print(encoder.inverse_transform(np.argmax(label, axis = 1))[0])
plt.imshow((imagem[0].numpy()/255).reshape(224,224,3))

# Avalia o modelo
loss, acc, prec, rec = modelo.evaluate(dataset_teste)

print("Acurácia: ", acc)
print("Precision: ", prec)
print("Recall: ", rec)

"""
Explanation: Carregamos os dados de teste.
End of explanation
"""


# Função para carregar uma nova imagem
def carrega_nova_imagem(image_path):
    image = tf.io.read_file(image_path)
    image = tf.io.decode_jpeg(image, channels = 3)
    image = tf.image.resize(image, [224,224], method = "bilinear")
    plt.imshow(image.numpy()/255)
    image = tf.expand_dims(image, 0) 
    return image

# Função para fazer previsões
def faz_previsao(image_path, model, enc):
    image = carrega_nova_imagem(image_path)
    prediction = model.predict(image)
    pred = np.argmax(prediction, axis = 1) 
    return enc.inverse_transform(pred)[0] 

# Previsão
faz_previsao("imagens/imagem1.jpg", modelo, encoder)

# Previsão
faz_previsao("imagens/imagem2.jpg", modelo, encoder)

# Previsão
faz_previsao("imagens/imagem3.jpg", modelo, encoder)

# Previsão
faz_previsao("imagens/imagem4.jpg", modelo, encoder)

# Previsão
faz_previsao("imagens/imagem5.jpg", modelo, encoder)

"""
Explanation: Previsões com o Modelo Treinado
End of explanation
"""


