honmono-ocr - Japanese book OCR (recognition)
A compact (~5M-param, 5.5 MB FP16) Japanese text-recognition model fine-tuned from PP-OCRv5_mobile_rec with improved vertical text (tategaki) and character recognition. It recognizes one already-cropped text line at a time, horizontal or vertical, over a fixed 7,854-character vocabulary (kanji, hiragana, katakana, ASCII, punctuation). This is the recognizer shipped in the Honmono Android reader.
- Code & recipe: https://github.com/eridgd/honmono-ocr
- Live demo: https://huggingface.co/spaces/eridgd/honmono-ocr-demo
Results
honmono-ocr is fine-tuned from PP-OCRv5_mobile_rec and clearly beats that base model on real Japanese book text:
| Real held-out data | PP-OCRv5 base | honmono-ocr | Δ |
|---|---|---|---|
| NDL PDM (Kindai printed) | 31.9% | 82.0% | +50.1 pp |
| NDL NDLOCR (modern print) | 26.8% | 72.8% | +46.0 pp |
| ICDAR MLT (scene text) | 38.1% | 51.7% | +13.6 pp |
| Overall (27,920 lines) | 33.2% | 72.0% | +38.8 pp |
Fine-tuning more than doubles overall line accuracy and wins on every category, with the biggest gains on printed book pages. Per-dataset CER, synthetic, and unseen-font numbers are in Evaluation below.
Intended use & limitations
Use it for recognizing lines of printed Japanese book text (phone-camera photos of books, scanned pages, lightly-degraded print) in both orientations (horizontal and vertical / 縦書き). Pair it with a text detector or line segmenter to build a full-page OCR pipeline (the demo uses the stock PP-OCRv5 detector).
Not intended for: detection or layout analysis, handwriting, calligraphy, heavy cursive classical script, dense scene text, signs in the wild, or severely damaged historical material.
Common failure modes: similar kanji under blur / low contrast; ruby/furigana mixed into the line crop; vertical punctuation and rotated Latin text; old typefaces, damaged print, bleed-through; scene text, handwriting, calligraphy; poorly segmented crops with multiple text lines.
How to use
import cv2, numpy as np, onnxruntime as ort
def load_chars(p): # ['blank'] + dict + [' '] (use_space_char)
chars = ["blank"] # keep whitespace glyphs (U+0020, U+3000); strip only newline
for ln in open(p, encoding="utf-8"):
ch = ln.rstrip("\r\n")
if ch: chars.append(ch)
return chars + [" "]
def preprocess(bgr): # -> [1,3,48,480], [-1,1], right-padded
h, w = bgr.shape[:2]
nw = min(int(round(w * 48 / h)), 480)
img = cv2.resize(bgr, (nw, 48)).astype(np.float32).transpose(2, 0, 1) / 255.0
img = (img - 0.5) / 0.5
out = np.zeros((3, 48, 480), np.float32); out[:, :, :nw] = img
return out[None]
def ctc_decode(logits, chars): # argmax, drop blanks(0), collapse repeats
prev, res = -1, []
for i in logits.argmax(-1):
if i != 0 and i != prev and i < len(chars): res.append(chars[i])
prev = i
return "".join(res)
chars = load_chars("jp_dict.txt")
sess = ort.InferenceSession("book_rec_fp16.onnx", providers=["CPUExecutionProvider"])
crop = cv2.imread("line.jpg") # BGR; rotate tall (vertical) crops 90° CCW first
logits = sess.run(None, {sess.get_inputs()[0].name: preprocess(crop)})[0][0]
print(ctc_decode(logits, chars))
A runnable version is example.py. FP16 on desktop ONNX Runtime needs
ORT_ENABLE_BASIC graph optimization (a SimplifiedLayerNorm fusion bug with FP16 Cast nodes);
Android NNAPI is unaffected. FP16 keeps I/O float32 (keep_io_types=True), so client code is
identical to FP32.
I/O contract
- Input
[1,3,48,W],W≤480, BGR. Resize to height 48 keeping aspect (W=min(480, ceil(48·w/h))), pad right, normalize(x/255−0.5)/0.5, layout CHW. - Vertical text is rendered top-to-bottom then rotated 90° CCW to a horizontal strip for training; at inference, rotate tall crops (h>w) 90° CCW before feeding them in.
- Output
[1,T,7856],T≈W/8.7856 = blank(0) + 7,854 dict chars (jp_dict.txt, file order) + space. - Verify the index mapping with
verify_ctc_index.pyin the repo.
Training data
| Source | Use | License |
|---|---|---|
| PP-OCRv5_mobile_rec | base weights | Apache-2.0 |
| Aozora Bunko | synthetic text | public-domain / author-permitted texts |
| Japanese Wikipedia | synthetic text | CC-BY-SA-3.0 |
| NDL OCR datasets (PDM, NDLOCR, one-line) | real lines | CC-BY-4.0 |
| ICDAR MLT 2017 & 2019 | real lines | CC-BY-4.0 |
The model artifacts are distributed under Apache-2.0. Training-data sources and required third-party notices are documented in NOTICE and DATA_PROVENANCE.md.
Training procedure
A two-phase curriculum - synthetic pretrain, then real-world adaptation:
- Phase A - synthetic pretrain from the PP-OCRv5 base: Aozora + Wikipedia text rendered across ~60 fonts with camera-style degradation and real-background compositing; cosine LR ~3e-4.
- Phase B - real-world adaptation: low-LR (5e-5) AMP O2 FP16 fine-tune on real lines (NDL + ICDAR MLT) + synthetic, equally weighted; batch 192, early stopping.
Evaluation
Real-data validation (val_real.txt, 27,920 held-out lines):
| Dataset | n | Seq acc | CER |
|---|---|---|---|
| NDL PDM (Kindai printed) | 16,441 | 82.0% | 0.096 |
| NDL NDLOCR (modern printed) | 2,935 | 72.8% | 0.123 |
| ICDAR MLT (scene text) | 8,497 | 51.7% | 0.240 |
| NDL one-line | 47 | 36.2% | 0.125 |
| Overall | 27,920 | 71.97% | 0.156 |
The head-to-head against the off-the-shelf base (decoded with its own 18,383-char dictionary on the same crops) is in Results above; the one-line subset there is base 4.3% → 36.2%.
Synthetic / generalization: synthetic val 90.52% seq acc; holdout (unseen) fonts 84.47% seq acc / 96.48% char acc.
Sequence accuracy is exact full-line match after greedy CTC. The NDL one-line subset (n=47) is anecdotal; ICDAR MLT reflects out-of-domain scene text; holdout-font accuracy (fonts never seen in training) is the cleaner generalization signal.
Note: The repo's recipe reproduces a model of comparable quality, not a bit-exact copy. Synthetic generation and GPU training are nondeterministic. These
.onnxfiles are the exact production artifacts; the recipe is the clean path to an equivalent one.
On INT8: not viable for this architecture - the SVTR attention neck and NRTR head are too precision-sensitive (QAT collapsed to ~30%; ORT PTQ dropped 25–65pp). FP16 is the deployed format.
Model specs
| Architecture | SVTR_LCNet - PPLCNetV3 (×0.95) backbone, MultiHead (CTC + NRTR), ~5M params |
| Input | float32 [1,3,48,W≤480], BGR, normalized to [-1,1] |
| Output | float32 [1,T,7856] CTC logits (blank + 7,854 chars + space) |
| Decoding | greedy CTC against jp_dict.txt |
| Files | book_rec_fp16.onnx (5.5 MB, recommended) · book_rec_simplified.onnx / book_rec.onnx (FP32, 11 MB) |
Citation & acknowledgements
@software{honmono_ocr,
title = {honmono-ocr: on-device Japanese book OCR recognition},
author = {Evan Davis},
year = {2026},
url = {https://github.com/eridgd/honmono-ocr}
}
Built on PaddleOCR / PP-OCRv5. Data: Aozora Bunko, Wikimedia, the National Diet Library, and the ICDAR Robust Reading Competition (RRC-MLT; Nayef et al., arXiv:1907.00945). License: Apache-2.0. Contact: support@honmono.app.
Model tree for eridgd/honmono-ocr
Base model
PaddlePaddle/PP-OCRv5_mobile_recSpace using eridgd/honmono-ocr 1
Paper for eridgd/honmono-ocr
Evaluation results
- sequence-accuracy on NDL PDM Part 1 (real Japanese book pages)self-reported0.820
- cer on NDL PDM Part 1 (real Japanese book pages)self-reported0.096