File size: 5,580 Bytes
19fd35d | 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 | #!/usr/bin/env python3
"""MODNet portrait matting -> ExecuTorch WebGPU.
The official code is on GitHub (ZHKKKe/MODNet) and the official checkpoints on Google
Drive; neither is reachable from the export host and the author has no Hugging Face
account. This pins a community Hugging Face mirror that carries a copy of the upstream
source tree and the photographic checkpoint (sha256 checked; four independent mirrors
publish the same checkpoint bytes). The webcam checkpoint comes from a second mirror.
Input: [1, 3, 256, 256] float32 RGB normalized to [-1, 1] ((x / 255 - 0.5) / 0.5).
Output: [1, 1, 256, 256] alpha matte in [0, 1] (inference=True, pred_matte only).
"""
import argparse
import importlib
import sys
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _common # noqa: E402
SOURCE = {
"repo": "boopathiraj/MODNet",
"revision": "a967175f8fd63de67a1c161d8daac966d2b57e3a",
"files": {
"modnet/__init__.py": None,
"modnet/modnet.py": None,
"modnet/src/__init__.py": None,
"modnet/src/models/__init__.py": None,
"modnet/src/models/backbones/__init__.py": None,
"modnet/src/models/backbones/wrapper.py": None,
"modnet/src/models/backbones/mobilenetv2.py": None,
"modnet/LICENSE": None,
},
}
CHECKPOINTS = {
"photographic": {
"repo": "boopathiraj/MODNet",
"revision": "a967175f8fd63de67a1c161d8daac966d2b57e3a",
"file": "modnet/pretrained/modnet_photographic_portrait_matting.ckpt",
"sha256": "7c22235f0925deba15d4d63e53afcb654c47055bbcd98f56e393ab2584007ed8",
},
"webcam": {
"repo": "DavG25/modnet-pretrained-models",
"revision": "903cc06311b3b12071edfa1b42b534e4a31c718a",
"file": "models/modnet_webcam_portrait_matting.ckpt",
"sha256": "913b82b66558db39b6286c150f809017d7528c872b156eb14333c9c6cb52108b",
},
}
class InstanceNorm2dDecomposed(torch.nn.Module):
"""InstanceNorm2d(affine=False) as mean/sub/mul/sqrt/div.
The Vulkan/WebGPU partitioner does not take aten._native_batch_norm_legit.no_stats,
so the stock module would leave 16 CPU kernels splitting the graph into 13 delegates.
"""
def __init__(self, eps):
super().__init__()
self.eps = eps
def forward(self, x):
d = x - x.mean(dim=(2, 3), keepdim=True)
var = (d * d).mean(dim=(2, 3), keepdim=True)
return d / torch.sqrt(var + self.eps)
def build_model(variant):
root = None
files = {}
for f in SOURCE["files"]:
path, digest = _common.hf_file(SOURCE["repo"], f, SOURCE["revision"])
files[f] = digest
root = Path(str(path)[: -len(f)])
sys.path.insert(0, str(root))
MODNet = importlib.import_module("modnet.modnet").MODNet
sys.path.remove(str(root))
ck = CHECKPOINTS[variant]
path, _ = _common.hf_file(ck["repo"], ck["file"], ck["revision"], ck["sha256"])
state = torch.load(path, map_location="cpu", weights_only=True)
# Checkpoints were saved from nn.DataParallel.
state = {k.removeprefix("module."): v for k, v in state.items()}
model = MODNet(backbone_pretrained=False)
model.load_state_dict(state, strict=True)
for parent in list(model.modules()):
for name, child in parent.named_children():
if isinstance(child, torch.nn.InstanceNorm2d):
assert not child.affine and not child.track_running_stats
setattr(parent, name, InstanceNorm2dDecomposed(child.eps))
class Matte(torch.nn.Module):
def __init__(self, m):
super().__init__()
self.m = m
def forward(self, img):
return self.m(img, True)[2]
return Matte(model).eval(), files
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--out", required=True, help="output directory")
ap.add_argument("--size", type=int, nargs=2, default=[256, 256], metavar=("H", "W"))
ap.add_argument("--variant", choices=sorted(CHECKPOINTS), default="photographic")
ap.add_argument(
"--golden",
help="optional goldens.manifest.json to compare against (key: bgremove)",
)
args = ap.parse_args()
model, source_files = build_model(args.variant)
golden = _common.load_golden(args.golden, "bgremove") if args.golden else None
_common.export_vision(
name="modnet",
model=model,
example_input=torch.rand(1, 3, *args.size) * 2 - 1,
out_dir=args.out,
upstream={
"model": f"ZHKKKe/MODNet ({args.variant} portrait matting checkpoint)",
"weights": {
**CHECKPOINTS[args.variant],
"official": False,
"note": "community mirror; official checkpoints are on Google Drive, linked from GitHub, neither reachable from the export host",
},
"architecture_source": {
"repo": SOURCE["repo"],
"revision": SOURCE["revision"],
"files_sha256": source_files,
"official": False,
"note": "copy of the upstream src/models tree with modnet.py moved to the package root",
},
"license": "Apache-2.0",
},
extra_recipe={
"variant": args.variant,
"preprocessing_outside_graph": "RGB / 255, then (x - 0.5) / 0.5",
"postprocessing_outside_graph": "alpha in [0, 1]",
},
golden=golden,
)
if __name__ == "__main__":
main()
|