Всем привет! Меня зовут Андрей Нестеров, я занимаюсь компьютерным зрением в применении к мобильным приложениям (ML на конечных устройствах) в компании Friflex и работаю продуктами по оцифровке спорта. Я стал замечать, что в обычной жизни не хватает технологий компьютерного зрения. Например, мне бы хотелось замерять, сколько времени я провожу за компьютером или трачу на сон. Но отслеживать эти действия можно и самостоятельно. С тех пор я начал думать о том, что действительно будет полезным, какую проблему можно было бы успешно решить с помощью технологий. Такая проблема вскоре нашлась.
Проблема: поддержать здоровье морской свинки
Моя морская свинка по какой-то причине не стачивает зубы естественным путем. Каждые две-три недели ее зубы отрастают и начинают вызывать раздражение полости рта, из-за чего она не может ни есть, ни пить. Свинку приходится везти к ветеринару на обточку зубов, после чего уже на следующий день она снова начинает нормально питаться. Но для здоровья морской свинки большие перерывы между приемом пищи нежелательны. Поэтому задача заключается в том, чтобы максимально быстро определить, когда свинка перестала есть и сразу отвести ее к ветеринару. Самостоятельно следить за такой проблемой не так сложно, но есть вероятность заметить ее слишком поздно.
Решение: мобильное приложение на ОС Android для мониторинга состояния морской свинки
Возьмём мобильный телефон, прикрепим его к штативу и поставим над клеткой так, чтобы камера телефона была направлена на место обитания свинки. Для распознавания будем использовать сверточную нейросеть, для сбора данных – Android приложение, подключенное к s3 серверу.
Скрытый текст
Сбор данных
Напишем простое приложение на Android для сбора данных, которые мы будем использовать для тестирования качества.
Заранее настроим s3 сервер на амазоне. S3 – Simple Storage Service позволяет бесплатно (в ограниченном объёме бесплатно, далее за деньги) хранить данные в облаке, а также иметь возможность обращаться к хранилищу через терминал, исполняемые скрипты, а также мобильные приложения. Инструкцию по настройке s3 можно найти тут.
Наиболее удобный вариант для настройки автоматической выгрузки данных с приложения – библиотека Amplify. Она позволяет подключать приложение к s3 серверу и закидывать на него кадры.
Мы хотим каждые несколько секунд сохранять кадр с камеры и отправлять его на сервер. Во время работы приложения камера телефона считывает кадры, мы ведём счётчик и каждый n-й кадр сохраняем в память телефона. Далее вызываем метод выгрузки файла на сервер для этого сохраненного кадра.
В Android Studio создадим новый проект с Empty Activity. Это позволит нам сразу сосредоточиться на коде сбора данных. На данном этапе мы имеем MainActivity.java. Зададим в ней поле класса, отвечающее за генерацию рандомного названия файла:
private static String imageName = new String();
Для того чтобы иметь возможность использовать Amplify, его нужно инициализировать в начале работы активити. В методе onCreate, который вызывается при запуске активити, добавляем:
Amplify.addPlugin(new AWSCognitoAuthPlugin());
Amplify.addPlugin(new AWSS3StoragePlugin());
Amplify.configure(getApplicationContext());
Также для того чтобы андроид студия знала, откуда вызывать библиотеку Amplify, добавим зависимость в build.gradle (Module).
implementation 'com.amplifyframework:aws-storage-s3:1.30.0'
implementation 'com.amplifyframework:aws-auth-cognito:1.30.0'
Определим метод, загружающий файл из памяти устройства на сервер.
// Uploads file to s3
private void uploadFile(File file) {
Uri uri = Uri.fromFile(file);
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
Amplify.Storage.uploadInputStream(imageName + “.png”, inputStream,
result -> { Log.i(“LOAD_TAG”, “Successfully uploaded: ” + imageName); },
error -> { Log.e(“LOAD_TAG”, “Upload failed”, error); }
} catch (FileNotFoundException e) {
Log.i(“SAVE_TAG”, e.getMessage());
}
);
}
Теперь мы можем написать финальный метод сохранения и загрузки файлов на сервер, который будет вызываться каждый n-й кадр.
private void saveImage(Bitmap image) {
imageName = UUID.randomUUID().toString();
String savePath = imageName + ".png";
File file = new File(getExternalFilesDir("/").getAbsolutePath(), savePath);
try {
FileOutputStream out = new FileOutputStream(file);
image.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
uploadFile(file);
} catch (IOException e) {
Log.i("WRITE_TAG", e.getMessage());
}
}
Для работы с камерой официальная документация Android предлагает три библиотеки: Camera, Camera2, CameraX. Camera – уже устаревшая и deprecated, Camera2 – довольно сложный вариант для тех, кто только начинает работать с камерой Android, а вот CameraX – идеальное решение. В ней можно настроить стандартный image capture буквально в несколько строк. Лучший способ разобраться с тем, как настроить камеру – официальная документация Android.
Обучение
В качестве эксперимента в данной задаче будем обучать нейросеть только на синтетических данных, а тестировать уже на реальных. Для генерации данных возьмем Blender – движок для работы с 3D объектами. Полноценный обзор о том, как работать с Blender в задачах компьютерного зрения можно посмотреть в статьях моего коллеги Глеба.
Для того, чтобы сгенерировать изображение морской свинки, необходима 3D модель. Для каждого кадра мы можем регулировать: угол обзора, позвоночник свинки, мощность и цвет освещения.
На выходе получаем готовые изображения, которые будут поступать на вход нейросети во время обучения.
После того, как мы определились с данными, необходимо подготовить нейросеть. Для обучения воспользуемся простым фреймворком Keras, в котором можно самостоятельно создать архитектуру нейросети. Архитектура должна быть легковесной, чтобы можно было имплементировать ее в мобильное приложение, но в то же время не слишком простой, т.к. для детекции момента касания свинки к поилке должна быть довольно хорошая точность.
Создадим кастомную архитектуру, в которой на входе принимается изображение (128, 128, 3), а на выходе – четыре числа, [x1, y1, x2, y2] – координаты точек головы и центра свинки.
from tensorflow.keras.layers import Conv2D, MaxPooling2D, BatchNormalization, Flatten, Dense, Input
from tensorflow.keras import Model
def build_model():
inputs = Input((128, 128, 3))
x = Conv2D(16, (3,3), padding='same', activation='relu')(inputs)
x = MaxPooling2D((2,2))(x)
x = BatchNormalization()(x)
x = Conv2D(16, (3,3), padding='same', activation='relu')(x)
x = MaxPooling2D((2,2))(x)
x = BatchNormalization()(x)
x = Conv2D(32, (3,3), padding='same', activation='relu')(x)
x = MaxPooling2D((2,2))(x)
x = BatchNormalization()(x)
x = Conv2D(32, (3,3), padding='same', activation='relu')(x)
x = MaxPooling2D((2,2))(x)
x = BatchNormalization()(x)
x = Conv2D(64, (3,3), padding='same', activation='relu')(x)
x = MaxPooling2D((2,2))(x)
x = BatchNormalization()(x)
x = Conv2D(64, (3,3), padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = Flatten()(x)
x = Dense(128, activation=None)(x)
outputs = Dense(4, activation=None)(x)
model = Model(inputs, outputs)
return model
В задачах обучения нейросетей часто возникают ситуации, когда обучающая выборка не отражает всех закономерностей генеральной совокупности данных. В нашей задаче преобладает большое разнообразие условий освещения на реальных данных. Одно из возможных решений этой проблемы — аугментация данных: искусственное увеличение обучающей выборки путем применения различных преобразований к исходных данным.
Воспользуемся библиотекой Albumentations, для того чтобы во время обучения применять преобразования ColorJitter, HueSaturationValue, RGBShift, RandomGamma, RandomBrightnessContrast и прочие. Наглядные примеры того, какие аугментации существуют и как они выглядят, можно найти на сайте этой библиотеки в разделе Demo.
Так одна картинка из реальных данных может выглядеть каждый раз по-новому в зависимости от применяемых аугментаций.
Разделим обучающие данные на train и validation, включим callback, который остановит обучение, если функция потерь на валидационной выборке val_loss будет стагнировать долгое время, и запустим обучение.
На выходе получаем .hdf5 файл с моделью обученной нейросети.
Внедрение и проверка
После того, как модель готова, можем внедрить ее на мобильное устройство и начинать проверку.
Для внедрения снова создадим проект в Android Studio.
Модели, обученные при помощи Keras, можно внедрить в мобильное приложение, используя TFLite – фреймворк для запуска нейросетей на мобильных устройствах.
Для начала сконвертируем модель в формат TFlite:
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
За запуск предсказаний на входящих изображениях будет отвечать интерпретатор TFLite - interpreter. Инициализируем его в поле класса MainActivity нашего приложения:
private static Interpreter interpreter;
Далее создаем папку assets (на папке app - Folder - Assets Folder).
В этой папке складываем нашу .tflite модель.
Воспользуемся методами, описанными в официальной документации.
В onCreate главной активити загружаем модель методом init
init(getAssets(), "model.tflite");
На каждом поступающем кадре вызовем runDetection, который принимает на вход Bitmap – структуру данных, которая по своей сути является простым изображением.
На выходе детекции получаем четыре числа – координаты точек головы и центра морской свинки.
Момент, когда точка головы будет ближе к поилке, чем заранее заданный порог – определим как процесс питья.
Поставим временной промежуток, в течение которого свинка должна подойти к поилке – 8 часов. Если в течение промежутка не было ни одного срабатывания, то выведем на экран сообщение и включим звук, обозначающий проблему.
Вот таким образом, используя простую нейросеть и мобильное приложение, мы настроили автоматическую проверку состояния морской свинки. Теперь за здоровьем морской свинки наблюдает компьютерное зрение.
Если у вас остались вопросы по интеграции компьютерного зрения в мобильное приложение, пишите в комментариях!
Если вы хотите присоединиться к нашей ML-команде, присылайте ваше резюме на hr@friflex.com
P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!