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