← Back to Articles

Делаем сервис распознавания паспорта РФ в текст

Недавно я собирал небольшой OCR-сервис для распознавания паспорта РФ

Зачем это нужно? В бухгалтерии устали вбивать все данные паспортов / СНИЛС / медкнижек вручную. Что ж, будем исправлять.

Задача на первый взгляд звучит просто: загружаешь фото или скан паспорта, а на выходе получаешь буковки от фамилии до «кем выдан»- про серию и номер тоже не забыть.

Погнали!

Итак. OCR может вернуть какой-то текст.

Но на практике оказалось, что «просто распознать текст» и «достать из паспорта нормальные поля» - это две разные задачи. И вторая оказалась сложнее.

Бухгалтерии нужен не текст, а аккуратная структура, которую потом нужно дать в UI для проверки ручками и отправить после в API / 1С / CRM.

Первая версия: просто прогнать через PaddleOCR

В качестве OCR-движка я взял PaddleOCR с русским языком.

На старте идея максимально простая:

  1. Пользователь загружает файл.
  2. Сервис приводит его к нормальному формату.
  3. PaddleOCR распознаёт текст.
  4. Парсер пытается вытащить из текста паспортные поля.

На вход я разрешил базовые форматы: jpg, jpeg, png, pdf.

Даже если пользователь загружает обычную картинку, я всё равно нормализую её во внутренний PNG. Это удобно, потому что дальше весь пайплайн работает с одним форматом.

И тут начались проблемы.

Первая проблема: OCR возвращает не документ, а набор кусочков

PaddleOCR не отдаёт текст сразу красиво. Он отдаёт набор найденных текстовых блоков, т.е. сам текст, confidence score, координаты блока, bbox и прочую лабуду.

То есть на выходе получается не:

Фамилия: Иванов
Имя: Иван

А примерно хаотичный список OCR-боксов, которые ещё как-то надо собрать обратно в строки. Ведь в UI бухгалтер должен глазками всё проверить и нажать кнопочку ОК.

Поэтому отдельной частью пришлось сделать преобразование OCR-результата в свою внутреннюю структуру:

  • текст блока;
  • score;
  • координаты;
  • центр по X/Y;
  • ширина и высота.

После этого блоки группируются в строки: сначала сортировка сверху вниз, потом объединение близких по вертикали элементов, а внутри строки- сортировка слева направо.

Почему всё так сложно, спросите вы? Ведь паспорт- это шаблон!

Проблема номер два: думать, что текст можно парсить только regex-ами

Да, сначала кажется: ну паспорт же шаблонный документ. Значит, можно найти регулярками даты, код подразделения, номер, ФИО и баста.

Но OCR рвёт логические блоки. Например, дата выдачи может быть в той же строке, что и подпись «Дата выдачи», а может быть строкой выше или ниже.

То же самое с датой рождения, кодом подразделения, полом и местом рождения. Всё потому, что при печати паспорта у нас шаблон не всегда лежит идеально ровно.

Да, есть боксы для каждого текста. Вот именно их нам и надо найти! Поэтому парсер пришлось делать свой для каждого отдельного бокса и экспериментировать с разным расположением букв / цифр разных паспортов.

Например:

  • дата выдачи ищется рядом с якорем «Дата выдачи»;
  • дата рождения ищется по якорю «рож...», а если не получилось- берётся вторая найденная дата;
  • код подразделения ищется по маске 000-000;
  • пол ищется по вариантам МУЖ., МУЖ, МУЖСКОЙ, ЖЕН., ЖЕН, ЖЕНСКИЙ;
  • место рождения и «кем выдан» собираются отдельно, потому что они могут занимать несколько строк.

Проблема номер три: ФИО пришлось искать не только по тексту, но и по геометрии

Отдельная боль - это ФИО. Я хз, почему оно во всех паспортах скачет по высоте строки и почему его нельзя печатать строго в миллиметраже на одной линии? Ну ладно, кто я такой, чтобы задавать эти вопросы.

Поэтому для ФИО пришлось добавить поиск по геометрии OCR-боксов.

