Evan Li commited on
Commit
dcbc516
Β·
1 Parent(s): b670fec

age and freckles/wrinkles

Browse files
analyzers/beauty_analyzer.py CHANGED
@@ -91,11 +91,22 @@ class BeautyAnalyzer:
91
 
92
  weights_path = self._resolve_weights_path()
93
  if weights_path is None:
 
 
 
 
 
 
 
 
 
 
94
  print(
95
- "[BeautyAnalyzer] No trained weights found at "
96
- f"{LOCAL_WEIGHTS_PATH} and BEAUTY_HF_REPO_ID is unset. "
97
  "Train one via `training/beauty/train.py` and drop the "
98
- ".pt into face-service/models/, or set BEAUTY_HF_REPO_ID."
 
99
  )
100
  return
101
 
 
91
 
92
  weights_path = self._resolve_weights_path()
93
  if weights_path is None:
94
+ local_status = (
95
+ f"found at {LOCAL_WEIGHTS_PATH}"
96
+ if os.path.exists(LOCAL_WEIGHTS_PATH)
97
+ else f"not found at {LOCAL_WEIGHTS_PATH}"
98
+ )
99
+ hub_status = (
100
+ f"BEAUTY_HF_REPO_ID={HF_REPO_ID} (download failed, see prior log line)"
101
+ if HF_REPO_ID
102
+ else "BEAUTY_HF_REPO_ID is unset"
103
+ )
104
  print(
105
+ "[BeautyAnalyzer] No usable weights β€” "
106
+ f"local: {local_status}; hub: {hub_status}. "
107
  "Train one via `training/beauty/train.py` and drop the "
108
+ ".pt into face-service/models/, or set BEAUTY_HF_REPO_ID "
109
+ "to a public HF model repo containing the .pt."
110
  )
111
  return
112
 
analyzers/insightface_analyzer.py CHANGED
@@ -57,6 +57,13 @@ except ImportError:
57
 
58
  MODEL_NAME = "buffalo_l"
59
 
 
 
 
 
 
 
 
60
  # Age buckets used by the legacy UI. We derive these from the regression
61
  # output so existing screens keep working.
62
  AGE_BUCKETS = [
@@ -121,8 +128,13 @@ class InsightFaceAnalyzer:
121
  else None
122
  )
123
 
124
- # Age head is a single float (years). Round for display.
125
- age = float(getattr(face, "age", 0.0))
 
 
 
 
 
126
 
127
  # Gender is exposed as 0 (female) / 1 (male) on Face objects.
128
  # InsightFace doesn't surface a softmax probability β€” we report
 
57
 
58
  MODEL_NAME = "buffalo_l"
59
 
60
+ # InsightFace's genderage head is known to overshoot adult ages by
61
+ # roughly 5 years in informal testing (no published calibration). We
62
+ # subtract a fixed offset to undo this bias; clamp to β‰₯1 so we never
63
+ # emit negative ages for kids. Override at runtime via the
64
+ # AGE_OFFSET_YEARS env var if you want to tune for your dataset.
65
+ AGE_OFFSET_YEARS = float(os.environ.get("AGE_OFFSET_YEARS", "5"))
66
+
67
  # Age buckets used by the legacy UI. We derive these from the regression
68
  # output so existing screens keep working.
69
  AGE_BUCKETS = [
 
128
  else None
129
  )
130
 
131
+ # Age head is a single float (years). Buffalo_L systematically
132
+ # over-predicts adults by ~5 years; subtract AGE_OFFSET_YEARS
133
+ # to recalibrate. Don't drop below 1 (negative ages would be
134
+ # absurd, and very young children are already on the noisy
135
+ # end of the model's training distribution).
136
+ raw_age = float(getattr(face, "age", 0.0))
137
+ age = max(1.0, raw_age - AGE_OFFSET_YEARS)
138
 
139
  # Gender is exposed as 0 (female) / 1 (male) on Face objects.
140
  # InsightFace doesn't surface a softmax probability β€” we report
analyzers/parsing_analyzer.py CHANGED
@@ -167,23 +167,46 @@ class ParsingAnalyzer:
167
  result["hat_detected"] = region_coverage.get("hat", 0) > 0.01
168
 
169
  # ── Skin texture / wrinkles / freckles ───────────────────────
170
- # Only worth computing if the face mask actually has substance.
171
- # Under ~100 pixels we don't have enough signal.
172
- if skin_mask.sum() > 100:
173
- # Wrinkles β†’ high-frequency edge energy on the face mask.
174
- # Laplacian responds to local intensity curvature; std/mean
175
- # over the masked region gives a "how much fine detail" score.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  skin_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
177
  laplacian = cv2.Laplacian(skin_gray, cv2.CV_64F)
178
  skin_edges = np.abs(laplacian)
179
- skin_edges[~skin_mask] = 0 # zero out non-face pixels
180
- edge_density = skin_edges.sum() / skin_mask.sum() if skin_mask.sum() else 0
 
 
181
 
182
- if edge_density > 15:
 
 
 
183
  result["wrinkle_level"] = "prominent"
184
- elif edge_density > 8:
185
  result["wrinkle_level"] = "moderate"
186
- elif edge_density > 4:
187
  result["wrinkle_level"] = "slight"
188
  else:
189
  result["wrinkle_level"] = "smooth"
