Hola, Amigos! На связи Александр Чаплыгин, Flutter-dev в Amiga. Недавно я выступал на конференции для разработчиков DevFest в Омске с докладом «Камера и Flutter». Решил поделиться с вами своим первым опытом выступления. Возможно, кому-то будет полезно понять, как это устроено изнутри. И расскажу про проект, в котором использовалась библиотека Google ML Kit Barcode Scanning.

Мы с командой Flutter-разработчиков Amiga ведем свой телеграм-канал Flutter.Много, где регулярно пишем интересные посты про кроссплатформенную разработку, делимся своим опытом, переводим статьи иностранных СМИ и анонсируем конфы, в которых будем участвовать. Подписывайтесь, нас там уже больше 1600!  

Немного о DevFest

Начну с того, что Омск – приятный город. Теплота Сибири, ровные дороги и отзывчивые люди. Город с очень богатой историей, которая не оставит вас равнодушным, если вы захотите в нее погрузиться. Забегая вперед, скажу, что приятные впечатления для меня оставил не только город, но и конференция, организованная компанией Effective. 

Для размещения нам предоставили только что построенную современную гостиницу, что с самого начала дало понять, что путешествие будет на должном уровне. DevFest был рассчитан на 3 дня и 10 треков, среди которых: 

  • Flutter;

  • iOS;

  • Android;

  • Аврора;

  • Golang;

  • Python;

  • Web;

  • и др.

Для меня, как для Flutter-разработчика, самым значимым был первый день Flutter-трека, где принимали участие такие компании как: Amiga, Effective, Яндекс.Про, Яндекс.Go. Это было мое первое выступление в качестве спикера и, надеюсь, не последнее ????

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

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

Для себя я бы выделил темы Тимура Моисеева (ML и Flutter, Amiga), Геннадия Евстратова (Flutter на Авроре, Яндекс.Go), Сергея Кольцова (Как одной командой писать полсотни приложений в 2 раза быстрее, Яндекс.Про).

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

У Геннадия была озвучена проблематика разработки на Flutter под Аврору (спойлер – трудно). Приобретение смартфона на Авроре и трудности с установкой такого эмулятора приводит к высокому порогу входа в эту нишу. А Сергей рассказал про преимущества Flutter перед нативом и про то, как легко билдить приложения под разные целевые аудитории. 

В своем выступлении я затронул тему камеры и Flutter. Работал над проектом под NDA, в котором использовалась библиотека Google ML Kit. Эта библиотека предназначается для распознавания различных объектов: текста, лиц, баркодов и т.д. Также я рассказал про то, как мы интегрировали КриптоПро SDK и подключили рутокен (nfc и носитель). 

Камера и Flutter 

Главный акцент моего выступления – библиотека Google ML Kit Barcode Scanning. Ее мы использовали для распознавания баркодов разных форматов и видов. Баркод – графическое средство представления цифровых, либо (и) буквенных данных. Библиотека распознает линейные и 2Д форматы баркодов. Из популярных форматов – это Datamatrix, QR-code, EAN-13, EAN-39. Вот так они выглядят:

То есть наша библиотека по сути считывает данный «штрихкод» и переводит его в численно-буквенное представление. Google ML Kit Barcode Scanning имеет высокую скорость обработки баркода, меньше 0,5 секунды, но она не без изъянов. Дело в том, что в численно-буквенном представлении Datamatrix код выглядит как то так:

{FNC1}010460843993429621JgXJ5.T<GS>93Mlcr

Жирным выделены разделители баркода. То есть все, что между ними – смысловые блоки. Серийный номер, крипточасть и другая информация о товаре (вес, рост, ширина, вышина, глубина и т.п.).

И проблема в том, что этот пакет подменяет разделитель FNC1 на GS, что создает неудобства для наших пользователей.

Решили мы данный изъян путем дополнительной отправки изображения на сервер, где ML сервера распознавала баркод в его настоящем виде. На картинке изображена схема обработки баркода в нашем приложении:

