@scanmate/ocr 0.4.0 → 0.5.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 +174 -55
- 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 +25 -2
- 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 {
|
|
@@ -949,69 +979,99 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
|
|
|
949
979
|
} = options;
|
|
950
980
|
const printed = [...run.text].filter(character => character.trim() !== '');
|
|
951
981
|
const wanted = scope === 'text' ? textCells(printed, characters) : figureCells(printed, minDigits);
|
|
952
|
-
if (wanted.size === 0) return
|
|
982
|
+
if (wanted.size === 0) return {
|
|
983
|
+
verified: false,
|
|
984
|
+
because: 'no-figure'
|
|
985
|
+
};
|
|
953
986
|
// A total on a shaded bar is printed light on dark; turned round, it is a figure like any other.
|
|
954
987
|
const lightOnDark = printPolarity(original, dpi, run) === 'light-on-dark';
|
|
955
988
|
const cells = placeGlyphs(original, dpi, run, run.text, {
|
|
956
989
|
lightOnDark
|
|
957
990
|
});
|
|
958
|
-
if (cells === null) return
|
|
991
|
+
if (cells === null) return {
|
|
992
|
+
verified: false,
|
|
993
|
+
because: 'unplaceable'
|
|
994
|
+
};
|
|
959
995
|
// Rivals: every character the page prints in this face and size that a figure could use.
|
|
960
996
|
const rivals = [...characters].filter(character => templates.has(templateKey(run, character)));
|
|
961
|
-
if (rivals.length < minRivals) return
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}];
|
|
970
|
-
}).filter(pair => pair.glyph !== null && pair.printed !== null);
|
|
971
|
-
const softening = softeningFor(pairs);
|
|
972
|
-
const reading = [...printed];
|
|
973
|
-
const scored = [];
|
|
974
|
-
let confidence = 1;
|
|
975
|
-
let checked = 0;
|
|
997
|
+
if (rivals.length < minRivals) return {
|
|
998
|
+
verified: false,
|
|
999
|
+
because: 'few-rivals'
|
|
1000
|
+
};
|
|
1001
|
+
// The cell exactly: a margin would bring in the neighbouring glyphs, which
|
|
1002
|
+
// both crops share, and shared ink correlates whatever the character is.
|
|
1003
|
+
const crops = new Map();
|
|
1004
|
+
let coarse = false;
|
|
976
1005
|
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
1006
|
const cell = cells[index];
|
|
980
1007
|
if (cell === null) continue;
|
|
981
1008
|
const glyph = cut(scan, dpi, cell, lightOnDark);
|
|
982
1009
|
// The original's own ink here: the same glyph, at the same size, in the same place.
|
|
983
1010
|
const asPrinted = cut(original, dpi, cell, lightOnDark);
|
|
984
1011
|
if (glyph === null || asPrinted === null) continue;
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1012
|
+
// Never invent resolution. Matching happens at MATCH_HEIGHT, so a shorter
|
|
1013
|
+
// cell is scaled up and the detail that decides between two digits is
|
|
1014
|
+
// interpolated rather than scanned - and an invented stroke favours
|
|
1015
|
+
// whichever rival it resembles. The one false call this check produced on
|
|
1016
|
+
// a 93 dpi scan came from cells of fourteen pixels.
|
|
1017
|
+
if (glyph.height < MATCH_HEIGHT) {
|
|
1018
|
+
coarse = true;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
crops.set(index, {
|
|
1022
|
+
glyph,
|
|
1023
|
+
printed: asPrinted
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
if (crops.size === 0) return {
|
|
1027
|
+
verified: false,
|
|
1028
|
+
because: coarse ? 'too-coarse' : 'unplaceable'
|
|
1029
|
+
};
|
|
1030
|
+
// How far this scan's print has to be sharpened, measured where the answer is
|
|
1031
|
+
// known: each cell against the very glyph the original prints there.
|
|
1032
|
+
const pairs = [...crops.values()];
|
|
1033
|
+
const recoveries = SHARPENING.map(amount => recoveryFor(pairs, amount));
|
|
1034
|
+
const limits = {
|
|
1035
|
+
minMargin,
|
|
1036
|
+
minScore,
|
|
1037
|
+
maxPrinted,
|
|
1038
|
+
minPrinted
|
|
1039
|
+
};
|
|
1040
|
+
const reading = [...printed];
|
|
1041
|
+
const scored = [];
|
|
1042
|
+
let confidence = 1;
|
|
1043
|
+
let checked = 0;
|
|
1044
|
+
for (const [index, crop] of crops) {
|
|
1045
|
+
const passes = recoveries.map(recovery => judgeCell(crop, printed[index], rivals, run, templates, recovery, limits));
|
|
1046
|
+
const first = passes[0];
|
|
1047
|
+
if (first === undefined) continue;
|
|
1048
|
+
// Undecided unless every pass reads it the same way: a verdict that changes
|
|
1049
|
+
// as the sharpening changes is the sharpening talking, not the ink.
|
|
1050
|
+
const settled = first.read !== null && passes.every(pass => pass.read === first.read);
|
|
996
1051
|
scored.push({
|
|
997
1052
|
at: index,
|
|
998
1053
|
printed: printed[index],
|
|
999
|
-
printedScore,
|
|
1000
|
-
rival: rival
|
|
1001
|
-
rivalScore:
|
|
1002
|
-
read:
|
|
1054
|
+
printedScore: first.printedScore,
|
|
1055
|
+
rival: first.rival,
|
|
1056
|
+
rivalScore: first.rivalScore,
|
|
1057
|
+
read: settled ? first.read : null
|
|
1003
1058
|
});
|
|
1004
|
-
if (!
|
|
1059
|
+
if (!settled || first.read === null) continue;
|
|
1005
1060
|
checked++;
|
|
1006
|
-
|
|
1007
|
-
|
|
1061
|
+
reading[index] = first.read;
|
|
1062
|
+
// The narrowest margin any pass decided by, so confidence is the weakest link.
|
|
1063
|
+
confidence = Math.min(confidence, ...passes.map(pass => Math.abs(pass.printedScore - pass.rivalScore)));
|
|
1008
1064
|
}
|
|
1009
1065
|
// Nothing decided is not an answer: say nothing rather than guess a digit.
|
|
1010
|
-
if (checked === 0) return
|
|
1066
|
+
if (checked === 0) return {
|
|
1067
|
+
verified: false,
|
|
1068
|
+
because: 'undecided'
|
|
1069
|
+
};
|
|
1011
1070
|
// Put the spaces back, so the reading reads like the run it is about.
|
|
1012
1071
|
let cell = 0;
|
|
1013
1072
|
const text = [...run.text].map(character => character.trim() === '' ? character : reading[cell++]).join('');
|
|
1014
1073
|
return {
|
|
1074
|
+
verified: true,
|
|
1015
1075
|
reading: text,
|
|
1016
1076
|
agrees: text === run.text,
|
|
1017
1077
|
checked,
|
|
@@ -1019,6 +1079,31 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
|
|
|
1019
1079
|
cells: scored
|
|
1020
1080
|
};
|
|
1021
1081
|
}
|
|
1082
|
+
/** What one pass of the sharpening sweep makes of a single cell. */
|
|
1083
|
+
function judgeCell(crop, printed, rivals, run, templates, recovery, limits) {
|
|
1084
|
+
const printedScore = correlate(crop.glyph, crop.printed, recovery);
|
|
1085
|
+
const others = rivals.filter(character => character !== printed).map(character => ({
|
|
1086
|
+
character,
|
|
1087
|
+
score: bestMatch(crop.glyph, templates.get(templateKey(run, character)) ?? [], recovery)
|
|
1088
|
+
})).toSorted((a, b) => b.score - a.score);
|
|
1089
|
+
const rival = others[0];
|
|
1090
|
+
if (rival === undefined) return {
|
|
1091
|
+
read: null,
|
|
1092
|
+
printedScore,
|
|
1093
|
+
rival: '',
|
|
1094
|
+
rivalScore: -1
|
|
1095
|
+
};
|
|
1096
|
+
// A change has to look like the character it is being read as, not merely
|
|
1097
|
+
// less like the printed one.
|
|
1098
|
+
const changed = rival.score >= printedScore + limits.minMargin && rival.score >= limits.minScore && printedScore <= limits.maxPrinted;
|
|
1099
|
+
const unchanged = printedScore >= rival.score + limits.minMargin && printedScore >= limits.minPrinted;
|
|
1100
|
+
return {
|
|
1101
|
+
read: changed ? rival.character : unchanged ? printed : null,
|
|
1102
|
+
printedScore,
|
|
1103
|
+
rival: rival.character,
|
|
1104
|
+
rivalScore: rival.score
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1022
1107
|
/**
|
|
1023
1108
|
* The digits of every figure of at least `minDigits` digits.
|
|
1024
1109
|
*
|
|
@@ -1047,38 +1132,62 @@ function figureCells(printed, minDigits) {
|
|
|
1047
1132
|
return wanted;
|
|
1048
1133
|
}
|
|
1049
1134
|
/** How soft the print has to be drawn to sit best on the scan's own glyphs. */
|
|
1050
|
-
function
|
|
1135
|
+
function recoveryFor(pairs, amount) {
|
|
1051
1136
|
let best = {
|
|
1052
|
-
|
|
1137
|
+
recovery: {
|
|
1138
|
+
sigma: 0,
|
|
1139
|
+
amount
|
|
1140
|
+
},
|
|
1053
1141
|
score: -Infinity
|
|
1054
1142
|
};
|
|
1055
|
-
for (const
|
|
1056
|
-
const
|
|
1143
|
+
for (const sigma of RECOVERY) {
|
|
1144
|
+
const recovery = {
|
|
1145
|
+
sigma,
|
|
1146
|
+
amount
|
|
1147
|
+
};
|
|
1148
|
+
const score = pairs.reduce((sum, pair) => sum + correlate(pair.glyph, pair.printed, recovery), 0);
|
|
1057
1149
|
if (score > best.score) best = {
|
|
1058
|
-
|
|
1150
|
+
recovery,
|
|
1059
1151
|
score
|
|
1060
1152
|
};
|
|
1061
1153
|
}
|
|
1062
|
-
return best.
|
|
1154
|
+
return best.recovery;
|
|
1063
1155
|
}
|
|
1064
1156
|
/** The best correlation between one glyph and a character's templates. */
|
|
1065
|
-
function bestMatch(glyph, templates,
|
|
1157
|
+
function bestMatch(glyph, templates, recovery) {
|
|
1066
1158
|
let best = -1;
|
|
1067
|
-
for (const template of templates) best = Math.max(best, correlate(glyph, template,
|
|
1159
|
+
for (const template of templates) best = Math.max(best, correlate(glyph, template, recovery));
|
|
1068
1160
|
return best;
|
|
1069
1161
|
}
|
|
1162
|
+
/** An unsharp mask: what the scanner blurred away, added back. */
|
|
1163
|
+
function sharpen(image, {
|
|
1164
|
+
sigma,
|
|
1165
|
+
amount
|
|
1166
|
+
}) {
|
|
1167
|
+
if (sigma <= 0 || amount <= 0) return image;
|
|
1168
|
+
const blurred = soften(image, sigma);
|
|
1169
|
+
const data = new Float32Array(image.data.length);
|
|
1170
|
+
for (let i = 0; i < data.length; i++) data[i] = image.data[i] + amount * (image.data[i] - blurred.data[i]);
|
|
1171
|
+
return {
|
|
1172
|
+
width: image.width,
|
|
1173
|
+
height: image.height,
|
|
1174
|
+
data
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1070
1177
|
/**
|
|
1071
1178
|
* How alike two glyphs are, each scaled to the same small box, and the template
|
|
1072
1179
|
* slid a pixel each way to allow for a cell that landed a fraction out.
|
|
1073
1180
|
*
|
|
1074
1181
|
* 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.
|
|
1182
|
+
* lighter than the print it came from and that must not decide anything. The
|
|
1183
|
+
* scan's glyph is sharpened; the template is left as it was printed. See
|
|
1184
|
+
* {@link RECOVERY} for why that direction and not the other.
|
|
1076
1185
|
*/
|
|
1077
|
-
function correlate(glyph, template,
|
|
1186
|
+
function correlate(glyph, template, recovery) {
|
|
1078
1187
|
const height = MATCH_HEIGHT;
|
|
1079
1188
|
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 =
|
|
1189
|
+
const a = sharpen(resizeGray(glyph, width, height), recovery);
|
|
1190
|
+
const b = resizeGray(template, width, height);
|
|
1082
1191
|
let best = -1;
|
|
1083
1192
|
for (let dy = -SHIFT; dy <= SHIFT; dy++) for (let dx = -SHIFT; dx <= SHIFT; dx++) best = Math.max(best, pearson(a, b, dx, dy));
|
|
1084
1193
|
return best;
|
|
@@ -1625,7 +1734,14 @@ async function readPage(page, engine, options, templates) {
|
|
|
1625
1734
|
// reading of a returned scan can: whether this is still the digit that was printed.
|
|
1626
1735
|
const printChecks = {
|
|
1627
1736
|
checked: 0,
|
|
1628
|
-
different: 0
|
|
1737
|
+
different: 0,
|
|
1738
|
+
skipped: {
|
|
1739
|
+
'no-figure': 0,
|
|
1740
|
+
'unplaceable': 0,
|
|
1741
|
+
'few-rivals': 0,
|
|
1742
|
+
'too-coarse': 0,
|
|
1743
|
+
'undecided': 0
|
|
1744
|
+
}
|
|
1629
1745
|
};
|
|
1630
1746
|
const seenChanged = new Set();
|
|
1631
1747
|
if (printCheck !== false && useLayer) {
|
|
@@ -1635,7 +1751,10 @@ async function readPage(page, engine, options, templates) {
|
|
|
1635
1751
|
}));
|
|
1636
1752
|
for (const [r, run] of printed.entries()) {
|
|
1637
1753
|
const verified = verifyPrintedRun(originalGray, scanGray, originalDpi, run, templates, printCheck);
|
|
1638
|
-
if (verified
|
|
1754
|
+
if (!verified.verified) {
|
|
1755
|
+
printChecks.skipped[verified.because]++;
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1639
1758
|
printChecks.checked++;
|
|
1640
1759
|
if (!verified.agrees) {
|
|
1641
1760
|
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
|
|
@@ -95,6 +95,29 @@ export interface CellVerification {
|
|
|
95
95
|
/** What this cell was taken to be, or `null` when it was too close to call. */
|
|
96
96
|
read: string | null;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Why a run was not checked.
|
|
100
|
+
*
|
|
101
|
+
* Reported rather than folded into a bare `null`, because "checked and agreed"
|
|
102
|
+
* and "never looked at" are the same silence from outside, and telling them
|
|
103
|
+
* apart by inference costs a consumer days and can still come out wrong.
|
|
104
|
+
*
|
|
105
|
+
* - `no-figure` - the run holds no group of digits long enough to be a figure.
|
|
106
|
+
* - `unplaceable` - the glyphs could not be cut apart.
|
|
107
|
+
* - `few-rivals` - the document prints too little of this face to rule other
|
|
108
|
+
* characters out.
|
|
109
|
+
* - `too-coarse` - the scan resolves the cell below the height it is matched
|
|
110
|
+
* at, so a verdict would rest on detail it never captured.
|
|
111
|
+
* - `undecided` - measured, and nothing won by enough to say.
|
|
112
|
+
*/
|
|
113
|
+
export type PrintAbstention = 'no-figure' | 'unplaceable' | 'few-rivals' | 'too-coarse' | 'undecided';
|
|
114
|
+
/** What the ink said, or why it would not say. */
|
|
115
|
+
export type PrintCheck = ({
|
|
116
|
+
verified: true;
|
|
117
|
+
} & PrintVerification) | {
|
|
118
|
+
verified: false;
|
|
119
|
+
because: PrintAbstention;
|
|
120
|
+
};
|
|
98
121
|
export interface PrintVerification {
|
|
99
122
|
/** The run as its ink reads, printed characters kept where nothing was checked. */
|
|
100
123
|
reading: string;
|
|
@@ -116,7 +139,7 @@ export interface PrintVerification {
|
|
|
116
139
|
* @param run - The run to check, from the original's text layer.
|
|
117
140
|
* @param templates - Glyphs collected from the original by `collectTemplates`.
|
|
118
141
|
* @param options - Margin, figure length and the characters figures use.
|
|
119
|
-
* @returns What the ink says, or
|
|
142
|
+
* @returns What the ink says, or why it would not say.
|
|
120
143
|
*/
|
|
121
|
-
export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions):
|
|
144
|
+
export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions): PrintCheck;
|
|
122
145
|
//# 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.5.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.5.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.5.0",
|
|
54
|
+
"@scanmate/extract": "^0.5.0"
|
|
55
55
|
}
|
|
56
56
|
}
|