Incostra commited on
Commit
f1af6fa
Β·
verified Β·
1 Parent(s): 925c9e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -81
app.py CHANGED
@@ -5,98 +5,105 @@ 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
- import tensorflow as tf
24
- tflite = tf.lite
25
- print("Using tensorflow.lite, TF version:", tf.__version__)
26
-
27
- detector_interp = tflite.Interpreter(model_path=DETECTOR_PATH)
28
- detector_interp.allocate_tensors()
29
- landmark_interp = tflite.Interpreter(model_path=MODEL_PATH)
30
  landmark_interp.allocate_tensors()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- det_in = detector_interp.get_input_details()
33
- det_out = detector_interp.get_output_details()
34
- lm_in = landmark_interp.get_input_details()
35
- lm_out = landmark_interp.get_output_details()
36
-
37
- print(f"Detector input shape: {det_in[0]['shape']}")
38
- print(f"Landmark input shape: {lm_in[0]['shape']}")
39
- print("Models ready.")
40
-
41
 
42
- def preprocess(image_pil, target_size):
43
- img = image_pil.convert("RGB").resize(target_size, Image.LANCZOS)
44
- arr = np.array(img, dtype=np.float32) / 255.0
45
- return arr[np.newaxis, ...] # (1, H, W, 3)
 
 
46
 
47
 
48
  def analyse_face(image):
49
  try:
50
  orig_w, orig_h = image.size
51
 
52
- # ── Step 1: Detect face bounding box ──
53
- det_size = (det_in[0]['shape'][2], det_in[0]['shape'][1]) # (W, H)
54
- det_input = preprocess(image, det_size)
55
- detector_interp.set_tensor(det_in[0]['index'], det_input)
56
- detector_interp.invoke()
57
-
58
- # Get detection output β€” boxes are [y_min, x_min, y_max, x_max] normalised
59
- boxes = detector_interp.get_tensor(det_out[0]['index'])[0] # (N, 4)
60
- scores = detector_interp.get_tensor(det_out[1]['index'])[0] # (N,)
61
-
62
- best_idx = int(np.argmax(scores))
63
- if scores[best_idx] < 0.4:
64
- return json.dumps({"error": "No face detected. Please upload a clear, front-facing photo."})
65
-
66
- y1, x1, y2, x2 = boxes[best_idx]
67
- # Add 20% padding
68
- pad = 0.20
69
- bw = x2 - x1
70
- bh = y2 - y1
71
- x1 = max(0.0, x1 - bw * pad)
72
- y1 = max(0.0, y1 - bh * pad)
73
- x2 = min(1.0, x2 + bw * pad)
74
- y2 = min(1.0, y2 + bh * pad)
75
-
76
- crop = image.crop((int(x1 * orig_w), int(y1 * orig_h),
77
- int(x2 * orig_w), int(y2 * orig_h)))
78
- crop_w, crop_h = crop.size
79
-
80
- # ── Step 2: Run landmark model on cropped face ──
81
- lm_size = (lm_in[0]['shape'][2], lm_in[0]['shape'][1])
82
- lm_input = preprocess(crop, lm_size)
83
- landmark_interp.set_tensor(lm_in[0]['index'], lm_input)
84
  landmark_interp.invoke()
85
 
86
- raw_lm = landmark_interp.get_tensor(lm_out[0]['index']) # shape varies
87
- raw_lm = raw_lm.reshape(-1, 3) # (468, 3) β€” x, y, z all in [0, lm_size]
 
 
 
 
 
 
 
 
 
 
88
 
89
- lm_w, lm_h = lm_size
90
- # Normalise to [0,1] relative to original image
91
  landmarks = []
92
- for pt in raw_lm:
93
- # pt in landmark input coords β†’ map to crop β†’ map to original
94
- nx = (pt[0] / lm_w) * (x2 - x1) + x1
95
- ny = (pt[1] / lm_h) * (y2 - y1) + y1
96
  nz = float(pt[2]) / lm_w
97
  landmarks.append({"x": float(nx), "y": float(ny), "z": nz})
98
 
99
- # ── Score calculation (same geometry as before) ──
100
  h, w = orig_h, orig_w
101
 
102
  def dist(a, b):
@@ -105,7 +112,7 @@ def analyse_face(image):
105
  ((landmarks[a]['y'] - landmarks[b]['y']) * h) ** 2
106
  )
107
 
108
- lm = [type('L', (), {'x': p['x'], 'y': p['y'], 'z': p['z']})() for p in landmarks]
109
 
110
  face_ref = dist(33, 263)
111
  if face_ref < 1:
@@ -115,14 +122,14 @@ def analyse_face(image):
115
  lower_face_h = dist(168, 152)
