Skip to content

Commit 03ad329

Browse files
MagicalTuxclaude
andcommitted
demo: track decoded codes across frames via the locate pass
The two-stage pipeline already locates every frame but only decodes occasionally, so a decoded lock used to sit at its last-decoded position and lag behind while the camera moved. Each decoded track now steers toward the located candidate that best overlaps it (IoU match) every frame and eases its drawn box toward that target, so the lock follows the code smoothly as the view moves — the locator does the tracking, the decoder just supplies the value. A locate match also refreshes track liveness (locate only fires on barcode-like texture, so a track still fades once the code leaves frame). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5c5d48c commit 03ad329

1 file changed

Lines changed: 31 additions & 5 deletions

File tree

‎web/index.html‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,24 @@ <h2>Decode an image</h2>
375375
// re-decodes a given code on a fraction of frames (the crop must be well-framed *and*
376376
// the decode must succeed), so without this hold the lock strobes on and off. Each
377377
// 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 }
378+
// Each track carries a `box` (what the HUD draws) that eases toward a `target` (where the
379+
// code currently is). The value comes from the sparse decode; the position is refreshed
380+
// every frame from the fast locate pass, so a lock follows the code as the camera moves
381+
// even between successful reads.
382+
const decodedTracks = new Map(); // "symbology|text" -> { symbology, text, box, target, lastSeen }
379383
const DECODE_HOLD_MS = 1400;
380384
const boxLerp = (a, b, t = 0.35) => !a ? b : !b ? a : {
381385
x0: a.x0 + (b.x0 - a.x0) * t, y0: a.y0 + (b.y0 - a.y0) * t,
382386
x1: a.x1 + (b.x1 - a.x1) * t, y1: a.y1 + (b.y1 - a.y1) * t,
383387
};
388+
// Intersection-over-union of two boxes: how to match a track to a located candidate.
389+
const iou = (a, b) => {
390+
const ix = Math.max(0, Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0));
391+
const iy = Math.max(0, Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0));
392+
const inter = ix * iy;
393+
const uni = (a.x1 - a.x0) * (a.y1 - a.y0) + (b.x1 - b.x0) * (b.y1 - b.y0) - inter;
394+
return uni > 0 ? inter / uni : 0;
395+
};
384396

385397
// Decoding runs in a background worker (its own wasm instance) so the heavy sampler
386398
// can never block the UI thread — the live overlay stays smooth even when a scan is
@@ -416,8 +428,8 @@ <h2>Decode an image</h2>
416428
for (const r of e.data) {
417429
const key = r.symbology + '|' + r.text;
418430
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 });
431+
if (tr) { tr.lastSeen = now; if (r.box) tr.target = r.box; }
432+
else decodedTracks.set(key, { symbology: r.symbology, text: r.text, box: r.box, target: r.box, lastSeen: now });
421433
}
422434
};
423435

@@ -620,6 +632,18 @@ <h2>Decode an image</h2>
620632
});
621633
lastMs = performance.now() - t0;
622634
liveScale = [pw, ph];
635+
// Track: steer each decoded code toward the located candidate that best overlaps it,
636+
// so the lock follows the code across camera movement between the sparse re-decodes.
637+
// A locate match also refreshes the track's liveness — locate only fires on
638+
// barcode-like texture, so a track fades once the code actually leaves the frame.
639+
for (const tr of decodedTracks.values()) {
640+
let best = null, bestIou = 0.15;
641+
for (const c of liveCands) {
642+
const j = iou(tr.target || tr.box, bbox(c.corners));
643+
if (j > bestIou) { bestIou = j; best = bbox(c.corners); }
644+
}
645+
if (best) { tr.target = best; tr.lastSeen = ts; }
646+
}
623647
// Decode off-thread: send a frame only when the worker is free, so a slow scan
624648
// throttles itself and can never stall the UI.
625649
if (!workerBusy && ts - lastSent > 120) {
@@ -630,8 +654,10 @@ <h2>Decode an image</h2>
630654
}
631655
}
632656

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.
657+
// Ease each track's drawn box toward its target every frame (smooth follow), then
658+
// expire tracks whose code has truly been gone for DECODE_HOLD_MS — not on a single
659+
// dropped read.
660+
for (const tr of decodedTracks.values()) if (tr.target) tr.box = boxLerp(tr.box, tr.target, 0.3);
635661
for (const [k, tr] of decodedTracks) if (ts - tr.lastSeen > DECODE_HOLD_MS) decodedTracks.delete(k);
636662
lastDecoded = [...decodedTracks.values()];
637663
document.getElementById('cam-result').textContent =

0 commit comments

Comments
 (0)