Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,65 +1,120 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
import mediapipe as mp
|
| 3 |
import numpy as np
|
| 4 |
import math
|
| 5 |
import json
|
| 6 |
-
from PIL import Image
|
| 7 |
-
from mediapipe.tasks import python as mp_python
|
| 8 |
-
from mediapipe.tasks.python import vision as mp_vision
|
| 9 |
import urllib.request
|
| 10 |
import os
|
|
|
|
| 11 |
|
| 12 |
-
# ββ Download
|
| 13 |
-
MODEL_PATH = "/tmp/
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
|
| 19 |
-
print("Model downloaded.")
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def analyse_face(image):
|
| 39 |
try:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
|
|
|
| 48 |
return json.dumps({"error": "No face detected. Please upload a clear, front-facing photo."})
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
def dist(a, b):
|
| 53 |
return math.sqrt(
|
| 54 |
-
((
|
| 55 |
-
((
|
| 56 |
)
|
| 57 |
|
|
|
|
|
|
|
| 58 |
face_ref = dist(33, 263)
|
| 59 |
-
if face_ref
|
| 60 |
-
return json.dumps({"error": "
|
| 61 |
|
| 62 |
-
# ββ Age estimation ββ
|
| 63 |
face_height = dist(10, 152)
|
| 64 |
lower_face_h = dist(168, 152)
|
| 65 |
lower_ratio = lower_face_h / face_height if face_height > 0 else 0.52
|
|
@@ -85,14 +140,10 @@ def analyse_face(image):
|
|
| 85 |
age_mid = max(18, min(72, round(age_raw)))
|
| 86 |
age_range = f"{max(18, age_mid - 4)}\u2013{age_mid + 4}"
|
| 87 |
|
| 88 |
-
|
| 89 |
-
wrinkle = round(max(1.0, min(9.9, 1 + texture * 18 + (age_mid - 18) * 0.10)), 1)
|
| 90 |
-
|
| 91 |
-
# ββ Elasticity score ββ
|
| 92 |
cheek_sag = ((dist(116, 61) + dist(345, 291)) / 2) / face_ref
|
| 93 |
elasticity = round(max(1.0, min(9.9, 10 - (cheek_sag - 0.55) * 18 - (age_mid - 18) * 0.10)), 1)
|
| 94 |
|
| 95 |
-
# ββ Jawline score ββ
|
| 96 |
jaw_pts = [234,93,132,58,172,136,150,149,176,148,152,377,400,378,379,365,397,288,361,323,454]
|
| 97 |
jaw_dev = 0.0
|
| 98 |
for i in range(1, len(jaw_pts) - 1):
|
|
@@ -108,8 +159,6 @@ def analyse_face(image):
|
|
| 108 |
age_factor = round(max(0.0, min(1.0, (age_mid - 18) / 54)), 3)
|
| 109 |
years_younger = max(3, round(age_factor * 14 + 2))
|
| 110 |
|
| 111 |
-
landmarks_out = [{"x": float(l.x), "y": float(l.y), "z": float(l.z)} for l in lm]
|
| 112 |
-
|
| 113 |
return json.dumps({
|
| 114 |
"age_range": age_range,
|
| 115 |
"age_mid": age_mid,
|
|
@@ -118,7 +167,7 @@ def analyse_face(image):
|
|
| 118 |
"jawline": jawline,
|
| 119 |
"years_younger": years_younger,
|
| 120 |
"age_factor": age_factor,
|
| 121 |
-
"landmarks":
|
| 122 |
"image_width": w,
|
| 123 |
"image_height": h
|
| 124 |
})
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import math
|
| 4 |
import json
|
|
|
|
|
|
|
|
|
|
| 5 |
import urllib.request
|
| 6 |
import os
|
| 7 |
+
from PIL import Image
|
| 8 |
|
| 9 |
+
# ββ Download face landmark TFLite model ββ
|
| 10 |
+
MODEL_PATH = "/tmp/face_landmark.tflite"
|
| 11 |
+
DETECTOR_PATH = "/tmp/face_detection.tflite"
|
| 12 |
|
| 13 |
+
MODEL_URL = "https://storage.googleapis.com/mediapipe-assets/face_landmark.tflite"
|
| 14 |
+
DETECTOR_URL = "https://storage.googleapis.com/mediapipe-assets/face_detection_short_range.tflite"
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
for path, url in [(MODEL_PATH, MODEL_URL), (DETECTOR_PATH, DETECTOR_URL)]:
|
| 17 |
+
if not os.path.exists(path):
|
| 18 |
+
print(f"Downloading {url}...")
|
| 19 |
+
urllib.request.urlretrieve(url, path)
|
| 20 |
+
print(f"Downloaded to {path}")
|
| 21 |
+
|
| 22 |
+
# ββ Load TFLite interpreter (no OpenGL needed) ββ
|
| 23 |
+
try:
|
| 24 |
+
import tflite_runtime.interpreter as tflite
|
| 25 |
+
print("Using tflite_runtime")
|
| 26 |
+
except ImportError:
|
| 27 |
+
import tensorflow as tf
|
| 28 |
+
tflite = tf.lite
|
| 29 |
+
print("Using tensorflow.lite")
|
| 30 |
+
|
| 31 |
+
detector_interp = tflite.Interpreter(model_path=DETECTOR_PATH)
|
| 32 |
+
detector_interp.allocate_tensors()
|
| 33 |
+
landmark_interp = tflite.Interpreter(model_path=MODEL_PATH)
|
| 34 |
+
landmark_interp.allocate_tensors()
|
| 35 |
+
|
| 36 |
+
det_in = detector_interp.get_input_details()
|
| 37 |
+
det_out = detector_interp.get_output_details()
|
| 38 |
+
lm_in = landmark_interp.get_input_details()
|
| 39 |
+
lm_out = landmark_interp.get_output_details()
|
| 40 |
+
|
| 41 |
+
print(f"Detector input shape: {det_in[0]['shape']}")
|
| 42 |
+
print(f"Landmark input shape: {lm_in[0]['shape']}")
|
| 43 |
+
print("Models ready.")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def preprocess(image_pil, target_size):
|
| 47 |
+
img = image_pil.convert("RGB").resize(target_size, Image.LANCZOS)
|
| 48 |
+
arr = np.array(img, dtype=np.float32) / 255.0
|
| 49 |
+
return arr[np.newaxis, ...] # (1, H, W, 3)
|
| 50 |
|
| 51 |
|
| 52 |
def analyse_face(image):
|
| 53 |
try:
|
| 54 |
+
orig_w, orig_h = image.size
|
| 55 |
+
|
| 56 |
+
# ββ Step 1: Detect face bounding box ββ
|
| 57 |
+
det_size = (det_in[0]['shape'][2], det_in[0]['shape'][1]) # (W, H)
|
| 58 |
+
det_input = preprocess(image, det_size)
|
| 59 |
+
detector_interp.set_tensor(det_in[0]['index'], det_input)
|
| 60 |
+
detector_interp.invoke()
|
| 61 |
|
| 62 |
+
# Get detection output β boxes are [y_min, x_min, y_max, x_max] normalised
|
| 63 |
+
boxes = detector_interp.get_tensor(det_out[0]['index'])[0] # (N, 4)
|
| 64 |
+
scores = detector_interp.get_tensor(det_out[1]['index'])[0] # (N,)
|
| 65 |
|
| 66 |
+
best_idx = int(np.argmax(scores))
|
| 67 |
+
if scores[best_idx] < 0.4:
|
| 68 |
return json.dumps({"error": "No face detected. Please upload a clear, front-facing photo."})
|
| 69 |
|
| 70 |
+
y1, x1, y2, x2 = boxes[best_idx]
|
| 71 |
+
# Add 20% padding
|
| 72 |
+
pad = 0.20
|
| 73 |
+
bw = x2 - x1
|
| 74 |
+
bh = y2 - y1
|
| 75 |
+
x1 = max(0.0, x1 - bw * pad)
|
| 76 |
+
y1 = max(0.0, y1 - bh * pad)
|
| 77 |
+
x2 = min(1.0, x2 + bw * pad)
|
| 78 |
+
y2 = min(1.0, y2 + bh * pad)
|
| 79 |
+
|
| 80 |
+
crop = image.crop((int(x1 * orig_w), int(y1 * orig_h),
|
| 81 |
+
int(x2 * orig_w), int(y2 * orig_h)))
|
| 82 |
+
crop_w, crop_h = crop.size
|
| 83 |
+
|
| 84 |
+
# ββ Step 2: Run landmark model on cropped face ββ
|
| 85 |
+
lm_size = (lm_in[0]['shape'][2], lm_in[0]['shape'][1])
|
| 86 |
+
lm_input = preprocess(crop, lm_size)
|
| 87 |
+
landmark_interp.set_tensor(lm_in[0]['index'], lm_input)
|
| 88 |
+
landmark_interp.invoke()
|
| 89 |
+
|
| 90 |
+
raw_lm = landmark_interp.get_tensor(lm_out[0]['index']) # shape varies
|
| 91 |
+
raw_lm = raw_lm.reshape(-1, 3) # (468, 3) β x, y, z all in [0, lm_size]
|
| 92 |
+
|
| 93 |
+
lm_w, lm_h = lm_size
|
| 94 |
+
# Normalise to [0,1] relative to original image
|
| 95 |
+
landmarks = []
|
| 96 |
+
for pt in raw_lm:
|
| 97 |
+
# pt in landmark input coords β map to crop β map to original
|
| 98 |
+
nx = (pt[0] / lm_w) * (x2 - x1) + x1
|
| 99 |
+
ny = (pt[1] / lm_h) * (y2 - y1) + y1
|
| 100 |
+
nz = float(pt[2]) / lm_w
|
| 101 |
+
landmarks.append({"x": float(nx), "y": float(ny), "z": nz})
|
| 102 |
+
|
| 103 |
+
# ββ Score calculation (same geometry as before) ββ
|
| 104 |
+
h, w = orig_h, orig_w
|
| 105 |
|
| 106 |
def dist(a, b):
|
| 107 |
return math.sqrt(
|
| 108 |
+
((landmarks[a]['x'] - landmarks[b]['x']) * w) ** 2 +
|
| 109 |
+
((landmarks[a]['y'] - landmarks[b]['y']) * h) ** 2
|
| 110 |
)
|
| 111 |
|
| 112 |
+
lm = [type('L', (), {'x': p['x'], 'y': p['y'], 'z': p['z']})() for p in landmarks]
|
| 113 |
+
|
| 114 |
face_ref = dist(33, 263)
|
| 115 |
+
if face_ref < 1:
|
| 116 |
+
return json.dumps({"error": "Face too small. Please use a closer photo."})
|
| 117 |
|
|
|
|
| 118 |
face_height = dist(10, 152)
|
| 119 |
lower_face_h = dist(168, 152)
|
| 120 |
lower_ratio = lower_face_h / face_height if face_height > 0 else 0.52
|
|
|
|
| 140 |
age_mid = max(18, min(72, round(age_raw)))
|
| 141 |
age_range = f"{max(18, age_mid - 4)}\u2013{age_mid + 4}"
|
| 142 |
|
| 143 |
+
wrinkle = round(max(1.0, min(9.9, 1 + texture * 18 + (age_mid - 18) * 0.10)), 1)
|
|
|
|
|
|
|
|
|
|
| 144 |
cheek_sag = ((dist(116, 61) + dist(345, 291)) / 2) / face_ref
|
| 145 |
elasticity = round(max(1.0, min(9.9, 10 - (cheek_sag - 0.55) * 18 - (age_mid - 18) * 0.10)), 1)
|
| 146 |
|
|
|
|
| 147 |
jaw_pts = [234,93,132,58,172,136,150,149,176,148,152,377,400,378,379,365,397,288,361,323,454]
|
| 148 |
jaw_dev = 0.0
|
| 149 |
for i in range(1, len(jaw_pts) - 1):
|
|
|
|
| 159 |
age_factor = round(max(0.0, min(1.0, (age_mid - 18) / 54)), 3)
|
| 160 |
years_younger = max(3, round(age_factor * 14 + 2))
|
| 161 |
|
|
|
|
|
|
|
| 162 |
return json.dumps({
|
| 163 |
"age_range": age_range,
|
| 164 |
"age_mid": age_mid,
|
|
|
|
| 167 |
"jawline": jawline,
|
| 168 |
"years_younger": years_younger,
|
| 169 |
"age_factor": age_factor,
|
| 170 |
+
"landmarks": landmarks,
|
| 171 |
"image_width": w,
|
| 172 |
"image_height": h
|
| 173 |
})
|