Ё-моё: почему с русским текстом так сложно
Вы когда-нибудь пробовали распознать сканер паспорта, где буква «ё» сливается с «е», а шрифт в графе «подпись» такой, будто дизайнер пил кофе с формалином? Я перепробовал десятки open-source OCR-движков на реальных русских документах — от нотариальных доверенностей до товарных накладных. Результат: половина решений даёт грязь, вторую половину нужно вылизывать предобработкой.
Проблема кириллицы не только в букве «ё». Это окончания, которые ломают n-граммовые модели, это шрифты вроде «гельветика-кириллица» с варварскими засечками, это старые сканы с трещинами на букве «ш». Англоязычные бенчмарки тут не помогут. Поэтому мы собрали датасет из 500 реальных изображений — паспорта, договоры, СНИЛС, квитанции — и прогнали через всех кандидатов.
Если вы уже сталкивались с ужасом распознавания рукописного текста — советую прочитать нашу статью «Рукописный кошмар и формулы, которые никто не понимает». Там те же выводы: современные VLM часто пасуют перед непечатными символами.
Кандидаты на ринге
Отобрали пять движков, которые можно запустить локально (без облаков) и которые заявляют поддержку русского языка. Версии — актуальные на апрель 2026:
| Движок | Версия | Основа | Дополнительно |
|---|---|---|---|
| Tesseract | 5.5 | LSTM + легаси | pytesseract, обученный rus |
| EasyOCR | 1.7 | CRNN + Attention | дообучен на русском |
| PaddleOCR | 4.5 | MobileNetV3 + PP-OCRv4 | русская модель (Cyrillic) |
| Surya OCR | 2.2 | Transformer (ViT + text decoder) | многоязычный |
| TrOCR (Microsoft) | 3.1 | Encoder-decoder (BEiT + RoBERTa) | дообучен на кириллице |
Методология — без неё это холивар
Берём 500 изображений. Делим на типы: печатные (шрифт Times New Roman, Arial, рукописные имитации), сканы 300 dpi, фотки с телефона (12 Мп). Для каждого считаем Character Error Rate (CER) и Word Error Rate (WER). Исправляем регистр — для русских имён и фамилий это критично. Запускаем всё на одном железе: CPU: AMD Ryzen 9 7950X, GPU: NVIDIA RTX 4090 (24 GB).
Ниже — код для расчёта метрик. Мы использовали библиотеку jiwer и кастомную функцию для нормализации (убрали лишние пробелы, привели к нижнему регистру, но сохранили «ё»).
import jiwer
def normalize_russian(text):
# оставляем ё, убираем пунктуацию, лишние пробелы
import re
text = text.lower()
text = re.sub(r'[^а-яё0-9\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def compute_cer_wer(reference, hypothesis):
ref_norm = normalize_russian(reference)
hyp_norm = normalize_russian(hypothesis)
cer = jiwer.cer(ref_norm, hyp_norm)
wer = jiwer.wer(ref_norm, hyp_norm)
return cer, wer
Важный нюанс: стандартный jiwer считает WER по словам, но для русского языка лучше считать по лексемам с учётом окончаний. Мы экспериментировали с лемматизацией — метрика менялась на 2-3%, но для бенчмарка оставили raw.
Результаты: кто читает «ё», а кто — слоги
Тесты проводились на трёх подмножествах. Вот средние показатели по всему датасету:
| Движок | CER (%) | WER (%) | Время на кадр (сек) |
|---|---|---|---|
| Tesseract 5.5 | 8.3 | 22.1 | 0.45 |
| EasyOCR 1.7 | 5.1 | 14.0 | 1.20 |
| PaddleOCR 4.5 | 3.8 | 9.2 | 0.30 |
| Surya OCR 2.2 | 4.2 | 10.5 | 2.10 |
| TrOCR 3.1 | 10.1 | 25.3 | 1.80 |
Лидер по точности — PaddleOCR 4.5. При этом он быстрее всех на CPU (благодаря легковесной архитектуре MobileNet). EasyOCR показал достойные результаты, но проигрывает в скорости. Tesseract — всё ещё неплох, если документ идеально прямолинейный, но малейший перекос или сжатие — и вы получаете кашу. TrOCR разочаровал: для русского явно требовался дообучающий датасет, которого в открытом доступе нет.
Схожий вывод мы сделали в тестировании арабского OCR: PaddleOCR стабильно обходит конкурентов на нелатинских алфавитах. Видимо, китайские разработчики лучше понимают проблемы иероглифического письма, которые коррелируют с кириллицей.
Почему PaddleOCR выиграл — и где его слабые места
Секрет PaddleOCR — в специально обученной русской языковой модели (PP-OCRv4 Cyrillic). Она учитывает окончания, букву «ё» и редкие сочетания. Плюс встроенный детектор текста (DB) отлично находит строки даже на зашумлённых квитанциях.
Но есть и недостатки:
- Проблемы с курсивом и наклонными шрифтами — детектор иногда рвёт строку на куски.
- Жирные шрифты (Bold) — модель путает «Т» и «Ш» при сильном насыщении.
- Сложная предобработка: если не выставить пороговые значения binarization, точность падает.
В свою очередь, EasyOCR проще в интеграции: import easyocr; reader = easyocr.Reader(['ru']); reader.readtext('img.jpg'). Но он медленнее и чаще ошибается на специфических символах (№, ₽, §).
Как мы предобрабатывали изображения — ключевой этап
Без хорошей предобработки даже лучший OCR даст грязь. Мы применяли универсальный пайплайн:
- Выравнивание — поворот по контурам текста (использовали OpenCV minAreaRect).
- Перевод в серый + бинаризация — метод Otsu для светлых фонов, Sauvola для тёмных.
- Удаление шума — медианный фильтр (kernel 3x3).
- Увеличение контраста — CLAHE.
- Определение языка — если документ частично на латинице, добавляли второй проход.
Полный код нашего препроцессора доступен в репозитории (гитхаб-ссылка будет ниже). Но вот ключевая функция:
import cv2
import numpy as np
def preprocess_image(image_path):
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# выравнивание
coords = np.column_stack(np.where(gray > 0))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = 90 + angle
h, w = gray.shape[:2]
M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1.0)
rotated = cv2.warpAffine(gray, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
# бинаризация
_, binary = cv2.threshold(rotated, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# медианный шум
denoised = cv2.medianBlur(binary, 3)
return denoised
Постобработка: превращаем «Ш» в «Ш» и «о» в «о»
Даже PaddleOCR ошибается. Мы добавили словарь русских слов (150k лемм) и простой spellchecker на основе trie. Это снизило WER ещё на 1-2%.
Пример: EasyOCR на слове «распознавание» выдаёт «распазнавание». Исправляем через yaspeller или pymorphy2.
from pymorphy2 import MorphAnalyzer
morph = MorphAnalyzer()
def correct_word(word):
# если слово есть в словаре, не трогаем
if word in vocab:
return word
# иначе решаем через pymorphy
p = morph.parse(word)[0]
return p.normal_form # осторожно: иногда ломает окончания
Не делайте так в production с именами собственными! Фамилия «Багров» может превратиться в «багор». Лучше использовать whitelist полей ФИО.
Выбор сценария: PaddleOCR vs EasyOCR vs Tesseract
В зависимости от задачи:
- Массовая оцифровка договоров — PaddleOCR 4.5. Скорость + точность = золотая середина. На CPU обрабатывает до 120 страниц/мин.
- Мобильное приложение — EasyOCR (или её облегчённая версия EasyOCR Lite). Tesseract тоже можно, но он требует статической компиляции.
- Низкое качество сканов — Tesseract после агрессивной предобработки показывает неплохие результаты, но PaddleOCR всё равно лучше.
- Высокая точность на сложном форматировании — советую посмотреть на PaddleOCR-VL в llama.cpp — тот же PaddleOCR, но на трансформерах, с пониманием структуры.
Будущее русского OCR: что изменится к 2027
Уже сейчас видно, что классические OCR-движки уступают место VLM (Vision Language Models). Модели вроде GLM-OCR или Nemotron OCR v2 умеют не только читать символы, но и понимать контекст: нумерованные списки, таблицы, сноски. Для русского языка это особенно важно — окончания осмысленно исправляются на основе грамматики.
Мы протестировали Nemotron OCR v2 на русских документах — он показал CER 3.0%, но на GPU. Зато время обработки 0.5 сек на страницу при батче 8 — отличный кандидат для пайплайнов.
Если вам нужна максимальная точность без железа — подождите полгода. Легковесные VLM-модели подтянутся к open-source. А пока PaddleOCR остаётся королём русских документов.
Ошибки, которые я совершил первым делом
Пара граблей, чтобы вы не наступали:
- Не обновлял языковые данные Tesseract. По умолчанию в Tesseract 5.5 для русского — модель 2022 года. Скачайте свежий
rus.traineddataс GitHub — WER падает с 25% до 15%. - Доверял стандартным датасетам. Классический MNIST не отражает реальность. Наши 500 реальных документов показали другую картину.
- Забывал про регистр. EasyOCR и PaddleOCR по умолчанию выдают lower case. Для паспортов регистр критичен. Пришлось дообучать head с готовым датасетом ФИО.
В итоге — наш внутренний рейтинг: PaddleOCR 4.5 (9/10), EasyOCR 1.7 (7/10), Surya OCR 2.2 (6/10), Tesseract 5.5 (5/10), TrOCR 3.1 (3/10).
Но если ваш документ — рукопись с формулами, прочитайте ту самую статью про рукописный кошмар — там всё станет ясно.