Как стать автором
Обновить
296.73
Ozon Tech
Стремимся делать лучший e-com в России

Камера как сканер штрих-кодов: проблемы, инструменты и эксперименты

Время на прочтение14 мин
Количество просмотров4.6K

Всем привет, меня зовут Никита, я старший разработчик в компании Озон и работаю над iOS-приложением «Пункт Озон».

«Пункт Озон» — это мобильное приложение для работников и менеджеров пункта выдачи заказов (далее просто ПВЗ). В приложении множество различных разделов для повседневной работы. Самые популярные разделы — это «Приёмка», «Выдача» и «Инвентаризация», что в мобильной, что в web-версии. Именно они помогают выполнить главные задачи работников ПВЗ — принять заказы на баланс и позже выдать их клиентам.

Для скорости работы и защиты от ошибок работники ПВЗ производят эти действия через сканирование штрих-кодов, расположенных на самих заказах или на тарах, в которых они приезжают. Если брать web-версию, то там «всё просто»: покупается нормальный/качественный отдельный сканер, подключается к ПК — и проблем, считай, нет. А вот с мобильным приложением дела обстоят немного не так. У смартфона есть только камера, и она явно не проектировалась под задачу «сканируем только штрих-коды». Но с другой стороны, камера делает фотографию, а раз у нас есть фотография, то можно там найти штрих-код. К счастью, Apple и Google предоставляют нам инструментарий для этого.

А раз я iOS-разработчик, то и рассказывать я вам буду о нашем iOS-приложении. И сегодня у нас в меню: 

  • с какими проблемами столкнулись пользователи при использовании мобильного сканера, и почему мы стали искать возможность его улучшения;

  • сравнение инструментов, которые предлагает Apple;

  • технический обзор одного из них;

  • история его внедрения в наше приложение и конечные результаты.

Немного контекста проблемы

К сожалению, мир неидеален и, как следствие, работникам ПВЗ порой приходится иметь дело с некачественными штрих-кодами. Они могут быть плохо напечатанными, помяты, наклеены набок или того хуже — размыты и порваны.

Вот парочка штрих кодов, с которыми иногда приходится иметь дело работникам ПВЗ:

Тут всё понятно — некачественная печать, а иногда может попасться вот такой:

ScanIT
ScanIT

Как видно на фото, на штрих-коде что-то написано. В данном случае так записывается номер полки, на которой отправление будет дожидаться клиента. Такой способ записи распространён в ПВЗ, которые в основном работают через web-версию. 

Строго говоря, данная запись, не приносит никаких проблем, когда отправление в будущем сканируется специальным сканером. Но иногда такие отправления могут быть аннулированы или возвращены, и существует определённая вероятность, что в будущем они могут попасть на ПВЗ, в котором пользуются только мобильной версией приложения. И тогда у пользователей, могут возникнуть трудности при работе с таким отправлением. Трудности возникают из-за того, что наш мобильный сканер с трудом считывал такие штрих-коды, а также некоторые другие. 

После такого, конечно же, пользователи идут и жалуются в техподдержку или в отзывах в сторах. И если прошерстить все отзывы и жалобы, становится понятно, что существует 2 основных проблемы:

  • с трудом считываются или совсем не считываются потрёпанные жизнью штрих-коды;

  • приходится долго искать ракурс для корректного сканирования.

И с этим надо что-то делать, т.к. такие некачественные штрих-коды и нестабильная работа нашего мобильного сканера является неприятным блокером для комфортной работы. Как итог было решено поискать разные возможные решения этих проблем и стабилизировать работу сканера.

Исследование