Тут, конечно, мы потеряли в производительности: по метрикам процесс обработки одного баркода с участием двух ML занимал от 6 до 10 секунд. Согласитесь, что долгое ожидание в приложении нервирует современного пользователя :)

Но после упрощения правил формирования баркодов пользователями, мы убрали этот запрос на сервер и получили прирост производительности. С участием только Google ML Kit это 4-5 секунд. При этом, заказчик хочет внедрить ML на сервере в наше приложение, что даст гарантию абсолютно правильного распознавания баркода и позволит нам уйти от использования сервиса Google в большом и серьезном приложении.

Расскажу про процесс интеграции этой самой библиотеки и распознавания баркода. Для начала мы получаем все нужные разрешения на доступ к камере. 

<uses-feature
   android:name="android.hardware.camera"
   android:required="true" />
<key>NSCameraUsageDescription</key>
<string>Need access to scan barcodes</string>

Затем подключаем сами библиотеки.

#camera
camera: ^0.10.5+4
google_mlkit_barcode_scanning: ^0.9.0

Дальше мы использовали mixin ввиду того, что у нас имеется множество страниц с камерами под разные нужды.

// Note: request() will check permission without request popup if already granted
 isPermissionGranted = await Permission.camera.request().isGranted;


 if (isPermissionGranted == true) {
   Future.delayed(const Duration(milliseconds: 200), () async {
     cameraController = await CameraUtils.getCameraController(
         ResolutionPreset.high, CameraLensDirection.back);
     await cameraController?.initialize(); // will throw CameraException if already disposed
     if (cameraController?.value.isInitialized == true) {
       if (mounted) {
         await cameraController?.lockCaptureOrientation(DeviceOrientation.portraitUp);
       }
       await _startImageStream();
       _stopped = false;
       if (mounted) {
         setState(() {});
       }
     }
   });
 } else {
   if (mounted) {
     setState(() {});
   }
   _stoppedByLifeCycle = true;
 }
});

По порядку:

  1. Проверяем разрешение на доступ к камере.

  2. Создаем контроллер. Инициализируем его. Запускаем Stream.

Future<void> _startImageStream() async {
 await guarded(() async {
   if (cameraController?.value.isStreamingImages == false) {
     await cameraController?.startImageStream((image) {
  if (!_isDetecting && DateTime.now().difference(_lastScan).inMilliseconds >= 200) {
         _toggleDetectionLock();
         handleCameraImage(image);
       }
     });
   }
 });
}
  1. Делаем проверку на то, распознается ли сейчас баркод и прошло ли больше 200 мс с последнего сканирования.

