File size: 7,511 Bytes
5a64a48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# /// script
# requires-python = ">=3.11,<3.13"
# dependencies = ["torch==2.9.0", "torchvision==0.24.0", "transformers==4.57.1", "timm", "kornia", "einops",
#                 "numpy", "pillow", "coreai-torch==0.4.3", "coreai-core==1.0.0b3"]
# ///
"""BiRefNet general (Zheng et al., CAAI AIR 2024; MIT) → cutout-birefnet-<dtype>.aimodel.

    uv run Tools/export_birefnet.py [--dtype float16|float32] [--out Tools/exports] [--check image.jpg …]

The network is the authors' own (ZhengPeng7/BiRefNet at a pinned revision, loaded with its remote code); one
change, made for the runtime and checked against the original: torchvision's `deform_conv2d`, which Core AI does
not have, is rewritten as what it computes — each kernel tap's bilinear sample (four gathers from the zero-padded
feature map), scaled by the modulator, then a matrix product with that tap's weights, summed over the taps.
Sample positions are computed in float32 even in a float16 export: at 256 px a half float steps by 0.125, which
would move every bilinear weight.

One entry point, `main`: `image` [1,3,1024,1024] float32 RGB in 0…1 (resized by the host) → `alpha`
[1,1,1024,1024] float32 in 0…1. The ImageNet normalisation and the final sigmoid are inside. In a float16 export
the weights and arithmetic are half; input and output stay float32, so the host is the same for both.

--check runs the original network (torchvision's deform_conv2d) and the rewritten one on each image in float32 on
the CPU and prints the alpha difference, before anything is exported."""
import argparse, os, sys, time, shutil
from pathlib import Path
import numpy as np, torch, torch.nn as nn, torch.nn.functional as F

HERE = Path(__file__).resolve().parent
REPOSITORY = "ZhengPeng7/BiRefNet"
REVISION = "e2bf8e4460fc8fa32bba5ea4d94b3233d367b0e4"
SIZE = 1024

ap = argparse.ArgumentParser()
ap.add_argument("--dtype", default="float16", choices=["float16", "float32"])
ap.add_argument("--out", default=str(HERE / "exports"))
ap.add_argument("--check", nargs="*", default=[], help="images to compare original vs rewritten on (float32, CPU)")
ap.add_argument("--no-export", action="store_true")
args = ap.parse_args()

from transformers import AutoModelForImageSegmentation
torch.set_num_threads(4)
model = AutoModelForImageSegmentation.from_pretrained(REPOSITORY, trust_remote_code=True, revision=REVISION).float().eval()
remote = sys.modules[type(model).__module__]
print(f"BiRefNet general @ {REVISION[:7]}: {sum(p.numel() for p in model.parameters())/1e6:.1f} M parameters", flush=True)


def deform_conv2d(x, offset, weight, bias, padding, mask):
    """torchvision.ops.deform_conv2d (stride 1, dilation 1, one offset group), from gathers and matmuls."""
    _, C, H, W = x.shape
    O, _, kh, kw = weight.shape
    pad = padding if isinstance(padding, int) else padding[0]
    Ho, Wo = H + 2 * pad - kh + 1, W + 2 * pad - kw + 1
    Hp, Wp = H + 2, W + 2
    # One row of zeros round the map: every corner that falls outside reads 0, as torchvision's does.
    table = F.pad(x, (1, 1, 1, 1)).reshape(C, Hp * Wp).t()                      # [Hp*Wp, C]
    off = offset.float().reshape(kh * kw, 2, Ho, Wo)                              # (dy, dx) per tap
    mod = mask.float().reshape(kh * kw, Ho * Wo, 1)
    ys = torch.arange(Ho, dtype=torch.float32).reshape(Ho, 1) - pad
    xs = torch.arange(Wo, dtype=torch.float32).reshape(1, Wo) - pad
    out = None
    for k in range(kh * kw):
        ky, kx = divmod(k, kw)
        py = ys + ky + off[k, 0]
        px = xs + kx + off[k, 1]
        y0, x0 = torch.floor(py), torch.floor(px)
        ly, lx = py - y0, px - x0
        y0c, y1c = y0.clamp(-1, H) + 1, (y0 + 1).clamp(-1, H) + 1
        x0c, x1c = x0.clamp(-1, W) + 1, (x0 + 1).clamp(-1, W) + 1
        sample = None
        for yc, xc, w in ((y0c, x0c, (1 - ly) * (1 - lx)), (y0c, x1c, (1 - ly) * lx),
                          (y1c, x0c, ly * (1 - lx)), (y1c, x1c, ly * lx)):
            index = (yc * Wp + xc).reshape(-1).to(torch.int64)
            term = F.embedding(index, table) * (w.reshape(-1, 1) * mod[k]).to(x.dtype)
            sample = term if sample is None else sample + term
        part = sample @ weight[:, :, ky, kx].t()                                  # [Ho*Wo, O]
        out = part if out is None else out + part
    out = out.t().reshape(1, O, Ho, Wo)
    return out if bias is None else out + bias.reshape(1, O, 1, 1)


