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

TL;DRПрокрутите статью вниз, чтобы получить пример кода и краткое изложение того, что я сделал

Моей задачей было реализовать определение живости лица во Flutter. Этот процесс предназначен для проверки живости селфи. Раньше от нас требовалось иметь при себе удостоверение личности гражданина и встречаться с банковским кассиром в физическом отделении, чтобы открыть банковский счет. Теперь мало кто идет в банк, чтобы сделать это. Личная встреча заменена процессом eKYC. Цифровая система сравнивает лицо с лицом на вашем удостоверении личности, чтобы подтвердить, что это тот же человек. Между тем, нам нужно убедиться, что отправленное нам изображение лица является реальным лицом, а не неподвижной фотографией. Вот почему нам нужно ML на устройстве, чтобы обнаружить это. Мы должны получить изображение, отправить его в ML, проверить изображение и ответить Flutter.

Первая версия

Я сделал снимок, установив таймер на селфи каждую секунду, затем преобразовал их в Uint8List, двоичный файл во Flutter и отправил его в Native, чтобы восстановить изображение для загрузки в ML. Эта первая версия хорошо работала только для устройств высокого класса, потому что, когда вы делаете фотографию, системе требуется около 300–400 мс, чтобы отправить фотографию во Flutter, и около 600 мс остается до того, как будет сделана следующая фотография. Моему iPhone 11 Pro Max потребовалось около 300–400 мс, тогда как iPhone X потребовалось около 1200 мс, что было недостаточно быстро, чтобы считать его в реальном времени. Более того, в iOS есть звук затвора, когда вы делаете селфи. Это создает неприятный опыт для пользователей, когда они выполняют проверку лица. Тем не менее, эту версию можно передать тестировщику для проверки функции реалистичности лица.

Вторая версия

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

В камере Flutter https://pub.dev/packages/camera есть функция startImageStream, которая передает изображение во Flutter.

controller.startImageStream((cameraImage) async {
   // Feed image into ML
});

Это вернет CameraImage. Когда дело доходит до изображений ML, большинство людей, вероятно, будут использовать Firebase ML для Flutter, который уже принимает CameraImage. Вы можете передать это в ML и вернуть результат для отображения в пользовательском интерфейсе.

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

Большинство SDK требуют, чтобы вы загружали формат RGB, JPG или PNG. В большинстве случаев мы используем JPG, потому что он меньше. CameraImage также поддерживает JPG, но только в Android, а не в iOS. Я начал с настройки Android для получения изображения JPG. Для iOS я прочитал документы камеры и узнал, что они поддерживают два формата: YUV420 и BGRA8888. Я решил использовать BGRA8888, потому что он больше похож на изображение, а другой больше похож на видео.

Android — это уже JPG, так что я мог без проблем скормить его тому же ML. Это iOS, в которой мне нужно было понять, как преобразовать эти два формата в JPG. Я провел небольшое исследование и нашел это.

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

Чтобы исправить это, я добавил функцию дросселирования во Flutter. Для каждого изображения с камеры, отправленного из Flutter, я преобразовывал бы его в изображение только раз в 500 мс. Flutter возвращает изображение каждые 20–30 мс. После проб и ошибок я понял, что 30 мс и 500 мс не так уж отличаются для моего варианта использования. Так как наше лицо оставалось в середине экрана и не двигалось так быстро, пользователь не чувствовал никакой задержки.

После того, как я столкнулся с iOS, я решил вместо этого перейти на Android, полагая, что мне нужно заставить его работать по крайней мере для одной платформы, чтобы позже я мог сосредоточить все свое внимание на другой. В Android флаг JPG работал хорошо, и производительность была довольно хорошей. Хотя у меня, похоже, возникла небольшая проблема с изображениями, которые я отправлял в Kotlin: в итоге они были повернуты на 90 градусов. Честно говоря, я вообще в этом не разбираюсь. Зачем вам отправлять нам изображение под углом 90 градусов из Flutter? 😢 В любом случае, у них должна быть причина для этого, поскольку это библиотека, которую используют люди во всем мире. В итоге я использовал код Kotlin для создания Bitmap и повернул изображение на 270 градусов с помощью Matrix, прежде чем передать его в ML. Android среднего уровня все еще немного отставал, так как весь процесс создания изображений, вращения изображений и преобразования в двоичный файл потреблял много энергии. Тем не менее, это все же лучше, чем сделать серию фотографий и отправить в ОД. Android высокого уровня работал нормально, средний уровень немного отставал, а низкий уровень был непригоден для использования. Тем не менее, это было достаточно хорошо в то время.

Возвращаясь к iOS, мне пришла в голову идея создания потоков во Flutter. Я следовал инструкции из этой статьи.



Обычно во Flutter мне почти не нужна многопоточность, потому что функция await не блокирует поток, в то время как для iOS нам приходится создавать многопоточность и постоянно помещать в поток обширные задачи.

Я решил впервые использовать многопоточность в Dart. Следуя приведенной выше статье, я получил образец с decodeImage и использовал библиотеку изображений от Flutter для преобразования BGRA в RGB.



