Привет!
Мы, 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/
Добавить комментарий