# -*- coding: utf-8 -*-
"""Fresh and Stale Images of Fruits and Vegetables Classification.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/13Vk9VOlRcgCYqDDUvFbFHDxcBSqmYcFN

# Fresh and Stale Images of Fruits and Vegetables Classification

The used dataset contains images of 6 fruits and vegetables: ***apple, banana, bitter gourd, capsicum, orange, and tomato***. The images of each fruit or vegetable are grouped into two categories: fresh and stale. The purpose behind the creation of this dataset is the development of a machine learning model to classify fruits and vegetables as fresh or stale. The dataset can be downloaded at https://www.kaggle.com/raghavrpotdar/fresh-and-stale-images-of-fruits-and-vegetables

Here I developed 2 architectural models, namely **CCN from Scratch** and **VGG16 + CNN with Transfer Learning**. From the training results obtained the following accuracy:

<center>
  <b>Table 1. Comparison between Models</b>
</center>

| Model | Tran Acc. (%) | Validation Acc. (%) | Time |
|-------|---------------|---------------------|------|
|CNN | 85.90  | 89.43  | 13m 46s |
|VGG16 + CNN | 90.25  | 91.74  | 13m 48s |


### TF-Lite Model Filename

- Model CCN from Scratch: ```best_model-cnn-d9373038-e2d4-11eb-acbf-0242ac1c0002.h5.tflite```
- Model VGG16 + CNN with Transfer Learning: ```best_model-vgg16-d0f03484-e2d8-11eb-acbf-0242ac1c0002.h5.tflite```
"""

# install library needed
!pip install -q kaggle
!pip install split_folders
!pip install awesome-slugify

# import library needed
import tensorflow as tf
from tensorflow import keras
from keras.utils.vis_utils import plot_model
from keras.optimizers import Adam
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from keras.models import load_model, Sequential
from keras import regularizers
from keras.callbacks import ModelCheckpoint, EarlyStopping, CSVLogger
from keras import layers, Model
from keras.applications.vgg16 import VGG16

from google.colab import files
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
import seaborn as sn
import cv2
import numpy as np
import pandas as pd

import os
import time
import uuid
import glob
import warnings
import splitfolders
from tqdm import tqdm
from IPython.display import Markdown, display
from slugify import slugify

# Ignore the unwanted warning messages
warnings.filterwarnings('ignore')

# Define base path for worksheet
BASE_PATH = '/content/drive/MyDrive/Playground/Fresh and Stale'
# Size of input images
IMAGE_HEIGHT=224
IMAGE_WIDTH=224
# No. of class
NUM_CLASS=12
# No. of images to be yielded from the generator per batch
BATCH_SIZE=32
# Set the dropout
DROPOUT=0.2
# Learning rate
LEARNING_RATE=0.001
# Number of iterations
EPOCH=50

# Mount our google drive
from google.colab import drive
drive.mount('/content/drive')

"""## Preprocessing

### Data Gathering
"""

# upload kaggle.json
files.upload()

# make directory and change permission
!mkdir -p ~/.kaggle && cp kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json && ls ~/.kaggle

# download the dataset
!kaggle datasets download -d raghavrpotdar/fresh-and-stale-images-of-fruits-and-vegetables

!unzip fresh-and-stale-images-of-fruits-and-vegetables.zip -d /content/drive/MyDrive/Playground/Fresh\ and\ Stale/dataset/

# remove downloaded dataset
!rm -rf fresh-and-stale-images-of-fruits-and-vegetables.zip

"""### Data Exploration"""

# Check total of data and sampling data
def count_data_sampling(path):
  images = []
  count_path = 0
  for dir in os.listdir(path):
    count_sub = 0
    sub = glob.glob(os.path.join(path, dir) + '/*')
    for file in sub:
      if count_sub < 1:
        image = cv2.imread(file)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        images.append([image, dir])

      count_sub += 1
      count_path += 1

    print("{} : {}".format(dir, count_sub))

  print("\nTotal data : {}".format(count_path))
  print("Sample images : \n")

  rows = 3
  plt.figure(figsize=(15,15))
  for i in range(9):
      plt.subplot(rows,3,i+1)
      plt.title("Sample {} image".format(images[i][1]))
      plt.imshow(images[i][0])