Ух ты! Результат был совершенно другим. Мой iPhone X был, как сказали бы BTS, гладким, как масло 😄

Теперь, когда моя вторая версия доказала, что она может поддерживать iOS и Android от среднего до высшего уровня, пришло время протестировать ее на более широкой аудитории.

После более широкого тестирования я обнаружил критическую проблему: флаг JPG в ImageFormatGroup.jpeg не поддерживается на некоторых устройствах!!!



Почему я получаю неверный «формат в журналах во время потоковой передачи с камеры?
Спасибо за ответ на вопрос о переполнении стека! Пожалуйста, обязательно ответьте на вопрос. Предоставьте подробности и поделитесь…stackoverflow.com»



Я столкнулся с этой ошибкой GetYUVPlaneInfo: Invalid format passed: 0x21

Из 20 Android-устройств только Xiaomi Note 8 зависал при попытке стримить изображение для JPG. Я не знаю, возникла ли эта проблема только для этой модели или были бы другие устройства, у которых была бы эта проблема, если бы я запустил эту функцию в производство.

После некоторых исследований кажется, что если я перейду на ImageFormatGroup.yuv420, это не создаст никаких проблем. Я не хотел рисковать и продвигать эту функцию в производство, поэтому вместо этого я решил использовать YUV420, который является форматом по умолчанию для Flutter. Проведя небольшое исследование, я получил этот код из Интернета. При этом я мог бы отправить CameraImage через канал метода на Android для создания изображения.

List<int> strides = Int32List(image.planes.length * 2);
int index = 0;
List<Uint8List> data = image.planes.map((plane) {
   strides[index] = (plane.bytesPerRow);
   index++;
   strides[index] = (plane.bytesPerPixel)!;
   index++;
   return plane.bytes;
}).toList();
await _channel.invokeMethod<Uint8List>("checkLiveness", {
  'platforms': data,
  'height': image.height,
  'width': image.width,
  'strides': strides
});

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

Что касается Android, я получил код для преобразования YUV в JPG с этой страницы.



После внедрения эта функция работала нормально, хотя она все еще отставала, поскольку до сих пор мы решали проблему с JPG в Android.

Теперь пришло время улучшить производительность для лучшего взаимодействия с пользователем.

Я изучил сопрограмму Kotlin для работы с потоками и применил ее к своему коду Kotlin, чтобы сделать процесс более плавным. Результат был положительным: Android стал намного более плавным и нормально работал на всех уровнях. Даже самый нижний уровень остается гладким. Мой Oppo a3s может работать с очень незначительной задержкой.

Финальная версия версия

На этот раз я исследовал низкоуровневые iOS: iPhone 6s, 6s Plus и 7.

Я обнаружил, что даже Isolate работает нормально, без задержек для среднего уровня, но не для низкого уровня. Чтобы Isolate завершил свою задачу, потребовалось около 1-1,5 секунды, и еще 1,5 секунды для обнаружения живости, что было слишком много для пользователей на более медленных устройствах. Вместо этого я решил перейти на родной, и производительность там превзошла все мои ожидания. Оно сократилось с 1,5 секунды до примерно 0,01 секунды!!! Я поделился кодом для преобразования его в репозиторий в конце статьи. Вот пример кода для преобразования изображений.

