Создайте Android-приложение для рукописного классификатора цифр с помощью MediaPipe Tasks.

1. Введение

Что такое MediaPipe?

MediaPipe Solutions позволяет применять решения на основе машинного обучения (ML) к вашим приложениям. Он предоставляет фреймворк для настройки предварительно созданных конвейеров обработки, которые обеспечивают немедленный, привлекательный и полезный результат для пользователей. Вы даже можете настроить эти решения с помощью MediaPipe Model Maker , чтобы обновить модели по умолчанию.

Классификация изображений — одна из нескольких задач машинного зрения, которые предлагает MediaPipe Solutions. MediaPipe Tasks доступен для Android, iOS, Python (включая Raspberry Pi!) и веб-приложений.

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

Что вы узнаете

  • Как интегрировать задачу классификации изображений в Android-приложение с помощью MediaPipe Tasks .

Что вам понадобится

  • Установленная версия Android Studio (данный практический пример был написан и протестирован с использованием Android Studio Giraffe).
  • Для запуска приложения потребуется устройство Android или эмулятор.
  • Базовые знания разработки под Android (это не "Hello World", но очень близко к нему!).

2. Добавьте задачи MediaPipe в приложение для Android.

Скачайте стартовое приложение для Android.

Этот практический урок начнётся с готового примера, позволяющего рисовать на экране. Вы можете найти это базовое приложение в официальном репозитории MediaPipe Samples здесь . Клонируйте репозиторий или скачайте ZIP-архив, нажав Code > Download ZIP.

Импортируйте приложение в Android Studio.

  1. Откройте Android Studio.
  2. На экране «Добро пожаловать в Android Studio» выберите «Открыть» в правом верхнем углу.

a0b5b070b802e4ea.png

  1. Перейдите в папку, куда вы клонировали или скачали репозиторий, и откройте каталог codelabs/digitclassifier/android/start .
  2. Убедитесь, что все открылось правильно, нажав на зеленую стрелку запуска . 7e15a9c9e1620fe7.png ) в правом верхнем углу Android Studio
  3. Вы должны увидеть, как приложение откроется с черным экраном, на котором можно рисовать, а также кнопкой «Очистить» , чтобы сбросить этот экран. Хотя на этом экране можно рисовать, это мало что меняет, поэтому мы начнем исправлять это прямо сейчас.

11a0f6fe021fdc92.jpeg

Модель

При первом запуске приложения вы можете заметить, что файл с именем mnist.tflite загружается и сохраняется в каталоге assets вашего приложения. Для простоты мы уже взяли известную модель MNIST, которая классифицирует цифры, и добавили её в приложение с помощью скрипта download_models.gradle в проекте. Если вы решите обучить свою собственную модель, например, для рукописных букв, то вам нужно будет удалить файл download_models.gradle , удалить ссылку на него в файле build.gradle на уровне приложения и изменить имя модели позже в коде (в частности, в файле DigitClassifierHelper.kt ).

Обновите файл build.gradle.

Прежде чем начать использовать MediaPipe Tasks, необходимо импортировать библиотеку.

  1. Откройте файл build.gradle, расположенный в модуле вашего приложения , затем прокрутите вниз до блока dependencies .
  2. Внизу этого блока вы должны увидеть комментарий со словами: // ШАГ 1 Импорт зависимостей .
  3. Замените эту строку следующим кодом реализации.
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Чтобы загрузить эту зависимость, нажмите кнопку «Синхронизировать сейчас» , которая появляется в баннере в верхней части Android Studio.

3. Создайте вспомогательный классификатор цифр для задач MediaPipe.

На следующем шаге вам нужно будет заполнить класс, который возьмет на себя основную работу по классификации с помощью машинного обучения. Откройте файл DigitClassifierHelper.kt и начнем!

  1. Найдите комментарий вверху класса, который гласит: // ШАГ 2 Создать слушатель
  2. Замените эту строку следующим кодом. Это создаст слушатель, который будет использоваться для передачи результатов из класса DigitClassifierHelper обратно туда, где эти результаты ожидаются (в данном случае это будет ваш класс DigitCanvasFragment , но мы к этому еще вернемся).
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. Также вам потребуется принять DigitClassifierListener в качестве необязательного параметра для класса:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. В строке ` // STEP 3 define classifier` добавьте следующую строку, чтобы создать заполнитель для класса ImageClassifier, который будет использоваться в этом приложении:

// ШАГ 3 определение классификатора

private var digitClassifier: ImageClassifier? = null
  1. Добавьте следующую функцию туда, где вы видите комментарий : // ШАГ 4 Настройка классификатора :
// STEP 4 set up classifier
private fun setupDigitClassifier() {

    val baseOptionsBuilder = BaseOptions.builder()
        .setModelAssetPath("mnist.tflite")

    // Describe additional options
    val optionsBuilder = ImageClassifierOptions.builder()
        .setRunningMode(RunningMode.IMAGE)
        .setBaseOptions(baseOptionsBuilder.build())

    try {
        digitClassifier =
            ImageClassifier.createFromOptions(
                context,
                optionsBuilder.build()
            )
    } catch (e: IllegalStateException) {
        digitClassifierListener?.onError(
            "Image classifier failed to initialize. See error logs for " +
                    "details"
        )
        Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
    }
}

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

val baseOptionsBuilder = BaseOptions.builder()
    .setModelAssetPath("mnist.tflite")

// Describe additional options
val optionsBuilder = ImageClassifierOptions.builder()
    .setRunningMode(RunningMode.IMAGE)
    .setBaseOptions(baseOptionsBuilder.build())