116
  lower_ratio = lower_face_h / face_height if face_height > 0 else 0.52
117
 
118
- eye_drop_l = (lm[33].y - lm[234].y) * h
119
- eye_drop_r = (lm[263].y - lm[454].y) * h
120
  orbital_drop = ((eye_drop_l + eye_drop_r) / 2) / face_ref
121
 
122
  nasolabial = ((dist(50, 61) + dist(280, 291)) / 2) / face_ref
123
 
124
  forehead_idx = [10,109,67,103,54,21,162,127,234,338,297,332,284,251,389,356,454]
125
- z_vals = [lm[i].z for i in forehead_idx]
126
  z_mean = sum(z_vals) / len(z_vals)
127
  z_var = sum((z - z_mean) ** 2 for z in z_vals) / len(z_vals)
128
  texture = math.sqrt(abs(z_var)) * 100
@@ -140,14 +147,15 @@ def analyse_face(image):
140
  cheek_sag = ((dist(116, 61) + dist(345, 291)) / 2) / face_ref
141
  elasticity = round(max(1.0, min(9.9, 10 - (cheek_sag - 0.55) * 18 - (age_mid - 18) * 0.10)), 1)
142
 
143
- jaw_pts = [234,93,132,58,172,136,150,149,176,148,152,377,400,378,379,365,397,288,361,323,454]
 
144
  jaw_dev = 0.0
145
  for i in range(1, len(jaw_pts) - 1):
146
  p, c, n = jaw_pts[i-1], jaw_pts[i], jaw_pts[i+1]
147
- ax = (lm[c].x - lm[p].x) * w
148
- ay = (lm[c].y - lm[p].y) * h
149
- bx = (lm[n].x - lm[c].x) * w
150
- by = (lm[n].y - lm[c].y) * h
151
  jaw_dev += abs(ax * by - ay * bx) / (face_ref ** 2)
152
  jaw_dev /= len(jaw_pts)
153
  jawline = round(max(1.0, min(9.9, 10 - jaw_dev * 0.8 - (age_mid - 18) * 0.09)), 1)
 
5
  import urllib.request
6
  import os
7
  from PIL import Image
8
+ import tensorflow as tf
9
 
10
+ print("TensorFlow version:", tf.__version__)
 
 
11
 
12
+ # ── Download face landmark model ──
13
+ MODEL_PATH = "/tmp/face_landmark.tflite"
14
+ MODEL_URL = "https://storage.googleapis.com/mediapipe-assets/face_landmark.tflite"
15
 
16
+ if not os.path.exists(MODEL_PATH):
17
+ print("Downloading face landmark model...")
18
+ urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
19
+ print("Downloaded.")
 
20
 
21
+ landmark_interp = tf.lite.Interpreter(model_path=MODEL_PATH)
 
 
 
 
 
 
 
22
  landmark_interp.allocate_tensors()
23
+ lm_in = landmark_interp.get_input_details()
24
+ lm_out = landmark_interp.get_output_details()
25
+ LM_SIZE = (lm_in[0]['shape'][2], lm_in[0]['shape'][1]) # (W, H)
26
+ print(f"Landmark model input: {lm_in[0]['shape']} β†’ size {LM_SIZE}")
27
+ print(f"Landmark model outputs: {[o['shape'] for o in lm_out]}")
28
+ print("Model ready.")
29
+
30
+
31
+ def detect_face_crop(image_pil):
32
+ """
33
+ Use OpenCV-based face detection to crop the face region.
34
+ Falls back to centre-crop if no face found.
35
+ """
36
+ try:
37
+ import cv2
38
+ img = np.array(image_pil.convert("RGB"))
39
+ gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
40
+ h, w = img.shape[:2]
41
+
42
+ cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
43
+ detector = cv2.CascadeClassifier(cascade_path)
44
+ faces = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=4, minSize=(60,60))
45
+
46
+ if len(faces) > 0:
47
+ # Take largest face
48
+ faces = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)
49
+ x, y, fw, fh = faces[0]
50
+ pad = int(max(fw, fh) * 0.30)
51
+ x1 = max(0, x - pad)
52
+ y1 = max(0, y - pad)
53
+ x2 = min(w, x + fw + pad)
54
+ y2 = min(h, y + fh + pad)
55
+ return image_pil.crop((x1, y1, x2, y2)), x1/w, y1/h, x2/w, y2/h
56
 
57
+ except Exception as e:
58
+ print(f"CV2 detection failed: {e}")
 
 
 
 
 
 
 
59
 
60
+ # Fallback: use centre 80% of image
61
+ img_w, img_h = image_pil.size
62
+ margin = 0.10
63
+ return (image_pil.crop((int(img_w*margin), int(img_h*margin),
64
+ int(img_w*(1-margin)), int(img_h*(1-margin)))),
65
+ margin, margin, 1-margin, 1-margin)
66
 
