perf: run OpenCV in a worker — 10.8MB script parse + embedded WASM compile no longer blocks the main thread
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
/* cv-worker.js — OpenCV board solve, off the main thread.
|
||||||
|
*
|
||||||
|
* WHY A WORKER: the opencv.js build embeds its ~8MB WASM as a base64 data URI
|
||||||
|
* inside a 10.8MB script. Loading it on the main thread means parsing that
|
||||||
|
* script, decoding the base64, and compiling the WASM synchronously — seconds
|
||||||
|
* of frozen UI and a "page unresponsive" prompt on mobile. In a worker the
|
||||||
|
* whole cost is off-thread, and per-frame solvePnP goes with it.
|
||||||
|
*
|
||||||
|
* Protocol (main <-> worker):
|
||||||
|
* -> { type:'init' } <- { type:'ready' } | { type:'failed', msg }
|
||||||
|
* -> { type:'solve', id, obj, img, n, W,H,f } <- { type:'pose', id, ok, C, m3, reproj, n }
|
||||||
|
* -> { type:'reset' } (drop temporal warm start)
|
||||||
|
* obj/img are plain Float64Array payloads (transferred, so no copy).
|
||||||
|
* The worker never touches three.js: it returns the camera position and the
|
||||||
|
* 3x3 world-from-camera rotation, and the client builds the quaternion.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let cv = null;
|
||||||
|
let prev = null; // { r:[3], t:[3], at } temporal warm start
|
||||||
|
const PREV_TTL = 1500;
|
||||||
|
const MAX_REPROJ_PX = 8;
|
||||||
|
|
||||||
|
// reusable Mats — allocating 7 Mats per frame was a measurable cost
|
||||||
|
let bufN = 0;
|
||||||
|
let objM = null, imgM = null, rvec = null, tvec = null, R = null, proj = null, jac = null, K = null, dist = null;
|
||||||
|
let kW = 0, kH = 0, kF = 0;
|
||||||
|
|
||||||
|
function ensureBuffers(n) {
|
||||||
|
if (bufN === n) return;
|
||||||
|
for (const m of [objM, imgM]) if (m) m.delete();
|
||||||
|
objM = new cv.Mat(n, 3, cv.CV_64F);
|
||||||
|
imgM = new cv.Mat(n, 2, cv.CV_64F);
|
||||||
|
bufN = n;
|
||||||
|
}
|
||||||
|
function ensureIntrinsics(W, H, f) {
|
||||||
|
if (K && kW === W && kH === H && kF === f) return;
|
||||||
|
if (K) { K.delete(); dist.delete(); }
|
||||||
|
K = cv.matFromArray(3, 3, cv.CV_64F, [f, 0, W / 2, 0, f, H / 2, 0, 0, 1]);
|
||||||
|
dist = cv.Mat.zeros(4, 1, cv.CV_64F);
|
||||||
|
kW = W; kH = H; kF = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function solve(msg) {
|
||||||
|
const { obj, img, n, W, H, f } = msg;
|
||||||
|
ensureIntrinsics(W, H, f);
|
||||||
|
ensureBuffers(n);
|
||||||
|
objM.data64F.set(obj);
|
||||||
|
imgM.data64F.set(img);
|
||||||
|
|
||||||
|
const fresh = prev && (Date.now() - prev.at) < PREV_TTL;
|
||||||
|
if (fresh) {
|
||||||
|
// warm start: iterative LM from the last frame. Fast, and it resolves
|
||||||
|
// single-marker planar flips by temporal continuity.
|
||||||
|
rvec.data64F.set(prev.r); tvec.data64F.set(prev.t);
|
||||||
|
cv.solvePnP(objM, imgM, K, dist, rvec, tvec, true, cv.SOLVEPNP_ITERATIVE);
|
||||||
|
} else {
|
||||||
|
let ok = false;
|
||||||
|
try { ok = cv.solvePnP(objM, imgM, K, dist, rvec, tvec, false, cv.SOLVEPNP_SQPNP); } catch { ok = false; }
|
||||||
|
if (!ok) { try { ok = cv.solvePnP(objM, imgM, K, dist, rvec, tvec, false, cv.SOLVEPNP_IPPE); } catch { ok = false; } }
|
||||||
|
if (!ok) return { ok: false };
|
||||||
|
cv.solvePnP(objM, imgM, K, dist, rvec, tvec, true, cv.SOLVEPNP_ITERATIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reprojection gate — rejects poisoned frames before they reach the filter
|
||||||
|
cv.projectPoints(objM, rvec, tvec, K, dist, proj, jac);
|
||||||
|
let err = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
err += Math.hypot(proj.data64F[2 * i] - img[2 * i], proj.data64F[2 * i + 1] - img[2 * i + 1]);
|
||||||
|
}
|
||||||
|
err /= n;
|
||||||
|
if (err > MAX_REPROJ_PX) { prev = null; return { ok: false, reproj: err }; }
|
||||||
|
|
||||||
|
prev = { r: [...rvec.data64F], t: [...tvec.data64F], at: Date.now() };
|
||||||
|
|
||||||
|
cv.Rodrigues(rvec, R);
|
||||||
|
const d = R.data64F; // row-major world->cvCam
|
||||||
|
const t = tvec.data64F;
|
||||||
|
// camera position in world: C = -R^T t
|
||||||
|
const C = [
|
||||||
|
-(d[0] * t[0] + d[3] * t[1] + d[6] * t[2]),
|
||||||
|
-(d[1] * t[0] + d[4] * t[1] + d[7] * t[2]),
|
||||||
|
-(d[2] * t[0] + d[5] * t[1] + d[8] * t[2]),
|
||||||
|
];
|
||||||
|
// world-from-threeCam = R^T * diag(1,-1,-1), row-major for Matrix4.set
|
||||||
|
const m3 = [d[0], -d[3], -d[6], d[1], -d[4], -d[7], d[2], -d[5], -d[8]];
|
||||||
|
return { ok: true, C, m3, reproj: err };
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (ev) => {
|
||||||
|
const msg = ev.data;
|
||||||
|
if (msg.type === 'init') {
|
||||||
|
try {
|
||||||
|
importScripts('/vendor/opencv.js');
|
||||||
|
} catch (e) {
|
||||||
|
self.postMessage({ type: 'failed', msg: 'importScripts: ' + (e && e.message || e) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mod = self.cv;
|
||||||
|
const finish = (m) => {
|
||||||
|
cv = m;
|
||||||
|
rvec = new cv.Mat(3, 1, cv.CV_64F);
|
||||||
|
tvec = new cv.Mat(3, 1, cv.CV_64F);
|
||||||
|
R = new cv.Mat(); proj = new cv.Mat(); jac = new cv.Mat();
|
||||||
|
self.postMessage({ type: 'ready' });
|
||||||
|
};
|
||||||
|
if (!mod) return self.postMessage({ type: 'failed', msg: 'cv missing after importScripts' });
|
||||||
|
if (mod.Mat) return finish(mod);
|
||||||
|
if (typeof mod.then === 'function') return mod.then(finish, (e) => self.postMessage({ type: 'failed', msg: String(e) }));
|
||||||
|
mod.onRuntimeInitialized = () => finish(self.cv || mod);
|
||||||
|
// safety poll: some builds consume onRuntimeInitialized before we attach
|
||||||
|
const t0 = Date.now();
|
||||||
|
(function poll() {
|
||||||
|
if (cv) return;
|
||||||
|
const m = self.cv;
|
||||||
|
if (m && m.Mat) return finish(m);
|
||||||
|
if (Date.now() - t0 > 60000) return self.postMessage({ type: 'failed', msg: 'init timeout' });
|
||||||
|
setTimeout(poll, 100);
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'reset') { prev = null; return; }
|
||||||
|
|
||||||
|
if (msg.type === 'solve') {
|
||||||
|
if (!cv) return self.postMessage({ type: 'pose', id: msg.id, ok: false });
|
||||||
|
let out;
|
||||||
|
try { out = solve(msg); }
|
||||||
|
catch (e) { prev = null; out = { ok: false }; }
|
||||||
|
self.postMessage({ type: 'pose', id: msg.id, n: msg.n, ...out });
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user