#How to Develop a Face Recognition System Using FaceNet in Keras
#https://machinelearningmastery.com/how-to-develop-a-face-recognition-system-using-facenet-in-keras-and-an-svm-classifier/

# NOTES: 
# 1--Read the above tutorial for a better understanding!
# 2--The code from the tutorial does not work anymore because libraries
# have been updated; recent versions do not support eralier stuff.
# 3--This code here has been fixed to make it work.

# The 2011 book on face recognition titled “Handbook of Face Recognition” 
# describes two main modes for face recognition, as:
#     1) Face Verification. A one-to-one mapping of a given face against a 
#     known identity (e.g. is this the person?).
#     2) Face Identification. A one-to-many mapping for a given face against 
#     a database of known faces (e.g. who is this person?).
# We will focus on the face identification task in this tutorial.


###############################################################################
# 1 -- How to Load a FaceNet Model in Keras


# Version #1: <--- does not work anymore!
# We will use the pre-trained Keras FaceNet model provided by Hiroki Taniai in 
# his tutorial. It was trained on MS-Celeb-1M dataset and expects input images 
# to be color, to have their pixel values whitened (standardized across all 
# three channels), and to have a square shape of 160×160 pixels.
# The model can be downloaded from here:
#     https://drive.google.com/drive/folders/1pwQ3H4aJ8a6yyJHZkTwtjcL4wYWQb7bn
#     Keras FaceNet Pre-Trained Model (88 megabytes)
# Download the model file and place it in your current working directory with 
# the filename ‘facenet_keras.h5‘.
# from keras.models import load_model
# # load the model
# model = load_model('facenet_keras.h5')
# # summarize input and output shape
# print(model.inputs)
# print(model.outputs)


# Version #2: <--- Works and is used later in this example!
# NEEDS:
# pip install keras-facenet

# from keras_facenet import FaceNet
# # load the model
# embedder = FaceNet() # Wrapper class
# model = embedder.model # Actual tf.keras.Model
# # summarize input and output shape
# print(model.inputs)
# print(model.outputs)


###############################################################################
# 2 -- How to Detect Faces for Face Recognition
# 3 -- How to Develop a Face Classification System

# NEEDS:
# pip install mtcnn==0.1.1



# ~~~~~~~~~~~~~~~~~~~~~~~ (A) "5 Celebrity Faces Dataset"

# We will develop a face detection system to predict the identity of a given face.
# The model will be trained and tested using the ‘5 Celebrity Faces Dataset‘:
# a small dataset that contains photographs of celebrities.
# It includes photos of: 
# Ben Affleck, Elton John, Jerry Seinfeld, Madonna, and Mindy Kaling.

# The dataset was prepared and made available by Dan Becker and provided for 
# free download on Kaggle. Note, a Kaggle account is required to download the 
# dataset.
# https://www.kaggle.com/datasets/dansbecker/5-celebrity-faces-dataset

# Download the dataset (this may require a Kaggle login), data.zip (2.5 megabytes), 
# and unzip it in your local directory with the folder name 
# ‘5-celebrity-faces-dataset‘.

# NOTES:
# --I downloaded it, placed it in folder: 5-celebrity-faces-dataset



# ~~~~~~~~~~~~~~~~~~~~~~~ (B) Detect Faces
# ~~~~~~~~~~~~~~~~~~~~~~~ Use MTCNN() for that

# We will use an MTCNN model for face detection, the FaceNet model will be used 
# to create a face embedding for each detected face, then we will develop a Linear 
# Support Vector Machine (SVM) classifier model to predict the identity of a given face.

# The first step is to detect the face in each photograph and reduce the dataset 
# to a series of faces only.

# NOTES:
# --Run the code below once only because it takes some time and it saves
#   a file that has been already created once:
#   5-celebrity-faces-dataset.npz


from os import listdir
from os.path import isdir
from PIL import Image
from matplotlib import pyplot
from numpy import savez_compressed
from numpy import asarray
from mtcnn.mtcnn import MTCNN

# extract a single face from a given photograph
def extract_face(filename, required_size=(160, 160)):
	# load image from file
	image = Image.open(filename)
	# convert to RGB, if needed
	image = image.convert('RGB')
	# convert to array
	pixels = asarray(image)
	# create the detector, using default weights
	detector = MTCNN() # <-------------------- model to detect faces in images;
	# detect faces in the image
	results = detector.detect_faces(pixels)
	# extract the bounding box from the first face
	x1, y1, width, height = results[0]['box']
	# bug fix
	x1, y1 = abs(x1), abs(y1)
	x2, y2 = x1 + width, y1 + height
	# extract the face
	face = pixels[y1:y2, x1:x2]
	# resize pixels to the model size
	image = Image.fromarray(face)
	image = image.resize(required_size)
	face_array = asarray(image)
	return face_array