Этот блок определяет параметры, используемые классификатором изображений. Сюда входят модель, хранящаяся в вашем приложении ( mnist.tflite ) в разделе BaseOptions, и RunningMode в разделе ImageClassifierOptions, который в данном случае равен IMAGE, но VIDEO и LIVE_STREAM также являются дополнительными доступными параметрами. Другие доступные параметры — MaxResults, который ограничивает модель максимальным количеством возвращаемых результатов, и ScoreThreshold, который устанавливает минимальный уровень уверенности, который модель должна иметь в результате, прежде чем его вернуть.

try {
    digitClassifier =
        ImageClassifier.createFromOptions(
            context,
            optionsBuilder.build()
        )
} catch (e: IllegalStateException) {
    digitClassifierListener?.onError(
        "Image classifier failed to initialize. See error logs for " +
                "details"
    )
    Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
}

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

  1. Поскольку нам потребуется инициализировать ImageClassifier перед его использованием, вы можете добавить блок init для вызова функции setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Наконец, прокрутите вниз до комментария, в котором написано // ШАГ 5 создайте функцию классификации, и добавьте следующий код. Эта функция будет принимать объект Bitmap , в данном случае — нарисованную цифру, преобразовывать его в объект MediaPipe Image (MPImage), а затем классифицировать это изображение с помощью ImageClassifier, а также записывать время, необходимое для вывода, прежде чем вернуть результаты через DigitClassifierListener.
// STEP 5 create classify function
fun classify(image: Bitmap) {
    if (digitClassifier == null) {
        setupDigitClassifier()
    }

    // Convert the input Bitmap object to an MPImage object to run inference.
    // Rotating shouldn't be necessary because the text is being extracted from
    // a view that should always be correctly positioned.
    val mpImage = BitmapImageBuilder(image).build()

    // Inference time is the difference between the system time at the start and finish of the
    // process
    val startTime = SystemClock.uptimeMillis()

    // Run image classification using MediaPipe Image Classifier API
    digitClassifier?.classify(mpImage)?.also { classificationResults ->
        val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
        digitClassifierListener?.onResults(classificationResults, inferenceTimeMs)
    }
}

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

4. Выполнение вывода с помощью задач MediaPipe.

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

  1. В самом низу этого файла вы должны увидеть комментарий со словами: // ШАГ 6 Настройка слушателя . Здесь вы добавите функции onResults() и onError(), связанные с слушателем.
// STEP 6 Set up listener
override fun onError(error: String) {
    activity?.runOnUiThread {
        Toast.makeText(requireActivity(), error, Toast.LENGTH_SHORT).show()
        fragmentDigitCanvasBinding.tvResults.text = ""
    }
}

override fun onResults(
    results: ImageClassifierResult,
    inferenceTime: Long
) {
    activity?.runOnUiThread {
        fragmentDigitCanvasBinding.tvResults.text = results
            .classificationResult()
            .classifications().get(0)
            .categories().get(0)
            .categoryName()

        fragmentDigitCanvasBinding.tvInferenceTime.text = requireActivity()
            .getString(R.string.inference_time, inferenceTime.toString())
    }
}

Функция onResults() особенно важна, поскольку она отображает результаты, полученные от ImageClassifier. Поскольку этот коллбэк запускается из фонового потока, вам также потребуется выполнять обновления пользовательского интерфейса в потоке пользовательского интерфейса Android.

  1. Поскольку на предыдущем шаге вы добавляете новые функции из интерфейса, вам также потребуется добавить объявление реализации в начало класса.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. В верхней части класса вы должны увидеть комментарий со словами: // ШАГ 7a Инициализация классификатора . Здесь вы разместите объявление класса DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Переходим к // ШАГ 7b Инициализация классификатора. Инициализировать классификатор digitClassifierHelper можно внутри функции onViewCreated().
// STEP 7b Initialize classifier
// Initialize the digit classifier helper, which does all of the
// ML work. This uses the default values for the classifier.
digitClassifierHelper = DigitClassifierHelper(
    context = requireContext(), digitClassifierListener = this
)
  1. На заключительном этапе найдите комментарий // ШАГ 8a *: классификация* и добавьте следующий код для вызова новой функции, которую вы добавите чуть позже. Этот блок кода запустит классификацию, когда вы уберете палец из области рисования в приложении.
// STEP 8a: classify
classifyDrawing()
  1. Наконец, найдите комментарий // ШАГ 8b classify , чтобы добавить новую функцию classifyDrawing(). Она извлечет растровое изображение с холста, а затем передаст его в DigitClassifierHelper для выполнения классификации, чтобы получить результаты в функции интерфейса onResults().
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. Разверните и протестируйте приложение.

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

  1. Нажмите «Выполнить» ( 7e15a9c9e1620fe7.png ) в панели инструментов Android Studio для запуска приложения.
  2. Нарисуйте любую цифру на планшете и посмотрите, сможет ли приложение её распознать. Оно должно отобразить как цифру, которую, по мнению модели, была нарисована, так и время, затраченное на её предсказание.

7f37187f8f919638.gif

6. Поздравляем!

У вас получилось! В этом практическом занятии вы научились добавлять классификацию изображений в Android-приложение, а именно, классифицировать нарисованные от руки цифры с помощью модели MNIST.

Следующие шаги

  • Теперь, когда вы умеете классифицировать цифры, вы, возможно, захотите обучить собственную модель для классификации нарисованных букв, животных или бесчисленного множества других предметов. Документацию по обучению новой модели классификации изображений с помощью MediaPipe Model Maker можно найти на странице developers.google.com/mediapipe .
  • Узнайте о других задачах MediaPipe , доступных для Android, включая распознавание лицевых ориентиров, распознавание жестов и классификацию звука.

Мы с нетерпением ждём всех тех замечательных вещей, которые вы создадите!