jennyxue commited on
Commit
936b68d
·
verified ·
1 Parent(s): efc89e2

fp16 build of ORMBG

Browse files
Files changed (3) hide show
  1. fp16/config.json +5 -0
  2. fp16/mlx_model.py +200 -0
  3. fp16/model.safetensors +3 -0
fp16/config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ormbg",
3
+ "input_size": 1024,
4
+ "precision": "fp16"
5
+ }
fp16/mlx_model.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ORMBG (Open Remove Background Model, an IS-Net from xuebinqin/DIS) in MLX: a port of schirrmacher/ormbg's
2
+ `ormbg/models/ormbg.py` (Apache-2.0).
3
+
4
+ import mlx_model
5
+ model = mlx_model.load("fp16") # a folder with config.json + model.safetensors
6
+ alpha = mlx_model.matte(model, rgb) # rgb: HxWx3 uint8 -> HxW uint8 alpha (255 = foreground)
7
+
8
+ Runs on its own next to its weights (mlx and numpy). NHWC; each BatchNorm is folded into the conv before it (exact
9
+ in fp32, done at conversion), so the weights are conv kernels and biases only.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+
17
+ import mlx.core as mx
18
+ import mlx.nn as nn
19
+ import numpy as np
20
+
21
+ CONFIG = {"model_type": "ormbg", "input_size": 1024}
22
+ PRECISIONS = ["bf16", "fp16"] # all convolutions: MLX can't quantize them (model.toml)
23
+
24
+
25
+ def resize_bilinear(x, size):
26
+ """torch F.interpolate(mode="bilinear", align_corners=False) for NHWC."""
27
+ b, h, w, c = x.shape
28
+ oh, ow = size
29
+ if (oh, ow) == (h, w):
30
+ return x
31
+
32
+ def axis(n_in, n_out):
33
+ s = (np.arange(n_out, dtype=np.float64) + 0.5) * (n_in / n_out) - 0.5
34
+ s = np.clip(s, 0, None)
35
+ i0 = np.minimum(np.floor(s).astype(np.int32), n_in - 1)
36
+ i1 = np.minimum(i0 + 1, n_in - 1)
37
+ f = (s - i0).astype(np.float32)
38
+ return mx.array(i0), mx.array(i1), mx.array(f)
39
+
40
+ y0, y1, fy = axis(h, oh)
41
+ x0, x1, fx = axis(w, ow)
42
+ fy = fy.astype(x.dtype)[None, :, None, None]
43
+ fx = fx.astype(x.dtype)[None, None, :, None]
44
+ top, bot = x[:, y0], x[:, y1]
45
+ top = top[:, :, x0] * (1 - fx) + top[:, :, x1] * fx
46
+ bot = bot[:, :, x0] * (1 - fx) + bot[:, :, x1] * fx
47
+ return top * (1 - fy) + bot * fy
48
+
49
+
50
+ def maxpool2(x):
51
+ """MaxPool2d(2, 2, ceil_mode=True), NHWC."""
52
+ b, h, w, c = x.shape
53
+ ph, pw = h % 2, w % 2
54
+ if ph or pw:
55
+ x = mx.pad(x, [(0, 0), (0, ph), (0, pw), (0, 0)], constant_values=-float("inf"))
56
+ h, w = h + ph, w + pw
57
+ return x.reshape(b, h // 2, 2, w // 2, 2, c).max(axis=(2, 4))
58
+
59
+
60
+ class REBNCONV(nn.Module):
61
+ def __init__(self, cin, cout, d=1):
62
+ super().__init__()
63
+ self.conv_s1 = nn.Conv2d(cin, cout, 3, padding=d, dilation=d)
64
+
65
+ def __call__(self, x):
66
+ return nn.relu(self.conv_s1(x))
67
+
68
+
69
+ class RSU(nn.Module):
70
+ """RSU-L (L = depth: 7, 6, 5, 4): an encoder-decoder with pooling; dilation 2 at the bottom."""
71
+
72
+ def __init__(self, depth, cin, mid, cout):
73
+ super().__init__()
74
+ self.depth = depth
75
+ self.rebnconvin = REBNCONV(cin, cout)
76
+ setattr(self, "rebnconv1", REBNCONV(cout, mid))
77
+ for i in range(2, depth):
78
+ setattr(self, f"rebnconv{i}", REBNCONV(mid, mid))
79
+ setattr(self, f"rebnconv{depth}", REBNCONV(mid, mid, d=2))
80
+ for i in range(depth - 1, 1, -1):
81
+ setattr(self, f"rebnconv{i}d", REBNCONV(mid * 2, mid))
82
+ self.rebnconv1d = REBNCONV(mid * 2, cout)
83
+
84
+ def __call__(self, x):
85
+ hxin = self.rebnconvin(x)
86
+ enc, hx = [], hxin
87
+ for i in range(1, self.depth):
88
+ hx = getattr(self, f"rebnconv{i}")(hx)
89
+ enc.append(hx)
90
+ if i < self.depth - 1:
91
+ hx = maxpool2(hx)
92
+ hx = getattr(self, f"rebnconv{self.depth}")(hx)
93
+ for i in range(self.depth - 1, 0, -1):
94
+ hx = getattr(self, f"rebnconv{i}d")(mx.concatenate([hx, enc[i - 1]], -1))
95
+ if i > 1:
96
+ hx = resize_bilinear(hx, enc[i - 2].shape[1:3])
97
+ return hx + hxin
98
+
99
+
100
+ class RSU4F(nn.Module):
101
+ """RSU-4F: no pooling, dilations 1, 2, 4, 8."""
102
+
103
+ def __init__(self, cin, mid, cout):
104
+ super().__init__()
105
+ self.rebnconvin = REBNCONV(cin, cout)
106
+ self.rebnconv1 = REBNCONV(cout, mid)
107
+ self.rebnconv2 = REBNCONV(mid, mid, 2)
108
+ self.rebnconv3 = REBNCONV(mid, mid, 4)
109
+ self.rebnconv4 = REBNCONV(mid, mid, 8)
110
+ self.rebnconv3d = REBNCONV(mid * 2, mid, 4)
111
+ self.rebnconv2d = REBNCONV(mid * 2, mid, 2)
112
+ self.rebnconv1d = REBNCONV(mid * 2, cout)
113
+
114
+ def __call__(self, x):
115
+ hxin = self.rebnconvin(x)
116
+ hx1 = self.rebnconv1(hxin)
117
+ hx2 = self.rebnconv2(hx1)
118
+ hx3 = self.rebnconv3(hx2)
119
+ hx4 = self.rebnconv4(hx3)
120
+ hx3d = self.rebnconv3d(mx.concatenate([hx4, hx3], -1))
121
+ hx2d = self.rebnconv2d(mx.concatenate([hx3d, hx2], -1))
122
+ return self.rebnconv1d(mx.concatenate([hx2d, hx1], -1)) + hxin
123
+
124
+
125
+ class Model(nn.Module):
126
+ def __init__(self, config: dict = CONFIG):
127
+ super().__init__()
128
+ self.size = config["input_size"]
129
+ self.conv_in = nn.Conv2d(3, 64, 3, stride=2, padding=1)
130
+ self.stage1, self.stage2 = RSU(7, 64, 32, 64), RSU(6, 64, 32, 128)
131
+ self.stage3, self.stage4 = RSU(5, 128, 64, 256), RSU(4, 256, 128, 512)
132
+ self.stage5, self.stage6 = RSU4F(512, 256, 512), RSU4F(512, 256, 512)
133
+ self.stage5d, self.stage4d = RSU4F(1024, 256, 512), RSU(4, 1024, 128, 256)
134
+ self.stage3d, self.stage2d, self.stage1d = RSU(5, 512, 64, 128), RSU(6, 256, 32, 64), RSU(7, 128, 16, 64)
135
+ self.side1 = nn.Conv2d(64, 1, 3, padding=1)
136
+
137
+ def __call__(self, x):
138
+ """x: (B, S, S, 3) in [0, 1] -> (B, S, S, 1) foreground probability (side output 1, the one used)."""
139
+ hxin = self.conv_in(x)
140
+ hx1 = self.stage1(hxin)
141
+ hx2 = self.stage2(maxpool2(hx1))
142
+ hx3 = self.stage3(maxpool2(hx2))
143
+ hx4 = self.stage4(maxpool2(hx3))
144
+ hx5 = self.stage5(maxpool2(hx4))
145
+ hx6 = self.stage6(maxpool2(hx5))
146
+ up = lambda a, b: resize_bilinear(a, b.shape[1:3])
147
+ hx5d = self.stage5d(mx.concatenate([up(hx6, hx5), hx5], -1))
148
+ hx4d = self.stage4d(mx.concatenate([up(hx5d, hx4), hx4], -1))
149
+ hx3d = self.stage3d(mx.concatenate([up(hx4d, hx3), hx3], -1))
150
+ hx2d = self.stage2d(mx.concatenate([up(hx3d, hx2), hx2], -1))
151
+ hx1d = self.stage1d(mx.concatenate([up(hx2d, hx1), hx1], -1))
152
+ return mx.sigmoid(resize_bilinear(self.side1(hx1d), x.shape[1:3]))
153
+
154
+
155
+ def load(folder) -> Model:
156
+ folder = Path(folder)
157
+ model = Model(json.loads((folder / "config.json").read_text()))
158
+ model.load_weights(str(folder / "model.safetensors"), strict=True)
159
+ mx.eval(model.parameters())
160
+ return model
161
+
162
+
163
+ def predict(model: Model, rgb: np.ndarray) -> np.ndarray:
164
+ """The raw model output at the original size, float32 HxW in [0, 1] (as ormbg/inference.py, before min-max)."""
165
+ dt = model.conv_in.weight.dtype
166
+ h, w, _ = rgb.shape
167
+ x = mx.array(rgb.astype(np.float32))[None]
168
+ x = mx.floor(resize_bilinear(x, (model.size, model.size))) / 255.0 # the original rounds to uint8 first
169
+ y = model(x.astype(dt)).astype(mx.float32)
170
+ return np.array(resize_bilinear(y, (h, w)))[0, :, :, 0]
171
+
172
+
173
+ def matte(model: Model, rgb: np.ndarray) -> np.ndarray:
174
+ """HxWx3 uint8 -> HxW uint8 alpha, min-max normalized like ormbg/inference.py."""
175
+ p = predict(model, rgb)
176
+ p = (p - p.min()) / max(p.max() - p.min(), 1e-8)
177
+ return (p * 255).astype(np.uint8)
178
+
179
+
180
+ def convert(source: Path) -> dict:
181
+ """runs/source/models/ormbg.pth -> conv weights with each BatchNorm folded in (fp32), MLX layout."""
182
+ import torch
183
+ sd = {k: v.double() for k, v in torch.load(Path(source) / "models" / "ormbg.pth", map_location="cpu",
184
+ weights_only=True).items()}
185
+ out = {}
186
+ for k, w in sd.items():
187
+ if not k.endswith(".weight") or w.ndim != 4:
188
+ continue
189
+ base = k[:-len(".weight")]
190
+ b = sd.get(base + ".bias", torch.zeros(w.shape[0], dtype=torch.float64))
191
+ bn = base.replace("conv_s1", "bn_s1")
192
+ if bn != base and bn + ".running_mean" in sd:
193
+ g = sd[bn + ".weight"] / torch.sqrt(sd[bn + ".running_var"] + 1e-5)
194
+ w = w * g[:, None, None, None]
195
+ b = (b - sd[bn + ".running_mean"]) * g + sd[bn + ".bias"]
196
+ if base.startswith("side") and base != "side1":
197
+ continue # side outputs 2-6 only matter for training
198
+ out[base + ".weight"] = w.float().numpy().transpose(0, 2, 3, 1)
199
+ out[base + ".bias"] = b.float().numpy()
200
+ return out
fp16/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7f5a13a20a96569cbe15f3a625e5304ea2b9321cb5e20dc1e1f643bf8d111b3
3
+ size 88032569