Skip to content

Commit 2cbf697

Browse files
committed
Rename options
1 parent 1296356 commit 2cbf697

3 files changed

Lines changed: 37 additions & 35 deletions

File tree

benchmark/decode-quality.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function runCategory(files) {
6161
if (vector.expected) stats.expected++;
6262
let decoded;
6363
try {
64-
decoded = decodeQR(readImage(vector.path), { moreEffort: true });
64+
decoded = decodeQR(readImage(vector.path), { multiPass: 'max' });
6565
} catch {
6666
stats.errors++;
6767
}

src/decode.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,21 +2325,19 @@ export type DecodeOpts = {
23252325
*/
23262326
grid?: DecodeGrid;
23272327
/**
2328-
* Retry failed decodes with inverted colors (light-on-dark codes, screens)
2329-
* and 2x..32x box-downscaled rasters (codes overflowing the frame, moire).
2328+
* Adaptive multi-pass detection.
23302329
*
2331-
* Enabled by default: both retries only run after the primary pass fails,
2332-
* so images that already decode pay nothing. Set to `false` to disable.
2330+
* - `true` (default): for large frames, detect on a downscaled raster
2331+
* first; retry failed decodes with inverted colors (light-on-dark codes,
2332+
* screens) and 2x..32x box-downscaled rasters (codes overflowing the
2333+
* frame, moire). Extra passes only run after the primary pass fails, so
2334+
* images that already decode pay nothing.
2335+
* - `'max'`: additionally try rotated, cropped, and upscaled fallback
2336+
* candidates — decodes more hard images, but can be several times
2337+
* slower on frames that still fail.
2338+
* - `false`: strict single-pass decode.
23332339
*/
2334-
invertDownscale?: boolean;
2335-
/**
2336-
* Try rotated, cropped, and upscaled fallback candidates after the primary
2337-
* decode attempt fails.
2338-
*
2339-
* Disabled by default because these retries can be much slower on hard
2340-
* failures.
2341-
*/
2342-
moreEffort?: boolean;
2340+
multiPass?: boolean | 'max';
23432341
/**
23442342
* Custom byte-to-text decoder used for byte segments.
23452343
*
@@ -2467,7 +2465,7 @@ function cropImageWithOffset(
24672465

24682466
// Fallback candidates copy pixels with plain byte moves: a per-pixel
24692467
// `data.subarray()` + `set()` allocated millions of temporary views per frame
2470-
// and dominated moreEffort profiles (~57% self time on 9MP failures).
2468+
// and dominated multiPass:'max' profiles (~57% self time on 9MP failures).
24712469
function copyPixel(
24722470
dst: Uint8Array,
24732471
dstPos: number,
@@ -2642,7 +2640,11 @@ function visitFallbackImageCandidates(img: TArg<Image>, visit: CandidateVisitor)
26422640
// from nearest-neighbor upscaling, while large multi-QR scenes need window
26432641
// crops before full-frame rotations. This also avoids allocating three full
26442642
// rotated frames before trying the much smaller windows on >4MP images.
2645-
if (area <= 150_000) {
2643+
// Upscaling adds no pixels of information, but it shrinks the binarizer's
2644+
// fixed 8x8 blocks relative to the modules — a cheap stand-in for a
2645+
// finer-grained binarizer. 1MP was measured as the sweet spot on BoofCV
2646+
// (multiPass:'max' 344->353); 2.5MP decodes nothing more and costs 18% extra.
2647+
if (area <= 1_000_000) {
26462648
const found = visitRotatedImageCandidates(base, (candidate) => {
26472649
if (visit(scaleNearestCandidate(candidate, 2))) return true;
26482650
return visit(scaleNearestCandidate(candidate, 3));
@@ -2770,8 +2772,8 @@ function decodePreparedImage(
27702772
// color-inverted bitmap (ISO/IEC 18004:2024 §12 b)5 reflectance reversal):
27712773
// light-on-dark codes can fail at any stage after the finder, so the legacy
27722774
// finder-level negate retry inside detectTriples() is not enough.
2773-
// `opts.invertDownscale: false` keeps only the legacy finder-level retry.
2774-
const fullInvert = options.invertDownscale !== false;
2775+
// `opts.multiPass: false` keeps only the legacy finder-level retry.
2776+
const fullInvert = options.multiPass !== false;
27752777
let lastErr: unknown;
27762778
for (let polarity = 0; polarity < 2; polarity++) {
27772779
let triples;
@@ -2964,12 +2966,12 @@ function validateDecodeOptions(opts: TArg<DecodeOpts>): DecodeOpts {
29642966
const options = validateObject(opts, 'opts') as DecodeOpts;
29652967
if (options.cropToSquare !== undefined && typeof options.cropToSquare !== 'boolean')
29662968
throw new Error(`invalid opts.cropToSquare=${options.cropToSquare}`);
2967-
if (options.moreEffort !== undefined && typeof options.moreEffort !== 'boolean')
2968-
throw new Error(`invalid opts.moreEffort=${options.moreEffort} (${typeof options.moreEffort})`);
2969-
if (options.invertDownscale !== undefined && typeof options.invertDownscale !== 'boolean')
2970-
throw new Error(
2971-
`invalid opts.invertDownscale=${options.invertDownscale} (${typeof options.invertDownscale})`
2972-
);
2969+
if (
2970+
options.multiPass !== undefined &&
2971+
typeof options.multiPass !== 'boolean' &&
2972+
options.multiPass !== 'max'
2973+
)
2974+
throw new Error(`invalid opts.multiPass=${options.multiPass} (${typeof options.multiPass})`);
29732975
if (options.grid !== undefined) {
29742976
const grid = validateObject(options.grid, 'opts.grid') as DecodeGrid;
29752977
asafenumber(grid.moduleSize, 'opts.grid.moduleSize');
@@ -3040,7 +3042,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30403042
// full-resolution cost; when it fails, the full pipeline below still runs,
30413043
// adding at most ~25% to the failure path.
30423044
let preScale = 0;
3043-
if (options.invertDownscale !== false && options.grid === undefined) {
3045+
if (options.multiPass !== false && options.grid === undefined) {
30443046
let img = image;
30453047
let scale = 1;
30463048
while (
@@ -3063,7 +3065,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30633065
return decodePreparedImage(image, options, mapPoint);
30643066
} catch (e) {
30653067
if (isUserDecodeError(e)) throw e.error;
3066-
if (options.invertDownscale !== false && options.grid === undefined) {
3068+
if (options.multiPass !== false && options.grid === undefined) {
30673069
try {
30683070
const retry = decodeWithDownscale(image, options, mapPoint, preScale);
30693071
if (retry !== undefined) return retry;
@@ -3072,7 +3074,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30723074
throw downscaleError;
30733075
}
30743076
}
3075-
if (options.moreEffort && options.grid === undefined) {
3077+
if (options.multiPass === 'max' && options.grid === undefined) {
30763078
try {
30773079
const retry = decodeWithFallback(image, options, mapPoint);
30783080
if (retry !== undefined) return retry;

test/decode.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ should('decodeQR validates textDecoder option before decoding', () => {
129129
new Error('invalid opts.textDecoder=1 (number)')
130130
);
131131
throws(
132-
() => readQR(img, { moreEffort: 1 as never }),
133-
new Error('invalid opts.moreEffort=1 (number)')
132+
() => readQR(img, { multiPass: 1 as never }),
133+
new Error('invalid opts.multiPass=1 (number)')
134134
);
135135
});
136136

@@ -334,7 +334,7 @@ should('automatic grid sampling covers representative versions and borders', ()
334334

335335
should('decodeQR fallback works with diagnostic hooks', () => {
336336
// rotations/image037 decodes directly since the BR-corner nudge retries;
337-
// nominal/image018 still requires the moreEffort fallback candidates.
337+
// nominal/image018 still requires the multiPass:'max' fallback candidates.
338338
const img = readJPEG('detection/nominal/image018.jpg');
339339
const expected = 'http://www.lonelyplanet.com/';
340340
let detectedPoints = 0;
@@ -343,7 +343,7 @@ should('decodeQR fallback works with diagnostic hooks', () => {
343343
let resultImages = 0;
344344
throws(() => readQR(img));
345345
const res = readQR(img, {
346-
moreEffort: true,
346+
multiPass: 'max',
347347
pointsOnDetect: (points) => {
348348
deepStrictEqual(points.length, 4);
349349
detectedPoints++;
@@ -369,7 +369,7 @@ should('decodeQR fallback maps diagnostic points to original image coordinates',
369369
const img = readJPEG('detection/blurred/image025.jpg');
370370
let detectedPoints;
371371
const res = readQR(img, {
372-
moreEffort: true,
372+
multiPass: 'max',
373373
pointsOnDetect: (points) => {
374374
detectedPoints = points;
375375
},
@@ -411,7 +411,7 @@ should('decodeQR fallback propagates user callback errors', () => {
411411
const textDecoderError = new Error('custom text decoder failed');
412412
try {
413413
readQR(img, {
414-
moreEffort: true,
414+
multiPass: 'max',
415415
textDecoder: () => {
416416
throw textDecoderError;
417417
},
@@ -423,7 +423,7 @@ should('decodeQR fallback propagates user callback errors', () => {
423423
const pointsError = new Error('points hook failed');
424424
try {
425425
readQR(img, {
426-
moreEffort: true,
426+
multiPass: 'max',
427427
pointsOnDetect: () => {
428428
throw pointsError;
429429
},
@@ -1070,7 +1070,7 @@ for (const category of listFiles(DETECTION_PATH, true)) {
10701070
// console.log('Decoding', p.replace(DIR, ''));
10711071
let res;
10721072
try {
1073-
res = readQR(img, { moreEffort: true });
1073+
res = readQR(img, { multiPass: 'max' });
10741074
} catch (e) {
10751075
//console.log('TEST ERR', e);
10761076
}

0 commit comments

Comments
 (0)