| 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 |
|
|
| |
| |
| model3 = joblib.load("model_general.pkl") |
| model4 = joblib.load("model_male_1.pkl") |
| model6 = joblib.load("model_male_2.pkl") |
| model7 = joblib.load("model_female.pkl") |
|
|
| |
| mtcnn_detector = MTCNN() |
| facenet = FaceNet() |
|
|
| |
| |
|
|
| 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) |
|
|
| |
| flipped = np.array(pil_img.transpose(Image.FLIP_LEFT_RIGHT)) |
| augmentations.append(("flipped", flipped)) |
|
|
| |
| bright_increase = ImageEnhance.Brightness(pil_img).enhance(1.3) |
| augmentations.append(("bright_up", np.array(bright_increase))) |
|
|
| |
| bright_decrease = ImageEnhance.Brightness(pil_img).enhance(0.7) |
| augmentations.append(("bright_down", np.array(bright_decrease))) |
|
|
| |
| rotated_left = pil_img.rotate(10, expand=False) |
| augmentations.append(("rot_left", np.array(rotated_left))) |
|
|
| |
| 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. |
| """ |
| |
| faces = mtcnn_detector.detect_faces(image_array) |
| if len(faces) == 0: |
| return None |
| |
| largest_face = max(faces, key=lambda face: face['box'][2] * face['box'][3]) |
| x, y, w, h = largest_face['box'] |
| |
| 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). |
| """ |
| |
| |
| |
| |
| emb = facenet.embeddings([image_array])[0] |
| 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) |
|
|
|
|
|
|
| |
| |
| 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. |
| """ |
|
|
| |
| images_list = [] |
| no_face_indices = [] |
| for idx, path in enumerate(img_paths): |
| |
| image_bgr = cv2.imread(path[0]) |
| if image_bgr is None: |
| no_face_indices.append(idx) |
| continue |
| |
| image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) |
|
|
| |
| faces = mtcnn_detector.detect_faces(image_rgb) |
| if len(faces) == 0: |
| no_face_indices.append(idx) |
| else: |
| |
| images_list.append((idx, image_rgb)) |
|
|
| |
| 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) |
|
|
| |
| |
| |
| all_aug_images = {} |
| for idx, orig_rgb in images_list: |
| temp = [] |
| temp.append(("original", orig_rgb)) |
|
|
| |
| aug_list = apply_augmentations(orig_rgb) |
| temp.extend(aug_list) |
| all_aug_images[idx] = temp |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| label_embeds_map = {} |
|
|
| for idx, aug_images in all_aug_images.items(): |
| |
| for aug_label, aug_img in aug_images: |
| cropped = crop_largest_face(aug_img) |
| if cropped is None: |
| |
| continue |
| |
| emb = get_embedding(cropped) |
| if aug_label not in label_embeds_map: |
| label_embeds_map[aug_label] = [] |
| label_embeds_map[aug_label].append(emb) |
|
|
| |
| |
| |
|
|
| final_label_embeddings = {} |
| 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 len(final_label_embeddings) == 0: |
| return "No faces found in augmented images. Cannot proceed." |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| approach3_results = [] |
|
|
| 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] |
|
|
|
|
| |
| pred_a1 = model3.predict(emb_2d)[0] |
|
|
| |
| if gender.lower().startswith("m"): |
| |
| p4 = model4.predict(emb_2d_4)[0] |
| p6 = model6.predict(emb_2d_6)[0] |
| pred_a2 = 0.5 * (p4 + p6) |
| else: |
| |
| pred_a2 = model7.predict(emb_2d_7)[0] |
|
|
| |
| pred_a3 = 0.5 * (pred_a1 + pred_a2) |
|
|
| |
| approach3_results.append(pred_a3) |
|
|
| |
| if len(approach3_results) == 0: |
| return "No valid augmented faces after cropping; cannot proceed." |
|
|
| final_score = np.mean(approach3_results) |
| |
| final_score = round_value(final_score) |
|
|
| |
| |
| |
|
|
| score = final_score |
|
|
| |
| 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: |
| |
| message = "slightly below average" |
| elif score == 5.0: |
| message = "average" |
| elif score < 6.0: |
| |
| message = "decent and slightly above average" |
| elif score <= 6.25: |
| message = "good and decently above average" |
| elif score < 6.5: |
| |
| message = "very attractive and well above average" |
| elif score == 6.5: |
| message = "very attractive and well above average" |
| elif score < 6.75: |
| |
| message = "very attractive and well above average" |
| elif score <= 7.5: |
| message = "highly attractive and very well above average" |
| elif score < 7.75: |
| |
| message = "highly attractive and very well above average" |
| elif score == 7.75: |
| message = "very attractive and significantly above average" |
| elif score < 8.0: |
| |
| 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: |
| |
| message = "extremely amazing and one of the best faces ever created" |
|
|
| |
| 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() |