images-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev b674d6e71b51
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 604467e480f8468389092a3b
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/cutout.worker.ts
// Background removal on this device: the ISNet general model run by onnxruntime-web, on the graphics
// card through WebGPU when the browser has it, otherwise on the processor. The model and engine are
// downloaded once (see model.ts), checked against their SHA-256 and kept with the Cache API, so later
// uses work offline. Images themselves are never uploaded. Loaded only when someone turns it on.
import * as ort from "onnxruntime-web/webgpu";
import { MODEL_BYTES, MODEL_FILES } from "./model";

const SIDE = 1024;
const CACHE = "images-background-model";
let session: Promise<ort.InferenceSession> | null = null;

async function fetchBytes(url: string, report: (loaded: number) => void): Promise<Uint8Array> {
  const failed = "The background model could not be downloaded. Check your connection and try again.";
  const res = await fetch(url).catch(() => { throw new Error(failed); });
  if (!res.ok || !res.body) throw new Error(failed);
  const chunks: Uint8Array[] = [];
  let loaded = 0;
  const reader = res.body.getReader();
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    loaded += value.length;
    report(loaded);
  }
  return join(chunks);
}

function join(parts: Uint8Array[]): Uint8Array {
  const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
  let at = 0;
  for (const p of parts) { out.set(p, at); at += p.length; }
  return out;
}

async function sha256(bytes: Uint8Array): Promise<string> {
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
  return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
}

/** Every file from the cache, or downloaded, checked and cached, reporting overall progress. */
async function getFiles(): Promise<Uint8Array[]> {
  // Private windows in some browsers refuse storage; then the files are downloaded each time.
  const cache = await caches.open(CACHE).catch(() => null);
  const loaded = MODEL_FILES.map(() => 0);
  let last = 0;
  const report = (i: number, n: number) => {
    loaded[i] = n;
    const sum = loaded.reduce((a, b) => a + b, 0);
    if (sum - last > 256 * 1024 || sum === MODEL_BYTES) { last = sum; self.postMessage({ progress: { loaded: sum, total: MODEL_BYTES } }); }
  };
  const files = await Promise.all(MODEL_FILES.map(async (f, i) => {
    const hit = await cache?.match(f.url);
    if (hit) {
      report(i, f.bytes);
      return new Uint8Array(await hit.arrayBuffer());
    }
    const bytes = await fetchBytes(f.url, (n) => report(i, n));
    if ((await sha256(bytes)) !== f.sha256) throw new Error("The background model did not download correctly. Try again.");
    await cache?.put(f.url, new Response(bytes)).catch(() => {}); // a full disk only means downloading again next time
    return bytes;
  }));
  // Drop files an earlier version of Images kept.
  for (const req of (await cache?.keys()) ?? []) if (!MODEL_FILES.some((f) => f.url === req.url)) await cache!.delete(req);
  return files;
}

function load(): Promise<ort.InferenceSession> {
  session ??= (async () => {
    const [wasm, model] = await getFiles();
    ort.env.wasm.wasmBinary = wasm;
    ort.env.wasm.numThreads = 1; // threads need cross-origin isolation, which the page does not have
    ort.env.logLevel = "error";
    const s = await ort.InferenceSession.create(model, { executionProviders: ["webgpu", "wasm"], logSeverityLevel: 3 });
    self.postMessage({ progress: { loaded: MODEL_BYTES, total: MODEL_BYTES } });
    return s;
  })().catch((err) => {
    session = null;
    throw err;
  });
  return session;
}

/** Returns how much of each pixel is the subject, as 1024 x 1024 values from 0 (background) to 255. */
async function cutout(src: Blob): Promise<Uint8ClampedArray> {
  const s = await load();
  const bmp = await createImageBitmap(src);
  const canvas = new OffscreenCanvas(SIDE, SIDE);
  const ctx = canvas.getContext("2d", { willReadFrequently: true })!;
  ctx.imageSmoothingQuality = "high";
  ctx.drawImage(bmp, 0, 0, SIDE, SIDE);
  bmp.close();
  const px = ctx.getImageData(0, 0, SIDE, SIDE).data;
  const n = SIDE * SIDE;
  const input = new Float32Array(3 * n);
  for (let i = 0; i < n; i++) {
    input[i] = (px[i * 4] - 128) / 256;
    input[n + i] = (px[i * 4 + 1] - 128) / 256;
    input[2 * n + i] = (px[i * 4 + 2] - 128) / 256;
  }
  const name = s.outputNames[0];
  const result = await s.run({ [s.inputNames[0]]: new ort.Tensor("float32", input, [1, 3, SIDE, SIDE]) }, [name]);
  const out = (await result[name].getData()) as Float32Array;
  result[name].dispose();
  let min = Infinity;
  let max = -Infinity;
  for (const v of out) { if (v < min) min = v; if (v > max) max = v; }
  // Stretched a little, so a sure subject is fully solid and a sure background fully clear, with no
  // faint haze left behind; edges in between (hair, fur) stay soft.
  const mask = new Uint8ClampedArray(n);
  const range = max - min || 1;
  for (let i = 0; i < n; i++) mask[i] = (((out[i] - min) / range - 0.04) / 0.92) * 255;
  return mask;
}

self.onmessage = async (e: MessageEvent<{ id: number; src: Blob }>) => {
  const { id, src } = e.data;
  try {
    const mask = await cutout(src);
    self.postMessage({ id, ok: true, value: mask }, { transfer: [mask.buffer] });
  } catch (err) {
    self.postMessage({ id, ok: false, error: (err as Error)?.message || String(err) });
  }
};

Change history

Quality pass: background removal, blur background, paste images
b674d6e71b · 5 hours ago
Bounded source sync
c6622bcbd1 · 1 day ago
Bounded source sync
92ecc89502 · 1 day ago
Bounded source sync
ae2f8b8be0 · 1 day ago