@scanmate/image-fix 0.0.3
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 +291 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +2786 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/src/index.d.ts +58 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/lib/align.d.ts +136 -0
- package/dist/src/lib/align.d.ts.map +1 -0
- package/dist/src/lib/analysis/content.d.ts +50 -0
- package/dist/src/lib/analysis/content.d.ts.map +1 -0
- package/dist/src/lib/analysis/score.d.ts +18 -0
- package/dist/src/lib/analysis/score.d.ts.map +1 -0
- package/dist/src/lib/estimate/coarse.d.ts +43 -0
- package/dist/src/lib/estimate/coarse.d.ts.map +1 -0
- package/dist/src/lib/estimate/features.d.ts +72 -0
- package/dist/src/lib/estimate/features.d.ts.map +1 -0
- package/dist/src/lib/estimate/match.d.ts +38 -0
- package/dist/src/lib/estimate/match.d.ts.map +1 -0
- package/dist/src/lib/estimate/models.d.ts +41 -0
- package/dist/src/lib/estimate/models.d.ts.map +1 -0
- package/dist/src/lib/estimate/phaseCorrelation.d.ts +34 -0
- package/dist/src/lib/estimate/phaseCorrelation.d.ts.map +1 -0
- package/dist/src/lib/estimate/ransac.d.ts +36 -0
- package/dist/src/lib/estimate/ransac.d.ts.map +1 -0
- package/dist/src/lib/image/codec.d.ts +30 -0
- package/dist/src/lib/image/codec.d.ts.map +1 -0
- package/dist/src/lib/image/gray.d.ts +67 -0
- package/dist/src/lib/image/gray.d.ts.map +1 -0
- package/dist/src/lib/image/raster.d.ts +19 -0
- package/dist/src/lib/image/raster.d.ts.map +1 -0
- package/dist/src/lib/image/resize.d.ts +32 -0
- package/dist/src/lib/image/resize.d.ts.map +1 -0
- package/dist/src/lib/image/warp.d.ts +28 -0
- package/dist/src/lib/image/warp.d.ts.map +1 -0
- package/dist/src/lib/math/fft.d.ts +15 -0
- package/dist/src/lib/math/fft.d.ts.map +1 -0
- package/dist/src/lib/math/linalg.d.ts +34 -0
- package/dist/src/lib/math/linalg.d.ts.map +1 -0
- package/dist/src/lib/math/matrix.d.ts +75 -0
- package/dist/src/lib/math/matrix.d.ts.map +1 -0
- package/dist/src/lib/math/random.d.ts +13 -0
- package/dist/src/lib/math/random.d.ts.map +1 -0
- package/dist/src/lib/regions.d.ts +75 -0
- package/dist/src/lib/regions.d.ts.map +1 -0
- package/dist/src/lib/testing/synthetic.d.ts +73 -0
- package/dist/src/lib/testing/synthetic.d.ts.map +1 -0
- package/dist/src/lib/types.d.ts +86 -0
- package/dist/src/lib/types.d.ts.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@scanmate/image-fix` - put a scanned page back on top of the page it came from.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { readFile } from 'node:fs/promises'
|
|
6
|
+
* import { alignScan, compareRegions } from '@scanmate/image-fix'
|
|
7
|
+
*
|
|
8
|
+
* const original = await readFile('contract.page1.png') // rendered from the PDF
|
|
9
|
+
* const scanned = await readFile('returned.jpg') // what came back
|
|
10
|
+
*
|
|
11
|
+
* const result = alignScan(original, scanned)
|
|
12
|
+
* console.log(result.confidence, result.transform.rotationDeg)
|
|
13
|
+
*
|
|
14
|
+
* const [signature] = compareRegions(original, result.raster, [
|
|
15
|
+
* { id: 'signature', rect: { x: 76, y: 905, width: 420, height: 78 } },
|
|
16
|
+
* ])
|
|
17
|
+
* console.log(signature.filled)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `result.raster` is on the original's canvas, so every coordinate you already
|
|
21
|
+
* know from the PDF still means what it meant - which is what makes both an
|
|
22
|
+
* OCR diff and a "was this box signed" check possible.
|
|
23
|
+
*
|
|
24
|
+
* Everything here is pure JavaScript. No native bindings, so nothing to rebuild
|
|
25
|
+
* per platform when it deploys to an Azure Function app.
|
|
26
|
+
*/
|
|
27
|
+
export { alignScan, polishTranslation } from './lib/align.js';
|
|
28
|
+
export type { AlignDiagnostics, AlignOptions, AlignResult } from './lib/align.js';
|
|
29
|
+
export { compareRegions, diffDocument, renderDiff } from './lib/regions.js';
|
|
30
|
+
export type { DocumentDiff, Region, RegionOptions, RegionReport } from './lib/regions.js';
|
|
31
|
+
export type { BinaryImage, GrayImage, ImageInput, Matrix3, Point, PointMatch, Raster, Rect, TransformModel, TransformSummary, } from './lib/types.js';
|
|
32
|
+
export { decodeImage, encodeImage, sniffFormat } from './lib/image/codec.js';
|
|
33
|
+
export type { EncodeOptions, ImageFormat } from './lib/image/codec.js';
|
|
34
|
+
export { binarize, boxBlur, coverage, dilate, grayToRaster, inkMap, otsuThreshold, toGrayscale } from './lib/image/gray.js';
|
|
35
|
+
export type { InkOptions } from './lib/image/gray.js';
|
|
36
|
+
export { createBinary, createGray, createRaster, cloneRaster, isRaster } from './lib/image/raster.js';
|
|
37
|
+
export { boxBlurRaster, downscaleGray, resizeGray } from './lib/image/resize.js';
|
|
38
|
+
export { sampleGrayBilinear, warpGray, warpRaster } from './lib/image/warp.js';
|
|
39
|
+
export type { Interpolation, WarpOptions } from './lib/image/warp.js';
|
|
40
|
+
export { contentExtent, estimateSkew } from './lib/analysis/content.js';
|
|
41
|
+
export type { ContentExtent, SkewOptions } from './lib/analysis/content.js';
|
|
42
|
+
export { correlation, intersectionOverUnion, mean } from './lib/analysis/score.js';
|
|
43
|
+
export { applyPoint, conjugateScale, decompose, determinant, IDENTITY, invert, isPlausible, mapRectCorners, multiply, normalize, rebase, reprojectionError, scaling, similarity, translation, } from './lib/math/matrix.js';
|
|
44
|
+
export { estimateCoarse } from './lib/estimate/coarse.js';
|
|
45
|
+
export type { CoarseOptions, CoarseResult } from './lib/estimate/coarse.js';
|
|
46
|
+
export { detectAndDescribe } from './lib/estimate/features.js';
|
|
47
|
+
export type { FeatureOptions, FeatureSet, Keypoint } from './lib/estimate/features.js';
|
|
48
|
+
export { hamming, matchFeatures, popcount } from './lib/estimate/match.js';
|
|
49
|
+
export type { MatchOptions } from './lib/estimate/match.js';
|
|
50
|
+
export { fitAffine, fitHomography, fitModel, fitSimilarity, minimumSamples } from './lib/estimate/models.js';
|
|
51
|
+
export type { Correspondence } from './lib/estimate/models.js';
|
|
52
|
+
export { phaseCorrelate } from './lib/estimate/phaseCorrelation.js';
|
|
53
|
+
export type { PhaseCorrelationResult } from './lib/estimate/phaseCorrelation.js';
|
|
54
|
+
export { findInliers, ransac } from './lib/estimate/ransac.js';
|
|
55
|
+
export type { RansacOptions, RansacResult } from './lib/estimate/ransac.js';
|
|
56
|
+
export { createSyntheticDocument, drawSignature, drawTick, simulateScan, } from './lib/testing/synthetic.js';
|
|
57
|
+
export type { DocumentOptions, ScanOptions, SimulatedScan, SyntheticDocument, } from './lib/testing/synthetic.js';
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ImageFormat } from './image/codec.js';
|
|
2
|
+
import type { InkOptions } from './image/gray.js';
|
|
3
|
+
import type { Interpolation } from './image/warp.js';
|
|
4
|
+
import type { GrayImage, ImageInput, Matrix3, Raster, TransformModel, TransformSummary } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Align a scan onto the page it was made from.
|
|
7
|
+
*
|
|
8
|
+
* ## What this is for
|
|
9
|
+
*
|
|
10
|
+
* Two questions about a returned form are easy to answer once the scan sits
|
|
11
|
+
* exactly on top of the original, and near-impossible before:
|
|
12
|
+
*
|
|
13
|
+
* 1. *Was anything in the printed text changed?* Run OCR on both and diff.
|
|
14
|
+
* That only works if the two are the same page at the same size, otherwise
|
|
15
|
+
* the OCR engine's own layout analysis is comparing different documents.
|
|
16
|
+
* 2. *Was the box at (x, y) signed?* That is a question about a fixed
|
|
17
|
+
* rectangle, and a fixed rectangle only means something once both images
|
|
18
|
+
* agree on where (x, y) is. See `compareRegions`.
|
|
19
|
+
*
|
|
20
|
+
* ## The pipeline
|
|
21
|
+
*
|
|
22
|
+
* ```text
|
|
23
|
+
* decode ─► ink ─► coarse guess ─► rough warp ─► features ─► RANSAC ─► warp
|
|
24
|
+
* (scale/skew) (ORB) (model)
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The coarse guess exists to make the feature stage possible at all: binary
|
|
28
|
+
* descriptors compare fixed pixel offsets, so they only match between images
|
|
29
|
+
* at comparable scale, and nothing in a JPEG tells you what dpi it was scanned
|
|
30
|
+
* at. Once the scan has been resampled to roughly the right size, matching is
|
|
31
|
+
* easy and RANSAC can throw away the inevitable wrong matches — a page of text
|
|
32
|
+
* is full of genuinely identical-looking corners.
|
|
33
|
+
*
|
|
34
|
+
* If the feature stage comes up short (a nearly blank form has few corners to
|
|
35
|
+
* find), the coarse estimate is returned on its own, and `method` says so.
|
|
36
|
+
*
|
|
37
|
+
* ## Why it is synchronous
|
|
38
|
+
*
|
|
39
|
+
* All of it is CPU-bound with no I/O to wait on. An `async` signature would
|
|
40
|
+
* suggest the event loop is free during the call, and it is not. To align
|
|
41
|
+
* several pages at once, put this in a worker thread.
|
|
42
|
+
*/
|
|
43
|
+
export interface AlignOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Transform family to fit.
|
|
46
|
+
*
|
|
47
|
+
* `similarity` (the default) covers a flatbed or sheet-fed scan: the page is
|
|
48
|
+
* flat, so it can only be turned, resized and moved. Use `affine` when one
|
|
49
|
+
* axis is stretched, and `homography` for photographs taken off-axis, where
|
|
50
|
+
* the far edge of the page is genuinely smaller than the near one.
|
|
51
|
+
*/
|
|
52
|
+
model?: TransformModel;
|
|
53
|
+
/** Longest side used for feature detection. Bigger is more precise and quadratically slower. */
|
|
54
|
+
workingSize?: number;
|
|
55
|
+
/** Longest side used for the coarse guess. */
|
|
56
|
+
coarseSize?: number;
|
|
57
|
+
maxFeatures?: number;
|
|
58
|
+
/** Inlier radius for RANSAC, in working-resolution pixels. */
|
|
59
|
+
ransacThreshold?: number;
|
|
60
|
+
/** Fewer surviving correspondences than this and the feature stage is not trusted. */
|
|
61
|
+
minInliers?: number;
|
|
62
|
+
/** Largest per-page skew the coarse stage considers, in degrees. */
|
|
63
|
+
maxSkewDeg?: number;
|
|
64
|
+
/** Cap on how much bigger or smaller the scan may be than the original. */
|
|
65
|
+
maxScaleRatio?: number;
|
|
66
|
+
/** How far a correspondence may move, as a fraction of the page diagonal, after the coarse warp. */
|
|
67
|
+
maxDisplacementRatio?: number;
|
|
68
|
+
/** Background/ink separation. The defaults suit printed pages on white. */
|
|
69
|
+
ink?: InkOptions;
|
|
70
|
+
interpolation?: Interpolation;
|
|
71
|
+
/** RGBA fill where the scan does not cover the original's canvas. */
|
|
72
|
+
background?: [number, number, number, number];
|
|
73
|
+
/** Encoding of `result.image`. `'none'` skips encoding, which is most of the cost on a big page. */
|
|
74
|
+
output?: ImageFormat | 'none';
|
|
75
|
+
/** JPEG quality when `output` is `'jpeg'`. */
|
|
76
|
+
quality?: number;
|
|
77
|
+
/** Seeds RANSAC and the descriptor pattern, so the same input gives the same matrix. */
|
|
78
|
+
seed?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface AlignDiagnostics {
|
|
81
|
+
coarseScore: number;
|
|
82
|
+
coarseStrategy: string;
|
|
83
|
+
/** Each page's own skew, in degrees, as measured independently. */
|
|
84
|
+
skewDeg: {
|
|
85
|
+
original: number;
|
|
86
|
+
scanned: number;
|
|
87
|
+
};
|
|
88
|
+
features: {
|
|
89
|
+
original: number;
|
|
90
|
+
scanned: number;
|
|
91
|
+
};
|
|
92
|
+
matches: number;
|
|
93
|
+
inliers: number;
|
|
94
|
+
inlierRatio: number;
|
|
95
|
+
/** Mean RANSAC reprojection error over the inliers, in working-resolution pixels. */
|
|
96
|
+
reprojectionError: number;
|
|
97
|
+
/** Ink correlation after alignment, in `[-1, 1]`. */
|
|
98
|
+
correlation: number;
|
|
99
|
+
/** Ink mask overlap after alignment, in `[0, 1]`. */
|
|
100
|
+
intersectionOverUnion: number;
|
|
101
|
+
/** Milliseconds spent, end to end. */
|
|
102
|
+
durationMs: number;
|
|
103
|
+
}
|
|
104
|
+
export interface AlignResult {
|
|
105
|
+
/** The scan resampled onto the original's canvas, same width and height as the original. */
|
|
106
|
+
raster: Raster;
|
|
107
|
+
/** `raster` encoded per `options.output`, or `null` when that was `'none'`. */
|
|
108
|
+
image: Uint8Array | null;
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
/** Maps original coordinates to scanned coordinates. */
|
|
112
|
+
matrix: Matrix3;
|
|
113
|
+
/** Maps scanned coordinates back to original coordinates. */
|
|
114
|
+
inverse: Matrix3;
|
|
115
|
+
transform: TransformSummary;
|
|
116
|
+
/**
|
|
117
|
+
* How much to trust the result, in `[0, 1]`.
|
|
118
|
+
*
|
|
119
|
+
* Derived from ink correlation after warping, so it measures agreement in the
|
|
120
|
+
* output rather than confidence in the process. Above ~0.6 is a solid match on
|
|
121
|
+
* a printed page; below ~0.3 treat the alignment as failed.
|
|
122
|
+
*/
|
|
123
|
+
confidence: number;
|
|
124
|
+
method: 'features' | 'coarse';
|
|
125
|
+
diagnostics: AlignDiagnostics;
|
|
126
|
+
}
|
|
127
|
+
export declare function alignScan(original: ImageInput, scanned: ImageInput, options?: AlignOptions): AlignResult;
|
|
128
|
+
/**
|
|
129
|
+
* Nudge an existing transform by whatever residual translation is still measurable.
|
|
130
|
+
*
|
|
131
|
+
* Exposed because it is occasionally useful on its own: if you already know the
|
|
132
|
+
* transform from a previous page of the same batch, this re-seats it on the
|
|
133
|
+
* current page for a fraction of the cost of a full alignment.
|
|
134
|
+
*/
|
|
135
|
+
export declare function polishTranslation(originalInk: GrayImage, scannedInk: GrayImage, matrix: Matrix3, workingSize?: number): Matrix3;
|
|
136
|
+
//# sourceMappingURL=align.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"align.d.ts","sourceRoot":"","sources":["../../../src/lib/align.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GrayImage, Point } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where the printing is, and which way up it is.
|
|
4
|
+
*
|
|
5
|
+
* Feature matching needs the two images at roughly the same scale before its
|
|
6
|
+
* descriptors mean anything, and nothing in the file headers tells us the dpi
|
|
7
|
+
* the scanner used. What does tell us is the printing itself: the block of ink
|
|
8
|
+
* on the page is the same physical object in both images, so the ratio of its
|
|
9
|
+
* measured sizes is the ratio of the resolutions. Measuring it needs the skew
|
|
10
|
+
* out of the way first, which is what {@link estimateSkew} is for.
|
|
11
|
+
*/
|
|
12
|
+
export interface ContentExtent {
|
|
13
|
+
/** Extent along the rotated axes, in pixels. */
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
/** Centre of the extent, back in unrotated image coordinates. */
|
|
17
|
+
center: Point;
|
|
18
|
+
/** The angle the extent was measured at, in radians. */
|
|
19
|
+
angle: number;
|
|
20
|
+
/** Mean ink over the whole image. Near zero means there was nothing to measure. */
|
|
21
|
+
density: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SkewOptions {
|
|
24
|
+
/** Largest skew to consider, in degrees, in either direction. */
|
|
25
|
+
maxAngleDeg?: number;
|
|
26
|
+
/** Longest side of the image the search runs on. Skew does not need detail. */
|
|
27
|
+
workingSize?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extent of the ink along axes rotated by `angle`, trimming outliers.
|
|
31
|
+
*
|
|
32
|
+
* `trim` is a fraction of the total ink discarded from each end of each axis.
|
|
33
|
+
* A scanner that clips a black strip down one edge, or a speck of dust, would
|
|
34
|
+
* otherwise set the page boundary — and since this measurement becomes the
|
|
35
|
+
* scale estimate, a 2% error here is a 2% error in every coordinate downstream.
|
|
36
|
+
*/
|
|
37
|
+
export declare function contentExtent(ink: GrayImage, angle?: number, trim?: number): ContentExtent;
|
|
38
|
+
/**
|
|
39
|
+
* The page's own skew, in radians, from the sharpness of its ink profile.
|
|
40
|
+
*
|
|
41
|
+
* Rotate the page until the rows of text stack up: at the right angle every
|
|
42
|
+
* line of type falls into one bin of the projection histogram and the profile
|
|
43
|
+
* is a comb of tall spikes; a degree off and each line smears across several
|
|
44
|
+
* bins. Sum of squares rewards exactly that concentration — same total ink,
|
|
45
|
+
* fewer bins, bigger number. Searched coarse to fine so the cost stays flat.
|
|
46
|
+
*/
|
|
47
|
+
export declare function estimateSkew(ink: GrayImage, options?: SkewOptions): number;
|
|
48
|
+
/** Sum of squares of the ink profile projected onto the axis perpendicular to `angle`. */
|
|
49
|
+
export declare function profileSharpness(ink: GrayImage, angle: number): number;
|
|
50
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../../src/lib/analysis/content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAEhD;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,KAAK,EAAI,MAAM,CAAA;IACf,MAAM,EAAG,MAAM,CAAA;IACf,iEAAiE;IACjE,MAAM,EAAG,KAAK,CAAA;IACd,wDAAwD;IACxD,KAAK,EAAI,MAAM,CAAA;IACf,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAE,GAAG,EAAE,SAAS,EAAE,KAAK,SAAI,EAAE,IAAI,SAAQ,GAAG,aAAa,CAoErF;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAE,GAAG,EAAE,SAAS,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CA2B/E;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsBvE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BinaryImage, GrayImage } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* How well two ink images actually overlap.
|
|
4
|
+
*
|
|
5
|
+
* Every stage of the pipeline proposes a transform; this is the referee. It
|
|
6
|
+
* has to be a *correlation*, not a difference: the scan is darker, or fainter,
|
|
7
|
+
* or contrast-stretched by the scanner's own firmware, and a sum of absolute
|
|
8
|
+
* differences would rank a badly aligned pale scan above a well aligned dark
|
|
9
|
+
* one. Zero-mean normalised cross correlation is invariant to both of those —
|
|
10
|
+
* it only asks whether the ink rises and falls in the same places.
|
|
11
|
+
*/
|
|
12
|
+
/** Zero-mean normalised cross correlation of two equally sized images, in `[-1, 1]`. */
|
|
13
|
+
export declare function correlation(a: GrayImage, b: GrayImage): number;
|
|
14
|
+
/** Intersection over union of two masks. The pixel-level version of "did it land on top of it". */
|
|
15
|
+
export declare function intersectionOverUnion(a: BinaryImage, b: BinaryImage): number;
|
|
16
|
+
/** Mean of a single channel image. */
|
|
17
|
+
export declare function mean(image: GrayImage): number;
|
|
18
|
+
//# sourceMappingURL=score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../../../src/lib/analysis/score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEtD;;;;;;;;;GASG;AAEH,wFAAwF;AACxF,wBAAgB,WAAW,CAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CA8B/D;AAED,mGAAmG;AACnG,wBAAgB,qBAAqB,CAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,MAAM,CAa7E;AAED,sCAAsC;AACtC,wBAAgB,IAAI,CAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAM9C"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GrayImage, Matrix3 } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A first, cheap answer good enough to make the expensive one possible.
|
|
4
|
+
*
|
|
5
|
+
* Descriptor matching has a blind spot: BRIEF compares fixed pixel offsets, so
|
|
6
|
+
* a scan at 300 dpi and a page rendered at 150 describe the same corner with
|
|
7
|
+
* two unrelated bit strings. Something has to establish roughly how big the
|
|
8
|
+
* scan is before the matcher runs, and nothing in the file says.
|
|
9
|
+
*
|
|
10
|
+
* So guess, several ways, and let the pixels judge:
|
|
11
|
+
*
|
|
12
|
+
* - **frame** — assume the scan is the whole page, so the frames correspond.
|
|
13
|
+
* - **content** — assume the *printing* corresponds. Robust to a scan with
|
|
14
|
+
* wider margins, which the frame guess gets badly wrong.
|
|
15
|
+
* - **deskew** — measure each page's own skew, and match the printing in the
|
|
16
|
+
* frame where each sits straight. This is the one that usually wins.
|
|
17
|
+
*
|
|
18
|
+
* Each gets a phase-correlation nudge, then all of them are warped and scored
|
|
19
|
+
* on ink correlation. Guessing several times and measuring is far more robust
|
|
20
|
+
* than one clever guess, and at this resolution each attempt costs very little.
|
|
21
|
+
*/
|
|
22
|
+
export interface CoarseOptions {
|
|
23
|
+
/** Longest side of the images the search runs on. */
|
|
24
|
+
workingSize?: number;
|
|
25
|
+
/** Largest per-page skew considered, in degrees. */
|
|
26
|
+
maxSkewDeg?: number;
|
|
27
|
+
/** Largest scale ratio between the two images that will be entertained. */
|
|
28
|
+
maxScaleRatio?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CoarseResult {
|
|
31
|
+
/** Maps full-resolution original coordinates to full-resolution scan coordinates. */
|
|
32
|
+
matrix: Matrix3;
|
|
33
|
+
/** Ink correlation achieved by this transform, in `[-1, 1]`. */
|
|
34
|
+
score: number;
|
|
35
|
+
/** Which guess won, for diagnostics. */
|
|
36
|
+
strategy: string;
|
|
37
|
+
skew: {
|
|
38
|
+
original: number;
|
|
39
|
+
scanned: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export declare function estimateCoarse(originalInk: GrayImage, scannedInk: GrayImage, options?: CoarseOptions): CoarseResult;
|
|
43
|
+
//# sourceMappingURL=coarse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coarse.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/coarse.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAGlD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,WAAW,CAAC,EAAI,MAAM,CAAA;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAK,MAAM,CAAA;IACtB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,MAAM,EAAI,OAAO,CAAA;IACjB,gEAAgE;IAChE,KAAK,EAAK,MAAM,CAAA;IAChB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAM;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAChD;AAOD,wBAAgB,cAAc,CAC5B,WAAW,EAAE,SAAS,EACtB,UAAU,EAAE,SAAS,EACrB,OAAO,GAAE,aAAkB,GAC1B,YAAY,CAkEd"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { GrayImage } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* FAST corners with steered BRIEF descriptors — an ORB, written out.
|
|
4
|
+
*
|
|
5
|
+
* This is the piece OpenCV would normally hand you, and it is here because the
|
|
6
|
+
* deployment target rules out a native binding. The three parts each answer a
|
|
7
|
+
* separate question:
|
|
8
|
+
*
|
|
9
|
+
* - **FAST** answers *where*. A pixel is a corner when a contiguous arc of the
|
|
10
|
+
* 16 pixels on a circle around it is all clearly brighter, or all clearly
|
|
11
|
+
* darker, than it is. On a page that fires on stroke ends, serifs, and the
|
|
12
|
+
* corners of rules and boxes — landmarks that survive being rescanned.
|
|
13
|
+
* - **The intensity centroid** answers *which way up*. The vector from the
|
|
14
|
+
* patch's centre to its centre of mass is a direction the ink itself defines,
|
|
15
|
+
* so it turns with the page.
|
|
16
|
+
* - **BRIEF** answers *what it looks like*, as 256 yes/no questions of the form
|
|
17
|
+
* "is this pixel darker than that one?", asked at positions rotated by that
|
|
18
|
+
* angle. Comparing two of those is one XOR and a bit count, which is why
|
|
19
|
+
* brute-force matching thousands of them is affordable.
|
|
20
|
+
*
|
|
21
|
+
* The descriptor is not scale invariant on its own, hence the pyramid: the same
|
|
22
|
+
* corner is described at several sizes so a scan at a different dpi still
|
|
23
|
+
* matches.
|
|
24
|
+
*/
|
|
25
|
+
export interface Keypoint {
|
|
26
|
+
/** Coordinates in the *input* image, pixel centres, regardless of the level found at. */
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
score: number;
|
|
30
|
+
/** Dominant ink direction in radians. */
|
|
31
|
+
angle: number;
|
|
32
|
+
level: number;
|
|
33
|
+
/** Pixel size of the patch described, in input pixels. */
|
|
34
|
+
size: number;
|
|
35
|
+
}
|
|
36
|
+
export interface FeatureSet {
|
|
37
|
+
keypoints: Keypoint[];
|
|
38
|
+
/** 8 x 32 bits per keypoint, laid out contiguously. */
|
|
39
|
+
descriptors: Uint32Array;
|
|
40
|
+
}
|
|
41
|
+
export interface FeatureOptions {
|
|
42
|
+
maxFeatures?: number;
|
|
43
|
+
/** Contrast a circle pixel must clear to count, in `[0, 1]` ink units. */
|
|
44
|
+
fastThreshold?: number;
|
|
45
|
+
/** Pyramid levels, including the original. */
|
|
46
|
+
levels?: number;
|
|
47
|
+
/** Ratio between consecutive levels. */
|
|
48
|
+
scaleFactor?: number;
|
|
49
|
+
/** Side of the described patch, in pixels of its own level. */
|
|
50
|
+
patchSize?: number;
|
|
51
|
+
/** Cells per axis used to spread keypoints over the page instead of over its densest paragraph. */
|
|
52
|
+
gridSize?: number;
|
|
53
|
+
seed?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare const DESCRIPTOR_WORDS = 8;
|
|
56
|
+
export declare function detectAndDescribe(image: GrayImage, options?: FeatureOptions): FeatureSet;
|
|
57
|
+
interface Corner {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
score: number;
|
|
61
|
+
}
|
|
62
|
+
/** FAST-9 with a 3x3 non-maximum suppression pass over the corner scores. */
|
|
63
|
+
export declare function detectFast(image: GrayImage, threshold: number, border: number): Corner[];
|
|
64
|
+
/**
|
|
65
|
+
* Angle from the patch centre to its centre of intensity mass.
|
|
66
|
+
*
|
|
67
|
+
* On an ink image the mass is the writing, so the angle turns with the page —
|
|
68
|
+
* which is the entire trick that makes a binary descriptor rotation invariant.
|
|
69
|
+
*/
|
|
70
|
+
export declare function orientation(image: GrayImage, cx: number, cy: number, radius: number): number;
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=features.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/features.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,QAAQ;IACvB,yFAAyF;IACzF,CAAC,EAAM,MAAM,CAAA;IACb,CAAC,EAAM,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,IAAI,EAAG,MAAM,CAAA;CACd;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAI,QAAQ,EAAE,CAAA;IACvB,uDAAuD;IACvD,WAAW,EAAE,WAAW,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAI,MAAM,CAAA;IACtB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8CAA8C;IAC9C,MAAM,CAAC,EAAS,MAAM,CAAA;IACtB,wCAAwC;IACxC,WAAW,CAAC,EAAI,MAAM,CAAA;IACtB,+DAA+D;IAC/D,SAAS,CAAC,EAAM,MAAM,CAAA;IACtB,mGAAmG;IACnG,QAAQ,CAAC,EAAO,MAAM,CAAA;IACtB,IAAI,CAAC,EAAW,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,gBAAgB,IAAI,CAAA;AAajC,wBAAgB,iBAAiB,CAAE,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,UAAU,CAuD7F;AAED,UAAU,MAAM;IACd,CAAC,EAAM,MAAM,CAAA;IACb,CAAC,EAAM,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA6DzF;AAyED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAqB7F"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PointMatch } from '../types.js';
|
|
2
|
+
import type { FeatureSet } from './features.js';
|
|
3
|
+
/**
|
|
4
|
+
* Brute-force descriptor matching.
|
|
5
|
+
*
|
|
6
|
+
* Brute force is the right algorithm here, not a concession. A page yields
|
|
7
|
+
* around a thousand keypoints per side; a million 256-bit comparisons is eight
|
|
8
|
+
* million XOR-and-popcount operations, which is milliseconds. Building an index
|
|
9
|
+
* to avoid that would cost more than it saves and would only return
|
|
10
|
+
* approximate neighbours.
|
|
11
|
+
*
|
|
12
|
+
* The filters matter more than the search does:
|
|
13
|
+
*
|
|
14
|
+
* - **Ratio test.** Keep a match only when the best candidate is clearly better
|
|
15
|
+
* than the runner-up. On a page of repeated letterforms the nearest neighbour
|
|
16
|
+
* is often meaningless, and the giveaway is that the second nearest is just
|
|
17
|
+
* as close.
|
|
18
|
+
* - **Cross-check.** Both sides must name each other. One-directional bests are
|
|
19
|
+
* not symmetric, and the asymmetric ones are usually wrong.
|
|
20
|
+
* - **Displacement gate.** The images are already roughly aligned when this
|
|
21
|
+
* runs, so a correspondence that jumps half the page is not a correspondence.
|
|
22
|
+
*/
|
|
23
|
+
export interface MatchOptions {
|
|
24
|
+
/** Lowe's ratio. Lower is stricter. */
|
|
25
|
+
ratio?: number;
|
|
26
|
+
/** Reject matches further apart than this many bits out of 256. */
|
|
27
|
+
maxDistance?: number;
|
|
28
|
+
/** Require both descriptors to pick each other. */
|
|
29
|
+
crossCheck?: boolean;
|
|
30
|
+
/** Reject correspondences that move further than this, in pixels. `Infinity` disables the gate. */
|
|
31
|
+
maxDisplacement?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function matchFeatures(source: FeatureSet, target: FeatureSet, options?: MatchOptions): PointMatch[];
|
|
34
|
+
/** Hamming distance between two 256-bit descriptors. */
|
|
35
|
+
export declare function hamming(a: Uint32Array, offsetA: number, b: Uint32Array, offsetB: number): number;
|
|
36
|
+
/** SWAR bit count: pair off, then nibble off, then one multiply to sum the bytes. */
|
|
37
|
+
export declare function popcount(value: number): number;
|
|
38
|
+
//# sourceMappingURL=match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAG5C;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,CAAC,EAAY,MAAM,CAAA;IACxB,mEAAmE;IACnE,WAAW,CAAC,EAAM,MAAM,CAAA;IACxB,mDAAmD;IACnD,UAAU,CAAC,EAAO,OAAO,CAAA;IACzB,mGAAmG;IACnG,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAUD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,YAAiB,GACzB,UAAU,EAAE,CA2Ed;AAED,wDAAwD;AACxD,wBAAgB,OAAO,CAAE,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjG;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM/C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Matrix3, PointMatch, TransformModel } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fitting a transform to a set of correspondences.
|
|
4
|
+
*
|
|
5
|
+
* Three models, and the choice between them is a bet about what the scan went
|
|
6
|
+
* through. A flatbed scanner moves the page in one plane, so a **similarity**
|
|
7
|
+
* (turn it, resize it, slide it) is the whole story and its four parameters
|
|
8
|
+
* are pinned down by very few points — which is exactly what you want when
|
|
9
|
+
* most of your matches are wrong. A sheet-fed scanner can stretch one axis;
|
|
10
|
+
* that needs **affine**. A photograph taken at an angle needs the full
|
|
11
|
+
* **homography**, and pays for those eight parameters by being far easier to
|
|
12
|
+
* fit to nonsense.
|
|
13
|
+
*
|
|
14
|
+
* Prefer the simplest model the physical situation allows.
|
|
15
|
+
*/
|
|
16
|
+
/** Correspondences the fitters consume. `distance` is ignored here. */
|
|
17
|
+
export type Correspondence = Pick<PointMatch, 'source' | 'target'>;
|
|
18
|
+
/** How many correspondences the model needs before it is determined at all. */
|
|
19
|
+
export declare function minimumSamples(model: TransformModel): number;
|
|
20
|
+
export declare function fitModel(model: TransformModel, matches: readonly Correspondence[], indices?: readonly number[]): Matrix3 | null;
|
|
21
|
+
/**
|
|
22
|
+
* Least-squares similarity, in closed form.
|
|
23
|
+
*
|
|
24
|
+
* No iteration and no matrix inverse: centre both point sets, and the rotation
|
|
25
|
+
* and scale fall out of two dot products. That closed form is why similarity
|
|
26
|
+
* survives a RANSAC sample that affine would choke on.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fitSimilarity(matches: readonly Correspondence[], indices?: readonly number[]): Matrix3 | null;
|
|
29
|
+
/** Least-squares affine: two independent 3x3 normal systems sharing one matrix. */
|
|
30
|
+
export declare function fitAffine(matches: readonly Correspondence[], indices?: readonly number[]): Matrix3 | null;
|
|
31
|
+
/**
|
|
32
|
+
* Direct Linear Transform with Hartley normalisation.
|
|
33
|
+
*
|
|
34
|
+
* The normalisation is not optional polish. Raw pixel coordinates put entries
|
|
35
|
+
* like `x * u` (order 10^6) next to a constant 1 in the same row, and the
|
|
36
|
+
* eigen solve then answers a question dominated by the big column. Centring
|
|
37
|
+
* each point set and scaling it to a mean radius of `sqrt(2)` puts every
|
|
38
|
+
* column on the same footing; the result is mapped back afterwards.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fitHomography(matches: readonly Correspondence[], indices?: readonly number[]): Matrix3 | null;
|
|
41
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/models.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAEnE;;;;;;;;;;;;;GAaG;AAEH,uEAAuE;AACvE,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAA;AAElE,+EAA+E;AAC/E,wBAAgB,cAAc,CAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAS7D;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,GAAG,IAAI,CAShB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,GAAG,IAAI,CA4ChB;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CACvB,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,GAAG,IAAI,CAmChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,GAAG,IAAI,CAoChB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GrayImage } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Global translation from the Fourier shift theorem.
|
|
4
|
+
*
|
|
5
|
+
* Shifting an image does not change the magnitude of its spectrum, only the
|
|
6
|
+
* phase — and it changes the phase by an amount proportional to the shift. So
|
|
7
|
+
* divide out the magnitudes entirely, keep the phase difference, transform
|
|
8
|
+
* back, and what comes out is a single spike at the offset between the two
|
|
9
|
+
* images.
|
|
10
|
+
*
|
|
11
|
+
* Its value here is that it does not care what is *on* the page. Feature
|
|
12
|
+
* matching needs corners to match; a mostly blank form does not have enough of
|
|
13
|
+
* them, and neither does a page that scanned faint. Phase correlation uses
|
|
14
|
+
* every pixel at once, which makes it the fallback when features fail, and a
|
|
15
|
+
* good final polish when they succeed.
|
|
16
|
+
*
|
|
17
|
+
* It only finds translation. Rotation and scale have to be dealt with first.
|
|
18
|
+
*/
|
|
19
|
+
export interface PhaseCorrelationResult {
|
|
20
|
+
/** Shift that takes `a` onto `b`: a feature at `p` in `a` sits at `p + (dx, dy)` in `b`. */
|
|
21
|
+
dx: number;
|
|
22
|
+
dy: number;
|
|
23
|
+
/** Height of the correlation spike. Near 1 is a clean single answer; near 0 is noise. */
|
|
24
|
+
peak: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Correlate two equally sized images.
|
|
28
|
+
*
|
|
29
|
+
* Both are Hann-windowed first. Without it the FFT sees the frame edges as a
|
|
30
|
+
* hard discontinuity repeating forever, and that cross pattern in the spectrum
|
|
31
|
+
* can be a stronger signal than the page.
|
|
32
|
+
*/
|
|
33
|
+
export declare function phaseCorrelate(a: GrayImage, b: GrayImage): PhaseCorrelationResult;
|
|
34
|
+
//# sourceMappingURL=phaseCorrelation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phaseCorrelation.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/phaseCorrelation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,sBAAsB;IACrC,4FAA4F;IAC5F,EAAE,EAAI,MAAM,CAAA;IACZ,EAAE,EAAI,MAAM,CAAA;IACZ,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,sBAAsB,CA6DlF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Matrix3, TransformModel } from '../types.js';
|
|
2
|
+
import type { Correspondence } from './models.js';
|
|
3
|
+
/**
|
|
4
|
+
* RANSAC: fit the model that the largest number of correspondences agree with.
|
|
5
|
+
*
|
|
6
|
+
* Feature matching on a document produces a lot of confident nonsense, because
|
|
7
|
+
* the page is full of things that genuinely look identical — every lowercase
|
|
8
|
+
* "e", every corner of every table cell. Least squares over all of them is
|
|
9
|
+
* dragged wherever the wrong ones point. RANSAC ignores the average: it draws
|
|
10
|
+
* the smallest sample that determines a transform, counts how many of the rest
|
|
11
|
+
* that transform explains, and repeats. A wrong sample agrees with almost
|
|
12
|
+
* nothing; the right one agrees with everything real on the page.
|
|
13
|
+
*/
|
|
14
|
+
export interface RansacOptions {
|
|
15
|
+
model: TransformModel;
|
|
16
|
+
/** A correspondence is an inlier when it reprojects within this many pixels. */
|
|
17
|
+
threshold: number;
|
|
18
|
+
maxIterations?: number;
|
|
19
|
+
/** Probability of having drawn at least one all-inlier sample. Drives early exit. */
|
|
20
|
+
confidence?: number;
|
|
21
|
+
/** Below this many inliers the answer is rejected outright. */
|
|
22
|
+
minInliers?: number;
|
|
23
|
+
seed?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RansacResult {
|
|
26
|
+
matrix: Matrix3;
|
|
27
|
+
/** Indices into the input array. */
|
|
28
|
+
inliers: number[];
|
|
29
|
+
inlierRatio: number;
|
|
30
|
+
iterations: number;
|
|
31
|
+
/** Mean reprojection error over the inliers, in pixels. */
|
|
32
|
+
error: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function ransac(matches: readonly Correspondence[], options: RansacOptions): RansacResult | null;
|
|
35
|
+
export declare function findInliers(matches: readonly Correspondence[], matrix: Matrix3, threshold: number): number[];
|
|
36
|
+
//# sourceMappingURL=ransac.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ransac.d.ts","sourceRoot":"","sources":["../../../../src/lib/estimate/ransac.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAG9C;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAW,cAAc,CAAA;IAC9B,gFAAgF;IAChF,SAAS,EAAO,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,qFAAqF;IACrF,UAAU,CAAC,EAAK,MAAM,CAAA;IACtB,+DAA+D;IAC/D,UAAU,CAAC,EAAK,MAAM,CAAA;IACtB,IAAI,CAAC,EAAW,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAO,OAAO,CAAA;IACpB,oCAAoC;IACpC,OAAO,EAAM,MAAM,EAAE,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAQ,MAAM,CAAA;CACpB;AAED,wBAAgB,MAAM,CACpB,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,OAAO,EAAE,aAAa,GACrB,YAAY,GAAG,IAAI,CAmErB;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAMV"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ImageInput, Raster } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encoding and decoding, in pure JavaScript on purpose.
|
|
4
|
+
*
|
|
5
|
+
* `sharp` would be faster, and it is the wrong choice here: it is a native
|
|
6
|
+
* binding to libvips, so the bytes that work on your laptop are not the bytes
|
|
7
|
+
* that run in the function app, and a deployment that skips the rebuild fails
|
|
8
|
+
* at *import* time — after the cold start, in production, with a stack trace
|
|
9
|
+
* about a missing `.node` file. `pngjs` and `jpeg-js` are slower and they are
|
|
10
|
+
* the same JavaScript everywhere, which is the whole point of the constraint
|
|
11
|
+
* this library was written under.
|
|
12
|
+
*/
|
|
13
|
+
export type ImageFormat = 'png' | 'jpeg';
|
|
14
|
+
export interface EncodeOptions {
|
|
15
|
+
format?: ImageFormat;
|
|
16
|
+
/** JPEG only, 1-100. Ignored for PNG. */
|
|
17
|
+
quality?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Identify a buffer by its magic bytes. Returns `null` when it is neither PNG nor JPEG. */
|
|
20
|
+
export declare function sniffFormat(bytes: Uint8Array): ImageFormat | null;
|
|
21
|
+
/**
|
|
22
|
+
* Decode PNG or JPEG bytes to RGBA, or pass a {@link Raster} straight through.
|
|
23
|
+
*
|
|
24
|
+
* Passing a raster through untouched is what makes it cheap to align a page
|
|
25
|
+
* against several scans: decode once, reuse.
|
|
26
|
+
*/
|
|
27
|
+
export declare function decodeImage(input: ImageInput): Raster;
|
|
28
|
+
/** Encode a raster. PNG by default, because a scan re-encoded as JPEG is a scan with new artefacts. */
|
|
29
|
+
export declare function encodeImage(image: Raster, options?: EncodeOptions): Uint8Array;
|
|
30
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../../../src/lib/image/codec.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGlD;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,CAAA;AAExC,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAG,WAAW,CAAA;IACrB,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAKD,4FAA4F;AAC5F,wBAAgB,WAAW,CAAE,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,CAKlE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAoBtD;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,UAAU,CAgBnF"}
|