Murtadhaa's picture
Update app.py
d13d92e verified
Raw
History Blame Contribute Delete
12.4 kB
import gradio as gr
import numpy as np
import cv2
import joblib
from mtcnn import MTCNN
from PIL import Image, ImageEnhance
from keras_facenet import FaceNet
# ------------------------------------------------------
# 1) Load your saved models
model3 = joblib.load("model_general.pkl") # Approach 1 general
model4 = joblib.load("model_male_1.pkl") # Male model #1
model6 = joblib.load("model_male_2.pkl") # Male model #2
model7 = joblib.load("model_female.pkl") # Female model
# 2) Prepare face detection & embedding
mtcnn_detector = MTCNN()
facenet = FaceNet()
# ------------------------------------------------------
# Helper functions
def round_value(value):
"""Round numeric value to the nearest 0.05."""
return round(value * 20) / 20
def apply_augmentations(image_array):
"""
Given a numpy array (RGB), return a list of 5 augmented images (numpy arrays).
We label them as: [flipped, bright_increase, bright_decrease, rotated_left, rotated_right].
"""
augmentations = []
pil_img = Image.fromarray(image_array)
# 1) Flip horizontally
flipped = np.array(pil_img.transpose(Image.FLIP_LEFT_RIGHT))
augmentations.append(("flipped", flipped))
# 2) Brightness increase
bright_increase = ImageEnhance.Brightness(pil_img).enhance(1.3)
augmentations.append(("bright_up", np.array(bright_increase)))
# 3) Brightness decrease
bright_decrease = ImageEnhance.Brightness(pil_img).enhance(0.7)
augmentations.append(("bright_down", np.array(bright_decrease)))
# 4) Rotate left (+10 deg)
rotated_left = pil_img.rotate(10, expand=False) # expand=False to keep same size
augmentations.append(("rot_left", np.array(rotated_left)))
# 5) Rotate right (-10 deg)
rotated_right = pil_img.rotate(-10, expand=False)
augmentations.append(("rot_right", np.array(rotated_right)))
return augmentations
def crop_largest_face(image_array):
"""
Use MTCNN to detect faces, and return the cropped region of the biggest face.
If no faces found, return None.
"""
# MTCNN expects RGB image
faces = mtcnn_detector.detect_faces(image_array)
if len(faces) == 0:
return None
# find face with largest area
largest_face = max(faces, key=lambda face: face['box'][2] * face['box'][3])
x, y, w, h = largest_face['box']
# clip to ensure we don't go out of bounds
height, width = image_array.shape[:2]
x1, y1 = max(x, 0), max(y, 0)
x2, y2 = min(x + w, width), min(y + h, height)
return image_array[y1:y2, x1:x2]
def get_embedding(image_array):
"""
Convert single face (RGB) to embedding using keras-facenet.
The .embeddings() method expects a list of arrays.
Returns 512-dim embedding (np.array).
"""
# FaceNet wants images in [RGB], shape ~ (H, W, 3).
# We'll assume each image is properly cropped around the face.
# If needed, you might have to resize to (160,160). But FaceNet from keras-facenet
# often does it internally. We'll pass as is.
emb = facenet.embeddings([image_array])[0] # shape (512,)
return emb
def prepare_input_for_model(model, embedding, gender):
"""
If the model expects 513 features, prepend the gender flag (1 for male, 0 for female).
Otherwise, just return the 512-dim embedding.
"""
if model.n_features_in_ == 513:
gender_flag = 1 if gender.lower().startswith("m") else 0
arr = np.concatenate(([gender_flag], embedding))
return arr.reshape(1, -1)
else:
return embedding.reshape(1, -1)
# ------------------------------------------------------
# Main pipeline function
def process_images(img_paths, gender):
"""
- gender: "Male" or "Female"
- img_paths: list of image file paths uploaded by user
Returns a string with final result or error messages.
"""
# 1) Verify each image has at least one face
images_list = []
no_face_indices = []
for idx, path in enumerate(img_paths):
# read with cv2
image_bgr = cv2.imread(path[0])
if image_bgr is None:
no_face_indices.append(idx)
continue
# convert BGR -> RGB
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# check face
faces = mtcnn_detector.detect_faces(image_rgb)
if len(faces) == 0:
no_face_indices.append(idx)
else:
# store valid image
images_list.append((idx, image_rgb))
# if ANY image had no face, report it and stop
if no_face_indices:
msg_lines = []
for bad_i in no_face_indices:
msg_lines.append(f"Image {bad_i+1} has no detected face.")
msg_lines.append("Please try again with different images.")
return "\n".join(msg_lines)
# 2) For each valid image, create 5 augmentations + original
# We'll gather them in a structure: { idx: [(aug_label, image_array), ...], ... }
# so that each input image has 6 versions.
all_aug_images = {} # key = input_idx, value = list of (aug_label, np_array)
for idx, orig_rgb in images_list:
temp = []
temp.append(("original", orig_rgb))
# augment
aug_list = apply_augmentations(orig_rgb)
temp.extend(aug_list) # now we have 6 total
all_aug_images[idx] = temp
# 3) Crop largest face in each augmented image. If no face found => skip that augmentation
# We'll keep them in { idx: { aug_label: [embedding arrays], ... }, ... }
# Actually we want a single embedding per augmented image, so we'll store that.
# Then we can average later by augmentation type across all input images.
# That means for each input_idx, for each "aug_label", we get a single embedding, or None if no face.
# We'll store per augmentation label across all images, so we can average them later:
# label_embeds_map = { 'original': [], 'flipped': [], ... }
label_embeds_map = {}
for idx, aug_images in all_aug_images.items():
# aug_images is a list of (aug_label, np_array) for that input
for aug_label, aug_img in aug_images:
cropped = crop_largest_face(aug_img)
if cropped is None:
# skip
continue
# get embedding
emb = get_embedding(cropped)
if aug_label not in label_embeds_map:
label_embeds_map[aug_label] = []
label_embeds_map[aug_label].append(emb)
# 4) Now we have up to 6 keys in label_embeds_map: [original, flipped, bright_up, bright_down, rot_left, rot_right].
# Some may have fewer if some faces were not found in augmented versions.
# We'll compute average embedding for each label if it has at least 1 embedding.
final_label_embeddings = {} # label -> (512,) average
for label, embed_list in label_embeds_map.items():
if len(embed_list) == 0:
continue
avg_emb = np.mean(embed_list, axis=0)
final_label_embeddings[label] = avg_emb
# If *none* of the 6 augmentations ended up with a face, we can't proceed
if len(final_label_embeddings) == 0:
return "No faces found in augmented images. Cannot proceed."
# 5) For each label's average embedding, compute:
# - Approach1 => model3.predict
# - Approach2 => depends on gender:
# male => average( model4.predict, model6.predict )
# female => model7.predict
# - Hybrid => 0.5*(Approach1 + Approach2)
# Then we'll store them in a list so we can average across labels at the end.
approach3_results = [] # We'll store the final "hybrid" predictions for each label
for label, emb in final_label_embeddings.items():
emb_2d = prepare_input_for_model(model3, emb, gender)
pred_a1 = model3.predict(emb_2d)[0]
emb_2d_4 = prepare_input_for_model(model4, emb, gender)
p4 = model4.predict(emb_2d_4)[0]
emb_2d_6 = prepare_input_for_model(model6, emb, gender)
p6 = model6.predict(emb_2d_6)[0]
emb_2d_7 = prepare_input_for_model(model7, emb, gender)
pred_a2 = model7.predict(emb_2d_7)[0]
# Approach 1
pred_a1 = model3.predict(emb_2d)[0]
# Approach 2
if gender.lower().startswith("m"):
# male => average of model4 + model6
p4 = model4.predict(emb_2d_4)[0]
p6 = model6.predict(emb_2d_6)[0]
pred_a2 = 0.5 * (p4 + p6)
else:
# female => model7 alone
pred_a2 = model7.predict(emb_2d_7)[0]
# Approach 3 => average(approach1, approach2)
pred_a3 = 0.5 * (pred_a1 + pred_a2)
# We'll store the final approach 3 result
approach3_results.append(pred_a3)
# 6) Average across all labels (the different augmentations)
if len(approach3_results) == 0:
return "No valid augmented faces after cropping; cannot proceed."
final_score = np.mean(approach3_results)
# Round to nearest quarter
final_score = round_value(final_score)
# clamp or keep it? The instructions say "X out of 10"
# We'll do a simple float formatting
# ... after computing final_score_quarter ...
score = final_score # just to shorten variable name
# Determine a descriptive message based on the user's intervals
if score <= 3.0:
message = "very unattractive and significantly below average"
elif score <= 4.0:
message = "very below average"
elif score <= 4.5:
message = "below average"
elif score < 5.0:
# Covers up to 4.75 or 4.99, etc.
message = "slightly below average"
elif score == 5.0:
message = "average"
elif score < 6.0:
# Covers 5.25, 5.5, 5.75, etc.
message = "decent and slightly above average"
elif score <= 6.25:
message = "good and decently above average"
elif score < 6.5:
# Covers 6.3, 6.4, etc.
message = "very attractive and well above average"
elif score == 6.5:
message = "very attractive and well above average"
elif score < 6.75:
# Covers 6.6, 6.7
message = "very attractive and well above average"
elif score <= 7.5:
message = "highly attractive and very well above average"
elif score < 7.75:
# Covers e.g. 7.6
message = "highly attractive and very well above average"
elif score == 7.75:
message = "very attractive and significantly above average"
elif score < 8.0:
# Covers e.g. 7.8
message = "very attractive and significantly above average"
elif score <= 8.5:
message = "extremely amazing and very attractive"
elif score < 8.75:
message = "extremely amazing and very attractive"
elif score <= 9.25:
message = "extremely amazing and one of the best faces in the world"
elif score < 9.5:
message = "extremely amazing and one of the best faces in the world"
else:
# >= 9.5
message = "extremely amazing and one of the best faces ever created"
# Now include that message in the final string
return f"This person is {score} out of 10 in looks, which is {message}."
interface = gr.Interface(
fn=process_images,
inputs=[
gr.Gallery(label="Upload Images", type='filepath'),
gr.Radio(["Male", "Female"], label="Gender")
],
outputs=gr.Textbox(label="Result"),
title="How Attractive Are You?",
description=(
"**Upload a photo (or multiple photos) and see how high you score out of 10.**\n\n"
"• Please ensure the image is well-lit and only shows your face, if possible.\n"
" (We automatically crop to the largest face, but it’s best to avoid extra faces.)\n\n"
"• The model can work with a single image, but **3–5 images** may yield a more accurate score.\n\n"
"• This tool focuses on **facial symmetry**—it does **not** account for personal preferences or other factors.\n"
" Please don’t take the result too seriously!\n\n"
"• This model was trained on faces of people from age 20-50 so don't expect reasonable scores for outside this range.\n\n"
"*If you’re curious about how this was made or the standards used, you can visit my kaggle notebook [here](https://www.kaggle.com/code/murtadhanajim/facial-attractiveness).*"
)
)
interface.launch()