private func bytesToPixelBuffer(width: Int, height: Int, baseAddress: UnsafeMutableRawPointer, bytesPerRow: Int) -> CVBuffer? {
   var dstPixelBuffer: CVBuffer?
   CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height,    kCVPixelFormatType_32BGRA, baseAddress, bytesPerRow,
   nil, nil, nil, &dstPixelBuffer)
   return dstPixelBuffer ?? nil
}
private func createImage(from pixelBuffer: CVPixelBuffer) -> CGImage? {
   var cgImage: CGImage?
   VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil,    imageOut: &cgImage)
   return cgImage
}
private func createUIImageFromRawData(data: Data, imageWidth: Int, imageHeight: Int, bytes: Int) -> UIImage? {
   data.withUnsafeBytes { rawBufferPointer in
      let rawPtr = rawBufferPointer.baseAddress!
      let address = UnsafeMutableRawPointer(mutating:rawPtr)
      guard let pxBuffer = bytesToPixelBuffer(width: imageWidth, height: imageHeight, baseAddress: address, bytesPerRow: bytes), let cgiImage = createImage(from: pxBuffer) else {
      return nil
   }
   return UIImage(cgImage: cgiImage)
}

Последняя проблема и последняя большая стена: Flutter image stream.

Я попытался создать пустой проект Flutter, реализовал камеру Flutter и открыл поток изображений, не выполняя никаких задач. Тем не менее, он все еще отставал от низкоуровневых iPhone. Если я отключил поток изображений, предварительный просмотр стал плавным, как на других iPhone. Но как я могу определить живучесть без получения изображения из потока изображений?

В конце концов, я придумал способ решить эту проблему: запустив поток изображений и выключив его через 50 мс. При внимательном рассмотрении приложение Flutter отстает на 50 мс, но в большинстве случаев пользователь этого не почувствует. За эти 50 мс я получил 1–2 изображения с камеры, которые затем передал в ML.

После его реализации, несмотря на небольшое отставание, мой iPhone 7 Plus смог определить живость лица. Что касается iPhone 6s и 6s Plus, то они стали намного лучше. Вы можете увидеть результат ниже.

Проверка производительности

iPhone 11 Pro Max — 53% загрузки ЦП, 190 МБ памяти, изображение возвращается каждые 20–40 мс.

iPhone 6s — 118% загрузки ЦП, 160 МБ памяти, изображение возвращается каждые 20–30 мс.

iPhone 6s Plus — 138% ЦП, 187 МБ памяти, изображение возвращается каждые 20–50 мс.

iPhone 6s Plus без потока камеры, открытый только предварительный просмотр камеры, потребляющий 46% ЦП

iPhone 6s Plus с включенной и выключенной трансляцией, она колеблется от 50% до 70%

Что касается Android, мое низкоуровневое устройство Oppo A3s (2018 г.), Andriod 8.1.0, Ram 2 ГБ работает лучше, чем мой iPhone 6s. Я постоянно открываю поток изображений, и он потребляет всего 14% ЦП и около 320 МБ оперативной памяти.

Между тем, iPhone 6s Plus со штатной камерой потребляет всего 57% процессора, 39,1 МБ.

Наконец-то все отлично работает на iOS среднего уровня и Android всех уровней. У меня осталась одна проблема с UIImage из кода, которым я делюсь выше. Когда liveness вернул изображение, которое нам нужно было использовать, я получил сообщение об ошибке Thread 1: EXC_BAD_ACCESS.

Я погуглил CGDataProvider_BufferIsNotBigEnough, чтобы узнать, как это исправить, но не смог найти ничего, связанного с моей проблемой. Некоторые упомянули, что изображение было слишком большим или ваш телефон устарел! Даже у моего самого быстрого устройства, iPhone 11 Pro Max, все еще была проблема, так что это определенно было не так. Я испробовал много способов, таких как подача изображений меньшего размера и подача изображений в живую жизнь намного медленнее, но ни один из них не увенчался успехом. Я был совершенно пуст. Я понятия не имел, что это такое. Это живость или мой код? После пары дней изучения следов и ошибок я, наконец, понял, что это были изображения, которые я загружал в SDK, которые возвращались, когда живучесть была успешной. Это изображение было отправлено в ссылочном типе вместо типа значения. Затем я посмотрел, как скопировать ссылочный тип в тип значения, и нашел функцию глубокого копирования для PixelBuffer. Я добавил его в свой проект, и он окончательно решил проблему доступа к памяти. Теперь все в порядке, фуууу

Окончательный вердикт!!

TL;DRНе выполняйте машинное обучение в реальном времени во Flutter, выполняйте все процессы в обычном представлении и отправляйте результат обратно во Flutter. Это намного проще, чем пытаться заставить это работать во Flutter.

Предполагается, что Flutter сокращает время разработки. Если это не может служить нашей цели, мы просто делаем это изначально. Ничего страшного, это не серебряная пуля, которая решает все проблемы 😁 Я считаю, что время, которое я трачу на Flutter для решения всех этих проблем, можно решить с помощью нативного кода.

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



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

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

Сводка

Изображение камеры Flutter не подходит для бюджетных устройств, потому что оно постоянно отправляет потоковые изображения обратно во Flutter. Я считаю, что если у камеры Flutter может быть параметр частоты кадров, который они могут вернуть, это очень поможет. Даже если бы я сделал троттлинг, Flutter все равно пришлось бы обрабатывать ненужные мне служебные изображения. Вместо того, чтобы получать изображение каждые 20–30 мс, если бы я мог вместо этого получать изображение каждые 200 мс, это уменьшило бы работу на 90%!!!

Этот запрос был открыт в течение более 2 лет.



Установка пользовательской частоты кадров и скорости передачи данных в подключаемом модуле «Камера · Проблема № 54339 · Флаттер/флаттер
Новая функция Ничего не сломано; запрос на новую возможность. Подключаемый модуль камеры p: собственные подключаемые модули, разработанные…github.com»



Я не думаю, что это произойдет в ближайшее время, так что давайте пока сделаем нативные. Я хотел бы открыть MR, чтобы добавить функциональности этой камере, но давайте посмотрим, будет ли у меня возможность это сделать.

Любой, кто заинтересован во внедрении живого лица в свой проект во Flutter или Native, может связаться с KBTG или со мной. Я передам ваш запрос нашей команде. Вот наш тест KBTG Face Liveness, ISO 30107–3, протестированный iBeta.

Справочный веб-сайт:



Хотите прочитать больше подобных историй? Или быть в курсе последних тенденций в мире технологий? Не забудьте посетить наш веб-сайт для получения дополнительной информации по адресу www.kbtg.tech