Instructions to use CoderViking/birefnet-lite-onnx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- BiRefNet
How to use CoderViking/birefnet-lite-onnx with BiRefNet:
# Option 1: use with transformers from transformers import AutoModelForImageSegmentation birefnet = AutoModelForImageSegmentation.from_pretrained("CoderViking/birefnet-lite-onnx", trust_remote_code=True)# Option 2: use with BiRefNet # Install from https://github.com/ZhengPeng7/BiRefNet from models.birefnet import BiRefNet model = BiRefNet.from_pretrained("CoderViking/birefnet-lite-onnx") - Notebooks
- Google Colab
- Kaggle
File size: 8,728 Bytes
99ca48f | 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 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | #!/usr/bin/env python
"""Browser-tuned ONNX export of BiRefNet_lite (ZhengPeng7/BiRefNet_lite, MIT).
Why a custom export exists: the community export (onnx-community/BiRefNet_lite-ONNX)
is a dynamic-shape trace whose deformable convolutions decompose into
GatherND/ScatterND/Clip chains. Those ops fall back to CPU on the ONNX Runtime
Web WebGPU EP and materialize im2col tensors of hundreds of MB, which overflows
the 32-bit wasm heap at 1024x1024 (OrtRun std::bad_alloc on every EP).
This export:
* pins the source checkpoint by commit sha and loads it with
transformers' trust_remote_code path (the model code ships in the repo);
* replaces DeformableConv2d.forward with a numerically identical per-tap
GridSample decomposition (one bilinear GridSample + 1x1 Conv per kernel
tap, accumulated) - peak extra memory is one [1,C,H,W] tensor per tap
instead of one [1,C,K,H,W] im2col blob, and the graph uses only ops the
WebGPU EP implements natively (no GatherND, no ScatterND, no Clip);
* traces with a static input shape (TorchScript exporter, opset 17, fp32,
constant folding) so all Shape/Where dynamism folds away;
* validates the patched module against torchvision.ops.deform_conv2d and the
final ONNX against the unpatched PyTorch reference.
Usage: python export_birefnet_lite.py [--size 1024] [--out birefnet_lite.onnx]
"""
import argparse
import sys
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
REPO = 'ZhengPeng7/BiRefNet_lite'
REVISION = '7838f1c3472f827cd8ce13ab5ccc2ce48077360f' # pinned 2026-07-18
def grid_sample_deform_forward(self, x):
"""Drop-in for DeformableConv2d.forward (birefnet.py) built on GridSample.
Equivalent to torchvision.ops.deform_conv2d(input, offset, weight, bias,
stride=1, padding=p, mask=modulator): for kernel tap k at (i, j) the op
samples x at (y + i - p + dy_k, x + j - p + dx_k) bilinearly with zero
padding, scales by the modulation mask, and accumulates W[:, :, i, j] as a
1x1 conv. Offsets are interleaved (dy, dx) per tap, row-major over the
kernel, matching torchvision's layout.
"""
offset = self.offset_conv(x) # [B, 2K, H, W]
modulator = 2.0 * torch.sigmoid(self.modulator_conv(x)) # [B, K, H, W]
B, C, H, W = x.shape
kh, kw = self.regular_conv.kernel_size
pad = self.padding
weight = self.regular_conv.weight # [Cout, Cin, kh, kw]
ys = torch.arange(H, dtype=x.dtype, device=x.device)
xs = torch.arange(W, dtype=x.dtype, device=x.device)
base_y, base_x = torch.meshgrid(ys, xs, indexing='ij') # [H, W] constants
# pre-normalized base grids (grid_sample align_corners=False convention);
# keeping the per-tap shift on the dynamic offset lets constant folding
# share ONE [H, W] grid pair per resolution instead of one per kernel tap
norm_y = (2 * base_y + 1) / H - 1
norm_x = (2 * base_x + 1) / W - 1
out = None
for k in range(kh * kw):
i, j = divmod(k, kw)
py = norm_y + (offset[:, 2 * k] + (i - pad)) * (2.0 / H) # [B, H, W]
px = norm_x + (offset[:, 2 * k + 1] + (j - pad)) * (2.0 / W)
grid = torch.stack([px, py], dim=-1)
sampled = F.grid_sample(x, grid, mode='bilinear', padding_mode='zeros',
align_corners=False)
sampled = sampled * modulator[:, k:k + 1]
contrib = F.conv2d(sampled, weight[:, :, i, j].unsqueeze(-1).unsqueeze(-1))
out = contrib if out is None else out + contrib
if self.regular_conv.bias is not None:
out = out + self.regular_conv.bias.view(1, -1, 1, 1)
return out
def verify_deform_patch(deform_cls):
"""Random-weight equivalence check: patched forward vs torchvision op."""
from torchvision.ops import deform_conv2d
torch.manual_seed(0)
worst = 0.0
for ksize, pad in [(1, 0), (3, 1), (7, 3)]:
m = deform_cls(8, 16, kernel_size=ksize, padding=pad, bias=False)
for p in m.parameters(): # zero-init offsets would hide layout bugs
nn.init.normal_(p, std=0.3)
x = torch.randn(1, 8, 20, 24)
with torch.no_grad():
offset = m.offset_conv(x)
modulator = 2.0 * torch.sigmoid(m.modulator_conv(x))
ref = deform_conv2d(x, offset, m.regular_conv.weight,
m.regular_conv.bias, padding=pad,
mask=modulator, stride=m.stride)
got = grid_sample_deform_forward(m, x)
worst = max(worst, (ref - got).abs().max().item())
return worst
class LogitsWrapper(nn.Module):
"""BiRefNet eval-forward returns a list of multi-scale preds; the last one
is the full-resolution logits map the demo pipeline consumes."""
def __init__(self, net):
super().__init__()
self.net = net
def forward(self, input_image):
return self.net(input_image)[-1]
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--size', type=int, default=1024)
ap.add_argument('--out', default='birefnet_lite.onnx')
args = ap.parse_args()
from transformers import AutoModelForImageSegmentation
torch.set_grad_enabled(False)
model = AutoModelForImageSegmentation.from_pretrained(
REPO, revision=REVISION, trust_remote_code=True)
model.eval().float()
birefnet_mod = sys.modules[type(model).__module__]
# reference logits from the unpatched model (torchvision deform_conv2d)
torch.manual_seed(1)
probe = torch.rand(1, 3, args.size, args.size)
ref_logits = LogitsWrapper(model)(probe)
deform_err = verify_deform_patch(birefnet_mod.DeformableConv2d)
print(f'deform patch vs torchvision: max abs dev {deform_err:.3e}')
assert deform_err < 1e-4, 'GridSample decomposition diverges from torchvision'
birefnet_mod.DeformableConv2d.forward = grid_sample_deform_forward
patched_logits = LogitsWrapper(model)(probe)
patch_err = (ref_logits - patched_logits).abs().max().item()
print(f'patched model vs reference: max abs logits dev {patch_err:.3e}')
assert patch_err < 1e-3
torch.onnx.export(
LogitsWrapper(model),
(probe,),
args.out,
input_names=['input_image'],
output_names=['output_image'],
opset_version=17,
do_constant_folding=True,
dynamo=False,
)
# The TorchScript trace leaves the (all-constant, static-shape) Swin
# attention-mask construction as live ScatterND/Where/Shape chains;
# onnxslim folds them into initializers.
# FusionGemm is skipped so the shared (deduped) MatMul weights are not
# re-split into per-call-site transposed Gemm copies.
import onnxslim
onnxslim.slim(args.out, output_model=args.out,
skip_fusion_patterns=['FusionGemm', 'FusionGemmMul', 'FusionGemmAdd'])
# The backbone is traced twice (multi-scale 'cat' input), and the
# TorchScript exporter emits one initializer per call site - every Swin
# weight lands in the file twice (+116 MB). Dedupe by content hash.
import hashlib
import onnx
m = onnx.load(args.out)
canonical, rename = {}, {}
keep = []
for init in m.graph.initializer:
arr = onnx.numpy_helper.to_array(init)
h = (str(arr.dtype), arr.shape,
hashlib.sha256(arr.tobytes()).hexdigest())
if h in canonical:
rename[init.name] = canonical[h]
else:
canonical[h] = init.name
keep.append(init)
del m.graph.initializer[:]
m.graph.initializer.extend(keep)
for node in m.graph.node:
for idx, name in enumerate(node.input):
if name in rename:
node.input[idx] = rename[name]
print(f'deduped {len(rename)} duplicate initializers')
onnx.save(m, args.out)
import onnx
from collections import Counter
m = onnx.load(args.out, load_external_data=False)
ops = Counter(n.op_type for n in m.graph.node)
print('ops:', sorted(ops.items(), key=lambda kv: -kv[1]))
for banned in ('GatherND', 'ScatterND', 'Clip'):
assert banned not in ops, f'{banned} in graph - not browser-clean'
import onnxruntime as ort_rt
sess = ort_rt.InferenceSession(args.out, providers=['CPUExecutionProvider'])
onnx_logits = sess.run(None, {'input_image': probe.numpy()})[0]
onnx_err = np.abs(ref_logits.numpy() - onnx_logits).max()
alpha_err = np.abs(torch.sigmoid(ref_logits).numpy()
- 1 / (1 + np.exp(-onnx_logits))).max()
print(f'onnx vs torch reference: max abs logits dev {onnx_err:.3e}, '
f'max sigmoid dev {alpha_err:.3e}')
if __name__ == '__main__':
main()
|