@@ -193,23 +216,30 @@ class ParsingAnalyzer:
193
  # Freckles/moles β†’ count pixels well below mean L* lightness.
194
  # Working in LAB rather than RGB makes the threshold tone-
195
  # independent (a freckle is "darker than surrounding skin"
196
- # regardless of base skin tone).
 
197
  skin_lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2LAB)
198
  l_channel = skin_lab[:, :, 0].astype(float)
199
- l_channel[~skin_mask] = np.nan
200
  mean_l = np.nanmean(l_channel)
201
- dark_spots = (l_channel < mean_l - 25) & skin_mask
202
- spot_ratio = dark_spots.sum() / skin_mask.sum() if skin_mask.sum() else 0
 
 
 
 
 
 
203
  result["freckles_or_moles"] = (
204
- "many" if spot_ratio > 0.05
205
  else "some" if spot_ratio > 0.015
206
  else "few" if spot_ratio > 0.005
207
  else "none"
208
  )
209
 
210
- # Uniformity = std-dev of L* over the face. Higher = more
211
  # variation (uneven skin tone, shadows, scarring).
212
- skin_l_values = l_channel[skin_mask]
213
  result["skin_uniformity"] = round(float(np.nanstd(skin_l_values)), 2)
214
  else:
215
  result["wrinkle_level"] = "unknown"
 
167
  result["hat_detected"] = region_coverage.get("hat", 0) > 0.01
168
 
169
  # ── Skin texture / wrinkles / freckles ───────────────────────
170
+ # IMPORTANT: SegFormer's "face" class covers the whole face
171
+ # region INCLUDING eyes, eyebrows, lips and nostrils. Those
172
+ # features are naturally darker than skin and have strong
173
+ # edges, which used to inflate every metric:
174
+ #
175
+ # β€’ the Laplacian-on-mask wrinkle score was really measuring
176
+ # eyebrow + eyelash edges, so almost every photo came back as
177
+ # "prominent" wrinkles;
178
+ # β€’ the LAB dark-spot freckle count was finding the eyebrows
179
+ # and pupils as "spots", so almost every photo came back as
180
+ # "many".
181
+ #
182
+ # Mitigation in this pass: erode the face mask substantially
183
+ # (β‰ˆ8 px) so we sample only the deep interior β€” cheeks,
184
+ # forehead, chin β€” and bump the thresholds. Not perfect; a
185
+ # proper fix wires MediaPipe landmarks in to mask out
186
+ # eyes/brows/lips explicitly.
187
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
188
+ interior_mask = cv2.erode(
189
+ skin_mask.astype(np.uint8), kernel, iterations=4
190
+ ).astype(bool)
191
+
192
+ if interior_mask.sum() > 100:
193
+ # Wrinkles β†’ Laplacian edge density over the interior mask.
194
  skin_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
195
  laplacian = cv2.Laplacian(skin_gray, cv2.CV_64F)
196
  skin_edges = np.abs(laplacian)
197
+ skin_edges[~interior_mask] = 0
198
+ edge_density = (
199
+ skin_edges.sum() / interior_mask.sum() if interior_mask.sum() else 0
200
+ )
201
 
202
+ # Thresholds rebumped for the interior-only mask: it
203
+ # naturally has much less edge energy than the un-eroded
204
+ # version, so the bands shifted down. Tune on real photos.
205
+ if edge_density > 22:
206
  result["wrinkle_level"] = "prominent"
207
+ elif edge_density > 14:
208
  result["wrinkle_level"] = "moderate"
209
+ elif edge_density > 8:
210
  result["wrinkle_level"] = "slight"
211
  else:
212
  result["wrinkle_level"] = "smooth"
 
216
  # Freckles/moles β†’ count pixels well below mean L* lightness.
217
  # Working in LAB rather than RGB makes the threshold tone-
218
  # independent (a freckle is "darker than surrounding skin"
219
+ # regardless of base skin tone). Restrict to interior_mask
220
+ # so eyes/brows/lips don't get counted as dark spots.
221
  skin_lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2LAB)
222
  l_channel = skin_lab[:, :, 0].astype(float)
223
+ l_channel[~interior_mask] = np.nan
224
  mean_l = np.nanmean(l_channel)
225
+ # Stricter dark-spot threshold (was βˆ’25 L*). Real freckles
226
+ # and moles are typically 30+ L* below cheek brightness;
227
+ # smaller deltas were picking up shadows and pores.
228
+ dark_spots = (l_channel < mean_l - 32) & interior_mask
229
+ spot_ratio = (
230
+ dark_spots.sum() / interior_mask.sum() if interior_mask.sum() else 0
231
+ )
232
+ # Bands also tightened β€” "many" is genuinely lots of spots.
233
  result["freckles_or_moles"] = (
234
+ "many" if spot_ratio > 0.04
235
  else "some" if spot_ratio > 0.015
236
  else "few" if spot_ratio > 0.005
237
  else "none"
238
  )
239
 
240
+ # Uniformity = std-dev of L* over the interior. Higher = more
241
  # variation (uneven skin tone, shadows, scarring).
242
+ skin_l_values = l_channel[interior_mask]
243
  result["skin_uniformity"] = round(float(np.nanstd(skin_l_values)), 2)
244
  else:
245
  result["wrinkle_level"] = "unknown"