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.

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.py in 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:

  1. 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.
  2. 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 .onnx files 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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for eridgd/honmono-ocr

Quantized
(1)
this model

Space using eridgd/honmono-ocr 1

Paper for eridgd/honmono-ocr

Evaluation results

  • sequence-accuracy on NDL PDM Part 1 (real Japanese book pages)
    self-reported
    0.820
  • cer on NDL PDM Part 1 (real Japanese book pages)
    self-reported
    0.096