PINTO0309/High-Angle_Robust_Fast_FaceAlignment

HRFFA: High-Angle Robust Fast Face Alignment. Whole-head face alignment dataset & training pipeline.

Python

190

46 commits

updated Oct 9, 2026

See the code

README

HRFFA: High-Angle Robust Fast FaceAlignment

GitHub DOI Ask DeepWiki

A training, distillation and ONNX deployment pipeline for whole-head face alignment that stays robust at extreme head poses while remaining light enough for CPU inference.

  • Face alignment on head crops, not face crops. Conventional face-alignment papers assume a tightly cropped face region; this is a special-purpose model that works on a crop of the whole head instead. The architecture is therefore at a considerable disadvantage against paper benchmarks, but the intent is to take in more features by processing the entire head and the background context contained in a small margin at the same time. It is also the practical choice: object detectors localize whole heads far more stably than faces.
  • For head crops that include looking up / looking down (measured pitch beyond ±85°), in-plane rotation over the full 360° roll, and profile views (yaw ±90°), the models output 68 / 98 / 29 landmarks plus a 3-class visibility label per point (outside the image / occluded / visible).
  • The teacher is a DINOv3 ViT-L/16 (320×320); the students are a ViT-T/16 (256 and 96 input) and a PP-HGNetV2-B0 based CNN (256 and 96 input). Students are trained by online distillation from the teacher and exported to ONNX graphs that run in 1.5–12 ms on a CPU.

teacher clean_v3: extreme pitch/yaw tiles with roll 0..320 deg student vitt-256: measured pitch extremes with roll

Left: teacher vitl-320 on nine looking-up × profile tiles with a roll of 0–320° composited onto each tile.
Right: student vitt-256 on measured pitch extremes (+86° to −89°) with roll. Predictions only.

1. Results at a glance

inter-ocular NME %, lower is better; CPU latency on an i9-10900K with onnxruntime 1.22 CPU EP, batch 1.

Checkpoints and ONNX/TFLite(LiteRT): https://github.com/PINTO0309/High-Angle_Robust_Fast_FaceAlignment/releases/tag/weights

ModelInputParamsGMACsGFLOPsCPU msWFLW FullWFLW Pose300W FullCOFW
D-ViT (paper)25696.4M≈73.15≈146.30–3.756.432.854.13
vitl-320 (teacher)320308.2M131.32262.635201.512.541.031.21
vitt-2562569.0M2.054.0912.43.365.642.662.76
hg0-2562561.6M0.701.405.25.329.533.873.77
vitt-096969.0M0.430.854.65.148.653.863.90
hg0-096961.6M0.130.261.57.4614.714.774.99

How to read these numbers (important): all splits of the real-image datasets — including WFLW test, 300W and COFW test — are used for training. The "official" numbers above therefore measure how well the models fit the training distribution and must not be compared with published benchmark results. The only unleaked instrument is 300wlp_val (head-NME on a held-out 300W-LP subset; values in 050). The full tables — WFLW subsets, FR/AUC, breakdowns by pose (yaw / pitch / roll) and by image degradation, and the D-ViT paper values listed side by side with an explicit non-comparability note — follow in §1.1 (same content as 050 §2). The D-ViT (paper) row lists the published benchmark values of arXiv 2411.07167 (Tables 1 / 2, "Ours": 256×256 input, 8 prediction blocks = 96.4M parameters per its Table S1) for format reference only. The paper does not report FLOPs or latency; the GMACs / GFLOPs are our estimate from the official code with its default arguments (8 prediction blocks, 32×32 heatmaps, depth 256, 256×256 input, all 8 stages, batch 1, same FlopCounterMode method as the other rows) and are approximate because that configuration has 101.1M parameters, about 5 % more than Table S1. Its COFW NME is normalized by the inter-pupil distance instead of inter-ocular, and its test sets are held out from training, so no ranking against the fit measures above is implied.

1.1 Full tables

All tables below are generated from runs/<run>/eval_best_{official,stratreal,stress,style}.log by scripts/make_results_tables.py. Notes:

  • Absolute values are fit measures (all real-image splits are in the training data). The D-ViT (paper) rows in Tables 1 / 2 are the published test-benchmark values of arXiv 2411.07167 (p.6, "Ours") and are listed for format reference only — the conditions differ, so no ranking is implied.
  • Yaw / pitch / roll: the models do not output head pose, so pose robustness is expressed as landmark accuracy per pose. Table 3: inter-ocular NME per effective pose bin (6DRepNet-estimated pose plus the applied rotation, 3 sets × 300 images × 16 configurations). Table 4: NME per in-plane roll (exact 360° roll equivariance; a small worst−base means equivariant). Table 5: NME under projective camera pitch ±15/±25° and yaw ±15° perturbations. Table 6: head-NME (normalized by the crop side, not inter-ocular) of the three real sets stratified by 6DRepNet-estimated pose.
  • Table 7 perturbations are applied in crop pixels (mblur9/21 = 30° motion blur of 9/21 px, warm/cool = channel gains, gamma 0.6/1.6, gray, jpeg30 = JPEG quality 30). Their strength therefore depends on the input resolution: for the 96 models (vitt-096 / hg0-096) a 21 px blur on a 96 px crop corresponds to ~56 px at 256, so their Table 7 rows are not comparable with the 256 models.

Table 1. NME (%, inter-ocular, lower is better)

ModelWFLW
Full

Pose

Exp

Ill

Mu

Occ

Blur
COFW
Full
300W
Full

Comm

Chal
D-ViT (paper)3.756.433.854.063.574.474.374.132.852.434.56
vitl-3201.512.541.551.471.421.561.581.211.030.961.34
vitt-2563.365.643.433.253.193.563.602.762.662.363.90
hg0-2565.329.535.545.165.096.296.043.773.873.375.96
vitt-0965.148.655.324.965.065.525.403.903.853.575.02
hg0-0967.4614.717.777.198.108.918.164.994.774.246.91

Table 2. FR10 (%, lower is better) and AUC10 (%, higher is better) on WFLW

ModelFR10
Full

Pose

Exp

Ill

Mu

Occ

Blur
AUC10
Full

Pose

Exp

Ill

Mu

Occ

Blur
D-ViT (paper)1.768.281.271.291.943.802.0763.740.162.664.764.757.158.6
vitl-3200.000.000.000.000.000.000.0084.974.584.585.485.884.484.2
vitt-2560.281.530.320.000.000.540.5266.543.965.767.568.264.564.1
hg0-2567.0032.215.735.446.3113.049.4449.217.546.750.350.242.442.8
vitt-0964.2023.314.782.722.915.573.7549.720.247.751.249.946.247.1
hg0-09617.8867.4818.7914.1820.8726.0922.3836.15.831.937.634.428.730.0

Table 3. NME (%) by yaw / pitch bin (effective pose bins of pose-stress)

ModelYaw
0–30
Yaw
30–60
Yaw
60–95
Pitch
−95..−45
Pitch
−45..−15
Pitch
−15..15
Pitch
15..45
Pitch
45..95
vitl-3201.261.712.041.761.371.301.291.85
vitt-2562.944.104.684.073.203.062.994.43
hg0-2564.216.358.146.264.634.464.357.51
vitt-0964.496.217.375.934.824.774.547.06
hg0-0965.698.7010.858.026.176.215.7910.07
n5933112414322325032944139848

Table 4. NME (%) by in-plane roll (pose-stress, n=300 per set)

ModelSetRoll 04590135180225270315worst−base
vitl-320wflw1.611.611.641.601.631.611.631.60+0.02
300w1.121.111.141.111.131.111.131.11+0.01
cofw1.291.301.311.301.301.301.301.29+0.02
vitt-256wflw3.673.613.673.603.703.613.683.61+0.03
300w2.932.882.892.892.952.922.962.90+0.03
cofw3.012.963.022.972.982.962.982.97+0.01
hg0-256wflw5.965.955.906.065.926.035.985.96+0.10
300w4.144.114.264.204.264.294.214.16+0.15
cofw4.124.084.064.064.034.114.124.06+0.00
vitt-096wflw6.306.056.175.986.306.266.406.19+0.10
300w4.584.414.554.505.035.115.144.71+0.57
cofw4.744.614.724.494.734.494.564.55+0.00
hg0-096wflw8.899.039.099.218.989.339.908.72+1.01
300w5.575.615.826.076.406.596.055.95+1.03
cofw5.845.796.055.715.765.515.685.73+0.21

Table 5. NME (%) under camera pitch / yaw perturbation (pose-stress, n=300 per set)

ModelSetbasecam
pitch
−25


−15


+15


+25
cam
yaw
−15


+15


p+25,y+15


p−25,y−15
vitl-320wflw1.611.781.631.581.601.551.551.561.78
300w1.121.251.151.091.101.081.081.081.22
cofw1.291.361.291.261.291.231.241.251.35
vitt-256wflw3.673.843.673.523.483.523.503.443.94
300w2.933.072.932.812.752.782.812.733.03
cofw3.013.123.012.952.922.902.902.923.12
hg0-256wflw5.965.995.715.715.385.555.605.316.08
300w4.144.464.244.053.983.974.043.904.39
cofw4.124.274.114.023.963.983.973.994.23
vitt-096wflw6.305.945.835.975.515.785.765.306.05
300w4.584.494.394.414.104.244.344.064.41
cofw4.744.534.494.454.184.334.424.204.47
hg0-096wflw8.898.208.278.447.728.378.197.358.34
300w5.575.385.385.454.945.165.254.815.34
cofw5.845.625.575.675.285.355.535.135.48

Table 6. head-NME (×100) stratified by 6DRepNet-estimated pose (3,696 real images from the three sets)

ModelmeanYaw
0–30
Yaw
30–60
Yaw
60–95
Pitch
−90..−30
Pitch
−30..−10
Pitch
−10..10
Pitch
10..30
Pitch
30..90
vitl-3200.370.360.420.450.420.370.360.400.50
vitt-2560.850.810.981.010.980.850.820.931.13
hg0-2561.291.211.601.851.711.281.211.502.09
vitt-0961.271.221.461.581.481.271.221.381.71
hg0-0961.751.602.222.852.531.761.592.042.94
n2939672851431076213623232

Table 7. style-shift: NME (%) under image degradations (in parentheses: degradation vs clean, n=300 per set)

