by Martin Monperrus

My perfect-OCR quest failed for encoding beyond 32 characters.

This post explores one can OCR base58 with no errors.

TLDR: The result is OcrMono, a FreeMono fork in which GOCR can read 1,051,200 random base58 characters with zero errors — 6.6 kB of payload per A4 page.

Goal

We want perfect OCR of base58.

OCR software: we will use GOCR because it’s open source and already a strong baseline.

We will read a A4 page full of base58 at 400 DPI. We aim at testing a million characters, and maintain zero errors.

Step 1: measure what actually breaks

Methodology: A4, ReportLab, 9 mm margins, lines filled edge-to-edge with uniform random base58, rasterized at 400 DPI grayscale (3308 x 4678 px), PGM for GOCR. Baseline font: FreeMono, the blog’s own favourite, at 9 pt (100 chars x 73 rows = 7,300 chars/page).

font size chars substitutions CER
FreeMono 12 pt 36,450 0 0.0000%
FreeMono 9 pt 65,700 43 0.0654%

Two campaigns went into this. First, the 9 pt / 12 pt runs from the table above: 65,700 and 36,450 characters, giving the 43 substitutions at 9 pt their exact confusion breakdown. Second, a sweep of every other printable FreeMono size (6, 7, 8, 10, 11 pt, one seed-42 page each) to check the edges of the usable range — pushing total base58 coverage past 150,000 characters and turning up more failure modes:

size chars truth read count proportion
9 pt 65,700 o O 36 36/43
9 pt 65,700 o 0 5 5/43
9 pt 65,700 U u 1 1/43
9 pt 65,700 C c 1 1/43
6 pt 16,459 n _ 284 284/306
6 pt 16,459 o 0 21 21/306
6 pt 16,459 g 9 1 1/306
8 pt 9,266 A h 2 2/2

6 pt and 11 pt are excluded from the rest of this post (see Appendix). 7 pt, 10 pt and 12 pt have zero substitutions and don’t appear above.

95% of all errors are one glyph: lowercase o wandering into the O/0 neighborhood. The explanation is pure geometry: in FreeMono, o spans y = -16..431 units/em while O spans -16..576 — the two shapes differ by only 145 units of vertical extent, i.e. 7 pixels at 9 pt and 400 DPI, and GOCR’s template matcher folds them together.

Step 2: edit the font

Next, we edit the FreeMono font definition with FontForge. After experimentation, we decide to only change the lowercase side, touch capitals never, and never change an advance width. (Enlarging capitals introduces new failure modes)

Final OcrMono geometry, measured from the shipped TTF:

glyph FreeMono OcrMono
o y -16..431 y -12..336
O y -16..576 y -16..576 (untouched)
0 w 374 w 292, y -15..618
s z v x k y 0..431 y 0..317-328
p y w g y -186..431 y -138..309-319
S Z V X K P Y W y 0..563-576 unchanged

Results: 1,051,200 characters, zero errors

144 independent random A4 pages, OcrMono 9 pt, 400 DPI, GOCR 0.52:

metric value
pages 144
characters 1,051,200
substitutions 0
insertions/deletions 0
lost lines 0
CER 0.0000000

The confusion matrix over the whole run is empty. Not one o/O/0, S/s, Z/z, V/v, X/x, P/p, Y/y, W/w, K/k, U/u, C/c, j/J, 8/ß or 9/g substitution occurred in more than a million random characters — nearly a megabyte of transcription without a single flipped bit.

Versus the blog’s reference results on A4:

pipeline payload/A4 accuracy
blog: base16, GOCR, Inconsolata 12 pt, 400 DPI ~2.5 kB 100%
blog: base64 + CRC32/Damm repair, Inconsolata 11 pt ~4.2 kB 100% after repair
OcrMono: base58, GOCR, 9 pt, 400 DPI 6.6 kB 100% raw, no repair

OcrMono can be downloaded at https://github.com/monperrus/OcrMono.

Practical Use

Practical uses of perfect base58 OCR: paper backup of cold-wallet keys and seeds, offline key transport, data-on-paper archiving — now at 6.6 kB/A4 with no error-correction overhead.

Repro

  # 1. build the font (FreeMono -> /tmp/ocrbench/OcrMono.ttf)
  python3 make_ocrmono.py

  # 2. one page: generate, rasterize, OCR, compare
  python3 gen.py --alphabet base58 --font /tmp/ocrbench/OcrMono.ttf \
      --size 9 --cols 0 --seed 5000 --pdf p.pdf --txt p.txt
  pdftoppm -r 400 -gray -png p.pdf p
  convert p-1.png p.pgm            # gocr reads PGM, not PNG
  gocr -f UTF8 -i p.pgm -o out.txt
  python3 cmp.py p.txt out.txt     # strips gocr's spurious spaces

  # 3. the million-character validation
  python3 run_gocr_batch.py 132 9  # 132 pages x 7,300 chars, running CER

Appendix

n->_ and W->_ are baseline-tracking “underscore storms”, not shape confusions — which is why 6 pt and 11 pt are excluded from the rest of this post.