Первое, что было сделано, — был проведён поиск инструментов, в результате которого было найдено три возможных реализации, предоставляемые нам Apple.

  1. AVCaptureMetadataOutput — старый, всем знакомый обработчик метаданных, полученных с камеры, доступный с iOS 7. На нем я сильно останавливаться не буду, скажу только, что именно он у нас использовался в качестве инструмента сканирования штрих-кодов.

  2. DataScannerViewController представляет собой уже написанное готовое решение, основанное на Vision, входит в пакет VisionKit. Позволяет сканировать как штрих-коды, так и кучу всего другого: обычные тексты, e-mail, телефоны, время и т.п. Обладает многими встроенными фичами: разные режимы работы, возможность выделять найденные данные. Предоставляет делегат, который оповещает о том, что объект появился в кадре, пропал, обновились геометрические либо другие данные объекта или пользователь тапнул на объект. При беглом тесте, DataScannerViewController показал, что сканирует потрёпанные штрих-коды намного лучше, чем наше решение. Казалось бы, бери да внедряй, но тут Apple не изменяет своим традициям — минимальная версия iOS, с которого поддерживается данный инструмент, — это 16, а наше приложение на тот момент поддерживало 14.

  3. VNDetectBarcodesRequest — этот инструмент находится на уровне ниже, входит в пакет Vision и ответственен только за одну вещь: распознавание штрих-/QR-кодов. Из-за этого всю необходимую функциональность придётся разрабатывать самостоятельно, но в этом есть свои плюсы — не тянется лишнее, и можно лучше контролировать процесс. Кроме того, огромным преимуществом, по сравнению с DataScannerViewController, является то, что VNDetectBarcodesRequest доступен с 11 iOS. И как раз на основе этого запроса и строится часть DataScannerViewController, отвечающая за распознавание barcodes.

Итак, у нас есть инструменты, с которыми мы можем сделать наш сканер лучше. Чтобы понять, стоит ли игра свеч и будущих усилий, было решено провести небольшое сравнение инструментов по следующим критериям:

  • как быстро определяет ШК;

  • с каким количеством повреждённых ШК справится;

  • а также будем следить за процентом разряда батареи.

Экспериментов будет два.

В первом было взято несколько хороших штрих-кодов, и они по кругу сканировались 200 раз. Причём переводить камеру на следующий ШК можно только после успешного сканирования текущего.

Для вычисления времени прибегнем к простейшей формуле: (Время последнего отсканированного ШК) - (Время текущего отсканированного).

Также в этом же эксперименте замерим, сколько процентов аккумулятора ушло на это действие, замер будем делать со 100 % заряженного устройства с ёмкостью в 85%.

Среднее время на 1 ШК, сек.

Медианное время на 1 ШК, сек.

Максимальное время на 1 ШК, сек

Минимальное время на 1 ШК, сек.

Общее время, мин.:сек. 

Процент разряда аккумулятора

AVCaptureMetadataOutput

3.9

3.0

16.4

1.3

13:28

0

DataScannerViewController

3.9

6.0

31.4

0.9

18:46

6

VNDetectBarcodesRequest

1.3

1.6

4.8

0.7

5:27

2

Честно говоря, результаты оказались неожиданными. Сильно удивила разница между DataScannerViewController и VNDetectBarcodesRequest. Напомню, что часть DataScannerViewController, которая отвечает за сканирование штрих-кодов, базируется на VNDetectBarcodesRequest. Причину этого вижу в том, что DataScannerViewController очень часто теряет фокус, и нужно время для его восстановление. А это, честно говоря, сильно раздражало, учитывая, что нужно отсканировать большое количество штрих-кодов.

Что касается энергоэффективности, то тут, конечно, лидерство у AVCaptureMetadataOutput.

Но это всё промежуточные выводы, предлагаю перейти ко второму эксперименту, в котором возьмём ряд испорченных ШК. Некоторые из них мы испортим собственноручно, а некоторые возьмём из жалоб наших пользователей. Затем проверим, какие сканеры смогут распознать максимальное количество штрих-кодов.

С результатами этого эксперимента можно ознакомиться ниже: 

Как нетрудно догадаться, в точности распознавания и тут первенство у VNDetectBarcodesRequest. Он не смог справиться только с жёлтым штрих-кодом и штрих-кодом, который был приклеен на угол коробки.

