{"id":46623836,"url":"https://github.com/ennsss/skinage","last_synced_at":"2026-03-07T22:01:00.720Z","repository":{"id":342832391,"uuid":"1175340425","full_name":"Ennsss/SkinAge","owner":"Ennsss","description":"AI-powered facial skin quality analysis — 7-zone scoring, concern heatmaps, biological age estimation. Upload a selfie, get instant results.","archived":false,"fork":false,"pushed_at":"2026-03-07T21:07:12.000Z","size":243,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-07T21:45:41.367Z","etag":null,"topics":["computer-vision","deep-learning","facial-analysis","fastapi","heatmap","machine-learning","python","pytorch","skin-analysis","streamlit"],"latest_commit_sha":null,"homepage":"https://ennsss-skinage.streamlit.app","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Ennsss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-07T15:22:57.000Z","updated_at":"2026-03-07T21:07:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Ennsss/SkinAge","commit_stats":null,"previous_names":["ennsss/skinage"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Ennsss/SkinAge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ennsss%2FSkinAge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ennsss%2FSkinAge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ennsss%2FSkinAge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ennsss%2FSkinAge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Ennsss","download_url":"https://codeload.github.com/Ennsss/SkinAge/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Ennsss%2FSkinAge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30233429,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T19:01:10.287Z","status":"ssl_error","status_checked_at":"2026-03-07T18:59:58.103Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["computer-vision","deep-learning","facial-analysis","fastapi","heatmap","machine-learning","python","pytorch","skin-analysis","streamlit"],"created_at":"2026-03-07T22:01:00.119Z","updated_at":"2026-03-07T22:01:00.709Z","avatar_url":"https://github.com/Ennsss.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# 🔬 SkinAge\n\n**AI-powered facial skin quality analysis**\n\n*Upload a selfie. Get instant 7-zone scoring, concern heatmaps, and biological age estimation.*\n\n[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge\u0026logo=python\u0026logoColor=white)](https://python.org)\n[![PyTorch](https://img.shields.io/badge/PyTorch-2.0+-EE4C2C?style=for-the-badge\u0026logo=pytorch\u0026logoColor=white)](https://pytorch.org)\n[![Streamlit](https://img.shields.io/badge/Streamlit-Dashboard-FF4B4B?style=for-the-badge\u0026logo=streamlit\u0026logoColor=white)](https://streamlit.io)\n[![FastAPI](https://img.shields.io/badge/FastAPI-Serving-009688?style=for-the-badge\u0026logo=fastapi\u0026logoColor=white)](https://fastapi.tiangolo.com)\n[![License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE)\n\n[Live Demo](https://ennsss-skinage.streamlit.app) · [Documentation](#how-it-works) · [API Reference](#api-reference)\n\n---\n\n\u003c/div\u003e\n\n**An end-to-end ML system that analyzes facial photographs to produce per-region skin quality scores, concern heatmaps, and estimated biological \"skin age\" — all from a single phone camera selfie.**\n\nThe system downloads public face datasets, generates pseudo-labels using classical computer vision (Canny edges, Laplacian variance, CIELAB color analysis), and trains a multi-task EfficientNet-B2 with a U-Net decoder, quality head, and age head. It ships with a FastAPI serving layer and a 5-page Streamlit dashboard featuring zone overlays, heatmap exploration, and before/after comparison.\n\n---\n\n## How It Works\n\n```\nDownload 3 datasets       Align \u0026 extract zones      Generate pseudo-labels\n UTKFace (20K)     --\u003e     MediaPipe 468-point  --\u003e   Wrinkle (Canny edges)\n FFHQ (10K)                face mesh, affine          Pigmentation (L* std)\n CelebA (20K)              warp to 512x512            Redness (a* mean)\n                                                      Pore texture (Laplacian)\n\n        |                        |                          |\n        v                        v                          v\n\n Quality gating            7 facial zones              4-channel heatmaps\n blur, angle, bright-      forehead, under-eyes,  --\u003e  pixel-level concern\n ness, occlusion check     cheeks, nose, chin,         maps at 512x512\n                           crow's feet, nasolabial\n\n        |                        |                          |\n        v                        v                          v\n\n Stratified splits         28 quality scores           Multi-task training\n 70/15/15 by age           (7 zones x 4 concerns) --\u003e  EfficientNet-B2 backbone\n decade + ethnicity        normalized 0-100            + 3 heads, two-phase\n```\n\n---\n\n## Target Metrics\n\nThe model is evaluated against these thresholds after training on pseudo-labeled data:\n\n### Quality \u0026 Heatmap Performance\n\n| Metric | Target | What It Measures |\n|--------|--------|------------------|\n| Per-zone Quality MAE | ≤ 8 points | Average error on 0-100 quality scores per zone |\n| Quality Pearson r | ≥ 0.80 | Correlation between predicted and pseudo-label scores |\n| Heatmap SSIM | ≥ 0.70 | Structural similarity of predicted vs pseudo-label heatmaps |\n\n### Age Estimation\n\n| Metric | Target | What It Measures |\n|--------|--------|------------------|\n| Overall Age MAE | ≤ 5.0 years | Mean absolute error on UTKFace test set |\n| Age MAE (20-50) | ≤ 4.0 years | Tighter target for the core demographic |\n\n### Fairness Guarantees\n\n| Metric | Target | What It Measures |\n|--------|--------|------------------|\n| Score Gap | ≤ 6 points | Max quality score difference between any two ethnic groups |\n| Age MAE Gap | ≤ 1.5 years | Max age prediction error difference between groups |\n| Redness Calibration | Per Fitzpatrick | Redness scoring adjusted for skin tone |\n\n---\n\n## Architecture\n\n```\n                         Input (B, 3, 512, 512)\n                                  |\n                    +---------------------------+\n                    |   EfficientNet-B2 Backbone |\n                    |   (timm, features_only)    |\n                    +---------------------------+\n                         |                |\n                    skip features     GAP pooled\n                    [16,24,48,         (B, 1408)\n                     120,352]              |\n                         |           +-----+-----+\n                         v           |           |\n                  +-----------+  +--------+  +--------+\n                  | U-Net     |  |Quality |  | Age    |\n                  | Decoder   |  | Head   |  | Head   |\n                  | 4 blocks  |  |FC-\u003e512 |  |FC-\u003e256 |\n                  | + skips   |  |-\u003e28 sig|  |-\u003e1 ReLU|\n                  +-----------+  +--------+  +--------+\n                       |              |           |\n                       v              v           v\n                  Heatmaps       Quality       Age\n                (B,4,512,512)    (B,28)       (B,1)\n                 [0,1] per       [0,1] x100   years\n                 concern         = 0-100\n```\n\n### Multi-Task Loss\n\n```\nL_total = 1.0 * L_heatmap(MSE) + 2.0 * L_quality(SmoothL1) + 1.5 * L_age(SmoothL1)\n```\n\nQuality is weighted highest — accurate zone scores are the core product. Age loss is only computed on UTKFace samples (mixed-label batches via `age_indices` tensor).\n\n### Two-Phase Training\n\n| Phase | Backbone | LR | Epochs | Purpose |\n|-------|----------|-----|--------|---------|\n| 1 — Warm-up | Frozen | 1e-3 | 3 | Train heads without corrupting pretrained features |\n| 2 — Fine-tune | Unfrozen | 5e-5 -\u003e 1e-6 | Up to 30 | End-to-end with cosine annealing + early stopping (patience 7) |\n\nBatchNorm in the frozen backbone stays in eval mode via a custom `train()` override — prevents running stats corruption.\n\n---\n\n## Data Sources\n\n| Source | What It Provides | Images | Coverage |\n|--------|-----------------|--------|----------|\n| [UTKFace](https://susanqq.github.io/UTKFace/) | Aligned faces with age, gender, ethnicity labels | 20K | Ages 0-116, 5 ethnic groups |\n| [FFHQ](https://github.com/NVlabs/ffhq-dataset) | High-quality 1024x1024 faces (no age labels) | 10K subset | Diverse demographics |\n| [CelebA](https://mmlab.ie.cuhk.edu.hk/projects/CelebA.html) | Celebrity faces with attribute annotations | 20K subset | 40 binary attributes |\n\nAll images are aligned to 512x512 using MediaPipe face detection + affine transformation (horizontal eye-line, 180px inter-eye distance).\n\n---\n\n## Pseudo-Label Pipeline\n\nSince no ground-truth cosmetic quality dataset exists, we generate training labels using classical computer vision:\n\n| Concern | Method | Signal |\n|---------|--------|--------|\n| **Wrinkle** | Canny edge density per zone | Edge pixels / total pixels after morphological filtering |\n| **Pigmentation** | L* channel std deviation | CIELAB lightness variation within zone |\n| **Redness** | a* channel mean | CIELAB red-green axis intensity |\n| **Pore/Texture** | Laplacian variance + Gabor energy | High-frequency texture roughness |\n\nScores are normalized to 0-100 using dataset-wide percentile mapping with age adjustment. Pixel-level heatmaps (Canny response, local L* std, local a*, local Laplacian variance) provide spatial supervision for the U-Net decoder.\n\n---\n\n## Facial Zones \u0026 Concerns\n\n### 7 Facial Zones\n\n| Zone | Weight | Concerns Assessed | Why It Matters |\n|------|--------|-------------------|----------------|\n| Forehead | 1.0 | Wrinkle, pigmentation | Horizontal expression lines, age-related laxity |\n| Under-eyes | 1.2 | Wrinkle, pigmentation, pore | Earliest zone to show intrinsic aging |\n| Cheeks | 1.5 | All 4 concerns | Largest surface area, pore visibility, redness |\n| Nose | 0.8 | Redness, pore | Sebaceous activity, pore texture |\n| Chin | 0.7 | Wrinkle, pigmentation | Volume loss, jowl formation |\n| Crow's feet | 1.0 | Wrinkle | Primary chronological age indicator |\n| Nasolabial | 1.0 | Wrinkle, redness | Fold depth strongly correlates with perceived age |\n\nCheeks carry the highest weight (1.5) — they represent the largest visible skin surface and are assessed across all four concern types.\n\n### 4 Concern Types (Heatmap Channels)\n\n| Channel | Name | Range | Severity Labels |\n|---------|------|-------|-----------------|\n| 0 | Wrinkle | 0.0 - 1.0 | Minimal -\u003e Mild -\u003e Moderate -\u003e Significant |\n| 1 | Pigmentation | 0.0 - 1.0 | Minimal -\u003e Mild -\u003e Moderate -\u003e Significant |\n| 2 | Redness | 0.0 - 1.0 | Minimal -\u003e Mild -\u003e Moderate -\u003e Significant |\n| 3 | Pore/Texture | 0.0 - 1.0 | Minimal -\u003e Mild -\u003e Moderate -\u003e Significant |\n\n---\n\n## Project Structure\n\n```\nSkinAge/\n├── config/\n│   ├── model_config.yaml          # Architecture, loss weights, training schedule\n│   ├── data_config.yaml           # Dataset paths, pseudo-label params, augmentation\n│   ├── zones_config.yaml          # 7 zones, landmarks, weights, score labels\n│   └── api_config.yaml            # Server settings, quality thresholds, inference\n├── src/\n│   ├── data/\n│   │   ├── download.py            # Dataset downloaders with resume support\n│   │   ├── face_alignment.py      # MediaPipe detection + affine alignment\n│   │   ├── lighting.py            # CLAHE + gray-world white balance\n│   │   ├── zone_extraction.py     # 7 zones from 468 landmarks, polygon masks\n│   │   ├── pseudo_labels.py       # Classical CV feature extraction + heatmaps\n│   │   ├── quality_gate.py        # 6 quality checks with actionable messages\n│   │   ├── dataset.py             # PyTorch Dataset, mixed-label collate\n│   │   ├── augmentation.py        # Albumentations (no color jitter — skin tone is signal)\n│   │   └── splits.py              # Stratified splits by age decade + ethnicity\n│   ├── models/\n│   │   ├── backbone.py            # EfficientNet-B2 encoder, BN freeze override\n│   │   ├── unet_decoder.py        # 4-block decoder with skip connections\n│   │   ├── quality_head.py        # FC -\u003e 28 sigmoid outputs\n│   │   ├── age_head.py            # FC -\u003e 1 ReLU output\n│   │   ├── skinage_model.py       # Full assembly, from_config(), checkpoints\n│   │   ├── losses.py              # MultiTaskLoss with mixed-label support\n│   │   └── trainer.py             # Two-phase training, mixed precision, early stopping\n│   ├── evaluation/\n│   │   ├── metrics.py             # MAE, Pearson, SSIM, age metrics\n│   │   ├── fairness.py            # Group gaps, Fitzpatrick redness calibration\n│   │   └── visualize.py           # Score distributions, correlation matrices\n│   ├── api/\n│   │   ├── schemas.py             # Pydantic v2 request/response models\n│   │   ├── inference.py           # Preprocess -\u003e predict -\u003e postprocess pipeline\n│   │   ├── routes.py              # /analyze, /compare, /health endpoints\n│   │   └── app.py                 # FastAPI factory with lifespan model loading\n│   ├── dashboard/\n│   │   ├── app.py                 # Multi-page Streamlit app\n│   │   └── pages/\n│   │       ├── live_demo.py       # Upload selfie, gauge chart, score cards\n│   │       ├── heatmap_explorer.py# Full-size overlays, concern toggle, opacity\n│   │       ├── comparison.py      # Before/after with delta indicators\n│   │       ├── model_internals.py # Distributions, correlations, fairness\n│   │       └── dataset_explorer.py# Browse by age/ethnicity/score filters\n│   └── utils/\n│       ├── cielab.py              # RGB \u003c-\u003e CIELAB conversion\n│       ├── landmarks.py           # MediaPipe landmark utilities\n│       └── reproducibility.py     # Seed setting, device detection\n├── scripts/\n│   ├── generate_pseudo_labels.py  # Batch pseudo-label generation CLI\n│   ├── train.py                   # Training CLI with --resume support\n│   ├── evaluate.py                # Evaluation + fairness report CLI\n│   ├── fairness_report.py         # Standalone fairness report generator\n│   ├── export_onnx.py             # ONNX export with verification\n│   ├── serve.py                   # Start FastAPI server\n│   └── dashboard.py               # Start Streamlit dashboard\n├── tests/                         # Unit + integration tests (\u003e= 65% coverage)\n│   ├── conftest.py                # Shared fixtures (dummy tensors, mock model)\n│   ├── test_backbone.py           # Backbone encoder tests\n│   ├── test_decoder.py            # U-Net decoder tests\n│   ├── test_heads.py              # Quality and age head tests\n│   ├── test_model.py              # Full model integration tests\n│   ├── test_losses.py             # Multi-task loss tests\n│   ├── test_dataset.py            # Dataset and collation tests\n│   ├── test_utils.py              # Utility module tests\n│   └── test_api.py                # API endpoint tests\n├── outputs/\n│   └── models/                    # Checkpoints, ONNX exports, MediaPipe models\n├── Dockerfile                     # Multi-stage build, \u003c 4GB\n├── docker-compose.yml             # API + Dashboard services\n├── requirements.txt               # All dependencies\n├── pyproject.toml                 # Project metadata, pytest, mypy, ruff config\n└── .gitignore\n```\n\n---\n\n## Quick Start\n\n```bash\n# Setup\npython -m venv venv\nvenv\\Scripts\\activate              # Windows\n# source venv/bin/activate         # macOS/Linux\npip install -r requirements.txt\n\n# Download datasets\npython -m SkinAge.src.data.download --dataset utk_face --output data/raw/\npython -m SkinAge.src.data.download --dataset ffhq --output data/raw/ --limit 10000\npython -m SkinAge.src.data.download --dataset celeba --output data/raw/ --limit 20000\n\n# Generate pseudo-labels\npython scripts/generate_pseudo_labels.py \\\n    --data-dir data/raw/ \\\n    --output-dir data/processed/\n\n# Train the model (two-phase: frozen backbone -\u003e full fine-tune)\npython scripts/train.py \\\n    --config config/model_config.yaml \\\n    --data-dir data/processed/\n\n# Evaluate\npython scripts/evaluate.py \\\n    --checkpoint outputs/models/best_model.pth \\\n    --data-dir data/processed/\n\n# Export to ONNX\npython scripts/export_onnx.py \\\n    --checkpoint outputs/models/best_model.pth \\\n    --verify\n\n# Launch the API\npython scripts/serve.py --port 8000\n\n# Launch the dashboard\npython scripts/dashboard.py\n```\n\n### Docker Deployment\n\n```bash\n# Build and run everything\ndocker-compose up --build\n\n# API available at http://localhost:8000\n# Dashboard available at http://localhost:8501\n```\n\n---\n\n## API Reference\n\n### POST `/api/v1/analyze`\n\nUpload a selfie and receive a full skin analysis.\n\n```bash\ncurl -X POST http://localhost:8000/api/v1/analyze \\\n  -F \"file=@selfie.jpg\" \\\n  -F \"age=30\"\n```\n\n**Response:**\n```json\n{\n  \"overall_score\": 74.2,\n  \"predicted_age\": 32.1,\n  \"age_delta\": 2.1,\n  \"zone_scores\": [\n    {\n      \"zone\": \"forehead\",\n      \"composite_score\": 78.5,\n      \"label\": \"Good\",\n      \"concerns\": {\n        \"wrinkle\": {\"score\": 72.3, \"severity\": \"mild\"},\n        \"pigmentation\": {\"score\": 84.7, \"severity\": \"minimal\"}\n      }\n    },\n    {\n      \"zone\": \"cheeks\",\n      \"composite_score\": 68.1,\n      \"label\": \"Fair\",\n      \"concerns\": {\n        \"wrinkle\": {\"score\": 65.2, \"severity\": \"mild\"},\n        \"pigmentation\": {\"score\": 71.0, \"severity\": \"mild\"},\n        \"redness\": {\"score\": 58.3, \"severity\": \"moderate\"},\n        \"pore_texture\": {\"score\": 77.8, \"severity\": \"mild\"}\n      }\n    }\n  ],\n  \"heatmaps\": {\n    \"wrinkle\": \"data:image/png;base64,...\",\n    \"pigmentation\": \"data:image/png;base64,...\",\n    \"redness\": \"data:image/png;base64,...\",\n    \"pore_texture\": \"data:image/png;base64,...\"\n  },\n  \"metadata\": {\n    \"processing_time_ms\": 1243,\n    \"model_version\": \"1.0.0\"\n  }\n}\n```\n\n### POST `/api/v1/compare`\n\nCompare two images (before/after).\n\n```bash\ncurl -X POST http://localhost:8000/api/v1/compare \\\n  -F \"before=@before.jpg\" \\\n  -F \"after=@after.jpg\"\n```\n\n**Response** includes both analyses plus per-zone delta scores with improvement indicators.\n\n### GET `/api/v1/health`\n\n```bash\ncurl http://localhost:8000/api/v1/health\n```\n\n```json\n{\n  \"status\": \"healthy\",\n  \"model_version\": \"1.0.0\",\n  \"device\": \"cuda\",\n  \"uptime_seconds\": 3621\n}\n```\n\n---\n\n## Streamlit Dashboard\n\nLaunch with `streamlit run SkinAge/src/dashboard/app.py` — 5 pages:\n\n| Page | What It Shows |\n|------|--------------|\n| **Live Demo** | Upload selfie, zone overlay, score cards with color-coded labels, heatmap thumbnails, gauge chart |\n| **Heatmap Explorer** | Full-size concern overlays, radio toggle between wrinkle/pigmentation/redness/pore, opacity slider |\n| **Before/After** | Side-by-side comparison, delta indicators with color coding, grouped bar chart |\n| **Model Internals** | Pseudo-label distributions, zone score histograms, correlation matrix, fairness metrics |\n| **Dataset Explorer** | Browse by age/ethnicity/score filters, paginated image grid, pseudo-label detail view |\n\n---\n\n## Quality Gating\n\nImages that fail any quality check are rejected with actionable guidance before inference:\n\n| Check | Threshold | Rejection Message |\n|-------|-----------|-------------------|\n| Face detection | Confidence \u003e= 0.70 | \"No face detected — ensure your face is clearly visible\" |\n| Head yaw | \u003c= 25 deg | \"Face is turned too far sideways — look straight at the camera\" |\n| Head pitch | \u003c= 20 deg | \"Face is tilted too far up/down — hold the camera at eye level\" |\n| Blur | Laplacian \u003e= 80 | \"Image is too blurry — hold the camera steady\" |\n| Brightness | 40-220 | \"Image is too dark/bright — move to even lighting\" |\n| Resolution | \u003e= 200x200 | \"Image resolution too low — move closer or use a higher-res camera\" |\n| Landmarks | \u003e= 90% visible | \"Face is partially occluded — remove sunglasses, hair, or hands\" |\n\nAll checks run unconditionally (no short-circuit) so the user can fix everything in one go.\n\n---\n\n## Fairness \u0026 Calibration\n\nThe system includes built-in fairness monitoring:\n\n- **Ethnicity mapping**: UTKFace categories (White, Black, Asian, Indian, Other) mapped to approximate Fitzpatrick types\n- **Score gap audit**: Maximum quality score difference between any two ethnic groups must be \u003c= 6 points\n- **Age MAE gap**: Maximum age prediction error difference between groups must be \u003c= 1.5 years\n- **Redness calibration**: Redness scoring calibrated per Fitzpatrick type to account for natural skin tone variation\n- **No color jitter**: Augmentation pipeline deliberately excludes color jitter — skin tone carries diagnostic signal for redness and pigmentation\n\nGenerate a full fairness report:\n\n```bash\npython scripts/fairness_report.py \\\n  --checkpoint outputs/models/best_model.pth \\\n  --data-dir data/processed/ \\\n  --output-dir outputs/fairness/\n```\n\nProduces: Markdown report + JSON data + PNG visualizations (score distributions, group comparisons, redness calibration curves).\n\n---\n\n## Configuration Guide\n\nAll configuration files are in `config/` and use YAML format:\n\n### Model Configuration (`model_config.yaml`)\n\n| Key | Description | Default |\n|-----|-------------|---------|\n| `backbone.pretrained` | Use ImageNet weights | `true` |\n| `backbone.feature_dim` | Backbone output dimension | `1408` |\n| `unet_decoder.output_channels` | Heatmap channels (one per concern) | `4` |\n| `quality_head.layers` | FC layer sizes | `[1408, 512, 28]` |\n| `quality_head.dropout` | Dropout rate | `0.3` |\n| `age_head.layers` | FC layer sizes | `[1408, 256, 1]` |\n| `loss_weights.heatmap` | Heatmap MSE weight | `1.0` |\n| `loss_weights.quality` | Quality SmoothL1 weight | `2.0` |\n| `loss_weights.age` | Age SmoothL1 weight | `1.5` |\n\n### Training Schedule\n\n| Key | Description | Default |\n|-----|-------------|---------|\n| `training.phase1.epochs` | Phase 1 epochs (heads only) | `3` |\n| `training.phase1.learning_rate` | Phase 1 LR | `1e-3` |\n| `training.phase2.epochs` | Phase 2 max epochs | `30` |\n| `training.phase2.learning_rate` | Phase 2 LR | `5e-5` |\n| `early_stopping.patience` | Epochs without improvement | `7` |\n| `dataloader.batch_size` | Training batch size | `16` |\n| `optimizer.name` | Optimizer | `AdamW` |\n| `optimizer.weight_decay` | Weight decay | `1e-4` |\n\n---\n\n## Testing\n\n```bash\n# Run the full test suite\npytest SkinAge/tests/ -v\n\n# Run with coverage report\npytest SkinAge/tests/ --cov=SkinAge/src --cov-report=term-missing\n\n# Run specific test module\npytest SkinAge/tests/test_model.py -v\n```\n\nTests are designed to run without trained models or downloaded datasets — all use mock fixtures and dummy tensors.\n\n---\n\n## ONNX Export\n\nFor optimized CPU inference in production:\n\n```bash\npython scripts/export_onnx.py \\\n  --checkpoint outputs/models/best_model.pth \\\n  --output outputs/models/skinage.onnx \\\n  --opset 17 \\\n  --verify\n```\n\nThe ONNX model supports dynamic batch sizes and produces three named outputs: `heatmaps`, `quality`, and `age`. The `--verify` flag runs ONNXRuntime inference and compares against PyTorch outputs (atol=1e-4).\n\n---\n\n## Tech Stack\n\n| Category | Tools |\n|----------|-------|\n| **ML** | PyTorch, timm (EfficientNet-B2), torch.amp (mixed precision) |\n| **Computer Vision** | OpenCV, MediaPipe (face mesh, 468 landmarks), scikit-image (SSIM) |\n| **Data** | Albumentations, pandas, NumPy, CIELAB color space |\n| **API** | FastAPI, Pydantic v2, uvicorn |\n| **Dashboard** | Streamlit, matplotlib |\n| **Production** | ONNX, ONNXRuntime, Docker, docker-compose |\n| **Testing** | pytest (\u003e= 65% coverage target) |\n| **Config** | YAML (4 config files: model, data, zones, api) |\n| **Code Quality** | mypy (strict), ruff, isort |\n\n---\n\n## Known Limitations\n\n- **Pseudo-labels, not ground truth** — All quality scores are derived from classical CV features, not dermatologist annotations. V2 will add professional annotation pipelines.\n- **No video/real-time analysis** — Single-image analysis only. Real-time webcam analysis is out of scope for V1.\n- **Age labels only from UTKFace** — FFHQ and CelebA don't carry age labels, so age loss is only computed on ~40% of training batches.\n- **Ethnicity categories are coarse** — UTKFace provides 5 broad categories; finer-grained Fitzpatrick typing would improve redness calibration.\n- **No mobile deployment** — V1 is server-side only. CoreML/TFLite export is planned for V2.\n- **MediaPipe model files required** — Face detection and landmark models must be downloaded separately to `outputs/models/mediapipe/`.\n- **xG-style proxy for skin quality** — Similar to how proxy xG models estimate expected goals from limited data, our pseudo-labels estimate quality from observable texture/color features. Professional annotations would improve accuracy.\n\n---\n\n## License\n\nMIT\n\n---\n\n*Built with PyTorch, MediaPipe, FastAPI, and Streamlit.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fennsss%2Fskinage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fennsss%2Fskinage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fennsss%2Fskinage/lists"}