# Let’s test out our face detector function defined in the previous section, 
# specifically extract_face().
# Looking in the ‘5-celebrity-faces-dataset/train/ben_afflek/‘ directory, 
# we can see that there are 14 photographs of Ben Affleck in the training 
# dataset. We can detect the face in each photograph, and create a plot 
# with 14 faces, with two rows of seven images each.

# NOTE: this is kust some testing here for fun; no needed to be executed;
# specify folder to plot for fun
# folder = '5-celebrity-faces-dataset/train/ben_afflek/'
# i = 1
# # enumerate files
# for filename in listdir(folder):
# 	# path
# 	path = folder + filename
# 	# get face
# 	face = extract_face(path)
# 	print(i, face.shape)
# 	# plot
# 	pyplot.subplot(2, 7, i)
# 	pyplot.axis('off')
# 	pyplot.imshow(face)
# 	i += 1
# pyplot.show()


# load images and extract faces for all images in a directory
def load_faces(directory):
	faces = list()
	# enumerate files
	for filename in listdir(directory):
		# path
		path = directory + filename
		# get face
		face = extract_face(path)
		# store
		faces.append(face)
	return faces

# load a dataset that contains one subdir for each class that in turn contains images
def load_dataset(directory):
	X, y = list(), list()
	# enumerate folders, on per class
	for subdir in listdir(directory):
		# path
		path = directory + subdir + '/'
		# skip any files that might be in the dir
		if not isdir(path):
			continue
		# load all faces in the subdirectory
		faces = load_faces(path)
		# create labels
		labels = [subdir for _ in range(len(faces))]
		# summarize progress
		print('>loaded %d examples for class: %s' % (len(faces), subdir))
		# store
		X.extend(faces)
		y.extend(labels)
	return asarray(X), asarray(y)


# load train dataset
trainX, trainy = load_dataset('5-celebrity-faces-dataset/train/')
print(trainX.shape, trainy.shape)
# load test dataset
testX, testy = load_dataset('5-celebrity-faces-dataset/val/')

# save arrays to one file in compressed format
savez_compressed('5-celebrity-faces-dataset.npz', trainX, trainy, testX, testy)

# This dataset is ready to be provided to a face detection model!


# ~~~~~~~~~~~~~~~~~~~~~~~ (C) Create Face Embeddings
# ~~~~~~~~~~~~~~~~~~~~~~~ Use FaceNet model for that

# The next step is to create a face embedding.
# A face embedding is a vector that represents the features extracted from 
# the face. This can then be compared with the vectors generated for other faces. 
# For example, another vector that is close (by some measure) may be the 
# same person, whereas another vector that is far (by some measure) may 
# be a different person.

# The classifier model that we want to develop will take a face embedding as 
# input and predict the identity of the face. The FaceNet model will generate 
# this embedding for a given image of a face.

# The FaceNet model can be used as part of the classifier itself, or we can 
# use the FaceNet model to pre-process a face to create a face embedding that 
# can be stored and used as input to our classifier model. This latter approach 
# is preferred as the FaceNet model is both large and slow to create a face embedding

# NOTES:
# --Run the code below once only because it takes some time and it saves
#   a file that has been already created once:
#   5-celebrity-faces-embeddings.npz


# calculate a face embedding for each face in the dataset using facenet
# and save all embeddings into: 5-celebrity-faces-embeddings.npz
from numpy import load
from numpy import expand_dims
from numpy import asarray
from numpy import savez_compressed
from keras.models import load_model
from keras_facenet import FaceNet


# get the face embedding for one face
def get_embedding(model, face_pixels):
	# scale pixel values
	face_pixels = face_pixels.astype('float32')
	# standardize pixel values across channels (global)
	mean, std = face_pixels.mean(), face_pixels.std()
	face_pixels = (face_pixels - mean) / std
	# transform face into one sample
	samples = expand_dims(face_pixels, axis=0)
	# make prediction to get embedding
	yhat = model.predict(samples)
	return yhat[0]

# load the face dataset
data = load('5-celebrity-faces-dataset.npz')
trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']
print('Loaded: ', trainX.shape, trainy.shape, testX.shape, testy.shape)


# load the facenet model
#model_facenet = load_model('facenet_keras.h5') <------ does not work anymore!
# use newer version of load the facenet model:
embedder = FaceNet() # Wrapper class
model_facenet = embedder.model # Actual tf.keras.Model
# summarize input and output shape
print(model_facenet.inputs)
print(model_facenet.outputs)
print('Loaded Model Facenet')

# convert each face in the train set to an embedding
newTrainX = list()
for face_pixels in trainX:
	embedding = get_embedding(model_facenet, face_pixels)
	newTrainX.append(embedding)
newTrainX = asarray(newTrainX)
print(newTrainX.shape)

# convert each face in the test set to an embedding
newTestX = list()
for face_pixels in testX:
	embedding = get_embedding(model_facenet, face_pixels)
	newTestX.append(embedding)
