OCR распознал таблицу, но перепутал строки: как проверить связь артикула и суммы

OCR верно прочитал два артикула и две суммы, но итоговая таблица связала их неправильно. У товара A появилась цена товара B и наоборот. Проверка общей суммы прошла: слагаемые просто поменялись местами. Такая ошибка опасна для загрузки накладных, где смысл строки задаётся связью ячеек, а не наличием всех нужных слов на странице.

Список распознанных текстов нужно превратить в структуру таблицы. Для простого прямого бланка помогает геометрия: артикул и сумма одной строки занимают близкие вертикальные интервалы. При этом нельзя просто отсортировать все блоки по порядку, в котором их вернул OCR. Движок может обходить сначала один столбец, затем другой, а небольшие наклоны изменят порядок центров.

Сопоставим ячейки по вертикальному пересечению

В примере координаты заданы в пикселях одной уже выровненной страницы. Артикулы стоят слева, суммы справа. Список сумм намеренно передан в обратном порядке. Для каждой пары считаем длину общего вертикального участка и делим на меньшую высоту ячеек. Это локальная мера совпадения строк, а не двумерный IoU рамок: горизонтально разные столбцы как раз не пересекаются.

import numpy as np
from scipy.optimize import linear_sum_assignment
articles = [{"text": "A-10", "box": [10, 10, 50, 20]},
            {"text": "B-20", "box": [10, 30, 50, 40]}]
amounts = [{"text": "200.00", "box": [80, 31, 120, 41]},
           {"text": "100.00", "box": [80, 11, 120, 21]}]
def vertical_overlap(a, b):
    shared = max(0, min(a[3], b[3])-max(a[1], b[1]))
    return shared/min(a[3]-a[1], b[3]-b[1])
score = np.array([[vertical_overlap(a["box"], b["box"])
                   for b in amounts] for a in articles])
left, right = linear_sum_assignment(score, maximize=True)
linked = []
for i, j in zip(left, right):
    if score[i, j] < .5:
        raise ValueError("ambiguous row")
    linked.append((articles[i]["text"], amounts[j]["text"]))
print("overlap", score.tolist())
print("linked", linked)
assert linked == [("A-10", "100.00"), ("B-20", "200.00")]

Матрица получает 0,9 для правильных пар и ноль для соседних строк. Назначение один к одному возвращает A-10 → 100.00 и B-20 → 200.00 независимо от исходного порядка списков. После сопоставления сохраняйте идентификаторы ячеек и рамки: это позволит показать спорную строку оператору и проверить источник каждой связи.

Где геометрического правила уже мало

Многострочное название товара может занимать высоту нескольких соседних сумм. Объединённые ячейки, наклон и перенос таблицы на следующую страницу тоже нарушают простое совпадение интервалов. Поэтому сначала выделяйте таблицу, выравнивайте страницу и определяйте столбцы. Если структура сложная, используйте модель распознавания таблиц либо правила конкретного бланка. Порог 0,5 в примере подходит только для показанного механизма и требует настройки на ваших документах.

Оптимальное назначение всегда старается найти пары, даже если все варианты плохие. Поэтому нужен порог допуска и возможность оставить ячейку несвязанной. В больших таблицах удобно добавлять фиктивные варианты «нет пары» или строить соответствия только внутри уверенно выделенных строк. Иначе лишняя сумма из итога может занять место отсутствующей суммы товара и сдвинуть последующие связи.

Проверим смысл строки после геометрии

У строки могут быть количество, цена, скидка и сумма. Их арифметическая связь помогает обнаружить ошибку сопоставления, но сначала согласуйте округление и налоги. Нельзя молча переставлять суммы до совпадения общего итога: разные комбинации способны дать один результат. Если арифметика не сходится, отметьте строку для проверки вместе с исходными ячейками.

Оценивайте качество по полным строкам: артикул, количество и сумма должны относиться к одному товару. Отдельная точность OCR по символам эту ошибку не видит. В тестах нужны обратный порядок выдачи блоков, пропущенная сумма, многострочное название, лишний итог и граница страниц. Отчёт должен различать ошибочно связанную строку и строку без уверенной пары.

При исправлении загрузчика сохраните предыдущий JSON и новый вариант связей для одной и той же распознанной страницы. Так можно проверить именно реконструкцию таблицы, не смешивая её результат с новым запуском OCR. После локальной проверки уже оцените полный путь на реальных сканах с теми же правилами разметки.

Для наклонных строк используйте координаты после одного согласованного выравнивания страницы. Сравнение вертикальных интервалов рамок до и после разных преобразований не имеет общего смысла. При сохранении результата укажите систему координат и возможность возврата к исходному скану.

В курсе вы работаете с OCR и Doc-AI, подготовкой документов и передачей результата в JSON и базу данных. Следующая задача — собрать извлечение таблицы с проверяемыми связями ячеек и измерить качество на уровне готовой строки, которую использует учётная система. Откройте программу платного курса «Computer Vision Engineer: от детекции и Doc-AI до продакшна».

Почему порядок распознанных блоков требует проверки.

Продолжить на практике

  • Computer Vision Engineer: от детекции и Doc-AI до продакшна ↗

    Практический курс по Computer Vision. Соберёте продакшн-сервис: датасет → обучение (YOLO/RT-DETR, SAM), трекинг, OCR/Doc-AI, веб-приложение (UI+API) и мониторинг. Экспорт в ONNX/TensorRT/OpenVINO, батчинг/квантование под CPU/GPU. Плюс профессиональные блоки: mAP/IoU/Dice и калибровка доверия, QA разметки и аугментации, p95/стоимость, дрейф данных и алерты, guardrails (NMS/правила), A/B-тесты, Triton/gRPC/FastAPI.