Прогонка моделей .tflite на Android с помощью MLKit

 В данной статье описан метод для прогонки модели .tflite на платформе Android с приминением MLKit

Распознование

Для того, чтобы модели .tflite давали верный результат - необходимо передавать им изображение в определенном формате. Обычно модели натренированы на определенном сете лиц, лэндмарки которых расположены в определенных координатах. Таким образом, модели ожидают, что в определенной точке будет определеннйы лэндмарк. Чтобы это сделать - необходимо совершить аффинное преобразование. Получив матрицу 3 на 3 - в kotlin мы сможем вызать функцию Bitmap.createBitmap(...,matrix=matrix), которая создаст новый Bitmap по заданной матрице преобразования. 

После того, как мы передадим MLKit наш Bitmap - на выход мы получим список лиц, который MLKit определил в нашей картинке, а также их лэндмарки, повороты и координаты. 

Чтобы получить конкретное лицо из изображения - необходимо обрезать картинку. Каждое лицо в MLKit содержит face.boundingBox - это прямоугольник, в координатах которого будет содержаться определенное лицо. Сделав отступ слева и сверху со значениями boundingBox .left и boundingBox.right с шириной и высотой boundingBox .width() и boundingBox.height() соответственно, с помощью Bitmap.createBitmap(...) мы получим Bitmap с конкретным лицом.

После этого необходимо совершить аффинное преобразование этого Bitmap. Как говорилось ранее, MLKit содержит лэндмарки каждого лица. Нам нужны лэндмарки глаз, носа и уголков рта. После преобразования они будут расположены в определенных координатах на новом Bitmap.

Аффинное преобразование на Python с испольхзованием numpy и остальных его прекрасных библиотек делается в ~70 строк кода. В котлине всё гораздо печальнее. Чтобы реализовать это без дополнительных библиотек - придетя вспомнить, как работать с матрицами.

Необходимо создать класс Matrix, который будет содержать следующие операции с ними:

  • Умножение матриц/матрицы на число
  • Деление матриц/матрицы на число
  • Детерминант матрицы
  • Обратная матрица
  • Вычитание матриц
  • Аналоги numpy.outer
  • Транспонирвание
  • Норма матрицы
  • Аналог numpy.mean
  • Ранг матрицы
  • Сингулярное разложение

Самой сложно и одновременно самой простой частью здесь является сингулярное разложение. На самом деле, учитывая, что у нас 5 лэндмарков, которые необходимо расположить  в определенные координаты - сингулярное разложение придется вычислять из матрицы 2x2. Так что писать общее решение, которое не особо-то и тривиально - нет смысла. Достаточно написать решение частного случая. Можно посмотреть алгоритм здесь(Сайт недоступен в РФ).

Теперь, зная алгоритм нахождения матрицы афинного преобразования и исползуя написанные на kotlin функции - можно относительно легко найти желаемое.

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

Готово! Теперь у нас есть изображение лица, лэндмарки которого выстроены в определенных координатах. Теперь обученные модели .tflite будут давать верные результаты.

Разницу можно увидеть между этими двумя изображениями. На картинке слева аффинного преобразования нет - лицо просто растянуто от минимальных и до максимальных по оси x координатам. На картинке справа оно присутствует

Инициализация и запуск моделей .tflite

Самый простой (но не лучший) способ прогнать модель - просто закинуть ее в папку res/ml/model.tflite. После этого в Android studio можно будет её открыть и увидеть что-то подобное:


Тут мы видим, что на вход должно подаваться изображение размером 200х200 пикселей. Цифра в конце 3 означает, что изображение цветное.

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

 

Тут мы создаём буффер указывая размеры. Проходимся по пикселям, нормализуем их и вставляем в наш буффер.

Отлично, теперь, как на предыдущей картинке мы можем выполнить функцию loadBuffer() - передав туда наш буффер, полученный из bitmapToBuffer().

Как видно, на выход подаётся outputFeature0. Теперь нужно узнать, какие данные можно получить из этой модели. Тут нам понадобится питон.

Этот код поможет нам узнать входные и выходные данные модели.

Видно, что на вход подается float32, как и было сделано, а на выход подаётся одномерный массив с одним значением float32. Это значение и определяет возраст. 

Это был самый просто способ и не самый лучший. Не самый лучший он потому, что работает только на CPU и на GPU его переключить никак нельзя. Правильным способом будет использование TFLite Interpreter, которому можно задать настройки использования CPU, GPU и т.д

Вывод

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