@scanmate/align 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Russo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ ![scanmate align](./scanmate-align.svg)
2
+
3
+ # `@scanmate/align`
4
+
5
+ > Coarse-to-fine geometric alignment engine for matching scanned pages back onto reference document templates.
6
+
7
+ `@scanmate/align` resamples photographs or flatbed scans of printed forms onto the exact coordinate canvas of their original digital templates (PDF pages). It solves translation, rotation, scaling, affine shear, and 8-DOF perspective distortions using a multi-hypothesis coarse estimator, Fast Fourier Transform (FFT) phase correlation, oriented FAST + steered BRIEF (ORB) feature matching, and RANSAC geometric model fitting.
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - 🎯 **Coordinate Preservation**: Resamples scanned pages directly onto the original PDF canvas dimensions, ensuring bounding box coordinates $(x, y, w, h)$ remain 1:1 comparable.
14
+ - 🚀 **Multi-Hypothesis Coarse Estimation**: Guesses transformation candidates via frame matching, content bounding-box matching, and projection-histogram deskewing at low resolution (512px).
15
+ - ⚡ **FFT Phase Correlation**: Fine-tunes 2D translational offsets using frequency-domain cross-power spectral density peaks.
16
+ - 🔬 **Steered BRIEF & FAST Corners (ORB)**: Detects corners with intensity centroids for scale/rotation invariance without native OpenCV dependencies.
17
+ - 🛡️ **RANSAC Model Fitting**: Filters up to 60%+ false matches (e.g. repeated table cells, identical letterforms) using RANdom SAmple Consensus.
18
+ - 📐 **Multiple Transformation Models**: Supports `similarity` (4-DOF), `affine` (6-DOF), and `homography` (8-DOF perspective transform).
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Using npm
26
+ npm install @scanmate/align @scanmate/ink
27
+
28
+ # Using pnpm
29
+ pnpm add @scanmate/align @scanmate/ink
30
+
31
+ # Using yarn
32
+ yarn add @scanmate/align @scanmate/ink
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ```ts
40
+ import { alignScan } from '@scanmate/align'
41
+ import { decodeImage } from '@scanmate/ink'
42
+ import { readFile } from 'node:fs/promises'
43
+
44
+ const original = await decodeImage(await readFile('template_p1.png'))
45
+ const scanned = await decodeImage(await readFile('returned_photo.jpg'))
46
+
47
+ const result = await alignScan(original, scanned, {
48
+ model: 'similarity',
49
+ workingSize: 1400,
50
+ })
51
+
52
+ console.log(`Confidence: ${(result.confidence * 100).toFixed(1)}%`)
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Architecture & Algorithm Deep-Dive
58
+
59
+ ### 1. Multi-Stage Coarse-to-Fine Pipeline
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ A["Original Template & Scanned Image"] --> B["Decode to Rasters & Ink Normalization"]
64
+ B --> C["Coarse Estimator (512px downsample)"]
65
+
66
+ subgraph CoarseStrategy["Coarse Estimation Hypotheses"]
67
+ C1["Hypothesis 1: Frame Matching"]
68
+ C2["Hypothesis 2: Content Bounding-Box"]
69
+ C3["Hypothesis 3: Projection Histogram Deskew"]
70
+ end
71
+
72
+ C --> CoarseStrategy
73
+ CoarseStrategy --> D["Score Candidates via Ink Correlation"]
74
+ D --> E["Best Coarse Transform H_coarse"]
75
+ E --> F["2D FFT Phase Correlation<br/>Fine translation shift (dx, dy)"]
76
+ F --> G["Coarse-Warped Scan"]
77
+ G --> H["ORB Feature Detection (1400px)<br/>FAST Corners + Steered BRIEF"]
78
+ H --> I["Hamming Distance Matching & Mutual Filter"]
79
+ I --> J["RANSAC Iterative Inlier Estimation"]
80
+ J --> K{"Inlier count >= minInliers?"}
81
+ K -- Yes --> L["Fit Final Homography H_fine<br/>Compose: H_total = H_coarse * H_fine"]
82
+ K -- No --> M["Fallback to H_coarse (method: 'coarse')"]
83
+ L --> N["Inverse Mapping Resample onto Original Canvas"]
84
+ M --> N
85
+ N --> O["Aligned Output Raster & Confidence Diagnostics"]
86
+ ```
87
+
88
+ ---
89
+
90
+ ### 2. Feature Matching & RANSAC Alignment Sequence
91
+
92
+ ```mermaid
93
+ sequenceDiagram
94
+ autonumber
95
+ participant App as Application
96
+ participant Align as alignScan()
97
+ participant ORB as Feature Matching (ORB)
98
+ participant RANSAC as RANSAC Solver
99
+ participant Kernel as @scanmate/ink Warp
100
+
101
+ App->>Align: Execute alignScan(original, scanned)
102
+ Align->>Align: Run Coarse Estimation & FFT Phase Correlation
103
+ Align->>ORB: detectAndDescribe(originalInk) & detectAndDescribe(scannedInk)
104
+ ORB->>ORB: Compute FAST corners & 256-bit steered BRIEF descriptors
105
+ Align->>ORB: matchFeatures(setA, setB)
106
+ ORB->>ORB: Calculate Hamming Bit Distances & Mutual Nearest Neighbor
107
+ ORB-->>Align: Return candidate PointMatch correspondences
108
+ Align->>RANSAC: ransac(matches, { model, threshold })
109
+ loop Random Sample Consensuses (N iterations)
110
+ RANSAC->>RANSAC: Sample minimal point set (e.g. 2 for Similarity, 4 for Homography)
111
+ RANSAC->>RANSAC: Solve model parameter matrix H_candidate via SVD
112
+ RANSAC->>RANSAC: Count inliers where distance(H * p_src, p_dst) < threshold
113
+ end
114
+ RANSAC->>RANSAC: Refit optimal model H_final on all consensus inliers
115
+ RANSAC-->>Align: Return RansacResult (inliers, matrix)
116
+ Align->>Kernel: warpRaster(scanned, outputCanvas, H_total_inv)
117
+ Kernel-->>Align: Aligned Raster
118
+ Align-->>App: Return AlignResult
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Comprehensive API Reference
124
+
125
+ ### 1. Primary Alignment Functions
126
+
127
+ #### `alignScan(original: ImageInput | Raster, scanned: ImageInput | Raster, options?: AlignOptions): Promise<AlignResult>`
128
+ Main entry point to align a single scanned image back onto the original template canvas.
129
+ - **Parameters**:
130
+ - `original`: Original template image (file `Buffer`, `Uint8Array`, or decoded `Raster`).
131
+ - `scanned`: Scanned page image (file `Buffer`, `Uint8Array`, or decoded `Raster`).
132
+ - `options` *(optional)*: `AlignOptions` object (see detailed breakdown below).
133
+ - **Returns**: `Promise<AlignResult>` containing resampled `raster`, transformation matrices, transform summary, `confidence` score (0.0 to 1.0), `method` (`'features'` | `'coarse'`), and diagnostics.
134
+
135
+ ##### Detailed Options Explanation (`AlignOptions`):
136
+
137
+ | Option | Type | Default | Description & Impact |
138
+ |---|---|---|---|
139
+ | `model` | `'similarity' \| 'affine' \| 'homography'` | `'similarity'` | Mathematical transformation model to fit. `similarity` (4-DOF) solves scale, rotation, translation; `affine` (6-DOF) adds axis shear; `homography` (8-DOF) solves perspective tilt. |
140
+ | `workingSize` | `number` | `1400` | Maximum dimension (width/height in px) to downscale images for feature detection. Higher values increase accuracy for faint text but increase execution time quadratically. |
141
+ | `coarseSize` | `number` | `512` | Maximum dimension for the fast initial coarse estimation sweep. |
142
+ | `maxFeatures` | `number` | `1200` | Maximum budget of ORB keypoints to detect per image. |
143
+ | `ransacThreshold` | `number` | `3.0` | Maximum reprojection distance in working-resolution pixels to consider a feature match an inlier during RANSAC. |
144
+ | `minInliers` | `number` | `12` | Minimum required RANSAC inlier matches. If inlier count is below this, feature stage is rejected and method falls back to `'coarse'`. |
145
+ | `maxSkewDeg` | `number` | `12.0` | Maximum page tilt angle considered during projection histogram deskewing. |
146
+ | `interpolation` | `'bilinear' \| 'bicubic' \| 'nearest'` | `'bilinear'` | Sub-pixel interpolation kernel used when inverse warping the scan onto the output canvas. |
147
+ | `background` | `Rgba` (`[r,g,b,a]`) | `[255,255,255,255]` | RGBA background fill color for canvas areas not covered by the warped scan. |
148
+ | `output` | `'png' \| 'jpeg' \| 'none'` | `'png'` | Format for encoded output bytes in `result.image`. Set to `'none'` if consuming `result.raster` directly to save PNG encoding overhead. |
149
+ | `seed` | `number` | `42` | PRNG seed for RANSAC sampling and steered BRIEF patterns, ensuring deterministic output across runs. |
150
+
151
+ ---
152
+
153
+ #### `alignPages(pages: PagePairing[], options?: AlignPagesOptions): Promise<AlignedPage[]>`
154
+ Executes multi-page document alignment in parallel or sequence.
155
+ - **Parameters**:
156
+ - `pages`: Array of paired document page objects (`{ pageNumber, original, scanned }`).
157
+ - `options` *(optional)*: Extends `AlignOptions` with `onProgress: (stage: string, progress: number) => void` callback.
158
+ - **Returns**: `Promise<AlignedPage[]>`
159
+
160
+ #### `polishTranslation(original: Raster, aligned: Raster): Matrix3`
161
+ Fine-tunes residual 1-2 pixel translational shifts between original and aligned rasters using phase correlation.
162
+
163
+ ---
164
+
165
+ ### 2. Intermediate Pipeline Building Blocks
166
+
167
+ #### `estimateCoarse(original: Raster, scanned: Raster, options?: CoarseOptions): Promise<CoarseResult>`
168
+ Evaluates triple-hypothesis coarse transformation candidates (`frame`, `content`, `deskew`) at low resolution (`coarseSize`).
169
+ - **`options`**:
170
+ - `coarseSize` *(default: 512)*: Downscaled image dimension.
171
+ - `maxSkewDeg` *(default: 12)*: Max search skew angle.
172
+ - **Returns**: `Promise<CoarseResult>` with best matrix, candidate scores, and downscaled warped gray images.
173
+
174
+ #### `detectAndDescribe(image: GrayImage, options?: FeatureOptions): FeatureSet`
175
+ Detects FAST corners and computes 256-bit steered BRIEF binary descriptors (ORB).
176
+ - **`options`**:
177
+ - `maxFeatures` *(default: 1200)*: Keypoint budget cap.
178
+ - `fastThreshold` *(default: 20)*: FAST corner detector intensity difference threshold.
179
+ - **Returns**: `FeatureSet` containing `keypoints` (position, angle, response) and `descriptors` (`Uint8Array` of size $N \times 32$).
180
+
181
+ #### `matchFeatures(setA: FeatureSet, setB: FeatureSet, options?: MatchOptions): PointMatch[]`
182
+ Matches binary BRIEF descriptors between two feature sets using Hamming distance.
183
+ - **`options`**:
184
+ - `maxDistance` *(default: 64)*: Maximum acceptable Hamming bit error distance (0-256).
185
+ - `crossCheck` *(default: true)*: Enforces mutual nearest-neighbor filter (A must choose B and B must choose A).
186
+ - **Returns**: Array of `PointMatch` (`{ src: Point, dst: Point, distance: number }`).
187
+
188
+ #### `hamming(a: Uint8Array, b: Uint8Array): number` / `popcount(n: number): number`
189
+ High-speed bitwise XOR popcount function for computing 256-bit Hamming distance between descriptors.
190
+
191
+ #### `phaseCorrelate(imageA: GrayImage, imageB: GrayImage): PhaseCorrelationResult`
192
+ Computes 2D FFT cross-power spectrum between two gray images to find global translation vector $(dx, dy)$ and correlation peak height.
193
+
194
+ ---
195
+
196
+ ### 3. Model Fitting & RANSAC Solvers
197
+
198
+ #### `ransac(matches: PointMatch[], options?: RansacOptions): RansacResult`
199
+ Iterative RANSAC solver for filtering false feature matches and fitting geometric transformation parameters.
200
+ - **`options`**:
201
+ - `model` *(default: `'similarity'`)*: `'similarity'`, `'affine'`, or `'homography'`.
202
+ - `threshold` *(default: 3.0)*: Inlier reprojection error threshold in pixels.
203
+ - `maxIterations` *(default: 2000)*: Max consensus iteration trials.
204
+ - `confidence` *(default: 0.99)*: Theoretical probability of finding optimal inlier set.
205
+ - `seed` *(default: 42)*: PRNG seed.
206
+ - **Returns**: `RansacResult` (`{ matrix: Matrix3, inliers: PointMatch[], iterations: number, rmsError: number }`).
207
+
208
+ #### `fitModel(model: TransformModel, points: PointMatch[]): Matrix3`
209
+ Direct non-iterative model fitting solver for specified `model` type on a set of point matches.
210
+
211
+ #### `fitSimilarity(points: PointMatch[]): Matrix3`
212
+ Fits 4-DOF Similarity matrix (Scale, Rotation, Translation) from 2+ point matches using least squares.
213
+
214
+ #### `fitAffine(points: PointMatch[]): Matrix3`
215
+ Fits 6-DOF Affine matrix (Scale X/Y, Rotation, Shear, Translation) from 3+ point matches.
216
+
217
+ #### `fitHomography(points: PointMatch[]): Matrix3`
218
+ Fits 8-DOF Homography matrix ($3 \times 3$ perspective transformation) from 4+ point matches using SVD / eigenvector solver.
219
+
220
+ #### `findInliers(matches: PointMatch[], matrix: Matrix3, threshold: number): PointMatch[]`
221
+ Filters point matches returning only those whose reprojection error under `matrix` is less than `threshold`.
222
+
223
+ #### `minimumSamples(model: TransformModel): number`
224
+ Returns minimum required point correspondences to solve model: `similarity` = 2, `affine` = 3, `homography` = 4.
225
+
226
+ ---
227
+
228
+ ## License
229
+
230
+ MIT © [ScanMate Team](https://github.com/russoedu/scanmate)
@@ -0,0 +1 @@
1
+ export * from "./src/index.js";