Skip to content

About

Apparent age from aligned face images - three heads compared, errors reported per age group, gender and ethnicity.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Age estimation from face images

Estimates a person's apparent age from a photo. Faces are found and aligned with MediaPipe landmarks, and a pretrained CNN predicts a probability distribution over ages 0-100. The reported age is the expected value of that distribution.

Three official NASA portraits: face mesh, the five alignment points, the aligned crop and the predicted distribution over age next to the actual age

The pipeline on official NASA astronaut portraits (public domain): 478 face landmarks, five of them for the alignment, the aligned crop the network sees, and its probability for every age from 0 to 100, next to the person's actual age when the photo was taken.
flowchart LR
    A[photo] --> B[MediaPipe face landmarks]
    B --> C[similarity alignment<br/>5-point template]
    C --> D[ConvNeXt-T / ResNet-50<br/>ImageNet or DINOv3 weights]
    D --> E[softmax over ages 0..100]
    E --> F[expected age]
Loading

Highlights

  • Same alignment for training and inference. Five landmarks (iris centres, nose tip, mouth corners) are mapped onto a fixed template with a least-squares similarity transform (Umeyama). Every face therefore has the same scale, position and roll before the network sees it.
  • Three interchangeable heads: plain L1 regression, label-distribution learning (DLDL-v2) and the mean-variance loss. All three run from one config switch, so they can be compared in an ablation.
  • Evaluation broken down by group. Besides MAE and CS@5, errors are reported per age group, gender and ethnicity, because a single MAE can hide large gaps between groups.
  • Ready for a Slurm cluster. Job scripts are generated for a Slurm GPU cluster. Training saves a checkpoint when Slurm warns about the time limit, and a resubmitted job resumes where it stopped.
  • Deployment: ONNX export, checked numerically against PyTorch, and a CLI for images, videos and webcams.

Results

UTKFace, random 80/10/10 split stratified by age, test set, flip TTA:

Model Head MAE ↓ CS@5 ↑ MAE in the unified benchmark ↓
ResNet-50 regression (L1) 4.83 0.635 4.53
ResNet-50 mean-variance 4.69 0.645 4.57
ResNet-50 DLDL 4.74 0.659 4.60 (DLDL-v2)
ConvNeXt-T (ImageNet-22k) DLDL 4.40 0.678
ConvNeXt-T (DINOv3), main model DLDL 4.25 0.687

The last column comes from Paplhám and Franc (CVPR 2024, Table 6): the same heads on ResNet-50, ImageNet initialisation, trained and tested on UTKFace with one protocol. Their splits, 256 px input and training schedule differ from this repo, so the comparison checks that the reimplementation is in the right range rather than that it matches to the decimal. That paper's main finding is worth knowing: with a fixed protocol, the choice of head changes MAE by at most a few tenths of a year. Pretraining matters about as much (4.38 with IMDB-WIKI pretraining vs. 4.75 from ImageNet, both cross-entropy), and input resolution far more (5.83 at 64 px vs. 4.38 at 256 px).

2371 test faces. The ResNet-50 heads land within 0.1-0.3 years of the unified benchmark, and, as that paper found, the head matters little (4.69-4.83). The backbone matters more: ConvNeXt-T is 0.3-0.4 years better than ResNet-50, and the self-supervised DINOv3 weights another 0.15 on top. DINOv3 also had the best validation MAE (4.24 against 4.38), which is how it became the main model, before the test set was scored.

Error of the main model by group (test set, MAE in years):

Age 0-12 13-19 20-34 35-49 50-64 65+
1.34 2.94 3.44 5.45 6.53 7.93
Female Male Asian Black Indian White Other
3.99 4.50 3.16 4.52 4.21 4.63 3.55

Errors grow with age: a year of difference shows more on a child's face than on a 70-year-old's, and UTKFace has fewer older faces to learn from. The spread across groups is smaller than the spread across ages. The full breakdown is written to eval_test.json next to each checkpoint.

Quickstart

pip install -e ".[train]"
bash scripts/download_models.sh        # MediaPipe landmarker + pretrained weights
bash scripts/download_utkface.sh       # needs a Kaggle API token, see the script

