Skip to content

Commit d9fa655

Browse files
committed
Add configurable scanner frame interval
1 parent f08325f commit d9fa655

2 files changed

Lines changed: 60 additions & 5 deletions

File tree

examples/web_scanner/app.js

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const ASSET_CHANNELS = {
1111
const DETECTOR_SIZE = 384;
1212
const MIN_MATCH_SCORE_DEFAULT = 0.50;
1313
const PREVIEW_ASPECT = 16 / 9;
14-
const SCAN_INTERVAL_MS = 900;
14+
const SCAN_INTERVAL_DEFAULT_MS = 50;
15+
const SCAN_INTERVAL_MAX_MS = 1000;
1516
const MOBILE_PREVIEW_INTERVAL_MS = 1000 / 15;
1617

1718
const SOUND_PATHS = {
@@ -25,6 +26,7 @@ const ASSET_STORE_NAME = "assets";
2526
const WEBGPU_PREF_KEY = "cv_webgpu_enabled";
2627
const MATCH_SCORE_KEY = "cv_min_match_score";
2728
const CORNER_CONFIDENCE_KEY = "cv_min_corner_confidence";
29+
const SCAN_INTERVAL_KEY = "cv_scan_interval_ms";
2830
const ROTATION_INVARIANT_KEY = "cv_rotation_invariant_enabled";
2931
const MIN_MATCHES_DEFAULT = 2;
3032
const MATCHES_KEY = "cv_min_matches";
@@ -1505,6 +1507,13 @@ function getMinMatchScore() {
15051507
return Number.isFinite(stored) ? stored : MIN_MATCH_SCORE_DEFAULT;
15061508
}
15071509

1510+
function getScanIntervalMs() {
1511+
const stored = Number.parseInt(localStorage.getItem(SCAN_INTERVAL_KEY), 10);
1512+
return Number.isFinite(stored)
1513+
? Math.min(SCAN_INTERVAL_MAX_MS, Math.max(0, stored))
1514+
: SCAN_INTERVAL_DEFAULT_MS;
1515+
}
1516+
15081517
function getMinCornerConfidence() {
15091518
const stored = Number.parseFloat(localStorage.getItem(CORNER_CONFIDENCE_KEY));
15101519
return Number.isFinite(stored) ? Math.min(0.1, Math.max(0, stored)) : MIN_CORNER_CONFIDENCE_DEFAULT;
@@ -1536,6 +1545,25 @@ function setupMatchScoreSlider() {
15361545
});
15371546
}
15381547

1548+
function setupScanIntervalSlider(onChange) {
1549+
const slider = document.getElementById("scan-interval-slider");
1550+
const label = document.getElementById("scan-interval-value");
1551+
if (!slider || !label) return;
1552+
1553+
const render = (value) => {
1554+
slider.value = String(value);
1555+
label.textContent = value === 0 ? "Max speed" : `${value} ms`;
1556+
};
1557+
render(getScanIntervalMs());
1558+
1559+
slider.addEventListener("input", () => {
1560+
const value = Math.min(SCAN_INTERVAL_MAX_MS, Math.max(0, Number.parseInt(slider.value, 10) || 0));
1561+
localStorage.setItem(SCAN_INTERVAL_KEY, String(value));
1562+
render(value);
1563+
onChange(value);
1564+
});
1565+
}
1566+
15391567
function setupCornerConfidenceSlider(scannerWorker = null) {
15401568
const slider = document.getElementById("corner-confidence-slider");
15411569
const label = document.getElementById("corner-confidence-value");
@@ -1650,7 +1678,7 @@ class PerformanceOverlay {
16501678
const card = data?.cardPresent ? (data.cornersValid ? "card" : "bad-quad") : "no-card";
16511679
const orientation = data?.orientation ? ` ${data.orientation}` : "";
16521680
this.el.textContent = [
1653-
`scan ${SCAN_INTERVAL_MS}ms result ${resultGap}`,
1681+
`minimum interval ${getScanIntervalMs()}ms result ${resultGap}`,
16541682
`total ${formatMs(timing.totalMs)} det ${formatMs(timing.detectMs)} (run ${formatMs(timing.detectorRunMs)})`,
16551683
`dew ${formatMs(timing.dewarpMs)} (warp ${formatMs(timing.dewarpWarpMs)}) emb ${formatMs(timing.embedMs)} (run ${formatMs(timing.embedRunMs)})`,
16561684
`prep det ${formatMs(timing.detectorInputMs)} prep emb ${formatMs(timing.embedInputMs)} lookup ${formatMs(timing.searchMs)}`,
@@ -1812,6 +1840,7 @@ function createScannerLoop(
18121840
const bucket = new ScanBucket();
18131841
let timer = null;
18141842
let workerBusy = false;
1843+
let scanIntervalMs = getScanIntervalMs();
18151844

18161845
scannerWorker.addEventListener("error", (event) => {
18171846
workerBusy = false;
@@ -1973,8 +2002,8 @@ function createScannerLoop(
19732002
return;
19742003
}
19752004
setText("camera-badge", "Scanning");
1976-
debugLog.info("scan interval", `${SCAN_INTERVAL_MS}ms`);
1977-
recordBootTrace("scan:loop-started", { intervalMs: SCAN_INTERVAL_MS });
2005+
debugLog.info("scan interval", `${scanIntervalMs}ms`);
2006+
recordBootTrace("scan:loop-started", { intervalMs: scanIntervalMs });
19782007
timer = setInterval(async () => {
19792008
if (workerBusy || !camera.stream) {
19802009
return;
@@ -1998,7 +2027,15 @@ function createScannerLoop(
19982027
debugLog.error("scan tick failed", error);
19992028
setText("camera-badge", error?.message || "Scan error");
20002029
}
2001-
}, SCAN_INTERVAL_MS);
2030+
}, scanIntervalMs);
2031+
},
2032+
setIntervalMs(value) {
2033+
scanIntervalMs = Math.min(SCAN_INTERVAL_MAX_MS, Math.max(0, value));
2034+
if (!timer) {
2035+
return;
2036+
}
2037+
this.stop();
2038+
this.start();
20022039
},
20032040
};
20042041
}
@@ -2241,6 +2278,7 @@ async function boot() {
22412278
const loop = createScannerLoop(
22422279
camera, scannerWorker, enricherWorker, scans, audioBus, manifest, debugLog, diag, captureState, perfOverlay,
22432280
);
2281+
setupScanIntervalSlider((value) => loop.setIntervalMs(value));
22442282
camera.bind(
22452283
async () => {
22462284
debugLog.info("starting scan loop");

examples/web_scanner/index.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,23 @@ <h2>Match Threshold</h2>
294294
<p class="capture-hint">Embeds the crop upright and rotated 180 degrees, then keeps the stronger match. Default on.</p>
295295
</section>
296296

297+
<section class="panel panel--nested">
298+
<header class="panel-header">
299+
<div>
300+
<p class="section-kicker">Performance</p>
301+
<h2>Scan Rate</h2>
302+
</div>
303+
</header>
304+
<label class="slider-row" for="scan-interval-slider">
305+
<span class="slider-row__label">Minimum frame interval</span>
306+
<span class="slider-row__value" id="scan-interval-value">50 ms</span>
307+
</label>
308+
<input type="range" id="scan-interval-slider"
309+
class="threshold-slider"
310+
min="0" max="1000" step="25" value="50" />
311+
<p class="capture-hint">Lower values analyze more camera frames. Max speed sends the next frame as soon as the scanner worker is ready; the worker never queues overlapping inference.</p>
312+
</section>
313+
297314
<dl class="settings-grid">
298315
<div>
299316
<dt>Build</dt>

0 commit comments

Comments
 (0)