dasumae / src /face-worker.js
NagaYu's picture
Upload 6 files
c4413a6 verified
Raw History Blame Contribute Delete
3.63 kB
// Face-detection worker — UltraFace (version-RFB-320) via onnxruntime-web, CPU
// WASM backend, self-hosted. Kept off the main thread so model load + inference
// never freeze the UI. Pre/post-processing mirrors the Phase 0 face spike.
//
// Protocol:
// main -> worker: { type:'init' }
// { type:'detect', rgba:ArrayBuffer(320*240*4), origW, origH, thr }
// worker -> main: { type:'ready' } | { type:'error', message }
// { type:'result', faces:[{x,y,w,h,score}] } (original-image px)
import * as ort from '../vendor/ort/ort.wasm.bundle.min.mjs';
const IN_W = 320, IN_H = 240;
const MODEL_URL = new URL('../vendor/models/version-RFB-320.onnx', import.meta.url);
// All WASM served from our own origin; single-threaded so no cross-origin
// isolation / SharedArrayBuffer is required on the static host.
ort.env.wasm.wasmPaths = new URL('../vendor/ort/', import.meta.url).href;
ort.env.wasm.numThreads = 1;
ort.env.wasm.proxy = false;
let session = null;
let inputName = null;
async function init() {
if (session) return;
const buf = await (await fetch(MODEL_URL)).arrayBuffer();
session = await ort.InferenceSession.create(new Uint8Array(buf), {
executionProviders: ['wasm'],
});
inputName = session.inputNames[0];
}
function toTensor(rgba) {
const chw = new Float32Array(3 * IN_W * IN_H);
const plane = IN_W * IN_H;
for (let y = 0; y < IN_H; y++) {
for (let x = 0; x < IN_W; x++) {
const i = (y * IN_W + x) * 4;
const o = y * IN_W + x;
chw[o] = (rgba[i] - 127) / 128; // R
chw[plane + o] = (rgba[i + 1] - 127) / 128; // G
chw[2 * plane + o] = (rgba[i + 2] - 127) / 128; // B
}
}
return new ort.Tensor('float32', chw, [1, 3, IN_H, IN_W]);
}
function iou(a, b) {
const ix = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
const iy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
const inter = ix * iy;
const uni = a.w * a.h + b.w * b.h - inter;
return uni <= 0 ? 0 : inter / uni;
}
function postprocess(scores, boxes, N, origW, origH, thr) {
const dets = [];
for (let i = 0; i < N; i++) {
const p = scores[i * 2 + 1];
if (p < thr) continue;
const x1 = boxes[i * 4] * origW, y1 = boxes[i * 4 + 1] * origH;
const x2 = boxes[i * 4 + 2] * origW, y2 = boxes[i * 4 + 3] * origH;
dets.push({ score: +p.toFixed(3), x: Math.round(x1), y: Math.round(y1), w: Math.round(x2 - x1), h: Math.round(y2 - y1) });
}
dets.sort((a, b) => b.score - a.score);
const keep = [];
for (const d of dets) {
if (d.w <= 1 || d.h <= 1) continue;
if (!keep.some((k) => iou(k, d) > 0.3)) keep.push(d);
}
return keep;
}
async function detect(rgbaBuf, origW, origH, thr) {
await init();
const input = toTensor(new Uint8ClampedArray(rgbaBuf));
const out = await session.run({ [inputName]: input });
const scoresT = out[session.outputNames.find((n) => /score/i.test(n)) || session.outputNames[0]];
const boxesT = out[session.outputNames.find((n) => /box/i.test(n)) || session.outputNames[1]];
const N = scoresT.dims[1];
return postprocess(scoresT.data, boxesT.data, N, origW, origH, thr);
}
self.onmessage = async (e) => {
const msg = e.data || {};
try {
if (msg.type === 'init') { await init(); self.postMessage({ type: 'ready' }); return; }
if (msg.type === 'detect') {
const faces = await detect(msg.rgba, msg.origW, msg.origH, msg.thr ?? 0.7);
self.postMessage({ type: 'result', faces });
}
} catch (err) {
self.postMessage({ type: 'error', message: String(err && err.message || err) });
}
};