Keep the good part.
Make it yours.
Remove a background, perfect the edges, set a new scene. A small creative studio that runs on your device. Your photos never leave this browser.
A fresh canvas awaits.
or paste one, or pick a file. The first run downloads the model (~44 MB, cached afterwards); every pixel stays on this device.
JPG · PNG · WEBP · paste or dropA 1024×1024 mask, a canvas, and nothing on a server.
- Model.
onnx-community/ormbg-ONNX— the open RMBG model, an ISNet architecture under Apache-2.0. On WASM I load the int8-quantized export (44.3 MB); on WebGPU the fp16 export (88.1 MB). Both are fetched from the Hugging Face hub and cached by the browser. - Runtime. transformers.js 3.8.1 from jsDelivr wraps ONNX Runtime Web. Inference runs in a module Web Worker so the page keeps painting while the network is busy.
- Pre/post. The processor resizes to 1024×1024 and rescales to 0–1. The output is a single-channel soft mask in 0–1, which I turn into an 8-bit alpha plane.
- Compositing. The mask is scaled back up to the photo with bilinear filtering and applied with
destination-in, so hair and edges keep their partial alpha. Backgrounds are painted underneath: nothing (checkerboard), a solid colour, a gradient, or the original photo blurred with a canvas filter. - Isolation. GitHub Pages can't send COOP/COEP headers, and without them there's no
SharedArrayBufferand so no WASM threads. A vendoredcoi-serviceworkerre-serves every response with those headers after the first load.
About two and a half seconds a photo, once the page is isolated.
Measured by me in headless Chrome 155 on a Ryzen 7 255 laptop (8 cores / 16 threads), driving this page over the DevTools protocol, three sample photos plus a repeat, four passes. Times come from performance.now() inside the worker; inference is the model call only, at 1024×1024 input.
| setup | model load | inference / photo |
|---|---|---|
| WASM, int8, 8 threads, isolated via service worker — cold cache (44.3 MB download) | 7.3 s | 2.0–2.6 s |
| same, warm cache | 0.25–0.78 s | 2.2–3.3 s |
WASM, int8, not isolated (?noiso) → 1 thread | 0.63 s | 15.6–15.8 s |
WebGPU, fp16 (88.1 MB) — adapter with shader-f16 present | 10.2 s | fails, falls back to WASM |
Pre-processing (resize + rescale) took 57–109 ms and turning the mask into alpha 2–7 ms, so the network is essentially the whole budget. The first inference after a load is usually the slowest; in later screenshot runs it reached 3.7 s. Your own numbers for this device appear in the panel next to the demo.
Headers mattered more than the model.
- Isolation is a 6× switch. The same int8 model on the same machine went from ~15.7 s to ~2.0–2.6 s per photo purely by making the page cross-origin isolated, which lets ONNX Runtime use
SharedArrayBufferthreads. On GitHub Pages that took a 118-line service worker, not a better model. - "WebGPU available" doesn't mean "this model runs on WebGPU". Headless Chrome exposed an fp16-capable adapter and the session compiled, then the first run threw
using ceil() in shape computation is not yet supported for MaxPool: ISNet's pooling layers useceil_mode. So WASM is the default, WebGPU is opt-in, and a failure restarts the worker on WASM — reusing the same worker kept hitting the WebGPU error. - Read the graph, don't guess the names. This export's input is
pixel_valuesand its output isalphas, not theinput/outputin most RMBG snippets. I read them from the ONNX session instead of hard-coding them. - The web build won't look for local models unless told to, so I set
allowLocalModels = falseexplicitly and fetch from the hub. Both this and the input name fail quietly unless you surface worker errors in the page — so every error is shown in the status line.
Good edges, not studio edges.
- On the portrait the long hair comes through soft and mostly right, but the outer strands carry a dark-green fringe from the foliage behind them, and a couple of isolated leaf specks survive in the background. It isn't matting-grade.
- The model decides what "the subject" is, and it's generous. On the dog photo it keeps the whole chair the dog is standing in front of; on the watch photo it keeps the black display cushion the strap is wrapped around. Both are defensible, neither is what I'd want for a product cut.
- Thin, low-contrast parts get partial alpha: the dog's hind legs come out partly transparent against a dark background.
- The mask is predicted at 1024×1024 and upscaled, so on large photos the edge is smoother than the pixels it cuts.
- Portrait blur blurs the original, subject included, so a faint halo can show around the edge. A real lens blur would inpaint behind the subject first.
- First visit costs a 44 MB download and one automatic reload (the service worker installing). WebGPU doesn't run this export with the ONNX Runtime bundled in transformers.js 3.8.1.