Что касается DataScannerViewController, то он показал похожие результаты, как и VNDetectBarcodesRequest, что неудивительно. Но он не смог распознать сильно помятый ШК и с трудом распознал штрих-код, на котором была надпись. Причина такого различия, скорее всего, заключается в ракурсах и возможных бликах от освещения.

А AVCaptureMetadataOutput показал не очень оптимистичные результаты, хорошо справился с нормальными штрих-кодами, средне — с немного рваными и совсем не справился, если на штрих-коде было что-то написано или зачёркнуто.

В итоге было решено реализовывать новую версию сканера на основе VNDetectBarcodesRequest. Нам понравилась скорость, точность распознавания и минимальная поддерживаемая версия iOS.

Обзор VNDetectBarcodesRequest

В этом разделе я не буду подробно рассматривать подключение VNDetectBarcodesRequest. Если интересно узнать подробности подключения, вы можете обратиться к исходному коду, который расположен здесь. Вместо этого, я сделаю небольшой обзор VNDetectBarcodesRequest, чтобы дать вам базовое представление об этом инструменте.

И начнём с того, что посмотрим на иерархию наследования VNDetectBarcodesRequest:

VNImageBasedRequest и VNRequest являются абстрактными классами, которые не рекомендуется использовать напрямую.

Класс VNRequest является корневым классом для всех запросов в Vision. В нем содержится основная настройка: подключение к вычислительному устройству, указание версий алгоритмов, настройка обработчиков ответа и т.п

В свою очередь, VNImageBasedRequest является наследником VNRequest и отвечает за анализ изображения. Он также предоставляет возможность указать ограничение области распознавания. 

Давайте вернёмся к классу VNDetectBarcodesReques. Один из интересных его аспектов — это ревизии алгоритмов распознавания штрих-кодов. На данный момент (iOS 17) существует четыре версии алгоритмов:

  1. VNDetectBarcodesRequestRevision1 доступен с анонса Vision, а это iOS 11, и поддерживает всего 17 видов ШК. В iOS 17 он помечен как deprecated.

Виды ШК

Aztec, Code128, Code39, Code39Checksum, Code39FullASCII, Code39FullASCIIChecksum, Code93, Code93i, DataMatrix, EAN13, EAN8, I2of5, I2of5Checksum, ITF14, PDF417, QR, UPCE.

  1. VNDetectBarcodesRequestRevision2 доступен уже с iOS 15 и поддержка штрих-кодов расширилась на 6 шт. и теперь составляет 23, а также обновились алгоритмы распознавания. 

Добавились:

GS1DataBarExpanded, MicroPDF417, GS1DataBar, Codabar, MicroQR, GS1DataBarLimited.

  1. VNDetectBarcodesRequestRevision3 доступен с iOS 16, поддерживает те же самые штрих-коды, что и первые две версии, был просто улучшен алгоритм распознавания.

  2. VNDetectBarcodesRequestRevision4 доступен с iOS 17, добавлена поддержка одного нового штрих-кода. Улучшена точность распознавания, а также добавлена поддержка инвертированного цвета. Кроме того, теперь появилась возможность распознавать комплексные штрих-коды. Подробнее об этом будет рассказано ниже.

Добавились:

MSIPlessey.

Остальные свойства не вызывают интереса, они подробно описаны в документации, поэтому я на них останавливаться не буду.

Сам по себе запрос работать не будет. Для выполнения поставленной задачи ему требуется handler. Vision предоставляет два вида handler, которые специализируются на конкретных работах: 

  • VNImageRequestHandler следует применять, если вам нужно обрабатывать одно изображение за раз. Изображение передаётся в момент инициализации обработчика.

  • VNSequenceRequestHandler следует применять, когда вам нужно обрабатывать серию изображений. Например, когда вам нужно искать объекты в изображениях, которые передаются напрямую с камеры в live-режиме. Отличие от первого обработчика только в том, что изображения передаются и обрабатываются путём их передачи через метод perform.

Эти обработчики (handlers) могут получать изображения из разных источников, начиная от URL и заканчивая CMSampleBuffer.

Важное замечание

