Skip to content

Commit 5c5d48c

Browse files
MagicalTuxclaude
andcommitted
demo: persist decoded codes with a hold window to stop lock flicker
A hand-held camera only re-decodes a given code on a fraction of frames, but the HUD replaced its decoded set on every worker message — so any dropped read made the value slate and lock brackets vanish and snap back, reading as constant flicker. Decoded codes now persist in a track map for DECODE_HOLD_MS (1.4s) past their last confirmation, with a position-smoothed box, and expire only after the code is genuinely gone. The lock stays put and the readout is stable across the frames that don't re-decode. Tracks reset on camera start. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d2641e1 commit 5c5d48c

1 file changed

Lines changed: 30 additions & 4 deletions

File tree

web/index.html

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,18 @@ <h2>Decode an image</h2>
370370
const scratch = document.createElement('canvas');
371371
let stream, running = false, lastSent = 0, lastDecoded = [], workerBusy = false;
372372

373+
// Decoded codes persist for a short window after their last confirmation instead of
374+
// vanishing the instant one frame fails to re-read them. A hand-held camera only
375+
// re-decodes a given code on a fraction of frames (the crop must be well-framed *and*
376+
// the decode must succeed), so without this hold the lock strobes on and off. Each
377+
// track keeps a position-smoothed box so the slate does not jitter frame to frame.
378+
const decodedTracks = new Map(); // "symbology|text" -> { symbology, text, box, lastSeen }
379+
const DECODE_HOLD_MS = 1400;
380+
const boxLerp = (a, b, t = 0.35) => !a ? b : !b ? a : {
381+
x0: a.x0 + (b.x0 - a.x0) * t, y0: a.y0 + (b.y0 - a.y0) * t,
382+
x1: a.x1 + (b.x1 - a.x1) * t, y1: a.y1 + (b.y1 - a.y1) * t,
383+
};
384+
373385
// Decoding runs in a background worker (its own wasm instance) so the heavy sampler
374386
// can never block the UI thread — the live overlay stays smooth even when a scan is
375387
// slow. The main thread only sends a frame when the worker is free. It reuses the
@@ -397,9 +409,16 @@ <h2>Decode an image</h2>
397409
`;
398410
const decodeWorker = new Worker(URL.createObjectURL(new Blob([workerSrc], { type: 'text/javascript' })));
399411
decodeWorker.onmessage = (e) => {
400-
lastDecoded = e.data; workerBusy = false;
401-
document.getElementById('cam-result').textContent =
402-
lastDecoded.length ? lastDecoded.map(r => `${r.symbology}: ${r.text}`).join('\n') : '';
412+
workerBusy = false;
413+
const now = performance.now();
414+
// Merge this batch into the persistent tracks: refresh a code that is still visible,
415+
// register a newly seen one. Expiry happens in the render loop by DECODE_HOLD_MS.
416+
for (const r of e.data) {
417+
const key = r.symbology + '|' + r.text;
418+
const tr = decodedTracks.get(key);
419+
if (tr) { tr.lastSeen = now; if (r.box) tr.box = boxLerp(tr.box, r.box); }
420+
else decodedTracks.set(key, { symbology: r.symbology, text: r.text, box: r.box, lastSeen: now });
421+
}
403422
};
404423

405424
function bbox(corners) {
@@ -541,7 +560,7 @@ <h2>Decode an image</h2>
541560
document.getElementById('cam-btn').disabled = true;
542561
document.getElementById('cam-stop').disabled = false;
543562
document.getElementById('cam-capture').disabled = false;
544-
running = true; workerBusy = false; lastSent = 0; requestAnimationFrame(loop);
563+
running = true; workerBusy = false; lastSent = 0; decodedTracks.clear(); requestAnimationFrame(loop);
545564
};
546565
document.getElementById('cam-stop').onclick = () => {
547566
running = false; stream && stream.getTracks().forEach(t => t.stop());
@@ -611,6 +630,13 @@ <h2>Decode an image</h2>
611630
}
612631
}
613632

633+
// Expire held tracks and rebuild the decoded view every frame, so a lock fades only
634+
// after the code has truly been gone for DECODE_HOLD_MS — not on a single dropped read.
635+
for (const [k, tr] of decodedTracks) if (ts - tr.lastSeen > DECODE_HOLD_MS) decodedTracks.delete(k);
636+
lastDecoded = [...decodedTracks.values()];
637+
document.getElementById('cam-result').textContent =
638+
lastDecoded.length ? lastDecoded.map(r => `${r.symbology}: ${r.text}`).join('\n') : '';
639+
614640
// HUD every frame (cheap: canvas only) for a smooth reticle/scan animation.
615641
overlay.width = video.clientWidth; overlay.height = video.clientHeight;
616642
const octx = overlay.getContext('2d');

0 commit comments

Comments
 (0)