YOLO26 Face Parsing β€” 19-class, realtime, Core ML

Two YOLO26 semantic segmentation models fine-tuned on CelebAMask-HQ for 19-class face parsing, plus Core ML exports that run in ~1.1 ms on the Apple Neural Engine.

params val mIoU test mIoU pixel acc ANE latency @512 Core ML size
yolo26n-sem (nano) 1.63 M 0.747 0.746 0.940 1.14 ms 3.2 MB (fp16)
yolo26l-sem (large) 17.87 M 0.776 0.773 0.948 4.22 ms 17.0 MB (int8)

Test = the 2 824-image held-out split, never seen during training or model selection. Test β‰ˆ val for both models, so neither overfits. The nano reaches 96.3 % of the large model's mIoU with 11Γ— fewer parameters β€” the gap is concentrated almost entirely in the rarest classes.

Latency measured on an Apple M5 Max ANE with computeUnits = .cpuAndNeuralEngine. Expect an iPhone to be slower; both still leave large headroom against a 33 ms/30 fps budget.

Demo

Live front-camera overlay on iPhone, nano model, ~30 fps.

Contents

checkpoints/  yolo26{n,l}-sem-celebamaskhq.pt    Ultralytics checkpoints (resume/fine-tune/val)
coreml/       yolo26n-sem-face-fp16.mlpackage    the shipping realtime model
              yolo26l-sem-face-int8.mlpackage    the accuracy variant
scripts/      the full pipeline, dataset prep -> training -> export -> evaluation
assets/       per-epoch metrics + qualitative contact sheets
demo.mp4      iPhone screen recording

Classes

0 background Β· 1 skin Β· 2 nose Β· 3 eyeglasses Β· 4 left_eye Β· 5 right_eye Β· 6 left_eyebrow Β· 7 right_eyebrow Β· 8 left_ear Β· 9 right_ear Β· 10 mouth Β· 11 upper_lip Β· 12 lower_lip Β· 13 hair Β· 14 hat Β· 15 earring Β· 16 necklace Β· 17 neck Β· 18 cloth

Index order matches the canonical CelebAMask-HQ g_mask.py ordering, so later classes overwrite earlier ones where the source masks overlap.

Results

Per-class mIoU on the CelebAMask-HQ validation split:

class nano large
mean (all) 0.747 0.776
background 0.920 0.932
skin 0.914 0.925
hair 0.901 0.916
nose 0.856 0.870
mouth 0.842 0.858
neck 0.816 0.832
eyeglasses 0.811 0.840
lower_lip 0.791 0.812
upper_lip 0.785 0.810
right_eye 0.777 0.800
left_eye 0.774 0.797
cloth 0.759 0.788
hat 0.756 0.782
right_ear 0.743 0.774
left_ear 0.742 0.776
left_eyebrow 0.704 0.731
right_eyebrow 0.693 0.719
earring 0.467 0.535
necklace 0.131 0.256

On the classes that dominate a face overlay β€” skin, hair, background β€” the two models are within 0.015 of each other. Choose the nano unless you specifically need jewellery.

Cross-dataset generalisation

Neither model saw these images. No ground truth, so this reports mean pixel confidence (qualitative sheets in assets/):

dataset nano large
Celebrity Faces 0.959 0.965
Human Faces (real photos) 0.946 0.957
Human Faces (AI-generated) 0.975 0.976
img_align_celeba 0.944 0.958

Confidence is consistently highest on AI-generated faces (unusually prototypical) and lowest on the low-resolution img_align_celeba crops.

Fine-tuning methodology

Initialised from the official Cityscapes-pretrained yolo26{n,l}-sem.pt and fine-tuned for 12 epochs at 512 px on the canonical CelebA partition (24 183 train / 2 993 val / 2 824 test), batch 32, AdamW (optimizer=auto, lr β‰ˆ 4.3e-4), on a single Apple M5 Max via MPS. Large: 7.77 h. Nano: 5.55 h.

