@scanmate/ocr 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.esm.js +211 -58
- package/dist/src/index.d.ts +1 -1
- package/dist/src/page-reading/ocr-report.contract.d.ts +8 -2
- package/dist/src/print-verification/index.d.ts +1 -0
- package/dist/src/print-verification/verify-print.use-case.d.ts +42 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ const report = await ocrPages(await alignPages(pages))
|
|
|
26
26
|
report.pages[0].text.differences // [{ kind: 'changed', expected: 'Account 4412-9087-3355',
|
|
27
27
|
// found: 'Account 4412-9987-3355', reason: 'numbers',
|
|
28
28
|
// verified: true, x, y, width, height }]
|
|
29
|
-
report.pages[0].text.printChecks // { checked, different }: figures matched against the original's glyphs
|
|
29
|
+
report.pages[0].text.printChecks // { checked, different, skipped }: figures matched against the original's glyphs
|
|
30
30
|
report.score // document score, weighted by characters
|
|
31
31
|
report.pages[0].text.metrics // levenshtein, jaccard, dice, cosine, CER, WER, word recall, ...
|
|
32
32
|
report.pages[0].text.original.text // the original's text
|
|
@@ -41,6 +41,7 @@ report.pages[0].aligned.raster // the page itself comes back too
|
|
|
41
41
|
- **Matching by place, not order.** Alignment puts the scan on the original's canvas, so each word read is claimed by the run of the original printed where it was read. A two-column page read column by column is therefore not a page of errors, and every difference has a position.
|
|
42
42
|
- **Figures must keep their digits.** A run whose digits read back differently has changed, however similar the rest is: "Total 1,250.00" read as "Total 7,250.00" is 93% similar. A figure whose separators alone differ ("5.768.700 00") is the same figure. Words keep OCR's tolerance (`matchThreshold`, 0.8).
|
|
43
43
|
- **Figures are matched, not only read.** Every printed figure is checked glyph by glyph against the original's own ink and against the other digits the page prints, at the scan's own sharpness. That settles what no reading of a coarse scan can: whether this is still the digit that was printed. On real returned scans it verified 35 of 40 printed figures at 125 dpi with no false calls, and read a digit replaced by another of the same run for what it is.
|
|
44
|
+
- **It says why it abstained.** `printChecks.skipped` counts each reason - `no-figure`, `unplaceable`, `few-rivals`, `too-coarse`, `undecided` - because "checked and agreed" and "never looked at" are otherwise the same silence, and telling them apart by inference is slow and can come out wrong.
|
|
44
45
|
- **It abstains rather than confirm.** A page that prints too few digits to offer a full set of rivals, a run too small to segment, a cell too soft to call: each is left to the reading, and the answer is applied only to the cells it actually decided. Confirming a digit that had in fact been altered would be worse than saying nothing, so every threshold is set to fail that way.
|
|
45
46
|
- **What is not an addition.** Words over something the original prints without text, such as a logo, are not additions. Nor are specks under 4 pt tall.
|
|
46
47
|
|
package/dist/index.esm.js
CHANGED
|
@@ -923,8 +923,38 @@ const MAX_PRINTED_SCORE = 0.85;
|
|
|
923
923
|
const MATCH_HEIGHT = 16;
|
|
924
924
|
/** The template is slid this far, in match pixels, to absorb a cell landing half a pixel out. */
|
|
925
925
|
const SHIFT = 1;
|
|
926
|
-
/**
|
|
927
|
-
|
|
926
|
+
/**
|
|
927
|
+
* The scan is sharpened to meet the print, rather than the print softened to
|
|
928
|
+
* meet the scan, and that direction is the whole of it.
|
|
929
|
+
*
|
|
930
|
+
* Softening the template was the obvious way round and it is wrong: the blur
|
|
931
|
+
* lands on the printed glyph and on all nine rivals alike, so above about one
|
|
932
|
+
* match pixel a `3` and an `8` become the same blob and every rival ties the
|
|
933
|
+
* truth. Measured on a returned order confirmation, the totals on its shaded
|
|
934
|
+
* bar fitted a softening of 1.5 and scored 0.937 against the digit actually
|
|
935
|
+
* printed and 0.928 against the best rival - a margin of 0.006 where 0.12 is
|
|
936
|
+
* needed, so not one of 109 cells could be decided. The same collapse hits
|
|
937
|
+
* ordinary black-on-white text on a 93 dpi scan, which is how we know it is
|
|
938
|
+
* the softening and not the shaded bar.
|
|
939
|
+
*
|
|
940
|
+
* Sharpening the scan puts back what the scanner took out and leaves the
|
|
941
|
+
* template's own detail intact, so what survives is what distinguishes one
|
|
942
|
+
* digit from another. On the same document that took light-on-dark cells from
|
|
943
|
+
* 0 decided to 12, and dark-on-light from 316 to 343, with no false call.
|
|
944
|
+
*
|
|
945
|
+
* `RECOVERY` is how far, in match pixels; `SHARPENING` is how hard.
|
|
946
|
+
*/
|
|
947
|
+
const RECOVERY = [0, 0.5, 1, 1.5, 2];
|
|
948
|
+
/**
|
|
949
|
+
* A verdict has to hold at every one of these, or the cell is left undecided.
|
|
950
|
+
*
|
|
951
|
+
* Not a search for the amount that gives an answer - that is how a sweep turns
|
|
952
|
+
* into a way of manufacturing one. Sharpening can create a stroke that was
|
|
953
|
+
* never scanned, and a fabricated stroke favours whichever rival it happens to
|
|
954
|
+
* resemble, so a reading that moves as the amount moves is an artefact of the
|
|
955
|
+
* sharpening and is discarded.
|
|
956
|
+
*/
|
|
957
|
+
const SHARPENING = [1, 1.5, 2];
|
|
928
958
|
/**
|
|
929
959
|
* Checks the figures of one run against the original's print.
|
|
930
960
|
*
|
|
@@ -934,7 +964,7 @@ const SOFTENING = [0, 0.5, 1, 1.5, 2];
|
|
|
934
964
|
* @param run - The run to check, from the original's text layer.
|
|
935
965
|
* @param templates - Glyphs collected from the original by `collectTemplates`.
|
|
936
966
|
* @param options - Margin, figure length and the characters figures use.
|
|
937
|
-
* @returns What the ink says, or
|
|
967
|
+
* @returns What the ink says, or why it would not say.
|
|
938
968
|
*/
|
|
939
969
|
function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
|
|
940
970
|
const {
|
|
@@ -945,73 +975,127 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
|
|
|
945
975
|
minPrinted = MIN_PRINTED_SCORE,
|
|
946
976
|
minRivals = scope === 'text' ? MIN_TEXT_RIVALS : MIN_RIVALS,
|
|
947
977
|
minDigits = 2,
|
|
948
|
-
characters = scope === '
|
|
978
|
+
characters = scope === 'figures' ? FIGURE_CHARACTERS$1 : TEXT_CHARACTERS,
|
|
979
|
+
claimed
|
|
949
980
|
} = options;
|
|
950
981
|
const printed = [...run.text].filter(character => character.trim() !== '');
|
|
951
|
-
|
|
952
|
-
|
|
982
|
+
// What the reading says it is, lined up with what the original printed. The
|
|
983
|
+
// two must correspond character for character or the comparison is meaningless.
|
|
984
|
+
const proposed = scope === 'confirm' ? [...(claimed ?? '')].filter(character => character.trim() !== '') : null;
|
|
985
|
+
if (proposed !== null && (proposed.length === 0 || proposed.length !== printed.length)) return {
|
|
986
|
+
verified: false,
|
|
987
|
+
because: 'no-claim'
|
|
988
|
+
};
|
|
989
|
+
const wanted = proposed === null ? scope === 'text' ? textCells(printed, characters) : figureCells(printed, minDigits) : disputedCells(printed, proposed, characters);
|
|
990
|
+
if (wanted.size === 0) return {
|
|
991
|
+
verified: false,
|
|
992
|
+
because: proposed === null ? 'no-figure' : 'no-claim'
|
|
993
|
+
};
|
|
953
994
|
// A total on a shaded bar is printed light on dark; turned round, it is a figure like any other.
|
|
954
995
|
const lightOnDark = printPolarity(original, dpi, run) === 'light-on-dark';
|
|
955
996
|
const cells = placeGlyphs(original, dpi, run, run.text, {
|
|
956
997
|
lightOnDark
|
|
957
998
|
});
|
|
958
|
-
if (cells === null) return
|
|
959
|
-
|
|
999
|
+
if (cells === null) return {
|
|
1000
|
+
verified: false,
|
|
1001
|
+
because: 'unplaceable'
|
|
1002
|
+
};
|
|
1003
|
+
/**
|
|
1004
|
+
* Who each cell is up against.
|
|
1005
|
+
*
|
|
1006
|
+
* Identifying an unknown glyph needs most of the alphabet: to assert "this is
|
|
1007
|
+
* a 7" you must be able to rule the others out, and the bar of eight digits
|
|
1008
|
+
* is what the false-call rate justified. A settlement is not asking that. The
|
|
1009
|
+
* reading has already named its answer, so the question is which of two named
|
|
1010
|
+
* characters this ink is - and two templates answer it. A wrong verdict then
|
|
1011
|
+
* requires the ink to match the *other specific character* better, not merely
|
|
1012
|
+
* to be hard to read.
|
|
1013
|
+
*/
|
|
960
1014
|
const rivals = [...characters].filter(character => templates.has(templateKey(run, character)));
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
let
|
|
975
|
-
let checked = 0;
|
|
1015
|
+
const rivalsAt = index => {
|
|
1016
|
+
if (proposed === null) return rivals;
|
|
1017
|
+
const against = proposed[index];
|
|
1018
|
+
return against !== undefined && templates.has(templateKey(run, against)) ? [against] : [];
|
|
1019
|
+
};
|
|
1020
|
+
const enough = proposed === null ? rivals.length >= minRivals : [...wanted].some(index => rivalsAt(index).length > 0);
|
|
1021
|
+
if (!enough) return {
|
|
1022
|
+
verified: false,
|
|
1023
|
+
because: 'few-rivals'
|
|
1024
|
+
};
|
|
1025
|
+
// The cell exactly: a margin would bring in the neighbouring glyphs, which
|
|
1026
|
+
// both crops share, and shared ink correlates whatever the character is.
|
|
1027
|
+
const crops = new Map();
|
|
1028
|
+
let coarse = false;
|
|
976
1029
|
for (const index of wanted) {
|
|
977
|
-
// The cell exactly: a margin would bring in the neighbouring glyphs, which
|
|
978
|
-
// both crops share, and shared ink correlates whatever the character is.
|
|
979
1030
|
const cell = cells[index];
|
|
980
1031
|
if (cell === null) continue;
|
|
981
1032
|
const glyph = cut(scan, dpi, cell, lightOnDark);
|
|
982
1033
|
// The original's own ink here: the same glyph, at the same size, in the same place.
|
|
983
1034
|
const asPrinted = cut(original, dpi, cell, lightOnDark);
|
|
984
1035
|
if (glyph === null || asPrinted === null) continue;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1036
|
+
// Never invent resolution. Matching happens at MATCH_HEIGHT, so a shorter
|
|
1037
|
+
// cell is scaled up and the detail that decides between two digits is
|
|
1038
|
+
// interpolated rather than scanned - and an invented stroke favours
|
|
1039
|
+
// whichever rival it resembles. The one false call this check produced on
|
|
1040
|
+
// a 93 dpi scan came from cells of fourteen pixels.
|
|
1041
|
+
if (glyph.height < MATCH_HEIGHT) {
|
|
1042
|
+
coarse = true;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
crops.set(index, {
|
|
1046
|
+
glyph,
|
|
1047
|
+
printed: asPrinted
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (crops.size === 0) return {
|
|
1051
|
+
verified: false,
|
|
1052
|
+
because: coarse ? 'too-coarse' : 'unplaceable'
|
|
1053
|
+
};
|
|
1054
|
+
// How far this scan's print has to be sharpened, measured where the answer is
|
|
1055
|
+
// known: each cell against the very glyph the original prints there.
|
|
1056
|
+
const pairs = [...crops.values()];
|
|
1057
|
+
const recoveries = SHARPENING.map(amount => recoveryFor(pairs, amount));
|
|
1058
|
+
const limits = {
|
|
1059
|
+
minMargin,
|
|
1060
|
+
minScore,
|
|
1061
|
+
maxPrinted,
|
|
1062
|
+
minPrinted
|
|
1063
|
+
};
|
|
1064
|
+
const reading = [...printed];
|
|
1065
|
+
const scored = [];
|
|
1066
|
+
let confidence = 1;
|
|
1067
|
+
let checked = 0;
|
|
1068
|
+
for (const [index, crop] of crops) {
|
|
1069
|
+
const passes = recoveries.map(recovery => judgeCell(crop, printed[index], rivalsAt(index), run, templates, recovery, limits));
|
|
1070
|
+
const first = passes[0];
|
|
1071
|
+
if (first === undefined) continue;
|
|
1072
|
+
// Undecided unless every pass reads it the same way: a verdict that changes
|
|
1073
|
+
// as the sharpening changes is the sharpening talking, not the ink.
|
|
1074
|
+
const settled = first.read !== null && passes.every(pass => pass.read === first.read);
|
|
996
1075
|
scored.push({
|
|
997
1076
|
at: index,
|
|
998
1077
|
printed: printed[index],
|
|
999
|
-
printedScore,
|
|
1000
|
-
rival: rival
|
|
1001
|
-
rivalScore:
|
|
1002
|
-
read:
|
|
1078
|
+
printedScore: first.printedScore,
|
|
1079
|
+
rival: first.rival,
|
|
1080
|
+
rivalScore: first.rivalScore,
|
|
1081
|
+
read: settled ? first.read : null
|
|
1003
1082
|
});
|
|
1004
|
-
if (!
|
|
1083
|
+
if (!settled || first.read === null) continue;
|
|
1005
1084
|
checked++;
|
|
1006
|
-
|
|
1007
|
-
|
|
1085
|
+
reading[index] = first.read;
|
|
1086
|
+
// The narrowest margin any pass decided by, so confidence is the weakest link.
|
|
1087
|
+
confidence = Math.min(confidence, ...passes.map(pass => Math.abs(pass.printedScore - pass.rivalScore)));
|
|
1008
1088
|
}
|
|
1009
1089
|
// Nothing decided is not an answer: say nothing rather than guess a digit.
|
|
1010
|
-
if (checked === 0) return
|
|
1090
|
+
if (checked === 0) return {
|
|
1091
|
+
verified: false,
|
|
1092
|
+
because: 'undecided'
|
|
1093
|
+
};
|
|
1011
1094
|
// Put the spaces back, so the reading reads like the run it is about.
|
|
1012
1095
|
let cell = 0;
|
|
1013
1096
|
const text = [...run.text].map(character => character.trim() === '' ? character : reading[cell++]).join('');
|
|
1014
1097
|
return {
|
|
1098
|
+
verified: true,
|
|
1015
1099
|
reading: text,
|
|
1016
1100
|
agrees: text === run.text,
|
|
1017
1101
|
checked,
|
|
@@ -1019,6 +1103,31 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
|
|
|
1019
1103
|
cells: scored
|
|
1020
1104
|
};
|
|
1021
1105
|
}
|
|
1106
|
+
/** What one pass of the sharpening sweep makes of a single cell. */
|
|
1107
|
+
function judgeCell(crop, printed, rivals, run, templates, recovery, limits) {
|
|
1108
|
+
const printedScore = correlate(crop.glyph, crop.printed, recovery);
|
|
1109
|
+
const others = rivals.filter(character => character !== printed).map(character => ({
|
|
1110
|
+
character,
|
|
1111
|
+
score: bestMatch(crop.glyph, templates.get(templateKey(run, character)) ?? [], recovery)
|
|
1112
|
+
})).toSorted((a, b) => b.score - a.score);
|
|
1113
|
+
const rival = others[0];
|
|
1114
|
+
if (rival === undefined) return {
|
|
1115
|
+
read: null,
|
|
1116
|
+
printedScore,
|
|
1117
|
+
rival: '',
|
|
1118
|
+
rivalScore: -1
|
|
1119
|
+
};
|
|
1120
|
+
// A change has to look like the character it is being read as, not merely
|
|
1121
|
+
// less like the printed one.
|
|
1122
|
+
const changed = rival.score >= printedScore + limits.minMargin && rival.score >= limits.minScore && printedScore <= limits.maxPrinted;
|
|
1123
|
+
const unchanged = printedScore >= rival.score + limits.minMargin && printedScore >= limits.minPrinted;
|
|
1124
|
+
return {
|
|
1125
|
+
read: changed ? rival.character : unchanged ? printed : null,
|
|
1126
|
+
printedScore,
|
|
1127
|
+
rival: rival.character,
|
|
1128
|
+
rivalScore: rival.score
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1022
1131
|
/**
|
|
1023
1132
|
* The digits of every figure of at least `minDigits` digits.
|
|
1024
1133
|
*
|
|
@@ -1031,6 +1140,15 @@ function textCells(printed, characters) {
|
|
|
1031
1140
|
for (const [index, character] of printed.entries()) if (characters.includes(character)) wanted.add(index);
|
|
1032
1141
|
return wanted;
|
|
1033
1142
|
}
|
|
1143
|
+
/** Where the print and the reading disagree, which is the whole of what a settlement asks about. */
|
|
1144
|
+
function disputedCells(printed, proposed, characters) {
|
|
1145
|
+
const wanted = new Set();
|
|
1146
|
+
for (const [index, character] of printed.entries()) {
|
|
1147
|
+
const against = proposed[index];
|
|
1148
|
+
if (against !== undefined && against !== character && characters.includes(character) && characters.includes(against)) wanted.add(index);
|
|
1149
|
+
}
|
|
1150
|
+
return wanted;
|
|
1151
|
+
}
|
|
1034
1152
|
function figureCells(printed, minDigits) {
|
|
1035
1153
|
const wanted = new Set();
|
|
1036
1154
|
let group = [];
|
|
@@ -1047,38 +1165,62 @@ function figureCells(printed, minDigits) {
|
|
|
1047
1165
|
return wanted;
|
|
1048
1166
|
}
|
|
1049
1167
|
/** How soft the print has to be drawn to sit best on the scan's own glyphs. */
|
|
1050
|
-
function
|
|
1168
|
+
function recoveryFor(pairs, amount) {
|
|
1051
1169
|
let best = {
|
|
1052
|
-
|
|
1170
|
+
recovery: {
|
|
1171
|
+
sigma: 0,
|
|
1172
|
+
amount
|
|
1173
|
+
},
|
|
1053
1174
|
score: -Infinity
|
|
1054
1175
|
};
|
|
1055
|
-
for (const
|
|
1056
|
-
const
|
|
1176
|
+
for (const sigma of RECOVERY) {
|
|
1177
|
+
const recovery = {
|
|
1178
|
+
sigma,
|
|
1179
|
+
amount
|
|
1180
|
+
};
|
|
1181
|
+
const score = pairs.reduce((sum, pair) => sum + correlate(pair.glyph, pair.printed, recovery), 0);
|
|
1057
1182
|
if (score > best.score) best = {
|
|
1058
|
-
|
|
1183
|
+
recovery,
|
|
1059
1184
|
score
|
|
1060
1185
|
};
|
|
1061
1186
|
}
|
|
1062
|
-
return best.
|
|
1187
|
+
return best.recovery;
|
|
1063
1188
|
}
|
|
1064
1189
|
/** The best correlation between one glyph and a character's templates. */
|
|
1065
|
-
function bestMatch(glyph, templates,
|
|
1190
|
+
function bestMatch(glyph, templates, recovery) {
|
|
1066
1191
|
let best = -1;
|
|
1067
|
-
for (const template of templates) best = Math.max(best, correlate(glyph, template,
|
|
1192
|
+
for (const template of templates) best = Math.max(best, correlate(glyph, template, recovery));
|
|
1068
1193
|
return best;
|
|
1069
1194
|
}
|
|
1195
|
+
/** An unsharp mask: what the scanner blurred away, added back. */
|
|
1196
|
+
function sharpen(image, {
|
|
1197
|
+
sigma,
|
|
1198
|
+
amount
|
|
1199
|
+
}) {
|
|
1200
|
+
if (sigma <= 0 || amount <= 0) return image;
|
|
1201
|
+
const blurred = soften(image, sigma);
|
|
1202
|
+
const data = new Float32Array(image.data.length);
|
|
1203
|
+
for (let i = 0; i < data.length; i++) data[i] = image.data[i] + amount * (image.data[i] - blurred.data[i]);
|
|
1204
|
+
return {
|
|
1205
|
+
width: image.width,
|
|
1206
|
+
height: image.height,
|
|
1207
|
+
data
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1070
1210
|
/**
|
|
1071
1211
|
* How alike two glyphs are, each scaled to the same small box, and the template
|
|
1072
1212
|
* slid a pixel each way to allow for a cell that landed a fraction out.
|
|
1073
1213
|
*
|
|
1074
1214
|
* Correlation, rather than a difference of pixels, because a scan is darker or
|
|
1075
|
-
* lighter than the print it came from and that must not decide anything.
|
|
1215
|
+
* lighter than the print it came from and that must not decide anything. The
|
|
1216
|
+
* scan's glyph is sharpened; the template is left as it was printed. See
|
|
1217
|
+
* {@link RECOVERY} for why that direction and not the other.
|
|
1076
1218
|
*/
|
|
1077
|
-
function correlate(glyph, template,
|
|
1219
|
+
function correlate(glyph, template, recovery) {
|
|
1078
1220
|
const height = MATCH_HEIGHT;
|
|
1079
1221
|
const width = Math.max(2, Math.round(height * ((glyph.width / glyph.height + template.width / template.height) / 2)));
|
|
1080
|
-
const a = resizeGray(glyph, width, height);
|
|
1081
|
-
const b =
|
|
1222
|
+
const a = sharpen(resizeGray(glyph, width, height), recovery);
|
|
1223
|
+
const b = resizeGray(template, width, height);
|
|
1082
1224
|
let best = -1;
|
|
1083
1225
|
for (let dy = -SHIFT; dy <= SHIFT; dy++) for (let dx = -SHIFT; dx <= SHIFT; dx++) best = Math.max(best, pearson(a, b, dx, dy));
|
|
1084
1226
|
return best;
|
|
@@ -1625,7 +1767,15 @@ async function readPage(page, engine, options, templates) {
|
|
|
1625
1767
|
// reading of a returned scan can: whether this is still the digit that was printed.
|
|
1626
1768
|
const printChecks = {
|
|
1627
1769
|
checked: 0,
|
|
1628
|
-
different: 0
|
|
1770
|
+
different: 0,
|
|
1771
|
+
skipped: {
|
|
1772
|
+
'no-figure': 0,
|
|
1773
|
+
'unplaceable': 0,
|
|
1774
|
+
'few-rivals': 0,
|
|
1775
|
+
'too-coarse': 0,
|
|
1776
|
+
'undecided': 0,
|
|
1777
|
+
'no-claim': 0
|
|
1778
|
+
}
|
|
1629
1779
|
};
|
|
1630
1780
|
const seenChanged = new Set();
|
|
1631
1781
|
if (printCheck !== false && useLayer) {
|
|
@@ -1635,7 +1785,10 @@ async function readPage(page, engine, options, templates) {
|
|
|
1635
1785
|
}));
|
|
1636
1786
|
for (const [r, run] of printed.entries()) {
|
|
1637
1787
|
const verified = verifyPrintedRun(originalGray, scanGray, originalDpi, run, templates, printCheck);
|
|
1638
|
-
if (verified
|
|
1788
|
+
if (!verified.verified) {
|
|
1789
|
+
printChecks.skipped[verified.because]++;
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1639
1792
|
printChecks.checked++;
|
|
1640
1793
|
if (!verified.agrees) {
|
|
1641
1794
|
printChecks.different++;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type { ScoreMetric, TextMetrics } from './text-similarity/index.js';
|
|
|
31
31
|
export { DEFAULT_NORMALISE, normaliseText, tokenise } from './text-normalisation/index.js';
|
|
32
32
|
export type { NormaliseOptions } from './text-normalisation/index.js';
|
|
33
33
|
export { collectInto, collectTemplates, FIGURE_CHARACTERS, glyphCells, glyphWords, placeGlyphs, printPolarity, templateKey, TEXT_CHARACTERS, verifyPrintedRun } from './print-verification/index.js';
|
|
34
|
-
export type { CellOptions, PrintPolarity, PrintVerification, TemplateStore, Templates, VerifyOptions } from './print-verification/index.js';
|
|
34
|
+
export type { CellOptions, PrintAbstention, PrintCheck, PrintPolarity, PrintVerification, TemplateStore, Templates, VerifyOptions } from './print-verification/index.js';
|
|
35
35
|
export { claimWords, DEFAULT_RECHECK_PASSES, judgeRun, judgeRuns, matchWords, readRun, recheckRun } from './page-reading/index.js';
|
|
36
36
|
export type { Claims, MatchOptions, Recheck, RecheckOptions, RecheckPass, Reference, Verdict, WordMatch } from './page-reading/index.js';
|
|
37
37
|
export { cosine, dice, jaccard, jaroWinkler, levenshtein, levenshteinSimilarity, wordDistance, wordRecall } from './text-similarity/index.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ProgressCallback, ReadablePage } from '@scanmate/ink';
|
|
2
2
|
import type { OcrEngine, TesseractEngineOptions } from '../ocr-engine/index.js';
|
|
3
|
-
import type { VerifyOptions } from '../print-verification/index.js';
|
|
3
|
+
import type { PrintAbstention, VerifyOptions } from '../print-verification/index.js';
|
|
4
4
|
import type { RecheckOptions } from './recheck-run.use-case.js';
|
|
5
5
|
import type { NormaliseOptions } from '../text-normalisation/index.js';
|
|
6
6
|
import type { ScoreMetric, TextMetrics } from '../text-similarity/index.js';
|
|
@@ -136,10 +136,16 @@ export interface PageOcr {
|
|
|
136
136
|
attempted: number;
|
|
137
137
|
cleared: number;
|
|
138
138
|
};
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Printed figures matched against the original's own glyphs: how many were
|
|
141
|
+
* decided, how many read as something else, and why the rest were not looked
|
|
142
|
+
* at. Without `skipped`, "checked and agreed" and "never checked" are the
|
|
143
|
+
* same silence from outside.
|
|
144
|
+
*/
|
|
140
145
|
printChecks: {
|
|
141
146
|
checked: number;
|
|
142
147
|
different: number;
|
|
148
|
+
skipped: Record<PrintAbstention, number>;
|
|
143
149
|
};
|
|
144
150
|
warnings: string[];
|
|
145
151
|
}
|
|
@@ -6,5 +6,6 @@ export { mergeVerifiedFigures } from './merge-reading.mapper.js';
|
|
|
6
6
|
export { printPolarity } from './print-polarity.policy.js';
|
|
7
7
|
export type { PrintPolarity } from './print-polarity.policy.js';
|
|
8
8
|
export { FIGURE_CHARACTERS, TEXT_CHARACTERS, verifyPrintedRun } from './verify-print.use-case.js';
|
|
9
|
+
export type { PrintAbstention, PrintCheck } from './verify-print.use-case.js';
|
|
9
10
|
export type { PrintVerification, VerifyOptions } from './verify-print.use-case.js';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -70,10 +70,22 @@ export interface VerifyOptions {
|
|
|
70
70
|
minRivals?: number;
|
|
71
71
|
/**
|
|
72
72
|
* What to check: the digits of a figure (`'figures'`, the default, cheap
|
|
73
|
-
* enough for a whole page),
|
|
74
|
-
*
|
|
73
|
+
* enough for a whole page), every letter and digit of the run (`'text'`, for
|
|
74
|
+
* a single run under dispute), or the one question a settlement actually
|
|
75
|
+
* asks (`'confirm'`).
|
|
76
|
+
*
|
|
77
|
+
* `'confirm'` needs {@link claimed} and answers "is this ink the character
|
|
78
|
+
* the original printed, or the one the reading says it is?" - which takes
|
|
79
|
+
* two templates rather than a near-complete alphabet, because the reading
|
|
80
|
+
* has already named the alternative. See {@link VerifyOptions.claimed}.
|
|
75
81
|
*/
|
|
76
|
-
scope?: 'figures' | 'text';
|
|
82
|
+
scope?: 'figures' | 'text' | 'confirm';
|
|
83
|
+
/**
|
|
84
|
+
* What the reading claims the run says, for `scope: 'confirm'`. Compared
|
|
85
|
+
* character by character against the original's own text, so it has to hold
|
|
86
|
+
* the same number of printed characters; anything else abstains.
|
|
87
|
+
*/
|
|
88
|
+
claimed?: string;
|
|
77
89
|
/** How badly the printed glyph must match for a character to count as changed. Default `0.85`. */
|
|
78
90
|
maxPrinted?: number;
|
|
79
91
|
/** Digits in a row before a group is treated as a figure. Default `2`. */
|
|
@@ -95,6 +107,31 @@ export interface CellVerification {
|
|
|
95
107
|
/** What this cell was taken to be, or `null` when it was too close to call. */
|
|
96
108
|
read: string | null;
|
|
97
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Why a run was not checked.
|
|
112
|
+
*
|
|
113
|
+
* Reported rather than folded into a bare `null`, because "checked and agreed"
|
|
114
|
+
* and "never looked at" are the same silence from outside, and telling them
|
|
115
|
+
* apart by inference costs a consumer days and can still come out wrong.
|
|
116
|
+
*
|
|
117
|
+
* - `no-figure` - the run holds no group of digits long enough to be a figure.
|
|
118
|
+
* - `unplaceable` - the glyphs could not be cut apart.
|
|
119
|
+
* - `few-rivals` - the document prints too little of this face to rule other
|
|
120
|
+
* characters out.
|
|
121
|
+
* - `too-coarse` - the scan resolves the cell below the height it is matched
|
|
122
|
+
* at, so a verdict would rest on detail it never captured.
|
|
123
|
+
* - `undecided` - measured, and nothing won by enough to say.
|
|
124
|
+
* - `no-claim` - `scope: 'confirm'` without a reading to test, or one that
|
|
125
|
+
* does not line up with the print character for character.
|
|
126
|
+
*/
|
|
127
|
+
export type PrintAbstention = 'no-figure' | 'unplaceable' | 'few-rivals' | 'too-coarse' | 'undecided' | 'no-claim';
|
|
128
|
+
/** What the ink said, or why it would not say. */
|
|
129
|
+
export type PrintCheck = ({
|
|
130
|
+
verified: true;
|
|
131
|
+
} & PrintVerification) | {
|
|
132
|
+
verified: false;
|
|
133
|
+
because: PrintAbstention;
|
|
134
|
+
};
|
|
98
135
|
export interface PrintVerification {
|
|
99
136
|
/** The run as its ink reads, printed characters kept where nothing was checked. */
|
|
100
137
|
reading: string;
|
|
@@ -116,7 +153,7 @@ export interface PrintVerification {
|
|
|
116
153
|
* @param run - The run to check, from the original's text layer.
|
|
117
154
|
* @param templates - Glyphs collected from the original by `collectTemplates`.
|
|
118
155
|
* @param options - Margin, figure length and the characters figures use.
|
|
119
|
-
* @returns What the ink says, or
|
|
156
|
+
* @returns What the ink says, or why it would not say.
|
|
120
157
|
*/
|
|
121
|
-
export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions):
|
|
158
|
+
export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions): PrintCheck;
|
|
122
159
|
//# sourceMappingURL=verify-print.use-case.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scanmate/ocr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "How closely a scan's text matches the original's: OCR matched run by run on the page, figures held exact, every difference located.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eduardo Russo",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"!**/*.js.map"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@scanmate/ink": "^0.
|
|
44
|
+
"@scanmate/ink": "^0.6.0",
|
|
45
45
|
"@tesseract.js-data/eng": "^1.0.0",
|
|
46
46
|
"fastest-levenshtein": "^1.0.16",
|
|
47
47
|
"tesseract.js": "^7.0.0"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@scanmate/align": "^0.
|
|
54
|
-
"@scanmate/extract": "^0.
|
|
53
|
+
"@scanmate/align": "^0.6.0",
|
|
54
|
+
"@scanmate/extract": "^0.6.0"
|
|
55
55
|
}
|
|
56
56
|
}
|