OCR
OCR находит текст на изображении и читает его. В LibreYOLO это задача ocr: она возвращает по одному четырёхточечному полигону и одной распознанной строке на каждую текстовую область, в порядке чтения.
Определение
Задача ocr делает за один вызов две вещи: находит на изображении все текстовые
области и распознаёт их текст. Области возвращаются четырёхточечными полигонами,
а не рамками по осям, потому что текст в сцене часто повёрнут, — и в порядке
чтения: сверху вниз, затем слева направо.
Предсказание заполняет result.ocr — структуру OCRRegions. В .data лежит
float-массив (N, 4, 2) с полигонами в пикселях исходного изображения, углы
идут слева сверху, справа сверху, справа снизу, слева снизу; .texts — список
из N распознанных строк; .conf — оценка уверенности распознавания по каждой
области, а .det_conf — оценка детекции; .xyxy даёт прямоугольник по осям,
описанный вокруг каждого полигона. Четырёхугольники — настоящие полигоны,
поэтому result.boxes они не заполняют. Срез OCRRegions переносит
распознанный текст и оба массива оценок вместе с геометрией.
Модели
За ocr отвечают два семейства.
PP-OCRv5 — специализированный пайплайн: детектор с
дифференцируемой бинаризацией находит четырёхугольники с текстом, а
распознаватель SVTR/CTC их читает, причём обе стадии упакованы в один файл .pt
вместе с набором символов распознавателя. Он поставляется в двух уровнях — более
лёгком для CPU и серверном для более высокой точности, — а один словарь
покрывает упрощённый и традиционный китайский, английский, японский и пиньинь.
SenseNova-Vision решает задачу OCR иначе: слова
генерируются как размеченный тегами текст тем же чекпойнтом на 7B, который
обслуживает остальные шесть его задач; загружается он вызовом
LibreVLM("sensenova-vision", task="ocr"). Ему нужен extra sensenova, а его
веса разрешены только для некоммерческого использования; лицензия указана на
странице модели.
Предсказание
Веса скачиваются с Hugging Face при первом запуске и кэшируются локально.
from libreyolo import LibreYOLO, SAMPLE_IMAGE # Уровень t — более лёгкий из двух, рассчитан на CPU. SAMPLE_IMAGE# делает пример запускаемым; подставьте своё изображение с текстом.model = LibreYOLO("LibrePPOCRt-ocr.pt")result = model(SAMPLE_IMAGE) regions = result.ocrprint(len(regions), "regions")for text, score in zip(regions.texts, regions.conf): print(repr(text), float(score))from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibrePPOCRt-ocr.pt")result = model(SAMPLE_IMAGE) regions = result.ocrprint(regions.data.shape) # полигоны (N, 4, 2), TL TR BR BLprint(regions.xyxy) # прямоугольники по осям вокруг этих полигоновprint(regions.det_conf) # оценка детекции, отдельно от .confimport numpy as npfrom libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibrePPOCRt-ocr.pt")result = model(SAMPLE_IMAGE) # Индексируйте позициями, а не булевой маской: срез переносит# распознанный текст и оба массива оценок вместе с геометрией.regions = result.ocr.numpy()keep = regions[np.flatnonzero(regions.conf >= 0.9)]print(keep.texts)PP-OCRv5 запускает детекцию с фиксированным ограничением по длинной стороне, а
затем распознаёт вырезанные области батчами; rec_batch задаёт, сколько
фрагментов проходит через распознаватель за один прямой проход. Источники из
нескольких изображений обрабатываются последовательно: двухстадийный пайплайн не
собирает батчи из разных изображений. Про источники, стриминг и обработку
результатов — в разделе предсказание.
Формат датасета
Разметка OCR — это один файл JSONL на сплит, по одному JSON-объекту на изображение, рядом с самими изображениями.
my-ocr-dataset/
images/
val/receipt.jpg
labels/
val.jsonlКаждая строка называет изображение и перечисляет его области:
{"image": "receipt.jpg", "regions": [{"polygon": [[10, 12], [118, 14], [117, 40], [9, 38]], "text": "TOTAL 12.50"}]}polygon — четырёхугольник, заданный четырьмя точками в абсолютных пиксельных
координатах: слева сверху, справа сверху, справа снизу, слева снизу. Область,
текст которой прочитать нельзя, помечается как "text": "###" — это соглашение
don't-care из ICDAR: она исключается из оценки распознавания, а предсказание,
которое с ней пересекается, игнорируется, а не засчитывается как
ложноположительное.
Достаточно передать корневой каталог как data=. Альтернатива — YAML датасета:
path плюс необязательные имена каталогов images и labels, а nc: 1 и
names: {0: text} служат заглушками схемы, поскольку OCR-модель возвращает
Results.ocr, а не детекции. Полный контракт — в разделе
форматы датасетов.
Обучение
Ни у одного из OCR-семейств нет реализации обучения: train() в обоих случаях
выбрасывает NotImplementedError, а поддержка OCR покрывает только предсказание
и валидацию. На странице PP-OCRv5 указаны оригинальный код обучения под
Apache-2.0 и скрипт конвертации, который возвращает дообученный чекпойнт обратно
в LibreYOLO.
Валидация
val() оценивает весь пайплайн целиком, детекцию и распознавание вместе,
сопоставляя предсказанные полигоны с эталонными (ground truth) один к одному при
IoU выше 0.5.
from libreyolo import LibreYOLO model = LibreYOLO("LibrePPOCRt-ocr.pt")metrics = model.val(data="my-ocr-dataset") print(metrics["metrics/det_precision"], metrics["metrics/det_recall"])print(metrics["metrics/det_hmean"])print(metrics["metrics/e2e_f1"]) # fitnessprint(metrics["metrics/rec_1-NED"])metrics/det_precision, metrics/det_recall и metrics/det_hmean оценивают
только локализацию: для совпадения достаточно пересечения полигонов, что бы ни
было в распознанном тексте. metrics/e2e_precision, metrics/e2e_recall и
metrics/e2e_f1 добавляют чтение: для совпадения нужно то же пересечение
полигонов и в точности тот же текст после нормализации NFKC и удаления
пробелов, причём сравнение учитывает регистр. metrics/e2e_f1 — это ещё и
fitness, число, по которому выбирается лучший чекпойнт.
metrics/rec_1-NED оценивает распознаватель отдельно, по тем парам, которые уже
сопоставила детекция: единица минус нормализованное расстояние редактирования,
поэтому строка с ошибкой в один символ получает почти 1 там, где сквозная F1
даёт ей 0.
Экспорт
Для этой задачи нет ни одного доступного формата экспорта. PP-OCRv5 — две сети,
работающие вместе, а не один трассируемый граф, и export() выбрасывает
исключение для любого формата в обоих семействах. Чтобы развернуть модель вне LibreYOLO,
дообучите её в оригинальном проекте и используйте его же путь развёртывания.