Распознавание банковских карт в видеопотоке в браузере с помощью SmartEngines и WebAssembly
С активным развитием и распространением технологии WebAssembly (или сокращённо WASM) появилась возможность создавать веб-модули, которые можно загружать с сервера и исполнять их прямо в браузере! Мы не смогли пройти мимо такой возможности, и, после долгих оптимизаций, представили свой модуль Smart Code Engine, умеющий распознавать банковские карты, баркоды, машиночитаемые зоны, номера телефонов и документы прямо в браузере.
Сегодня мы расскажем, как с помощью wasm-модуля от Smart Engines распознать номер банковской карты, просто поднеся её к веб-камере ноутбука.
Итак, наша цель — распознавать данные с банковской карты, поднесенной к камере ноутбука, с помощью wasm-модуля от SmartEngines, и заполнять готовую форму (в рамках этой статьи — просто выводить на экран). Механизм действия такой:
Выводим изображение от видеокамеры на экран
По готовности забираем изображения и передаём на распознавание в wasm-модуль до тех пор, пока модуль не вернёт флаг терминальности (не скажет, что ему достаточно картинок для уверенного распознавания)
Забрать результат, распарсить его и вывести на экран
Делается это очень просто: для этого нужен сам модуль распознавания, код интеграции модуля, а также механизм взаимодействия с ним. Давайте пройдёмся по порядку по всем указанным компонентам.
Технически WASM-модули представляют собой некоторый набор инструкций в промежуточном представлении, который, попав в браузер, «компилируется» в исполняемый браузерным движком код. Результат компиляции — обычный js-объект в памяти браузера со своим списком методов. Это позволяет работать с модулем обычным front-end разработчикам, без необходимости разбираться в новых технологиях или языках программирования.
Наш модуль поставляется в виде двух файлов — .js в качестве служебного файла для загрузки wasm-файла в память, компиляции в js-объект и создания подключаемого модуля, и самого .wasm файла. Собственно, благодаря js-файлу с вспомогательным кодом, компиляция и инициализация модуля занимают всего пару строк кода:
importScripts(`./bin/idengine_wasm.js`);
let wasmFilePath = {
mainScriptUrlOrBlob: `./bin/idengine_wasm.js`,
};
const SE = await SmartIDEngine(wasmFilePath);
Модуль мы будем исполнять в отдельном потоке, для этого в js есть механизм Web Workers — это позволит разделить работу модуля и взаимодействие с ним, а так же не блокировать исполнение основного потока, отвечающего за отрисовку графики.
Дальше переходим непосредственно к интеграции модуля — необходимо инициализировать инструменты распознавания, создать настройки и сессию распознавания.
// Инициализируем набор инструментов для распознавания
let engine;
try {
engine = new SE.seIdEngine('false', 1);
} catch (e) {
throw 'Create engine ' + SE.printExceptionMessage(e);
}
// Создаём объект с настройками для распознавания:
// с его помощью мы можем настраивать поведение библиотеки при распознавании (например, тип распознаваемого баркода)
let sessionSettings;
try {
sessionSettings = engine.CreateSessionSettings();
} catch (e) {
console.error(SE.printExceptionMessage(e));
}
// Задаём тип объекта, который будет искаться библиотекой на картинке
sessionSettings.AddEnabledDocumentTypes("card.*”);
// Создаём сессию распознавания - объект, который выбирает и использует нужные инструменты распознавания, а также хранит все промежуточные данные
let spawnedSession;
try {
spawnedSession = engine.SpawnSession(sessionSettings, signature);
} catch (e) {
console.error(SE.printExceptionMessage(e));
}
Для передачи изображений в модуль и для возвращения результата реализуем простенький протокол на основе механизма передачи сообщений между воркерами:
Со стороны основного потока:
SEWorker.onmessage = function (msg) {
switch (msg.data.requestType) {
// Вернулся готовый результат распознавания
case 'result':
let result = msg.data;
printResult(result);
// Cбрасываем сессию распознавания
SEWorker.postMessage({ requestType: 'reset' });
break;
// нужны новые изображения для поднятия качества распознавания
case 'FeedMeMore':
console.log('Need new frame');
SEWorker.postMessage(requestFrame());
break;
}
};
// так мы забираем изображение с canvas и отдаём на распознавание
function requestFrame() {
return {
requestType: 'frame',
imageData: canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height),
width: canvas.width,
height: canvas.height,
};
}
Со стороны worker«a:
onmessage = async function (msg) {
switch (msg.data.requestType) {
case 'frame':
const resultFrame = recognizerFrame(msg.data.imageData);
postMessage(resultFrame);
break;
case 'reset':
spawnedSession.Reset();
postMessage({ requestType: 'wasmEvent', data: { type: 'reset' } });
break;
// no default
}
Распознаётся изображение с помощью функции:
function recognizerFrame(canvas) {
const width = canvas.width;
const height = canvas.height;
const rawData = canvas.data.buffer;
const channels = rawData.byteLength / (height * width); // считаем количество каналов
const stride = channels >= 3 ? rawData.byteLength / height : width; // Высчитываем stride
// Создаём объект, хранящий изображение
const imgSrc = new SE.seImageFromBuffer(rawData, width, height, stride, channels);
// Распознаём изображение
const result = spawnedSession.Process(imgSrc);
if (!result.GetIsTerminal()) { // Если нужны дополнительные кадры
return {
requestType: 'FeedMeMore'
};
}
// Тут парсим объект
const resultMessage = resultObject(result);
imgSrc.delete();
result.delete();
return resultMessage;
}
В итоге распознавания нескольких кадров нам вернётся объект с результатом, который нужно распарсить для получения текстовых строк:
function resultObject(result) {
return {
requestType: 'result',
docType: result.GetDocumentType(),
data: getTextFields(result)
};
}
// Парсим текстовые поля
function getTextFields(result) {
const data = {};
const tf = result.TextFieldsBegin();
for (; !tf.Equals(result.TextFieldsEnd()); tf.Advance()) {
const key = tf.GetKey();
const field = tf.GetValue();
let value = field.GetValue().GetFirstString();
data[key] = {
name: key,
value: value,
isAccepted: field.GetBaseFieldInfo().GetIsAccepted()
};
}
return data;
}
Итак, модуль встроен, давайте посмотрим, как это работает!
При использовании всех доступных оптимизаций и картинки хорошего качества время уверенного распознавания банковской карты занимает от одной до двух секунд. Подобного качества удалось добиться потому, что мы владеем всеми исходными кодами библиотеки, это же позволяет нам добиться минимального размера модуля — сжатый стандартным для серверов шифрованием gzip, модуль весит примерно 2,5 мегабайта!
WebAssembly — удобный инструмент, позволяющий создавать легковесные и простые в интеграции продукты. WASM-модули «берут на себя» все проблемы, связанные с кроссплатформенностью и интеграцией новых продуктов за счет выполнения в браузере. Модули имеют простой и понятный каждому front-end разработчику js-интерфейс, и все современные браузеры поддерживают их исполнение. Мы верим в большое будущее этой технологии, поэтому продолжим оптимизировать наши продукты для работы в браузере, делая их ещё быстрее и компактнее!