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
| """ | |
| 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() | |