Встраиваем распознавание документов от Smart Engines куда угодно за пять минут

—

от автора

Привет!

Мы, Smart Engines, многие годы занимаемся созданием ПО для распознавания документов, удостоверяющих личность, гибких форм, банковских карт, штрихкодов и так далее — всего более двух с половиной тысяч различных документов. С помощью нашего ПО клиенты решают самые разные задачи с различными сценариями использования (сканирование штрихкодов и банковских карт в мобильных приложениях и вебе, автоматизация заполнения шаблонов на основе распознанных ДУЛ, распознавание паспорта РФ). За всё время мы тысячу раз сталкивались с запросом “дайте какое-нибудь простое решение с API, которым нам можно было бы пользоваться”. Дело, конечно, хорошее, но функциональность у нашей системы очень богатая. Единый API, который подходил бы всем нашим заказчикам со своими разными задачами и разными сценариями использования, был бы переусложнен. В этой статье мы покажем пример того, как с помощью Docker, Python и нашего SDK самому реализовать простейшее решение для распознавания документов. 


Дисклеймер: в статье описан лишь пример использования библиотеки, предназначенный для оценки ресурсов на встраивание или быстрого развёртывания MVP!

Предлагаем такую схему — сервер внутри докера, реализующий некоторое API и линкующийся к нашей библиотеке распознавания, и клиент (не зависящий от нашей библиотеки) отправляющий на контролируемый вами сервер и получения результата распознавания. Это позволит иметь постоянно “живой” сервер распознавания, готовый к работе. Так, клиент отправляет серверу запрос в виде json, в котором лежат настройки для распознавания и сама картинка, потом ждёт ответ в виде json с полями распознанного документа. В самом простом случае всё сводится к отправке одной картинки на сервер.

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