До iOS 14, VNSequenceRequestHandler и VNImageRequestHandler не умели принимать CMSampleBuffer, а только CVPixelBuffer. Для перевода из одного формата в другой приходилось дополнительно использовать метод CMSampleBufferGetImageBuffer.

После обработки нашего запроса (VNDetectBarcodesRequest) мы получаем обработанные данные в формате VNBarcodeObservation. Как и Request, Observation строится на наследовании. Вот так будет выглядеть его иерархия:

Предлагаю немного рассмотреть их.

VNObservation — базовый класс, на котором строятся все наблюдения. У него всего три открытых свойства: confidence, timeRange и uuid. В контексте нашего сканера штрих-кодов нам интересно только одно свойство — confidence. Это свойство показывает, насколько Vision уверен в результатах, которые он обнаружил. Vision нормализует значение и в большинстве случаев предоставляет их в отрезке от 0 до 1. На практике свойство оказалось неоднозначным и при этом очень важным, более подробно на этом я остановлюсь в следующем разделе.

VNDetectedObjectObservation — класс, который определяет положение искомого объекта на изображении и предоставляет эти данные в формате CGRect.

VNRectangleObservation — данный класс предоставляет координаты вершин прямоугольника найденного объекта. Вершины представляют в формате CGPoint. 

VNBarcodeObservation — и, наконец, в этом классе находится вся нужная нам информация о найденном barcode. Тут нам больше всего интересно несколько свойств:

  • payloadStringValue — данные, которые находятся в ШК, предоставляются в формате String;

  • symbology — вид найденного ШК;

  • barcodeDescriptor — низкоуровневые данные, которые можно запихнуть в Core Image и получить изображение найденного ШК. Работает не со всеми видами ШК.

Кроме того, в iOS 17 и при использовании VNDetectBarcodesRequestRevision4 добавились новые свойства, которые позволяют работать с комплексными ШК формата GS1. Более подробно о том, что это за формат, можно узнать вот тут:

1234567890123|2D-data
1234567890123|2D-data
  • supplementalPayloadString — дополнительные данные, если смотреть на ШК выше, это 2D-data;

  • supplementalPayloadData — то же самое, что и выше, но уже в формате Data;

  • supplementalCompositeType — тип дополнительного штрих-кода;

  • isGS1DataCarrier — флаг указывает на то, что ШК относится к стандарту GS1;

  • payloadData — то же самое, что и payloadStringValue, но в формате Data.

История внедрения

В этом разделе я расскажу, с какими трудностями пришлось встретиться при внедрении и какие баги были побеждены. Небольшой спойлер: на какие-то грабли наступил по невнимательности, где-то не прочитал документацию, где-то не так её понял.

Начну, пожалуй, с того, что покажу вам наш дизайн нескольких экранов, где используется сканер.

Исходя из дизайна, можно увидеть 2 важные вещи:

  • сканер может занимать разную высоту на экране;

  • есть некая область сканирования, куда надо подносить штрих-код.

В старой реализации выделенная область сканирования была чисто декоративной штукой, по факту сканер искал штрих-код на всей доступной области.

В новой же реализации из-за того, что она стала работать лучше и точнее, было решено сделать честную область сканирования. Это необходимо, чтобы снизить риски ошибок сканирование некорректных штрих-кодов.

Как мы разобрали ранее, у VNDetectBarcodesRequest есть возможность искать в ранее заданной области. Но есть загвоздка — параметр может принимать только нормализованные координаты по отношению к изображению, которое подаётся на вход для обработки. После продолжительного гугления был найден интересный метод, который в теории может помочь: VNNormalizedRectForImageRect.

VNNormalizedRectForImageRect проецирует искомую область из координат изображения в нормализованные координаты. 

В итоге был получен такой код: 