python scripts/prepare_celebamaskhq_semantic.py --mask-size 512 --img-mode resize
python scripts/train.py --model yolo26l-sem.pt --epochs 12 --imgsz 512 --batch 32 \
       --cls-pw 1.0 --loss-size 256 --workers 12 --mosaic 0.5 --no-deterministic --amp

Four choices did most of the work. They are documented because three of them are silent failures β€” the defaults produce a plausible-looking model that is quietly wrong.

1. cls_pw=1.0 β€” the single biggest quality win. Ultralytics computes ENet inverse-log class weights as (1/ln(1.02+p))**cls_pw but defaults the exponent to 0.0, which makes every weight exactly 1.0; set_class_weights then returns early, so class weighting is silently disabled. On a dataset this imbalanced (necklace is 0.02 % of pixels, hair is 32 %) that is fatal for rare classes. Setting cls_pw=1.0 (the maximum an internal assert allows) gives rare classes ~14Γ— the weight of hair/skin:

cls_pw=0.0 (default) cls_pw=1.0
earring 0.000 0.535
necklace 0.000 0.256
hat 0.187 0.782
eyeglasses 0.352 0.840
ears 0.10 – 0.17 ~0.775
mean mIoU 0.438 0.776

(Left column is an early short run, so it is indicative rather than a controlled ablation β€” but classes scoring exactly zero are attributable to the missing weighting.)

2. fliplr=0.0 β€” mandatory for this label set. Ultralytics' RandomFlip mirrors geometry without swapping left/right class labels, so a horizontally flipped left_eye is still labelled left_eye. Nine of the 19 classes are laterally paired (eyes, brows, ears). Any fliplr > 0 teaches the model to confuse them. Rotation (degrees=10), translation, scale and mosaic are used instead.

3. --loss-size 256 β€” ~5Γ— faster training for βˆ’0.002 mIoU. The stock semantic loss upsamples the stride-8 logits to full mask resolution before CE/Dice β€” a 159 M-element tensor at 512 px / batch 32 β€” then softmaxes it and boolean-index-gathers it (a data-dependent shape that forces a device sync), twice counting the auxiliary head. The loss ends up costing more than the entire forward+backward pass. scripts/loss_patch.py evaluates it at a fixed lower resolution instead. A/B at 320 px:

stock --loss-size 256
large @512 14.2 s/iter 2.7 s/iter
mean mIoU 0.644 0.642
thin-class (eyes/brows/lips) mean Ξ” β€” βˆ’0.0016

Profiling first mattered: the dataloader was not the bottleneck (it sustains 1900–4000 img/s). See scripts/isolate_bottleneck.py.

4. Apple-Silicon throughput. deterministic=True (a default) more than doubles loss cost on MPS by forcing slow scatter kernels. AMP helps on MPS (8.1 vs 12.6 s/iter). Batch 64 is worse per-image than batch 32. Ultralytics also hard-forces workers=0 on MPS (trainer.py:162, a CPU-era assumption); scripts/fast_trainer.py restores them and adds validation throttling, since the 2 993-image val split otherwise costs ~18 min/epoch.

Core ML export

yolo export format=coreml and a naive manual conversion both fail with coremltools 9 + numpy β‰₯ 2:

TypeError: only 0-dimensional arrays can be converted to Python scalars

coremltools' _cast op handler does mb.const(val=int(x.val)), and numpy 2 forbids int() on a size-1, >0-d array. YOLO26's attention block emits exactly that cast. scripts/coreml_patch.py re-registers _cast with .item() coercion β€” import it before ct.convert. scripts/export_coreml.py then traces the raw module (fp16 logits, no baked argmax) and compares quantization variants.

Quantize-then-convert vs convert-then-quantize, measured on the large model:

argmax agreement mIoU vs PyTorch latency size
fp16 (convert only) 99.854 % 0.990 5.39 ms 33.5 MB
convert β†’ int8 99.727 % 0.974 4.33 ms 17.0 MB
convert β†’ 6-bit palettize 99.089 % 0.924 4.06 ms 12.8 MB
int8 β†’ convert 99.630 % 0.975 4.49 ms 17.2 MB

