Skip to content

Commit 55e57be

Browse files
MagicalTuxclaude
andcommitted
demo: parallel full-frame 2D worker so QR works without the locator
QR/DataMatrix/PDF417 self-localize via their finder patterns, so they don't need the coarse locator at all — but the demo only decoded located crops, so a QR the locator failed to box never decoded. A second worker now runs decode2d on a half-resolution copy of the whole frame (throttled ~400ms) and returns each code's box in full-frame pixels, merged into the same decoded tracks (so it locks and follows like any other). It's a separate worker so its heavier full-frame pass runs in parallel and never stalls the crop worker's ~120ms 1D decoding. When a QR is in view decode2d returns in ~15ms; on textured scenes with no code it self-throttles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 547aabd commit 55e57be

1 file changed

Lines changed: 68 additions & 17 deletions

File tree

web/index.html

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ <h2>Decode an image</h2>
368368
const video = document.getElementById('cam-video');
369369
const overlay = document.getElementById('cam-overlay');
370370
const scratch = document.createElement('canvas');
371-
let stream, running = false, lastSent = 0, lastDecoded = [], workerBusy = false;
371+
let stream, running = false, lastSent = 0, lastDecoded = [], workerBusy = false, worker2dBusy = false, last2d = 0;
372372

373373
// Decoded codes persist for a short window after their last confirmation instead of
374374
// vanishing the instant one frame fails to re-read them. A hand-held camera only
@@ -393,6 +393,16 @@ <h2>Decode an image</h2>
393393
const uni = (a.x1 - a.x0) * (a.y1 - a.y0) + (b.x1 - b.x0) * (b.y1 - b.y0) - inter;
394394
return uni > 0 ? inter / uni : 0;
395395
};
396+
// Fold a batch of decoded results (each {symbology, text, box}) into the persistent
397+
// tracks — shared by the crop worker (1D + located regions) and the full-frame 2D worker.
398+
function mergeDecoded(list, now) {
399+
for (const r of list) {
400+
const key = r.symbology + '|' + r.text;
401+
const tr = decodedTracks.get(key);
402+
if (tr) { tr.lastSeen = now; if (r.box) tr.target = r.box; }
403+
else decodedTracks.set(key, { symbology: r.symbology, text: r.text, box: r.box, target: r.box, lastSeen: now });
404+
}
405+
}
396406

397407
// Located candidates favour recall, so each frame reports transient boxes on text,
398408
// hands and background clutter. Rather than draw every raw hit — which strobes — we
@@ -446,17 +456,46 @@ <h2>Decode an image</h2>
446456
const decodeWorker = new Worker(URL.createObjectURL(new Blob([workerSrc], { type: 'text/javascript' })));
447457
decodeWorker.onmessage = (e) => {
448458
workerBusy = false;
449-
const now = performance.now();
450-
// Merge this batch into the persistent tracks: refresh a code that is still visible,
451-
// register a newly seen one. Expiry happens in the render loop by DECODE_HOLD_MS.
452-
for (const r of e.data) {
453-
const key = r.symbology + '|' + r.text;
454-
const tr = decodedTracks.get(key);
455-
if (tr) { tr.lastSeen = now; if (r.box) tr.target = r.box; }
456-
else decodedTracks.set(key, { symbology: r.symbology, text: r.text, box: r.box, target: r.box, lastSeen: now });
457-
}
459+
// Merge this batch into the persistent tracks; expiry happens in the render loop.
460+
mergeDecoded(e.data, performance.now());
458461
};
459462

