wonfy commited on
Commit
041c6b2
·
verified ·
1 Parent(s): fb7dc8c

BiRefNet rebuilt for WebGPU, fp32 and fp16

Browse files
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ZhengPeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: transformers.js
4
+ base_model: ZhengPeng7/BiRefNet
5
+ pipeline_tag: image-segmentation
6
+ tags:
7
+ - background-removal
8
+ - onnx
9
+ - webgpu
10
+ ---
11
+
12
+ # BiRefNet, ONNX for WebGPU
13
+
14
+ [BiRefNet](https://huggingface.co/ZhengPeng7/BiRefNet) for background removal, rebuilt so it runs on WebGPU in the browser. This is the full model with the Swin-L backbone. For the smaller one see [wonfy/BiRefNet_lite-ONNX-webgpu](https://huggingface.co/wonfy/BiRefNet_lite-ONNX-webgpu). The weights are unchanged. Only the graph was rewritten, starting from [onnx-community/BiRefNet-ONNX](https://huggingface.co/onnx-community/BiRefNet-ONNX).
15
+
16
+ The stock export doesn't run on WebGPU. Some of its shaders need more storage buffers than a GPU allows (WebGPU only promises 8 per shader), and its int64 index maths and `Sum` nodes have no WebGPU kernels, so they fall back to the CPU with very large tensors.
17
+
18
+ | File | Precision | Size |
19
+ |---|---|---|
20
+ | `onnx/model.onnx` | fp32 | 1038 MB |
21
+ | `onnx/model_fp16.onnx` | fp16 weights, float32 input and output | 520 MB |
22
+
23
+ Input `input_image`: float32 `[1, 3, 1024, 1024]`, RGB resized to 1024 x 1024, scaled to 0..1, then ImageNet mean `[0.485, 0.456, 0.406]` and std `[0.229, 0.224, 0.225]`.
24
+ Output `output_image`: float32 `[1, 1, 1024, 1024]` logits. Apply a sigmoid to get the alpha mask.
25
+
26
+ ## What was changed
27
+
28
+ 1. **Constant folding** with ONNX Runtime's basic optimizer, which only produces standard ONNX ops. It takes the graph from 21,968 to 5,271 nodes and removes every shape op, `ScatterND`, `Where` and `ConstantOfShape`.
29
+ 2. **4 `Concat` nodes** with 16, 64, 256 and 1024 inputs became trees with at most 6 inputs each.
30
+ 3. **59 `Split` nodes** with up to 32 outputs became trees with at most 6 outputs each.
31
+ 4. **20 `Sum` nodes** became chains of `Add` in the same order, so the result stays bit-identical.
32
+ 5. **80 `GatherND` nodes** (the sampling in the deformable convolutions) became a plain `Gather` on the data reshaped to `[H*W, C]`. Their index maths, which was int64, now runs in float (the values are whole numbers), then `y * W + x` is taken in int32. No int64 tensor is left in the graph. Doing the last step in int32 also keeps the index exact in the fp16 model.
33
+ 6. **fp16:** converted with `onnxconverter-common` and `keep_io_types`, so input and output stay float32.
34
+
35
+ Every node now uses at most 7 storage buffers, under the WebGPU minimum of 8.
36
+
37
+ ## Results
38
+
39
+ On the CPU (ONNX Runtime 1.31, three test images: hair, a car and two cats), compared with the original onnx-community fp32 model:
40
+
41
+ | File | Logits | Alpha mask |
42
+ |---|---|---|
43
+ | `model.onnx` | bit-identical, max difference 0.0 | identical |
44
+ | `model_fp16.onnx` | max difference 0.06 to 1.25 | mean difference under 1e-5, at most 0.02% of pixels move by more than 1/255 |
45
+
46
+ On WebGPU (Chrome 155, AMD Radeon RX 9070 XT, Transformers.js 4.3.1 with ONNX Runtime Web 1.31.0-dev.20260914), `model_fp16.onnx` on the same three inputs:
47
+
48
+ - About 800 ms per image once warm, about 4.3 s to load.
49
+ - 0.5% to 1.3% of pixels move by more than 1/255 from the fp32 CPU result, and 0.01% to 0.02% of pixels change between inside and outside, all of them on edges.
50
+
51
+ The fp32 file is about 1 GB. ONNX Runtime Web reads the whole model into WebAssembly memory before it moves it to the GPU, so the fp16 file is the safer choice in a browser.
52
+
53
+ ## Use it
54
+
55
+ With Transformers.js:
56
+
57
+ ```js
58
+ import { AutoModel, AutoProcessor, RawImage } from "@huggingface/transformers";
59
+
60
+ const model = await AutoModel.from_pretrained("wonfy/BiRefNet-ONNX-webgpu", {
61
+ device: "webgpu",
62
+ dtype: "fp32",
63
+ model_file_name: "model_fp16", // leave this out for the fp32 file
64
+ });
65
+ const processor = await AutoProcessor.from_pretrained("wonfy/BiRefNet-ONNX-webgpu");
66
+
67
+ const image = await RawImage.fromURL("photo.jpg");
68
+ const { pixel_values } = await processor(image);
69
+ const { output_image } = await model({ input_image: pixel_values });
70
+ // alpha = sigmoid(output_image.data[i]), at 1024 x 1024
71
+ ```
72
+
73
+ `dtype: "fp32"` with `model_file_name: "model_fp16"` is the combination that was tested. It loads the fp16 file as it is.
74
+
75
+ With ONNX Runtime Web directly, create an `InferenceSession` from either file with `executionProviders: ["webgpu"]`.
76
+
77
+ ## Rebuild it
78
+
79
+ The scripts are in `scripts/`. They were run with Python 3.13, onnx 1.23.2, onnxruntime 1.31.0, onnxconverter-common 1.16.0, numpy and Pillow.
80
+
81
+ ```
82
+ python scripts/fold.py model.onnx folded.onnx
83
+ python scripts/patch.py folded.onnx onnx/model.onnx
84
+ python scripts/to_fp16.py onnx/model.onnx onnx/model_fp16.onnx
85
+ python scripts/verify.py samples/ results/ model.onnx onnx/model.onnx onnx/model_fp16.onnx
86
+ ```
87
+
88
+ `model.onnx` in the first line is the fp32 file from onnx-community/BiRefNet-ONNX. `patch.py` prints an audit at the end: the most storage buffers any node uses, and any op still producing int64.
89
+
90
+ ## Licence
91
+
92
+ MIT, the same as BiRefNet. See `LICENSE`. BiRefNet is by Peng Zheng et al., [paper](https://arxiv.org/abs/2401.03407) and [code](https://github.com/ZhengPeng7/BiRefNet).
93
+
94
+ BiRefNet's training data includes DIS5K, and DIS5K's own terms restrict commercial use of the dataset. The MIT licence covers the model and code, not the training data. Check what that means for your use.
config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "swin",
3
+ "transformers.js_config": {
4
+ "dtype": "fp32"
5
+ }
6
+ }
onnx/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90a8e190dcc73f0f553f83df14aa0bf7ad888c3cfe8f2180d9ab474ca86462d4
3
+ size 1038412111
onnx/model_fp16.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5a69905dd90e6592a8030d3f7d1701d3c8c6bb840515691b2f7aab2e3d4bfd6
3
+ size 520175713
preprocessor_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "do_rescale": true,
4
+ "do_resize": true,
5
+ "feature_extractor_type": "ViTFeatureExtractor",
6
+ "image_mean": [0.485, 0.456, 0.406],
7
+ "image_processor_type": "ViTFeatureExtractor",
8
+ "image_std": [0.229, 0.224, 0.225],
9
+ "resample": 2,
10
+ "rescale_factor": 0.00392156862745098,
11
+ "size": { "height": 1024, "width": 1024 }
12
+ }
scripts/fold.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import time
3
+
4
+ import onnxruntime as ort
5
+
6
+ source, target = sys.argv[1], sys.argv[2]
7
+ options = ort.SessionOptions()
8
+ options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
9
+ options.optimized_model_filepath = target
10
+ started = time.time()
11
+ ort.InferenceSession(source, options, providers=["CPUExecutionProvider"])
12
+ print(f"folded in {time.time() - started:.1f} s -> {target}")
scripts/patch.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rewrites a constant-folded BiRefNet graph so every WebGPU shader fits in 8 storage buffers and nothing falls back to the CPU."""
2
+ import math
3
+ import sys
4
+ from collections import Counter, defaultdict
5
+
6
+ import numpy as np
7
+ import onnx
8
+ from onnx import TensorProto, helper, numpy_helper, shape_inference
9
+
10
+ FAN = 6
11
+
12
+ source, target = sys.argv[1], sys.argv[2]
13
+ model = shape_inference.infer_shapes(onnx.load(source))
14
+ graph = model.graph
15
+ shapes = {}
16
+ for value in list(graph.value_info) + list(graph.input) + list(graph.output):
17
+ t = value.type.tensor_type
18
+ shapes[value.name] = ([d.dim_value for d in t.shape.dim], t.elem_type)
19
+ inits = {i.name: i for i in graph.initializer}
20
+ for name, init in inits.items():
21
+ shapes[name] = (list(init.dims), init.data_type)
22
+
23
+ counter = Counter()
24
+
25
+
26
+ def fresh(base):
27
+ counter[base] += 1
28
+ return f"wonfy/{base}/{counter[base]}"
29
+
30
+
31
+ new_inits = []
32
+
33
+
34
+ def const(base, array):
35
+ name = fresh(base)
36
+ new_inits.append(numpy_helper.from_array(np.asarray(array), name))
37
+ return name
38
+
39
+
40
+ def attr(node, name, default=None):
41
+ for a in node.attribute:
42
+ if a.name == name:
43
+ return helper.get_attribute_value(a)
44
+ return default
45
+
46
+
47
+ consumers = defaultdict(list)
48
+ for n in graph.node:
49
+ for x in n.input:
50
+ consumers[x].append(n)
51
+ producers = {out: n for n in graph.node for out in n.output}
52
+
53
+ # The int64 index chain of each deformable-conv sample, done in float instead (the values are whole numbers).
54
+ cast_nodes = [n for n in graph.node if n.op_type == "Cast" and attr(n, "to") == TensorProto.INT64]
55
+ region = set()
56
+ frontier = [n.output[0] for n in cast_nodes]
57
+ while frontier:
58
+ name = frontier.pop()
59
+ if name in region:
60
+ continue
61
+ region.add(name)
62
+ for c in consumers[name]:
63
+ if c.op_type in ("Slice", "Add", "Clip", "Reshape"):
64
+ frontier.append(c.output[0])
65
+ elif c.op_type == "Concat":
66
+ region.add(c.output[0])
67
+ elif c.op_type != "GatherND":
68
+ raise SystemExit(f"unexpected consumer {c.op_type} of int64 tensor {name}")
69
+ cast_source = {n.output[0]: n.input[0] for n in cast_nodes}
70
+
71
+ drop = set(id(n) for n in cast_nodes)
72
+ renames = {}
73
+ for n in graph.node:
74
+ if n.op_type in ("Add", "Clip") and n.output[0] in region:
75
+ for i, x in enumerate(n.input):
76
+ if x in inits and inits[x].data_type == TensorProto.INT64:
77
+ n.input[i] = const("float_const", numpy_helper.to_array(inits[x]).astype(np.float32))
78
+ for n in graph.node:
79
+ for i, x in enumerate(n.input):
80
+ if x in cast_source:
81
+ n.input[i] = cast_source[x]
82
+
83
+ out_nodes = []
84
+ stats = Counter()
85
+
86
+
87
+ def emit(op, inputs, outputs, **attrs):
88
+ out_nodes.append(helper.make_node(op, inputs, outputs, name=fresh(op.lower()), **attrs))
89
+
90
+
91
+ def concat_tree(inputs, output, axis):
92
+ while len(inputs) > FAN:
93
+ grouped = []
94
+ for start in range(0, len(inputs), FAN):
95
+ group = inputs[start:start + FAN]
96
+ if len(group) == 1:
97
+ grouped.append(group[0])
98
+ continue
99
+ name = fresh("concat")
100
+ emit("Concat", group, [name], axis=axis)
101
+ grouped.append(name)
102
+ inputs = grouped
103
+ emit("Concat", inputs, [output], axis=axis)
104
+
105
+
106
+ def split_tree(source_name, sizes, outputs, axis):
107
+ if len(outputs) <= FAN:
108
+ emit("Split", [source_name, const("split_sizes", np.array(sizes, dtype=np.int64))], outputs, axis=axis)
109
+ return
110
+ size = math.ceil(len(outputs) / FAN)
111
+ parts, part_sizes = [], []
112
+ for start in range(0, len(outputs), size):
113
+ group_outputs, group_sizes = outputs[start:start + size], sizes[start:start + size]
114
+ name = group_outputs[0] if len(group_outputs) == 1 else fresh("split")
115
+ parts.append((name, group_sizes, group_outputs))
116
+ part_sizes.append(sum(group_sizes))
117
+ split_tree(source_name, part_sizes, [p[0] for p in parts], axis)
118
+ for name, group_sizes, group_outputs in parts:
119
+ if len(group_outputs) > 1:
120
+ split_tree(name, group_sizes, group_outputs, axis)
121
+
122
+
123
+ concat_inputs = {n.output[0]: list(n.input) for n in graph.node if n.op_type == "Concat" and n.output[0] in region}
124
+
125
+ for n in graph.node:
126
+ if id(n) in drop:
127
+ continue
128
+ if n.op_type == "Concat" and n.output[0] in region:
129
+ stats["index concat removed"] += 1
130
+ continue
131
+ if n.op_type == "Concat" and len(n.input) > FAN:
132
+ concat_tree(list(n.input), n.output[0], attr(n, "axis"))
133
+ stats["concat"] += 1
134
+ continue
135
+ if n.op_type == "Split" and len(n.output) > FAN:
136
+ sizes = numpy_helper.to_array(inits[n.input[1]]).tolist()
137
+ axis = attr(n, "axis", 0)
138
+ split_tree(n.input[0], sizes, list(n.output), axis)
139
+ stats["split"] += 1
140
+ continue
141
+ if n.op_type == "Sum":
142
+ total = n.input[0]
143
+ for i, x in enumerate(n.input[1:], start=1):
144
+ name = n.output[0] if i == len(n.input) - 1 else fresh("sum")
145
+ emit("Add", [total, x], [name])
146
+ total = name
147
+ stats["sum"] += 1
148
+ continue
149
+ if n.op_type == "GatherND":
150
+ assert attr(n, "batch_dims") == 2, "only batch_dims 2 is handled"
151
+ data, index = n.input
152
+ data_shape = shapes[data][0]
153
+ assert data_shape[:2] == [1, 1], data_shape
154
+ _, _, height, width, channels = data_shape
155
+ y, x = concat_inputs[index]
156
+ count = shapes[n.output[0]][0][2]
157
+ y32, x32, row, flat, flat1, table, picked = (fresh(k) for k in ("y32", "x32", "row", "flat", "flat1", "table", "picked"))
158
+ emit("Cast", [y], [y32], to=TensorProto.INT32)
159
+ emit("Cast", [x], [x32], to=TensorProto.INT32)
160
+ emit("Mul", [y32, const("width", np.array(width, dtype=np.int32))], [row])
161
+ emit("Add", [row, x32], [flat])
162
+ emit("Reshape", [flat, const("shape", np.array([count], dtype=np.int64))], [flat1])
163
+ emit("Reshape", [data, const("shape", np.array([height * width, channels], dtype=np.int64))], [table])
164
+ emit("Gather", [table, flat1], [picked], axis=0)
165
+ emit("Reshape", [picked, const("shape", np.array([1, 1, count, channels], dtype=np.int64))], [n.output[0]])
166
+ stats["gathernd"] += 1
167
+ continue
168
+ out_nodes.append(n)
169
+
170
+ del graph.node[:]
171
+ graph.node.extend(out_nodes)
172
+ graph.initializer.extend(new_inits)
173
+ del graph.value_info[:]
174
+ used = {x for n in graph.node for x in n.input}
175
+ kept = [i for i in graph.initializer if i.name in used]
176
+ del graph.initializer[:]
177
+ graph.initializer.extend(kept)
178
+ del model.opset_import[:]
179
+ model.opset_import.extend([helper.make_opsetid("", 17)])
180
+
181
+ model = shape_inference.infer_shapes(model, strict_mode=True)
182
+ onnx.checker.check_model(model)
183
+ onnx.save(model, target)
184
+ print("rewrote", dict(stats))
185
+
186
+ # Audit: buffers per node, and anything WebGPU would run on the CPU.
187
+ graph = model.graph
188
+ types = {v.name: v.type.tensor_type.elem_type for v in list(graph.value_info) + list(graph.output) + list(graph.input)}
189
+ cpu_inputs = {("Reshape", 1), ("Split", 1), ("Slice", 1), ("Slice", 2), ("Slice", 3), ("Slice", 4), ("Clip", 1), ("Clip", 2), ("Pad", 1), ("Pad", 2), ("Resize", 1), ("Resize", 2), ("Resize", 3)}
190
+ worst = Counter()
191
+ for n in graph.node:
192
+ buffers = sum(1 for i, x in enumerate(n.input) if x and (n.op_type, i) not in cpu_inputs) + len(n.output)
193
+ worst[n.op_type] = max(worst[n.op_type], buffers)
194
+ print("most buffers per op:", dict(sorted(worst.items(), key=lambda kv: -kv[1])))
195
+ int64 = Counter(n.op_type for n in graph.node if any(types.get(o) == TensorProto.INT64 for o in n.output))
196
+ print("ops still making int64:", dict(int64) or "none")
197
+ print("ops:", dict(Counter(n.op_type for n in graph.node).most_common()))
scripts/to_fp16.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from collections import Counter
3
+
4
+ import onnx
5
+ from onnxconverter_common import float16
6
+
7
+ source, target = sys.argv[1], sys.argv[2]
8
+ model = float16.convert_float_to_float16(onnx.load(source), keep_io_types=True, disable_shape_infer=False)
9
+ onnx.checker.check_model(model)
10
+ onnx.save(model, target)
11
+ graph = model.graph
12
+ float_inits = Counter(onnx.TensorProto.DataType.Name(i.data_type) for i in graph.initializer)
13
+ print("initializer types", dict(float_inits))
14
+ print("casts", sum(1 for n in graph.node if n.op_type == "Cast"), "of", len(graph.node), "nodes")
scripts/verify.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runs each model on the sample images on the CPU and compares its output with the first model's."""
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ import numpy as np
7
+ import onnxruntime as ort
8
+ from PIL import Image
9
+
10
+ samples_dir, out_dir, *models = sys.argv[1:]
11
+ os.makedirs(out_dir, exist_ok=True)
12
+ MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
13
+ STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
14
+
15
+
16
+ def pixels(path):
17
+ image = Image.open(path).convert("RGB")
18
+ resized = np.asarray(image.resize((1024, 1024), Image.BILINEAR), dtype=np.float32) / 255
19
+ return image, ((resized - MEAN) / STD).transpose(2, 0, 1)[None]
20
+
21
+
22
+ def sigmoid(x):
23
+ return 1 / (1 + np.exp(-x))
24
+
25
+
26
+ samples = [(name, *pixels(os.path.join(samples_dir, name))) for name in sorted(os.listdir(samples_dir))]
27
+ reference = {}
28
+ for index, path in enumerate(models):
29
+ label = os.path.splitext(os.path.basename(path))[0]
30
+ options = ort.SessionOptions()
31
+ options.log_severity_level = 3
32
+ started = time.time()
33
+ session = ort.InferenceSession(path, options, providers=["CPUExecutionProvider"])
34
+ print(f"{label}: loaded in {time.time() - started:.1f} s, {os.path.getsize(path) / 1e6:.0f} MB")
35
+ input_name = session.get_inputs()[0].name
36
+ for name, image, x in samples:
37
+ started = time.time()
38
+ logits = session.run(None, {input_name: x})[0].astype(np.float32)[0, 0]
39
+ took = time.time() - started
40
+ alpha = sigmoid(logits)
41
+ Image.fromarray((alpha * 255).round().astype(np.uint8)).resize(image.size, Image.BILINEAR).save(os.path.join(out_dir, f"{os.path.splitext(name)[0]}-{label}.png"))
42
+ if index == 0:
43
+ reference[name] = (logits, alpha)
44
+ print(f" {name}: {took:.1f} s, logits {logits.min():.2f}..{logits.max():.2f}, {100 * (alpha > 0.5).mean():.1f}% foreground")
45
+ continue
46
+ ref_logits, ref_alpha = reference[name]
47
+ diff = np.abs(alpha - ref_alpha)
48
+ print(
49
+ f" {name}: {took:.1f} s, max logit diff {np.abs(logits - ref_logits).max():.2e}, "
50
+ f"alpha diff max {diff.max():.2e} mean {diff.mean():.2e}, "
51
+ f"pixels off by more than 1/255: {100 * (diff > 1 / 255).mean():.3f}%"
52
+ )