Murtadhaa commited on
Commit
632bebc
·
verified ·
1 Parent(s): c8f2316

Create code.py

Browse files
Files changed (1) hide show
  1. code.py +318 -0
code.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import cv2
4
+ import joblib # or import pickle
5
+ from mtcnn import MTCNN
6
+ from PIL import Image, ImageEnhance
7
+ from keras_facenet import FaceNet
8
+
9
+ # ------------------------------------------------------
10
+ # 1) Load your saved models
11
+ model3 = joblib.load("models/model_general.pkl") # Approach 1 general
12
+ model4 = joblib.load("models/model_male_1.pkl") # Male model #1
13
+ model6 = joblib.load("models/model_male_2.pkl") # Male model #2
14
+ model7 = joblib.load("models/model_female.pkl") # Female model
15
+
16
+ # 2) Prepare face detection & embedding
17
+ mtcnn_detector = MTCNN()
18
+ facenet = FaceNet()
19
+
20
+ # ------------------------------------------------------
21
+ # Helper functions
22
+
23
+ def round_to_quarter(value):
24
+ """Round numeric value to the nearest 0.25."""
25
+ return round(value * 4) / 4
26
+
27
+ def apply_augmentations(image_array):
28
+ """
29
+ Given a numpy array (RGB), return a list of 5 augmented images (numpy arrays).
30
+ We label them as: [flipped, bright_increase, bright_decrease, rotated_left, rotated_right].
31
+ """
32
+ augmentations = []
33
+ pil_img = Image.fromarray(image_array)
34
+
35
+ # 1) Flip horizontally
36
+ flipped = np.array(pil_img.transpose(Image.FLIP_LEFT_RIGHT))
37
+ augmentations.append(("flipped", flipped))
38
+
39
+ # 2) Brightness increase
40
+ bright_increase = ImageEnhance.Brightness(pil_img).enhance(1.3)
41
+ augmentations.append(("bright_up", np.array(bright_increase)))
42
+
43
+ # 3) Brightness decrease
44
+ bright_decrease = ImageEnhance.Brightness(pil_img).enhance(0.7)
45
+ augmentations.append(("bright_down", np.array(bright_decrease)))
46
+
47
+ # 4) Rotate left (+10 deg)
48
+ rotated_left = pil_img.rotate(10, expand=False) # expand=False to keep same size
49
+ augmentations.append(("rot_left", np.array(rotated_left)))
50
+
51
+ # 5) Rotate right (-10 deg)
52
+ rotated_right = pil_img.rotate(-10, expand=False)
53
+ augmentations.append(("rot_right", np.array(rotated_right)))
54
+
55
+ return augmentations
56
+
57
+ def crop_largest_face(image_array):
58
+ """
59
+ Use MTCNN to detect faces, and return the cropped region of the biggest face.
60
+ If no faces found, return None.
61
+ """
62
+ # MTCNN expects RGB image
63
+ faces = mtcnn_detector.detect_faces(image_array)
64
+ if len(faces) == 0:
65
+ return None
66
+ # find face with largest area
67
+ largest_face = max(faces, key=lambda face: face['box'][2] * face['box'][3])
68
+ x, y, w, h = largest_face['box']
69
+ # clip to ensure we don't go out of bounds
70
+ height, width = image_array.shape[:2]
71
+ x1, y1 = max(x, 0), max(y, 0)
72
+ x2, y2 = min(x + w, width), min(y + h, height)
73
+ return image_array[y1:y2, x1:x2]
74
+
75
+ def get_embedding(image_array):
76
+ """
77
+ Convert single face (RGB) to embedding using keras-facenet.
78
+ The .embeddings() method expects a list of arrays.
79
+ Returns 512-dim embedding (np.array).
80
+ """
81
+ # FaceNet wants images in [RGB], shape ~ (H, W, 3).
82
+ # We'll assume each image is properly cropped around the face.
83
+ # If needed, you might have to resize to (160,160). But FaceNet from keras-facenet
84
+ # often does it internally. We'll pass as is.
85
+ emb = facenet.embeddings([image_array])[0] # shape (512,)
86
+ return emb
87
+
88
+
89
+ def prepare_input_for_model(model, embedding, gender):
90
+ """
91
+ If the model expects 513 features, prepend the gender flag (1 for male, 0 for female).
92
+ Otherwise, just return the 512-dim embedding.
93
+ """
94
+ if model.n_features_in_ == 513:
95
+ gender_flag = 1 if gender.lower().startswith("m") else 0
96
+ arr = np.concatenate(([gender_flag], embedding))
97
+ return arr.reshape(1, -1)
98
+ else:
99
+ return embedding.reshape(1, -1)
100
+
101
+
102
+
103
+ # ------------------------------------------------------
104
+ # Main pipeline function
105
+ def process_images(img_paths, gender):
106
+ """
107
+ - gender: "Male" or "Female"
108
+ - img_paths: list of image file paths uploaded by user
109
+ Returns a string with final result or error messages.
110
+ """
111
+
112
+ # 1) Verify each image has at least one face
113
+ images_list = []
114
+ no_face_indices = []
115
+ for idx, path in enumerate(img_paths):
116
+ # read with cv2
117
+ image_bgr = cv2.imread(path[0])
118
+ if image_bgr is None:
119
+ no_face_indices.append(idx)
120
+ continue
121
+ # convert BGR -> RGB
122
+ image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
123
+
124
+ # check face
125
+ faces = mtcnn_detector.detect_faces(image_rgb)
126
+ if len(faces) == 0:
127
+ no_face_indices.append(idx)
128
+ else:
129
+ # store valid image
130
+ images_list.append((idx, image_rgb))
131
+
132
+ # if ANY image had no face, report it and stop
133
+ if no_face_indices:
134
+ msg_lines = []
135
+ for bad_i in no_face_indices:
136
+ msg_lines.append(f"Image {bad_i+1} has no detected face.")
137
+ msg_lines.append("Please try again with different images.")
138
+ return "\n".join(msg_lines)
139
+
140
+ # 2) For each valid image, create 5 augmentations + original
141
+ # We'll gather them in a structure: { idx: [(aug_label, image_array), ...], ... }
142
+ # so that each input image has 6 versions.
143
+ all_aug_images = {} # key = input_idx, value = list of (aug_label, np_array)
144
+ for idx, orig_rgb in images_list:
145
+ temp = []
146
+ temp.append(("original", orig_rgb))
147
+
148
+ # augment
149
+ aug_list = apply_augmentations(orig_rgb)
150
+ temp.extend(aug_list) # now we have 6 total
151
+ all_aug_images[idx] = temp
152
+
153
+ # 3) Crop largest face in each augmented image. If no face found => skip that augmentation
154
+ # We'll keep them in { idx: { aug_label: [embedding arrays], ... }, ... }
155
+ # Actually we want a single embedding per augmented image, so we'll store that.
156
+ # Then we can average later by augmentation type across all input images.
157
+ # That means for each input_idx, for each "aug_label", we get a single embedding, or None if no face.
158
+
159
+ # We'll store per augmentation label across all images, so we can average them later:
160
+ # label_embeds_map = { 'original': [], 'flipped': [], ... }
161
+ label_embeds_map = {}
162
+
163
+ for idx, aug_images in all_aug_images.items():
164
+ # aug_images is a list of (aug_label, np_array) for that input
165
+ for aug_label, aug_img in aug_images:
166
+ cropped = crop_largest_face(aug_img)
167
+ if cropped is None:
168
+ # skip
169
+ continue
170
+ # get embedding
171
+ emb = get_embedding(cropped)
172
+ if aug_label not in label_embeds_map:
173
+ label_embeds_map[aug_label] = []
174
+ label_embeds_map[aug_label].append(emb)
175
+
176
+ # 4) Now we have up to 6 keys in label_embeds_map: [original, flipped, bright_up, bright_down, rot_left, rot_right].
177
+ # Some may have fewer if some faces were not found in augmented versions.
178
+ # We'll compute average embedding for each label if it has at least 1 embedding.
179
+
180
+ final_label_embeddings = {} # label -> (512,) average
181
+ for label, embed_list in label_embeds_map.items():
182
+ if len(embed_list) == 0:
183
+ continue
184
+ avg_emb = np.mean(embed_list, axis=0)
185
+ final_label_embeddings[label] = avg_emb
186
+
187
+ # If *none* of the 6 augmentations ended up with a face, we can't proceed
188
+ if len(final_label_embeddings) == 0:
189
+ return "No faces found in augmented images. Cannot proceed."
190
+
191
+ # 5) For each label's average embedding, compute:
192
+ # - Approach1 => model3.predict
193
+ # - Approach2 => depends on gender:
194
+ # male => average( model4.predict, model6.predict )
195
+ # female => model7.predict
196
+ # - Hybrid => 0.5*(Approach1 + Approach2)
197
+ # Then we'll store them in a list so we can average across labels at the end.
198
+
199
+ approach3_results = [] # We'll store the final "hybrid" predictions for each label
200
+
201
+ for label, emb in final_label_embeddings.items():
202
+ emb_2d = prepare_input_for_model(model3, emb, gender)
203
+ pred_a1 = model3.predict(emb_2d)[0]
204
+
205
+ emb_2d_4 = prepare_input_for_model(model4, emb, gender)
206
+ p4 = model4.predict(emb_2d_4)[0]
207
+
208
+ emb_2d_6 = prepare_input_for_model(model6, emb, gender)
209
+ p6 = model6.predict(emb_2d_6)[0]
210
+
211
+ emb_2d_7 = prepare_input_for_model(model7, emb, gender)
212
+ pred_a2 = model7.predict(emb_2d_7)[0]
213
+
214
+
215
+ # Approach 1
216
+ pred_a1 = model3.predict(emb_2d)[0]
217
+
218
+ # Approach 2
219
+ if gender.lower().startswith("m"):
220
+ # male => average of model4 + model6
221
+ p4 = model4.predict(emb_2d_4)[0]
222
+ p6 = model6.predict(emb_2d_6)[0]
223
+ pred_a2 = 0.5 * (p4 + p6)
224
+ else:
225
+ # female => model7 alone
226
+ pred_a2 = model7.predict(emb_2d_7)[0]
227
+
228
+ # Approach 3 => average(approach1, approach2)
229
+ pred_a3 = 0.5 * (pred_a1 + pred_a2)
230
+
231
+ # We'll store the final approach 3 result
232
+ approach3_results.append(pred_a3)
233
+
234
+ # 6) Average across all labels (the different augmentations)
235
+ if len(approach3_results) == 0:
236
+ return "No valid augmented faces after cropping; cannot proceed."
237
+
238
+ final_score = np.mean(approach3_results)
239
+ # Round to nearest quarter
240
+ final_score_quarter = round_to_quarter(final_score)
241
+
242
+ # clamp or keep it? The instructions say "X out of 10"
243
+ # We'll do a simple float formatting
244
+ # ... after computing final_score_quarter ...
245
+
246
+ score = final_score_quarter # just to shorten variable name
247
+
248
+ # Determine a descriptive message based on the user's intervals
249
+ if score <= 3.0:
250
+ message = "very unattractive and significantly below average"
251
+ elif score <= 4.0:
252
+ message = "very below average"
253
+ elif score <= 4.5:
254
+ message = "below average"
255
+ elif score < 5.0:
256
+ # Covers up to 4.75 or 4.99, etc.
257
+ message = "slightly below average"
258
+ elif score == 5.0:
259
+ message = "average"
260
+ elif score < 6.0:
261
+ # Covers 5.25, 5.5, 5.75, etc.
262
+ message = "decent and slightly above average"
263
+ elif score <= 6.25:
264
+ message = "good and decently above average"
265
+ elif score < 6.5:
266
+ # Covers 6.3, 6.4, etc.
267
+ message = "very attractive and well above average"
268
+ elif score == 6.5:
269
+ message = "very attractive and well above average"
270
+ elif score < 6.75:
271
+ # Covers 6.6, 6.7
272
+ message = "very attractive and well above average"
273
+ elif score <= 7.5:
274
+ message = "highly attractive and very well above average"
275
+ elif score < 7.75:
276
+ # Covers e.g. 7.6
277
+ message = "highly attractive and very well above average"
278
+ elif score == 7.75:
279
+ message = "very attractive and significantly above average"
280
+ elif score < 8.0:
281
+ # Covers e.g. 7.8
282
+ message = "very attractive and significantly above average"
283
+ elif score <= 8.5:
284
+ message = "extremely amazing and very attractive"
285
+ elif score < 8.75:
286
+ message = "extremely amazing and very attractive"
287
+ elif score <= 9.25:
288
+ message = "extremely amazing and one of the best faces in the world"
289
+ elif score < 9.5:
290
+ message = "extremely amazing and one of the best faces in the world"
291
+ else:
292
+ # >= 9.5
293
+ message = "extremely amazing and one of the best faces ever created"
294
+
295
+ # Now include that message in the final string
296
+ return f"This person is {score} out of 10 in looks, which is {message}."
297
+
298
+
299
+ interface = gr.Interface(
300
+ fn=process_images,
301
+ inputs=[
302
+ gr.Gallery(label="Upload Images", type='filepath'),
303
+ gr.Radio(["Male", "Female"], label="Gender")
304
+ ],
305
+ outputs=gr.Textbox(label="Result"),
306
+ title="How Attractive Are You?",
307
+ description=(
308
+ "**Upload a photo (or multiple photos) and see how high you score out of 10.**\n\n"
309
+ "• Please ensure the image is well-lit and only shows your face, if possible.\n"
310
+ " (We automatically crop to the largest face, but it’s best to avoid extra faces.)\n\n"
311
+ "• The model can work with a single image, but **3–5 images** may yield a more accurate score.\n\n"
312
+ "• This tool focuses on **facial symmetry**—it does **not** account for personal preferences or other factors.\n"
313
+ " Please don’t take the result too seriously!\n\n"
314
+ "*If you’re curious about how this was made or the standards used, feel free to message me wherever you got this link.*"
315
+ )
316
+ )
317
+
318
+ interface.launch()