463+
// A second, parallel worker reads 2D codes (QR / Data Matrix / PDF417) directly off the
464+
// whole frame — they self-localize, so they do not need the coarse locator, which fixes
465+
// the case where a QR is in view but the locator never boxes it. It runs on a
466+
// half-resolution copy (fast, and big enough for a held-up code) and hands back each
467+
// code's box in full-frame pixels. Kept separate so its heavier full-frame pass never
468+
// stalls the crop worker's snappy 1D decoding.
469+
const worker2dSrc = `
470+
let w; const mem = () => new Uint8Array(w.memory.buffer);
471+
const ready = fetch(${JSON.stringify(WASM_URL)}).then(r => WebAssembly.instantiateStreaming(r, {})).then(x => { w = x.instance.exports; });
472+
self.onmessage = async (e) => {
473+
await ready;
474+
const { width, height, luma, scale } = e.data;
475+
const p = w.alloc(luma.length); mem().set(luma, p);
476+
const pk = w.decode2d(width, height, p); w.dealloc(p, luma.length);
477+
const ptr = Number(pk >> 32n), len = Number(pk & 0xffffffffn);
478+
const bytes = mem().slice(ptr, ptr + len); w.dealloc(ptr, len);
479+
const res = JSON.parse(new TextDecoder().decode(bytes) || '[]');
480+
self.postMessage(res.map(r => ({
481+
symbology: r.symbology, text: r.text,
482+
box: r.box ? { x0: r.box.x0 * scale, y0: r.box.y0 * scale, x1: r.box.x1 * scale, y1: r.box.y1 * scale } : null,
483+
})));
484+
};
485+
`;
486+
const worker2d = new Worker(URL.createObjectURL(new Blob([worker2dSrc], { type: 'text/javascript' })));
487+
worker2d.onmessage = (e) => { worker2dBusy = false; mergeDecoded(e.data, performance.now()); };
488+
489+
// Box-average a luma frame down 2x — a cheap, safe downscale for the full-frame 2D pass.
490+
function downscale2(luma, w, h) {
491+
const w2 = w >> 1, h2 = h >> 1, out = new Uint8Array(w2 * h2);
492+
for (let y = 0; y < h2; y++) {
493+
const s0 = (2 * y) * w, s1 = (2 * y + 1) * w, d = y * w2;
494+
for (let x = 0; x < w2; x++) { const a = 2 * x; out[d + x] = (luma[s0 + a] + luma[s0 + a + 1] + luma[s1 + a] + luma[s1 + a + 1]) >> 2; }
495+
}
496+
return { luma: out, w: w2, h: h2 };
497+
}
498+
460499
function bbox(corners) {
461500
const xs = corners.map(p => p[0]), ys = corners.map(p => p[1]);
462501
return { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) };
@@ -596,7 +635,7 @@ <h2>Decode an image</h2>
596635
document.getElementById('cam-btn').disabled = true;
597636
document.getElementById('cam-stop').disabled = false;
598637
document.getElementById('cam-capture').disabled = false;
599-
running = true; workerBusy = false; lastSent = 0; decodedTracks.clear(); candTracks = []; requestAnimationFrame(loop);
638+
running = true; workerBusy = false; worker2dBusy = false; lastSent = 0; last2d = 0; decodedTracks.clear(); candTracks = []; requestAnimationFrame(loop);
600639
};
601640
document.getElementById('cam-stop').onclick = () => {
602641
running = false; stream && stream.getTracks().forEach(t => t.stop());
@@ -678,13 +717,25 @@ <h2>Decode an image</h2>
678717
}
679718
if (best) { tr.target = best; tr.lastSeen = ts; }
680719
}
681-
// Decode off-thread: send a frame only when the worker is free, so a slow scan
682-
// throttles itself and can never stall the UI.
683-
if (!workerBusy && ts - lastSent > 120) {
684-
lastSent = ts; workerBusy = true;
720+
// Decode off-thread on two parallel workers, each dispatched only when free so a slow
721+
// scan throttles itself and never stalls the UI: the crop worker (1D + located
722+
// regions, every ~120ms) and the full-frame 2D worker (self-localizing QR/2D on a
723+
// half-res copy, every ~400ms). Both read the one luma we build here; the 2D downscale
724+
// happens before the crop worker transfers the buffer away.
725+
const wantCrop = !workerBusy && ts - lastSent > 120;
726+
const want2d = !worker2dBusy && ts - last2d > 400;
727+
if (wantCrop || want2d) {
685728
const { luma } = lumaFrom(img);
686-
const crops = liveCands.map(c => { const b = bbox(c.corners); return { x0: b.x0 - 16, y0: b.y0 - 16, x1: b.x1 + 16, y1: b.y1 + 16 }; });
687-
decodeWorker.postMessage({ width: pw, height: ph, luma, crops }, [luma.buffer]);
729+
if (want2d) {
730+
last2d = ts; worker2dBusy = true;
731+
const d = downscale2(luma, pw, ph);
732+
worker2d.postMessage({ width: d.w, height: d.h, luma: d.luma, scale: 2 }, [d.luma.buffer]);
733+
}
734+
if (wantCrop) {
735+
lastSent = ts; workerBusy = true;
736+
const crops = liveCands.map(c => { const b = bbox(c.corners); return { x0: b.x0 - 16, y0: b.y0 - 16, x1: b.x1 + 16, y1: b.y1 + 16 }; });
737+
decodeWorker.postMessage({ width: pw, height: ph, luma, crops }, [luma.buffer]);
738+
}
688739
}
689740
}
690741

0 commit comments

Comments
 (0)