● your private photo workbench01 / cut · 02 / refine · 03 / export

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.

THE WORKBENCHmodel not loaded · ~44 mb on first run

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 drop
samples
01 — how it works

A 1024×1024 mask, a canvas, and nothing on a server.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Isolation. GitHub Pages can't send COOP/COEP headers, and without them there's no SharedArrayBuffer and so no WASM threads. A vendored coi-serviceworker re-serves every response with those headers after the first load.
02 — measured

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.

setupmodel loadinference / photo
WASM, int8, 8 threads, isolated via service worker — cold cache (44.3 MB download)7.3 s2.0–2.6 s
same, warm cache0.25–0.78 s2.2–3.3 s
WASM, int8, not isolated (?noiso) → 1 thread0.63 s15.6–15.8 s
WebGPU, fp16 (88.1 MB) — adapter with shader-f16 present10.2 sfails, 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.

03 — what i learned

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 SharedArrayBuffer threads. 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 use ceil_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_values and its output is alphas, not the input/output in 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 = false explicitly 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.
04 — limits

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.