ModelSetcleanmblur9mblur21warmcoolgamma0.6gamma1.6grayjpeg30
vitl-320wflw_test1.491.57 (+5.5%)2.09 (+40.4%)1.51 (+1.4%)1.51 (+1.5%)1.50 (+1.1%)1.51 (+1.3%)1.60 (+7.8%)1.55 (+4.5%)
300w_valid1.041.13 (+8.6%)1.46 (+40.6%)1.06 (+2.0%)1.05 (+1.2%)1.06 (+1.8%)1.05 (+1.4%)1.09 (+4.9%)1.08 (+4.3%)
cofw_test1.211.27 (+5.3%)1.60 (+32.3%)1.22 (+1.3%)1.23 (+1.5%)1.22 (+1.1%)1.22 (+1.2%)1.27 (+5.0%)1.25 (+3.6%)
vitt-256wflw_test3.263.32 (+1.8%)4.60 (+41.1%)3.32 (+1.9%)3.33 (+2.1%)3.35 (+2.7%)3.36 (+3.0%)3.48 (+6.6%)3.36 (+3.1%)
300w_valid2.652.73 (+2.6%)3.66 (+37.7%)2.68 (+1.1%)2.69 (+1.3%)2.71 (+2.0%)2.72 (+2.3%)2.75 (+3.7%)2.73 (+2.9%)
cofw_test2.762.82 (+2.1%)3.51 (+27.2%)2.79 (+1.0%)2.79 (+1.1%)2.83 (+2.4%)2.81 (+1.7%)2.85 (+3.3%)2.81 (+1.8%)
hg0-256wflw_test5.085.28 (+3.9%)8.16 (+60.5%)5.14 (+1.2%)5.23 (+2.9%)5.13 (+0.9%)5.29 (+4.2%)5.41 (+6.4%)5.35 (+5.2%)
300w_valid3.853.88 (+0.8%)5.20 (+35.0%)3.90 (+1.2%)3.90 (+1.3%)3.88 (+0.7%)3.92 (+1.7%)3.95 (+2.5%)3.90 (+1.0%)
cofw_test3.793.90 (+2.9%)4.93 (+30.0%)3.83 (+1.1%)3.83 (+0.9%)3.82 (+0.8%)3.82 (+0.8%)3.91 (+3.2%)3.85 (+1.5%)
vitt-096wflw_test5.016.26 (+25.0%)24.80 (+395.2%)5.10 (+1.9%)5.09 (+1.6%)5.13 (+2.4%)5.21 (+4.1%)5.37 (+7.3%)5.36 (+7.0%)
300w_valid3.824.42 (+15.8%)18.31 (+379.4%)3.86 (+1.1%)3.86 (+1.1%)3.90 (+2.1%)3.94 (+3.1%)3.96 (+3.8%)4.04 (+5.8%)
cofw_test3.894.52 (+16.3%)15.95 (+310.4%)3.95 (+1.8%)3.95 (+1.6%)3.99 (+2.8%)4.00 (+2.8%)4.05 (+4.2%)4.08 (+5.0%)
hg0-096wflw_test7.019.18 (+31.0%)32.00 (+356.7%)7.15 (+2.0%)7.30 (+4.2%)7.11 (+1.5%)7.36 (+5.0%)7.77 (+10.9%)9.10 (+29.9%)
300w_valid4.645.68 (+22.5%)19.79 (+326.5%)4.68 (+1.0%)4.74 (+2.1%)4.70 (+1.2%)4.72 (+1.7%)4.80 (+3.5%)5.60 (+20.8%)
cofw_test5.035.71 (+13.7%)18.03 (+258.9%)5.07 (+0.8%)5.15 (+2.4%)5.04 (+0.3%)5.16 (+2.6%)5.19 (+3.2%)5.74 (+14.2%)

2. Features

  • Unified dataset: 300W-LP / WFLW / 300W / COFW converted into a single JSONL format (head bbox, visibility, pose, and a per-record license_tag). Head boxes and face parts come from DEIMv2-Wholebody49 (Apache-2.0) pseudo labels; extreme poses are reinforced with depth-reprojection synthesis and generated image pools.
  • Geometric augmentation composed into a single homography: full 360° roll, camera pitch ±25° / yaw ±15°, horizontal flip (with left/right index swap), scale and translation are applied in one warp, and the GT coordinates, visibility and rotation matrix follow exactly. Photometric augmentation (brightness, gamma, grayscale, noise, blur, JPEG quality 35–85, motion blur) is applied on the training side as well.
  • Online teacher → student distillation: the same augmented crop is rendered at 320 for the teacher and at 256 (or 96) for the student; coordinate, visibility and decoder-token KD are combined with the GT losses. The learning-rate schedule is WSD (warmup → constant → cosine decay over the last epochs); the stable phase can be extended by editing epochs and resuming.
  • Evaluation instruments (hrffa.train.evaluate): official (inter-ocular NME / FR@0.1 / AUC@0.1), stratify-real (stratified by 6DRepNet-estimated pose), pose-stress (real images + exact GT geometric transforms to measure equivariance under 360° roll and camera pose), and style-shift (motion blur, color temperature, gamma, grayscale, JPEG).
  • ONNX export (hrffa.export.export_onnx): a static batch-1 graph is optimized with onnxslim → onnxsim (without Gemm fusion), checked for parity against PyTorch, and an N-batch variant (<stem>_n.onnx) is derived from it. The batch axis stays the leading axis of every Reshape and no rank-5 tensors are produced.

3. Models and the ONNX I/O contract

Teacher vitl-320Students vitt-256 / vitt-096Students hg0-256 / hg0-096
BackboneDINOv3 ViT-L/16 (the official implementation is imported at runtime from the torch.hub cache; its code is not vendored)ViT-T/16 (own implementation with RoPE, initialized from ckpts/vitt_distill.pt)PP-HGNetV2-B0 stages 0–2 (own implementation) + FPN (stride 8)
HeadPoint-query Transformer decoder (d256, 4 layers) with coordinate regression and visibilitySame (d256, 3 layers)Same (d128, 2 layers, 4 heads)
Input normalizationImageNet mean/stdcenter05 (x/255 − 0.5)/0.5 (no normalization op inside the ONNX graph)center05, folded into the stem conv (no input op at all)
Presetclean_v3student_s256_96gb_r2 / student_s096_96gb_r2student_hg0_wsd / student_hg0_s096_wsd

ONNX (example for --scheme ibug68; wflw98 / cofw29 are exported as separate files):

# RGB, normalized as above. S = 320 / 256 / 96 (the preset's out_size)
input   images      float32 [N, 3, S, S]
# coordinates relative to the crop (0..1 inside the crop; points outside still get coordinates)
output  points      float32 [N, K, 2]
# visibility logits (0 = outside the image, 1 = occluded, 2 = visible)
output  vis_logits  float32 [N, K, 3]
  • Crop: a square around the head bbox with a margin of 0.05 of the side length, rendered at S×S (the same geometry as in training and evaluation). The downscaling method (bilinear / area, with or without antialiasing) changes the results by less than ±1%.
  • Two files are written: the fixed batch-1 *.onnx and *_n.onnx with a symbolic batch axis N. Both are verified with onnxruntime to agree at batch sizes 1 / 2 / 3.
  • Head pose (rotation / roll_bit) is not an output. Pose supervision is not used in training; derive pose with an add-on head or PnP if needed.

4. Setup

Prerequisites: Linux, Python 3.13 (pinned in .python-version), uv, and an NVIDIA GPU (torch built for CUDA 12.8; an 8 GB GPU is enough for smoke tests, full training targets a 96 GB class GPU). Dependencies are pinned with == in pyproject.toml and hash-locked in uv.lock.

git clone https://github.com/PINTO0309/High-Angle_Robust_Fast_FaceAlignment.git
cd High-Angle_Robust_Fast_FaceAlignment
uv sync --frozen                                   # default: onnxruntime-gpu 1.22.0 (the validated toolchain)
uv sync --frozen --no-group ort --group tensorrt   # alternative: onnxruntime-gpu 1.26.0 for TensorRT BF16 (trt_bf16_enable)
# NOTE: `uv run` re-syncs the environment with the DEFAULT groups before running, i.e. a plain
# `uv run python ...` silently switches back to 1.22.0. Keep the same group flags on `uv run`
# (or use `uv run --no-sync` / `.venv/bin/python` after the manual sync):
uv run --no-group ort --group tensorrt \
python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d tensorrt

uv run python -m unittest tests/test_model_arms.py

4.1 Weights (ckpts/, none of them are bundled)

FilePurposeSource / license
dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth (also vits16 / vitb16 / vith16plus)Teacher backboneDistributed by Meta's DINOv3 (DINOv3 License, no redistribution). The official implementation is fetched into ~/.cache/torch/hub on first run
vitt_distill.ptInitial weights of the ViT-T/16 studentImageNet-pretrained weights distributed with DEIMv2 (Apache-2.0)
PPHGNetV2_B0_stage1.pthInitial backbone weights of the CNN studentImageNet-pretrained weights distributed with DEIMv2 (Apache-2.0)
data/models/deimv2_*wholebody49*.onnxPseudo labels for head boxes and face parts (DEIMv2-Wholebody49)Apache-2.0
data/models/depth_anything_v2_small.onnx, sixdrepnet360_*.onnxDepth-reprojection synthesis; pose QA and stratificationFollow each distributor's license

4.2 Data (data/ → datasets/unified/)

300W-LP, WFLW, 300W and COFW are distributed for research use and are not bundled. Place them under data/ and convert them into the unified format (the 300W-LP conversion includes a coordinate offset correction).

uv run python -m hrffa.dataset.convert --source all --data-root data --out datasets/unified
# run DEIMv2 for head boxes / face parts (cached)
uv run python -m hrffa.dataset.pseudolabel.run   ...
# apply the cache to the unified annotations
uv run python -m hrffa.dataset.pseudolabel.apply ...
uv run python -m hrffa.dataset.materialize --source all

See --help of each CLI for the arguments.

In addition, I added a large volume of synthetic datasets I created myself to significantly compensate for distributional weaknesses in the publicly available datasets. Everything is mechanically composited using gpt-image-2, combining non-existent people and backgrounds. The GT automatically generated values ​​that appear largely correct based on statistical data. 8,000 images were automatically generated.

grid_3x3

5. Training, evaluation and export

# Teacher (DINOv3 ViT-L/16 @320, WSD, 200 epochs)
uv run python -m hrffa.train.train_teacher --preset clean_v3

# Students (online distillation from teacher clean_v3)
## ViT-T @256
uv run python -m hrffa.train.distill_student --preset student_s256_96gb_r2
## CNN @256
uv run python -m hrffa.train.distill_student --preset student_hg0_wsd
## ViT-T @96 (fine-tuned from the 256 model)
uv run python -m hrffa.train.distill_student --preset student_s096_96gb_r2
# To resume, or to extend the constant-LR phase: edit epochs in the preset and
# add --resume runs/<preset>/<preset>_last.pt

# Evaluation (4 instruments)
B=runs/student_s256_96gb_r2/student_s256_96gb_r2_best_e0449_0.007970.pt
uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--official

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--stratify-real

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--pose-stress \
--stress-n 300

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--style-shift \
--style-n 300

# ONNX (fixed batch-1 graph + N-batch variant;
#  parity and N-batch agreement verified with onnxruntime)
uv run python -m hrffa.export.export_onnx \
--ckpt $B \
--preset student_s256_96gb_r2 \
--scheme ibug68 \
--output runs/student_s256_96gb_r2/student_s256_96gb_r2_e449_ibug68.onnx

# Results tables (history/050 format)
uv run python scripts/make_results_tables.py > tables.md

Training logs are written to runs/<preset>/train_log_<preset>.jsonl (validation metrics per epoch); the best checkpoint is runs/<preset>/<preset>_best_eXXXX_<val>.pt and the resume checkpoint is <preset>_last.pt. scripts/run_phase_a.sh runs the student arms of Phase A back to back.

ONNX demo (demo/demo_hrffa_onnx.py)

A self-contained two-stage demo (only numpy, opencv and onnxruntime are needed): DEIMv2-Wholebody49 detects heads (class 7), each head is cropped exactly as in training / evaluation (square crop of the longer bbox side × 1.1, --crop_pad 0.05), and the HRFFA graph predicts the landmarks, which are mapped back to the image. Both models are switchable; the normalization of the alignment model is inferred from its file name (vitl → ImageNet mean/std, otherwise center05) and can be forced with --input_norm. Fixed batch-1 and N-batch graphs are both accepted (an N-batch graph processes all heads of a frame in one run).

# images (default models: DEIMv2 dinov3_s wholebody49 + hrffa_vitt_ibug68_1x3x256x256)
uv run python demo/demo_hrffa_onnx.py \
-i images_dir \
-o output_dir

# camera 0 / video file, CUDA EP, other alignment models
uv run python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d cuda

uv run --no-group ort --group tensorrt \
python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d tensorrt \
--head_score_threshold 0.70

uv run python demo/demo_hrffa_onnx.py \
-v input.mp4 \
-o output_dir \
-am data/models/hrffa_hg0_ibug68_Nx3x96x96.onnx