func updateRegionOfInterest(_ regionRect: CGRect) { 
    var normalizedRect: CGRect = VNNormalizedRectForImageRect( 
        regionRect, Int(UIScreen.main.bounds.width), 
        Int(UIScreen.main.bounds.height) 
    ) 

    // if the preview is not on full screen, we flip the y coordinate and calculate it based on the height.
    if (UIScreen.main.bounds.width / view.bounds.height) > 1 { 
        normalizedRect = .init( 
            x: normalizedRect.origin.x, 
            y: 0.5 - (normalizedRect.height / 2), 
            width: normalizedRect.width,
            height: normalizedRect.height 
        ) 
    } 
    request?.regionOfInterest = normalizedRect 
}

И, как вы могли заметить, этот код ужасен и негибок, поддерживает только 2 дизайна и в случае, если область сканирования начнёт гулять по доступной области сканера, то этот код не будет корректно работать. Плюс держим в уме, что у AVCaptureVideoPreviewLayer могут быть разные режимы videoGravity, а этого данный метод не учитывает.

Честно говоря, этот найденный метод был «поворотом не туда», который стоил кучу времени.

Для более оптимального решения требовалось просто более внимательно посмотреть документацию AVCaptureVideoPreviewLayer. У него есть замечательный метод, который влёгкую решает эту проблему. Это metadataOutputRectConverted — в него просто необходимо передать необходимую нам область, и он уже переведёт всё в нормализованные координаты, а также учтёт выставленный режим videoGravity. В итоге код получается лёгким и понятным:

func updateRegionOfInterest(_ regionRect: CGRect) { 
    guard previewLayer != nil else { return } 
    let regionOfInterest = previewLayer.metadataOutputRectConverted(
        fromLayerRect: regionRect
    ) 
    request?.regionOfInterest = regionOfInterest
}

А сейчас предлагаю вернуться к параметру confidence у VNObservation. Вторая ошибка, которая была допущена при разработке, — это пренебрежение этой самой confidence.

Проблема выявилась на стадии тестирования. У нашей тестировщицы Анны стол «под дерево» и, когда сканер просто лежал на столе камерой вниз, он умудрялся находить ШК и получать из него данные.

Вот видео этого чудного поведения:

Проблема решается достаточно просто: нужно проверять полученные данные на какую-то минимальную уверенность. Но возникает вопрос, какова эта минимальная уверенность?

Если вернуться немного назад, на этап исследования решений, то этот параметр уже тогда начинал немного смущать:

Вот скрин того, о чём я говорю, здесь сканировался один и тот же штрих-код. Тут у нас выводятся найденные данные, тип штрих-кода, и confidence. Как вы могли заметить, при определённых обстоятельствах, будь то блик на ШК, неудачный угол и/или отдаление камеры, VNDetectBarcodesRequest может выдавать ошибочные значения. При этом значение confidence в данном конкретном примере максимум достигает до 0.64. 64% уверенности, что значение соответствует тому, что на закодировано на штрих-коде.

Также, если посмотреть на достоверные значения, то confidence может колебаться от ~0.5 до 1. Может, конечно, и меньше 0.5, но, по моим наблюдениями, чаще всего confidence находится в этом диапазоне.

Со значением в 1 не всё так однозначно: если посмотреть в документацию, то можно обнаружить следующую приписку: «Confidence can always be returned as 1.0 if confidence is not supported or has no meaning». То есть, если верить документации, то уверенность в 1.0 может вернуться в нескольких случаях:

  • сonfidence не поддерживается или не имеет значения, могу предположить, что данное утверждение относится к пользовательским моделям;

  • штрих-код суперкачественный, и тогда Vision на 100% уверен, что он прав. А это случается, когда штрих-код отображается на экране, либо это QR-код. А вот для напечатанных штрих-кодов, 100% уверенности не встречал ни разу. 

В итоге, так и не найдя правильного ответа на вопрос о минимальной уверенности, было решено ткнуть пальцем в небо, установить минимальную confidence в 0.6 и начать замерять, сколько «неправильных» штрих-кодов прилетает в запросах к бэку на самом популярном разделе приёмки. И в случае, если этот процент будет высок, через бэк изменять значение на клиенте и начать искать оптимальное значение.

