Image Segmentation
ultralytics
Core ML
mask-generation
face-parsing
semantic-segmentation
yolo26
ios
on-device
celebamask-hq
Instructions to use a-ml/yolo26-face with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use a-ml/yolo26-face with ultralytics:
# Couldn't find a valid YOLO version tag. # Replace XX with the correct version. from ultralytics import YOLOvXX model = YOLOvXX.from_pretrained("a-ml/yolo26-face") source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
File size: 7,854 Bytes
e2f3b24 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """
Export a trained YOLO26 semantic checkpoint to Core ML and pick the best
quantization variant.
Path: manual trace -> coremltools (NOT `yolo export`, whose baked argmax breaks
MIL; and coremltools 9.0 x numpy 2.x needs coreml_patch). Output contract is
locked to the app: input "image" (512x512 RGB), output "logits" fp16
(1,19,64,64); computeUnits .cpuAndNeuralEngine.
Variants: fp16 baseline, int8 weight-only linear, 6-bit palettization.
Each is parity-checked against PyTorch on real val images (argmax agreement)
and timed on the ANE. Writes a summary + copies the recommended variant to
--app-dest as FaceSegModel.mlpackage (same name -> Xcode swap is automatic).
Usage:
export_coreml.py --weights /Users/ari/FaceSegmentation/runs_semantic/celeba_large/weights/best.pt \
--tag large [--app-dest .../facesegmentation/FaceSegModel.mlpackage]
"""
import argparse, glob, json, os, shutil, time
import numpy as np
import torch
from PIL import Image
import coremltools as ct
import coreml_patch # noqa: F401
from ultralytics import YOLO
VAL_IMAGES = "/Users/ari/FaceSegmentation/dataset_celebamaskhq_semantic/images/val"
class LogitsOnly(torch.nn.Module):
def __init__(self, m):
super().__init__()
self.m = m
def forward(self, x):
z = self.m(x)
return z[0] if isinstance(z, (list, tuple)) else z
def torch_logits(wrap, img):
x = torch.from_numpy(np.asarray(img, dtype=np.float32) / 255.0).permute(2, 0, 1)[None]
with torch.no_grad():
return wrap(x).float().numpy()
def evaluate(mlpath, wrap, imgs, R):
m = ct.models.MLModel(mlpath, compute_units=ct.ComputeUnit.CPU_AND_NE)
agree, maxdiff = [], []
for img in imgs:
ref = torch_logits(wrap, img) # (1,19,g,g)
out = m.predict({"image": img})
got = np.asarray(out["logits"], dtype=np.float32)
maxdiff.append(float(np.abs(ref - got).max()))
agree.append(float((ref.argmax(1) == got.argmax(1)).mean()))
# latency
m.predict({"image": imgs[0]})
t0 = time.time()
N = 30
for _ in range(N):
m.predict({"image": imgs[0]})
ms = (time.time() - t0) / N * 1000
size_mb = sum(os.path.getsize(p) for p in glob.glob(mlpath + "/**/*", recursive=True) if os.path.isfile(p)) / 1e6
return {"argmax_agreement": float(np.mean(agree)), "max_abs_diff": float(np.mean(maxdiff)),
"latency_ms": round(ms, 2), "size_mb": round(size_mb, 2)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--weights", required=True)
ap.add_argument("--tag", required=True, help="output name suffix, e.g. large / nano")
ap.add_argument("--imgsz", type=int, default=512)
ap.add_argument("--out", default="/Users/ari/FaceSegmentation/exports_semantic")
ap.add_argument("--n-val", type=int, default=8)
ap.add_argument("--app-dest", default="", help="if set, copy recommended variant here")
ap.add_argument("--min-agree", type=float, default=0.995)
ap.add_argument("--quant-min-mb", type=float, default=10.0,
help="only consider quantized variants if fp16 is at least this large")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
R = args.imgsz
y = YOLO(args.weights)
wrap = LogitsOnly(y.model).eval().float().cpu()
val = sorted(glob.glob(os.path.join(VAL_IMAGES, "*.jpg")))[: args.n_val]
assert val, f"no val images at {VAL_IMAGES}"
imgs = [Image.open(p).convert("RGB").resize((R, R), Image.BILINEAR) for p in val]
ts = torch.jit.trace(wrap, torch.rand(1, 3, R, R), strict=False)
base = ct.convert(
ts,
inputs=[ct.ImageType(name="image", shape=(1, 3, R, R), scale=1 / 255.0,
bias=[0, 0, 0], color_layout=ct.colorlayout.RGB)],
outputs=[ct.TensorType(name="logits")],
convert_to="mlprogram",
compute_precision=ct.precision.FLOAT16,
compute_units=ct.ComputeUnit.CPU_AND_NE,
minimum_deployment_target=ct.target.iOS17,
)
paths, results = {}, {}
p_fp16 = f"{args.out}/FaceSeg_{args.tag}_fp16.mlpackage"
base.save(p_fp16)
paths["fp16"] = p_fp16
from coremltools.optimize.coreml import (
OpLinearQuantizerConfig, OpPalettizerConfig, OptimizationConfig,
linear_quantize_weights, palettize_weights,
)
try:
q = linear_quantize_weights(
base, OptimizationConfig(global_config=OpLinearQuantizerConfig(mode="linear_symmetric", dtype="int8")))
p = f"{args.out}/FaceSeg_{args.tag}_int8.mlpackage"
q.save(p)
paths["int8"] = p
except Exception as e:
print("int8 quant failed:", repr(e)[:200])
try:
q = palettize_weights(
base, OptimizationConfig(global_config=OpPalettizerConfig(mode="kmeans", nbits=6)))
p = f"{args.out}/FaceSeg_{args.tag}_pal6.mlpackage"
q.save(p)
paths["pal6"] = p
except Exception as e:
print("palettization failed:", repr(e)[:200])
for name, p in paths.items():
results[name] = evaluate(p, wrap, imgs, R)
print(f"[{name:5s}] {results[name]}")
# Prefer fidelity. Quantizing only pays if it saves real space: for a ~3 MB
# nano, trading measurable accuracy for 1.5 MB is a bad deal, while for a
# 33 MB model halving the size is worth ~0.1% argmax disagreement.
# (Measured: convert-then-quantize beats quantize-then-convert; see
# ml/compare_quant_order.py.)
rec = "fp16"
base_mb = results["fp16"]["size_mb"]
if base_mb >= args.quant_min_mb:
ok = [n for n in results
if n != "fp16" and results[n]["argmax_agreement"] >= args.min_agree]
if ok:
rec = min(ok, key=lambda n: results[n]["size_mb"])
summary = {"weights": args.weights, "imgsz": R, "results": results, "recommended": rec,
"recommended_path": paths[rec]}
with open(f"{args.out}/summary_{args.tag}.json", "w") as f:
json.dump(summary, f, indent=2)
print("RECOMMENDED:", rec, "->", paths[rec])
if args.app_dest:
if os.path.exists(args.app_dest):
shutil.rmtree(args.app_dest)
shutil.copytree(paths[rec], args.app_dest)
print("copied to app:", args.app_dest)
# Keep SegModelContract in sync. A stale inputSize/gridSize makes
# InferenceEngine.upload() reject every frame (shape guard) and the
# overlay silently never appears.
# NB: compute_units is REQUIRED here -- the default (.all) aborts in
# MPSGraph ("MLIR pass manager failed") on this machine at >=384px,
# which killed the export stage with SIGABRT after the model was saved.
grid = int(np.asarray(
ct.models.MLModel(paths[rec], compute_units=ct.ComputeUnit.CPU_AND_NE).predict(
{"image": Image.new("RGB", (R, R))})["logits"]).shape[-1])
sync_swift_contract(args.app_dest, R, grid)
def sync_swift_contract(app_dest, input_size, grid_size):
"""Rewrite inputSize/gridSize in SegmentationShared.swift to match the model."""
shared = os.path.join(os.path.dirname(app_dest), "SegmentationShared.swift")
if not os.path.exists(shared):
print(f"WARNING: {shared} not found; update SegModelContract manually "
f"(inputSize={input_size}, gridSize={grid_size})")
return
import re
src = open(shared).read()
new = re.sub(r"(inputSize:\s*Int\s*=\s*)\d+", rf"\g<1>{input_size}", src)
new = re.sub(r"(gridSize:\s*Int\s*=\s*)\d+", rf"\g<1>{grid_size}", new)
if new != src:
open(shared, "w").write(new)
print(f"synced SegModelContract -> inputSize={input_size}, gridSize={grid_size}")
else:
print(f"SegModelContract already correct (inputSize={input_size}, gridSize={grid_size})")
if __name__ == "__main__":
main()
|