def rewritten_forward(self, x):
    offset = self.offset_conv(x)
    modulator = 2. * torch.sigmoid(self.modulator_conv(x))
    return deform_conv2d(x, offset, self.regular_conv.weight, self.regular_conv.bias, self.padding, modulator)


original_forward = remote.DeformableConv2d.forward
MEAN = torch.tensor([0.485, 0.456, 0.406]).reshape(1, 3, 1, 1)
STD = torch.tensor([0.229, 0.224, 0.225]).reshape(1, 3, 1, 1)


class Net(nn.Module):
    """0…1 RGB in, alpha out: the normalisation and the sigmoid inside, the last (full-size) prediction only."""
    def __init__(self, m, dtype):
        super().__init__(); self.m = m.to(dtype); self.dtype = dtype
        self.register_buffer("mean", MEAN.to(dtype)); self.register_buffer("std", STD.to(dtype))
    def forward(self, image):
        x = (image.to(self.dtype) - self.mean) / self.std
        return self.m(x)[-1].sigmoid().to(torch.float32)


if args.check:
    from PIL import Image
    with torch.no_grad():
        for path in args.check:
            im = Image.open(path).convert("RGB").resize((SIZE, SIZE), Image.BILINEAR)
            x = torch.from_numpy(np.asarray(im, np.float32) / 255).permute(2, 0, 1)[None]
            remote.DeformableConv2d.forward = original_forward
            a = Net(model, torch.float32)(x)
            remote.DeformableConv2d.forward = rewritten_forward
            b = Net(model, torch.float32)(x)
            d = (a - b).abs()
            print(f"{Path(path).name}: |original − rewritten| max {d.max():.2e} mean {d.mean():.2e}", flush=True)

if args.no_export:
    sys.exit(0)

import coreai_torch
from coreai.runtime import AIModelAssetMetadata
remote.DeformableConv2d.forward = rewritten_forward
dtype = torch.float16 if args.dtype == "float16" else torch.float32
net = Net(model, dtype).eval()
t0 = time.time()
converter = coreai_torch.TorchConverter(mode=coreai_torch.TorchConverter.Mode.RELEASE)
with torch.no_grad():
    ex = torch.export.export(net, (torch.rand(1, 3, SIZE, SIZE),)).run_decompositions(coreai_torch.get_decomp_table())
    print(f"traced in {time.time()-t0:.0f} s", flush=True)
    converter.add_exported_program(ex, input_names=["image"], output_names=["alpha"], entrypoint_name="main")
program = converter.to_coreai()
print(f"converted in {time.time()-t0:.0f} s", flush=True)
meta = AIModelAssetMetadata()
meta.author = "Zheng, Gao, Fan, Liu, Laaksonen, Ouyang, Sebe — BiRefNet (CAAI AIR 2024), general; Core AI export by ObjectCutout"
meta.license = "MIT"
meta.model_description = (f"BiRefNet general ({REPOSITORY}@{REVISION[:7]}), {args.dtype}. Entry 'main': image [1,3,{SIZE},{SIZE}] "
                          f"float32 RGB 0…1 → alpha [1,1,{SIZE},{SIZE}] float32 0…1. Resize to {SIZE}² and back in the host.")
meta.creation_date = int(time.time())
out = Path(args.out); out.mkdir(parents=True, exist_ok=True)
asset = out / f"cutout-birefnet-{args.dtype}.aimodel"
if asset.exists(): shutil.rmtree(asset)
program.save_asset(asset, meta)
print(f"saved {asset} ({sum(f.stat().st_size for f in asset.rglob('*'))/1e6:.0f} MB) in {time.time()-t0:.0f} s", flush=True)