age-align --src data/utkface/raw --out data/utkface/aligned --model models/face_landmarker.task
age-train --config configs/convnext_tiny_dinov3_dldl.yaml --set data.root=data/utkface/aligned
age-eval  --checkpoint runs/convnext_tiny_dinov3_dldl/best.pt --split test
age-predict --checkpoint runs/convnext_tiny_dinov3_dldl/best.pt --source path/to/photos/
python -m age_estimation.export --checkpoint runs/convnext_tiny_dinov3_dldl/best.pt

Any config value can be overridden from the command line, for example --set model.head=regression --set optim.lr=1e-3.

Training on a GPU cluster (Slurm)

bash scripts/setup_cluster.sh                      # once, on a login node: venv + install
bash scripts/download_models.sh                 # login node has internet; compute nodes may not
python3 slurm/generate.py --list                # jobs defined in slurm/cluster.toml
python3 slurm/generate.py align --submit        # CPU job
python3 slurm/generate.py train-convnext --submit
python3 slurm/generate.py ablation-heads --submit   # array job, one task per head
python3 slurm/generate.py ablation-backbone --submit   # ConvNeXt-T with DINOv3 weights

slurm/generate.py renders slurm/cluster.toml into sbatch scripts. It sets the account, partition, GPU constraint, environment and log paths, and handles array arguments. Put your Slurm account in slurm/cluster.local.toml (not committed) before submitting.

Project layout

configs/                  experiment configs (YAML, overridable with --set)
scripts/                  data/model download, cluster environment setup
slurm/                    cluster.toml (cluster + jobs) and the sbatch generator
src/age_estimation/
  align.py                MediaPipe landmarks -> similarity transform -> aligned crop
  data/                   UTKFace parsing, stratified split, dataset and augmentation
  models.py               timm backbone + regression / distribution head
  losses.py               L1, DLDL (KL + expectation L1), mean-variance
  train.py                training loop: AMP (bf16/fp16), cosine LR, resumable checkpoints
  evaluate.py             test metrics with per-group breakdown
  export.py               ONNX export + ONNX Runtime parity check
  predict.py              images / video / webcam inference
tests/                    unit tests and an end-to-end smoke test (train -> eval -> ONNX)

Design notes

  • Why a distribution head? Neighbouring ages look almost the same. A Gaussian label distribution (σ = 2 years) encodes that a 31-year-old is "nearly right" for a 30-year-old, which gives a smoother training signal than one-hot classification. It is also more robust to label noise than plain regression.
  • Alignment context. The template is scaled by face_scale = 0.8, so the crop keeps the forehead, hairline and chin, which carry age cues that a tight ArcFace crop cuts away.
  • Which pretraining? The same ConvNeXt-T with supervised ImageNet-22k weights and distilled from Meta's self-supervised DINOv3 (2025), the newest ConvNeXt weights in timm. DINOv3 won on validation and is the main model. Architecture, input size and schedule stay the same; the self-supervised backbone is fine-tuned gently, at a tenth of the ImageNet model's backbone learning rate (2e-5). DINOv3 weights come under Meta's DINOv3 licence.
  • Mixed precision uses bf16 on A100/H100 and fp16 with loss scaling on V100. Both are picked automatically.

Limitations

The model estimates apparent age as labelled in UTKFace, not real age. UTKFace is unbalanced across ages and ethnicities, so the per-group breakdown should be read before trusting the model on any particular population. UTKFace is licensed for non-commercial research only.

References

  • Zhang, Song, Qi. Age Progression/Regression by Conditional Adversarial Autoencoder (UTKFace), CVPR 2017.
  • Gao et al. Age Estimation Using Expectation of Label Distribution Learning (DLDL-v2), IJCAI 2018.
  • Pan et al. Mean-Variance Loss for Deep Age Estimation from a Face, CVPR 2018.
  • Paplhám, Franc. A Call to Reflect on Evaluation Practices for Age Estimation: Comparative Analysis of the State-of-the-Art and a Unified Benchmark, CVPR 2024.
  • Siméoni et al. DINOv3, arXiv:2508.10104, 2025.
  • Umeyama. Least-squares estimation of transformation parameters between two point patterns, TPAMI 1991.

About

Apparent age from aligned face images - three heads compared, errors reported per age group, gender and ethnicity.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages