Spaces:
Sleeping
Sleeping
Evan Li commited on
Commit Β·
dcbc516
1
Parent(s): b670fec
age and freckles/wrinkles
Browse files- analyzers/beauty_analyzer.py +14 -3
- analyzers/insightface_analyzer.py +14 -2
- analyzers/parsing_analyzer.py +48 -18
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
|
| 96 |
-
f"{
|
| 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).
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 171 |
-
#
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[~
|
| 180 |
-
edge_density =
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
result["wrinkle_level"] = "prominent"
|
| 184 |
-
elif edge_density >
|
| 185 |
result["wrinkle_level"] = "moderate"
|
| 186 |
-
elif edge_density >
|
| 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[~
|
| 200 |
mean_l = np.nanmean(l_channel)
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
result["freckles_or_moles"] = (
|
| 204 |
-
"many" if spot_ratio > 0.
|
| 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
|
| 211 |
# variation (uneven skin tone, shadows, scarring).
|
| 212 |
-
skin_l_values = l_channel[
|
| 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"
|