newTestX = asarray(newTestX)
print(newTestX.shape)

# save arrays to one file in compressed format
savez_compressed('5-celebrity-faces-embeddings.npz', newTrainX, trainy, newTestX, testy)



# ~~~~~~~~~~~~~~~~~~~~~~~ (D) Perform Face Classification
# ~~~~~~~~~~~~~~~~~~~~~~~ Use SVM for that

# In this section, we will develop a model to classify face embeddings as one 
# of the known celebrities in the 5 Celebrity Faces Dataset.

# We will fit a Linear SVM on the face embeddings for the 5 Celebrity Faces Dataset!


# develop a classifier for the 5 Celebrity Faces Dataset
from numpy import load
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import Normalizer
from sklearn.svm import SVC

# load dataset
data = load('5-celebrity-faces-embeddings.npz')
trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']
print('Dataset: train=%d, test=%d' % (trainX.shape[0], testX.shape[0]))

# normalize input vectors
in_encoder = Normalizer(norm='l2')
trainX = in_encoder.transform(trainX)
testX = in_encoder.transform(testX)

# label encode targets
out_encoder = LabelEncoder()
out_encoder.fit(trainy)
trainy = out_encoder.transform(trainy)
testy = out_encoder.transform(testy)

# fit model
model_SVC = SVC(kernel='linear', probability=True)
model_SVC.fit(trainX, trainy)

# predict
yhat_train = model_SVC.predict(trainX)
yhat_test =model_SVC.predict(testX)

# score
score_train = accuracy_score(trainy, yhat_train)
score_test = accuracy_score(testy, yhat_test)
# summarize
print('Accuracy: train=%.3f, test=%.3f' % (score_train*100, score_test*100))


# ~~~~~~~~~~~~~~~~~~~~~~~ (E) Making it more interesting... 
# by plotting the original face and the prediction
# A different random example from the test dataset will be selected each 
# time the code below is run.

from random import choice

# load faces
data_faces = load('5-celebrity-faces-dataset.npz')
testX_faces = data_faces['arr_2']

# test model on a random example from the test dataset
selection = choice([i for i in range(testX.shape[0])])
random_face_pixels = testX_faces[selection]
random_face_emb = testX[selection]
random_face_class = testy[selection]
random_face_name = out_encoder.inverse_transform([random_face_class])

# prediction for the face
samples = expand_dims(random_face_emb, axis=0)
yhat_class = model_SVC.predict(samples)
yhat_prob = model_SVC.predict_proba(samples)

# get name
class_index = yhat_class[0]
class_probability = yhat_prob[0,class_index] * 100
predict_names = out_encoder.inverse_transform(yhat_class)
print('Predicted: %s (%.3f)' % (predict_names[0], class_probability))
print('Expected: %s' % random_face_name[0])

# plot for fun
pyplot.imshow(random_face_pixels)
title = '%s (%.3f)' % (predict_names[0], class_probability)
pyplot.title(title)
pyplot.show()



###############################################################################
# Supplemental test

# More fun: search online for additional photos and do here more testing;
# We essentially take individual photos thru all the steps of the pipeline
# developed above:
# new_photo --> MTCNN model for face detection --->
# --> FaceNet model to pre-process face to create face embedding (stored and 
#     used as input to classifier model) --->
# ---> Linear SVM to classify face embedding as one of the 5 people!


# 0--download from the Internet new photos
# Saved as: 
# 5-celebrity-faces-dataset/jerry1.jpg
# 5-celebrity-faces-dataset/jerry2.jpg
# 5-celebrity-faces-dataset/john1.jpg


# 1--first, detect face and extract as new image 160x160 with only the face
# MTCNN model for face detection
#new_photo = '5-celebrity-faces-dataset/jerry1.jpg'
#new_photo = '5-celebrity-faces-dataset/jerry2.jpg'
new_photo = '5-celebrity-faces-dataset/john1.jpg'
new_face = extract_face(new_photo)
print(new_face)
print(new_face.shape)
pyplot.imshow(new_face) # (160, 160, 3)
pyplot.show()


# 2--second, create face embedding, using FaceNet model 
# convert each face in the train set to an embedding
new_embedding = get_embedding(model_facenet, new_face)
print(new_embedding.shape) # (512,)

# 3--third, do classification 
# Linear SVM to classify face embedding as one of the 5 people
new_samples = expand_dims(new_embedding, axis=0)
yhat_class = model_SVC.predict(new_samples)
yhat_prob = model_SVC.predict_proba(new_samples)
# get name
class_index = yhat_class[0]
class_probability = yhat_prob[0,class_index] * 100
predict_names = out_encoder.inverse_transform(yhat_class)
print('Predicted: %s (%.3f)' % (predict_names[0], class_probability))
# plot for fun
pyplot.imshow(new_face)
title = '%s (%.3f)' % (predict_names[0], class_probability)
pyplot.title(title)
pyplot.show()