uv run python demo/demo_hrffa_onnx.py \
-i images_dir \
-o output_dir \
-dm data/models/deimv2_hgnetv2_n_wholebody49_ins_s08_maskhead64x2_center_1240query_masks.onnx \
-am data/models/hrffa_vitl_ibug68_1x3x320x320.onnx

Both demos can also estimate the head yaw with YawNet, an original lightweight CNN (SynthYaw project; it borrows only the biternion output representation and the von-Mises loss from the BiternionNet paper, §8.3 — the architecture is our own). The default data/models/yawnet_distill_064_unified_v6u_kappa_1x3x64x64.onnx is a YawNet-64 student distilled from a DINOv3 ViT-L teacher; it also outputs kappa (the von-Mises concentration = confidence, unused by the demos). Preprocessing: direct 64×64 resize of the head crop, RGB, center05 normalization ((x/255)−0.5)/0.5 → unit (cos θ, sin θ), θ = atan2(sin, cos); the yawpose convention (+90° = viewer-left) is mirrored to the display convention: 0° = facing the camera (image bottom), 90° = image right, 180° = facing away, 270° = image left. It is shown as a ring in the top-right corner for the largest head (-om/--orientation_model, --disable_orientation, --disable_orientation_ring, --orientation_smooth_tau — a circular EMA on the (cos θ, sin θ) vector smoothing the ring on video / camera input, display only; default 0 = disabled, e.g. 0.15 to enable — and key r; the raw orientation_deg is added to the JSON). Camera input is opened at VGA (640×480) in both demos. Options: -d cpu|cuda|tensorrt (default: CUDA if available), --inference_type auto|fp16|bf16|int8 for TensorRT (auto, the default, picks BF16 when onnxruntime-gpu ≥ 1.25 — the tensorrt dependency group — runs on an Ampere or newer GPU and FP16 otherwise, printing a recommendation to install ≥ 1.25), --trt_cache_dir data/models/trt_cache (TensorRT engines are cached per onnxruntime / TensorRT version, precision and GPU under ort-<ver>_trt-<ver>_<precision>_sm<cc>/; caches built with another onnxruntime version are deleted at startup so engines are always rebuilt after an upgrade), --ort_log_level verbose|info|warning|error|fatal (onnxruntime's global logger; the default error hides the TensorRT engine-build warnings such as Make sure input ... has Int64 binding and Could not read timing cache), --head_score_threshold 0.5, --draw_lines (ibug68 contour lines), --disable_bbox, --point_radius, --save_raw_predictions (per-image / per-frame JSON with head boxes, points and the 3-class visibility), --disable_imshow, --disable_video_writer. Keys for video / camera input: ESC quit, b toggle head boxes, l toggle contour lines, r toggle the orientation ring. Rendering follows the project convention: predictions only, single color, no visibility color coding.

Web demo (demo/web/, Electron + onnxruntime-web on WebGPU / WASM)

The same two-stage pipeline entirely in the browser engine, derived from the web runtime of PINTO0309/soma: Electron + Vite + React + TypeScript with onnxruntime-web 1.27.0 (WebGPU execution provider, WASM fallback). Inference runs in a dedicated Web Worker by default (--web-inference-worker main runs the engines on the UI thread instead), the input is a webcam (VGA), a video file or an image file, and the crop geometry and normalization are identical to the Python demo. The Electron shell (electron/main.ts) carries the WebGPU command-line switches of the reference (ignore-gpu-blocklist, enable-zero-copy, disable-gpu-sandbox, enable-unsafe-webgpu, enable-webgpu-developer-features, enable-features=Vulkan,WebGPU,WebGPUService,SharedArrayBuffer, use-webgpu-adapter=default), injects the COOP / COEP isolation headers and grants camera access. The page also runs in a plain Chrome / Edge 113+ (pnpm run dev:browser); every asset (page, wasm runtime, models) is served from the app's own origin — no CDN.

# DEIMv2 detectors must be rewritten for onnxruntime-web first (double -> float32, negative axes
# normalized, 1-D MatMul operands made 2-D, IsNaN / IsInf expressed with Equal / Abs / Greater so the
# decoder stays on the GPU, unreachable nodes pruned) and are passed through the same
# optimization pipeline as the HRFFA exports (onnxslim without Gemm fusion -> onnxsim without optimization
# passes -> rank-5 qkv rewrite of the ViT attention -> canonicalized fixed graph); every output is verified
# against the original with onnxruntime (graph optimizations off).
# Output names are shortened and tagged, e.g. deimv2_dinov3_s_wholebody49_boxes_only_webgpu.onnx. Only the
# boxes-only graphs are staged and listed (masks output removed with hrffa.dataset.pseudolabel.make_boxes_only,
# mask head pruned): {hgnetv2_n, dinov3_s, dinov3_x}; the UI defaults to the MIT-licensed
# yolov9_t_wholebody34_0100_1x3x640x640.onnx (see below). The legacy data/models/deimv2_wholebody49_boxes_only.onnx
# (X backbone) is published as deimv2_dinov3_x_wholebody49_boxes_only_webgpu.onnx:
uv run python scripts/onnx_web_compat.py data/models/deimv2_*_boxes_only.onnx --out-dir demo/web/models

cd demo/web
corepack enable pnpm     # Node >= 22; the project pins pnpm@10.33.0 via "packageManager"
pnpm install             # pnpm only (npm / yarn are refused); stages models + wasm, provisions the electron binary
pnpm run fetch:models    # no local models? download the ONNX files from the `weights` release into demo/web/models/
                         # (every file is checked against the SHA-256 pinned in models.lock.json), then pnpm run prepare:assets
pnpm run dev -- --web-inference-worker main   # engines on the UI thread instead of the worker
pnpm run dev -- --devtools                    # open DevTools (detached); closed by default, View > Toggle Developer Tools also works
pnpm run dev             # vite dev server + electron window (dedicated inference worker)
pnpm run start           # production build (dist/ + dist-electron/) and launch electron from file://
pnpm run dev:browser     # vite only: open http://localhost:5274 in Chrome / Edge

The detector slot also accepts the raw-head YOLOv9-Wholebody34 exports of PINTO_model_zoo/455_YOLOv9-Wholebody34 — these are the MIT-licensed YOLOv9 models trained with PINTO0309/YOLO (an MIT re-implementation of YOLOv9 / YOLOv7 / YOLO-RD), not the GPL-3.0 official YOLOv9 code, so they can be redistributed with this demo (yolov9_{n,t}_wholebody34_0100_1x3x640x640.onnx, output [1, 4+34, 8400], class 7 = head; both are on the weights release and fetched by fetch:models; yolov9_t is the default detector of the web demo): they are staged as they are — the score threshold and a greedy NMS (IoU 0.5) run in the app, no graph rewrite is needed (≈ 22 ms per frame for yolov9_n / yolov9_t on the RTX 3070 with WebGPU). All ONNX files of the demo (the rewritten deimv2_*_boxes_only_webgpu.onnx detectors, the YOLOv9 exports and the hrffa_* graphs) are published on the weights release; scripts/fetch-models.mjs downloads them and refuses any file whose SHA-256 differs from models.lock.json. scripts/prepare-assets.mjs (run by pnpm install, dev and build) stages demo/web/models/deimv2_*_boxes_only_webgpu.onnx (the rewritten detectors; masks variants are neither staged nor listed), the YOLOv9 exports, the optional yawnet_distill_{064,096,128}_unified_v6u_kappa head-orientation models (from demo/web/models/ or data/models/; not part of the weights release, so place the file there yourself; selectable in the UI and drawn as a top-right ring; the Smooth the ring checkbox — off by default — smooths it with a circular EMA, τ = 150 ms, on video / camera input) and data/models/hrffa_*.onnx (the HRFFA graphs run unmodified; files above 300 MB such as the ViT-L teacher are skipped unless HRFFA_WEB_INCLUDE_LARGE=1), and copies the onnxruntime-web wasm files next to them. URL parameters: ?backend=wasm|webgpu, ?worker=dedicated|main, ?detector=<file>, ?aligner=<file>, ?orientation=<file>|off, ?image=<same-origin URL> or ?video=<same-origin URL> with &autostart=1.

Measured in Chrome 151 / Electron 43 on an RTX 3070 (WebGPU EP, second run on a still image; WASM = single thread): DEIMv2 boxes-only detectors ≈ 60–65 ms per frame in the app including preprocessing (dinov3_s graph alone ≈ 50 ms; after the rewrites only the 9 int64 index ops of the post-processor remain on the CPU, the rest of the graph runs on WebGPU), vitt-256 ≈ 20 ms per head (N-batch: 9 heads in 144 ms), hg0-256 ≈ 10 ms per head (9 heads in 46 ms); WASM: hgnetv2_n ≈ 610 ms, hg0-256 ≈ 55 ms per head.

Supply-chain hardening (same policy as the reference project this demo is derived from): every dependency is pinned to an exact version and pnpm-lock.yaml is committed with integrity hashes (the resolution was taken over unchanged from the reference project — the lockfile is a strict subset of it); minimumReleaseAge: 10080 in pnpm-workspace.yaml refuses any package version published less than 7 days ago; dependency build scripts are blocked except electron (official binary download verified against SHASUMS256) and esbuild; installs with npm / yarn are refused by a preinstall guard; the page ships a strict Content-Security-Policy and loads nothing from third-party hosts.

Configuration (configs/*.yaml)

Each preset stores only the differences from the defaults of TrainConfig (src/hrffa/train/config.py). _base_: inherits another preset (chained, last-wins merge, cycle detection; unknown keys are errors).

PresetContents
clean_v3Deployment teacher (DINOv3 ViT-L/16 @320, initialized from clean_v2, WSD 200)
student_s_base → student_s256_96gbShared student settings (teacher clean_v3, crop pad 0.05, KD 0.5/0.5/0.2, WSD 250/50, EMA 0.999)
student_s256_96gb_r2ViT-T @256, second round from the old A1 best (WSD 450)
student_s096_96gb_r2ViT-T @96, fine-tuned from the r2 best
student_hg0_wsd / student_hg0_s096_wsdCNN student @256 / @96
abl_*Ablations
smoke_s_8gbSmoke test on an 8 GB GPU

6. Repository layout

configs/            training presets (YAML with _base_ inheritance)
src/hrffa/
  dataset/          unified-dataset conversion, pseudo labeling, augmentation, QA tools (converters / pseudolabel / augment / qa / selftrain)
  data/             Dataset classes for training and evaluation (apply geometric / photometric augmentation)
  model/            teacher.py (the model), vit_tiny.py, hgnetv2.py, export_modules.py (export-time MHA), popos.py, losses.py
  train/            train_teacher.py, distill_student.py, evaluate.py, config.py
  export/           export_onnx.py, nbatch.py (fixed batch-1 → N-batch conversion and graph rewrites)
scripts/            run_phase_a.sh (run the student arms back to back), make_results_tables.py, onnx_web_compat.py (DEIMv2 graphs -> onnxruntime-web)
demo/               demo_hrffa_onnx.py (ONNX demo: DEIMv2-Wholebody49 head detection + HRFFA landmarks; images / video / camera)
  web/              Electron / browser demo (Vite + React + onnxruntime-web, WebGPU / WASM, dedicated worker; pinned pnpm lockfile)
tests/              unit tests (model, export, N-batch conversion, dataset conversion)
history/            050_results_tables.md (results tables) and assets/050/ (README figures)
ckpts/ data/ datasets/ runs/ onnx/   weights, raw data, unified data and training outputs (not tracked by git)

7. License

  • Code: MIT License (Copyright (c) 2026 Katsuya Hyodo).
  • The datasets (300W-LP / WFLW / 300W / COFW) and the generated / self-training data are not bundled; follow their research-use distribution terms. The unified format carries a per-record license_tag so that records can be filtered by license.
  • Weights: DINOv3 (Meta, DINOv3 License), vitt_distill and PP-HGNetV2 (DEIMv2, Apache-2.0) are neither bundled nor redistributed. Check the derived-work terms of these sources before distributing trained HRFFA weights or ONNX files.

8. Citation and acknowledgements

8.1 Citing this repository

@software{hyodo2026hrffa,
  author    = {Katsuya Hyodo},
  title     = {HRFFA: High-Angle Robust Fast FaceAlignment},
  month     = {aug},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.22161811},
  url       = {https://doi.org/10.5281/zenodo.22161811},
}

8.2 Backbones, pretrained weights and tools

DINOv3 — teacher backbone (ViT-L/16). The official implementation is imported at runtime from the torch.hub cache and the weights are distributed under the DINOv3 License. Paper: https://arxiv.org/abs/2508.10104 · Code: https://github.com/facebookresearch/dinov3

@misc{simeoni2025dinov3,
  title         = {{DINOv3}},
  author        = {Sim{\'e}oni, Oriane and Vo, Huy V. and Seitzer, Maximilian and Baldassarre, Federico and Oquab, Maxime and Jose, Cijo and Khalidov, Vasil and Szafraniec, Marc and Yi, Seungeun and Ramamonjisoa, Micha{\"e}l and Massa, Francisco and Haziza, Daniel and Wehrstedt, Luca and Wang, Jianyuan and Darcet, Timoth{\'e}e and Moutakanni, Th{\'e}o and Sentana, Leonel and Roberts, Claire and Vedaldi, Andrea and Tolan, Jamie and Brandt, John and Couprie, Camille and Mairal, Julien and J{\'e}gou, Herv{\'e} and Labatut, Patrick and Bojanowski, Piotr},
  year          = {2025},
  eprint        = {2508.10104},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CV},
  url           = {https://arxiv.org/abs/2508.10104}
}

DEIMv2 — source of the ImageNet-pretrained PP-HGNetV2-B0 weights used by the CNN student (Apache-2.0). Paper: https://arxiv.org/abs/2509.20787 · Code: https://github.com/Intellindust-AI-Lab/DEIMv2

@article{huang2025deimv2,
  title   = {Real-Time Object Detection Meets {DINOv3}},
  author  = {Huang, Shihua and Hou, Yongjie and Liu, Longfei and Yu, Xuanlong and Shen, Xi},
  journal = {arXiv preprint arXiv:2509.20787},
  year    = {2025},
  url     = {https://arxiv.org/abs/2509.20787}
}

DEIMv2-Wholebody49 — pseudo labels for head boxes and face parts (Apache-2.0). https://github.com/PINTO0309/PINTO_model_zoo/tree/main/488_DEIMv2-Wholebody49

@software{DEIMv2-Wholebody49,
  author = {Katsuya Hyodo},
  title  = {Unified multi-task model for detection, pose estimation, and instance segmentation. 49 classes.},
  url    = {https://github.com/PINTO0309/PINTO_model_zoo/tree/main/488_DEIMv2-Wholebody49},
  year   = {2026},
  month  = {05},
  doi    = {10.5281/zenodo.10229410}
}

YOLOv9-Wholebody34 (MIT-licensed YOLOv9) — optional head detector of the web demo (yolov9_{n,t}_wholebody34_0100_1x3x640x640.onnx, PINTO_model_zoo #455). The models are trained and exported with PINTO0309/YOLO, an MIT-licensed re-implementation of YOLOv9 / YOLOv7 / YOLO-RD; the GPL-3.0 official YOLOv9 code is not used. Original paper: https://arxiv.org/abs/2402.13616

@software{PINTO0309_YOLO,
  author = {Katsuya Hyodo},
  title  = {An {MIT} License of {YOLOv9}, {YOLOv7}, {YOLO-RD}},
  url    = {https://github.com/PINTO0309/YOLO},
  year   = {2025}
}

@software{YOLOv9-Wholebody34,
  author = {Katsuya Hyodo},
  title  = {{YOLOv9-Wholebody34}: whole-body 34-class detector trained with the {MIT}-licensed {YOLOv9}},
  url    = {https://github.com/PINTO0309/PINTO_model_zoo/tree/main/455_YOLOv9-Wholebody34},
  year   = {2025}
}

@article{wang2024yolov9,
  title   = {{YOLOv9}: Learning What You Want to Learn Using Programmable Gradient Information},
  author  = {Wang, Chien-Yao and Yeh, I-Hau and Liao, Hong-Yuan Mark},
  journal = {arXiv preprint arXiv:2402.13616},
  year    = {2024},
  url     = {https://arxiv.org/abs/2402.13616}
}

6DRepNet / 6DRepNet360 — head-pose estimator used for pose QA and for the pose-stratified evaluation (ONNX from PINTO_model_zoo #423). Papers: https://arxiv.org/abs/2202.12555 · https://arxiv.org/abs/2309.07654

@inproceedings{hempel2022sixdrepnet,
  title     = {{6D} Rotation Representation For Unconstrained Head Pose Estimation},
  author    = {Hempel, Thorsten and Abdelrahman, Ahmed A. and Al-Hamadi, Ayoub},
  booktitle = {IEEE International Conference on Image Processing (ICIP)},
  year      = {2022},
  url       = {https://arxiv.org/abs/2202.12555}
}

@article{hempel2023sixdrepnet360,
  title   = {Towards Robust and Unconstrained Full Range of Rotation Head Pose Estimation},
  author  = {Hempel, Thorsten and Abdelrahman, Ahmed A. and Al-Hamadi, Ayoub},
  journal = {arXiv preprint arXiv:2309.07654},
  year    = {2023},
  url     = {https://arxiv.org/abs/2309.07654}
}

Depth Anything V2 — monocular depth used for the depth-reprojection synthesis of extreme pitch (ONNX: onnx-community/depth-anything-v2-small, Apache-2.0). Paper: https://arxiv.org/abs/2406.09414

@inproceedings{yang2024depthanythingv2,
  title     = {Depth Anything {V2}},
  author    = {Yang, Lihe and Kang, Bingyi and Huang, Zilong and Zhao, Zhen and Xu, Xiaogang and Feng, Jiashi and Zhao, Hengshuang},
  booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
  year      = {2024},
  url       = {https://arxiv.org/abs/2406.09414}
}

8.3 Methods referenced (reimplemented from the papers; no code was copied)

D-ViT — the results tables in §1.1 follow the layout of its Tables 1 / 2; its published values are listed for format reference only. Paper: https://arxiv.org/abs/2411.07167

@inproceedings{dang2025dvit,
  title     = {Cascaded Dual Vision Transformer for Accurate Facial Landmark Detection},
  author    = {Dang, Ziqiang and Li, Jianfang and Liu, Lin},
  booktitle = {IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)},
  year      = {2025},
  url       = {https://arxiv.org/abs/2411.07167}
}

POPoS — distance-map + multilateration decoding (ablation arm D8). Paper: https://arxiv.org/abs/2410.09583

@inproceedings{xiang2025popos,
  title     = {{POPoS}: Improving Efficient and Robust Facial Landmark Detection with Parallel Optimal Position Search},
  author    = {Xiang, Chong-Yang and He, Jun-Yan and Cheng, Zhi-Qi and Wu, Xiao and Hua, Xian-Sheng},
  booktitle = {Proceedings of the AAAI Conference on Artificial Intelligence (AAAI)},
  year      = {2025},
  url       = {https://arxiv.org/abs/2410.09583}
}

LESA — local term added in parallel to self-attention (ablation arm D7). Paper: https://arxiv.org/abs/2107.05637

@article{yang2021lesa,
  title   = {Locally Enhanced Self-Attention: Combining Self-Attention and Convolution as Local and Context Terms},
  author  = {Yang, Chenglin and Qiao, Siyuan and Kortylewski, Adam and Yuille, Alan},
  journal = {arXiv preprint arXiv:2107.05637},
  year    = {2021},
  url     = {https://arxiv.org/abs/2107.05637}
}

BiternionNet — YawNet, the head-yaw model of the §5 demos, is an original architecture that borrows only this paper's biternion output representation (unit (cos θ, sin θ) regression) and von-Mises loss. Paper: https://lucasb.eyer.be/academic/biternions/biternions_gcpr15.pdf, original implementation (MIT): https://github.com/lucasb-eyer/BiternionNet

@inproceedings{beyer2015biternion,
  title     = {Biternion Nets: Continuous Head Pose Regression from Discrete Training Labels},
  author    = {Beyer, Lucas and Hermans, Alexander and Leibe, Bastian},
  booktitle = {German Conference on Pattern Recognition (GCPR)},
  year      = {2015},
  url       = {https://lucasb.eyer.be/academic/biternions/biternions_gcpr15.pdf}
}

PersonViT — the fixed batch-1 → N-batch ONNX conversion follows the approach of its export_onnx.py. https://github.com/PINTO0309/PersonViT

8.4 Datasets

300W-LP (used with a coordinate offset correction; also the source of the unleaked 300wlp_val set). Paper: https://arxiv.org/abs/1511.07212

@inproceedings{zhu2016face,
  title     = {Face Alignment Across Large Poses: A {3D} Solution},
  author    = {Zhu, Xiangyu and Lei, Zhen and Liu, Xiaoming and Shi, Hailin and Li, Stan Z.},
  booktitle = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
  year      = {2016},
  url       = {https://arxiv.org/abs/1511.07212}
}

WFLW. Paper: https://arxiv.org/abs/1805.10483

@inproceedings{wu2018wflw,
  title     = {Look at Boundary: A Boundary-Aware Face Alignment Algorithm},
  author    = {Wu, Wayne and Qian, Chen and Yang, Shuo and Wang, Quan and Cai, Yici and Zhou, Qiang},
  booktitle = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
  year      = {2018},
  url       = {https://arxiv.org/abs/1805.10483}
}

300W. https://ibug.doc.ic.ac.uk/resources/300-W/

@inproceedings{sagonas2013300w,
  title     = {300 Faces in-the-Wild Challenge: The First Facial Landmark Localization Challenge},
  author    = {Sagonas, Christos and Tzimiropoulos, Georgios and Zafeiriou, Stefanos and Pantic, Maja},
  booktitle = {IEEE International Conference on Computer Vision Workshops (ICCVW)},
  year      = {2013}
}

COFW. http://www.vision.caltech.edu/xpburgos/ICCV13/

@inproceedings{burgosartizzu2013cofw,
  title     = {Robust Face Landmark Estimation under Occlusion},
  author    = {Burgos-Artizzu, Xavier P. and Perona, Pietro and Doll{\'a}r, Piotr},
  booktitle = {IEEE International Conference on Computer Vision (ICCV)},
  year      = {2013}
}

8.5 Acknowledgements

  • Meta AI for DINOv3, the DEIMv2 authors for the detector and the PP-HGNetV2 weights (originally from PaddlePaddle's PaddleClas / PaddleDetection), the authors of 6DRepNet360 and Depth Anything V2 for the tools used in data QA and synthesis, and the dataset authors of 300W-LP, WFLW, 300W and COFW.
  • The generated head-image pools used to reinforce extreme poses were produced with OpenAI's image generation API; the synthetic data is not redistributed.
  • The ONNX post-processing know-how (batch-axis preservation, N-batch derivation, rank-5 elimination) builds on the author's PersonViT export pipeline and the PINTO_model_zoo conventions.
face-alignment
facial-landmark-detection
head-alignment
head-detection

PINTO0309/High-Angle_Robust_Fast_FaceAlignment

HRFFA: High-Angle Robust Fast Face Alignment. Whole-head face alignment dataset & training pipeline.

Python

190

46 commits

updated Oct 9, 2026

See the code

README

HRFFA: High-Angle Robust Fast FaceAlignment

GitHub DOI Ask DeepWiki

A training, distillation and ONNX deployment pipeline for whole-head face alignment that stays robust at extreme head poses while remaining light enough for CPU inference.

  • Face alignment on head crops, not face crops. Conventional face-alignment papers assume a tightly cropped face region; this is a special-purpose model that works on a crop of the whole head instead. The architecture is therefore at a considerable disadvantage against paper benchmarks, but the intent is to take in more features by processing the entire head and the background context contained in a small margin at the same time. It is also the practical choice: object detectors localize whole heads far more stably than faces.
  • For head crops that include looking up / looking down (measured pitch beyond ±85°), in-plane rotation over the full 360° roll, and profile views (yaw ±90°), the models output 68 / 98 / 29 landmarks plus a 3-class visibility label per point (outside the image / occluded / visible).
  • The teacher is a DINOv3 ViT-L/16 (320×320); the students are a ViT-T/16 (256 and 96 input) and a PP-HGNetV2-B0 based CNN (256 and 96 input). Students are trained by online distillation from the teacher and exported to ONNX graphs that run in 1.5–12 ms on a CPU.

teacher clean_v3: extreme pitch/yaw tiles with roll 0..320 deg student vitt-256: measured pitch extremes with roll

Left: teacher vitl-320 on nine looking-up × profile tiles with a roll of 0–320° composited onto each tile.
Right: student vitt-256 on measured pitch extremes (+86° to −89°) with roll. Predictions only.

1. Results at a glance

inter-ocular NME %, lower is better; CPU latency on an i9-10900K with onnxruntime 1.22 CPU EP, batch 1.

Checkpoints and ONNX/TFLite(LiteRT): https://github.com/PINTO0309/High-Angle_Robust_Fast_FaceAlignment/releases/tag/weights

ModelInputParamsGMACsGFLOPsCPU msWFLW FullWFLW Pose300W FullCOFW
D-ViT (paper)25696.4M≈73.15≈146.30–3.756.432.854.13
vitl-320 (teacher)320308.2M131.32262.635201.512.541.031.21
vitt-2562569.0M2.054.0912.43.365.642.662.76
hg0-2562561.6M0.701.405.25.329.533.873.77
vitt-096969.0M0.430.854.65.148.653.863.90
hg0-096961.6M0.130.261.57.4614.714.774.99

How to read these numbers (important): all splits of the real-image datasets — including WFLW test, 300W and COFW test — are used for training. The "official" numbers above therefore measure how well the models fit the training distribution and must not be compared with published benchmark results. The only unleaked instrument is 300wlp_val (head-NME on a held-out 300W-LP subset; values in 050). The full tables — WFLW subsets, FR/AUC, breakdowns by pose (yaw / pitch / roll) and by image degradation, and the D-ViT paper values listed side by side with an explicit non-comparability note — follow in §1.1 (same content as 050 §2). The D-ViT (paper) row lists the published benchmark values of arXiv 2411.07167 (Tables 1 / 2, "Ours": 256×256 input, 8 prediction blocks = 96.4M parameters per its Table S1) for format reference only. The paper does not report FLOPs or latency; the GMACs / GFLOPs are our estimate from the official code with its default arguments (8 prediction blocks, 32×32 heatmaps, depth 256, 256×256 input, all 8 stages, batch 1, same FlopCounterMode method as the other rows) and are approximate because that configuration has 101.1M parameters, about 5 % more than Table S1. Its COFW NME is normalized by the inter-pupil distance instead of inter-ocular, and its test sets are held out from training, so no ranking against the fit measures above is implied.

1.1 Full tables

All tables below are generated from runs/<run>/eval_best_{official,stratreal,stress,style}.log by scripts/make_results_tables.py. Notes:

  • Absolute values are fit measures (all real-image splits are in the training data). The D-ViT (paper) rows in Tables 1 / 2 are the published test-benchmark values of arXiv 2411.07167 (p.6, "Ours") and are listed for format reference only — the conditions differ, so no ranking is implied.
  • Yaw / pitch / roll: the models do not output head pose, so pose robustness is expressed as landmark accuracy per pose. Table 3: inter-ocular NME per effective pose bin (6DRepNet-estimated pose plus the applied rotation, 3 sets × 300 images × 16 configurations). Table 4: NME per in-plane roll (exact 360° roll equivariance; a small worst−base means equivariant). Table 5: NME under projective camera pitch ±15/±25° and yaw ±15° perturbations. Table 6: head-NME (normalized by the crop side, not inter-ocular) of the three real sets stratified by 6DRepNet-estimated pose.
  • Table 7 perturbations are applied in crop pixels (mblur9/21 = 30° motion blur of 9/21 px, warm/cool = channel gains, gamma 0.6/1.6, gray, jpeg30 = JPEG quality 30). Their strength therefore depends on the input resolution: for the 96 models (vitt-096 / hg0-096) a 21 px blur on a 96 px crop corresponds to ~56 px at 256, so their Table 7 rows are not comparable with the 256 models.

Table 1. NME (%, inter-ocular, lower is better)

ModelWFLW
Full

Pose

Exp

Ill

Mu

Occ

Blur
COFW
Full
300W
Full

Comm

Chal
D-ViT (paper)3.756.433.854.063.574.474.374.132.852.434.56
vitl-3201.512.541.551.471.421.561.581.211.030.961.34
vitt-2563.365.643.433.253.193.563.602.762.662.363.90
hg0-2565.329.535.545.165.096.296.043.773.873.375.96
vitt-0965.148.655.324.965.065.525.403.903.853.575.02
hg0-0967.4614.717.777.198.108.918.164.994.774.246.91

Table 2. FR10 (%, lower is better) and AUC10 (%, higher is better) on WFLW

ModelFR10
Full

Pose

Exp

Ill

Mu

Occ

Blur
AUC10
Full

Pose

Exp

Ill

Mu

Occ

Blur
D-ViT (paper)1.768.281.271.291.943.802.0763.740.162.664.764.757.158.6
vitl-3200.000.000.000.000.000.000.0084.974.584.585.485.884.484.2
vitt-2560.281.530.320.000.000.540.5266.543.965.767.568.264.564.1
hg0-2567.0032.215.735.446.3113.049.4449.217.546.750.350.242.442.8
vitt-0964.2023.314.782.722.915.573.7549.720.247.751.249.946.247.1
hg0-09617.8867.4818.7914.1820.8726.0922.3836.15.831.937.634.428.730.0

Table 3. NME (%) by yaw / pitch bin (effective pose bins of pose-stress)

ModelYaw
0–30
Yaw
30–60
Yaw
60–95
Pitch
−95..−45
Pitch
−45..−15
Pitch
−15..15
Pitch
15..45
Pitch
45..95
vitl-3201.261.712.041.761.371.301.291.85
vitt-2562.944.104.684.073.203.062.994.43
hg0-2564.216.358.146.264.634.464.357.51
vitt-0964.496.217.375.934.824.774.547.06
hg0-0965.698.7010.858.026.176.215.7910.07
n5933112414322325032944139848

Table 4. NME (%) by in-plane roll (pose-stress, n=300 per set)

ModelSetRoll 04590135180225270315worst−base
vitl-320wflw1.611.611.641.601.631.611.631.60+0.02
300w1.121.111.141.111.131.111.131.11+0.01
cofw1.291.301.311.301.301.301.301.29+0.02
vitt-256wflw3.673.613.673.603.703.613.683.61+0.03
300w2.932.882.892.892.952.922.962.90+0.03
cofw3.012.963.022.972.982.962.982.97+0.01
hg0-256wflw5.965.955.906.065.926.035.985.96+0.10
300w4.144.114.264.204.264.294.214.16+0.15
cofw4.124.084.064.064.034.114.124.06+0.00
vitt-096wflw6.306.056.175.986.306.266.406.19+0.10
300w4.584.414.554.505.035.115.144.71+0.57
cofw4.744.614.724.494.734.494.564.55+0.00
hg0-096wflw8.899.039.099.218.989.339.908.72+1.01
300w5.575.615.826.076.406.596.055.95+1.03
cofw5.845.796.055.715.765.515.685.73+0.21

Table 5. NME (%) under camera pitch / yaw perturbation (pose-stress, n=300 per set)

ModelSetbasecam
pitch
−25


−15


+15


+25
cam
yaw
−15


+15


p+25,y+15


p−25,y−15
vitl-320wflw1.611.781.631.581.601.551.551.561.78
300w1.121.251.151.091.101.081.081.081.22
cofw1.291.361.291.261.291.231.241.251.35
vitt-256wflw3.673.843.673.523.483.523.503.443.94
300w2.933.072.932.812.752.782.812.733.03
cofw3.013.123.012.952.922.902.902.923.12
hg0-256wflw5.965.995.715.715.385.555.605.316.08
300w4.144.464.244.053.983.974.043.904.39
cofw4.124.274.114.023.963.983.973.994.23
vitt-096wflw6.305.945.835.975.515.785.765.306.05
300w4.584.494.394.414.104.244.344.064.41
cofw4.744.534.494.454.184.334.424.204.47
hg0-096wflw8.898.208.278.447.728.378.197.358.34
300w5.575.385.385.454.945.165.254.815.34
cofw5.845.625.575.675.285.355.535.135.48

Table 6. head-NME (×100) stratified by 6DRepNet-estimated pose (3,696 real images from the three sets)

ModelmeanYaw
0–30
Yaw
30–60
Yaw
60–95
Pitch
−90..−30
Pitch
−30..−10
Pitch
−10..10
Pitch
10..30
Pitch
30..90
vitl-3200.370.360.420.450.420.370.360.400.50
vitt-2560.850.810.981.010.980.850.820.931.13
hg0-2561.291.211.601.851.711.281.211.502.09
vitt-0961.271.221.461.581.481.271.221.381.71
hg0-0961.751.602.222.852.531.761.592.042.94
n2939672851431076213623232

Table 7. style-shift: NME (%) under image degradations (in parentheses: degradation vs clean, n=300 per set)

ModelSetcleanmblur9mblur21warmcoolgamma0.6gamma1.6grayjpeg30
vitl-320wflw_test1.491.57 (+5.5%)2.09 (+40.4%)1.51 (+1.4%)1.51 (+1.5%)1.50 (+1.1%)1.51 (+1.3%)1.60 (+7.8%)1.55 (+4.5%)
300w_valid1.041.13 (+8.6%)1.46 (+40.6%)1.06 (+2.0%)1.05 (+1.2%)1.06 (+1.8%)1.05 (+1.4%)1.09 (+4.9%)1.08 (+4.3%)
cofw_test1.211.27 (+5.3%)1.60 (+32.3%)1.22 (+1.3%)1.23 (+1.5%)1.22 (+1.1%)1.22 (+1.2%)1.27 (+5.0%)1.25 (+3.6%)
vitt-256wflw_test3.263.32 (+1.8%)4.60 (+41.1%)3.32 (+1.9%)3.33 (+2.1%)3.35 (+2.7%)3.36 (+3.0%)3.48 (+6.6%)3.36 (+3.1%)
300w_valid2.652.73 (+2.6%)3.66 (+37.7%)2.68 (+1.1%)2.69 (+1.3%)2.71 (+2.0%)2.72 (+2.3%)2.75 (+3.7%)2.73 (+2.9%)
cofw_test2.762.82 (+2.1%)3.51 (+27.2%)2.79 (+1.0%)2.79 (+1.1%)2.83 (+2.4%)2.81 (+1.7%)2.85 (+3.3%)2.81 (+1.8%)
hg0-256wflw_test5.085.28 (+3.9%)8.16 (+60.5%)5.14 (+1.2%)5.23 (+2.9%)5.13 (+0.9%)5.29 (+4.2%)5.41 (+6.4%)5.35 (+5.2%)
300w_valid3.853.88 (+0.8%)5.20 (+35.0%)3.90 (+1.2%)3.90 (+1.3%)3.88 (+0.7%)3.92 (+1.7%)3.95 (+2.5%)3.90 (+1.0%)
cofw_test3.793.90 (+2.9%)4.93 (+30.0%)3.83 (+1.1%)3.83 (+0.9%)3.82 (+0.8%)3.82 (+0.8%)3.91 (+3.2%)3.85 (+1.5%)
vitt-096wflw_test5.016.26 (+25.0%)24.80 (+395.2%)5.10 (+1.9%)5.09 (+1.6%)5.13 (+2.4%)5.21 (+4.1%)5.37 (+7.3%)5.36 (+7.0%)
300w_valid3.824.42 (+15.8%)18.31 (+379.4%)3.86 (+1.1%)3.86 (+1.1%)3.90 (+2.1%)3.94 (+3.1%)3.96 (+3.8%)4.04 (+5.8%)
cofw_test3.894.52 (+16.3%)15.95 (+310.4%)3.95 (+1.8%)3.95 (+1.6%)3.99 (+2.8%)4.00 (+2.8%)4.05 (+4.2%)4.08 (+5.0%)
hg0-096wflw_test7.019.18 (+31.0%)32.00 (+356.7%)7.15 (+2.0%)7.30 (+4.2%)7.11 (+1.5%)7.36 (+5.0%)7.77 (+10.9%)9.10 (+29.9%)
300w_valid4.645.68 (+22.5%)19.79 (+326.5%)4.68 (+1.0%)4.74 (+2.1%)4.70 (+1.2%)4.72 (+1.7%)4.80 (+3.5%)5.60 (+20.8%)
cofw_test5.035.71 (+13.7%)18.03 (+258.9%)5.07 (+0.8%)5.15 (+2.4%)5.04 (+0.3%)5.16 (+2.6%)5.19 (+3.2%)5.74 (+14.2%)

2. Features

  • Unified dataset: 300W-LP / WFLW / 300W / COFW converted into a single JSONL format (head bbox, visibility, pose, and a per-record license_tag). Head boxes and face parts come from DEIMv2-Wholebody49 (Apache-2.0) pseudo labels; extreme poses are reinforced with depth-reprojection synthesis and generated image pools.
  • Geometric augmentation composed into a single homography: full 360° roll, camera pitch ±25° / yaw ±15°, horizontal flip (with left/right index swap), scale and translation are applied in one warp, and the GT coordinates, visibility and rotation matrix follow exactly. Photometric augmentation (brightness, gamma, grayscale, noise, blur, JPEG quality 35–85, motion blur) is applied on the training side as well.
  • Online teacher → student distillation: the same augmented crop is rendered at 320 for the teacher and at 256 (or 96) for the student; coordinate, visibility and decoder-token KD are combined with the GT losses. The learning-rate schedule is WSD (warmup → constant → cosine decay over the last epochs); the stable phase can be extended by editing epochs and resuming.
  • Evaluation instruments (hrffa.train.evaluate): official (inter-ocular NME / FR@0.1 / AUC@0.1), stratify-real (stratified by 6DRepNet-estimated pose), pose-stress (real images + exact GT geometric transforms to measure equivariance under 360° roll and camera pose), and style-shift (motion blur, color temperature, gamma, grayscale, JPEG).
  • ONNX export (hrffa.export.export_onnx): a static batch-1 graph is optimized with onnxslim → onnxsim (without Gemm fusion), checked for parity against PyTorch, and an N-batch variant (<stem>_n.onnx) is derived from it. The batch axis stays the leading axis of every Reshape and no rank-5 tensors are produced.

3. Models and the ONNX I/O contract

Teacher vitl-320Students vitt-256 / vitt-096Students hg0-256 / hg0-096
BackboneDINOv3 ViT-L/16 (the official implementation is imported at runtime from the torch.hub cache; its code is not vendored)ViT-T/16 (own implementation with RoPE, initialized from ckpts/vitt_distill.pt)PP-HGNetV2-B0 stages 0–2 (own implementation) + FPN (stride 8)
HeadPoint-query Transformer decoder (d256, 4 layers) with coordinate regression and visibilitySame (d256, 3 layers)Same (d128, 2 layers, 4 heads)
Input normalizationImageNet mean/stdcenter05 (x/255 − 0.5)/0.5 (no normalization op inside the ONNX graph)center05, folded into the stem conv (no input op at all)
Presetclean_v3student_s256_96gb_r2 / student_s096_96gb_r2student_hg0_wsd / student_hg0_s096_wsd

ONNX (example for --scheme ibug68; wflw98 / cofw29 are exported as separate files):

# RGB, normalized as above. S = 320 / 256 / 96 (the preset's out_size)
input   images      float32 [N, 3, S, S]
# coordinates relative to the crop (0..1 inside the crop; points outside still get coordinates)
output  points      float32 [N, K, 2]
# visibility logits (0 = outside the image, 1 = occluded, 2 = visible)
output  vis_logits  float32 [N, K, 3]
  • Crop: a square around the head bbox with a margin of 0.05 of the side length, rendered at S×S (the same geometry as in training and evaluation). The downscaling method (bilinear / area, with or without antialiasing) changes the results by less than ±1%.
  • Two files are written: the fixed batch-1 *.onnx and *_n.onnx with a symbolic batch axis N. Both are verified with onnxruntime to agree at batch sizes 1 / 2 / 3.
  • Head pose (rotation / roll_bit) is not an output. Pose supervision is not used in training; derive pose with an add-on head or PnP if needed.

4. Setup

Prerequisites: Linux, Python 3.13 (pinned in .python-version), uv, and an NVIDIA GPU (torch built for CUDA 12.8; an 8 GB GPU is enough for smoke tests, full training targets a 96 GB class GPU). Dependencies are pinned with == in pyproject.toml and hash-locked in uv.lock.

git clone https://github.com/PINTO0309/High-Angle_Robust_Fast_FaceAlignment.git
cd High-Angle_Robust_Fast_FaceAlignment
uv sync --frozen                                   # default: onnxruntime-gpu 1.22.0 (the validated toolchain)
uv sync --frozen --no-group ort --group tensorrt   # alternative: onnxruntime-gpu 1.26.0 for TensorRT BF16 (trt_bf16_enable)
# NOTE: `uv run` re-syncs the environment with the DEFAULT groups before running, i.e. a plain
# `uv run python ...` silently switches back to 1.22.0. Keep the same group flags on `uv run`
# (or use `uv run --no-sync` / `.venv/bin/python` after the manual sync):
uv run --no-group ort --group tensorrt \
python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d tensorrt

uv run python -m unittest tests/test_model_arms.py

4.1 Weights (ckpts/, none of them are bundled)

FilePurposeSource / license
dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth (also vits16 / vitb16 / vith16plus)Teacher backboneDistributed by Meta's DINOv3 (DINOv3 License, no redistribution). The official implementation is fetched into ~/.cache/torch/hub on first run
vitt_distill.ptInitial weights of the ViT-T/16 studentImageNet-pretrained weights distributed with DEIMv2 (Apache-2.0)
PPHGNetV2_B0_stage1.pthInitial backbone weights of the CNN studentImageNet-pretrained weights distributed with DEIMv2 (Apache-2.0)
data/models/deimv2_*wholebody49*.onnxPseudo labels for head boxes and face parts (DEIMv2-Wholebody49)Apache-2.0
data/models/depth_anything_v2_small.onnx, sixdrepnet360_*.onnxDepth-reprojection synthesis; pose QA and stratificationFollow each distributor's license

4.2 Data (data/ → datasets/unified/)

300W-LP, WFLW, 300W and COFW are distributed for research use and are not bundled. Place them under data/ and convert them into the unified format (the 300W-LP conversion includes a coordinate offset correction).

uv run python -m hrffa.dataset.convert --source all --data-root data --out datasets/unified
# run DEIMv2 for head boxes / face parts (cached)
uv run python -m hrffa.dataset.pseudolabel.run   ...
# apply the cache to the unified annotations
uv run python -m hrffa.dataset.pseudolabel.apply ...
uv run python -m hrffa.dataset.materialize --source all

See --help of each CLI for the arguments.

In addition, I added a large volume of synthetic datasets I created myself to significantly compensate for distributional weaknesses in the publicly available datasets. Everything is mechanically composited using gpt-image-2, combining non-existent people and backgrounds. The GT automatically generated values ​​that appear largely correct based on statistical data. 8,000 images were automatically generated.

grid_3x3

5. Training, evaluation and export

# Teacher (DINOv3 ViT-L/16 @320, WSD, 200 epochs)
uv run python -m hrffa.train.train_teacher --preset clean_v3

# Students (online distillation from teacher clean_v3)
## ViT-T @256
uv run python -m hrffa.train.distill_student --preset student_s256_96gb_r2
## CNN @256
uv run python -m hrffa.train.distill_student --preset student_hg0_wsd
## ViT-T @96 (fine-tuned from the 256 model)
uv run python -m hrffa.train.distill_student --preset student_s096_96gb_r2
# To resume, or to extend the constant-LR phase: edit epochs in the preset and
# add --resume runs/<preset>/<preset>_last.pt

# Evaluation (4 instruments)
B=runs/student_s256_96gb_r2/student_s256_96gb_r2_best_e0449_0.007970.pt
uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--official

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--stratify-real

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--pose-stress \
--stress-n 300

uv run python -m hrffa.train.evaluate \
--ckpt $B \
--preset student_s256_96gb_r2 \
--use-ema \
--style-shift \
--style-n 300

# ONNX (fixed batch-1 graph + N-batch variant;
#  parity and N-batch agreement verified with onnxruntime)
uv run python -m hrffa.export.export_onnx \
--ckpt $B \
--preset student_s256_96gb_r2 \
--scheme ibug68 \
--output runs/student_s256_96gb_r2/student_s256_96gb_r2_e449_ibug68.onnx

# Results tables (history/050 format)
uv run python scripts/make_results_tables.py > tables.md

Training logs are written to runs/<preset>/train_log_<preset>.jsonl (validation metrics per epoch); the best checkpoint is runs/<preset>/<preset>_best_eXXXX_<val>.pt and the resume checkpoint is <preset>_last.pt. scripts/run_phase_a.sh runs the student arms of Phase A back to back.

ONNX demo (demo/demo_hrffa_onnx.py)

A self-contained two-stage demo (only numpy, opencv and onnxruntime are needed): DEIMv2-Wholebody49 detects heads (class 7), each head is cropped exactly as in training / evaluation (square crop of the longer bbox side × 1.1, --crop_pad 0.05), and the HRFFA graph predicts the landmarks, which are mapped back to the image. Both models are switchable; the normalization of the alignment model is inferred from its file name (vitl → ImageNet mean/std, otherwise center05) and can be forced with --input_norm. Fixed batch-1 and N-batch graphs are both accepted (an N-batch graph processes all heads of a frame in one run).

# images (default models: DEIMv2 dinov3_s wholebody49 + hrffa_vitt_ibug68_1x3x256x256)
uv run python demo/demo_hrffa_onnx.py \
-i images_dir \
-o output_dir

# camera 0 / video file, CUDA EP, other alignment models
uv run python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d cuda

uv run --no-group ort --group tensorrt \
python demo/demo_hrffa_onnx.py \
-v 0 \
-o output_dir \
-d tensorrt \
--head_score_threshold 0.70

uv run python demo/demo_hrffa_onnx.py \
-v input.mp4 \
-o output_dir \
-am data/models/hrffa_hg0_ibug68_Nx3x96x96.onnx

uv run python demo/demo_hrffa_onnx.py \
-i images_dir \
-o output_dir \
-dm data/models/deimv2_hgnetv2_n_wholebody49_ins_s08_maskhead64x2_center_1240query_masks.onnx \
-am data/models/hrffa_vitl_ibug68_1x3x320x320.onnx

Both demos can also estimate the head yaw with YawNet, an original lightweight CNN (SynthYaw project; it borrows only the biternion output representation and the von-Mises loss from the BiternionNet paper, §8.3 — the architecture is our own). The default data/models/yawnet_distill_064_unified_v6u_kappa_1x3x64x64.onnx is a YawNet-64 student distilled from a DINOv3 ViT-L teacher; it also outputs kappa (the von-Mises concentration = confidence, unused by the demos). Preprocessing: direct 64×64 resize of the head crop, RGB, center05 normalization ((x/255)−0.5)/0.5 → unit (cos θ, sin θ), θ = atan2(sin, cos); the yawpose convention (+90° = viewer-left) is mirrored to the display convention: 0° = facing the camera (image bottom), 90° = image right, 180° = facing away, 270° = image left. It is shown as a ring in the top-right corner for the largest head (-om/--orientation_model, --disable_orientation, --disable_orientation_ring, --orientation_smooth_tau — a circular EMA on the (cos θ, sin θ) vector smoothing the ring on video / camera input, display only; default 0 = disabled, e.g. 0.15 to enable — and key r; the raw orientation_deg is added to the JSON). Camera input is opened at VGA (640×480) in both demos. Options: -d cpu|cuda|tensorrt (default: CUDA if available), --inference_type auto|fp16|bf16|int8 for TensorRT (auto, the default, picks BF16 when onnxruntime-gpu ≥ 1.25 — the tensorrt dependency group — runs on an Ampere or newer GPU and FP16 otherwise, printing a recommendation to install ≥ 1.25), --trt_cache_dir data/models/trt_cache (TensorRT engines are cached per onnxruntime / TensorRT version, precision and GPU under ort-<ver>_trt-<ver>_<precision>_sm<cc>/; caches built with another onnxruntime version are deleted at startup so engines are always rebuilt after an upgrade), --ort_log_level verbose|info|warning|error|fatal (onnxruntime's global logger; the default error hides the TensorRT engine-build warnings such as Make sure input ... has Int64 binding and Could not read timing cache), --head_score_threshold 0.5, --draw_lines (ibug68 contour lines), --disable_bbox, --point_radius, --save_raw_predictions (per-image / per-frame JSON with head boxes, points and the 3-class visibility), --disable_imshow, --disable_video_writer. Keys for video / camera input: ESC quit, b toggle head boxes, l toggle contour lines, r toggle the orientation ring. Rendering follows the project convention: predictions only, single color, no visibility color coding.

Web demo (demo/web/, Electron + onnxruntime-web on WebGPU / WASM)

The same two-stage pipeline entirely in the browser engine, derived from the web runtime of PINTO0309/soma: Electron + Vite + React + TypeScript with onnxruntime-web 1.27.0 (WebGPU execution provider, WASM fallback). Inference runs in a dedicated Web Worker by default (--web-inference-worker main runs the engines on the UI thread instead), the input is a webcam (VGA), a video file or an image file, and the crop geometry and normalization are identical to the Python demo. The Electron shell (electron/main.ts) carries the WebGPU command-line switches of the reference (ignore-gpu-blocklist, enable-zero-copy, disable-gpu-sandbox, enable-unsafe-webgpu, enable-webgpu-developer-features, enable-features=Vulkan,WebGPU,WebGPUService,SharedArrayBuffer, use-webgpu-adapter=default), injects the COOP / COEP isolation headers and grants camera access. The page also runs in a plain Chrome / Edge 113+ (pnpm run dev:browser); every asset (page, wasm runtime, models) is served from the app's own origin — no CDN.

# DEIMv2 detectors must be rewritten for onnxruntime-web first (double -> float32, negative axes
# normalized, 1-D MatMul operands made 2-D, IsNaN / IsInf expressed with Equal / Abs / Greater so the
# decoder stays on the GPU, unreachable nodes pruned) and are passed through the same
# optimization pipeline as the HRFFA exports (onnxslim without Gemm fusion -> onnxsim without optimization
# passes -> rank-5 qkv rewrite of the ViT attention -> canonicalized fixed graph); every output is verified
# against the original with onnxruntime (graph optimizations off).
# Output names are shortened and tagged, e.g. deimv2_dinov3_s_wholebody49_boxes_only_webgpu.onnx. Only the
# boxes-only graphs are staged and listed (masks output removed with hrffa.dataset.pseudolabel.make_boxes_only,
# mask head pruned): {hgnetv2_n, dinov3_s, dinov3_x}; the UI defaults to the MIT-licensed
# yolov9_t_wholebody34_0100_1x3x640x640.onnx (see below). The legacy data/models/deimv2_wholebody49_boxes_only.onnx
# (X backbone) is published as deimv2_dinov3_x_wholebody49_boxes_only_webgpu.onnx:
uv run python scripts/onnx_web_compat.py data/models/deimv2_*_boxes_only.onnx --out-dir demo/web/models

cd demo/web
corepack enable pnpm     # Node >= 22; the project pins pnpm@10.33.0 via "packageManager"
pnpm install             # pnpm only (npm / yarn are refused); stages models + wasm, provisions the electron binary
pnpm run fetch:models    # no local models? download the ONNX files from the `weights` release into demo/web/models/
                         # (every file is checked against the SHA-256 pinned in models.lock.json), then pnpm run prepare:assets
pnpm run dev -- --web-inference-worker main   # engines on the UI thread instead of the worker
pnpm run dev -- --devtools                    # open DevTools (detached); closed by default, View > Toggle Developer Tools also works
pnpm run dev             # vite dev server + electron window (dedicated inference worker)
pnpm run start           # production build (dist/ + dist-electron/) and launch electron from file://
pnpm run dev:browser     # vite only: open http://localhost:5274 in Chrome / Edge

The detector slot also accepts the raw-head YOLOv9-Wholebody34 exports of PINTO_model_zoo/455_YOLOv9-Wholebody34 — these are the MIT-licensed YOLOv9 models trained with PINTO0309/YOLO (an MIT re-implementation of YOLOv9 / YOLOv7 / YOLO-RD), not the GPL-3.0 official YOLOv9 code, so they can be redistributed with this demo (yolov9_{n,t}_wholebody34_0100_1x3x640x640.onnx, output [1, 4+34, 8400], class 7 = head; both are on the weights release and fetched by fetch:models; yolov9_t is the default detector of the web demo): they are staged as they are — the score threshold and a greedy NMS (IoU 0.5) run in the app, no graph rewrite is needed (≈ 22 ms per frame for yolov9_n / yolov9_t on the RTX 3070 with WebGPU). All ONNX files of the demo (the rewritten deimv2_*_boxes_only_webgpu.onnx detectors, the YOLOv9 exports and the hrffa_* graphs) are published on the weights release; scripts/fetch-models.mjs downloads them and refuses any file whose SHA-256 differs from models.lock.json. scripts/prepare-assets.mjs (run by pnpm install, dev and build) stages demo/web/models/deimv2_*_boxes_only_webgpu.onnx (the rewritten detectors; masks variants are neither staged nor listed), the YOLOv9 exports, the optional yawnet_distill_{064,096,128}_unified_v6u_kappa head-orientation models (from demo/web/models/ or data/models/; not part of the weights release, so place the file there yourself; selectable in the UI and drawn as a top-right ring; the Smooth the ring checkbox — off by default — smooths it with a circular EMA, τ = 150 ms, on video / camera input) and data/models/hrffa_*.onnx (the HRFFA graphs run unmodified; files above 300 MB such as the ViT-L teacher are skipped unless HRFFA_WEB_INCLUDE_LARGE=1), and copies the onnxruntime-web wasm files next to them. URL parameters: ?backend=wasm|webgpu, ?worker=dedicated|main, ?detector=<file>, ?aligner=<file>, ?orientation=<file>|off, ?image=<same-origin URL> or ?video=<same-origin URL> with &autostart=1.

Measured in Chrome 151 / Electron 43 on an RTX 3070 (WebGPU EP, second run on a still image; WASM = single thread): DEIMv2 boxes-only detectors ≈ 60–65 ms per frame in the app including preprocessing (dinov3_s graph alone ≈ 50 ms; after the rewrites only the 9 int64 index ops of the post-processor remain on the CPU, the rest of the graph runs on WebGPU), vitt-256 ≈ 20 ms per head (N-batch: 9 heads in 144 ms), hg0-256 ≈ 10 ms per head (9 heads in 46 ms); WASM: hgnetv2_n ≈ 610 ms, hg0-256 ≈ 55 ms per head.

Supply-chain hardening (same policy as the reference project this demo is derived from): every dependency is pinned to an exact version and pnpm-lock.yaml is committed with integrity hashes (the resolution was taken over unchanged from the reference project — the lockfile is a strict subset of it); minimumReleaseAge: 10080 in pnpm-workspace.yaml refuses any package version published less than 7 days ago; dependency build scripts are blocked except electron (official binary download verified against SHASUMS256) and esbuild; installs with npm / yarn are refused by a preinstall guard; the page ships a strict Content-Security-Policy and loads nothing from third-party hosts.

Configuration (configs/*.yaml)

Each preset stores only the differences from the defaults of TrainConfig (src/hrffa/train/config.py). _base_: inherits another preset (chained, last-wins merge, cycle detection; unknown keys are errors).

PresetContents
clean_v3Deployment teacher (DINOv3 ViT-L/16 @320, initialized from clean_v2, WSD 200)
student_s_base → student_s256_96gbShared student settings (teacher clean_v3, crop pad 0.05, KD 0.5/0.5/0.2, WSD 250/50, EMA 0.999)
student_s256_96gb_r2ViT-T @256, second round from the old A1 best (WSD 450)
student_s096_96gb_r2ViT-T @96, fine-tuned from the r2 best
student_hg0_wsd / student_hg0_s096_wsdCNN student @256 / @96
abl_*Ablations
smoke_s_8gbSmoke test on an 8 GB GPU

6. Repository layout

configs/            training presets (YAML with _base_ inheritance)
src/hrffa/
  dataset/          unified-dataset conversion, pseudo labeling, augmentation, QA tools (converters / pseudolabel / augment / qa / selftrain)
  data/             Dataset classes for training and evaluation (apply geometric / photometric augmentation)
  model/            teacher.py (the model), vit_tiny.py, hgnetv2.py, export_modules.py (export-time MHA), popos.py, losses.py
  train/            train_teacher.py, distill_student.py, evaluate.py, config.py
  export/           export_onnx.py, nbatch.py (fixed batch-1 → N-batch conversion and graph rewrites)
scripts/            run_phase_a.sh (run the student arms back to back), make_results_tables.py, onnx_web_compat.py (DEIMv2 graphs -> onnxruntime-web)
demo/               demo_hrffa_onnx.py (ONNX demo: DEIMv2-Wholebody49 head detection + HRFFA landmarks; images / video / camera)
  web/              Electron / browser demo (Vite + React + onnxruntime-web, WebGPU / WASM, dedicated worker; pinned pnpm lockfile)
tests/              unit tests (model, export, N-batch conversion, dataset conversion)
history/            050_results_tables.md (results tables) and assets/050/ (README figures)
ckpts/ data/ datasets/ runs/ onnx/   weights, raw data, unified data and training outputs (not tracked by git)

7. License

  • Code: MIT License (Copyright (c) 2026 Katsuya Hyodo).
  • The datasets (300W-LP / WFLW / 300W / COFW) and the generated / self-training data are not bundled; follow their research-use distribution terms. The unified format carries a per-record license_tag so that records can be filtered by license.
  • Weights: DINOv3 (Meta, DINOv3 License), vitt_distill and PP-HGNetV2 (DEIMv2, Apache-2.0) are neither bundled nor redistributed. Check the derived-work terms of these sources before distributing trained HRFFA weights or ONNX files.

8. Citation and acknowledgements

8.1 Citing this repository

@software{hyodo2026hrffa,
  author    = {Katsuya Hyodo},
  title     = {HRFFA: High-Angle Robust Fast FaceAlignment},
  month     = {aug},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.22161811},
  url       = {https://doi.org/10.5281/zenodo.22161811},
}

8.2 Backbones, pretrained weights and tools

DINOv3 — teacher backbone (ViT-L/16). The official implementation is imported at runtime from the torch.hub cache and the weights are distributed under the DINOv3 License. Paper: https://arxiv.org/abs/2508.10104 · Code: https://github.com/facebookresearch/dinov3

@misc{simeoni2025dinov3,
  title         = {{DINOv3}},
  author        = {Sim{\'e}oni, Oriane and Vo, Huy V. and Seitzer, Maximilian and Baldassarre, Federico and Oquab, Maxime and Jose, Cijo and Khalidov, Vasil and Szafraniec, Marc and Yi, Seungeun and Ramamonjisoa, Micha{\"e}l and Massa, Francisco and Haziza, Daniel and Wehrstedt, Luca and Wang, Jianyuan and Darcet, Timoth{\'e}e and Moutakanni, Th{\'e}o and Sentana, Leonel and Roberts, Claire and Vedaldi, Andrea and Tolan, Jamie and Brandt, John and Couprie, Camille and Mairal, Julien and J{\'e}gou, Herv{\'e} and Labatut, Patrick and Bojanowski, Piotr},
  year          = {2025},
  eprint        = {2508.10104},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CV},
  url           = {https://arxiv.org/abs/2508.10104}
}

DEIMv2 — source of the ImageNet-pretrained PP-HGNetV2-B0 weights used by the CNN student (Apache-2.0). Paper: https://arxiv.org/abs/2509.20787 · Code: https://github.com/Intellindust-AI-Lab/DEIMv2

@article{huang2025deimv2,
  title   = {Real-Time Object Detection Meets {DINOv3}},
  author  = {Huang, Shihua and Hou, Yongjie and Liu, Longfei and Yu, Xuanlong and Shen, Xi},
  journal = {arXiv preprint arXiv:2509.20787},
  year    = {2025},
  url     = {https://arxiv.org/abs/2509.20787}
}

DEIMv2-Wholebody49 — pseudo labels for head boxes and face parts (Apache-2.0). https://github.com/PINTO0309/PINTO_model_zoo/tree/main/488_DEIMv2-Wholebody49

@software{DEIMv2-Wholebody49,
  author = {Katsuya Hyodo},
  title  = {Unified multi-task model for detection, pose estimation, and instance segmentation. 49 classes.},
  url    = {https://github.com/PINTO0309/PINTO_model_zoo/tree/main/488_DEIMv2-Wholebody49},
  year   = {2026},
  month  = {05},
  doi    = {10.5281/zenodo.10229410}
}

YOLOv9-Wholebody34 (MIT-licensed YOLOv9) — optional head detector of the web demo (yolov9_{n,t}_wholebody34_0100_1x3x640x640.onnx, PINTO_model_zoo #455). The models are trained and exported with PINTO0309/YOLO, an MIT-licensed re-implementation of YOLOv9 / YOLOv7 / YOLO-RD; the GPL-3.0 official YOLOv9 code is not used. Original paper: https://arxiv.org/abs/2402.13616

@software{PINTO0309_YOLO,
  author = {Katsuya Hyodo},
  title  = {An {MIT} License of {YOLOv9}, {YOLOv7}, {YOLO-RD}},
  url    = {https://github.com/PINTO0309/YOLO},
  year   = {2025}
}

@software{YOLOv9-Wholebody34,
  author = {Katsuya Hyodo},
  title  = {{YOLOv9-Wholebody34}: whole-body 34-class detector trained with the {MIT}-licensed {YOLOv9}},
  url    = {https://github.com/PINTO0309/PINTO_model_zoo/tree/main/455_YOLOv9-Wholebody34},
  year   = {2025}
}

@article{wang2024yolov9,
  title   = {{YOLOv9}: Learning What You Want to Learn Using Programmable Gradient Information},
  author  = {Wang, Chien-Yao and Yeh, I-Hau and Liao, Hong-Yuan Mark},
  journal = {arXiv preprint arXiv:2402.13616},
  year    = {2024},
  url     = {https://arxiv.org/abs/2402.13616}
}

6DRepNet / 6DRepNet360 — head-pose estimator used for pose QA and for the pose-stratified evaluation (ONNX from PINTO_model_zoo #423). Papers: https://arxiv.org/abs/2202.12555 · https://arxiv.org/abs/2309.07654

@inproceedings{hempel2022sixdrepnet,
  title     = {{6D} Rotation Representation For Unconstrained Head Pose Estimation},
  author    = {Hempel, Thorsten and Abdelrahman, Ahmed A. and Al-Hamadi, Ayoub},
  booktitle = {IEEE International Conference on Image Processing (ICIP)},
  year      = {2022},
  url       = {https://arxiv.org/abs/2202.12555}
}

@article{hempel2023sixdrepnet360,
  title   = {Towards Robust and Unconstrained Full Range of Rotation Head Pose Estimation},
  author  = {Hempel, Thorsten and Abdelrahman, Ahmed A. and Al-Hamadi, Ayoub},
  journal = {arXiv preprint arXiv:2309.07654},
  year    = {2023},
  url     = {https://arxiv.org/abs/2309.07654}
}

Depth Anything V2 — monocular depth used for the depth-reprojection synthesis of extreme pitch (ONNX: onnx-community/depth-anything-v2-small, Apache-2.0). Paper: https://arxiv.org/abs/2406.09414

@inproceedings{yang2024depthanythingv2,
  title     = {Depth Anything {V2}},
  author    = {Yang, Lihe and Kang, Bingyi and Huang, Zilong and Zhao, Zhen and Xu, Xiaogang and Feng, Jiashi and Zhao, Hengshuang},
  booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
  year      = {2024},
  url       = {https://arxiv.org/abs/2406.09414}
}

8.3 Methods referenced (reimplemented from the papers; no code was copied)

D-ViT — the results tables in §1.1 follow the layout of its Tables 1 / 2; its published values are listed for format reference only. Paper: https://arxiv.org/abs/2411.07167

@inproceedings{dang2025dvit,
  title     = {Cascaded Dual Vision Transformer for Accurate Facial Landmark Detection},
  author    = {Dang, Ziqiang and Li, Jianfang and Liu, Lin},
  booktitle = {IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)},
  year      = {2025},
  url       = {https://arxiv.org/abs/2411.07167}
}

POPoS — distance-map + multilateration decoding (ablation arm D8). Paper: https://arxiv.org/abs/2410.09583

@inproceedings{xiang2025popos,
  title     = {{POPoS}: Improving Efficient and Robust Facial Landmark Detection with Parallel Optimal Position Search},
  author    = {Xiang, Chong-Yang and He, Jun-Yan and Cheng, Zhi-Qi and Wu, Xiao and Hua, Xian-Sheng},
  booktitle = {Proceedings of the AAAI Conference on Artificial Intelligence (AAAI)},
  year      = {2025},
  url       = {https://arxiv.org/abs/2410.09583}
}

LESA — local term added in parallel to self-attention (ablation arm D7). Paper: https://arxiv.org/abs/2107.05637

@article{yang2021lesa,
  title   = {Locally Enhanced Self-Attention: Combining Self-Attention and Convolution as Local and Context Terms},
  author  = {Yang, Chenglin and Qiao, Siyuan and Kortylewski, Adam and Yuille, Alan},
  journal = {arXiv preprint arXiv:2107.05637},
  year    = {2021},
  url     = {https://arxiv.org/abs/2107.05637}
}

BiternionNet — YawNet, the head-yaw model of the §5 demos, is an original architecture that borrows only this paper's biternion output representation (unit (cos θ, sin θ) regression) and von-Mises loss. Paper: https://lucasb.eyer.be/academic/biternions/biternions_gcpr15.pdf, original implementation (MIT): https://github.com/lucasb-eyer/BiternionNet

@inproceedings{beyer2015biternion,
  title     = {Biternion Nets: Continuous Head Pose Regression from Discrete Training Labels},
  author    = {Beyer, Lucas and Hermans, Alexander and Leibe, Bastian},
  booktitle = {German Conference on Pattern Recognition (GCPR)},
  year      = {2015},
  url       = {https://lucasb.eyer.be/academic/biternions/biternions_gcpr15.pdf}
}

PersonViT — the fixed batch-1 → N-batch ONNX conversion follows the approach of its export_onnx.py. https://github.com/PINTO0309/PersonViT

8.4 Datasets

300W-LP (used with a coordinate offset correction; also the source of the unleaked 300wlp_val set). Paper: https://arxiv.org/abs/1511.07212

@inproceedings{zhu2016face,
  title     = {Face Alignment Across Large Poses: A {3D} Solution},
  author    = {Zhu, Xiangyu and Lei, Zhen and Liu, Xiaoming and Shi, Hailin and Li, Stan Z.},
  booktitle = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
  year      = {2016},
  url       = {https://arxiv.org/abs/1511.07212}
}

WFLW. Paper: https://arxiv.org/abs/1805.10483

@inproceedings{wu2018wflw,
  title     = {Look at Boundary: A Boundary-Aware Face Alignment Algorithm},
  author    = {Wu, Wayne and Qian, Chen and Yang, Shuo and Wang, Quan and Cai, Yici and Zhou, Qiang},
  booktitle = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
  year      = {2018},
  url       = {https://arxiv.org/abs/1805.10483}
}

300W. https://ibug.doc.ic.ac.uk/resources/300-W/

@inproceedings{sagonas2013300w,
  title     = {300 Faces in-the-Wild Challenge: The First Facial Landmark Localization Challenge},
  author    = {Sagonas, Christos and Tzimiropoulos, Georgios and Zafeiriou, Stefanos and Pantic, Maja},
  booktitle = {IEEE International Conference on Computer Vision Workshops (ICCVW)},
  year      = {2013}
}

COFW. http://www.vision.caltech.edu/xpburgos/ICCV13/

@inproceedings{burgosartizzu2013cofw,
  title     = {Robust Face Landmark Estimation under Occlusion},
  author    = {Burgos-Artizzu, Xavier P. and Perona, Pietro and Doll{\'a}r, Piotr},
  booktitle = {IEEE International Conference on Computer Vision (ICCV)},
  year      = {2013}
}

8.5 Acknowledgements

  • Meta AI for DINOv3, the DEIMv2 authors for the detector and the PP-HGNetV2 weights (originally from PaddlePaddle's PaddleClas / PaddleDetection), the authors of 6DRepNet360 and Depth Anything V2 for the tools used in data QA and synthesis, and the dataset authors of 300W-LP, WFLW, 300W and COFW.
  • The generated head-image pools used to reinforce extreme poses were produced with OpenAI's image generation API; the synthetic data is not redistributed.
  • The ONNX post-processing know-how (batch-axis preservation, N-batch derivation, rank-5 elimination) builds on the author's PersonViT export pipeline and the PINTO_model_zoo conventions.
face-alignment
facial-landmark-detection
head-alignment
head-detection

Significant stargazers

Tushar Sadhwani

364 followers · starred Aug 2026

Onur Solmaz

315 followers · starred Aug 2026

Michael Pedersen

46 followers · starred Aug 2026

Daniel Wang

57 followers · starred Sep 2026