Incostra commited on
Commit
2d415c4
Β·
verified Β·
1 Parent(s): 2836940

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -46
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 model if not cached ──
13
- MODEL_PATH = "/tmp/face_landmarker.task"
14
- MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
15
 
16
- if not os.path.exists(MODEL_PATH):
17
- print("Downloading face landmarker model...")
18
- urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
19
- print("Model downloaded.")
20
 
21
- # ── Build FaceLandmarker ──
22
- base_options = mp_python.BaseOptions(
23
- model_asset_path=MODEL_PATH,
24
- delegate=mp_python.BaseOptions.Delegate.CPU
25
- )
26
- options = mp_vision.FaceLandmarkerOptions(
27
- base_options=base_options,
28
- output_face_blendshapes=False,
29
- output_facial_transformation_matrixes=False,
30
- num_faces=1,
31
- min_face_detection_confidence=0.4,
32
- min_face_presence_confidence=0.4
33
- )
34
- landmarker = mp_vision.FaceLandmarker.create_from_options(options)
35
- print("FaceLandmarker ready.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  def analyse_face(image):
39
  try:
40
- # Convert PIL to MediaPipe Image
41
- img_rgb = np.array(image.convert("RGB"))
42
- h, w = img_rgb.shape[:2]
43
- mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
 
 
 
44
 
45
- result = landmarker.detect(mp_image)
 
 
46
 
47
- if not result.face_landmarks or len(result.face_landmarks) == 0:
 
48
  return json.dumps({"error": "No face detected. Please upload a clear, front-facing photo."})
49
 
50
- lm = result.face_landmarks[0] # list of NormalizedLandmark
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def dist(a, b):
53
  return math.sqrt(
54
- ((lm[a].x - lm[b].x) * w) ** 2 +
55
- ((lm[a].y - lm[b].y) * h) ** 2
56
  )
57
 
 
 
58
  face_ref = dist(33, 263)
59
- if face_ref == 0:
60
- return json.dumps({"error": "Could not measure face. Please try a different photo."})
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
- # ── Wrinkle score ──
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": landmarks_out,
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
  })