Логика такая:

  1. Находим OCR-бокс с якорем поля: «ФАМИЛИЯ», «ИМЯ», «ОТЧЕСТВО».
  2. Смотрим кандидатов рядом.
  3. Сначала проверяем значение справа на той же строке.
  4. Потом проверяем значение над подписью.
  5. Отбрасываем мусор, цифры и служебные слова.
  6. Выбираем наиболее похожий вариант.

Вообще-то паспорт - это не просто про текст. Это про строгую геометрию. И по сути мы везде можем использовать геометрический поиск OCR-боксов.

Проблема номер четыре: серия и номер паспорта

Ну тут и так в принципе всё понятно. Серия и номер сбоку справа, да ещё и вертикально. Т.е. напечатано в другой ориентации. Обычный OCR пытается её прочитать как горизонтальный текст, и вместо цифр получаются какие-то новые буквы алфавита.

Поэтому для серии / номера пришлось пилить отдельный пайплайн:

  1. Найти правую вертикальную зону паспорта.
  2. Вырезать её как отдельный crop.
  3. Повернуть crop в обе стороны.
  4. Отдельно прогнать OCR по этим вариантам.
  5. Попытаться собрать серию по шаблонам: две цифры + две цифры, либо серия рядом с номером.

Вывод сей «басни» такой: паспорт нельзя нормально распознать одним OCR-запросом. У разных зон документа разное поведение, и некоторые поля требуют отдельной обработки.

Проблема номер пять: паспорт есть, сканер есть, но никто не сказал, как правильно сканировать

Подумаешь - мелочь. Истерический смех.

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

Поэтому пишем препроцессор для каждого паспорта.

После первого распознавания парсер смотрит, сколько критичных полей не найдено. Критичные поля - это дата выдачи, дата рождения, пол, ФИО, кем выдан, место рождения и т.п. То есть все наши основные поля.

Если таких ошибок слишком много- больше половины, значит документ, скорее всего:

  • перевёрнут;
  • лежит боком;
  • очень плохо читается;
  • или это вообще не паспорт? О_о

Тогда мы пробуем повернуть изображение на 180, 90 и даже 270 градусов, снова распознать и сравнить результаты.

Лучшим считается тот вариант, где меньше ошибок и больше заполненных полей.

Итоговый смысл на самом деле простой: если при первом прогоне СОВСЕМ всё плохо, значит, нам надо повернуть документ.

Проблема номер шесть: немного улучшаем качество для движка распознавания

Не сказать, чтобы прям проблема, просто стандартный этап пайплайна перед тем, как засунуть фотку в OCR-модель.

Например: превратить в ЧБ, сделать чуть контрастнее, поднять резкость и ещё некоторые другие фильтры, которые я тестил и собирал непосредственно для паспорта.

Итоговый препроцессор получился таким:

  1. Перевод в grayscale.
  2. Приведение размера по длинной стороне.
  3. Лёгкое шумоподавление.
  4. Мягкое усиление локального контраста.
  5. Очень аккуратный unsharp.
  6. Сохранение в grayscale PNG.

То есть цель была не «сделать красиво для человека», а аккуратно улучшить читаемость для PaddleOCR и не сломать буквы.

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

Препроцессинг- не всегда благо. Иногда исходное изображение OCR понимает лучше, чем «улучшенное».

Логи - обязательны

Ну и конечно, логи обязательны. OCR без логов - это гадание на кофейной гуще.

Бухгалтер нам говорит, что не распознало паспорт. Но без логов мы вообще не поймём, что произошло. Поэтому пришлось прикрутить JSON-логи по ключевым событиям:

  • загрузка модуля;
  • нормализация файла;
  • запуск OCR;
  • группировка строк;
  • парсинг полей;
  • попытки поворота;
  • запуск препроцессора;
  • выбор лучшего результата;
  • финальная сводка.

Это сильно упрощает отладку. Особенно когда работаешь не с одним идеальным тестовым паспортом, а с реальными фото, которые могут быть кривыми, тёмными, с бликами и т.д.

More articles