settings = {    "mask": [mask], # тип распознаваемого документа, или "*" для режима автодетекции    "input": input, # файл с картинкой в бинарном виде    "signature": signature # персональная подпись клиента  }

В качестве “input” будет взятое из файловой системы изображение, передавать будем через питоновский asyncio:

reader, writer = await asyncio.open_connection(endpoint, port)  # Отправляем запрос  serialized_data = pickle.dumps(settings)  writer.write(struct.pack('<L', len(serialized_data)))  writer.write(serialized_data)  await writer.drain()  #Дальше ждём ответ и сохраняем его в файл:  request_size = struct.unpack('<L', await reader.readexactly(4))[0]  request_body = await reader.readexactly(request_size)  message = pickle.loads(request_body)  writer.close()  await save_result(message)

Механизм максимально простой, перейдём теперь непосредственно к серверу. Вначале разберёмся, как вообще использовать библиотеку распознавания, потом посмотрим, что делать с полученной из неё информацией.

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

sys.path.append(os.path.join(sys.path[0], './bindings/python/'))  sys.path.append(os.path.join(sys.path[0], './bin/'))  import pyidengine

Следом нужно инициализировать движок — под движком мы подразумеваем набор инструментов для распознавания. Библиотека поддерживает ленивую инициализацию, что позволяет экономить оперативную память, но увеличивает время распознавания первого кадра. Инициализация производится с помощью конфигурационного бандла, описывающего набор поддерживаемых системой документов. Инициализация движка должна проводиться один раз за всё время, т.к. в процессе распознавания библиотека переиспользует ранее инициализированные компоненты. 

global_engine = pyidengine.IdEngine.Create(bundle_path, lazy_init)

За само распознавание отвечает “сессия распознавания” — объект, в который нужно передать изображение, чтобы на выходе получить результат. Сессией это называется, потому что в один объект можно последовательно передать несколько изображений одного и того же документа. Тогда результат, получаемый после передачи каждого нового изображения, будет комбинированным по всем предыдущим распознаваниям из этой же сессии. Это можно использовать для повышения точности распознавания в случае, если у вас есть несколько фотографий одного и того же документа (особенно в случае неважного качества изображений), также на этом механизме строится распознавание из видеопотока. Для того чтобы настроить распознавание конкретных документов, существует отдельный механизм “опций сессии” — тоже объект, в котором хранятся настройки. 

# Создаём объект с опциями сессии  session_settings = global_engine.CreateSessionSettings()  # Сообщаем сессии, какие типы документов можно искать  for docmask in mask:    session_settings.AddEnabledDocumentTypes(docmask)  # Создаём сессию, передавая контейнер настроек и подпись клиента  session = global_engine.SpawnSession(session_settings, signature)

  Теперь осталось передать изображение в сессию распознавания и получить результат:

# Картинка JPEG, переданная в виде base64  file = base64.b64encode(buffer).decode("utf-8")  image = pyidengine.Image.FromBase64Buffer(file)  # Отдаем картинку на распознавание и получаем результат  session.Process(image)  current_result = session.GetCurrentResult()

Результат получили, теперь нужно вытащить из него необходимую нам информацию. В возвращаемом библиотекой результате распознавания куча разной информации: координаты шаблона документа (в пикселях) и всех полей, поля текстовые, поля с изображениями (стандарт — фото и подпись владельца документа, но можно достать даже вырезанные и проективно исправленные изображения текстовых полей), для каждого символа из текстовых полей есть набор альтернатив вместе с весами каждой альтернативы (позволяет работать с результатом распознавания уже вне нашей библиотеки), всяческие атрибуты (реальный DPI изображения, рассчитываемый исходя из физических размеров документа, форматы полей и так далее). В данном примере мы воспользуемся только содержимым текстовых полей:

def RecognitionResult(recog_result):    result = {}    result['docType'] = recog_result.GetDocumentType()    fields = {}    # Извлекаем текстовые поля    tf = recog_result.TextFieldsBegin()    while(tf != recog_result.TextFieldsEnd()):      info = tf.GetValue().GetBaseFieldInfo()      field = {        'name': tf.GetKey(),        'value': tf.GetValue().GetValue().GetFirstString().GetCStr(),        'isAccepted': info.GetIsAccepted()      }    # ... и извлекаем все остальное что нам нужно, если нужно ...      return result

На выходе получим json, который и перешлём обратно к клиенту.

Теперь можно переходить к сборке Docker-образа с библиотекой, Python-обёрткой и скриптом сервера.

Пару слов об обёртках: поскольку мы поставляем основную библиотеку в виде shared library, обёртки представляют собой набор из интерфейса на том языке, под который собираем обёртку, и библиотеки-транслятора вызовов из приложения в нашу библиотеку. Соответственно, для использования нужно подключить интерфейс к приложению и правильно слинковать все библиотеки. Для того, чтобы пользователь мог собрать обёртки под именно ту систему, которую он использует у себя в проде, мы кладём в SDK все инструменты для автогенерации Python-обёртки.

Для того чтобы избежать проблем с линковкой к разным версиям Python, мы будем использовать так называемый multi-stage build. Это позволит нам в первом образе установить необходимые зависимости для сборки, произвести саму сборку обёртки, а потом просто перенести результат в новый чистый образ.

# Берем образ Ubuntu и задаем его как образ для сборки  FROM ubuntu:22.04 AS builder  # Копируем наш SDK в этот образ  COPY "./IdEngineSDK" /home/idengine  # Зададим рабочий каталог  WORKDIR "/home/idengine/samples/idengine_sample_python/"  # Отключим интерактивный режим, иначе установка образа из Dockerfile не получится  ENV DEBIAN_FRONTEND noninteractive  # А теперь поставим необходимые зависимости  RUN set -xe </span>  && apt -y update  </span>  && apt -y install tcl </span>  && apt -y install build-essential </span>  && apt -y install make </span>  && apt -y install cmake </span>  && apt -y install python3-dev  # Эта команда компилирует модуль для питона для нашей библиотеки.  RUN ./build_python_wrapper.sh "../../bin" 3

Итак, модуль для Python собран. Теперь же нам нужно создать чистый образ и перенести все туда. Для этого нам нужно только поставить Python в новый образ, а после скопировать из предыдущего образа папку с библиотекой, обертку для нее, конфигурационный бандл и сам скрипт сервера, а после запустить вместе с контейнером.

В итоге всех проделанных манипуляций имеем готовый Docker-образ с сервером распознавания и скрипт, позволяющий с помощью json отправлять на этот сервер картинку с документом и получать ответ в виде json с распознанными полями документа.

Давайте протестируем на картинке из википедии. Отправим запрос вида:

 python3 client.py --image_path=/home/tolstov/Deutscher_Personalausweis_(1987_Version).jpg 

Получим ответ: 

JSON с данными
{     "error": false,     "response": {         "docType": "deu.id.type2",         "fields": {             "birth_date": {                 "isAccepted": true,                 "name": "birth_date",                 "value": "12.08.1964"             },             "birth_place": {                 "isAccepted": true,                 "name": "birth_place",                 "value": "MÜNCHEN"             },             "expiry_date": {                 "isAccepted": true,                 "name": "expiry_date",                 "value": "07.10.2011"             },             "full_mrz": {                 "isAccepted": true,                 "name": "full_mrz",                 "value": "IDD<<MUSTERMANN<<ERIKA<<<<<<<<<<<<<<1220001518D<<6408125<1110078<<<<<<<0"             },             "last_name": {                 "isAccepted": true,                 "name": "last_name",                 "value": "MUSTERMANN"             },             "last_name0": {                 "isAccepted": true,                 "name": "last_name0",                 "value": "MUSTERMANN"             },             "mrz_birth_date": {                 "isAccepted": true,                 "name": "mrz_birth_date",                 "value": "12.08.1964"             },             "mrz_cd_birth_date": {                 "isAccepted": true,                 "name": "mrz_cd_birth_date",                 "value": "5"             },             "mrz_cd_composite": {                 "isAccepted": true,                 "name": "mrz_cd_composite",                 "value": "0"             },             "mrz_cd_expiry_date": {                 "isAccepted": true,                 "name": "mrz_cd_expiry_date",                 "value": "8"             },             "mrz_cd_number": {                 "isAccepted": true,                 "name": "mrz_cd_number",                 "value": "8"             },             "mrz_doc_type_code": {                 "isAccepted": true,                 "name": "mrz_doc_type_code",                 "value": "ID"             },             "mrz_expiry_date": {                 "isAccepted": true,                 "name": "mrz_expiry_date",                 "value": "07.10.2011"             },             "mrz_gender": {                 "isAccepted": true,                 "name": "mrz_gender",                 "value": "<"             },             "mrz_issuer": {                 "isAccepted": true,                 "name": "mrz_issuer",                 "value": "D"             },             "mrz_last_name": {                 "isAccepted": true,                 "name": "mrz_last_name",                 "value": "MUSTERMANN"             },             "mrz_line1": {                 "isAccepted": true,                 "name": "mrz_line1",                 "value": "IDD<<MUSTERMANN<<ERIKA<<<<<<<<<<<<<<"             },             "mrz_line2": {                 "isAccepted": true,                 "name": "mrz_line2",                 "value": "1220001518D<<6408125<1110078<<<<<<<0"             },             "mrz_name": {                 "isAccepted": true,                 "name": "mrz_name",                 "value": "ERIKA"             },             "mrz_nationality": {                 "isAccepted": true,                 "name": "mrz_nationality",                 "value": "D"             },             "mrz_number": {                 "isAccepted": true,                 "name": "mrz_number",                 "value": "122000151"             },             "mrz_opt_data_2": {                 "isAccepted": true,                 "name": "mrz_opt_data_2",                 "value": ""             },             "name": {                 "isAccepted": true,                 "name": "name",                 "value": "ERIKA"             },             "nationality": {                 "isAccepted": true,                 "name": "nationality",                 "value": "DEUTSCH"             },             "number": {                 "isAccepted": false,                 "name": "number",                 "value": "1946881314"             }         }     } }

Описанные выше скрипты для клиент-серверного взаимодействия, а также dockerfile для сборки и документацию мы предоставляем в составе отгружаемого SDK, как один из примеров использования нашей библиотеки. Посмотреть их можно также на гитхабе.


ссылка на оригинал статьи https://habr.com/ru/company/smartengines/blog/711822/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *