@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Raster, Rect } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* Outline what the report says onto the overlay, so the picture and the numbers
|
|
4
|
+
* can be checked against each other at a glance: green for an expected region
|
|
5
|
+
* that was filled in, amber for one that was not, magenta around every change
|
|
6
|
+
* nobody expected, and blue - the overlay's colour for lost ink - around ink
|
|
7
|
+
* that went missing.
|
|
8
|
+
*/
|
|
9
|
+
export type Rgba = readonly [number, number, number, number];
|
|
10
|
+
export declare const IDENTIFIED: Rgba;
|
|
11
|
+
export declare const NOT_IDENTIFIED: Rgba;
|
|
12
|
+
export declare const UNEXPECTED: Rgba;
|
|
13
|
+
export declare const MISSING: Rgba;
|
|
14
|
+
export interface Annotation {
|
|
15
|
+
rect: Rect;
|
|
16
|
+
color: Rgba;
|
|
17
|
+
}
|
|
18
|
+
export declare function annotateOverlay(overlay: Raster, annotations: readonly Annotation[], thickness?: number): void;
|
|
19
|
+
//# sourceMappingURL=annotate-overlay.use-case.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BinaryImage } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* Connected-component labelling: which set pixels of a mask touch, and the box
|
|
4
|
+
* around each group.
|
|
5
|
+
*
|
|
6
|
+
* The overlay says *which pixels* changed; a caller asking "was anything added
|
|
7
|
+
* outside the boxes I expected" needs *regions*, so the pixels have to be grouped.
|
|
8
|
+
* No maintained package does this for a flat mask, and it is small enough that
|
|
9
|
+
* a dependency would cost more than it saves.
|
|
10
|
+
*
|
|
11
|
+
* Classic two-pass labelling with union-find. The first pass gives each set pixel
|
|
12
|
+
* the smallest label among its already-visited neighbours and records that any
|
|
13
|
+
* other neighbouring labels are the same component; the second resolves each
|
|
14
|
+
* label to its root and accumulates the box and pixel count, so a third pass is
|
|
15
|
+
* never needed. Union by size and path halving keep the forest flat.
|
|
16
|
+
*/
|
|
17
|
+
export interface Component {
|
|
18
|
+
/** Bounding box, in pixels, inclusive of every pixel in the component. */
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
/** Set pixels in the component. */
|
|
24
|
+
pixels: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LabelOptions {
|
|
27
|
+
/**
|
|
28
|
+
* `8` (the default) joins diagonal neighbours, so a handwritten stroke at an
|
|
29
|
+
* angle stays one component instead of shattering into a staircase of pieces.
|
|
30
|
+
*/
|
|
31
|
+
connectivity?: 4 | 8;
|
|
32
|
+
}
|
|
33
|
+
export declare function connectedComponents(mask: BinaryImage, options?: LabelOptions): Component[];
|
|
34
|
+
export interface LabelledComponents {
|
|
35
|
+
components: Component[];
|
|
36
|
+
/**
|
|
37
|
+
* Per pixel, one plus the index of its component in `components`; `0` where
|
|
38
|
+
* the mask is clear. What a caller needs to ask which pixels a component owns.
|
|
39
|
+
*/
|
|
40
|
+
labels: Int32Array;
|
|
41
|
+
}
|
|
42
|
+
/** {@link connectedComponents}, keeping the label image the second pass resolves anyway. */
|
|
43
|
+
export declare function labelComponents(mask: BinaryImage, options?: LabelOptions): LabelledComponents;
|
|
44
|
+
//# sourceMappingURL=connected-components.use-case.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AlignedPage } from '@scanmate/ink';
|
|
2
|
+
import type { DiffOptions, ExpectedChange, PageDiff } from './page-diff.contract.js';
|
|
3
|
+
/**
|
|
4
|
+
* What changed on each page, and whether it was supposed to.
|
|
5
|
+
*
|
|
6
|
+
* The overlay is the detection: the original's ink, fattened by a tolerance band,
|
|
7
|
+
* subtracted from the aligned scan, leaves the ink the scan added. What this adds
|
|
8
|
+
* is the reporting. What changed is grouped into changes - labelled, merged
|
|
9
|
+
* across small gaps, filtered below a physical size. A change whose ink lies
|
|
10
|
+
* mostly in an expected region belongs to that region; every other change is
|
|
11
|
+
* unexpected. Each region is also analysed on its own - its new ink grouped,
|
|
12
|
+
* its printed rules discarded, the rest measured - and is identified once that
|
|
13
|
+
* ink adds up to `minFillArea` without covering more than `maxFill` of it. The
|
|
14
|
+
* same grouping is done for ink the scan lost.
|
|
15
|
+
*
|
|
16
|
+
* Masks are built once per page and read four ways: the overlay, the expected
|
|
17
|
+
* regions, the added changes and the missing ones.
|
|
18
|
+
*/
|
|
19
|
+
export declare function diffPages(pages: readonly AlignedPage[], expected?: readonly ExpectedChange[], options?: DiffOptions): Promise<PageDiff[]>;
|
|
20
|
+
/** One page. `expected` should already be the regions for this page. */
|
|
21
|
+
export declare function diffPage(page: AlignedPage, expected?: readonly ExpectedChange[], options?: DiffOptions): Promise<PageDiff>;
|
|
22
|
+
//# sourceMappingURL=diff-pages.use-case.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** What changed on a page and whether it was supposed to: changed pixels grouped into reportable regions. */
|
|
2
|
+
export { annotateOverlay, IDENTIFIED, MISSING, NOT_IDENTIFIED, UNEXPECTED } from './annotate-overlay.use-case.js';
|
|
3
|
+
export type { Annotation, Rgba } from './annotate-overlay.use-case.js';
|
|
4
|
+
export { connectedComponents, labelComponents } from './connected-components.use-case.js';
|
|
5
|
+
export type { Component, LabelledComponents, LabelOptions } from './connected-components.use-case.js';
|
|
6
|
+
export { diffPage, diffPages } from './diff-pages.use-case.js';
|
|
7
|
+
export { mergeBoxes } from './merge-boxes.use-case.js';
|
|
8
|
+
export type { MergedBox } from './merge-boxes.use-case.js';
|
|
9
|
+
export type { Change, CoordinateUnits, DiffOptions, ExpectedChange, ExpectedResult, PageDiff, RegionInkMetrics } from './page-diff.contract.js';
|
|
10
|
+
export { measureRegionInk } from './region-ink.use-case.js';
|
|
11
|
+
export type { RegionInk, RegionInkOptions } from './region-ink.use-case.js';
|
|
12
|
+
export { composeSideBySide } from './side-by-side.use-case.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Component } from './connected-components.use-case.js';
|
|
2
|
+
/**
|
|
3
|
+
* Join components that belong to one change.
|
|
4
|
+
*
|
|
5
|
+
* A signature is not one connected component: the pen lifts between letters,
|
|
6
|
+
* the dot of an i floats above its stem, a tick's two strokes may not quite meet.
|
|
7
|
+
* Reported raw, one signature would be forty boxes. So components whose boxes
|
|
8
|
+
* come within `gap` pixels of each other are merged, and merged boxes that now
|
|
9
|
+
* reach a neighbour merge again, until nothing moves.
|
|
10
|
+
*
|
|
11
|
+
* Naive pairwise merging is quadratic, and a badly aligned page can produce
|
|
12
|
+
* thousands of fragments. Boxes are bucketed on a grid of cells at least as
|
|
13
|
+
* large as the gap, so each box is only compared with the few that share or
|
|
14
|
+
* border its cells.
|
|
15
|
+
*/
|
|
16
|
+
export interface MergedBox {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
/** Changed pixels inside the merged box. */
|
|
22
|
+
pixels: number;
|
|
23
|
+
/** How many components it was built from. */
|
|
24
|
+
components: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function mergeBoxes(components: readonly Component[], gap: number): MergedBox[];
|
|
27
|
+
//# sourceMappingURL=merge-boxes.use-case.d.ts.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { ImageFormat, InkOptions, ProgressCallback, Raster, Rect } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* Coordinates for regions going in and changes coming out.
|
|
4
|
+
*
|
|
5
|
+
* `points` (the default) are PDF points, 1/72 inch, from the page's top-left
|
|
6
|
+
* corner - the units a document generator already knows its fields in, and the
|
|
7
|
+
* only ones that stay put when `@scanmate/extract` renders each document at its
|
|
8
|
+
* scan's resolution. `pixels` are the original's rendered pixels, for callers
|
|
9
|
+
* working from images rather than PDFs.
|
|
10
|
+
*/
|
|
11
|
+
export type CoordinateUnits = 'points' | 'pixels';
|
|
12
|
+
/** A place on a page where a change is expected - a signature box, a tick box. */
|
|
13
|
+
export interface ExpectedChange {
|
|
14
|
+
/** One-based page number in the original. */
|
|
15
|
+
page: number;
|
|
16
|
+
id: string;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
export interface DiffOptions {
|
|
23
|
+
/** Units of `ExpectedChange` rectangles and of every rectangle reported back. Default `'points'`. */
|
|
24
|
+
units?: CoordinateUnits;
|
|
25
|
+
/** Pixels the original's ink is fattened by before diffing, to absorb sub-pixel misalignment. Default `2`. */
|
|
26
|
+
tolerance?: number;
|
|
27
|
+
/**
|
|
28
|
+
* New ink an expected region needs to count as identified, in square
|
|
29
|
+
* millimetres. Default `2`.
|
|
30
|
+
*
|
|
31
|
+
* Counted from the region's changes - each already past `minChangeArea`, so
|
|
32
|
+
* scattered speckle never adds up to a signature - and in physical units, not
|
|
33
|
+
* as a share of the region: a pen signature is the same few tens of mm2
|
|
34
|
+
* whether its box is a stamp or the width of the page, and the same mark is
|
|
35
|
+
* four times the pixels at twice the dpi. A tick is about 5 mm2, initials a
|
|
36
|
+
* little more; after merging, the largest noise measured on real scans was
|
|
37
|
+
* 0.85 mm2.
|
|
38
|
+
*/
|
|
39
|
+
minFillArea?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Share of an expected region that may be new ink before it stops counting
|
|
42
|
+
* as filled in. Default `0.5`.
|
|
43
|
+
*
|
|
44
|
+
* A signature or a name leaves most of its box empty; a box more than half
|
|
45
|
+
* covered was scribbled out, blacked over or hit by a stamp. It is not
|
|
46
|
+
* identified, and its result says `overfilled`.
|
|
47
|
+
*/
|
|
48
|
+
maxFill?: number;
|
|
49
|
+
/**
|
|
50
|
+
* A piece of new ink inside an expected region that spans at least this share
|
|
51
|
+
* of the region's width or height, and is thin, is a form rule, not writing.
|
|
52
|
+
* Default `0.9`.
|
|
53
|
+
*
|
|
54
|
+
* Field boxes are often drawn with printed borders, and where the alignment
|
|
55
|
+
* leaves a border a pixel or two out of place, a sliver of it outlives the
|
|
56
|
+
* tolerance band. Such slivers are discarded and counted in `ink.formLines`.
|
|
57
|
+
*/
|
|
58
|
+
formLineSpan?: number;
|
|
59
|
+
/**
|
|
60
|
+
* How thin, in millimetres, a spanning piece must be to be a form rule - or
|
|
61
|
+
* 4% of the region's other side, if that is more. Default `0.6`: a printed
|
|
62
|
+
* rule is a few tenths of a millimetre, a pen stroke rarely runs the length of
|
|
63
|
+
* its box.
|
|
64
|
+
*/
|
|
65
|
+
formLineThickness?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Smallest change worth reporting, in square millimetres of ink. Default `1`.
|
|
68
|
+
*
|
|
69
|
+
* Measured on real scans: after merging, the largest noise specks came to
|
|
70
|
+
* 0.31-0.85 mm2, while a tick in a 6 mm box is about 5 mm2 and a signature
|
|
71
|
+
* tens of mm2. Physical units, because the render resolution varies with the
|
|
72
|
+
* scan and the same speck is four times the pixels at twice the dpi.
|
|
73
|
+
*/
|
|
74
|
+
minChangeArea?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Smallest loss of original ink worth reporting, in square millimetres.
|
|
77
|
+
* Default `4`.
|
|
78
|
+
*
|
|
79
|
+
* Higher than `minChangeArea` on purpose. The poorest of three real scans
|
|
80
|
+
* softened the ends of solid header bars into strips of 1-8 mm2 of apparent
|
|
81
|
+
* loss on most pages, while an erased word, a line or a paragraph measured
|
|
82
|
+
* 8-22 mm2 and more. Losses too small to clear this - a deleted three-letter
|
|
83
|
+
* word is about 1 mm2 - are textual, and are what `@scanmate/ocr` compares
|
|
84
|
+
* text for.
|
|
85
|
+
*/
|
|
86
|
+
minMissingArea?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Scan ink fainter than this fraction of the scan's own ink threshold counts
|
|
89
|
+
* as gone. Scanners wash colour out - a red link comes back pink - and faded
|
|
90
|
+
* is not missing. Default `0.25`.
|
|
91
|
+
*/
|
|
92
|
+
faintInk?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Changes within this many millimetres of each other are one change - the
|
|
95
|
+
* strokes and dots of a signature, the two arms of a tick. Default `3`.
|
|
96
|
+
*/
|
|
97
|
+
mergeGap?: number;
|
|
98
|
+
/** Resolution assumed for a page that does not say what it was rendered at. Default `150`. */
|
|
99
|
+
assumeDpi?: number;
|
|
100
|
+
/** Fraction of a change's ink inside an expected region for the change to count as expected. Default `0.5`. */
|
|
101
|
+
regionOverlap?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Most changes reported per page, largest first. A badly aligned page turns
|
|
104
|
+
* every stroke into a change; past this it is reported as `truncated`
|
|
105
|
+
* instead of as a thousand boxes. Default `50`.
|
|
106
|
+
*/
|
|
107
|
+
maxChanges?: number;
|
|
108
|
+
/** Encoding of `diffImage`. `'none'` skips it. Default `'png'`. */
|
|
109
|
+
output?: ImageFormat | 'none';
|
|
110
|
+
/** Draw each expected region and each unexpected change onto the overlay. Default `false`. */
|
|
111
|
+
annotate?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Also compose the original and the aligned scan side by side, with every
|
|
114
|
+
* expected region, unexpected change and missing ink boxed on both halves -
|
|
115
|
+
* the image a reviewer looks at. Encoded like the overlay. Default `false`:
|
|
116
|
+
* it is twice a page of pixels.
|
|
117
|
+
*/
|
|
118
|
+
sideBySide?: boolean;
|
|
119
|
+
ink?: InkOptions;
|
|
120
|
+
onProgress?: ProgressCallback;
|
|
121
|
+
}
|
|
122
|
+
/** What happened in one expected region. */
|
|
123
|
+
export interface ExpectedResult {
|
|
124
|
+
id: string;
|
|
125
|
+
/** The region gained enough new ink to count as filled in. */
|
|
126
|
+
identified: boolean;
|
|
127
|
+
/** The region as given, in the requested units. */
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
width: number;
|
|
131
|
+
height: number;
|
|
132
|
+
/** New ink in the region's changes, in square millimetres. */
|
|
133
|
+
addedInk: number;
|
|
134
|
+
/** Original ink the region lost, in square millimetres. */
|
|
135
|
+
removedInk: number;
|
|
136
|
+
/** `addedInk` as a share of `minFillArea`, clamped to `[0, 1]`. */
|
|
137
|
+
score: number;
|
|
138
|
+
/** More of the region is new ink than `maxFill` allows: blacked out or scribbled over, not filled in. */
|
|
139
|
+
overfilled: boolean;
|
|
140
|
+
/** The shape of the region's new ink. */
|
|
141
|
+
ink: RegionInkMetrics;
|
|
142
|
+
}
|
|
143
|
+
/** How the new ink in an expected region is laid out. */
|
|
144
|
+
export interface RegionInkMetrics {
|
|
145
|
+
/** Separate changes, after grouping the pieces of each - one for a signature, two for a name and a date. */
|
|
146
|
+
changes: number;
|
|
147
|
+
/** Ink in the largest change, in square millimetres. */
|
|
148
|
+
largestArea: number;
|
|
149
|
+
/** Box around all of it, in the requested units; `null` when there is none. */
|
|
150
|
+
bounds: Rect | null;
|
|
151
|
+
/** `bounds` as a share of the region's width and height. */
|
|
152
|
+
widthRatio: number;
|
|
153
|
+
heightRatio: number;
|
|
154
|
+
/**
|
|
155
|
+
* Share of the ink lying along the region's border (a band 3% of its shorter
|
|
156
|
+
* side). Writing sits inside its box; ink that is mostly border-band came in
|
|
157
|
+
* from outside or is the box's own frame.
|
|
158
|
+
*/
|
|
159
|
+
edgeTouch: number;
|
|
160
|
+
/** Share of the region's area that is new ink. */
|
|
161
|
+
fill: number;
|
|
162
|
+
/** Form-rule slivers discarded before measuring. */
|
|
163
|
+
formLines: number;
|
|
164
|
+
}
|
|
165
|
+
/** A change found where nothing was expected, or ink that went missing. */
|
|
166
|
+
export interface Change {
|
|
167
|
+
/** Bounding box, in the requested units. */
|
|
168
|
+
x: number;
|
|
169
|
+
y: number;
|
|
170
|
+
width: number;
|
|
171
|
+
height: number;
|
|
172
|
+
/** Changed ink, in square millimetres. */
|
|
173
|
+
inkArea: number;
|
|
174
|
+
/** Changed pixels. */
|
|
175
|
+
pixels: number;
|
|
176
|
+
}
|
|
177
|
+
export interface PageDiff {
|
|
178
|
+
page: number;
|
|
179
|
+
/** The overlay: red added, blue lost, grey agreed - annotated when asked. */
|
|
180
|
+
diffRaster: Raster;
|
|
181
|
+
diffImage: Uint8Array | null;
|
|
182
|
+
/** Original and aligned scan side by side with the report boxed on both, when `sideBySide` was asked for. */
|
|
183
|
+
sideBySideRaster: Raster | null;
|
|
184
|
+
sideBySideImage: Uint8Array | null;
|
|
185
|
+
expected: ExpectedResult[];
|
|
186
|
+
/** New ink outside every expected region, merged into one box per change. */
|
|
187
|
+
unexpected: Change[];
|
|
188
|
+
/**
|
|
189
|
+
* Original ink the scan lost. A dropped line is as suspicious as an added one.
|
|
190
|
+
* A scan blurry enough to wash out a hairline rule reports that rule here too:
|
|
191
|
+
* it is gone from the image, whatever happened to the paper.
|
|
192
|
+
*/
|
|
193
|
+
missing: Change[];
|
|
194
|
+
/** More changes than `maxChanges` were found - usually a sign the alignment failed. */
|
|
195
|
+
truncated: boolean;
|
|
196
|
+
summary: {
|
|
197
|
+
/** Page-wide fraction of new ink. */
|
|
198
|
+
addedInk: number;
|
|
199
|
+
removedInk: number;
|
|
200
|
+
identified: number;
|
|
201
|
+
notIdentified: number;
|
|
202
|
+
unexpected: number;
|
|
203
|
+
missing: number;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=page-diff.contract.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BinaryImage, Rect } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* The shape of the new ink inside one expected region.
|
|
4
|
+
*
|
|
5
|
+
* "Was ink added here" is one number; whether that ink is a filled-in field is
|
|
6
|
+
* a question about its shape. A signature is a few strokes somewhere inside the
|
|
7
|
+
* box. A field someone blacked out is ink across most of it. A sliver along one
|
|
8
|
+
* border, spanning the whole box and a hair thick, is the field's own printed
|
|
9
|
+
* rule that the alignment left a pixel or two out of place - no one wrote it.
|
|
10
|
+
* So the region is analysed on its own: its new ink labelled, form-rule slivers
|
|
11
|
+
* discarded, the rest grouped the way page-level changes are, and measured.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is in pixels of the region; the caller converts.
|
|
14
|
+
*/
|
|
15
|
+
export interface RegionInkOptions {
|
|
16
|
+
/** Pixels within which pieces of ink are one change. */
|
|
17
|
+
mergeGap: number;
|
|
18
|
+
/** Smallest change, in pixels, that counts. */
|
|
19
|
+
minChangePixels: number;
|
|
20
|
+
/** A piece spanning at least this share of the region's width (or height) may be a form rule. */
|
|
21
|
+
lineSpan: number;
|
|
22
|
+
/** ...if it is also no thicker than this many pixels, */
|
|
23
|
+
lineThickness: number;
|
|
24
|
+
/** ...or than this share of the region's other side, whichever is more. */
|
|
25
|
+
lineThicknessRatio: number;
|
|
26
|
+
/** Width of the border band, as a share of the region's shorter side (at least one pixel). */
|
|
27
|
+
edgeBand: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RegionInk {
|
|
30
|
+
/** New-ink pixels in the changes that count. */
|
|
31
|
+
pixels: number;
|
|
32
|
+
/** Changes that count, after grouping. */
|
|
33
|
+
changes: number;
|
|
34
|
+
/** Pixels in the largest one. */
|
|
35
|
+
largest: number;
|
|
36
|
+
/** Box around every change that counts, in page pixels; `null` when there is none. */
|
|
37
|
+
bounds: Rect | null;
|
|
38
|
+
/** Share of those pixels that lie in the band along the region's border. */
|
|
39
|
+
edgeTouch: number;
|
|
40
|
+
/** Share of the region's area that is counted new ink. */
|
|
41
|
+
fill: number;
|
|
42
|
+
/** Form-rule slivers discarded. */
|
|
43
|
+
formLines: number;
|
|
44
|
+
}
|
|
45
|
+
/** Analyse `added` (the page's new-ink mask) inside `rect` (page pixels). */
|
|
46
|
+
export declare function measureRegionInk(added: BinaryImage, rect: Rect, options: RegionInkOptions): RegionInk;
|
|
47
|
+
//# sourceMappingURL=region-ink.use-case.d.ts.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Raster } from '@scanmate/ink';
|
|
2
|
+
import type { Annotation } from './annotate-overlay.use-case.js';
|
|
3
|
+
export declare function composeSideBySide(original: Raster, aligned: Raster, annotations: readonly Annotation[], thickness: number, gutter: number): Raster;
|
|
4
|
+
//# sourceMappingURL=side-by-side.use-case.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@scanmate/diff` - what changed between an original and its aligned scan, where,
|
|
3
|
+
* and whether it was supposed to.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const diffs = await diffPages(alignedPages, [
|
|
7
|
+
* { page: 1, id: 'signature', x: 76, y: 700, width: 300, height: 60 },
|
|
8
|
+
* ])
|
|
9
|
+
* diffs[0].expected // [{ id: 'signature', identified: true, ... }]
|
|
10
|
+
* diffs[0].unexpected // changes outside every expected region, one box each
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Every function here assumes both images already share a canvas - which is what
|
|
14
|
+
* `@scanmate/align` produces. Feed it a raw scan and every rectangle names a
|
|
15
|
+
* different part of the page in each image.
|
|
16
|
+
*/
|
|
17
|
+
export { diffPage, diffPages } from './change-detection/index.js';
|
|
18
|
+
export type { Change, CoordinateUnits, DiffOptions, ExpectedChange, ExpectedResult, PageDiff, RegionInkMetrics } from './change-detection/index.js';
|
|
19
|
+
export { compareRegions, diffDocument, renderDiff } from './region-comparison/index.js';
|
|
20
|
+
export type { DocumentDiff, Region, RegionOptions, RegionReport } from './region-comparison/index.js';
|
|
21
|
+
export { annotateOverlay, composeSideBySide, connectedComponents, IDENTIFIED, labelComponents, measureRegionInk, mergeBoxes, MISSING, NOT_IDENTIFIED, UNEXPECTED } from './change-detection/index.js';
|
|
22
|
+
export type { Annotation, Component, LabelledComponents, LabelOptions, MergedBox, RegionInk, RegionInkOptions, Rgba } from './change-detection/index.js';
|
|
23
|
+
export { buildMasks, measureRegion, paintOverlay } from './region-comparison/index.js';
|
|
24
|
+
export type { Masks } from './region-comparison/index.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ImageInput } from '@scanmate/ink';
|
|
2
|
+
import type { Masks } from './ink-masks.use-case.js';
|
|
3
|
+
import type { DocumentDiff, Region, RegionOptions, RegionReport } from './region.model.js';
|
|
4
|
+
/**
|
|
5
|
+
* What changed, and where.
|
|
6
|
+
*
|
|
7
|
+
* Once the scan sits on the original's canvas, "was this box signed?" stops
|
|
8
|
+
* being an image problem and becomes arithmetic: count the ink inside the
|
|
9
|
+
* rectangle that is present in the scan and absent from the original.
|
|
10
|
+
*
|
|
11
|
+
* The one subtlety is the tolerance band. Alignment is good to a pixel or so,
|
|
12
|
+
* never to zero, and printed text is mostly edges — so a half-pixel shift
|
|
13
|
+
* lights up the outline of every character as "new ink". Dilating the
|
|
14
|
+
* original's mask first (fattening every stroke by a couple of pixels) absorbs
|
|
15
|
+
* that, the way a proofreader ignores a letter sitting a hair off the baseline.
|
|
16
|
+
* What it cannot absorb is a signature, which is ink in places the original has
|
|
17
|
+
* none.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Compare an aligned scan against its original over a set of known rectangles.
|
|
21
|
+
*
|
|
22
|
+
* `aligned` must be the output of `alignScan` - or anything else already on the
|
|
23
|
+
* original's canvas. Feeding a raw scan in produces confident nonsense, because
|
|
24
|
+
* every rectangle then names a different part of the page in each image.
|
|
25
|
+
*/
|
|
26
|
+
export declare function compareRegions(original: ImageInput, aligned: ImageInput, regions: readonly Region[], options?: RegionOptions): Promise<RegionReport[]>;
|
|
27
|
+
/** Page-wide added/removed ink, plus per-region detail for any regions supplied. */
|
|
28
|
+
export declare function diffDocument(original: ImageInput, aligned: ImageInput, regions?: readonly Region[], options?: RegionOptions): Promise<DocumentDiff>;
|
|
29
|
+
/** One region's added and removed ink, from masks already built. */
|
|
30
|
+
export declare function measureRegion(region: Region, masks: Masks, defaultThreshold: number): RegionReport;
|
|
31
|
+
//# sourceMappingURL=compare-regions.use-case.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Which known rectangles gained ink, what changed page-wide, and an overlay to look at. */
|
|
2
|
+
export { compareRegions, diffDocument, measureRegion } from './compare-regions.use-case.js';
|
|
3
|
+
/** Consumed by change-detection, which builds the masks once per page and reads them several ways. */
|
|
4
|
+
export { buildMasks } from './ink-masks.use-case.js';
|
|
5
|
+
export type { Masks } from './ink-masks.use-case.js';
|
|
6
|
+
export type { DocumentDiff, Region, RegionOptions, RegionReport } from './region.model.js';
|
|
7
|
+
export { paintOverlay, renderDiff } from './render-diff.use-case.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BinaryImage, ImageInput, InkOptions } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* The ink masks every comparison starts from, plus their tolerance bands.
|
|
4
|
+
*
|
|
5
|
+
* Both images must already share a canvas - that is what alignment is for - so
|
|
6
|
+
* a size mismatch is refused outright rather than compared into nonsense.
|
|
7
|
+
*
|
|
8
|
+
* ## Why "lost" uses a lower bar than "added"
|
|
9
|
+
*
|
|
10
|
+
* Deciding that the scan *added* ink and deciding that it *lost* ink are not
|
|
11
|
+
* symmetric, because scanning is not. A scanner washes colour out: on real
|
|
12
|
+
* scans a crisp red hyperlink came back pink and a purple header bar came back
|
|
13
|
+
* lavender, both light enough to fall below the scan's own ink threshold -
|
|
14
|
+
* which, judged by the same bar as added ink, reported them as missing on every
|
|
15
|
+
* page. Nothing had gone; it had faded. So the scan's ink is read twice: at its
|
|
16
|
+
* normal threshold for what it added, and at `faintInk` times that threshold for
|
|
17
|
+
* what is still there at all. Original ink counts as lost only where the scan
|
|
18
|
+
* shows none even at the lower bar. An erased word leaves bare paper, which
|
|
19
|
+
* fails both.
|
|
20
|
+
*/
|
|
21
|
+
export interface Masks {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
original: BinaryImage;
|
|
25
|
+
/** The scan's ink at its normal threshold: what counts as *added*. */
|
|
26
|
+
scan: BinaryImage;
|
|
27
|
+
/** The scan's ink at the faint threshold: anything still there at all. */
|
|
28
|
+
scanFaint: BinaryImage;
|
|
29
|
+
originalDilated: BinaryImage;
|
|
30
|
+
/** `scanFaint`, fattened by the tolerance: original ink outside it is *lost*. */
|
|
31
|
+
scanDilated: BinaryImage;
|
|
32
|
+
}
|
|
33
|
+
/** Default for {@link buildMasks}' `faintInk`, measured against real scans. */
|
|
34
|
+
export declare const FAINT_INK = 0.25;
|
|
35
|
+
export declare function buildMasks(original: ImageInput, aligned: ImageInput, ink: InkOptions | undefined, tolerance: number, faintInk?: number): Promise<Masks>;
|
|
36
|
+
//# sourceMappingURL=ink-masks.use-case.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InkOptions, Rect } from '@scanmate/ink';
|
|
2
|
+
export interface Region {
|
|
3
|
+
id: string;
|
|
4
|
+
/** In the *original's* pixel coordinates - the whole point of aligning first. */
|
|
5
|
+
rect: Rect;
|
|
6
|
+
/** Fraction of the region that must be new ink before `filled` is true. Overrides the global default. */
|
|
7
|
+
threshold?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RegionOptions {
|
|
10
|
+
ink?: InkOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Radius, in pixels, that the original's ink is fattened by before diffing.
|
|
13
|
+
* Raise it if alignment is loose; lower it to catch very fine additions.
|
|
14
|
+
*/
|
|
15
|
+
tolerance?: number;
|
|
16
|
+
/** Default fraction of new ink that counts as filled. */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Scan ink fainter than this fraction of the scan's own ink threshold counts
|
|
20
|
+
* as gone. Anything lighter but still visible has faded, not disappeared -
|
|
21
|
+
* scanners wash colour out. Default `0.25`.
|
|
22
|
+
*/
|
|
23
|
+
faintInk?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RegionReport {
|
|
26
|
+
id: string;
|
|
27
|
+
rect: Rect;
|
|
28
|
+
/** Ink coverage of the region in the original, in `[0, 1]`. */
|
|
29
|
+
originalInk: number;
|
|
30
|
+
/** Ink coverage of the region in the aligned scan. */
|
|
31
|
+
scanInk: number;
|
|
32
|
+
/** Coverage that is ink in the scan and not within `tolerance` of ink in the original. */
|
|
33
|
+
added: number;
|
|
34
|
+
/** Coverage that is ink in the original and missing from the scan. Mostly a faint-scan warning. */
|
|
35
|
+
removed: number;
|
|
36
|
+
filled: boolean;
|
|
37
|
+
/** `added` as a multiple of the threshold, clamped to `[0, 1]`. A reportable confidence. */
|
|
38
|
+
score: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DocumentDiff {
|
|
41
|
+
/** Page-wide version of {@link RegionReport.added}. */
|
|
42
|
+
added: number;
|
|
43
|
+
removed: number;
|
|
44
|
+
regions: RegionReport[];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=region.model.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ImageInput, Raster } from '@scanmate/ink';
|
|
2
|
+
import type { Masks } from './ink-masks.use-case.js';
|
|
3
|
+
import type { RegionOptions } from './region.model.js';
|
|
4
|
+
/**
|
|
5
|
+
* An RGBA overlay of the comparison, for looking at with your own eyes.
|
|
6
|
+
*
|
|
7
|
+
* Red is ink the scan added, blue is ink it lost, grey is ink both agree on.
|
|
8
|
+
* A correctly aligned pair of a signed form is almost entirely grey with a red
|
|
9
|
+
* signature; a misaligned one is red and blue confetti along every stroke,
|
|
10
|
+
* which is the fastest way to tell the two failures apart.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderDiff(original: ImageInput, aligned: ImageInput, options?: RegionOptions): Promise<Raster>;
|
|
13
|
+
/** The overlay, from masks already built. Red added, blue lost, grey agreed, white paper. */
|
|
14
|
+
export declare function paintOverlay(masks: Masks): Raster;
|
|
15
|
+
//# sourceMappingURL=render-diff.use-case.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scanmate/diff",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "What changed between an original and its aligned scan: expected regions filled in, unexpected marks, lost ink, and a side-by-side evidence image.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Eduardo Russo",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/russoedu/scanmate.git",
|
|
10
|
+
"directory": "packages/diff"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/russoedu/scanmate/tree/main/packages/diff#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/russoedu/scanmate/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"scan",
|
|
18
|
+
"diff",
|
|
19
|
+
"signature-detection",
|
|
20
|
+
"document",
|
|
21
|
+
"verification"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.esm.js",
|
|
25
|
+
"module": "./dist/index.esm.js",
|
|
26
|
+
"types": "./dist/src/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/src/index.d.ts",
|
|
31
|
+
"import": "./dist/index.esm.js",
|
|
32
|
+
"default": "./dist/index.esm.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"!**/*.tsbuildinfo",
|
|
38
|
+
"!**/*.d.ts.map",
|
|
39
|
+
"!**/*.js.map"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@scanmate/ink": "^0.0.2"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@scanmate/align": "^0.0.2"
|
|
49
|
+
}
|
|
50
|
+
}
|