Автоматическое распознавание документов в браузере позволяет реализовать удаленную идентификацию пользователя, цифровой онбординг, KYC и другие востребованные бизнес-сценарии непосредственно на устройстве клиента. Оценка качества изображения в процессе распознавания документа помогает повысить надежность идентификации, защититься от атак на предъявление, а также своевременно проинструктировать пользователя в случае ненамеренного перекрытия значимых элементов документа бликами, пальцами и посторонними предметами.
Ниже на примере двух кейсов – распознавание паспорта в видеопотоке и распознавание свидетельств о рождении по фото – приведена пошаговая инструкция по интеграции SDK Smart Engines с технологией оценки качества изображения в браузер с помощью WebAssembly.
Оценка качества изображения
На фоне растущих ограничений, связанных с дистрибуцией нативных мобильных приложений в магазинах, браузер становится одним из ключевых каналов взаимодействия бизнеса с клиентами. Вместе с тем задачи идентификации личности по документам постепенно переходят в веб-среду. В таком сценарии пользователь самостоятельно предъявляет документ, удостоверяющий личность, используя доступное устройство – смартфон, планшет или веб-камеру компьютера.
Самостоятельное предъявление документа создает дополнительный риск: реквизиты и элементы документа, а также значимые признаки подлинности могут оказаться перекрыты пальцем, посторонним предметом, бликом или голограммой. Часть изображения может также оказаться расфокусирована или выйти за пределы кадра. Такие атаки на предъявление могут быть как намеренными, так и случайными, однако в обоих случаях важно своевременно их выявлять, классифицировать и локализовывать.
Кроме того, требования регуляторов в области идентификации предполагают, что для проведения проверки должны быть получены все необходимые сведения из документа. Недостаточно просто распознать отдельные реквизиты – система должна убедиться, что в кадре присутствуют все значимые данные и элементы документа, необходимые для идентификации. Поэтому контроль качества изображения также становится инструментом проверки полноты предъявленных данных.
Для исключения необоснованных отказов в случае ненамеренного перекрытия данных требуется оперативно проинструктировать пользователя о том, что именно мешает пройти проверку. Именно эту задачу решает оценка качества изображения – технология проверяет читаемость данных документа и сигнализирует, если необходимые реквизиты или элементы защиты оказались перекрыты. Распознавание документов с возможностью обратной связи в реальном времени значительно улучшает пользовательский опыт и напрямую сказывается на конверсионных показателях.
При этом документы, используемые для подтверждения личности или возрастной идентификации, различаются по формату, шаблону, структуре и набору защитных элементов. Поэтому при оценке качества система распознавания должна учитывать особенности конкретного документа. Кроме того, в веб-сценариях документ может поступать как отдельная фотография или как видеопоследовательность. Ниже рассмотрены два показательных сценария: распознавание паспорта РФ в видеопотоке и распознавание свидетельства о рождении по фотографии. На их примере можно увидеть, как оценка качества изображения работает с разными типами входных данных и документами и как реализовать технологию в браузере.
Распознавание в браузере с помощью WebAssembly
Smart ID Engine может работать в браузере в виде WebAssembly-модуля. Это позволяет выполнять распознавание непосредственно на стороне клиента, не отправляя изображение документа на сервер.
Браузеры предоставляют следующие фичи в зависимости от возможностей устройства:
- Single Instruction Multiple Data (SIMD) – использование векторных инструкций процессора, которые позволяют значительно увеличить скорость вычислений.
- Threads – выполнение вычислений параллельно.
Проверить поддержку SIMD-инструкций и многопоточности в браузере можно с помощью библиотеки wasm-feature-detect**.
Smart Engines предоставляет набор библиотек для всех комбинаций этих фич:
- nosimd.nothreads – базовая сборка, поддерживаемая везде.
- simd.nothreads – самая быстрая однопоточная сборка, поддерживается почти на всех современных устройствах (Safari 17.1+, Chrome 91+, Firefox 89+); часто является оптимальным выбором.
- simd.threads – сборка с поддержкой многопоточности; требует дополнительных настроек на сервере, но позволяет добиться максимальной производительности.
- nosimd.threads – редкая комбинация, она здесь на всякий случай.
Настройка веб-сервера
До подключения библиотеки к веб-приложению необходимо подготовить сервер, с которого браузер будет получать WASM-модули.
Первые два пункта являются строго обязательными для нормального функционирования библиотеки в браузере.
- Content-Type. Веб-сервер должен возвращать
Content-Type: application/wasmдля файлов с расширением*.wasm. - Сжатие. Веб-сервер должен поддерживать компрессию файлов c расширением
.wasm.
WASM-файлы хорошо сжимаются и это значительно позволит сократить время доставки файлов до клиента. Убедитесь в наличии заголовкаcontent-encodingу файла*.wasmпри отдаче сервером. Рекомендуется отдаватьwasmфайлы уже сжатыми, чтобы не нагружать сервер сжатием файлов на лету на каждый запрос. - Поддержка CSP. Если у вас настроена строгая политика безопасности контента (CSP), она заблокирует JIT-компиляцию WebAssembly для защиты от XSS-атак. Для того, чтобы ее включить, необходимо добавить директиву wasm-unsafe-eval. Она разрешает выполнение только WebAssembly и в отличие от unsafe-eval по-прежнему блокирует вызовы JavaScript eval() и new Function().
Без этой директивы загрузка или инстанцирование (создание экземпляра) WASM-модуля будет невозможно. - Многопоточность. Для использования многопоточной сборки браузеру дополнительно требуется обеспечить изоляцию контекста (Cross-Origin Isolation), настроив на сервере заголовки Cross-Origin-Opener-Policy и Cross-Origin-Embedder-Policy.
Например, в nginx это можно сделать, дописав в конфигурацию:
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
Камера
Широкоугольная камера
В большинстве современных мобильных устройств размещается несколько камер. По умолчанию браузер использует основной модуль, однако именно на трехкамерных iPhone эти модули не способны фокусироваться на объектах, располагаемых ближе 20 см. Поэтому для съемки документов на iPhone лучше выбирать широкоугольную камеру, если она доступна на устройстве.
При этом на Android широкоугольные камеры могут давать изображение очень низкого качества, поэтому для Android-устройств наиболее универсальным решением будет использовать главный модуль камеры.
Разрешение
Существует распространенное представление о том, что для качественного распознавания изображение необходимо подавать в максимально возможном разрешении. Этот подход был оправдан для алгоритмов предыдущего поколения. Современные системы распознавания зависят от исходного разрешения гораздо меньше. По возможности лучше всегда избегать работы с большим объемом данных, поскольку это так или иначе добавляет накладные расходы на пересылку UI-> Worker->VM WASM.
Оценим необходимое разрешение для случая оценки качества. Для уверенного распознавания и проверки подлинности документа необходимо обеспечить не менее 120 DPI. Физический размер страницы паспорта составляет 88 × 125 мм. Будем считать, что разворот паспорта предъявляется наиболее естественным образом – вертикально. Это означает, что изображение паспорта должно иметь разрешение не менее 591 на 832 пикселей. Паспорт занимает не весь кадр; если он занимает 0.8 по ширине и высоте (около 65% от площади кадра), то необходимое разрешение получится 739 на 1040. Разумеется, лучше обеспечить некоторый запас по DPI и предусмотреть погрешности предъявления. При этом для съемки паспорта РФ лучше подходит формат кадра с соотношением сторон 4:3.
У свидетельства о рождении в формате А4 физический размер 210 × 297 мм или 8,27 × 11,69 дюйма. Если считать, что оно занимает по 0.9 кадра по каждой из сторон, необходимо разрешение 1102 на 1559. Тут также лучше подходит формат кадра 4:3.
В результате, оптимальным будет разрешение кадра 1600 на 1200 в вертикальной ориентации.
Отметим, что как правило у веб-камер даже сравнительно современных ноутбуков разрешение ограничено 720 на 1280 px, что ограничивает распознавание документов А4, таких как свидетельство о рождении. Дополнительно ситуацию может ухудшить отсутствие автофокуса.
Поскольку для качественного распознавания важно как именно пользователь предъявляет документ, хорошей идеей будет дать ему подсказку: отрисовать достаточно крупную рамочку или маску документа.
Пример работы с камерой
Открываем камеру для съемки видео:
stream = await navigator.mediaDevices
.getUserMedia({ video: { facingMode: { ideal: "environment" } } })
На Android и iOS, при одинаковых настройках, камера может инициализироваться в различной ориентации. Чтобы всегда инициализировать камеру в портретной или ландшафтной ориентации согласно положению телефона, можно использовать следующий подход:
let stream
let cameraAnimationFrame
const LANDSCAPE = {
width: 1600,
height: 1200,
}
const PORTRAIT = {
width: 1200,
height: 1600,
}
function isPortraitScreen() {
return window.matchMedia("(orientation: portrait)").matches
}
function buildCameraConstraints(cameraId, useExactResolution = true) {
const resolution = useExactResolution ? {
width: { exact: LANDSCAPE.width },
height: { exact: LANDSCAPE.height },
} : {
width: { ideal: LANDSCAPE.width },
height: { ideal: LANDSCAPE.height },
}
const videoConstraints = {
...resolution,
aspectRatio: { ideal: LANDSCAPE.width / LANDSCAPE.height },
}
if (cameraId) {
videoConstraints.deviceId = { exact: cameraId }
} else {
videoConstraints.facingMode = { ideal: "environment" }
}
return {
audio: false,
video: videoConstraints,
}
}
function resizeCanvasesForCamera(videoWidth, videoHeight) {
const usePortraitCanvas = isPortraitScreen() && videoWidth > videoHeight
const target = usePortraitCanvas ? PORTRAIT : { width: videoWidth, height: videoHeight }
canvas.width = overlayCanvas.width = target.width
canvas.height = overlayCanvas.height = target.height
return { usePortraitCanvas }
}
function drawCameraFrame(ctx, usePortraitCanvas) {
ctx.save()
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (usePortraitCanvas && video.videoWidth > video.videoHeight) {
ctx.translate(canvas.width, 0)
ctx.rotate(Math.PI / 2)
ctx.drawImage(video, 0, 0, canvas.height, canvas.width)
} else {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
}
ctx.restore()
}
async function cameraInit(cameraId) {
const constraints = buildCameraConstraints(cameraId, true)
try {
// Remove all streams before switching devices
if (cameraAnimationFrame) {
cancelAnimationFrame(cameraAnimationFrame)
}
if (stream) {
stream.getTracks().forEach((track) => track.stop())
}
try {
stream = await navigator.mediaDevices.getUserMedia(constraints)
} catch (error) {
if (error.name !== "OverconstrainedError") throw error
console.warn("Exact camera mode is not available. Falling back to ideal resolution.", error)
stream = await navigator.mediaDevices.getUserMedia(buildCameraConstraints(cameraId, false))
}
console.log("Got stream with constraints:", constraints)
console.log("Camera settings:", stream.getVideoTracks()[0].getSettings())
console.log("Camera capabilities:", stream.getVideoTracks()[0].getCapabilities?.())
video.srcObject = stream
await video.play()
} catch (error) {
if (error.name === "OverconstrainedError") {
const v = constraints.video
console.error(`The resolution ${v.width.exact}x${v.height.exact} px is not supported by your device.`)
} else if (error.name === "NotAllowedError") {
console.error("Permissions have not been granted to use your camera and " + "microphone, you need to allow the page access to your devices in " + "order for the demo to work.")
}
console.error(`getUserMedia error: ${error.name}`, error)
return
}
const { videoWidth, videoHeight } = video
const { usePortraitCanvas } = resizeCanvasesForCamera(videoWidth, videoHeight)
const ctx = canvas.getContext("2d", { willReadFrequently: true })
const animate = function () {
drawCameraFrame(ctx, usePortraitCanvas)
cameraAnimationFrame = requestAnimationFrame(animate)
}
animate()
}
Базовые понятия библиотеки распознавания
После настройки веб-сервера можно переходить к интеграции распознавания. Ниже приведены базовые понятия библиотеки распознавания Smart Engines.
- Движок распознавания: создается один раз при старте приложения. От него создаются следующие элементы интерфейса:
- Сессия распознавания: процесс распознавания конкретного документа (паспорта РФ, ВУ, СТС и других – всего библиотека поддерживает более 3000 типов). В сессию можно передать одно изображение или серию кадров из видеопотока: с каждым новым кадром результат уточняется; система автоматически определяет оптимальный момент остановки сессии распознавания.
- Настройки сессии: хранят список доступных для распознавания типов документов (определяется конфигурационным бандлом), дополнительную информацию о каждом документе, а также позволяют настроить процесс распознавания: задать список ожидаемых типов документов для сессии распознавания, включить\выключить проверки признаков подлинности, задать таймаут для сессии распознавания и пр. Настройки создаются перед созданием самой сессии и не могут редактироваться в процессе распознавания.
- Результат распознавания: объект со структурированными текстовыми полями, изображениями (например, фото владельца), координатами документа на кадре и данными проверок подлинности.
Общая логика работы одинакова для любой платформы и любого продукта линейки Smart Engines: движок → настройки сессии → создание сессии → передача изображения / последовательности изображений → разбор результата.
Инициализация движка распознавания
Необходимо создать и инициализировать один экземпляр движка распознавания. Инициализация является достаточно «тяжелой» операцией, поэтому ее следует выполнять вне потока пользовательского интерфейса. Для этого используется Web Worker.
При распознавании в браузере полезно учитывать следующую опцию инициализации:
bool lazy:ленивая и неленивая инициализация. При ленивой инициализации все необходимые ресурсы конфигурируются во время первого обращения к ним (как правило, при обработке первого кадра). Это позволяет эффективно использовать память при использовании библиотеки с большим набором поддерживаемых документов, но немного замедляет распознавание первого кадра. В неленивом режиме все ресурсы конфигурируются во время инициализации движка.
В случае наличия 1-2 документов в движке оптимальным будет использование не ленивой инициализации для ускорения обработки первого кадра, а в случае большого количества документов предпочтительнее использовать ленивый режим.
// Init recognition engine
function createEngine(SE, MODULE) {
try {
console.time("Create Engine")
ENGINE = new SE.seIdEngine(lazy, 1, false)
console.timeEnd("Create Engine")
wasm_module = SE
readyEmitter({ version: SE.seIdEngineGetVersion() + " / " + MODULE + " / ", doclist: getAvailableDocList(ENGINE) })
} catch (e) {
errorEmitter({ message: e, SE })
}
return ENGINE
}
Конфигурирование сессии распознавания
Маска и режим
Сессия должна быть сконфигурирована для обнаружения конкретного типа документа или группы документов. Это осуществляется благодаря двум параметрам: маска (docTypes) и режим (mode):
Возможные значения маски и режима можно посмотреть в README конкретного WASM-модуля. Для примера с паспортом и свидетельством о рождении выберем режим anyrus (все российские документы) и любой тип (*).
// Main config
const ENGINE_CONFIG = {
activationUrl: "<activationUrl>",
mode: "anyrus",
docTypes: "*",
signature: "<signature>",
}
spawnedSessionRGBA = createRGBASession(ENGINE, ENGINE_CONFIG)
Индивидуальная подпись (signature) привязана к конкретной сборке и необходима для инициализации сессии распознавания. Подпись представляет собой строку в 256 символов. activationUrl – адрес сервера активации, про который речь пойдет в следующем разделе.
Терминальность
В случае видеопотока необходимо определить момент, когда закончить передавать кадры в сессию и решить, что результат распознавания является финальным. Сессия может завершиться в следующих случаях:
- Stoppers. Когда все поля уверенно распознались, движок выставляет результату флаг “терминальности”. В этом случае подача дополнительных кадров не улучшит результаты.
- Timeout. Когда истек заданный таймаут, чтобы ограничить возможное ожидание пользователя. Для режима Quality Gate характерное значение таймаута составляет около 15 секунд, чтобы пользователь успел скорректировать найденные проблемы.
Для такой настройки сессии необходимо поставить опции:
sessionSettings.SetOption("common.enableStoppers", "true")
sessionSettings.SetOption("common.sessionTimeout", "15.0");
Quality Gate
Для включения Quality Gate необходимо указать следующие опции:
sessionSettings.SetOption("common.enableImageQualityCheck", "true")
sessionSettings.SetOption("common.enableForensics", "true")
sessionSettings.SetOption("common.extractRectifiedDocumentImage", "true")
Последняя опция добавит вывод проективно исправленного и обрезанного изображения с лучшего кадра: как будто паспорт отсканировали и вырезали по границам.
Активация сессии распознавания
Поскольку WASM-модуль загружается с сервера на устройство пользователя, его достаточно легко скопировать. Для защиты от несанкционированного использования Smart ID Engine предусматривает привязку к инфраструктуре заказчика с помощью механизма активаций.
Перед каждой передачей изображения в сессию необходимо проверить, активирована ли она. Для активации сессия генерирует динамический ключ, который необходимо передать на сервер и вернуть ключ активации обратно в сессию. Только после успешной активации сессия готова принимать изображения.
Рассмотрим пример. Создадим сессию:
function createRGBASession(engine, ENGINE_CONFIG) {
let sessionSettings = engine.CreateSessionSettings()
sessionSettings.SetCurrentMode(ENGINE_CONFIG.mode)
sessionSettings.AddEnabledDocumentTypes(ENGINE_CONFIG.docTypes)
sessionSettings.SetOption("common.extractTemplateImages", "true")
sessionSettings.SetOption("common.sessionTimeout", "15.0")
sessionSettings.SetOption("common.enableImageQualityCheck", "true")
sessionSettings.SetOption("common.enableForensics", "true")
sessionSettings.SetOption("common.extractRectifiedDocumentImage", "true")
spawnedSession = engine.SpawnSession(sessionSettings, ENGINE_CONFIG.signature)
return spawnedSession
}
Активируем сессию:
const checkSession = async (spawnedSession, ENGINE_CONFIG) => {
if (spawnedSession.IsActivated()) {
return
}
const dynKey = spawnedSession.GetActivationRequest() // Get dynamic key
const response = await fetch(ENGINE_CONFIG.activationUrl, {
method: "POST",
headers: { "Content-Type": "application/json"},
body: JSON.stringify({ action: "activate_id_session", message: dynKey }),
signal: AbortSignal.timeout(3000),
})
if (!response.ok) {
let desc = await response.json()
throw new Error(desc.message)
}
const desc = await response.json()
spawnedSession.Activate(desc.message) // Response is ok, activate session
}
Теперь сессию можно использовать для распознавания.
Изображение и процесс распознавания
Создание объекта изображения документа Image
В браузере библиотека Smart Engines работает только с сырыми пикселями RGBA/RGB/YUV. Работа со сжатыми форматами jpg/gif/png/heic переложена на canvas браузера. Это позволяет читать изображения как с камеры, так и получить поддержку всех нативных форматов платформы и даже работу с PDF.
Так или иначе необходимо создать изображение в виде объекта и получить переменную со ссылкой на него.
Процесс распознавания
В идеальном сценарии документ сразу оказывается перед камерой: занимает большую часть кадра, находится в фокусе и хорошо освещен. В реальности между открытием камеры и появлением подходящего кадра проходит некоторое время, поэтому часть входного потока неизбежно оказывается непригодной для распознавания.
Сократить объем таких кадров можно уже на этапе построения пользовательского сценария. Если открытие камеры одновременно запускает распознавание, имеет смысл предусмотреть небольшую задержку перед началом анализа. Пользователю в любом случае требуется время, чтобы достать документ и правильно расположить его в кадре. Длительность задержки лучше подобрать эмпирически для конкретного сценария.
Разбор результата распознавания с оценкой качества
В качестве результата система Smart Engines возвращает набор распознанных полей с текстом или изображениями (например, поле фотографии). При включении проверки качества в результат добавляется поле image_quality_score, которое содержит численную оценку качества изображения от 0 до 1, а также атрибут reason, который содержит список проблем изображения. Возможные проблемы:
- finger_is_found (Найден палец)
- zone_is_invalid (Зона обрезана)
- hlt_is_found (Найден блик)
- optic_is_found (Найдена голограмма)
- «low DPI» (Низкое разрешение)
- focus (Кадр не в фокусе)
- luminosity (Слишком темно / слишком ярко)
При включении опции common.extractRectifiedDocumentImage добавляется поле-изображение rectified_document_image, содержащее проективно исправленное изображение.
Кроме того, движок возвращает координаты документа, найденных полей и проблемных точек в рамках структуры TemplateSegmentationResult в виде координат точек и радиуса проблемной зоны. Если пользователь закрыл часть контента пальцами, то там окажутся координаты пальцев, которые не позволяют движку прочитать информацию.
После распознавания очередного кадра можно дать пользователю подсказку: нарисовать положение проблемных точек или вывести сообщение о проблеме с фокусировкой камеры, освещением и т.п.
Интеграция распознавания в веб-приложение
Рассмотрим интеграцию распознавания паспорта РФ в видеопотоке в веб-приложение. Чтобы распознавать изображения с камеры в реальном времени, понадобятся элементы video и canvas для вывода изображения с камеры.
<video id="video" class="video" playsinline muted autoplay></video>
<canvas id="canvas" class="canvas"></canvas>
Кнопка для начала распознавания изображения:
<button id="scan-button" class="button">
Сканировать документ
</button>
Блок для вывода результатов распознавания:
<div id="result-wrapper" class="result-wrapper">
<h2>Результат сканирования</h2>
<div id="output"></div>
</div>
Клиентский JavaScript-код
Работа с камерой
В JavaScript-файле запрашиваем видеопоток с камеры:
stream = await navigator.mediaDevices
.getUserMedia({ video: { facingMode: { ideal: "environment" } } })
И направляем его в элемент video на странице:
const videoEl = document.querySelector("#video")
videoEl.srcObject = stream
await videoEl.play()
Теперь на странице отображается видео с камеры.
Передаём видео в canvas.
Чтобы захватывать и передавать кадры на распознавание, необходимо предварительно отрисовать их на canvas.
const canvasEl = document.querySelector("#canvas")
const ctx = canvasEl.getContext("2d", { willReadFrequently: true })
const animate = function () {
ctx.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height)
requestAnimationFrame(animate)
}
animate()
Подключение движка
В процессе подключения движка определяются возможности среды исполнения (поддержка SIMD и многопоточности), и подключается соответствующий Wasm-модуль.
// Import getModuleName()
let MODULE = await getModuleName()
// Import the relevant global Engine object
importScripts(`${ROOT_PATH}bin/${MODULE}/idengine_wasm.js`)
let module = {
mainScriptUrlOrBlob: `${ROOT_PATH}bin/${MODULE}/idengine_wasm.js`,
locateFile: (file) => `${ROOT_PATH}bin/${MODULE}/` + file,
}
// Init Wasm module
const SE = await SmartIDEngine(module)
// Init recognition engine
let ENGINE = createEngine(SE, MODULE)
const getModuleName = async () => {
const a = async (e) => {
try {
return "undefined" != typeof MessageChannel && new MessageChannel().port1.postMessage(new SharedArrayBuffer(1)), WebAssembly.validate(e)
} catch (e) {
return !1
}
}
const simd = async () => WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11]))
const threads = () => a(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 5, 4, 1, 3, 1, 1, 10, 11, 1, 9, 0, 65, 0, 254, 16, 2, 0, 26, 11]))
const hasSimd = await simd()
const hasThreads = await threads()
let module
if (hasSimd === true) {
hasThreads
? (module = "simd.threads")
: (module = "simd.nothreads")
} else {
module = "nosimd.nothreads"
}
console.log("module")
console.log(module)
return module
}
Подключение веб-воркера
Подключим веб-воркер и передадим ему тип документа, который собираемся распознать:
SEWorker = new Worker("./worker.js")
SEWorker.postMessage({
requestType: "createSession",
docData: "default:rus.passport.national"
})
По нажатию на кнопку, отправляем воркеру кадр из видео на распознавание:
const scanButtonEl = document.querySelector("#scan-button")
scanButtonEl.addEventListener("click", async () => {
SEWorker.postMessage({
requestType: "frame",
imageData: canvasEl.getContext("2d", { willReadFrequently: true })
.getImageData(0, 0, canvasEl.width, canvasEl.height)
})
})
Результат распознавания содержит текст и изображения отдельных элементов документа, выведем их:
SEWorker.onmessage = function (message) {
switch (message.data.requestType) {
case "result":
let result = message.data
if (Object.keys(result.data).length === 0) {
console.log("Документ не найден")
SEWorker.postMessage({ requestType: "reset" })
return
}
printResult(result)
canvasHandler.clear(canvasOverlayEl)
SEWorker.postMessage({ requestType: "reset" })
break
}
Выведем результаты оценки качества:
const textFields = getTextFields(streamResult)
const imageScore = textFields.image_quality_score
function printQualityToStatusBar (data) {
const score = Number(data.imageScore.attr.last_frame_score) // числовая оценка качества кадра
const indicator = getImageQualityIndicator(score) // цветовой индикатор качества кадра
const message = getImageQualityProblems(data.imageScore) // сообщение о проблемах кадра
print(`${indicator}\n${message}`)
}
function getImageQualityIndicator(value) {
if (value >= 0.8) {
return "🔘🔘🟢"
} else if (value >= 0.4) {
return "🔘🟡🔘"
} else {
return "🔴🔘🔘"
}
}
function getImageQualityProblems(imageScore) {
const verboseMessages = {
finger_is_found: "Найден палец",
zone_is_invalid: "Зона обрезана",
hlt_is_found: "Найден блик",
optic_is_found: "Найдена голограмма",
"low DPI": "Низкое разрешение",
focus: "Кадр не в фокусе",
luminosity: "Слишком темно / слишком ярко",
}
let message = imageScore.attr && imageScore.attr.last_frame_verbose
if (!message) return null
message = message
.split(";")
.map((code) => code.trim())
.filter(Boolean)
.map((code) => {
let translatedMessage = verboseMessages[code] || code
return translatedMessage
})
.join("; ")
return message
}
При обнаружении пальца на кадре, движок возвращает координаты проблемных точек в виде кругов. Их можно использовать для визуализации на канвасе, чтобы пользователь заметил что загораживает документ.
function getCircles(result) {
const arr = []
for (let i = 0; i < result.GetTemplateSegmentationResultsCount(); i++) {
// sr = IdTemplateSegmentationResult
const sr = result.GetTemplateSegmentationResult(i)
let ppvi = sr.ProblemPointsVectorBegin()
for (; !ppvi.Equals(sr.ProblemPointsVectorEnd()); ppvi.Advance()) {
const circle = ppvi.GetValue()
arr.push([circle.x.x, circle.x.y, circle.r])
}
}
return arr
}
```
```
case "frame": {
let streamResult, frame
try {
await checkSession(spawnedSessionRGBA, ENGINE_CONFIG)
frame = ImageFromRGBAbytes(msg.data.imageData, SE)
streamResult = spawnedSessionRGBA.Process(frame)
// Check terminality
if (!streamResult.GetIsTerminal()) {
const textFields = getTextFields(streamResult);
const imageScore = textFields.image_quality_score
postMessage({
requestType: "FeedMeMore",
imageScore: imageScore,
templateDetection: getTemplateDetection(streamResult),
templateSegmentation: getTemplateSegmentation(streamResult),
templateCircles: getCircles(streamResult),
})
return
}
```
```
if (result?.templateCircles) {
for (let i = 0; i < result.templateCircles.length; i++) {
const circle = result.templateCircles[i]
if (!circle || circle.length < 3) continue
const centerX = Number(circle[0])
const centerY = Number(circle[1])
const radius = Number(circle[2]) * 0.75
if (!Number.isFinite(centerX) || !Number.isFinite(centerY) || !Number.isFinite(radius) || radius <= 0) {
continue
}
ctx.beginPath()
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
ctx.fillStyle = "rgba(255, 0, 0, 0.5)"
ctx.fill()
ctx.lineWidth = 3
ctx.strokeStyle = "#ff0000"
ctx.stroke()
}
if (this._circleTimer) {
clearTimeout(this._circleTimer)
}
const noCirclesResult = {
templateDetection: result?.templateDetection,
templateSegmentation: result?.templateSegmentation,
}
this._circleTimer = setTimeout(() => {
this.draw(noCirclesResult, overlayCanvas)
this._circleTimer = null
}, 250)
}
В приведенном коде выход из цикла обработки кадров осуществляется с учетом результатов распознавания и таймаута сессии. Можно дополнительно учесть результаты проверки качества и не останавливаться, пока пользователь не добьется качественного изображения. Финальное условие выхода, учитывающее качество распознавания, качество изображения и таймаут, будет иметь вид:
const isTerminal = streamResult.GetIsTerminal();
const qualityIsGood = Number(imageScore?.value) >= 0.8;
const sessionTimeout = Number(ENGINE_CONFIG.sessionOptions.sessionTimeout);
const elapsedSeconds = (performance.now() - recognitionStartedAt) / 1000;
const timeoutReached = isTerminal && elapsedSeconds >= sessionTimeout;
needsMoreFrames = !(timeoutReached || (isTerminal && qualityIsGood))
Веб-приложение с возможностью распознавания изображений готово. Для распознавания фотографии свидетельства о рождении отпадает необходимость ожидать терминальности сессии, а в остальном работа с распознающим движком или обработка результатов не будут отличаться.
Управление памятью
В интеграции С++ библиотеки в другие языковые среды есть свои особенности, одна из них — ручной контроль за освобождением памяти.
Библиотека никак не может знать, когда созданные ресурсы перестают использоваться, а сборщик мусора никак не может повлиять на память, выделяемую в нативной части. Поэтому обязательно необходимо удалять за собой сессии, изображения и результат распознавания когда в них уже нет нужды.
Пример освобождения памяти в finally, после получения данных о распознанном кадре:
case "frame": {
let streamResult, frame
try {
await checkSession(spawnedSessionRGBA, ENGINE_CONFIG)
frame = ImageFromRGBAbytes(msg.data.imageData, SE)
streamResult = spawnedSessionRGBA.Process(frame)
if (!streamResult.GetIsTerminal()) {
const textFields = getTextFields(streamResult);
const imageScore = textFields.image_quality_score
postMessage({
requestType: "FeedMeMore",
imageScore: imageScore,
templateDetection: getTemplateDetection(streamResult),
templateSegmentation: getTemplateSegmentation(streamResult),
templateCircles: getCircles(streamResult),
})
return
}
const r = resultObject(streamResult, frame.GetSize())
postMessage(r)
} catch (e) {
errorEmitter({ message: e, SE })
} finally {
if (streamResult?.GetIsTerminal()) spawnedSessionRGBA.Reset()
streamResult?.delete()
frame?.delete()
}
break
Заключение
Распознавание документов в браузере – это не только перенос OCR на веб-страницу, но и построение полноценного пользовательского сценария предъявления документа. Контроль качества изображения играет в нем определяющую роль, позволяя одновременно оценивать читаемость данных и при необходимости помогать пользователю.
Такой механизм позволяет минимизировать число неудачных попыток и сделать процесс идентификации прозрачным и максимально удобным для пользователя. А выполнение обработки непосредственно в браузере с помощью WebAssembly дает возможность реализовать этот сценарий без передачи изображений документов на внешний сервер.
Браузерное распознавание документов – не единственный вариант интеграции Smart ID Engine. Та же библиотека может работать и на сервере, как часть десктоп-приложения (например, для работы с планшетными сканерами и веб-камерами) или мобильные приложения. Выбор конкретного варианта зависит от того, какие документы нужно распознавать и откуда поступает изображение – подробнее об этом можно прочитать на сайте Smart Engines.