gyf-gpu / skintone /classify.py
GetYourFit's picture
Upload folder using huggingface_hub
a1fdb98 verified
Raw
History Blame Contribute Delete
2.63 kB
"""Map a CIELAB skin readout to a Monk Skin Tone bucket + undertone (pure).
Deliberately explainable (no black-box CNN): tone comes from ``L*`` against
per-bucket anchors, undertone from the ``a*``/``b*`` balance. Explainability is a
fairness feature β€” we can audit *why* a tone was assigned, and the anchors are a
single, reviewable table rather than opaque weights.
⚠ The ``L*`` anchors below are **provisional**, pending calibration against a
balanced full-MST image set (``fairness_eval.py``). Until that gate passes, the
module runs in shadow (computed, not surfaced) β€” see
docs/plans/p1b-cycle3-photo-skin-tone.md.
"""
from __future__ import annotations
from gyf_contracts.usermodel import UNKNOWN_SKIN_TONE, UNKNOWN_UNDERTONE
# Provisional L* anchor per MST bucket (lightest mst1 β†’ deepest mst10). Roughly
# even perceptual spacing across the lightness range observed for facial skin.
_MST_ANCHORS: list[tuple[str, float]] = [
("mst1", 90.0),
("mst2", 82.0),
("mst3", 74.0),
("mst4", 66.0),
("mst5", 58.0),
("mst6", 50.0),
("mst7", 42.0),
("mst8", 34.0),
("mst9", 27.0),
("mst10", 20.0),
]
# Half the spacing between adjacent anchors (~4 L*): a reading this far from its
# nearest anchor sits exactly between two buckets β†’ confidence 0.5.
_ANCHOR_HALF_STEP = 4.0
# Below this, abstain to "unknown" rather than present a guessed tone (D6 honesty).
MIN_TONE_CONFIDENCE = 0.45
def lab_to_mst(L: float, a: float, b: float) -> tuple[str, float]:
"""Nearest MST bucket by ``L*`` + a distance-based confidence in [0, 1]."""
bucket, anchor_L = min(_MST_ANCHORS, key=lambda kv: abs(kv[1] - L))
distance = abs(anchor_L - L)
confidence = max(0.0, 1.0 - distance / (2 * _ANCHOR_HALF_STEP))
if confidence < MIN_TONE_CONFIDENCE:
return UNKNOWN_SKIN_TONE, confidence
return bucket, confidence
def lab_to_undertone(a: float, b: float) -> tuple[str, float]:
"""Warm / cool / neutral / olive from the ``a*``/``b*`` balance.
Warm skin skews yellow (high ``b*``), cool skews pink/blue (``b*`` low
relative to ``a*``), olive carries a green-yellow cast (``b*`` high, ``a*``
low). Confidence grows with the margin between the leading signal and neutral.
"""
# Neutral band: small chroma in both axes.
if abs(b) < 6.0 and abs(a) < 6.0:
return "neutral", 0.6
if b >= 12.0 and a < 8.0:
return "olive", min(1.0, (b - 12.0) / 12.0 + 0.5)
if b > a:
return "warm", min(1.0, (b - a) / 20.0 + 0.5)
if a >= b:
return "cool", min(1.0, (a - b) / 20.0 + 0.5)
return UNKNOWN_UNDERTONE, 0.4