@@ -394,6 +394,30 @@ <h2>Decode an image</h2>
394394 return uni > 0 ? inter / uni : 0 ;
395395} ;
396396
397+ // Located candidates favour recall, so each frame reports transient boxes on text,
398+ // hands and background clutter. Rather than draw every raw hit — which strobes — we
399+ // track candidates across locate cycles and only surface one once it has been seen in a
400+ // few consecutive frames (a real object persists; single-frame noise does not).
401+ let candTracks = [ ] ; // { box, target, lastSeen, hits }
402+ const CAND_MIN_HITS = 3 ; // consecutive locate cycles before a candidate is drawn
403+ const CAND_HOLD_MS = 260 ; // drop a track not re-located within this window
404+ const CAND_IOU = 0.25 ; // overlap needed to treat two hits as the same object
405+ function updateCandTracks ( cands , now ) {
406+ const boxes = cands . map ( c => bbox ( c . corners ) ) ;
407+ const used = new Set ( ) ;
408+ for ( const b of boxes ) {
409+ let best = - 1 , bestIou = CAND_IOU ;
410+ for ( let i = 0 ; i < candTracks . length ; i ++ ) {
411+ if ( used . has ( i ) ) continue ;
412+ const j = iou ( candTracks [ i ] . target , b ) ;
413+ if ( j > bestIou ) { bestIou = j ; best = i ; }
414+ }
415+ if ( best >= 0 ) { const t = candTracks [ best ] ; t . target = b ; t . lastSeen = now ; t . hits ++ ; used . add ( best ) ; }
416+ else candTracks . push ( { box : b , target : b , lastSeen : now , hits : 1 } ) ;
417+ }
418+ candTracks = candTracks . filter ( t => now - t . lastSeen <= CAND_HOLD_MS ) ;
419+ }
420+
397421// Decoding runs in a background worker (its own wasm instance) so the heavy sampler
398422// can never block the UI thread — the live overlay stays smooth even when a scan is
399423// slow. The main thread only sends a frame when the worker is free. It reuses the
@@ -539,9 +563,9 @@ <h2>Decode an image</h2>
539563 viewportFrame ( ctx , W , H ) ;
540564
541565 const dboxes = decoded . filter ( r => r . box ) . map ( r => r . box ) ;
542- // acquiring: faint pulsing ticks on located-but-unresolved candidates
566+ // acquiring: faint pulsing ticks on persisted located-but-unresolved candidates
543567 for ( const c of cands ) {
544- const b = bbox ( c . corners ) ;
568+ const b = c . box ;
545569 if ( dboxes . some ( d => boxesOverlap ( d , b ) ) ) continue ;
546570 const pulse = REDUCED ? 0.22 : 0.14 + 0.12 * ( 0.5 + 0.5 * Math . sin ( ts / 420 ) ) ;
547571 reticle ( ctx , b . x0 * sx , b . y0 * sy , ( b . x1 - b . x0 ) * sx , ( b . y1 - b . y0 ) * sy , HUD . acquire , 5 , pulse , 9 ) ;
@@ -572,7 +596,7 @@ <h2>Decode an image</h2>
572596 document . getElementById ( 'cam-btn' ) . disabled = true ;
573597 document . getElementById ( 'cam-stop' ) . disabled = false ;
574598 document . getElementById ( 'cam-capture' ) . disabled = false ;
575- running = true ; workerBusy = false ; lastSent = 0 ; decodedTracks . clear ( ) ; requestAnimationFrame ( loop ) ;
599+ running = true ; workerBusy = false ; lastSent = 0 ; decodedTracks . clear ( ) ; candTracks = [ ] ; requestAnimationFrame ( loop ) ;
576600} ;
577601document . getElementById ( 'cam-stop' ) . onclick = ( ) => {
578602 running = false ; stream && stream . getTracks ( ) . forEach ( t => t . stop ( ) ) ;
@@ -632,6 +656,7 @@ <h2>Decode an image</h2>
632656 } ) ;
633657 lastMs = performance . now ( ) - t0 ;
634658 liveScale = [ pw , ph ] ;
659+ updateCandTracks ( liveCands , ts ) ;
635660 // Track: steer each decoded code toward the located candidate that best overlaps it,
636661 // so the lock follows the code across camera movement between the sparse re-decodes.
637662 // A locate match also refreshes the track's liveness — locate only fires on
@@ -660,16 +685,20 @@ <h2>Decode an image</h2>
660685 for ( const tr of decodedTracks . values ( ) ) if ( tr . target ) tr . box = boxLerp ( tr . box , tr . target , 0.3 ) ;
661686 for ( const [ k , tr ] of decodedTracks ) if ( ts - tr . lastSeen > DECODE_HOLD_MS ) decodedTracks . delete ( k ) ;
662687 lastDecoded = [ ...decodedTracks . values ( ) ] ;
688+ // Ease located-candidate tracks too, and only surface the ones that have persisted
689+ // past the noise threshold — this is what the acquiring reticles are drawn from.
690+ for ( const t of candTracks ) t . box = boxLerp ( t . box , t . target , 0.35 ) ;
691+ const acquiring = candTracks . filter ( t => t . hits >= CAND_MIN_HITS ) ;
663692 document . getElementById ( 'cam-result' ) . textContent =
664693 lastDecoded . length ? lastDecoded . map ( r => `${ r . symbology } : ${ r . text } ` ) . join ( '\n' ) : '' ;
665694
666695 // HUD every frame (cheap: canvas only) for a smooth reticle/scan animation.
667696 overlay . width = video . clientWidth ; overlay . height = video . clientHeight ;
668697 const octx = overlay . getContext ( '2d' ) ;
669698 octx . clearRect ( 0 , 0 , overlay . width , overlay . height ) ;
670- renderHUD ( octx , overlay . width , overlay . height , liveCands , lastDecoded , overlay . width / liveScale [ 0 ] , overlay . height / liveScale [ 1 ] , ts ) ;
699+ renderHUD ( octx , overlay . width , overlay . height , acquiring , lastDecoded , overlay . width / liveScale [ 0 ] , overlay . height / liveScale [ 1 ] , ts ) ;
671700 document . getElementById ( 'cam-stat' ) . innerHTML =
672- `◈ tracking <b>${ liveCands . length } </b> · locked <b>${ lastDecoded . length } </b> · scan <b>${ lastMs . toFixed ( 1 ) } ms</b>` ;
701+ `◈ tracking <b>${ acquiring . length } </b> · locked <b>${ lastDecoded . length } </b> · scan <b>${ lastMs . toFixed ( 1 ) } ms</b>` ;
673702}
674703
675704// ---- file decode ----
0 commit comments