# Set dataset path
DATASET_PATH = os.path.join(BASE_PATH, 'dataset')
# Check total dataset every label
count_data_sampling(DATASET_PATH)

"""### Data Spliting"""

# Split dataset with 8:2 ratio 
SPLIT_PATH = os.path.join(BASE_PATH, 'split')
splitfolders.ratio(
    os.path.join(DATASET_PATH), 
    os.path.join(SPLIT_PATH), 
    seed=1, 
    ratio=(.8, .2)
)

# Check total of data
def count_data(path):
  count_path = 0
  for dir in os.listdir(path):
    count_sub = 0
    sub = glob.glob(os.path.join(path, dir) + '/*')
    for file in sub:
      count_sub += 1
      count_path += 1

    print("{} : {}".format(dir, count_sub))

  print("\nTotal data : {}".format(count_path))

# Set dataset path
TRAIN_PATH = os.path.join(SPLIT_PATH, 'train')
VALIDATION_PATH = os.path.join(SPLIT_PATH, 'val')

# Check total train data
count_data(TRAIN_PATH)

# Check total validation data
count_data(VALIDATION_PATH)

# Remove unsplited dataset to make more free space
!rm -rf /content/drive/MyDrive/Playground/Fresh\ and\ Stale/dataset/*

"""### Data Augmentation"""

train_data_generator = ImageDataGenerator(
  rescale=1./255,
  rotation_range=45,
  width_shift_range=.15,
  height_shift_range=.15,
  shear_range=.2,
  zoom_range=.2,
  horizontal_flip=True)

validation_data_generator = ImageDataGenerator(rescale=1./255)

train_generator = train_data_generator.flow_from_directory(
  TRAIN_PATH,
  target_size=(IMAGE_HEIGHT, IMAGE_WIDTH),
  batch_size=BATCH_SIZE,
  color_mode='rgb',
  class_mode='categorical',
  shuffle = True,
  seed=42)

validation_generator = validation_data_generator.flow_from_directory(
  VALIDATION_PATH,
  target_size=(IMAGE_HEIGHT, IMAGE_WIDTH),
  batch_size=BATCH_SIZE,
  color_mode='rgb',
  class_mode='categorical',
  shuffle = True,
  seed=42)

sample_train_images, _ = next(train_generator)
sample_validation_images, _ = next(validation_generator)

# Plot image
def plot_image(images):
  fig, axes = plt.subplots(1, 5, figsize=(20,20))
  axes = axes.flatten()
  for image, pl in zip(images, axes):
    pl.imshow(image)
    pl.axis('off')

# show sample image of train data after data augmentation
plot_image(sample_train_images)

# show sample image of validation data after data augmentation
plot_image(sample_validation_images)

"""## Model

### Simple CNN from Scratch
"""

def CNN():
  model = Sequential()
  model.add(layers.Conv2D(8, (3, 3), padding='same', activation='relu', input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, 3)))
  model.add(layers.MaxPooling2D(2, 2))
  model.add(layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001 ))
  model.add(layers.Conv2D(32, (3,3), activation='relu'))
  model.add(layers.MaxPooling2D(2, 2))
  model.add(layers.Conv2D(64, (3,3), activation='relu'))
  model.add(layers.MaxPooling2D(2, 2))
  model.add(layers.Conv2D(128, (3,3), activation='relu'))
  model.add(layers.MaxPooling2D(2, 2))
  model.add(layers.Conv2D(128, (3,3), activation='relu'))
  model.add(layers.MaxPooling2D(2, 2))
  model.add(layers.Flatten())
  model.add(layers.Dense(256, kernel_regularizer = regularizers.l2(l = 0.016),
                         activity_regularizer=regularizers.l1(0.006), bias_regularizer=regularizers.l1(0.006),
                         activation='relu'))
  model.add(layers.Dropout(rate=DROPOUT))
  model.add(layers.Dense(NUM_CLASS, activation='softmax'))

  optimizer = Adam(LEARNING_RATE)
  model.compile(optimizer=optimizer,
                loss='categorical_crossentropy',
                metrics=['accuracy'])
  
  return model

# Visualize simple CNN model
CNN().summary()

"""### VGG16 + CNN Transfer Learning"""

model_name='InceptionResNetV2'
base_model=tf.keras.applications.InceptionResNetV2(include_top=False, weights="imagenet",input_shape=img_shape, pooling='max') 
x=base_model.output
x=keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001 )(x)
x = Dense(256, kernel_regularizer = regularizers.l2(l = 0.016),activity_regularizer=regularizers.l1(0.006),
                bias_regularizer=regularizers.l1(0.006) ,activation='relu')(x)
x=Dropout(rate=.45, seed=123)(x)        
output=Dense(class_count, activation='softmax')(x)
model=Model(inputs=base_model.input, outputs=output)
model.compile(Adamax(lr=.001), loss='categorical_crossentropy', metrics=['accuracy'])

def VGGCNN():
  input_tensor = layers.Input(shape=(IMAGE_WIDTH, IMAGE_HEIGHT, 3))
  vgg16 = VGG16(
      weights='imagenet',
      include_top=False,
      input_tensor=input_tensor
  )
  
  # Make CNN layers not trainable / Freezing Convolutional layers
  # Don't train existing weights, because the weights are ready fix
  for layer in vgg16.layers:
      layer.trainable = False
        
  # Defreeze all batch normalization layers due to a bug in transfer learning using keras
  for layer in vgg16.layers:
      if "BatchNormalization" in layer.__class__.__name__:
          layer.trainable = True
  
  cnn = Sequential()
  cnn.add(layers.Flatten(input_shape=vgg16.output_shape[1:]))
  cnn.add(layers.Dense(512, activation='relu'))
  cnn.add(layers.Dropout(0.5))
  cnn.add(layers.Dense(NUM_CLASS, activation='softmax'))

  model = Model(vgg16.input, cnn(vgg16.output))

  optimizer = Adam(LEARNING_RATE)
  model.compile(optimizer=optimizer,
                loss='categorical_crossentropy',
                metrics=['accuracy'])
  
  return model

# Visualize custom VGG16+CNN transfer learning model
VGGCNN().summary()

"""## Traing

### Utility & Helper
"""

# Set target names/label
target_names = ['fresh_apple', 'fresh_banana', 'fresh_bitter_gourd', 'fresh_capsicum', 'fresh_orange', 'fresh_tomato', 
                'stale_apple', 'stale_banana', 'stale_bitter_gourd', 'stale_capsicum', 'stale_orange', 'stale_tomato']

# Print in markdown format
def printmd(string):
    display(Markdown(string))

# Plot training loss and accuracy
def plot_training(history, title, save=True, save_path=os.path.join(BASE_PATH, 'visualization')):
    printmd("### Plot Training Loss and Accuracy")
    
    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']
    loss = history.history['loss']
    val_loss = history.history['val_loss']
    epoch_list = range(1, len(acc) + 1)
    
    plt.figure(figsize=(8, 6))
    plt.plot(loss, label='Training Loss')
    plt.plot(val_loss, label='Validation Loss')
    plt.legend(loc=0, fontsize='large')
    plt.xlabel("Epoch #", fontsize=14)
    plt.ylabel("Loss", fontsize=14)
    plt.show()
    
    if save is True:
        filename = slugify(title) + '_loss_model.png'
        plt.savefig(save_path + '/' + filename)

    plt.figure(figsize=(8, 6))
    plt.plot(acc, label='Training Accuracy')
    plt.plot(val_acc, label='Validation Accuracy')
    plt.legend(loc=4, fontsize='large')
    plt.xlabel("Epoch #", fontsize=14)
    plt.ylabel("Accuracy", fontsize=14)
    plt.show()
    
    if save is True:
        filename = slugify(title) + '_acc_model.png'
        plt.savefig(save_path + '/' + filename)

# Plot confusion matrix
def plot_confusion_matrix(classes, preds, title, save=True, save_path=os.path.join(BASE_PATH, 'visualization')):
    printmd('#### Confusion Matrix')
    cm = confusion_matrix(classes, preds)
    print('True Positive (TP):', cm[0][0])
    print('False Positive (FP):', cm[0][1])
    print('False Negative (FN):', cm[1][0])
    print('True Negative (TN):', cm[1][1])
    df_cm = pd.DataFrame(cm, range(len(target_names)), range(len(target_names)))
    sn.set(font_scale=1.4) # for label size
    sn.heatmap(df_cm, annot=True, fmt=".0f", annot_kws={"size": 16}) # font size
    plt.ylabel('Actual label', size = 20)
    plt.xlabel('Predicted label', size = 20)
    plt.xticks(np.arange(len(target_names)))
    plt.yticks(np.arange(len(target_names)))
    plt.ylim([len(target_names), 0])
    
    if save is True:
        filename = slugify(title) + '_confusion_matrix.png'
        plt.savefig(save_path + '/' + filename)

    plt.show()
    
    # Calculate accuracy
    calculated_acc = (cm[0][0]+cm[1][1])/(cm[0][0]+cm[0][1]+cm[1][0]+ cm[1][1])
    print("Calculated Accuracy:", calculated_acc*100)

# Table classification report
def table_classification_report(classes, preds):
    printmd('#### Classification Report')
    print(classification_report(classes, preds, target_names=target_names))

    print('Accuracy: {:.2f}\n'.format(accuracy_score(classes, preds)))

    print('Macro Precision: {:.2f}'.format(precision_score(classes, preds, average='macro')))
    print('Macro Recall: {:.2f}'.format(recall_score(classes, preds, average='macro')))
    print('Macro F1-score: {:.2f}\n'.format(f1_score(classes, preds, average='macro')))

    print('Weighted Precision: {:.2f}'.format(precision_score(classes, preds, average='weighted')))
    print('Weighted Recall: {:.2f}'.format(recall_score(classes, preds, average='weighted')))
    print('Weighted F1-score: {:.2f}\n'.format(f1_score(classes, preds, average='weighted')))

# Evaluate model, confusion matrix and classification report
def model_evaluation(title, model, train_generator, validation_generator, save=True, save_path=os.path.join(BASE_PATH, 'visualization')):
    printmd('### Training Accuracy')
    loss, val_acc = model.evaluate_generator(train_generator, verbose=1)

    print("loss: %.2f" % (loss*100))
    print("acc: %.2f" % (val_acc*100))
    
    printmd('### Validation Accuracy')
    loss, val_acc = model.evaluate_generator(validation_generator, verbose=1)

    print("loss: %.2f" % (loss*100))
    print("acc: %.2f" % (val_acc*100))
    
    printmd('### Evaluation of Model Performance')
    Y_pred = model.predict_generator(validation_generator, verbose=1)
    y_pred = np.argmax(Y_pred, axis=1)
    x_pred = validation_generator.classes
    
    # Plot confusion matrix
    plot_confusion_matrix(x_pred, y_pred, title, save, save_path)
    
    # Table classification report
    table_classification_report(x_pred, y_pred)

"""### Training using CNN Model"""

# Adding callbacks
train_id = 'cnn-' + str(uuid.uuid1())
best_model = os.path.join(BASE_PATH, 'model/best_model-' + train_id + '.h5')
last_model = os.path.join(BASE_PATH, 'model/last_model-' + train_id + '.h5')
csv_log = os.path.join(BASE_PATH, 'monitor/logs-' + train_id + '.h5')

ES = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=10)
MC = ModelCheckpoint(best_model, monitor='val_accuracy', mode='max', save_best_only=True, verbose=1)
CSVL = CSVLogger(csv_log, separator=",", append=True)

callbacks = [MC, ES, CSVL]

# Training start time
start_time = time.time()

print("Training strated with Train ID: {}".format(train_id))

model = CNN()
history = model.fit_generator(train_generator, 
                    steps_per_epoch=25, 
                    epochs=EPOCH, 
                    validation_data=validation_generator,
                    validation_steps=5,
                    verbose=1,
                    callbacks=callbacks)

# Training complete
time_elapsed = time.time() - start_time
# Save last model
model.save(last_model)

print("=" * 30)
print("Training Finished, took {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
print("Best model:", best_model)
print("Last model:", last_model)

# Plot training and loss accuracy
plot_training(history, train_id)

# Load best model and evaluate
print("Load best model to evaluate ...")
model = load_model(best_model)
model_evaluation(train_id, model, train_generator, validation_generator)

# Writing code to save the model into TF-Lite format
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
tflite_save_path = best_model + '.tflite'

print("Save path and name:", tflite_save_path)

try:
  with tf.io.gfile.GFile(tflite_save_path, 'wb') as f:
    f.write(tflite_model)

  print("Saving into TF-Lite file successfully ...")
except Exception as e:
  print("Saving into TF-Lite file error: {}".format(e))

"""### Training using VGG16 + CNN Model Transfer Learning"""

# Adding callbacks
train_id = 'vgg16-' + str(uuid.uuid1())
best_model = os.path.join(BASE_PATH, 'model/best_model-' + train_id + '.h5')
last_model = os.path.join(BASE_PATH, 'model/last_model-' + train_id + '.h5')
csv_log = os.path.join(BASE_PATH, 'monitor/logs-' + train_id + '.h5')

ES = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=10)
MC = ModelCheckpoint(best_model, monitor='val_accuracy', mode='max', save_best_only=True, verbose=1)
CSVL = CSVLogger(csv_log, separator=",", append=True)

callbacks = [MC, ES, CSVL]

# Training start time
start_time = time.time()

print("Training strated with Train ID: {}".format(train_id))

model = VGGCNN()
history = model.fit_generator(train_generator, 
                    steps_per_epoch=25, 
                    epochs=EPOCH, 
                    validation_data=validation_generator,
                    validation_steps=5,
                    verbose=1,
                    callbacks=callbacks)

# Training complete
time_elapsed = time.time() - start_time
# Save last model
model.save(last_model)

print("=" * 30)
print("Training Finished, took {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
print("Best model:", best_model)
print("Last model:", last_model)

# Plot training and loss accuracy
plot_training(history, train_id)

# Load best model
print("Load best model to evaluate ...")
model = load_model(best_model)
model_evaluation(train_id, model, train_generator, validation_generator)

# Writing code to save the model into TF-Lite format
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
tflite_save_path = best_model + '.tflite'

print("Save path and name:", tflite_save_path)

try:
  with tf.io.gfile.GFile(tflite_save_path, 'wb') as f:
    f.write(tflite_model)

  print("Saving into TF-Lite file successfully ...")
except Exception as e:
  print("Saving into TF-Lite file error: {}".format(e))

"""## Prediction"""

def prediction(model_path):
  # Load model
  model = load_model(model_path)
  # Start uploading
  upload = files.upload()
  for path in upload.keys():
    # predicting uploaded image
    img = image.load_img(path, target_size=(IMAGE_WIDTH, IMAGE_HEIGHT))

    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)

    images = np.vstack([x])
    classes = model.predict(images, batch_size=BATCH_SIZE)

    # define text labels
    food_labels = ['fresh_apple', 'fresh_banana', 'fresh_bitter_gourd', 'fresh_capsicum', 'fresh_orange', 'fresh_tomato', 
                   'stale_apple', 'stale_banana', 'stale_bitter_gourd', 'stale_capsicum', 'stale_orange', 'stale_tomato']
    
    # get detection label
    idx = np.argmax(classes[0])
    label = food_labels[idx]
    # get prediction percentage/similarities
    similarities = classes[0,idx]*100

    # ploting the image
    plt.title("Prediction : {} ({:.2f}%)".format(label, similarities))
    plt.imshow(img)

# List available model
# Model using CNN from Scratch
cnn_best_model = '/content/drive/MyDrive/Playground/Fresh and Stale/model/best_model-cnn-d9373038-e2d4-11eb-acbf-0242ac1c0002.h5'
cnn_last_model = '/content/drive/MyDrive/Playground/Fresh and Stale/model/last_model-cnn-d9373038-e2d4-11eb-acbf-0242ac1c0002.h5'
# Model using VGG16 + CNN Transfer Learning
vgg16_best_model = '/content/drive/MyDrive/Playground/Fresh and Stale/model/best_model-vgg16-d0f03484-e2d8-11eb-acbf-0242ac1c0002.h5'
vgg16_last_model = '/content/drive/MyDrive/Playground/Fresh and Stale/model/last_model-vgg16-d0f03484-e2d8-11eb-acbf-0242ac1c0002.h5'

# Make prediction, show upload page to predict using CNN from Scratch
prediction(model_path=cnn_best_model)

# Make prediction, show upload page to predict using CNN from Scratch
prediction(model_path=cnn_best_model)