Зачем это нужно? В бухгалтерии устали вбивать все данные паспортов / СНИЛС / медкнижек вручную. Что ж, будем исправлять.
Задача на первый взгляд звучит просто: загружаешь фото или скан паспорта, а на выходе получаешь буковки от фамилии до «кем выдан»- про серию и номер тоже не забыть.
Погнали!
Итак. OCR может вернуть какой-то текст.
Но на практике оказалось, что «просто распознать текст» и «достать из паспорта нормальные поля» - это две разные задачи. И вторая оказалась сложнее.
Бухгалтерии нужен не текст, а аккуратная структура, которую потом нужно дать в UI для проверки ручками и отправить после в API / 1С / CRM.
Первая версия: просто прогнать через PaddleOCR
В качестве OCR-движка я взял PaddleOCR с русским языком.
На старте идея максимально простая:
- Пользователь загружает файл.
- Сервис приводит его к нормальному формату.
- PaddleOCR распознаёт текст.
- Парсер пытается вытащить из текста паспортные поля.
На вход я разрешил базовые форматы: jpg, jpeg, png, pdf.
Даже если пользователь загружает обычную картинку, я всё равно нормализую её во внутренний PNG. Это удобно, потому что дальше весь пайплайн работает с одним форматом.
И тут начались проблемы.
Первая проблема: OCR возвращает не документ, а набор кусочков
PaddleOCR не отдаёт текст сразу красиво. Он отдаёт набор найденных текстовых блоков, т.е. сам текст, confidence score, координаты блока, bbox и прочую лабуду.
То есть на выходе получается не:
Фамилия: Иванов
Имя: Иван
А примерно хаотичный список OCR-боксов, которые ещё как-то надо собрать обратно в строки. Ведь в UI бухгалтер должен глазками всё проверить и нажать кнопочку ОК.
Поэтому отдельной частью пришлось сделать преобразование OCR-результата в свою внутреннюю структуру:
- текст блока;
- score;
- координаты;
- центр по X/Y;
- ширина и высота.
После этого блоки группируются в строки: сначала сортировка сверху вниз, потом объединение близких по вертикали элементов, а внутри строки- сортировка слева направо.
Почему всё так сложно, спросите вы? Ведь паспорт- это шаблон!
Проблема номер два: думать, что текст можно парсить только regex-ами
Да, сначала кажется: ну паспорт же шаблонный документ. Значит, можно найти регулярками даты, код подразделения, номер, ФИО и баста.
Но OCR рвёт логические блоки. Например, дата выдачи может быть в той же строке, что и подпись «Дата выдачи», а может быть строкой выше или ниже.
То же самое с датой рождения, кодом подразделения, полом и местом рождения. Всё потому, что при печати паспорта у нас шаблон не всегда лежит идеально ровно.
Да, есть боксы для каждого текста. Вот именно их нам и надо найти! Поэтому парсер пришлось делать свой для каждого отдельного бокса и экспериментировать с разным расположением букв / цифр разных паспортов.
Например:
- дата выдачи ищется рядом с якорем «Дата выдачи»;
- дата рождения ищется по якорю «рож...», а если не получилось- берётся вторая найденная дата;
- код подразделения ищется по маске
000-000; - пол ищется по вариантам
МУЖ.,МУЖ,МУЖСКОЙ,ЖЕН.,ЖЕН,ЖЕНСКИЙ; - место рождения и «кем выдан» собираются отдельно, потому что они могут занимать несколько строк.
Проблема номер три: ФИО пришлось искать не только по тексту, но и по геометрии
Отдельная боль - это ФИО. Я хз, почему оно во всех паспортах скачет по высоте строки и почему его нельзя печатать строго в миллиметраже на одной линии? Ну ладно, кто я такой, чтобы задавать эти вопросы.
Поэтому для ФИО пришлось добавить поиск по геометрии OCR-боксов.
Логика такая:
- Находим OCR-бокс с якорем поля: «ФАМИЛИЯ», «ИМЯ», «ОТЧЕСТВО».
- Смотрим кандидатов рядом.
- Сначала проверяем значение справа на той же строке.
- Потом проверяем значение над подписью.
- Отбрасываем мусор, цифры и служебные слова.
- Выбираем наиболее похожий вариант.
Вообще-то паспорт - это не просто про текст. Это про строгую геометрию. И по сути мы везде можем использовать геометрический поиск OCR-боксов.
Проблема номер четыре: серия и номер паспорта
Ну тут и так в принципе всё понятно. Серия и номер сбоку справа, да ещё и вертикально. Т.е. напечатано в другой ориентации. Обычный OCR пытается её прочитать как горизонтальный текст, и вместо цифр получаются какие-то новые буквы алфавита.
Поэтому для серии / номера пришлось пилить отдельный пайплайн:
- Найти правую вертикальную зону паспорта.
- Вырезать её как отдельный crop.
- Повернуть crop в обе стороны.
- Отдельно прогнать OCR по этим вариантам.
- Попытаться собрать серию по шаблонам: две цифры + две цифры, либо серия рядом с номером.
Вывод сей «басни» такой: паспорт нельзя нормально распознать одним OCR-запросом. У разных зон документа разное поведение, и некоторые поля требуют отдельной обработки.
Проблема номер пять: паспорт есть, сканер есть, но никто не сказал, как правильно сканировать
Подумаешь - мелочь. Истерический смех.
Нам проще машину научить крутить сальто паспортом, чем выдать жёсткую инструкцию бухгалтеру: сканируем строго вверх головой, один лист паспорта на одной странице, желательно в хорошем разрешении.
Поэтому пишем препроцессор для каждого паспорта.
После первого распознавания парсер смотрит, сколько критичных полей не найдено. Критичные поля - это дата выдачи, дата рождения, пол, ФИО, кем выдан, место рождения и т.п. То есть все наши основные поля.
Если таких ошибок слишком много- больше половины, значит документ, скорее всего:
- перевёрнут;
- лежит боком;
- очень плохо читается;
- или это вообще не паспорт? О_о
Тогда мы пробуем повернуть изображение на 180, 90 и даже 270 градусов, снова распознать и сравнить результаты.
Лучшим считается тот вариант, где меньше ошибок и больше заполненных полей.
Итоговый смысл на самом деле простой: если при первом прогоне СОВСЕМ всё плохо, значит, нам надо повернуть документ.
Проблема номер шесть: немного улучшаем качество для движка распознавания
Не сказать, чтобы прям проблема, просто стандартный этап пайплайна перед тем, как засунуть фотку в OCR-модель.
Например: превратить в ЧБ, сделать чуть контрастнее, поднять резкость и ещё некоторые другие фильтры, которые я тестил и собирал непосредственно для паспорта.
Итоговый препроцессор получился таким:
- Перевод в grayscale.
- Приведение размера по длинной стороне.
- Лёгкое шумоподавление.
- Мягкое усиление локального контраста.
- Очень аккуратный unsharp.
- Сохранение в grayscale PNG.
То есть цель была не «сделать красиво для человека», а аккуратно улучшить читаемость для PaddleOCR и не сломать буквы.
Препроцессор тоже не используется всегда. Сначала сервис пробует обычное распознавание. Если документ ровный, но критичных ошибок всё ещё много, только тогда включается fallback через препроцессинг.
Препроцессинг- не всегда благо. Иногда исходное изображение OCR понимает лучше, чем «улучшенное».
Логи - обязательны
Ну и конечно, логи обязательны. OCR без логов - это гадание на кофейной гуще.
Бухгалтер нам говорит, что не распознало паспорт. Но без логов мы вообще не поймём, что произошло. Поэтому пришлось прикрутить JSON-логи по ключевым событиям:
- загрузка модуля;
- нормализация файла;
- запуск OCR;
- группировка строк;
- парсинг полей;
- попытки поворота;
- запуск препроцессора;
- выбор лучшего результата;
- финальная сводка.
Это сильно упрощает отладку. Особенно когда работаешь не с одним идеальным тестовым паспортом, а с реальными фото, которые могут быть кривыми, тёмными, с бликами и т.д.