@scanmate/diff 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 +21 -0
- package/README.md +183 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +783 -0
- package/dist/src/change-detection/annotate-overlay.use-case.d.ts +19 -0
- package/dist/src/change-detection/connected-components.use-case.d.ts +44 -0
- package/dist/src/change-detection/diff-pages.use-case.d.ts +22 -0
- package/dist/src/change-detection/index.d.ts +13 -0
- package/dist/src/change-detection/merge-boxes.use-case.d.ts +27 -0
- package/dist/src/change-detection/page-diff.contract.d.ts +206 -0
- package/dist/src/change-detection/region-ink.use-case.d.ts +47 -0
- package/dist/src/change-detection/side-by-side.use-case.d.ts +4 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/region-comparison/compare-regions.use-case.d.ts +31 -0
- package/dist/src/region-comparison/index.d.ts +8 -0
- package/dist/src/region-comparison/ink-masks.use-case.d.ts +36 -0
- package/dist/src/region-comparison/region.model.d.ts +46 -0
- package/dist/src/region-comparison/render-diff.use-case.d.ts +15 -0
- package/package.json +50 -0
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,183 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# `@scanmate/diff`
|
|
4
|
+
|
|
5
|
+
> Visual change detection, form field verification, and unexpected modification analysis for aligned document pairs.
|
|
6
|
+
|
|
7
|
+
`@scanmate/diff` analyzes differences between original digital document templates and aligned scanned pages. It verifies whether expected form regions (such as signature blocks, checkboxes, and fillable fields) were completed, measures added/removed ink quantities, isolates unexpected handwritten marks or edits using 2-pass connected components analysis, and renders color-coded visual difference overlays.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- ✍️ **Form Region Verification (`compareRegions`)**: Quantifies added ink inside specific bounding boxes in original canvas coordinates.
|
|
14
|
+
- 🔍 **Sub-Pixel Dilation Tolerance**: Fattens original ink boundaries before subtraction to eliminate false-positive edge noise caused by minor printing/scanning shifts.
|
|
15
|
+
- 🎨 **Color-Coded Visual Overlay (`renderDiff`)**: Produces RGBA difference overlays (Red = added ink / signature, Blue = removed ink, Grey = matching ink).
|
|
16
|
+
- 🧩 **Unexpected Mark Isolation (`diffPage`)**: Uses 8-connectivity Connected Component Analysis (CCL) to group un-matched ink pixels into isolated bounding boxes.
|
|
17
|
+
- 📦 **Automated Box Merging**: Consolidates adjacent connected components to present clean, readable change boxes around handwritten notes or stamps.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Using npm
|
|
25
|
+
npm install @scanmate/diff @scanmate/ink
|
|
26
|
+
|
|
27
|
+
# Using pnpm
|
|
28
|
+
pnpm add @scanmate/diff @scanmate/ink
|
|
29
|
+
|
|
30
|
+
# Using yarn
|
|
31
|
+
yarn add @scanmate/diff @scanmate/ink
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { compareRegions } from '@scanmate/diff'
|
|
40
|
+
import { decodeImage } from '@scanmate/ink'
|
|
41
|
+
|
|
42
|
+
const original = await decodeImage(originalBuffer)
|
|
43
|
+
const aligned = await decodeImage(alignedBuffer)
|
|
44
|
+
|
|
45
|
+
const reports = compareRegions(original, aligned, [
|
|
46
|
+
{ id: 'signature', rect: { x: 100, y: 750, width: 350, height: 80 } },
|
|
47
|
+
])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Architecture & Algorithm Deep-Dive
|
|
53
|
+
|
|
54
|
+
### 1. Dilation Masking & Sub-Pixel Tolerance
|
|
55
|
+
|
|
56
|
+
Even when a scan is perfectly aligned, real-world printing and scanning artifacts (ink bleed, scanner MTF blur, rasterization anti-aliasing) create sub-pixel outline differences along text character edges. Subtracting raw ink maps directly produces false-positive "halos" around every letter on the page.
|
|
57
|
+
|
|
58
|
+
To prevent this, `@scanmate/diff` applies **morphological dilation** with radius $r$ (default 2px) to the original template's ink map $M_{orig}$:
|
|
59
|
+
|
|
60
|
+
$$M_{orig, dilated} = \text{dilate}(M_{orig}, r)$$
|
|
61
|
+
|
|
62
|
+
$$\text{Ink}_{added}(x, y) = \max\left(0, \text{Ink}_{scan}(x, y) - M_{orig, dilated}(x, y)\right)$$
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
flowchart TD
|
|
66
|
+
A["Original Ink Map"] --> B["Morphological Dilation (Radius r = 2px)"]
|
|
67
|
+
B --> C["Dilated Original Mask M_dilated"]
|
|
68
|
+
D["Aligned Scan Ink Map M_scan"] --> E["Ink Subtraction:<br/>Added = max(0, M_scan - M_dilated)"]
|
|
69
|
+
C --> E
|
|
70
|
+
E --> F["Clean Added Ink Map<br/>(Character outline noise suppressed,<br/>Signatures & Checkmarks retained)"]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### 2. Connected Component Analysis & Unexpected Mark Grouping
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
sequenceDiagram
|
|
79
|
+
autonumber
|
|
80
|
+
participant Diff as diffPage()
|
|
81
|
+
participant Mask as Mask Engine
|
|
82
|
+
participant CCL as Connected Components
|
|
83
|
+
participant Merge as Box Merger
|
|
84
|
+
|
|
85
|
+
Diff->>Mask: Compute Added Ink Map & Mask expected regions
|
|
86
|
+
Mask-->>Diff: Un-matched Added Ink Map
|
|
87
|
+
Diff->>CCL: connectedComponents(binaryInkMap)
|
|
88
|
+
CCL->>CCL: Pass 1: Label 8-connected pixel clusters & track equivalences
|
|
89
|
+
CCL->>CCL: Pass 2: Resolve label equivalences & calculate component stats
|
|
90
|
+
CCL-->>Diff: Return raw pixel blob Components
|
|
91
|
+
Diff->>Merge: mergeBoxes(components, { maxGap: 15px })
|
|
92
|
+
Merge->>Merge: Calculate bounding box overlaps & expand by maxGap
|
|
93
|
+
Merge->>Merge: Merge intersecting bounding boxes into unified regions
|
|
94
|
+
Merge-->>Diff: Return MergedBox array
|
|
95
|
+
Diff-->>Diff: Annotate overlay & produce PageDiff report
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Comprehensive API Reference
|
|
101
|
+
|
|
102
|
+
### 1. Region Comparison & Form Verification
|
|
103
|
+
|
|
104
|
+
#### `compareRegions(original: Raster, aligned: Raster, regions: Region[], options?: RegionOptions): RegionReport[]`
|
|
105
|
+
Evaluates specific rectangular form regions to check if signatures, checkboxes, or text boxes were filled in.
|
|
106
|
+
- **Parameters**:
|
|
107
|
+
- `original`: Original template `Raster`.
|
|
108
|
+
- `aligned`: Aligned scan `Raster` (must match `original` canvas width/height).
|
|
109
|
+
- `regions`: Array of `Region` objects (`{ id: string, rect: Rect, threshold?: number }`).
|
|
110
|
+
- `options` *(optional)*: `RegionOptions` object (see breakdown below).
|
|
111
|
+
- **Returns**: Array of `RegionReport` (`{ id, rect, filled, score, added, removed, addedPixels, totalPixels }`).
|
|
112
|
+
|
|
113
|
+
##### Detailed Options Explanation (`RegionOptions`):
|
|
114
|
+
|
|
115
|
+
| Option | Type | Default | Description & Impact |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `tolerance` | `number` | `2` | Morphological dilation radius in pixels applied to original ink before subtraction. Absorbs minor sub-pixel rendering shifts. |
|
|
118
|
+
| `threshold` | `number` | `0.02` | Ink ratio threshold (2% of region area) above which `filled` is set to `true`. |
|
|
119
|
+
| `addedColor` | `Rgba` | `[239, 68, 68, 255]` | RGBA color (Red) for added ink in diff overlays. |
|
|
120
|
+
| `removedColor` | `Rgba` | `[59, 130, 246, 255]` | RGBA color (Blue) for removed ink in diff overlays. |
|
|
121
|
+
| `matchedColor` | `Rgba` | `[156, 163, 175, 255]`| RGBA color (Grey) for matching ink in diff overlays. |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
#### `diffDocument(original: Raster, aligned: Raster, regions?: Region[], options?: RegionOptions): DocumentDiff`
|
|
126
|
+
Computes whole-page added/removed ink statistics plus per-region details in a single efficient pass.
|
|
127
|
+
- **Returns**: `DocumentDiff` (`{ overallAdded, overallRemoved, overallAddedPixels, overallTotalPixels, regions: RegionReport[] }`).
|
|
128
|
+
|
|
129
|
+
#### `renderDiff(original: Raster, aligned: Raster, options?: RegionOptions): Raster`
|
|
130
|
+
Generates a 4-color RGBA overlay `Raster` suitable for visual inspection (Red = scan additions, Blue = template deletions, Grey = matched ink, White = paper background).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### 2. High-Level Page & Document Diffing
|
|
135
|
+
|
|
136
|
+
#### `diffPage(options: DiffOptions): Promise<PageDiff>`
|
|
137
|
+
Full change detection pipeline for a single page, matching expected form regions and isolating unexpected handwritten edits using connected component analysis.
|
|
138
|
+
- **Parameters (`DiffOptions`)**:
|
|
139
|
+
- `page`: Page number index.
|
|
140
|
+
- `original`: Original template `Raster`.
|
|
141
|
+
- `aligned`: Aligned scan `Raster`.
|
|
142
|
+
- `expectedRegions` *(optional)*: Array of expected form field bounding boxes.
|
|
143
|
+
- `minChangePixels` *(default: 20)*: Minimum area in pixels to consider a connected component a valid unexpected change box.
|
|
144
|
+
- `tolerance` *(default: 2)*: Dilation tolerance radius.
|
|
145
|
+
- `addedColor` / `removedColor`: Visual overlay colors.
|
|
146
|
+
- **Returns**: `Promise<PageDiff>` (`{ page, expected: ExpectedResult[], unexpected: UnexpectedChange[], overlay: Raster }`).
|
|
147
|
+
|
|
148
|
+
#### `diffPages(alignedPages: AlignedPage[], expectedRegions: ExpectedRegion[], options?: DiffOptions): Promise<PageDiff[]>`
|
|
149
|
+
Batch page diffing for multi-page document collections.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### 3. Pipeline Building Blocks & Connected Components
|
|
154
|
+
|
|
155
|
+
#### `buildMasks(original: Raster, aligned: Raster, options?: RegionOptions): Masks`
|
|
156
|
+
Computes intermediate Float32 ink maps and binary addition/subtraction masks.
|
|
157
|
+
- **Returns**: `Masks` (`{ originalInk, alignedInk, addedMask, removedMask, width, height }`).
|
|
158
|
+
|
|
159
|
+
#### `measureRegion(masks: Masks, region: Region, options?: RegionOptions): RegionReport`
|
|
160
|
+
Measures ink statistics inside a single `Region` using pre-computed `Masks`.
|
|
161
|
+
|
|
162
|
+
#### `paintOverlay(masks: Masks, options?: RegionOptions): Raster`
|
|
163
|
+
Paints RGBA overlay `Raster` from pre-computed `Masks`.
|
|
164
|
+
|
|
165
|
+
#### `connectedComponents(binary: BinaryImage, options?: ComponentOptions): Component[]`
|
|
166
|
+
Executes 2-pass 8-connectivity Connected Component Analysis (CCL) to extract disjoint pixel blobs.
|
|
167
|
+
- **Options**:
|
|
168
|
+
- `minPixels` *(default: 1)*: Ignore components with pixel count below this limit.
|
|
169
|
+
- **Returns**: Array of `Component` (`{ id, minX, minY, maxX, maxY, pixelCount, width, height }`).
|
|
170
|
+
|
|
171
|
+
#### `mergeBoxes(boxes: MergedBox[], options?: MergeOptions): MergedBox[]`
|
|
172
|
+
Consolidates overlapping or closely adjacent bounding boxes.
|
|
173
|
+
- **Options**:
|
|
174
|
+
- `maxGap` *(default: 15)*: Maximum distance in pixels between box boundaries to trigger a box merge.
|
|
175
|
+
|
|
176
|
+
#### `annotateOverlay(overlay: Raster, annotations: Annotation[], options?: LabelOptions): Raster`
|
|
177
|
+
Draws bounding box rectangles and text labels onto a diff overlay `Raster`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT © [ScanMate Team](https://github.com/russoedu/scanmate)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index.js";
|