67
 
68
  def analyse_face(image):
69
  try:
70
  orig_w, orig_h = image.size
71
 
72
+ # Step 1: Detect and crop face
73
+ crop, cx1, cy1, cx2, cy2 = detect_face_crop(image)
74
+
75
+ # Step 2: Resize crop to landmark model input size
76
+ crop_resized = crop.convert("RGB").resize(LM_SIZE, Image.LANCZOS)
77
+ inp = np.array(crop_resized, dtype=np.float32) / 255.0
78
+ inp = inp[np.newaxis, ...] # (1, H, W, 3)
79
+
80
+ # Step 3: Run landmark model
81
+ landmark_interp.set_tensor(lm_in[0]['index'], inp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  landmark_interp.invoke()
83
 
84
+ # Output 0: landmarks, shape (1, 1404) = 468 points * 3 (x,y,z)
85
+ raw = landmark_interp.get_tensor(lm_out[0]['index'])
86
+ raw = raw.reshape(-1, 3) # (468, 3)
87
+
88
+ # Check if face was actually detected (confidence output if available)
89
+ if len(lm_out) > 1:
90
+ conf = float(landmark_interp.get_tensor(lm_out[1]['index']).flatten()[0])
91
+ print(f"Landmark confidence: {conf:.3f}")
92
+ if conf < 0.3:
93
+ return json.dumps({"error": "No face detected. Please upload a clear, front-facing photo."})
94
+
95
+ lm_w, lm_h = LM_SIZE
96
 
97
+ # Denormalise: landmark coords are in pixel space of the crop input
98
+ # Map back to original image normalised coords
99
  landmarks = []
100
+ for pt in raw:
101
+ nx = (pt[0] / lm_w) * (cx2 - cx1) + cx1
102
+ ny = (pt[1] / lm_h) * (cy2 - cy1) + cy1
 
103
  nz = float(pt[2]) / lm_w
104
  landmarks.append({"x": float(nx), "y": float(ny), "z": nz})
105
 
106
+ # ── Score calculation from geometry ──
107
  h, w = orig_h, orig_w
108
 
109
  def dist(a, b):
 
112
  ((landmarks[a]['y'] - landmarks[b]['y']) * h) ** 2
113
  )
114
 
115
+ lm = landmarks
116
 
117
  face_ref = dist(33, 263)
118
  if face_ref < 1:
 
122
  lower_face_h = dist(168, 152)
123
  lower_ratio = lower_face_h / face_height if face_height > 0 else 0.52
124
 
125
+ eye_drop_l = (lm[33]['y'] - lm[234]['y']) * h
126
+ eye_drop_r = (lm[263]['y'] - lm[454]['y']) * h
127
  orbital_drop = ((eye_drop_l + eye_drop_r) / 2) / face_ref
128
 
129
  nasolabial = ((dist(50, 61) + dist(280, 291)) / 2) / face_ref
130
 
131
  forehead_idx = [10,109,67,103,54,21,162,127,234,338,297,332,284,251,389,356,454]
132
+ z_vals = [lm[i]['z'] for i in forehead_idx]
133
  z_mean = sum(z_vals) / len(z_vals)
134
  z_var = sum((z - z_mean) ** 2 for z in z_vals) / len(z_vals)
135
  texture = math.sqrt(abs(z_var)) * 100
 
147
  cheek_sag = ((dist(116, 61) + dist(345, 291)) / 2) / face_ref
148
  elasticity = round(max(1.0, min(9.9, 10 - (cheek_sag - 0.55) * 18 - (age_mid - 18) * 0.10)), 1)
149
 
150
+ jaw_pts = [234,93,132,58,172,136,150,149,176,148,152,
151
+ 377,400,378,379,365,397,288,361,323,454]
152
  jaw_dev = 0.0
153
  for i in range(1, len(jaw_pts) - 1):
154
  p, c, n = jaw_pts[i-1], jaw_pts[i], jaw_pts[i+1]
155
+ ax = (lm[c]['x'] - lm[p]['x']) * w
156
+ ay = (lm[c]['y'] - lm[p]['y']) * h
157
+ bx = (lm[n]['x'] - lm[c]['x']) * w
158
+ by = (lm[n]['y'] - lm[c]['y']) * h
159
  jaw_dev += abs(ax * by - ay * bx) / (face_ref ** 2)
160
  jaw_dev /= len(jaw_pts)
161
  jawline = round(max(1.0, min(9.9, 10 - jaw_dev * 0.8 - (age_mid - 18) * 0.09)), 1)