toolsmonk commited on
Commit
2c110d2
·
verified ·
1 Parent(s): b4172d4

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +43 -0
  2. build-lama-webgpu.py +107 -0
  3. lama_webgpu.onnx +3 -0
README.md CHANGED
@@ -1,3 +1,46 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: onnxruntime
4
+ pipeline_tag: image-to-image
5
+ tags:
6
+ - inpainting
7
+ - object-removal
8
+ - lama
9
+ - onnx
10
+ - webgpu
11
  ---
12
+
13
+ # LaMa inpainting, WebGPU-ready ONNX
14
+
15
+ LaMa ("Resolution-robust Large Mask Inpainting with Fourier Convolutions", Suvorov et al., WACV 2022)
16
+ as a single fp32 ONNX graph that runs on **onnxruntime-web's WebGPU execution provider**. It is used by
17
+ the on-device AI eraser of the ToolsMonk Photo Editor and Background Remover.
18
+
19
+ ## Why this build exists
20
+
21
+ The published LaMa ONNX exports run in the browser on WASM only. On WebGPU (onnxruntime-web 1.30) every
22
+ run fails inside the Fourier unit ("Failed to run JSEP kernel"). This file is the same model with three
23
+ **numerically equivalent** graph rewrites:
24
+
25
+ 1. constant folding at a fixed batch of 1, so the DFT matrices the export builds at run time become constants;
26
+ 2. every `Einsum` (`ij,jk->ik`, 216 nodes) rewritten as `MatMul`;
27
+ 3. every negative-step `Slice` (72 nodes) rewritten as `Gather` with constant reversed indices.
28
+
29
+ Maximum output difference against the source on a 512x512 test input: 0.00026 on the 0..255 scale.
30
+ Measured in Chrome: ~1.7-2.2 s per 512x512 inpaint on WebGPU, against ~30 s on single-threaded WASM.
31
+
32
+ ## Inputs and outputs
33
+
34
+ - `image`: float32 `[1, 3, 512, 512]`, RGB in 0..1
35
+ - `mask`: float32 `[1, 1, 512, 512]`, 1 = area to fill, 0 = keep
36
+ - `output`: float32 `[1, 3, 512, 512]`, RGB in 0..255
37
+
38
+ ## Provenance and licence
39
+
40
+ - Model: [advimman/lama](https://github.com/advimman/lama), Apache-2.0.
41
+ - Source ONNX: [Carve/LaMa-ONNX](https://huggingface.co/Carve/LaMa-ONNX) `lama_fp32.onnx`, Apache-2.0
42
+ (sha256 `1faef5301d78db7dda502fe59966957ec4b79dd64e16f03ed96913c7a4eb68d6`).
43
+ - Conversion script: `build-lama-webgpu.py` in this repository (requires `onnx`, `onnxruntime`, `numpy`).
44
+
45
+ This derivative is distributed under the same Apache-2.0 licence. The rewrites change the graph's
46
+ structure, not its weights.
build-lama-webgpu.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the WebGPU-capable LaMa used by the AI eraser (Photo Editor, Background Remover).
2
+
3
+ Why this exists (measured 2026-11-11, onnxruntime-web 1.30, Chrome WebGPU):
4
+ LaMa's published ONNX exports run on WASM only (~30 s per 512 erase on one thread). On WebGPU every
5
+ run failed inside the Fourier unit ("Failed to run JSEP kernel": Add, then Sub, Slice, MatMul as each
6
+ was worked around). Three graph rewrites, each numerically equivalent, make it run on WebGPU at
7
+ ~1.7-2.2 s per erase with the same fill:
8
+ 1. fold at a fixed batch=1 (ORT basic optimisation = constant folding), so the DFT matrices the
9
+ export builds at run time from Range/Cos/Sin/int64 shape maths become constants;
10
+ 2. Einsum "ij,jk->ik" -> MatMul (216 nodes);
11
+ 3. negative-step Slice -> Gather with constant reversed indices (72 nodes).
12
+ The int8 OpenCV Zoo export gives WRONG output on WebGPU after the same rewrites (white fills), so the
13
+ source must be the fp32 export.
14
+
15
+ Source: https://huggingface.co/Carve/LaMa-ONNX lama_fp32.onnx (Apache-2.0; sha256 1faef530...d68d6,
16
+ 208,044,816 bytes). LaMa itself: advimman/lama, Apache-2.0. The output is a derivative under the same
17
+ licence; keep this script and the licence with it wherever it is hosted.
18
+
19
+ Usage (Python 3.11+, `pip install onnx onnxruntime numpy`):
20
+ python -I scripts/models/build-lama-webgpu.py lama_fp32.onnx lama_webgpu.onnx
21
+ It refuses to keep the result unless it matches the source within 1e-2 on the 0..255 scale.
22
+ """
23
+
24
+ import collections
25
+ import sys
26
+
27
+ import numpy as np
28
+ import onnx
29
+ import onnxruntime as ort
30
+ from onnx import helper, numpy_helper, shape_inference
31
+
32
+
33
+ def fold(src: str, dst: str) -> None:
34
+ so = ort.SessionOptions()
35
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
36
+ so.add_free_dimension_override_by_name("batch", 1)
37
+ so.optimized_model_filepath = dst
38
+ ort.InferenceSession(src, so, providers=["CPUExecutionProvider"])
39
+
40
+
41
+ def rewrite(path: str, dst: str) -> dict:
42
+ m = onnx.load(path)
43
+ mi = shape_inference.infer_shapes(m)
44
+ vi = {v.name: v for v in list(mi.graph.value_info) + list(mi.graph.input)}
45
+ init = {i.name: numpy_helper.to_array(i) for i in m.graph.initializer}
46
+ counts = collections.Counter()
47
+ nodes, inits = [], []
48
+ for node in m.graph.node:
49
+ if node.op_type == "Einsum":
50
+ eq = [a.s.decode() for a in node.attribute if a.name == "equation"][0]
51
+ if eq != "ij,jk->ik":
52
+ raise SystemExit(f"unexpected Einsum equation {eq!r} in {node.name}")
53
+ node.op_type = "MatMul"
54
+ del node.attribute[:]
55
+ counts["einsum->matmul"] += 1
56
+ nodes.append(node)
57
+ elif node.op_type == "Slice" and len(node.input) >= 5 and node.input[4] in init and (init[node.input[4]] < 0).any():
58
+ starts, ends, axes, steps = (init[node.input[i]].tolist() for i in (1, 2, 3, 4))
59
+ v = vi.get(node.input[0])
60
+ d = [x.dim_value for x in v.type.tensor_type.shape.dim] if v else None
61
+ if not d or any(x <= 0 for x in d):
62
+ raise SystemExit(f"{node.name}: input shape not static after folding: {d}")
63
+ cur = node.input[0]
64
+ for k, (st, en, ax, sp) in enumerate(zip(starts, ends, axes, steps)):
65
+ idx = np.arange(d[ax])[slice(st if abs(st) < 2**62 else None, en if abs(en) < 2**62 else None, sp)]
66
+ name = f"{node.name}_gidx{k}"
67
+ inits.append(numpy_helper.from_array(idx.astype(np.int64), name))
68
+ out = node.output[0] if k == len(axes) - 1 else f"{node.name}_g{k}"
69
+ nodes.append(helper.make_node("Gather", [cur, name], [out], axis=ax, name=f"{node.name}_gather{k}"))
70
+ cur = out
71
+ d = list(d)
72
+ d[ax] = len(idx)
73
+ counts["negslice->gather"] += 1
74
+ else:
75
+ nodes.append(node)
76
+ del m.graph.node[:]
77
+ m.graph.node.extend(nodes)
78
+ m.graph.initializer.extend(inits)
79
+ onnx.checker.check_model(m)
80
+ onnx.save(m, dst)
81
+ return dict(counts)
82
+
83
+
84
+ def max_diff(a: str, b: str) -> float:
85
+ n = 512
86
+ img = ((np.sin(np.arange(3 * n * n) * 0.37) + 1) / 2).astype(np.float32).reshape(1, 3, n, n)
87
+ msk = np.zeros((1, 1, n, n), np.float32)
88
+ msk[:, :, 200:300, 200:320] = 1
89
+ feeds = {"image": img, "mask": msk}
90
+ oa = ort.InferenceSession(a, providers=["CPUExecutionProvider"]).run(None, feeds)[0]
91
+ ob = ort.InferenceSession(b, providers=["CPUExecutionProvider"]).run(None, feeds)[0]
92
+ return float(np.abs(oa - ob).max())
93
+
94
+
95
+ def main() -> None:
96
+ src, dst = sys.argv[1], sys.argv[2]
97
+ folded = dst + ".folded.onnx"
98
+ fold(src, folded)
99
+ counts = rewrite(folded, dst)
100
+ diff = max_diff(src, dst)
101
+ print("rewrites:", counts, "max |diff| (0..255):", round(diff, 5))
102
+ if diff > 1e-2:
103
+ raise SystemExit("the rewritten model does not match the source; refusing to keep it")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
lama_webgpu.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75ddeeb344ca4603b261b09fce0e9323e17872042907f5ee205c9875464c8e3b
3
+ size 209033286