Views
No views yet
onnxruntime-web,
meant to be run together. Built for Danmu, a local-first interior decoration
app — everything runs on-device, and works offline once the files are cached.| recall | |
|---|---|
yolov8n-oiv7 alone | 7/19 (37%) |
yolov8s-worldv2-danmu alone | 10/19 (53%) |
| both | 13/19 (68%) |
| File | Size | Output | Notes |
|---|---|---|---|
yolov8n-oiv7.onnx | 14.2 MB | 1x605x8400 | YOLOv8 nano, Open Images V7, 601 fixed classes |
yolov8n-oiv7.names.json | 12 KB | — | class index → name, 601 entries |
yolov8s-worldv2-danmu.onnx | 50.4 MB | 1x48x8400 | YOLO-World small, 44 furniture prompts baked in |
1x3x640x640 NCHW, and share the standard YOLOv8
detect-head layout: channels are cx, cy, w, h then per-class scores
(605 - 4 = 601, 48 - 4 = 44) across 8400 anchors. No objectness channel and
no sigmoid — scores are ready to threshold. Boxes are in letterboxed 640-space
and must be unpadded.onnxruntime-web on WASM or WebGPU.yolov8s-worldv2-danmu.onnx was produced with Ultralytics set_classes(),
which runs the CLIP text encoder once at export and freezes the embeddings into
the graph — so no text encoder is needed at runtime. Class index N is
PROMPTS[N], in exactly this order:1const PROMPTS = [
2 'sofa', 'couch', 'armchair',
3 'chair', 'office chair', 'stool',
4 'table', 'coffee table', 'dining table',
5 'desk',
6 'bed', 'mattress',
7 'nightstand',
8 'wardrobe', 'closet', 'chest of drawers', 'storage cabinet',
9 'shelf', 'bookshelf', 'shoe rack',
10 'mirror',
11 'curtain', 'window curtain', 'window blind',
12 'picture frame', 'wall art', 'poster',
13 'lamp', 'light bulb', 'ceiling light',
14 'ceiling fan', 'electric fan',
15 'refrigerator',
16 'potted plant',
17 'door', 'wooden door',
18 'computer monitor',
19 'television',
20 'window',
21 'laptop',
22 'washing machine',
23 'microwave oven',
24 'clothes rack', 'hanging clothes',
25];clothes rack, hanging clothes, storage cabinet → wardrobe). Real rooms contain clothes rails and
stacked fabric cubes, not the canonical Wardrobe a fixed-label model was
trained on, and naming what is actually there is the whole advantage of an
open-vocabulary model. To retarget it, re-export with your own prompt list.1const BASE = 'https://huggingface.co/DearthAI/danmu-detector/resolve/main/';
2
3const providers = [];
4if (typeof navigator !== 'undefined' && 'gpu' in navigator) providers.push('webgpu');
5providers.push('wasm');
6
7const oiv = await ort.InferenceSession.create(BASE + 'yolov8n-oiv7.onnx', { executionProviders: providers });
8const world = await ort.InferenceSession.create(BASE + 'yolov8s-worldv2-danmu.onnx', { executionProviders: providers });
9const names = await (await fetch(BASE + 'yolov8n-oiv7.names.json')).json();/resolve/ URL, not /blob/ — /blob/ returns the HTML viewer page,
which passes a HEAD status check and then hands your runtime a page of markup.0..1,
feed planar RGB (all R, then all G, then all B — not interleaved).1const INPUT = 640;
2
3function toTensor(img, ox, oy, cw, ch) { // crop rect in source pixels
4 const scale = INPUT / Math.max(cw, ch);
5 const sw = Math.round(cw * scale), sh = Math.round(ch * scale);
6 const dw = Math.floor((INPUT - sw) / 2), dh = Math.floor((INPUT - sh) / 2);
7
8 const c = document.createElement('canvas');
9 c.width = c.height = INPUT;
10 const ctx = c.getContext('2d');
11 ctx.fillStyle = '#727272'; // letterbox grey
12 ctx.fillRect(0, 0, INPUT, INPUT);
13 ctx.drawImage(img, ox, oy, cw, ch, dw, dh, sw, sh);
14
15 const px = ctx.getImageData(0, 0, INPUT, INPUT).data;
16 const area = INPUT * INPUT;
17 const data = new Float32Array(3 * area);
18 for (let i = 0; i < area; i++) {
19 data[i] = px[i * 4] / 255;
20 data[area + i] = px[i * 4 + 1] / 255;
21 data[2 * area + i] = px[i * 4 + 2] / 255;
22 }
23 return { data, scale, dw, dh };
24}1function tilesFor(iw, ih) {
2 const ox = iw * 0.15, oy = ih * 0.15;
3 const crops = [{ ox: 0, oy: 0, cw: iw, ch: ih }];
4 for (let r = 0; r < 2; r++)
5 for (let c = 0; c < 2; c++) {
6 const x0 = Math.max(0, c * iw / 2 - ox), y0 = Math.max(0, r * ih / 2 - oy);
7 const x1 = Math.min(iw, (c + 1) * iw / 2 + ox);
8 const y1 = Math.min(ih, (r + 1) * ih / 2 + oy);
9 crops.push({ ox: x0, oy: y0, cw: x1 - x0, ch: y1 - y0 });
10 }
11 return crops;
12}1const CONF = 0.35, IOU_T = 0.45, MAX_PER_IMAGE = 12;
2const pool = [];
3
4for (const crop of tilesFor(iw, ih)) {
5 const pre = toTensor(img, crop.ox, crop.oy, crop.cw, crop.ch);
6 const toX = v => (crop.ox + (v - pre.dw) / pre.scale) / iw;
7 const toY = v => (crop.oy + (v - pre.dh) / pre.scale) / ih;
8
9 for (const [sess, labelOf] of [
10 [oiv, i => names[String(i)]],
11 [world, i => PROMPTS[i]],
12 ]) {
13 const t = new ort.Tensor('float32', pre.data.slice(), [1, 3, INPUT, INPUT]);
14 const out = (await sess.run({ [sess.inputNames[0]]: t }))[sess.outputNames[0]];
15 const [, channels, anchors] = out.dims;
16 const nc = channels - 4, d = out.data;
17 const at = (ch, a) => d[ch * anchors + a];
18
19 for (let a = 0; a < anchors; a++) {
20 let best = 0, bestC = -1;
21 for (let ci = 0; ci < nc; ci++) {
22 const s = at(4 + ci, a);
23 if (s > best) { best = s; bestC = ci; }
24 }
25 if (best < CONF || bestC < 0) continue;
26 pool.push({
27 x: toX(at(0, a)), y: toY(at(1, a)),
28 w: at(2, a) / pre.scale / iw,
29 h: at(3, a) / pre.scale / ih,
30 conf: best, label: labelOf(bestC),
31 });
32 }
33 }
34}1function iou(a, b) {
2 const x1 = Math.max(a.x - a.w / 2, b.x - b.w / 2);
3 const y1 = Math.max(a.y - a.h / 2, b.y - b.h / 2);
4 const x2 = Math.min(a.x + a.w / 2, b.x + b.w / 2);
5 const y2 = Math.min(a.y + a.h / 2, b.y + b.h / 2);
6 const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
7 return inter / (a.w * a.h + b.w * b.h - inter + 1e-9);
8}
9
10const kept = [];
11for (const b of [...pool].sort((p, q) => q.conf - p.conf)) {
12 if (kept.every(k => iou(k, b) < IOU_T)) kept.push(b);
13 if (kept.length >= MAX_PER_IMAGE) break;
14}
15const boxes = kept.map(b => ({
16 label: b.label, conf: b.conf,
17 x: b.x - b.w / 2, y: b.y - b.h / 2, w: b.w, h: b.h,
18}));dw/dh before dividing is the step that is easy to miss — skip it
and every box drifts toward the image centre on non-square inputs.sofa(0.29).MAX_PER_IMAGE 12. Raising it to 30 added one box and changed no score.yolov8s/m/x-oiv7 (46 / 105 / 275 MB) all score the
same 7/19 as the 14 MB nano. Spend the bytes on the second model instead.1pip install --index-url https://download.pytorch.org/whl/cpu torch
2pip install ultralytics onnx onnxslim onnxruntime clip-anytorch ftfy
3
4python - <<'PY'
5from ultralytics import YOLO
6YOLO('yolov8n-oiv7.pt').export(format='onnx', imgsz=640, opset=12)
7
8m = YOLO('yolov8s-worldv2.pt')
9m.set_classes(PROMPTS) # the 44 phrases above, in order
10m.export(format='onnx', imgsz=640, opset=12)
11PY