GlowCheck is a research/demo tool for face and skin metrics. It analyzes a single facial photo and reports age, gender and emotion estimates (DeepFace/TensorFlow) together with objective skin measurements — ITA° skin tone with an approximate Fitzpatrick category, relative erythema (redness), and a geometric facial-asymmetry index. The same Python pipeline runs two ways: as a local CLI (detectarpiel.py, single image or whole folder with CSV export) and as a Firebase Cloud Function behind a mobile-first web frontend. It is aimed at developers and researchers exploring computer-vision skin/face metrics — it is not a medical device and its outputs are estimates, not diagnoses.
- Demographics (DeepFace): age, gender (with confidence), dominant emotion and race estimates, using the RetinaFace detector by default.
- Skin tone (ITA°): computed on segmented skin only (forehead + both cheeks, YCrCb skin mask), with median/IQR, per-region values, an approximate Fitzpatrick category (I–VI), and an honest low-confidence flag when lighting is uneven.
- Erythema: relative redness as median CIELAB a* on the same segmented skin, per region.
- Facial asymmetry: coarse geometric index from RetinaFace's 5 landmarks (nasal deviation, mouth offset, mouth cant), normalized by interocular distance, roll-invariant, with a frontality check that flags head-pose contamination.
- Robust preprocessing: EXIF-aware loading; CLAHE lighting normalization is applied in memory for the detection models only, while color metrics always use the original pixels.
- Batch mode + CSV: point the CLI at a folder to process every image with per-image error isolation and a consolidated
skin_report.csv. - Web app:
public/index.html(Spanish UI, mobile-first) sends the photo as base64 to the Cloud Function at/api/detect-ageand renders the results.glowcheck.htmlis the standalone static UI mockup.
- Estimates, not facts. Age, gender, emotion and race outputs are model guesses with real error rates; treat them as approximate signals only. Inferring attributes like gender, race or emotion from a face is inherently sensitive — do not use this project for decisions about people (hiring, access, profiling, moderation, etc.).
- Not medical. ITA/Fitzpatrick and erythema values are relative, uncalibrated screen-side metrics — not clinical instruments and not a substitute for dermatological assessment. Erythema (a*) is less reliable on darker skin; skin-tone readings need diffuse lighting (ideally a grey/color card) to be trustworthy, and the pipeline flags low-confidence cases.
- Asymmetry is coarse. The 5-landmark index conflates head pose with true asymmetry on non-frontal faces (flagged via the frontality score); a dense-landmark model (e.g. MediaPipe 468 points) would be the upgrade.
- No server-side image storage by design. The Cloud Function decodes the base64 image in memory, analyzes it, and returns JSON — it never writes the photo to storage or a database. The local CLI likewise processes images in place (debug overlays are written only when you pass
--debug). Get informed consent before analyzing anyone else's photo.
- Python 3.10 — analysis pipeline
- DeepFace 0.0.96 / TensorFlow 2.20 — age, gender, emotion, race models
- RetinaFace — face detection + 5-point landmarks
- OpenCV + NumPy + Pillow — skin segmentation (YCrCb), CIELAB/ITA math, CLAHE, EXIF handling
- Firebase — Cloud Functions (Python runtime) + Hosting, with a
/api/detect-agerewrite - Vanilla HTML/CSS/JS — mobile-first frontend, no framework
detectarpiel.py # Local CLI pipeline (single image or folder batch -> CSV)
imagen1.jpg # Sample input image (AI-generated face)
skin_report.csv # Sample CSV output for imagen1.jpg
skin_debug/ # Sample --debug overlay (face box, ROIs, skin mask)
functions/ # Firebase Cloud Function (main.py = HTTP adaptation of the pipeline)
public/ # Deployed web frontend (calls /api/detect-age)
glowcheck.html # Standalone static UI mockup
firebase.json # Hosting + functions config (python312, /api/detect-age rewrite)
python -m venv .venv
.venv\Scripts\activate # Windows (Linux/macOS: source .venv/bin/activate)
pip install -r requirements.txt
# Analyze the bundled sample image (writes a debug overlay to ./skin_debug/)
python detectarpiel.py imagen1.jpg --debug
# Analyze a whole folder -> <folder>/skin_report.csv
python detectarpiel.py path/to/folder
# Optional: choose a detector backend and JSON/CSV output
python detectarpiel.py imagen1.jpg retinaface --json --csvNotes:
- Python 3.10 is recommended (matches the Cloud Function runtime and the pinned TensorFlow build).
- On first run DeepFace downloads its model weights (a few hundred MB) to
~/.deepface/— the first analysis is slow, later runs are fast. - Facial asymmetry needs the default
retinafacebackend and a roughly frontal face.
npm install -g firebase-tools
firebase login
# Bind this folder to YOUR Firebase project (creates .firebaserc, see Configuration)
firebase use --add
# Deploy the Python Cloud Function + hosting
firebase deployAfter deploy, the hosted site (public/index.html) posts photos to /api/detect-age, which firebase.json rewrites to the detect_age function (2 GB memory, 60 s timeout, us-central1). You can also deploy pieces separately with firebase deploy --only functions or --only hosting.
Running python detectarpiel.py imagen1.jpg --debug on the bundled sample (an AI-generated face) produces:
- Console report per face: age/gender/emotion estimates, ITA° with IQR and per-region values, Fitzpatrick approximation, erythema a*, and the asymmetry breakdown with confidence flags.
skin_report.csv— the flat CSV row forimagen1.jpg(age 39, tone "tan" / Fitzpatrick ~IV, ITA 21.1°, flagged low-confidence due to uneven lighting).skin_debug/imagen1_skin.png— debug overlay showing the face box (blue), the forehead/cheek ROIs (yellow) and the accepted skin pixels (green).
The local CLI needs no configuration at all beyond pip install -r requirements.txt.
The deployed Cloud Function reads two optional environment variables — both are access controls, not secrets-to-obtain-from-a-vendor:
| Variable | What it is | How to set it |
|---|---|---|
APP_TOKEN |
Shared secret a caller must send in the X-App-Token header. If unset, the function accepts unauthenticated calls and logs a warning — fine locally, not for a public deployment. |
Pick a long random string; set it under Cloud Run → your function → Variables, or firebase functions:secrets:set. Put the same value in public/index.html (APP_TOKEN). |
ALLOWED_ORIGINS |
Comma-separated CORS allowlist. Defaults to localhost dev ports — never *. |
Set it to your site origin, e.g. https://your-project.web.app. |
Because public/index.html is served to the browser, the token in it is not
a secret — it raises the cost of casual abuse, and App Check (below) is the real
control. The only value you must supply to deploy is your Firebase project:
| Value | What it is | Where to get it |
|---|---|---|
| Firebase project ID | The project hosting the function + site. Stored in .firebaserc (gitignored here). |
Create a project at console.firebase.google.com, then run firebase use --add in the repo root. |
Additional one-time setup:
- Enable billing (Blaze plan). Python Cloud Functions with 2 GB memory require it.
firebase use --add— recreates.firebasercbound to your project (this repo intentionally does not ship one).firebase deploy— deploysfunctions/(runtimepython312) andpublic/.
This function loads TensorFlow into 2 GB of memory on every cold start. An
unauthenticated, uncapped version of it is a standing invitation to run up
someone else's Cloud bill, so the following limits are enforced in code
(functions/main.py) and ship enabled by default:
| Control | Value | Why it bounds spend |
|---|---|---|
max_instances |
3 |
The hard ceiling. Sustained abuse can occupy at most 3 instances instead of fanning out to Cloud Run's default limit. |
concurrency |
1 |
One 2 GB analysis per instance; the gen2 default of 80 would OOM and multiply cost. |
min_instances |
0 |
Never pay for idle warm instances. |
timeout_sec |
60 |
A warm analysis takes seconds; a hung request cannot burn 120 s. |
| Payload cap | 5 MB base64 | Checked against Content-Length before the body is parsed, and again on the field before base64-decoding or touching a model. |
| Pixel budget | 40 MP | A byte cap is not a memory cap — a ~20 KB PNG can declare 30000×30000 and expand to gigabytes. Dimensions are read from the image header and rejected (413) before any pixels are materialised; PIL's own DecompressionBombError guard backs it up. |
APP_TOKEN gate |
401 | Rejected before any model work happens. |
| Rate limit | 5 burst, 1 per 6 s per IP | Per-instance token bucket → 429, applied before the token check so token-guessing is throttled too. Keyed on the load balancer's appended peer, never the caller-supplied leftmost X-Forwarded-For (which an attacker can rotate per request). Trims scripted abuse only — the state is per-instance and dies on cold start. |
| CORS | allowlist | Never *. Note this is a browser control — it does nothing against curl; App Check below is the server-side one. |
| App Check | enforced in code | enforce_app_check=True on the function (override with ENFORCE_APP_CHECK=0 for local testing). The platform rejects callers without a valid attestation from your registered app before any billed work runs — the strongest control here, and it ships on by default. |
Worst-case concurrent billed execution is therefore max_instances × concurrency
= 3, each capped at 60 s.
Do these two things before you expose a real deployment:
- Set a budget alert — console.cloud.google.com/billing → your billing account → Budgets & alerts → Create budget. Scope it to this project and set email alerts at 50 / 90 / 100 %. A budget alert notifies; it does not cap spend, which is exactly why
max_instancesis set in code. To hard-stop, add a Pub/Sub budget notification that disables billing. - Register the app for Firebase App Check — enforcement is already ON in code, so you must complete the registration or your own site will be rejected too: console.firebase.google.com → App Check → register the web app with reCAPTCHA Enterprise, and initialise App Check in
public/index.html. For local testing without it, setENFORCE_APP_CHECK=0.
The computer-vision / ML project in this portfolio: one Python pipeline, shipped two ways.
| Languages | Python 3.12, JavaScript, HTML/CSS |
| ML / CV | DeepFace + TensorFlow (age, gender, emotion), RetinaFace detection and 5-point landmarks, OpenCV and NumPy for all pixel work |
| Original algorithms | Skin segmentation in YCrCb, ITA skin-tone with median/IQR and Fitzpatrick mapping, relative erythema as CIELAB a-star, and a roll-invariant facial-asymmetry index normalised by interocular distance |
| Signal honesty | CLAHE normalisation is applied to the detector's copy only, never to the pixels used for colour metrics; low-confidence cases (uneven lighting, non-frontal pose) are flagged rather than silently reported |
| Deployment | The same pipeline runs as a local CLI (single image or batch folder to CSV) and as a Firebase Cloud Function behind a mobile-first web frontend |
| Production hardening | Instance caps, a pixel budget that rejects decompression bombs from the image header before decoding, request-size limits, token gate, rate limiting keyed on a non-spoofable peer, and App Check enforced in code |
Skills demonstrated: computer vision, applied ML, colour-science maths, serverless deployment, cost and abuse modelling for expensive endpoints, and writing honestly about model limitations.
Read the Ethics and limitations section first — it is the important one. In addition:
- Faces are sensitive personal data. In many jurisdictions facial analysis is regulated biometric processing (Chile's Ley 19.628; the GDPR treats biometric identification as a special category). Get informed, documented consent before analysing anyone's photo, and do not deploy this publicly without a privacy notice and a lawful basis.
- Never use it to make decisions about people. Age, gender, emotion and especially race outputs are error-prone model guesses with known demographic bias. Using them for hiring, access control, pricing, profiling, moderation, or law enforcement is out of scope and actively discouraged.
- Not a medical device. Skin-tone, erythema and asymmetry values are uncalibrated, screen-side metrics. They diagnose nothing and are not a substitute for a dermatologist.
- Deploying it costs money. The function loads TensorFlow into 2 GB of memory on a pay-as-you-go plan. The controls in Cost controls ship enabled, but the bill is yours — set a budget alert and complete App Check registration before exposing a public URL.
- Not affiliated with Google, Firebase, or the DeepFace / RetinaFace projects; those are used under their own licences.
- No warranty. Provided "as is", without warranty of any kind (see LICENSE).
MIT — see LICENSE. Copyright (c) 2026 Diego Ostertag.