final barcodes = await ScannerUtils().detect(
 image: image,
 detectInImage: _barcodeDetector.processImage,
 imageRotation: controller.description.sensorOrientation,
);
// Удалим из результатов штрихкоды за прицелом сканера (с периферии экрана)
if (barcodes.isNotEmpty) {
 if (controller.description.sensorOrientation == 90 ||
     controller.description.sensorOrientation == 270) {
   _removePeripherialObjects(barcodes, image.height.toDouble(), image.width.toDouble());
 } else {
   _removePeripherialObjects(barcodes, image.width.toDouble(), image.height.toDouble());
 }
  1. Закидываем байты изображения с контроллера в нашу библиотеку. _barcodeDetector — экземпляр класса BarcodeScanner.

Future<List<Barcode>> detect({
 required CameraImage image,
 required Future<List<Barcode>> Function(InputImage image) detectInImage,
 required int imageRotation,
}) async {
 final results = <Barcode>[];
 final bytes = _concatenatePlanes(image.planes);


 results.addAll(await detectInImage(
   InputImage.fromBytes(
     bytes: bytes,
     metadata: buildMetaData(image, rotationIntToImageRotation(imageRotation)),
   ),
 ));


 if (results.isNotEmpty) {
   return results;
 }


 final img_lib.Image? img = convertCameraImageToImageColor(image, true, pngFormat: false);
 final List<Barcode> resultsWhite = await detectInImage(
   InputImage.fromBytes(
     bytes: img?.getBytes() ?? Uint8List.fromList([]),
     metadata: buildMetaData(image, rotationIntToImageRotation(imageRotation)),
   ),
 );
 results.addAll(resultsWhite);


 return results;
}
  1. Две проверки. Если ML сразу ничего не нашла, то инвертируем изображение и снова отправляем в ML.

void _removePeripherialObjects(List<Barcode> barcodes, double width, double height) {
 final currentWidth = width * 0.76;
 final squareRect = Rect.fromCenter(
     center: Offset(width / 2, height / 2), width: currentWidth, height: currentWidth);
 for (int i = 0; i < barcodes.length; i++) {
   final barcode = barcodes[i];
   final boundingBox = barcode.boundingBox;
   final intersect = squareRect.intersect(boundingBox);
   if (boundingBox == null ||
       intersect.width < boundingBox.width * 0.5 ||
       intersect.height < boundingBox.height * 0.5) {
     barcodes.removeAt(i);
     i--;
   }
 }
}
  1. Удаляем коды за «прицелом» — квадратом на экране. Если больше половины баркода заходит в «прицел» — сохраняем результат, если нет — удаляем.

Теперь мы можем обрабатывать любые баркоды, получать их численно-буквенное представление.

Пора подвести итоги. Мы внедрили библиотеку Google ML Kit Barcode Scanning и, знаете, я бы лучше написал 1000 строк кода, чем один раз выступил???? Но, как говорится, выходить из зоны комфорта необходимо, если ты хочешь расти. Я рад, что мне удалось посетить такое мероприятие, выступить в качестве спикера, и я очень благодарен организаторам. Надеюсь, что в будущем коммьюнити Flutter будет расти, и такие конференции станут чаще и доступнее для любого желающего их посетить.

Присоединяйтесь к нашему телеграм-каналу Flutter.Много, будем с вами чаще на связи!

Комментарии (4)


  1. TDMNS
    21.10.2023 03:57

    Я бы послушал вживую. Записи нет?

    У меня очень много вопросов, автор надеюсь получу ответы на них.

    1. Скорость сканирования замеряли и на iOS и на Android?

    2. Как считали скорость? Среднее взяли?

    3. Есть ли какая-то зависимость от чего-либо?

    4. Какая все таки финальная скорость сканирования? 4-5 секунд?

    5. Почему выбрали именно гугл сервис? Гугл разве единственный в этом секторе?


    1. kcliffor Автор
      21.10.2023 03:57

      Запись должна быть где-то, но не знаю где

      1) Точные метрики конкретно не делал, но на Android когда проверял, как я уже сказал точно меньше 0,5 с. Практически мгновенно. Могу дополнить статью метриками )

      2) Stopwatch и среднее )

      3) конкретно ML гугла если брать, то тут зависимость чисто от устройства пользователя. Но даже не на самых свежих девайсах распознавание не заставляет сильно ждать.

      Если говорим про весь процесс распознавания (c ML сервера и с получением инфо о товаре) , то тут все завязано больше на сервер естественно. Зачастую тупил. И как раз и доходило до 10 сек.

      4) Весь процесс сканирования (измерял с помощью Stopwatch) сейчас 4-5 сек, там много запросов именно с инфой о товаре. сам баркод распознается мгновенно.

      5)Все просто. Когда пришел на проект, сервис гугла уже был. Решил рассказать :) Глянул по поводу других ML, особо инфы не нашел.


  1. gun_dose
    21.10.2023 03:57

    Я так и не понял, зачем баркод отправлять на ML-сервер. Что мешает привести его к нужной форме прямо на устройстве?


    1. kcliffor Автор
      21.10.2023 03:57

      ML на сервере абсолютно верно распознает баркод, а вот гугла подменяет разделитель FNC1 на GS. До изменения правил, ML гугла служила только для того, чтобы понять, есть ли в объективе баркод