Skip to content

Commit 5d67a5f

Browse files
committed
fix: scan
1 parent 7d955c5 commit 5d67a5f

2 files changed

Lines changed: 37 additions & 30 deletions

File tree

web/app/composables/useCardAutoScan.ts

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,19 @@ export interface UseCardAutoScanOptions {
3232
const FRAME_MS = 100
3333
/** Long edge of the downscaled frame sent for detection (more = sharper quad). */
3434
const PROC_EDGE = 640
35-
/** Steady tolerance as a fraction of the frame's long edge. */
36-
const STEADY_FRAC = 0.014
37-
/** Consecutive steady detections before firing (~0.4 s @ 100 ms cadence). */
38-
const STEADY_TICKS = 4
3935
/** "No card" detections before re-arming after a shot (card removed). */
4036
const LOST_TICKS_REARM = 3
4137
/** Floor between two captures (anti double-shot). */
4238
const MIN_COOLDOWN_MS = 1500
43-
/** How many shots we take in a burst — we keep the sharpest for OCR. */
44-
const BURST_COUNT = 2
45-
/** Gap between burst shots so focus / exposure has a chance to settle. */
46-
const BURST_INTERVAL_MS = 130
39+
/**
40+
* How many shots we take in a burst — we keep the sharpest for OCR. Pikacheck-
41+
* style: a slightly larger burst over a longer window so the user can pivot
42+
* the card during capture (revealing a glary corner, exposing missing text…)
43+
* and the sharpest / clearest angle still wins.
44+
*/
45+
const BURST_COUNT = 3
46+
/** Gap between burst shots — wide enough for hand movement to expose new info. */
47+
const BURST_INTERVAL_MS = 280
4748
/** Lerp weight (new vs previous) when tracking the displayed quad. */
4849
const SMOOTH_LERP = 0.5
4950
/** Drift above this fraction of the long edge ⇒ new scene, snap instead of lerp. */
@@ -96,7 +97,6 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
9697
let displayedCorners: Pt[] | null = null
9798
let pendingCorners: Pt[] | null = null
9899
let missTicks = 0
99-
let steadyTicks = 0
100100
let lostTicks = 0
101101
let lastCaptureAt = 0
102102

@@ -241,25 +241,28 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
241241

242242
/** Send the next warp request from the burst (or finalise if we've collected enough). */
243243
function requestWarp(): void {
244-
if (!worker || !burstCorners) {
244+
if (!worker) {
245245
finaliseBurst()
246246
return
247247
}
248248
if (burstShots.length >= burstAwaiting) {
249249
finaliseBurst()
250250
return
251251
}
252+
// Use the freshest detected corners so a pivot/move during the burst still
253+
// yields a correctly cropped card. Fall back to the trigger corners if the
254+
// detector hasn't returned a new quad yet.
255+
const corners = lastCorners ?? burstCorners
252256
const full = grabFrame(0)
253-
if (!full) {
254-
// Skip this shot but keep trying — maybe the next one lands.
257+
if (!corners || !full) {
255258
if (burstShots.length + 1 < burstAwaiting) {
256259
burstTimer = setTimeout(requestWarp, BURST_INTERVAL_MS)
257260
} else {
258261
finaliseBurst()
259262
}
260263
return
261264
}
262-
worker.postMessage({ t: 'warp', buf: full.buf, w: full.w, h: full.h, corners: burstCorners }, [full.buf])
265+
worker.postMessage({ t: 'warp', buf: full.buf, w: full.w, h: full.h, corners }, [full.buf])
263266
}
264267

265268
/** Pick the sharpest shot from the burst, encode JPEG and hand it to `onCapture`. */
@@ -333,7 +336,6 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
333336
lastCorners = null
334337
displayedCorners = null
335338
quad.value = null
336-
steadyTicks = 0
337339
if (phase.value === 'cooldown') {
338340
lostTicks += 1
339341
if (lostTicks >= LOST_TICKS_REARM) {
@@ -368,28 +370,31 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
368370
quad.value = [shaped[0]!, shaped[1]!, shaped[2]!, shaped[3]!]
369371
lostTicks = 0
370372

373+
lastCorners = corners
374+
371375
if (phase.value === 'cooldown' || capturing) {
372376
return
373377
}
374378

375-
if (lastCorners && cornerDrift(corners, lastCorners) < longEdge * STEADY_FRAC) {
376-
steadyTicks += 1
377-
} else {
378-
steadyTicks = 0
379-
}
380-
lastCorners = corners
381-
382-
if (steadyTicks < STEADY_TICKS || busy.value || Date.now() - lastCaptureAt < MIN_COOLDOWN_MS) {
383-
phase.value = 'settling'
379+
// No "hold steady" wait — Pikacheck-style. As soon as a card is confirmed
380+
// (vote passed) and we're past the cooldown, we shoot a burst. The user is
381+
// free to pivot/move the card during the burst; each shot uses the freshest
382+
// corners and the sharpest wins.
383+
if (busy.value || Date.now() - lastCaptureAt < MIN_COOLDOWN_MS) {
384+
phase.value = 'watching'
384385
return
385386
}
386387

387388
beginBurst(corners)
388389
}
389390

390-
/** Grab one downscaled frame and hand it to the worker for detection. */
391+
/**
392+
* Grab one downscaled frame and hand it to the worker for detection. Note we
393+
* intentionally keep running during a capture burst so `lastCorners` stays
394+
* fresh — each burst shot uses the latest detection, supporting movement.
395+
*/
391396
function pumpFrame(): void {
392-
if (!worker || !enabled.value || detectInFlight || capturing || busy.value) {
397+
if (!worker || !enabled.value || detectInFlight || busy.value) {
393398
return
394399
}
395400
const el = video.value

web/app/pages/collection/scan.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</h1>
2525
<p class="text-muted text-sm">
2626
Scannez vos cartes à la chaîne&nbsp;: la langue (FR / JP…) est reconnue automatiquement. En HTTPS, le mode
27-
caisse capture chaque carte tenue immobile sans rien toucher.
27+
caisse capture chaque carte dès qu'elle est tracée — vous pouvez bouger / pivoter pour aider l'OCR.
2828
</p>
2929
</div>
3030
</div>
@@ -41,7 +41,9 @@
4141
v-model="autoScan"
4242
label="Scan auto (caisse)"
4343
:description="
44-
autoScan ? 'Capture quand vous passez une carte puis la tenez immobile' : 'Capture manuelle uniquement'
44+
autoScan
45+
? 'Capture dès qu\'une carte est tracée — vous pouvez bouger ou pivoter'
46+
: 'Capture manuelle uniquement'
4547
"
4648
/>
4749
<div v-else />
@@ -249,8 +251,8 @@
249251
</div>
250252
<p class="text-muted text-[11px]">
251253
<template v-if="autoScan">
252-
Présentez chaque carte bien à plat dans le cadre et tenez-la immobile&nbsp;: elle est capturée toute
253-
seule, puis retirez-la pour enchaîner. Molette pour zoomer.
254+
Présentez chaque carte dans le cadre&nbsp;: dès qu'elle est tracée, elle est capturée toute seule, puis
255+
retirez-la pour enchaîner. Molette pour zoomer.
254256
</template>
255257
<template v-else>Molette sur l’aperçu pour zoomer. {{ zoomHint }}</template>
256258
</p>
@@ -1513,7 +1515,7 @@ const autoScanStatus = computed<{ label: string; color: 'primary' | 'success' |
15131515
case 'watching':
15141516
return { label: 'Placez ou passez une carte', color: 'neutral' }
15151517
case 'settling':
1516-
return { label: 'Lecture… tenez la carte immobile', color: 'primary' }
1518+
return { label: 'Lecture en cours…', color: 'primary' }
15171519
case 'captured':
15181520
return { label: 'Carte capturée', color: 'success' }
15191521
case 'cooldown':

0 commit comments

Comments
 (0)