OCR якутского языка: детектор строк и распознаватель

Два весовых файла и рецепт, по которому они получены. На газетной вёрстке CER 0.22%, WER 2.05%; специфические якутские буквы ҕ ҥ ө һ ү читаются наравне с остальными (пропуск 0.3%).

Готовые движки на этом языке не работают структурно, а не «плохо»:

движок CER недобор ҕҥөһү
ABBYY FineReader 10 (оцифровка соседнего архива) 100% (искажено 31.4% слов)
tesseract-rus 9.76% 100%
tesseract-kaz 3.52% 26.2%
эта модель 0.22% 0.3%

Ноль правильно прочитанных спец-букв на 460 тысячах букв — это не настройка порогов, а отсутствие символов в алфавите модели. Они составляют около 7% всех букв и встречаются в 31% слов, так что треть слов повреждается.

Состав

файл что это параметров
onnx/detector.onnx детектор строк, компактный DB 3.25 M
onnx/recognizer.onnx распознаватель строки, CRNN + CTC 10.08 M
pytorch/*.pt те же веса для дообучения
src/ код: инференс, обучение, экспорт — плоской раскладкой, чтобы импорты работали как есть

Как пользоваться

import json, numpy as np, onnxruntime as ort
from PIL import Image

meta = json.load(open("onnx/recognizer.json"))
charset, H = meta["charset"], meta["height"]          # 123 символа, высота 48
sess = ort.InferenceSession("onnx/recognizer.onnx", providers=["CPUExecutionProvider"])

im = Image.open("line.png").convert("L")
# поле вокруг строки: сегментатор режет вплотную, а край кропа модель
# принимает за знак препинания
p = int(im.height * 0.18)
bg = int(np.median(np.asarray(im)[:, -3:]))
canvas = Image.new("L", (im.width + 2*p, im.height + 2*p), bg)
canvas.paste(im, (p, p)); im = canvas

im = im.resize((max(8, int(im.width * H / im.height)), H), Image.BILINEAR)
x = (1.0 - np.asarray(im, np.float32) / 255.0)[None, None]

logits = sess.run(None, {"image": x})[0][0]
ids, out, prev = logits.argmax(-1), [], -1
for k in ids:                                          # схлопывание CTC, 0 = blank
    if k != prev and k != 0:
        out.append(charset[k - 1])
    prev = k
print("".join(out))

Детектор принимает целую страницу (1 - gray/255, стороны кратны 32, длинная сторона до 2000 px) и отдаёт карту вероятностей в половинном разрешении. Разбор карты в рамки строк и порядок чтения — в src/detect.py.

Рецепт

1. Данные

  • Синтетика — 500 тыс. строк, lab-ii/sakha-ocr-synth. Заголовки, экранный рендер, «шакалирование», аугментация начертания.
  • Реальные строки — 258 тыс. строк газетной периодики с точной разметкой, извлечённых из born-digital PDF: текстовый слой даёт эталон бесплатно. Сами данные не публикуются (авторские права на газетный архив), но скрипт извлечения лежит в src/.
  • Страницы для детектора — 3220 полос из 274 выпусков; рамки строк взяты из того же текстового слоя. 9 выпусков, из которых нарезана оценочная выборка, отложены целиком.

2. Распознаватель

python src/train_rec.py --arch crnn \
    --data data_v6,real_train \
    --fonts fonts_manifest.json \
    --steps 9000 --bs 96 --workers 24 --lr 3e-4 \
    --out runs/GEN_v8

CRNN: свёрточный ствол, двунаправленный LSTM, CTC. Высота входа 48 px, ширина произвольная; батчи собираются по близкой ширине. OneCycleLR, ~26 минут на одной H200.

Две вещи, без которых не работает:

  • margin_jitter — строка обрезается до рамки чернил и получает случайное поле 0…0.42 её высоты, на 75% примеров. Без этого модель зависит от того, насколько плотно обрезан кроп: на тех же изображениях CER менялся с 10.2% до 2.9% от одного добавления поля, потому что край кропа принимался за точку.
  • Разбиение батчей по ширине — иначе короткие строки добиваются нулями, CTC читает добивку и дописывает символы.

3. Детектор строк

DET_SCALE=0.6,2.2 DET_AUG=0.70,0.18,0.12 \
python src/train_det.py \
    --data det_train --val det_val \
    --out runs/DET_v4 --steps 14000 --bs 16 --size 640 --lr 2e-3

Компактный DB (differentiable binarization) с нуля: предобученных весов взять неоткуда. Карта учится по сжатым рамкам — между строками и колонками остаётся зазор, и обученный порог разворачивает пятно обратно в полную строку. Шаг выхода 2, а не 4 как в оригинале: в газете строка занимает 10–14 px, при шаге 4 сжатая рамка вырождается в один пиксель.

Оценка идёт по ходу обучения на отложенных полосах, лучший чекпоинт сохраняется по F1 — качество не растёт монотонно, и last.pt затирает лучший результат.

4. Постобработка словарём

Пакет sakhaspell даёт лексикон и биграммную модель — языковой приор, которого у CTC нет. Подключённый в лоб, как исправитель текста, он вредит: на 5400 реальных строках CER 0.241% → 0.894%, испорчено 838 строк против 44 улучшенных. Он не видит изображение и правит имена, русские вставки и обрывки слов на переносах.

Поэтому словарь только предлагает кандидатов, а решает картинка: для каждой замены считается CTC log-prob кандидата, и замена проходит, если выигрыш языковой модели перевешивает потерю по изображению. При весе 1.0 — 36 улучшенных строк против одной ухудшенной на 2100, минус 8% ошибок там, где распознаватель слаб. См. src/lm_rescore.py.

Метрики

Распознаватель, 5400 отложенных строк газеты с точным эталоном:

CER WER пропуск ҕҥөһү
эта модель 0.22% 2.05% 0.3%
она же, только на синтетике 0.95% 1.7%
CRNN на синтетике, 521 шрифт 1.26% 2.2%
SVTR на синтетике 2.31% 2.4%

Синтетические отложенные выборки: книга 0.12%, газета 3.79%, незнакомые гарнитуры 0.89% и 0.47%.

Детектор, 24 отложенные полосы (10 тыс. строк эталона, IoU 0.5):

полнота точность порядок чтения
морфологическая сегментация 62.9% 75.1% 89.8%
детектор 93.5% 94.9% 96.9%

Сквозной результат на книжной странице (скриншот, не газета): CER 0.4%.

Чего ждать не стоит

  • Рукописи не поддерживаются вовсе.
  • Крупный книжный кегль (строка выше ~35 px) — слабое место: модель училась на строках 6–22 px, и рамка садится на базовую линию, срезая выносные элементы букв. Лечится расширением рамки по вертикали на 0.3 высоты строки, это уже сделано в src/detect.py.
  • Советские книжные гарнитуры (Литературная, Школьная, Академическая, Журнальная) в обучении не представлены — их нет в открытом доступе, а именно ими набраны якутские книги 1940–1990-х.
  • Газета 1940 года: колонки вплотную, межколоночные линейки прокрашены — разбор страницы там не работает.

Лицензия

CC BY 4.0. Веса получены в том числе на газетном архиве, который сам не публикуется; синтетическая часть данных открыта: lab-ii/sakha-ocr-synth.

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

Dataset used to train lab-ii/sakha-ocr