Convert-then-quantize wins β€” better fidelity, smaller and faster than the reverse. Judge palettization by mIoU rather than argmax agreement: 99.1 % agreement hides a drop to 0.924 mIoU, because the disagreements land on small classes.

Reproduce with scripts/compare_quant_order.py.

Using the Core ML models

  • input image β€” 512Γ—512 RGB (CVPixelBuffer; Core ML handles BGRAβ†’RGB)
  • output logits β€” MLMultiArray float16, (1, 19, 64, 64), NCHW

The output is a stride-8 logit grid, not an argmax map β€” bilinearly upsample and argmax over the 19 channels on the GPU. Doing it in a fragment shader keeps the whole network on the ANE and costs nothing measurable:

half best = logitsTex.sample(bilinear, modelUV, 0).r;
ushort cls = 0;
for (ushort c = 1; c < 19; c++) {
    half v = logitsTex.sample(bilinear, modelUV, c).r;
    if (v > best) { best = v; cls = c; }
}

Set configuration.computeUnits = .cpuAndNeuralEngine. Avoid .all: it aborts in MPSGraph (MLIR pass manager failed) for these shapes on some machines, and keeping the model off the GPU leaves it free for rendering.

⚠️ Feed the model an upright face. It was trained exclusively on upright, roughly centred faces, so a rotated input degrades output badly enough to look like a broken model. AVCaptureConnection.videoRotationAngle is silently ignored on some device/format combinations β€” verify the buffer dimensions rather than trusting it.

Letterbox the whole frame, don't centre-crop it. The input is square while camera frames are not. Cropping an inscribed square (min(w, h)) is tempting because it keeps the subject at full resolution, but it produces no logits at all outside that square β€” a face near the top or bottom of a portrait frame simply gets no mask. Scale the whole frame to fit and pad the short axis (max(w, h)) instead. A 9:16 frame then fills ~56 % of the input width, which costs some fine detail but covers every visible pixel.

Using the PyTorch checkpoints

from ultralytics import YOLO
model = YOLO("checkpoints/yolo26n-sem-celebamaskhq.pt")
result = model("face.jpg")[0]
class_map = result.semantic_mask.data          # HxW int class indices

Validate against your own copy of the dataset (edit path in scripts/data.yaml):

yolo semantic val model=checkpoints/yolo26l-sem-celebamaskhq.pt data=scripts/data.yaml \
     imgsz=512 split=test

Limitations

  • necklace is weak (0.131 nano / 0.256 large) and earring is moderate. Only 143 validation images contain a necklace, totalling ~163 k pixels. This is a dataset limit, not a training one.
  • Upright, roughly centred faces only β€” see the warning above.
  • CelebAMask-HQ demographic bias carries over; the training distribution is celebrity photography and is not representative. Validate on your own population before relying on it, and do not use it for identification, surveillance, or any consequential decision.
  • The 64Γ—64 logit grid is coarse for very thin structures; boundaries are smoothed by the bilinear upsample.
  • Latency figures are from an M5 Max ANE, not a phone.

Licensing β€” read before use

  • Ultralytics is AGPL-3.0. These weights are derived from Ultralytics code and pretrained checkpoints, so the AGPL's copyleft terms apply. Shipping them inside a closed-source application may require a commercial licence from Ultralytics.
  • CelebAMask-HQ is non-commercial: research and educational use only. The dataset authors do not own the underlying image copyrights.

Both apply to anything you build from these artifacts. This repository is published for research and educational purposes.

Citation

@article{CelebAMask-HQ,
  title   = {MaskGAN: Towards Diverse and Interactive Facial Image Manipulation},
  author  = {Lee, Cheng-Han and Liu, Ziwei and Wu, Lingyun and Luo, Ping},
  journal = {Technical Report},
  year    = {2019}
}
Downloads last month
97
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support