PP-OCRv6 Hanzi Unicode OCR

Single-character OCR for CJK glyph images, built on PaddleOCR PP-OCRv6: one cropped image of one glyph in, one Unicode ideograph out.

  • Scope: clean, single-colour, dark-on-light renderings of one character from a computer font — the rare-character images embedded in dictionary data. Handwriting, scans, rubbings and photographs are outside the validated envelope.
  • Accuracy: 97–99% on that input, across modern regular-script computer fonts (宋体 / 黑体 / 仿宋 / 楷体). Decorative and calligraphic faces are excluded.
  • Coverage: all 101,996 CJK unified ideographs of Unicode 17.0 are classes — Extensions A–J plus the URO block, no gaps.

单字字图识别:输入一张只含一个字的图片,输出该字的 Unicode 码位。面向电脑字体渲染的 黑字白底单字图(最初为 OCR 字典中的生僻字插图而做),完整覆盖 Unicode 17.0 全部 101,996 个中日韩统一表意文字。在上述输入条件下(现代电脑正文字体:宋 / 黑 / 仿宋 / 楷) 准确率 **97–99%**,花体字、书法字不在其列。与 IDS 模型相互独立,可交叉核验。

Model Output
pp-ocrv6-hanzi-unicode-ocr (this repo) the character, e.g. 明
pp-ocrv6-hanzi-ids-ocr its structure, e.g. ⿰日月

The two are independent. Run both and compare: disagreements get surfaced for review instead of being silently accepted.

Files

File Size SHA-256
model.onnx 140,836,271 B 78367b92ea954129eac2153aa6f0ace6a32b087b45456d322658135c057f0aca
metadata.json 788,577 B 684d5c955ac556a8f32737215e4f9d0e12dee6f5a8d8d28c721b1e00c620a61d
example_usage.py — —

metadata.json holds the class dictionary (vocab), input shape and preprocessing spec.

Model details

Base PaddleOCR PP-OCRv6 PP-OCRv6_medium_gaiji_fonts_v2 (medium recognition), fine-tuned on glyph fonts, exported to ONNX
Head CTC, 101,997 classes (1 blank + 101,996 ideographs)
Input x — (N, 3, 48, W) float32, dynamic batch and width
Output fetch_name_0 — (N, T, 101997) float32, already softmax-normalised
Time steps T = ceil(W / 8); at the released W = 320, (N, 40, 101997)
Runtime onnxruntime (CPU, CUDA, DirectML, CoreML)

The width axis is dynamic in the graph, but the released configuration always feeds W = 320 zero-padded on the right, which is how the model was trained.

Coverage

vocab lists all 101,997 classes in order: index 0 is the CTC blank, then characters by ascending code point. Compared code point by code point against the official UnicodeData.txt of Unicode 17.0:

Block Range Classes Missing
Ext A U+3400–U+4DBF 6,592 0
URO U+4E00–U+9FFF 20,992 0
Ext B U+20000–U+2A6DF 42,720 0
Ext C U+2A700–U+2B73F 4,160 0
Ext D U+2B740–U+2B81F 222 0
Ext E U+2B820–U+2CEAF 5,774 0
Ext F U+2CEB0–U+2EBEF 7,473 0
Ext I U+2EBF0–U+2EE5F 622 0
Ext G U+30000–U+3134F 4,939 0
Ext H U+31350–U+323AF 4,192 0
Ext J U+323B0–U+3347F 4,298 0
subtotal 101,984 0
CJK Compatibility Ideographs non-normalising only 12 0
101,996 0

Counts are the officially assigned code points; unassigned slots (U+3347A–U+3347F in Ext J, 2 in Ext I) are correctly absent. The 12 compatibility ideographs shipped are exactly those Unicode normalisation maps to themselves (U+FA0E 﨎 U+FA0F 﨏 U+FA11 﨑 U+FA13 﨓 U+FA14 﨔 U+FA1F 﨟 U+FA21 﨡 U+FA23 﨣 U+FA24 﨤 U+FA27 﨧 U+FA28 﨨 U+FA29 﨩); the other 990 normalise to an existing unified ideograph, so the model reports that unified code point rather than duplicating it. No class collapses to another under NFC/NFKC, and nothing outside the CJK ideograph repertoire is in the dictionary — a glyph outside it cannot be produced.

Usage

pip install onnxruntime pillow numpy
python example_usage.py glyph.png
import json
import math

import numpy as np
import onnxruntime as ort
from PIL import Image, ImageDraw, ImageFont

meta = json.load(open("metadata.json", encoding="utf-8"))
vocab = meta["vocab"]
C, H, W = meta["image_shape"]  # [3, 48, 320]

session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name


def prepare(im):
    im = im.convert("RGB")
    iw = min(math.ceil(H * im.width / im.height), W)
    bgr = np.asarray(im)[:, :, ::-1]                       # RGB -> BGR
    bgr = np.asarray(Image.fromarray(bgr).resize((iw, H), Image.BILINEAR))
    plane = bgr.astype(np.float32).transpose(2, 0, 1) / 127.5 - 1
    canvas = np.zeros((C, H, W), np.float32)
    canvas[:, :, :iw] = plane
    return canvas[None]                                    # (1, 3, 48, 320)


def recognise(im):
    probs = session.run(None, {input_name: prepare(im)})[0][0]  # (T, 101997)
    index, scores = probs.argmax(-1), probs.max(-1)
    text, kept, last = [], [], None
    for i, s in zip(index, scores):
        if i != last and i != 0:                           # collapse repeats, drop blank
            text.append(vocab[i])
            kept.append(float(s))
        last = i
    return "".join(text), (sum(kept) / len(kept) if kept else 0.0)


# Round-trip a character through a font, then recognise it back.
char = "明"
font = ImageFont.truetype("C:/Windows/Fonts/simsun.ttc", 200)
box = font.getbbox(char)
img = Image.new("L", (box[2] - box[0] + 20, box[3] - box[1] + 20), 255)
ImageDraw.Draw(img).text((10 - box[0], 10 - box[1]), char, font=font, fill=0)

print(recognise(img))   # ('明', 0.9954...)

Preprocessing must be exact — the model is sensitive to channel order and normalisation: RGB source read as BGR, aspect-preserving resize to height 48, scale to [-1, 1], left-aligned in a (3, 48, 320) zero array with the padding on the right. Decoding is greedy CTC: argmax, collapse repeats, drop the blank index 0. The output is already a per-step distribution, so no extra softmax. The score is the mean of the retained step probabilities — a confidence hint, not a calibrated accuracy.

Limitations

  • One character per image. No detection, no segmentation.
  • High confidence is not proof. It is a closed-set 101,996-way classifier, so a malformed or out-of-repertoire glyph is still forced onto the nearest known character, sometimes with a high score. Cross-check with the IDS model when correctness matters.
  • Coverage stops at Unicode 17.0. Ideographs assigned later will be missing until the model is retrained.

Provenance

  • Upstream licence: PaddleOCR is Apache-2.0 — see THIRD_PARTY_NOTICES.md.
  • The conversion keeps the original 101,997-class CTC dictionary unchanged. On a sample compared against the source Paddle model, the CPU ONNX outputs are character-identical, with a maximum per-frame probability difference of 8.43×10⁻⁵.
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