К счастью, 0.6 хватило, процент неправильного сканирований в районе 3%, и жалоб по этой проблеме от пользователей не поступало.

Ну, и расскажу о последнем случае, который привёл к появлению вот такой вот задачи: 

Если вернуться немного к нашему старому сканеру, у него была определённая стратегия сканирования: нашёл штрих-код →  отправил дальше → обнаружил следующий → сравнил с последним отсканированным, если новый штрих-код такой же, как и предыдущий, → ждёт одну секунду, если он другой, то сразу отправляет данные. Эта стратегия хорошо работала, учитывая скорость старого решения, но вот с новым инструментом дала сбой.

Сбой заключался в том, что если одновременно в зоне сканирования находились 2 штрих-кода, то новый сканер начинал как из пулемёта отправлять данные дальше. А так как наша бизнес-логика не ожидала такого подвоха и на каждый полученный штрих-код ходила к бэку, у нас получился мини ддос бэка. Если бы этот баг не обнаружили бы на стадии тестирования и раскатили бы на пользователей, наш бэк, конечно, выдержал бы, но ситуация всё равно была бы не особо приятной. 

Поэтому было решено установить минимальное время между обработкой штрих-кодов. Получилась стратегия: 1 штрих-код в 1 секунду.

Бета-тест и эксперименты

Как я говорил ранее, сканер является важной частью нашего приложения, и на горизонте уже начинал маячить сезон больших распродаж, а значит, в это время будет большая нагрузка на ПВЗ. Хотелось окончательно убедиться, что наше новое решение принесёт больше пользы, чем вреда. Поэтому было решено протестировать его в два этапа:

  1. сначала отправить на бета-тест небольшой группе пользователей;

  2. раскатить новый сканер на 50% пользователей, промониторить графики и последить за реакцией пользователей.

Поскольку наше приложение ориентировано на рабочие процессы, а не на развлечения, поиск участников для бета-тестирования был непростой задачей. Спасибо большое Диане, нашей ПМ, которая все же смогла растормошить народ и найти авантюристов для бета-теста. В итоге бета-тест выглядел так:

Цифры, конечно, не особо большие. Многие, как всегда бывает, отвалились на этапе установки, но те, кто всё же установили и попользовались сканером несколько недель, дали положительный фидбэк.

Этот бета-тест окончательно убедил нас, что мы не зря заморочились и потратили время на реализацию нового сканера. Однако это все были субъективные отзывы, а нам хотелось получить больше цифр и установить более точный ответ. Поэтому пришло время экспериментов на большем количестве наших пользователей. 

Эксперимент был прост: взяли всех пользователей и разделили пополам, одним оставили старый сканер, другим дали новый сканер и стали замерять, сколько времени уйдёт на сканирование одного отправления.

Во время этого эксперимента были получены следующие графики. На них отображены одни и те же данные, просто представленные в разных визуальных форматах для простоты восприятия.

По графикам видно, что у нового сканер большой отрыв в начальных значениях. Если посмотреть на медианное значение, среднее не берём, так как данные сильно размыты по временной шкале, то выходит следующее:

  • медиана старого — 6.0

  • медиана нового — 4.0

Исходя из этого, можно предположить, что наше новое решение стало быстрее в 1.5 раза, чем старое.

Итоги

В заключение хочу сказать: хоть наш мобильный сканер далёк от идеала, но нам удалось немного облегчить жизнь работникам ПВЗ. Сканер стал лучше сканировать повреждённые и плохо напечатанные ШК, кроме того, улучшилась скорость сканирования. Это подтверждают как наши данные, так и значительное уменьшение жалоб в отношении качества сканирования.

Впереди, конечно, ещё много работы, в данный момент VNDetectBarcodesRequest работает в большинстве времени вхолостую, а отсюда вылезает повышенное энергопотребление и нагрев устройства. Но это уже другая история, которая будет дорабатываться.

Теги:
Хабы:
Всего голосов 17: ↑17 и ↓0+17
Комментарии8

Публикации

Информация

Сайт
ozon.tech
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия