Привет!

Мы, 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, как один из примеров использования нашей библиотеки. Посмотреть их можно также на гитхабе.

Комментарии (0)