tapirscan 1.2.0 → 1.2.2
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/README.md +9 -10
- package/dist/index.d.ts +24 -29
- package/dist/index.js +64 -154
- package/dist/multiformat/format-registry.d.ts +26 -0
- package/dist/multiformat/format-registry.js +62 -0
- package/dist/multiformat/formats.d.ts +2 -24
- package/dist/multiformat/formats.js +3 -41
- package/dist/rust-session.d.ts +19 -0
- package/dist/rust-session.js +92 -0
- package/package.json +7 -9
- package/wasm/high-release-1.2.2-public-low-r2.wasm +0 -0
- package/wasm/low-release-1.2.2-public-low-r2.wasm +0 -0
- package/wasm/medium-release-1.2.2-public-low-r2.wasm +0 -0
- package/wasm/very-high-release-1.2.2-public-low-r2.wasm +0 -0
- package/dist/completion-host.d.mts +0 -40
- package/dist/completion-host.mjs +0 -188
- package/dist/detail-20260914/continuity.d.mts +0 -2
- package/dist/detail-20260914/continuity.mjs +0 -62
- package/dist/detail-20260914/detail-proposals-rich.d.mts +0 -13
- package/dist/detail-20260914/detail-proposals-rich.mjs +0 -53
- package/dist/detail-20260914/direct-recovery.d.mts +0 -28
- package/dist/detail-20260914/direct-recovery.mjs +0 -139
- package/dist/detail-20260914/host.d.mts +0 -40
- package/dist/detail-20260914/host.mjs +0 -280
- package/dist/detail-20260914/scanner.d.mts +0 -12
- package/dist/detail-20260914/scanner.mjs +0 -64
- package/dist/detail-20260914/source-evidence.d.mts +0 -10
- package/dist/detail-20260914/source-evidence.mjs +0 -94
- package/dist/detail-canvas.d.ts +0 -35
- package/dist/detail-canvas.js +0 -73
- package/dist/detail-runtime/direct-recovery.d.mts +0 -28
- package/dist/detail-runtime/direct-recovery.mjs +0 -142
- package/dist/detail-runtime/host.d.mts +0 -40
- package/dist/detail-runtime/host.mjs +0 -282
- package/dist/detail-runtime/scanner.d.mts +0 -12
- package/dist/detail-runtime/scanner.mjs +0 -70
- package/dist/detail.d.ts +0 -18
- package/dist/detail.js +0 -84
- package/dist/host.d.ts +0 -112
- package/dist/host.js +0 -184
- package/dist/host64.d.ts +0 -112
- package/dist/host64.js +0 -184
- package/dist/multiformat/coverage.d.ts +0 -7
- package/dist/multiformat/coverage.js +0 -26
- package/dist/multiformat/geometry.d.ts +0 -37
- package/dist/multiformat/geometry.js +0 -132
- package/dist/multiformat/linear-duplicates.d.ts +0 -16
- package/dist/multiformat/linear-duplicates.js +0 -163
- package/dist/multiformat/pixels.d.ts +0 -3
- package/dist/multiformat/pixels.js +0 -36
- package/dist/multiformat/scanner.d.ts +0 -60
- package/dist/multiformat/scanner.js +0 -361
- package/dist/multiformat-host.d.ts +0 -121
- package/dist/multiformat-host.js +0 -207
- package/dist/policy.d.ts +0 -12
- package/dist/policy.js +0 -12
- package/wasm/high-complete-release-20260916.wasm +0 -0
- package/wasm/low-complete-release-20260916.wasm +0 -0
- package/wasm/medium-complete-release-20260916.wasm +0 -0
- package/wasm/multiformat.json +0 -7
- package/wasm/multiformat.wasm +0 -0
- package/wasm/very-high-complete-release-20260916.wasm +0 -0
package/dist/host64.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/** Explicit indices reject sparse arrays; nonfinite numeric geometry reaches Rust. */
|
|
2
|
-
export function isQuadShape(value) {
|
|
3
|
-
if (!Array.isArray(value) || value.length !== 4)
|
|
4
|
-
return false;
|
|
5
|
-
for (let i = 0; i < 4; i++) {
|
|
6
|
-
if (!Object.hasOwn(value, i))
|
|
7
|
-
return false;
|
|
8
|
-
const point = value[i];
|
|
9
|
-
if (!Array.isArray(point) || point.length !== 2)
|
|
10
|
-
return false;
|
|
11
|
-
for (let j = 0; j < 2; j++)
|
|
12
|
-
if (!Object.hasOwn(point, j) || typeof point[j] !== 'number')
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
export class ScannerError extends Error {
|
|
18
|
-
code;
|
|
19
|
-
constructor(code, message) { super(message); this.name = 'ScannerError'; this.code = code; }
|
|
20
|
-
}
|
|
21
|
-
function integer(value, min, max, name) {
|
|
22
|
-
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
23
|
-
throw new ScannerError('invalid_input', `Invalid ${name}`);
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
function status(code) {
|
|
27
|
-
if (code !== 0)
|
|
28
|
-
throw new ScannerError(`core_${code}`, `Independent scanner rejected operation (${code})`);
|
|
29
|
-
}
|
|
30
|
-
function parseFrame(text, count) {
|
|
31
|
-
const v = JSON.parse(text);
|
|
32
|
-
if (!v || typeof v !== 'object')
|
|
33
|
-
throw new ScannerError('invalid_output', 'Expected frame object');
|
|
34
|
-
const f = v;
|
|
35
|
-
if (!Array.isArray(f.candidates) || f.candidates.length !== count || !Array.isArray(f.barcodes) || typeof f.unfinished !== 'boolean' || !f.reconciliation)
|
|
36
|
-
throw new ScannerError('invalid_output', 'Invalid frame shape');
|
|
37
|
-
for (let i = 0; i < f.candidates.length; i++) {
|
|
38
|
-
const c = f.candidates[i];
|
|
39
|
-
if (c.candidate_index !== i || !Array.isArray(c.coverage) || c.coverage.length !== 4 || !Array.isArray(c.observations) || !Array.isArray(c.detections) || typeof c.error !== 'boolean')
|
|
40
|
-
throw new ScannerError('invalid_output', 'Invalid candidate shape');
|
|
41
|
-
}
|
|
42
|
-
for (const b of f.barcodes)
|
|
43
|
-
if (!/^\d{13}$/.test(b.text) || !Array.isArray(b.polygon) || b.polygon.length !== 4 || !Array.isArray(b.candidate_indices) || b.candidate_indices.some(i => !Number.isInteger(i) || i < 0 || i >= count))
|
|
44
|
-
throw new ScannerError('invalid_output', 'Invalid barcode shape');
|
|
45
|
-
return f;
|
|
46
|
-
}
|
|
47
|
-
/** Validate before asynchronous work; returned bytes are owned by the caller. */
|
|
48
|
-
export function snapshotImage(image) {
|
|
49
|
-
if (!image || typeof image !== 'object')
|
|
50
|
-
throw new ScannerError('invalid_input', 'Invalid image');
|
|
51
|
-
const width = integer(image.width, 1, 0xffffffff, 'width'), height = integer(image.height, 1, 0xffffffff, 'height');
|
|
52
|
-
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
53
|
-
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
54
|
-
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
55
|
-
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
56
|
-
if (image.data.byteLength < required)
|
|
57
|
-
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
58
|
-
return { data: new Uint8Array(image.data.subarray(0, required)), width, height, channels: image.channels, stride };
|
|
59
|
-
}
|
|
60
|
-
export function snapshotPolicy(policy) {
|
|
61
|
-
if (!policy || typeof policy !== 'object')
|
|
62
|
-
throw new ScannerError('invalid_input', 'Invalid policy');
|
|
63
|
-
const copy = { ...policy };
|
|
64
|
-
for (const key of ['transitionCleanup', 'sourceIdentity', 'interiorNormalization', 'guardBias', 'allowSingleRow'])
|
|
65
|
-
if (copy[key] !== undefined && typeof copy[key] !== 'boolean')
|
|
66
|
-
throw new ScannerError('invalid_input', `Invalid ${key}`);
|
|
67
|
-
integer(copy.maxRetryPathsPerCandidate ?? 512, 0, 4096, 'candidate budget');
|
|
68
|
-
integer(copy.maxRetryPathsPerFrame ?? 8192, 0, 65536, 'frame budget');
|
|
69
|
-
integer(copy.maxAssociationChecks ?? 200000, 0, 2000000, 'comparison budget');
|
|
70
|
-
integer(copy.maxAssociationPixels ?? 2000000, 0, 16000000, 'pixel budget');
|
|
71
|
-
integer(copy.maxResults ?? 1024, 1, 4096, 'result budget');
|
|
72
|
-
return copy;
|
|
73
|
-
}
|
|
74
|
-
/** Uncalibrated support ordering; ties preserve spatial output order. */
|
|
75
|
-
export function rankBarcodes(barcodes) {
|
|
76
|
-
return [...barcodes].sort((a, b) => b.support - a.support);
|
|
77
|
-
}
|
|
78
|
-
export class IndependentScanner {
|
|
79
|
-
#exports;
|
|
80
|
-
#handle;
|
|
81
|
-
constructor(exports) {
|
|
82
|
-
this.#exports = exports;
|
|
83
|
-
if (exports.regions_version() !== 1)
|
|
84
|
-
throw new ScannerError('abi_version', 'Unsupported scanner ABI');
|
|
85
|
-
this.#handle = exports.regions_new();
|
|
86
|
-
if (!this.#handle)
|
|
87
|
-
throw new ScannerError('capacity', 'Scanner handle capacity exhausted');
|
|
88
|
-
}
|
|
89
|
-
static async create(bytes) {
|
|
90
|
-
const module = await WebAssembly.compile(bytes);
|
|
91
|
-
const instance = await WebAssembly.instantiate(module, {});
|
|
92
|
-
const exports = instance.exports;
|
|
93
|
-
for (const name of ['regions_localize', 'regions_version', 'regions_new', 'regions_destroy', 'regions_prepare', 'regions_input_ptr', 'regions_input_len', 'regions_quads_ptr', 'regions_output_ptr', 'regions_output_len', 'regions_scan']) {
|
|
94
|
-
if (typeof instance.exports[name] !== 'function')
|
|
95
|
-
throw new ScannerError('abi_shape', `Missing ${name}`);
|
|
96
|
-
}
|
|
97
|
-
if (!(exports.memory instanceof WebAssembly.Memory))
|
|
98
|
-
throw new ScannerError('abi_shape', 'Missing memory');
|
|
99
|
-
return new IndependentScanner(exports);
|
|
100
|
-
}
|
|
101
|
-
/** All supplied regions receive the cheap pass; successful reads do not end scanning. */
|
|
102
|
-
scan(image, quads, policy = {}) {
|
|
103
|
-
return this.#scan(image, quads, policy, false);
|
|
104
|
-
}
|
|
105
|
-
/** Synchronous transaction: upload once, localize, decode the same owned pixels. */
|
|
106
|
-
scanLocalized(image, policy = {}, fitLimit = 8, fullFrame = false) {
|
|
107
|
-
const start = performance.now();
|
|
108
|
-
if (!this.#handle)
|
|
109
|
-
throw new ScannerError('disposed', 'Scanner is disposed');
|
|
110
|
-
if (!image || typeof image !== 'object')
|
|
111
|
-
throw new ScannerError('invalid_input', 'Invalid image');
|
|
112
|
-
const width = integer(image.width, 3, 0xffffffff, 'width'), height = integer(image.height, 3, 0xffffffff, 'height');
|
|
113
|
-
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
114
|
-
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
115
|
-
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
116
|
-
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
117
|
-
if (image.data.byteLength < required)
|
|
118
|
-
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
119
|
-
integer(fitLimit, 0, 8, 'shear limit');
|
|
120
|
-
const e = this.#exports, id = this.#handle;
|
|
121
|
-
status(e.regions_prepare(id, width, height, image.channels, stride));
|
|
122
|
-
if (e.regions_input_len(id) !== required)
|
|
123
|
-
throw new ScannerError('abi_shape', 'Input allocation mismatch');
|
|
124
|
-
new Uint8Array(e.memory.buffer, e.regions_input_ptr(id), required).set(image.data.subarray(0, required));
|
|
125
|
-
status(e.regions_localize(id, fitLimit));
|
|
126
|
-
const localization = JSON.parse(new TextDecoder().decode(new Uint8Array(e.memory.buffer, e.regions_output_ptr(id), e.regions_output_len(id))));
|
|
127
|
-
if (!Array.isArray(localization.proposals) || localization.proposals.length > (fullFrame ? 63 : 64) || localization.proposals.some((p) => !isQuadShape(p.polygon)))
|
|
128
|
-
throw new ScannerError('abi_shape', 'Invalid localization');
|
|
129
|
-
const searchWindows = fullFrame ? [{ kind: 'full_frame_search', polygon: [[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], candidateIndex: localization.proposals.length }] : [];
|
|
130
|
-
const localizationMs = performance.now() - start, decodeStart = performance.now();
|
|
131
|
-
const scan = this.#scan(image, [...localization.proposals.map((p) => p.polygon), ...searchWindows.map(p => p.polygon)], policy, true);
|
|
132
|
-
return { localization, searchWindows, scan, localizationMs, decodingMs: performance.now() - decodeStart, scanMs: performance.now() - start };
|
|
133
|
-
}
|
|
134
|
-
#scan(image, quads, policy, prepared) {
|
|
135
|
-
const start = performance.now();
|
|
136
|
-
if (!this.#handle)
|
|
137
|
-
throw new ScannerError('disposed', 'Scanner is disposed');
|
|
138
|
-
if (!image || typeof image !== 'object' || !Array.isArray(quads) || !policy || typeof policy !== 'object')
|
|
139
|
-
throw new ScannerError('invalid_input', 'Invalid scan arguments');
|
|
140
|
-
for (const key of ['transitionCleanup', 'sourceIdentity', 'interiorNormalization', 'guardBias', 'allowSingleRow'])
|
|
141
|
-
if (policy[key] !== undefined && typeof policy[key] !== 'boolean')
|
|
142
|
-
throw new ScannerError('invalid_input', `Invalid ${key}`);
|
|
143
|
-
const e = this.#exports, id = this.#handle;
|
|
144
|
-
const width = integer(image.width, 1, 0xffffffff, 'width'), height = integer(image.height, 1, 0xffffffff, 'height');
|
|
145
|
-
if (![1, 3, 4].includes(image.channels) || !(image.data instanceof Uint8Array))
|
|
146
|
-
throw new ScannerError('invalid_input', 'Invalid image buffer/channels');
|
|
147
|
-
const stride = integer(image.stride, width * image.channels, 0xffffffff, 'stride');
|
|
148
|
-
const required = integer((height - 1) * stride + width * image.channels, 1, 128 * 1024 * 1024, 'image length');
|
|
149
|
-
if (image.data.byteLength < required)
|
|
150
|
-
throw new ScannerError('invalid_input', 'Image buffer is too short');
|
|
151
|
-
integer(quads.length, 0, 64, 'candidate count');
|
|
152
|
-
for (const q of quads)
|
|
153
|
-
if (!isQuadShape(q))
|
|
154
|
-
throw new ScannerError('invalid_input', 'Invalid quad shape');
|
|
155
|
-
const perCandidate = integer(policy.maxRetryPathsPerCandidate ?? 512, 0, 4096, 'candidate budget');
|
|
156
|
-
const perFrame = integer(policy.maxRetryPathsPerFrame ?? 8192, 0, 65536, 'frame budget');
|
|
157
|
-
const checks = integer(policy.maxAssociationChecks ?? 200000, 0, 2000000, 'comparison budget');
|
|
158
|
-
const pixels = integer(policy.maxAssociationPixels ?? 2000000, 0, 16000000, 'pixel budget');
|
|
159
|
-
const results = integer(policy.maxResults ?? 1024, 1, 4096, 'result budget');
|
|
160
|
-
const flags = (policy.transitionCleanup ? 1 : 0) | (policy.sourceIdentity ? 2 : 0) | (policy.interiorNormalization ? 4 : 0) | (policy.guardBias ? 8 : 0) | (policy.allowSingleRow ? 16 : 0);
|
|
161
|
-
if (!prepared) {
|
|
162
|
-
status(e.regions_prepare(id, width, height, image.channels, stride));
|
|
163
|
-
if (e.regions_input_len(id) !== required)
|
|
164
|
-
throw new ScannerError('abi_shape', 'Input allocation mismatch');
|
|
165
|
-
new Uint8Array(e.memory.buffer, e.regions_input_ptr(id), required).set(image.data.subarray(0, required));
|
|
166
|
-
}
|
|
167
|
-
const coordinates = new Float64Array(e.memory.buffer, e.regions_quads_ptr(id), 512);
|
|
168
|
-
quads.forEach((q, i) => q.forEach((p, j) => { coordinates[i * 8 + j * 2] = p[0]; coordinates[i * 8 + j * 2 + 1] = p[1]; }));
|
|
169
|
-
status(e.regions_scan(id, quads.length, flags, perCandidate, perFrame, checks, pixels, results));
|
|
170
|
-
// Scan can grow memory. Never reuse the earlier input/coordinate views.
|
|
171
|
-
const output = new Uint8Array(e.memory.buffer, e.regions_output_ptr(id), e.regions_output_len(id)).slice();
|
|
172
|
-
const frame = parseFrame(new TextDecoder().decode(output), quads.length);
|
|
173
|
-
return { ...frame, candidateTimingsAvailable: false, elapsedMs: performance.now() - start };
|
|
174
|
-
}
|
|
175
|
-
/** Separate convenience; it never changes find-all work or suppresses frame evidence. */
|
|
176
|
-
best(result) {
|
|
177
|
-
return rankBarcodes(result.barcodes)[0];
|
|
178
|
-
}
|
|
179
|
-
dispose() { if (this.#handle) {
|
|
180
|
-
const id = this.#handle;
|
|
181
|
-
this.#handle = 0;
|
|
182
|
-
status(this.#exports.regions_destroy(id));
|
|
183
|
-
} }
|
|
184
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Quad } from "../host.js";
|
|
2
|
-
/** Convex containment, including the boundary; reject degenerate coverage. */
|
|
3
|
-
export declare function containsPoint(point: readonly number[], quad: Quad): boolean;
|
|
4
|
-
/** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
|
|
5
|
-
export declare function uncoveredRetryMask(proposals: readonly {
|
|
6
|
-
polygon: Quad;
|
|
7
|
-
}[], coverage: readonly Quad[], initial?: readonly number[]): [number, number];
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** Convex containment, including the boundary; reject degenerate coverage. */
|
|
2
|
-
export function containsPoint(point, quad) {
|
|
3
|
-
if (!point.every(Number.isFinite) || quad.some((p) => !p.every(Number.isFinite)))
|
|
4
|
-
return false;
|
|
5
|
-
let positive = false;
|
|
6
|
-
let negative = false;
|
|
7
|
-
let area = 0;
|
|
8
|
-
for (let i = 0; i < 4; i++) {
|
|
9
|
-
const a = quad[i], b = quad[(i + 1) % 4];
|
|
10
|
-
const cross = (b[0] - a[0]) * (point[1] - a[1]) - (b[1] - a[1]) * (point[0] - a[0]);
|
|
11
|
-
positive ||= cross > 1e-6;
|
|
12
|
-
negative ||= cross < -1e-6;
|
|
13
|
-
area += a[0] * b[1] - b[0] * a[1];
|
|
14
|
-
}
|
|
15
|
-
return Math.abs(area) > 1e-6 && !(positive && negative);
|
|
16
|
-
}
|
|
17
|
-
/** Only deep retries are deferred. Normal proposals and the full-frame bit remain. */
|
|
18
|
-
export function uncoveredRetryMask(proposals, coverage, initial = [0xffffffff, 0xffffffff]) {
|
|
19
|
-
const mask = [initial[0] >>> 0, initial[1] >>> 0];
|
|
20
|
-
proposals.forEach((proposal, index) => {
|
|
21
|
-
if (coverage.some((quad) => proposal.polygon.every((point) => containsPoint(point, quad)))) {
|
|
22
|
-
mask[index >>> 5] = (mask[index >>> 5] & ~(1 << (index & 31))) >>> 0;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
return mask;
|
|
26
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { Quad } from "../host.js";
|
|
2
|
-
export type Transform = readonly number[];
|
|
3
|
-
export declare function project(t: Transform, x: number, y: number): [number, number];
|
|
4
|
-
export declare function transformFor(quad: Quad, width: number, height: number): Transform;
|
|
5
|
-
export declare function rectificationPlan(quad: Quad): {
|
|
6
|
-
width: number;
|
|
7
|
-
height: number;
|
|
8
|
-
transform: number[];
|
|
9
|
-
};
|
|
10
|
-
export declare function rectify(gray: Uint8Array, width: number, height: number, quad: Quad): {
|
|
11
|
-
data: Uint8Array<ArrayBuffer>;
|
|
12
|
-
width: number;
|
|
13
|
-
height: number;
|
|
14
|
-
transform: number[];
|
|
15
|
-
};
|
|
16
|
-
/** Convex intersection, including opposite winding and projective quadrilaterals. */
|
|
17
|
-
export declare function polygonOverlap(a: Quad, b: Quad): {
|
|
18
|
-
iou: number;
|
|
19
|
-
smaller: number;
|
|
20
|
-
a: number;
|
|
21
|
-
b: number;
|
|
22
|
-
};
|
|
23
|
-
/** Suppress overlapping reads of the same physical symbol, never by text alone. */
|
|
24
|
-
export declare function distinctReads<T extends {
|
|
25
|
-
text: string;
|
|
26
|
-
format: string;
|
|
27
|
-
polygon: Quad;
|
|
28
|
-
support: number;
|
|
29
|
-
eanAddOn?: string;
|
|
30
|
-
readerInitialization?: boolean;
|
|
31
|
-
structuredAppend?: {
|
|
32
|
-
index: number;
|
|
33
|
-
count: number;
|
|
34
|
-
id?: string;
|
|
35
|
-
parity?: number;
|
|
36
|
-
};
|
|
37
|
-
}>(reads: readonly T[]): T[];
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
export function project(t, x, y) {
|
|
2
|
-
const z = t[6] * x + t[7] * y + 1;
|
|
3
|
-
return [(t[0] * x + t[1] * y + t[2]) / z, (t[3] * x + t[4] * y + t[5]) / z];
|
|
4
|
-
}
|
|
5
|
-
export function transformFor(quad, width, height) {
|
|
6
|
-
const source = [
|
|
7
|
-
[0, 0],
|
|
8
|
-
[width, 0],
|
|
9
|
-
[width, height],
|
|
10
|
-
[0, height],
|
|
11
|
-
];
|
|
12
|
-
const a = [];
|
|
13
|
-
for (let i = 0; i < 4; i++) {
|
|
14
|
-
const [x, y] = source[i], [u, v] = quad[i];
|
|
15
|
-
a.push([x, y, 1, 0, 0, 0, -u * x, -u * y, u], [0, 0, 0, x, y, 1, -v * x, -v * y, v]);
|
|
16
|
-
}
|
|
17
|
-
for (let col = 0; col < 8; col++) {
|
|
18
|
-
let pivot = col;
|
|
19
|
-
for (let i = col + 1; i < 8; i++)
|
|
20
|
-
if (Math.abs(a[i][col]) > Math.abs(a[pivot][col]))
|
|
21
|
-
pivot = i;
|
|
22
|
-
if (Math.abs(a[pivot][col]) < 1e-9)
|
|
23
|
-
throw Error("Degenerate barcode region.");
|
|
24
|
-
[a[col], a[pivot]] = [a[pivot], a[col]];
|
|
25
|
-
const divisor = a[col][col];
|
|
26
|
-
for (let j = col; j <= 8; j++)
|
|
27
|
-
a[col][j] /= divisor;
|
|
28
|
-
for (let i = 0; i < 8; i++) {
|
|
29
|
-
if (i === col)
|
|
30
|
-
continue;
|
|
31
|
-
const scale = a[i][col];
|
|
32
|
-
for (let j = col; j <= 8; j++)
|
|
33
|
-
a[i][j] -= scale * a[col][j];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return a.map((row) => row[8]);
|
|
37
|
-
}
|
|
38
|
-
export function rectificationPlan(quad) {
|
|
39
|
-
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
40
|
-
const w = Math.max(8, Math.ceil(Math.max(distance(quad[0], quad[1]), distance(quad[3], quad[2]))));
|
|
41
|
-
const h = Math.max(8, Math.ceil(Math.max(distance(quad[0], quad[3]), distance(quad[1], quad[2]))));
|
|
42
|
-
if (w * h > 8 * 1024 * 1024)
|
|
43
|
-
throw Error("Barcode region exceeds rectification budget.");
|
|
44
|
-
const base = transformFor(quad, w, h);
|
|
45
|
-
const pad = 8, divisor = 1 - pad * (base[6] + base[7]);
|
|
46
|
-
const t = [
|
|
47
|
-
base[0] / divisor,
|
|
48
|
-
base[1] / divisor,
|
|
49
|
-
(base[2] - pad * (base[0] + base[1])) / divisor,
|
|
50
|
-
base[3] / divisor,
|
|
51
|
-
base[4] / divisor,
|
|
52
|
-
(base[5] - pad * (base[3] + base[4])) / divisor,
|
|
53
|
-
base[6] / divisor,
|
|
54
|
-
base[7] / divisor,
|
|
55
|
-
];
|
|
56
|
-
const paddedWidth = w + 2 * pad, paddedHeight = h + 2 * pad;
|
|
57
|
-
return { width: paddedWidth, height: paddedHeight, transform: t };
|
|
58
|
-
}
|
|
59
|
-
export function rectify(gray, width, height, quad) {
|
|
60
|
-
const { width: paddedWidth, height: paddedHeight, transform: t } = rectificationPlan(quad);
|
|
61
|
-
const data = new Uint8Array(paddedWidth * paddedHeight);
|
|
62
|
-
for (let y = 0; y < paddedHeight; y++) {
|
|
63
|
-
const cy = y + 0.5;
|
|
64
|
-
const yX = t[1] * cy, yY = t[4] * cy, yZ = t[7] * cy;
|
|
65
|
-
for (let x = 0; x < paddedWidth; x++) {
|
|
66
|
-
const cx = x + 0.5;
|
|
67
|
-
const z = t[6] * cx + yZ + 1;
|
|
68
|
-
const xx = (t[0] * cx + yX + t[2]) / z;
|
|
69
|
-
const yy = (t[3] * cx + yY + t[5]) / z;
|
|
70
|
-
if (xx < 0 || yy < 0 || xx > width - 1 || yy > height - 1) {
|
|
71
|
-
data[y * paddedWidth + x] = 255;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const x0 = Math.floor(xx), y0 = Math.floor(yy), fx = xx - x0, fy = yy - y0;
|
|
75
|
-
const x1 = Math.min(width - 1, x0 + 1), y1 = Math.min(height - 1, y0 + 1);
|
|
76
|
-
data[y * paddedWidth + x] = Math.round((gray[y0 * width + x0] * (1 - fx) + gray[y0 * width + x1] * fx) * (1 - fy) +
|
|
77
|
-
(gray[y1 * width + x0] * (1 - fx) + gray[y1 * width + x1] * fx) * fy);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return { data, width: paddedWidth, height: paddedHeight, transform: t };
|
|
81
|
-
}
|
|
82
|
-
/** Convex intersection, including opposite winding and projective quadrilaterals. */
|
|
83
|
-
export function polygonOverlap(a, b) {
|
|
84
|
-
const signedArea = (p) => p.reduce((sum, q, i) => {
|
|
85
|
-
const r = p[(i + 1) % p.length];
|
|
86
|
-
return sum + q[0] * r[1] - q[1] * r[0];
|
|
87
|
-
}, 0) / 2;
|
|
88
|
-
const aa = Math.abs(signedArea(a)), bb = Math.abs(signedArea(b));
|
|
89
|
-
if (Math.min(aa, bb) < 1e-6)
|
|
90
|
-
return { iou: 0, smaller: 0, a: 0, b: 0 };
|
|
91
|
-
const winding = Math.sign(signedArea(b));
|
|
92
|
-
let points = [...a];
|
|
93
|
-
for (let i = 0; i < 4 && points.length; i++) {
|
|
94
|
-
const p = b[i], q = b[(i + 1) % 4];
|
|
95
|
-
const side = (r) => winding * ((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]));
|
|
96
|
-
const clipped = [];
|
|
97
|
-
for (let j = 0; j < points.length; j++) {
|
|
98
|
-
const u = points[j], v = points[(j + 1) % points.length], su = side(u), sv = side(v);
|
|
99
|
-
if (su >= 0)
|
|
100
|
-
clipped.push(u);
|
|
101
|
-
if (su >= 0 !== sv >= 0) {
|
|
102
|
-
const t = su / (su - sv);
|
|
103
|
-
clipped.push([u[0] + t * (v[0] - u[0]), u[1] + t * (v[1] - u[1])]);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
points = clipped;
|
|
107
|
-
}
|
|
108
|
-
const intersection = Math.min(aa, bb, Math.abs(signedArea(points)));
|
|
109
|
-
return {
|
|
110
|
-
iou: intersection / (aa + bb - intersection),
|
|
111
|
-
smaller: intersection / Math.min(aa, bb),
|
|
112
|
-
a: intersection / aa,
|
|
113
|
-
b: intersection / bb,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/** Suppress overlapping reads of the same physical symbol, never by text alone. */
|
|
117
|
-
export function distinctReads(reads) {
|
|
118
|
-
const result = [];
|
|
119
|
-
for (const read of [...reads].sort((a, b) => b.support - a.support)) {
|
|
120
|
-
if (!result.some((r) => r.text === read.text &&
|
|
121
|
-
r.format === read.format &&
|
|
122
|
-
r.eanAddOn === read.eanAddOn &&
|
|
123
|
-
Boolean(r.readerInitialization) === Boolean(read.readerInitialization) &&
|
|
124
|
-
r.structuredAppend?.index === read.structuredAppend?.index &&
|
|
125
|
-
r.structuredAppend?.count === read.structuredAppend?.count &&
|
|
126
|
-
r.structuredAppend?.id === read.structuredAppend?.id &&
|
|
127
|
-
r.structuredAppend?.parity === read.structuredAppend?.parity &&
|
|
128
|
-
polygonOverlap(r.polygon, read.polygon).smaller >= 0.65))
|
|
129
|
-
result.push(read);
|
|
130
|
-
}
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Image, Quad } from "../host.js";
|
|
2
|
-
type Read = {
|
|
3
|
-
text: string;
|
|
4
|
-
format: string;
|
|
5
|
-
polygon: Quad;
|
|
6
|
-
support: number;
|
|
7
|
-
eanAddOn?: string;
|
|
8
|
-
gs1?: boolean;
|
|
9
|
-
readerInitialization?: boolean;
|
|
10
|
-
};
|
|
11
|
-
/** Only merge equal-value, aligned bands when source pixels connect their bars.
|
|
12
|
-
* This does not change search coverage or retry budgets. Exhausting the small
|
|
13
|
-
* evidence budget leaves detections separate, never drops an unchecked read.
|
|
14
|
-
*/
|
|
15
|
-
export declare function mergeLinearDuplicates<T extends Read>(reads: readonly T[], image: Image): T[];
|
|
16
|
-
export {};
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { polygonOverlap } from "./geometry.js";
|
|
2
|
-
const formats = new Set(["EAN13", "UPCA", "EAN8", "UPCE", "Code128", "Code39", "ITF"]);
|
|
3
|
-
function bitCount(value) {
|
|
4
|
-
let bits = value - ((value >>> 1) & 0x55555555);
|
|
5
|
-
bits = (bits & 0x33333333) + ((bits >>> 2) & 0x33333333);
|
|
6
|
-
return (((bits + (bits >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
|
|
7
|
-
}
|
|
8
|
-
const midpoint = (a, b) => [
|
|
9
|
-
(a[0] + b[0]) / 2,
|
|
10
|
-
(a[1] + b[1]) / 2,
|
|
11
|
-
];
|
|
12
|
-
const line = (q) => [midpoint(q[0], q[3]), midpoint(q[1], q[2])];
|
|
13
|
-
/** Only merge equal-value, aligned bands when source pixels connect their bars.
|
|
14
|
-
* This does not change search coverage or retry budgets. Exhausting the small
|
|
15
|
-
* evidence budget leaves detections separate, never drops an unchecked read.
|
|
16
|
-
*/
|
|
17
|
-
export function mergeLinearDuplicates(reads, image) {
|
|
18
|
-
if (reads.length < 2)
|
|
19
|
-
return [...reads];
|
|
20
|
-
const seen = new Set();
|
|
21
|
-
let repeated = false;
|
|
22
|
-
for (const read of reads) {
|
|
23
|
-
if (!read.text || !formats.has(read.format))
|
|
24
|
-
continue;
|
|
25
|
-
const key = `${read.format}:${read.text}`;
|
|
26
|
-
if (seen.has(key)) {
|
|
27
|
-
repeated = true;
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
seen.add(key);
|
|
31
|
-
}
|
|
32
|
-
if (!repeated)
|
|
33
|
-
return [...reads];
|
|
34
|
-
let remaining = 32768;
|
|
35
|
-
const result = [];
|
|
36
|
-
let values;
|
|
37
|
-
const profile = (left, right) => {
|
|
38
|
-
if (remaining < 64)
|
|
39
|
-
return undefined;
|
|
40
|
-
remaining -= 64;
|
|
41
|
-
values ??= new Float64Array(64);
|
|
42
|
-
let lo = 255, hi = 0;
|
|
43
|
-
for (let i = 0; i < 64; i++) {
|
|
44
|
-
const f = (i + 0.5) / 64;
|
|
45
|
-
const x = Math.round(left[0] + (right[0] - left[0]) * f);
|
|
46
|
-
const y = Math.round(left[1] + (right[1] - left[1]) * f);
|
|
47
|
-
if (x < 0 || y < 0 || x >= image.width || y >= image.height)
|
|
48
|
-
return undefined;
|
|
49
|
-
const p = y * image.stride + x * image.channels;
|
|
50
|
-
const value = image.channels === 1
|
|
51
|
-
? image.data[p]
|
|
52
|
-
: (77 * image.data[p] + 150 * image.data[p + 1] + 29 * image.data[p + 2]) / 256;
|
|
53
|
-
values[i] = value;
|
|
54
|
-
lo = Math.min(lo, value);
|
|
55
|
-
hi = Math.max(hi, value);
|
|
56
|
-
}
|
|
57
|
-
if (hi - lo < 24)
|
|
58
|
-
return undefined;
|
|
59
|
-
const threshold = (lo + hi) / 2;
|
|
60
|
-
let first = 0, second = 0;
|
|
61
|
-
for (let i = 0; i < 32; i++) {
|
|
62
|
-
if (values[i] < threshold)
|
|
63
|
-
first |= 1 << i;
|
|
64
|
-
if (values[i + 32] < threshold)
|
|
65
|
-
second |= 1 << i;
|
|
66
|
-
}
|
|
67
|
-
return [first, second];
|
|
68
|
-
};
|
|
69
|
-
const connected = (a, inputB) => {
|
|
70
|
-
if (!a.every((p) => p.every(Number.isFinite)) || !inputB.every((p) => p.every(Number.isFinite)))
|
|
71
|
-
return undefined;
|
|
72
|
-
const al = line(a), bl = line(inputB);
|
|
73
|
-
const ax = al[1][0] - al[0][0], ay = al[1][1] - al[0][1];
|
|
74
|
-
let bx = bl[1][0] - bl[0][0], by = bl[1][1] - bl[0][1];
|
|
75
|
-
const aw = Math.hypot(ax, ay), bw = Math.hypot(bx, by);
|
|
76
|
-
if (aw < 24 || bw / aw < 0.9 || bw / aw > 1.1)
|
|
77
|
-
return undefined;
|
|
78
|
-
let b = inputB;
|
|
79
|
-
if (ax * bx + ay * by < 0) {
|
|
80
|
-
b = [inputB[2], inputB[3], inputB[0], inputB[1]];
|
|
81
|
-
bx = -bx;
|
|
82
|
-
by = -by;
|
|
83
|
-
}
|
|
84
|
-
if ((ax * bx + ay * by) / (aw * bw) < 0.996)
|
|
85
|
-
return undefined;
|
|
86
|
-
const ac = midpoint(al[0], al[1]), bline = line(b), bc = midpoint(bline[0], bline[1]);
|
|
87
|
-
const dx = bc[0] - ac[0], dy = bc[1] - ac[1];
|
|
88
|
-
if (Math.abs(dx * ax + dy * ay) / aw > aw * 0.06)
|
|
89
|
-
return undefined;
|
|
90
|
-
// Bound endpoint movement too: rotation/perspective can move an edge farther
|
|
91
|
-
// than the centers. Every interpolated sample then moves at most one pixel.
|
|
92
|
-
const distance = Math.max(Math.hypot(bline[0][0] - al[0][0], bline[0][1] - al[0][1]), Math.hypot(bline[1][0] - al[1][0], bline[1][1] - al[1][1]));
|
|
93
|
-
const steps = Math.ceil(distance);
|
|
94
|
-
if (steps < 1 || steps > 384 || (steps + 2) * 64 > remaining)
|
|
95
|
-
return undefined;
|
|
96
|
-
const reference = profile(al[0], al[1]);
|
|
97
|
-
if (!reference)
|
|
98
|
-
return undefined;
|
|
99
|
-
let dark0 = reference[0], dark1 = reference[1];
|
|
100
|
-
let light0 = ~dark0, light1 = ~dark1;
|
|
101
|
-
const minimumDark = Math.max(4, Math.ceil((bitCount(dark0) + bitCount(dark1)) / 4));
|
|
102
|
-
const minimumLight = Math.max(4, Math.ceil((bitCount(light0) + bitCount(light1)) / 4));
|
|
103
|
-
for (let step = 1; step <= steps; step++) {
|
|
104
|
-
const f = step / steps;
|
|
105
|
-
const l = [
|
|
106
|
-
al[0][0] + (bline[0][0] - al[0][0]) * f,
|
|
107
|
-
al[0][1] + (bline[0][1] - al[0][1]) * f,
|
|
108
|
-
];
|
|
109
|
-
const r = [
|
|
110
|
-
al[1][0] + (bline[1][0] - al[1][0]) * f,
|
|
111
|
-
al[1][1] + (bline[1][1] - al[1][1]) * f,
|
|
112
|
-
];
|
|
113
|
-
const sample = profile(l, r);
|
|
114
|
-
if (!sample)
|
|
115
|
-
return undefined;
|
|
116
|
-
const disagreement = bitCount(reference[0] ^ sample[0]) + bitCount(reference[1] ^ sample[1]);
|
|
117
|
-
if (disagreement > 12)
|
|
118
|
-
return undefined;
|
|
119
|
-
dark0 &= sample[0];
|
|
120
|
-
dark1 &= sample[1];
|
|
121
|
-
light0 &= ~sample[0];
|
|
122
|
-
light1 &= ~sample[1];
|
|
123
|
-
// A skewed separator may cross columns at different heights. Require
|
|
124
|
-
// individual bars AND spaces to survive the entire bridge, not just rows.
|
|
125
|
-
if (bitCount(dark0) + bitCount(dark1) < minimumDark ||
|
|
126
|
-
bitCount(light0) + bitCount(light1) < minimumLight)
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
const along = (p) => (-ay * p[0] + ax * p[1]) / aw;
|
|
130
|
-
const top = along(midpoint(a[0], a[1])) < along(midpoint(b[0], b[1])) ? a : b;
|
|
131
|
-
const bottom = along(midpoint(a[2], a[3])) > along(midpoint(b[2], b[3])) ? a : b;
|
|
132
|
-
return [top[0], top[1], bottom[2], bottom[3]];
|
|
133
|
-
};
|
|
134
|
-
for (const read of [...reads].sort((a, b) => b.support - a.support)) {
|
|
135
|
-
if (!formats.has(read.format) || !read.text || remaining < 192) {
|
|
136
|
-
result.push(read);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
let merged = false;
|
|
140
|
-
for (let i = 0; i < result.length; i++) {
|
|
141
|
-
const other = result[i];
|
|
142
|
-
if (read.format !== other.format ||
|
|
143
|
-
read.text !== other.text ||
|
|
144
|
-
read.eanAddOn !== other.eanAddOn ||
|
|
145
|
-
Boolean(read.gs1) !== Boolean(other.gs1) ||
|
|
146
|
-
Boolean(read.readerInitialization) !== Boolean(other.readerInitialization))
|
|
147
|
-
continue;
|
|
148
|
-
if (polygonOverlap(other.polygon, read.polygon).smaller >= 0.65) {
|
|
149
|
-
merged = true;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
const polygon = connected(other.polygon, read.polygon);
|
|
153
|
-
if (!polygon)
|
|
154
|
-
continue;
|
|
155
|
-
result[i] = { ...other, polygon }; // Keep strongest support; observations may overlap.
|
|
156
|
-
merged = true;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
if (!merged)
|
|
160
|
-
result.push(read);
|
|
161
|
-
}
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
2
|
-
/** Input dimensions are validated by the scanner. Preserve its integer luma rule. */
|
|
3
|
-
export function toGray({ data, width, height, channels, stride }) {
|
|
4
|
-
const count = width * height;
|
|
5
|
-
if (channels === 1 && stride === width)
|
|
6
|
-
return data.slice(0, count);
|
|
7
|
-
const gray = new Uint8Array(count);
|
|
8
|
-
if (channels === 4 && stride === width * 4 && littleEndian && data.byteOffset % 4 === 0) {
|
|
9
|
-
const words = new Uint32Array(data.buffer, data.byteOffset, count);
|
|
10
|
-
for (let i = 0; i < count; i++) {
|
|
11
|
-
const value = words[i];
|
|
12
|
-
gray[i] =
|
|
13
|
-
((value & 255) * 77 + ((value >>> 8) & 255) * 150 + ((value >>> 16) & 255) * 29 + 128) >>>
|
|
14
|
-
8;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
else if (channels === 4 && stride === width * 4) {
|
|
18
|
-
for (let i = 0, j = 0; j < count; i += 4, j++)
|
|
19
|
-
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
20
|
-
}
|
|
21
|
-
else if (channels === 3 && stride === width * 3) {
|
|
22
|
-
for (let i = 0, j = 0; j < count; i += 3, j++)
|
|
23
|
-
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
24
|
-
}
|
|
25
|
-
else if (channels === 1) {
|
|
26
|
-
for (let y = 0; y < height; y++)
|
|
27
|
-
gray.set(data.subarray(y * stride, y * stride + width), y * width);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
for (let y = 0; y < height; y++) {
|
|
31
|
-
for (let x = 0, i = y * stride, j = y * width; x < width; x++, i += channels, j++)
|
|
32
|
-
gray[j] = (data[i] * 77 + data[i + 1] * 150 + data[i + 2] * 29 + 128) >> 8;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return gray;
|
|
36
|
-
}
|