tapirscan 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/THIRD_PARTY_NOTICES.md +656 -0
- package/dist/detail-20260914/continuity.d.mts +2 -0
- package/dist/detail-20260914/continuity.mjs +62 -0
- package/dist/detail-20260914/detail-proposals-rich.d.mts +13 -0
- package/dist/detail-20260914/detail-proposals-rich.mjs +53 -0
- package/dist/detail-20260914/direct-recovery.d.mts +28 -0
- package/dist/detail-20260914/direct-recovery.mjs +139 -0
- package/dist/detail-20260914/host.d.mts +40 -0
- package/dist/detail-20260914/host.mjs +280 -0
- package/dist/detail-20260914/scanner.d.mts +12 -0
- package/dist/detail-20260914/scanner.mjs +64 -0
- package/dist/detail-20260914/source-evidence.d.mts +10 -0
- package/dist/detail-20260914/source-evidence.mjs +94 -0
- package/dist/detail-canvas.d.ts +35 -0
- package/dist/detail-canvas.js +73 -0
- package/dist/detail.d.ts +19 -0
- package/dist/detail.js +87 -0
- package/dist/host.d.ts +112 -0
- package/dist/host.js +184 -0
- package/dist/host64.d.ts +112 -0
- package/dist/host64.js +184 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +199 -0
- package/dist/multiformat/formats.d.ts +26 -0
- package/dist/multiformat/formats.js +59 -0
- package/dist/multiformat/geometry.d.ts +32 -0
- package/dist/multiformat/geometry.js +122 -0
- package/dist/multiformat/pixels.d.ts +3 -0
- package/dist/multiformat/pixels.js +36 -0
- package/dist/multiformat/scanner.d.ts +51 -0
- package/dist/multiformat/scanner.js +258 -0
- package/dist/multiformat-host.d.ts +121 -0
- package/dist/multiformat-host.js +207 -0
- package/dist/policy.d.ts +12 -0
- package/dist/policy.js +12 -0
- package/package.json +52 -0
- package/wasm/high-release-20260915.wasm +0 -0
- package/wasm/low-release-20260915.wasm +0 -0
- package/wasm/medium-release-20260915.wasm +0 -0
- package/wasm/multiformat.json +6 -0
- package/wasm/multiformat.wasm +0 -0
- package/wasm/very-high-release-20260915.wasm +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IndependentScanner } from "./host.mjs";
|
|
2
|
+
import { evidencePlan } from "./source-evidence.mjs";
|
|
3
|
+
import { recoverDirectSeed } from "./direct-recovery.mjs";
|
|
4
|
+
/** Workbench composition. Each crop keeps its own candidate-index namespace. */
|
|
5
|
+
export class DetailScanner {
|
|
6
|
+
constructor(primary, recovery, directions) {
|
|
7
|
+
this.primary = primary;
|
|
8
|
+
this.recovery = recovery;
|
|
9
|
+
this.directions = directions;
|
|
10
|
+
}
|
|
11
|
+
static async create(primaryBytes, recoveryBytes, directions) {
|
|
12
|
+
if (![1, 2, 3].includes(directions))
|
|
13
|
+
throw new Error("Invalid recovery direction budget");
|
|
14
|
+
const primary = await IndependentScanner.create(primaryBytes);
|
|
15
|
+
try {
|
|
16
|
+
return new DetailScanner(primary, await IndependentScanner.create(recoveryBytes), directions);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
primary.dispose();
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
scanLocalized(image, policy, fitLimit, fullFrame) {
|
|
24
|
+
// The source samplers use tightly packed RGBA, as supplied by the camera worker.
|
|
25
|
+
if (image.channels !== 4 || image.stride !== image.width * 4)
|
|
26
|
+
throw new Error("Detail discovery requires tightly packed RGBA");
|
|
27
|
+
const start = performance.now();
|
|
28
|
+
const found = this.primary.scanLocalized(image, policy, fitLimit, fullFrame, (localization) => evidencePlan(image, localization, 48));
|
|
29
|
+
const recovery = recoverDirectSeed(image, this.recovery, policy, found, 64, 3, true, true, 1, this.directions);
|
|
30
|
+
return {
|
|
31
|
+
...found,
|
|
32
|
+
// Do not concatenate crop-local candidate indices into the primary frame.
|
|
33
|
+
// Consumers use detailRegions; raw frames and coordinate transforms stay inspectable.
|
|
34
|
+
recovery,
|
|
35
|
+
detailRegions: detailRegions(found, recovery),
|
|
36
|
+
scanMs: performance.now() - start,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
dispose() {
|
|
40
|
+
this.primary.dispose();
|
|
41
|
+
this.recovery.dispose();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Preserve undecoded geometry without assigning crop indices to primary proposals. */
|
|
45
|
+
export function detailRegions(found, recovery) {
|
|
46
|
+
const decoded = new Set(found.scan.barcodes.flatMap((barcode) => barcode.candidate_indices));
|
|
47
|
+
const regions = recovery.barcodes.map((barcode) => ({
|
|
48
|
+
text: barcode.text,
|
|
49
|
+
polygon: barcode.polygon,
|
|
50
|
+
support: barcode.support,
|
|
51
|
+
}));
|
|
52
|
+
found.localization.proposals.forEach((proposal, index) => {
|
|
53
|
+
if (!decoded.has(index))
|
|
54
|
+
regions.push({ text: "", polygon: proposal.polygon, score: proposal.score });
|
|
55
|
+
});
|
|
56
|
+
for (const attempt of recovery.attempts) {
|
|
57
|
+
const accepted = new Set(attempt.reads.flatMap((barcode) => barcode.candidate_indices));
|
|
58
|
+
attempt.proposals.forEach((proposal, index) => {
|
|
59
|
+
if (!accepted.has(index))
|
|
60
|
+
regions.push({ text: "", polygon: proposal.polygon, score: proposal.score });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return regions;
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Cheap alternating-run evidence. It prioritizes geometry, never accepts digits. */
|
|
2
|
+
export function sourceEvidence(image: any, polygon: any, local?: boolean, target?: number): {
|
|
3
|
+
transitions: number;
|
|
4
|
+
localTransitions: number;
|
|
5
|
+
rows: number;
|
|
6
|
+
contrast: number;
|
|
7
|
+
};
|
|
8
|
+
export function evidenceFilter(image: any, localization: any, minimum?: number): any;
|
|
9
|
+
/** Retain all candidates. Evidence stops once eligibility is established; counts are lower bounds. */
|
|
10
|
+
export function evidencePlan(image: any, localization: any, minimum?: number): any;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Cheap alternating-run evidence. It prioritizes geometry, never accepts digits. */
|
|
2
|
+
export function sourceEvidence(image, polygon, local = false, target = Infinity) {
|
|
3
|
+
const { data, width, height } = image;
|
|
4
|
+
const gray = (x, y) => {
|
|
5
|
+
x = Math.max(0, Math.min(width - 1, x));
|
|
6
|
+
y = Math.max(0, Math.min(height - 1, y));
|
|
7
|
+
const x0 = Math.floor(x), y0 = Math.floor(y), x1 = Math.min(width - 1, x0 + 1), y1 = Math.min(height - 1, y0 + 1), fx = x - x0, fy = y - y0;
|
|
8
|
+
const v = (a, b) => {
|
|
9
|
+
const i = (b * width + a) * 4;
|
|
10
|
+
return (77 * data[i] + 150 * data[i + 1] + 29 * data[i + 2]) / 256;
|
|
11
|
+
};
|
|
12
|
+
return ((v(x0, y0) * (1 - fx) + v(x1, y0) * fx) * (1 - fy) +
|
|
13
|
+
(v(x0, y1) * (1 - fx) + v(x1, y1) * fx) * fy);
|
|
14
|
+
};
|
|
15
|
+
let best = 0, localBest = 0, rows = 0, contrast = 0;
|
|
16
|
+
for (let axis = 0; axis < 2; axis++)
|
|
17
|
+
for (const f of [0.2, 0.4, 0.6, 0.8]) {
|
|
18
|
+
const a = polygon[axis], b = polygon[(axis + 1) % 4], c = polygon[(axis + 2) % 4], d = polygon[(axis + 3) % 4];
|
|
19
|
+
const p = [a[0] * (1 - f) + d[0] * f, a[1] * (1 - f) + d[1] * f], q = [b[0] * (1 - f) + c[0] * f, b[1] * (1 - f) + c[1] * f];
|
|
20
|
+
const n = Math.min(2048, Math.max(64, Math.ceil(Math.hypot(q[0] - p[0], q[1] - p[1]) * 1.25)));
|
|
21
|
+
const samples = new Float32Array(n);
|
|
22
|
+
let lo = 255, hi = 0;
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const t = -0.125 + (1.25 * i) / (n - 1), v = gray(p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t);
|
|
25
|
+
samples[i] = v;
|
|
26
|
+
lo = Math.min(lo, v);
|
|
27
|
+
hi = Math.max(hi, v);
|
|
28
|
+
}
|
|
29
|
+
contrast = Math.max(contrast, hi - lo);
|
|
30
|
+
if (hi - lo < (local ? 12 : 20))
|
|
31
|
+
continue;
|
|
32
|
+
const threshold = (lo + hi) / 2, hysteresis = (hi - lo) * 0.06;
|
|
33
|
+
let state = samples[0] > threshold, runs = 0;
|
|
34
|
+
for (const v of samples) {
|
|
35
|
+
if (state ? v < threshold - hysteresis : v > threshold + hysteresis) {
|
|
36
|
+
state = !state;
|
|
37
|
+
runs++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
best = Math.max(best, runs);
|
|
41
|
+
if (runs >= 20)
|
|
42
|
+
rows++;
|
|
43
|
+
if (best >= target)
|
|
44
|
+
return { transitions: best, localTransitions: localBest, rows, contrast };
|
|
45
|
+
if (local) {
|
|
46
|
+
const sum = new Float64Array(n + 1);
|
|
47
|
+
for (let i = 0; i < n; i++)
|
|
48
|
+
sum[i + 1] = sum[i] + samples[i];
|
|
49
|
+
let previous, localRuns = 0;
|
|
50
|
+
for (let i = 0; i < n; i++) {
|
|
51
|
+
const left = Math.max(0, i - 8), right = Math.min(n, i + 9), mean = (sum[right] - sum[left]) / (right - left);
|
|
52
|
+
const next = samples[i] > mean + 2 ? true : samples[i] < mean - 2 ? false : undefined;
|
|
53
|
+
if (next !== undefined) {
|
|
54
|
+
if (previous !== undefined && previous !== next)
|
|
55
|
+
localRuns++;
|
|
56
|
+
previous = next;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
localBest = Math.max(localBest, localRuns);
|
|
60
|
+
if (localBest >= target)
|
|
61
|
+
return { transitions: best, localTransitions: localBest, rows, contrast };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { transitions: best, localTransitions: localBest, rows, contrast };
|
|
65
|
+
}
|
|
66
|
+
export function evidenceFilter(image, localization, minimum = 20) {
|
|
67
|
+
const ranked = localization.proposals.map((p) => ({
|
|
68
|
+
...p,
|
|
69
|
+
evidence: sourceEvidence(image, p.polygon),
|
|
70
|
+
}));
|
|
71
|
+
const proposals = ranked.filter((p) => p.evidence.transitions >= minimum), deferred = ranked.filter((p) => p.evidence.transitions < minimum);
|
|
72
|
+
return {
|
|
73
|
+
...localization,
|
|
74
|
+
proposals,
|
|
75
|
+
deferred,
|
|
76
|
+
workLimited: localization.workLimited || deferred.length > 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Retain all candidates. Evidence stops once eligibility is established; counts are lower bounds. */
|
|
80
|
+
export function evidencePlan(image, localization, minimum = 20) {
|
|
81
|
+
const proposals = localization.proposals.map((p) => ({
|
|
82
|
+
...p,
|
|
83
|
+
evidence: sourceEvidence(image, p.polygon, false, minimum),
|
|
84
|
+
}));
|
|
85
|
+
const masks = [0, 0];
|
|
86
|
+
proposals.forEach((p, i) => {
|
|
87
|
+
if (p.evidence.transitions >= minimum)
|
|
88
|
+
masks[i >>> 5] |= 1 << (i & 31);
|
|
89
|
+
});
|
|
90
|
+
const fullFrame = proposals.length;
|
|
91
|
+
if (fullFrame < 64)
|
|
92
|
+
masks[fullFrame >>> 5] |= 1 << (fullFrame & 31);
|
|
93
|
+
return { ...localization, proposals, retryMask: masks.map((x) => x >>> 0) };
|
|
94
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Canvas interpolation in browsers; deterministic bilinear RGB interpolation in Node. */
|
|
2
|
+
interface Pixels {
|
|
3
|
+
data: Uint8ClampedArray;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function makeCanvas(width: number, height: number): {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
getContext(): {
|
|
11
|
+
imageSmoothingEnabled: boolean;
|
|
12
|
+
putImageData(value: Pixels): void;
|
|
13
|
+
getImageData(): Pixels;
|
|
14
|
+
drawImage(source: {
|
|
15
|
+
getContext(): {
|
|
16
|
+
getImageData(): Pixels;
|
|
17
|
+
};
|
|
18
|
+
}, _x: number, _y: number, w: number, h: number): void;
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
native: OffscreenCanvas;
|
|
24
|
+
getContext(_kind: string, options?: {
|
|
25
|
+
willReadFrequently?: boolean;
|
|
26
|
+
}): {
|
|
27
|
+
imageSmoothingEnabled: boolean;
|
|
28
|
+
putImageData(pixels: Pixels, x: number, y: number): void;
|
|
29
|
+
drawImage(source: {
|
|
30
|
+
native: OffscreenCanvas;
|
|
31
|
+
}, x: number, y: number, w: number, h: number): void;
|
|
32
|
+
getImageData(x: number, y: number, w: number, h: number): ImageData;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function makeCanvas(width, height) {
|
|
2
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
3
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
4
|
+
return {
|
|
5
|
+
get width() {
|
|
6
|
+
return canvas.width;
|
|
7
|
+
},
|
|
8
|
+
set width(value) {
|
|
9
|
+
canvas.width = value;
|
|
10
|
+
},
|
|
11
|
+
get height() {
|
|
12
|
+
return canvas.height;
|
|
13
|
+
},
|
|
14
|
+
set height(value) {
|
|
15
|
+
canvas.height = value;
|
|
16
|
+
},
|
|
17
|
+
native: canvas,
|
|
18
|
+
getContext(_kind, options) {
|
|
19
|
+
const context = canvas.getContext("2d", options);
|
|
20
|
+
if (!context)
|
|
21
|
+
throw Error("Could not create recovery canvas");
|
|
22
|
+
return {
|
|
23
|
+
imageSmoothingEnabled: true,
|
|
24
|
+
putImageData(pixels, x, y) {
|
|
25
|
+
context.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), x, y);
|
|
26
|
+
},
|
|
27
|
+
drawImage(source, x, y, w, h) {
|
|
28
|
+
context.imageSmoothingEnabled = true;
|
|
29
|
+
context.drawImage(source.native, x, y, w, h);
|
|
30
|
+
},
|
|
31
|
+
getImageData(x, y, w, h) {
|
|
32
|
+
return context.getImageData(x, y, w, h);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
let pixels = { data: new Uint8ClampedArray(), width, height };
|
|
39
|
+
const canvas = {
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
getContext() {
|
|
43
|
+
return {
|
|
44
|
+
imageSmoothingEnabled: true,
|
|
45
|
+
putImageData(value) {
|
|
46
|
+
pixels = value;
|
|
47
|
+
},
|
|
48
|
+
getImageData() {
|
|
49
|
+
return pixels;
|
|
50
|
+
},
|
|
51
|
+
drawImage(source, _x, _y, w, h) {
|
|
52
|
+
const input = source.getContext().getImageData();
|
|
53
|
+
const data = new Uint8ClampedArray(w * h * 4);
|
|
54
|
+
for (let y = 0; y < h; y++) {
|
|
55
|
+
const sy = Math.max(0, Math.min(input.height - 1, ((y + 0.5) * input.height) / h - 0.5));
|
|
56
|
+
const y0 = Math.floor(sy), y1 = Math.min(input.height - 1, y0 + 1), fy = sy - y0;
|
|
57
|
+
for (let x = 0; x < w; x++) {
|
|
58
|
+
const sx = Math.max(0, Math.min(input.width - 1, ((x + 0.5) * input.width) / w - 0.5));
|
|
59
|
+
const x0 = Math.floor(sx), x1 = Math.min(input.width - 1, x0 + 1), fx = sx - x0;
|
|
60
|
+
for (let c = 0; c < 4; c++) {
|
|
61
|
+
const a = input.data[(y0 * input.width + x0) * 4 + c], b = input.data[(y0 * input.width + x1) * 4 + c];
|
|
62
|
+
const d = input.data[(y1 * input.width + x0) * 4 + c], e = input.data[(y1 * input.width + x1) * 4 + c];
|
|
63
|
+
data[(y * w + x) * 4 + c] = Math.round((a * (1 - fx) + b * fx) * (1 - fy) + (d * (1 - fx) + e * fx) * fy);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
pixels = { data, width: w, height: h };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
return canvas;
|
|
73
|
+
}
|
package/dist/detail.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DetailResult } from "./detail-20260914/scanner.mjs";
|
|
2
|
+
import { IndependentScanner, type Image, type ScanFrame } from "./host.js";
|
|
3
|
+
import type { Mode } from "./index.js";
|
|
4
|
+
export declare const fitLimits: {
|
|
5
|
+
readonly low: 0;
|
|
6
|
+
readonly medium: 1;
|
|
7
|
+
readonly high: 4;
|
|
8
|
+
readonly "very-high": 1;
|
|
9
|
+
};
|
|
10
|
+
/** Flatten decoded outputs only; retain crop-local evidence in recovery.attempts. */
|
|
11
|
+
export declare class ReleaseDetailScanner {
|
|
12
|
+
private readonly scanner;
|
|
13
|
+
private disposed;
|
|
14
|
+
private constructor();
|
|
15
|
+
static create(primary: ArrayBuffer, low: ArrayBuffer, mode: Exclude<Mode, "low">): Promise<ReleaseDetailScanner>;
|
|
16
|
+
scanLocalized(image: Image, policy: Parameters<IndependentScanner["scanLocalized"]>[1], fit: number, full: boolean): DetailResult;
|
|
17
|
+
best(frame: ScanFrame): import("./host.js").Barcode | undefined;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
package/dist/detail.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DetailScanner } from "./detail-20260914/scanner.mjs";
|
|
2
|
+
export const fitLimits = { low: 0, medium: 1, high: 4, "very-high": 1 };
|
|
3
|
+
/** Preserve the public gray/RGB/RGBA and padded-stride image contract. */
|
|
4
|
+
function packed(image) {
|
|
5
|
+
if (
|
|
6
|
+
// Runtime callers can bypass the TypeScript image contract.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
8
|
+
!image ||
|
|
9
|
+
!Number.isSafeInteger(image.width) ||
|
|
10
|
+
!Number.isSafeInteger(image.height) ||
|
|
11
|
+
image.width < 3 ||
|
|
12
|
+
image.height < 3 ||
|
|
13
|
+
image.width * image.height > 32 * 1024 * 1024 ||
|
|
14
|
+
![1, 3, 4].includes(image.channels) ||
|
|
15
|
+
!Number.isSafeInteger(image.stride) ||
|
|
16
|
+
image.stride < image.width * image.channels ||
|
|
17
|
+
!(image.data instanceof Uint8Array) ||
|
|
18
|
+
image.data.length < (image.height - 1) * image.stride + image.width * image.channels)
|
|
19
|
+
throw Error("Invalid image dimensions or buffer");
|
|
20
|
+
if (image.channels === 4 && image.stride === image.width * 4) {
|
|
21
|
+
let opaque = true;
|
|
22
|
+
for (let i = 3; i < image.width * image.height * 4; i += 4) {
|
|
23
|
+
if (image.data[i] !== 255) {
|
|
24
|
+
opaque = false;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (opaque)
|
|
29
|
+
return image;
|
|
30
|
+
}
|
|
31
|
+
const data = new Uint8Array(image.width * image.height * 4);
|
|
32
|
+
for (let y = 0; y < image.height; y++)
|
|
33
|
+
for (let x = 0; x < image.width; x++) {
|
|
34
|
+
const src = y * image.stride + x * image.channels, dst = (y * image.width + x) * 4;
|
|
35
|
+
data[dst] = image.data[src];
|
|
36
|
+
data[dst + 1] = image.data[src + (image.channels === 1 ? 0 : 1)];
|
|
37
|
+
data[dst + 2] = image.data[src + (image.channels === 1 ? 0 : 2)];
|
|
38
|
+
data[dst + 3] = 255;
|
|
39
|
+
}
|
|
40
|
+
return { data, width: image.width, height: image.height, channels: 4, stride: image.width * 4 };
|
|
41
|
+
}
|
|
42
|
+
/** Flatten decoded outputs only; retain crop-local evidence in recovery.attempts. */
|
|
43
|
+
export class ReleaseDetailScanner {
|
|
44
|
+
scanner;
|
|
45
|
+
disposed = false;
|
|
46
|
+
constructor(scanner) {
|
|
47
|
+
this.scanner = scanner;
|
|
48
|
+
}
|
|
49
|
+
static async create(primary, low, mode) {
|
|
50
|
+
return new ReleaseDetailScanner(await DetailScanner.create(primary, low, mode === "medium" ? 1 : 2));
|
|
51
|
+
}
|
|
52
|
+
scanLocalized(image, policy, fit, full) {
|
|
53
|
+
if (this.disposed)
|
|
54
|
+
throw Error("Scanner is disposed");
|
|
55
|
+
const result = this.scanner.scanLocalized(packed(image), policy ?? {}, fit, full);
|
|
56
|
+
const primaryCount = result.scan.barcodes.length;
|
|
57
|
+
const barcodes = result.recovery.barcodes.map((b, i) => i < primaryCount ? b : { ...b, candidate_indices: [] });
|
|
58
|
+
const localization = result.localization;
|
|
59
|
+
return {
|
|
60
|
+
...result,
|
|
61
|
+
localization: {
|
|
62
|
+
...localization,
|
|
63
|
+
proposals: localization.proposals.map((p) => ({
|
|
64
|
+
polygon: p.polygon,
|
|
65
|
+
score: p.score,
|
|
66
|
+
text: p.text,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
scan: {
|
|
70
|
+
...result.scan,
|
|
71
|
+
barcodes,
|
|
72
|
+
unfinished: result.scan.unfinished ||
|
|
73
|
+
result.recovery.searchLimited ||
|
|
74
|
+
result.recovery.attempts.some((a) => a.unfinished),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
best(frame) {
|
|
79
|
+
return frame.barcodes.reduce((best, b) => (!best || b.support > best.support ? b : best), undefined);
|
|
80
|
+
}
|
|
81
|
+
dispose() {
|
|
82
|
+
if (!this.disposed) {
|
|
83
|
+
this.scanner.dispose();
|
|
84
|
+
this.disposed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Independent EAN-13 supplied-region scanner. Experimental; no localizer or
|
|
2
|
+
* reference decoder is imported. All result confidence remains uncalibrated. */
|
|
3
|
+
export type Point = readonly [number, number];
|
|
4
|
+
export type Quad = readonly [Point, Point, Point, Point];
|
|
5
|
+
export interface Image {
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
channels: 1 | 3 | 4;
|
|
10
|
+
stride: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Policy {
|
|
13
|
+
transitionCleanup?: boolean;
|
|
14
|
+
sourceIdentity?: boolean;
|
|
15
|
+
interiorNormalization?: boolean;
|
|
16
|
+
guardBias?: boolean;
|
|
17
|
+
/** Experimental: accept one full-quiet scanline; increases false-read risk. Default false. */
|
|
18
|
+
allowSingleRow?: boolean;
|
|
19
|
+
maxRetryPathsPerCandidate?: number;
|
|
20
|
+
maxRetryPathsPerFrame?: number;
|
|
21
|
+
maxAssociationChecks?: number;
|
|
22
|
+
maxAssociationPixels?: number;
|
|
23
|
+
maxResults?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface Detection {
|
|
26
|
+
text: string;
|
|
27
|
+
polygon: Quad;
|
|
28
|
+
support: number;
|
|
29
|
+
axis: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Barcode extends Detection {
|
|
32
|
+
candidate_indices: number[];
|
|
33
|
+
}
|
|
34
|
+
export interface Observation {
|
|
35
|
+
text: string;
|
|
36
|
+
axis: number;
|
|
37
|
+
fraction: number;
|
|
38
|
+
left: number;
|
|
39
|
+
right: number;
|
|
40
|
+
cost: number;
|
|
41
|
+
gap: number;
|
|
42
|
+
}
|
|
43
|
+
export interface Candidate {
|
|
44
|
+
candidate_index: number;
|
|
45
|
+
coverage: readonly (readonly [number | null, number | null])[];
|
|
46
|
+
error: boolean;
|
|
47
|
+
error_detail: 'invalid_geometry' | 'sampling_error' | null;
|
|
48
|
+
unfinished: boolean;
|
|
49
|
+
ms: number;
|
|
50
|
+
work: Record<string, number>;
|
|
51
|
+
detections: Detection[];
|
|
52
|
+
observations: Observation[];
|
|
53
|
+
}
|
|
54
|
+
export interface ScanFrame {
|
|
55
|
+
unfinished: boolean;
|
|
56
|
+
candidates: Candidate[];
|
|
57
|
+
barcodes: Barcode[];
|
|
58
|
+
reconciliation: {
|
|
59
|
+
comparisons: number;
|
|
60
|
+
merged: number;
|
|
61
|
+
ambiguous: number;
|
|
62
|
+
conflicting: number;
|
|
63
|
+
pending_observations: number;
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
source_pairs: number;
|
|
66
|
+
source_matches: number;
|
|
67
|
+
source_pixels: number;
|
|
68
|
+
source_capped: number;
|
|
69
|
+
pending_coverage_checks: number;
|
|
70
|
+
pending_quarantined: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface ScanResult extends ScanFrame {
|
|
74
|
+
/** This WASM build has no internal clock. Candidate ms values are unavailable zeros. */
|
|
75
|
+
candidateTimingsAvailable: false;
|
|
76
|
+
/** Actual host wall time, including validation, preparation/copies, decode and JSON parsing. */
|
|
77
|
+
elapsedMs: number;
|
|
78
|
+
}
|
|
79
|
+
/** Explicit indices reject sparse arrays; nonfinite numeric geometry reaches Rust. */
|
|
80
|
+
export declare function isQuadShape(value: unknown): value is Quad;
|
|
81
|
+
export declare class ScannerError extends Error {
|
|
82
|
+
readonly code: string;
|
|
83
|
+
constructor(code: string, message: string);
|
|
84
|
+
}
|
|
85
|
+
/** Validate before asynchronous work; returned bytes are owned by the caller. */
|
|
86
|
+
export declare function snapshotImage(image: Image): Image;
|
|
87
|
+
export declare function snapshotPolicy(policy: Policy): Policy;
|
|
88
|
+
/** Uncalibrated support ordering; ties preserve spatial output order. */
|
|
89
|
+
export declare function rankBarcodes(barcodes: readonly Barcode[]): Barcode[];
|
|
90
|
+
export declare class IndependentScanner {
|
|
91
|
+
#private;
|
|
92
|
+
private constructor();
|
|
93
|
+
static create(bytes: BufferSource): Promise<IndependentScanner>;
|
|
94
|
+
/** All supplied regions receive the cheap pass; successful reads do not end scanning. */
|
|
95
|
+
scan(image: Image, quads: readonly Quad[], policy?: Policy): ScanResult;
|
|
96
|
+
/** Synchronous transaction: upload once, localize, decode the same owned pixels. */
|
|
97
|
+
scanLocalized(image: Image, policy?: Policy, fitLimit?: number, fullFrame?: boolean): {
|
|
98
|
+
localization: any;
|
|
99
|
+
searchWindows: {
|
|
100
|
+
kind: string;
|
|
101
|
+
polygon: number[][];
|
|
102
|
+
candidateIndex: any;
|
|
103
|
+
}[];
|
|
104
|
+
scan: ScanResult;
|
|
105
|
+
localizationMs: number;
|
|
106
|
+
decodingMs: number;
|
|
107
|
+
scanMs: number;
|
|
108
|
+
};
|
|
109
|
+
/** Separate convenience; it never changes find-all work or suppresses frame evidence. */
|
|
110
|
+
best(result: ScanFrame): Barcode | undefined;
|
|
111
|
+
dispose(): void;
|
|
112
|
+
}
|