MediaPipe Tasks で手書きの数字分類器を作成する Android アプリを作成する

1. はじめに

MediaPipe とは

MediaPipe Solutions を使用すると、アプリに機械学習(ML)ソリューションを適用できます。このソリューションが提供するフレームワークでは、事前構築済みの処理パイプラインを構成して、ユーザーに有益で魅力的な出力を迅速に配信できます。これらのソリューションを MediaPipe Model Maker でカスタマイズし、デフォルトのモデルを更新することもできます。

画像分類は、MediaPipe Solutions が提供できる ML ビジョンタスクの一つです。MediaPipe Tasks は、Android、iOS、Python(Raspberry Pi を含む)、ウェブで使用できます。

この Codelab では、画面に数字を描画できる Android アプリから始めて、それらの描画された数字を 0 ~ 9 の 1 つの値として分類する機能を追加します。

学習内容

  • MediaPipe Tasks を使用して Android アプリに画像分類タスクを組み込む方法

必要なもの

  • Android Studio のインストール バージョン(この Codelab は Android Studio Giraffe を使用して記述、テストされています)。
  • アプリを実行するための Android デバイスまたはエミュレータ。
  • Android 開発に関する基本的な知識(これは「Hello World」ではありませんが、それほど変わりません)。

2. MediaPipe Tasks を Android アプリに追加する

Android スターター アプリをダウンロードする

この Codelab では、画面に描画できる既製のサンプルを使用します。開始用アプリは、こちらの公式 MediaPipe サンプル リポジトリにあります。リポジトリのクローンを作成するか、[コード] >ZIP をダウンロード。

アプリを Android Studio にインポートする

  1. Android Studio を開きます。
  2. [Welcome to Android Studio] 画面で、右上にある [Open] を選択します。

a0b5b070b802e4ea.png

  1. リポジトリのクローンを作成またはダウンロードした場所に移動し、codelabs/digitclassifier/android/start ディレクトリを開きます。
  2. Android Studio の右上にある緑色の実行矢印(7e15a9c9e1620fe7.png)をクリックして、すべてが正しく開いたことを確認します。
  3. アプリが開き、描画できる黒い画面と、その画面をリセットする [Clear] ボタンが表示されます。その画面に描画することもできますが、それ以外は大きな効果がないため、これからこの問題を修正します。

11a0f6fe021fdc92.jpeg

モデル

アプリを初めて実行すると、mnist.tflite という名前のファイルがダウンロードされて、アプリの assets ディレクトリに保存されていることがわかります。わかりやすくするため、数字を分類する既知のモデル MNIST を、プロジェクトで download_models.gradle スクリプトを使用してアプリに追加しています。手書き文字用のモデルなど、独自のカスタムモデルをトレーニングする場合は、download_models.gradle ファイルを削除し、アプリレベルの build.gradle ファイルでそのモデルへの参照を削除し、コードの後半(特に DigitClassifierHelper.kt ファイル内で)でモデルの名前を変更します。

build.gradle を更新する

MediaPipe Tasks を使い始める前に、ライブラリをインポートする必要があります。

  1. app モジュールにある build.gradle ファイルを開き、execution ブロックまで下にスクロールします。
  2. このブロックの下部に「// STEP 1 Dependency Import」というコメントが表示されます。
  3. この行を次の実装に置き換えます。
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Android Studio の上部にあるバナーに表示される [Sync Now] ボタンをクリックして、この依存関係をダウンロードします。

3. MediaPipe Tasks の数字分類ヘルパーを作成する

次のステップでは、ML の分類に関する手間のかかる作業を担当するクラスに記入します。DigitClassifierHelper.kt を開いて、始めましょう。

  1. クラスの先頭にある「// STEP 2 Create リスナー」というコメントを探します。
  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())

このブロックは、ImageClassifier で使用するパラメータを定義します。これには、アプリ(mnist.tflite)内の BaseOptions に格納されたモデルと、ImageClassifierOptions の下の RunningMode が含まれます。この場合は IMAGE ですが、VIDEO と LIVE_STREAM も追加オプションとして利用できます。その他の利用可能なパラメータには、モデルが返す結果を最大数に制限する MaxResults と、ScoreThreshold があります。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 Tasks を使用して推論を実行する

このセクションを開始するには、Android Studio で DigitCanvasFragment クラスを開きます。このクラスで、すべての処理が行われます。

  1. このファイルの一番下に、「// STEP 6 Set up リスナー」というコメントがあります。ここでは、リスナーに関連付けられた 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 の UI スレッドでも UI の更新を実行する必要があります。

  1. 上記の手順でインターフェースから新しい関数を追加する場合は、クラスの先頭に実装宣言も追加する必要があります。
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. クラスの一番上に、「// STEP 7a Initialize classifier」というコメントが表示されます。このファイルに DigitClassifierHelper の宣言を配置します。
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. // ステップ 7b の [分類器を初期化する] に進むと、onViewCreated() 関数内で digitClassifierHelper を初期化できます。
// 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. 最後のステップとして、コメント「// STEP 8a*: classification*」を見つけて、以下のコードを追加して、後日追加する新しい関数を呼び出します。このコードブロックは、アプリの描画領域から指を離すと分類をトリガーします。
// STEP 8a: classify
classifyDrawing()
  1. 最後に、コメント // STEP 8b classify を探して、新しい classifyDrawing()関数を追加します。これにより、キャンバスからビットマップが抽出され、DigitClassifierHelper に渡され、分類が実行されて onResults() インターフェース関数で結果を受け取ります。
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. アプリをデプロイしてテストする

これで、画面上に描画された数字を分類できるアプリが完成しました。次に、アプリを Android Emulator または実際の Android デバイスにデプロイしてテストします。

  1. Android Studio ツールバーの実行アイコン(7e15a9c9e1620fe7.png)をクリックしてアプリを実行します。
  2. 描画パッドに任意の数字を描いて、アプリが認識できるかどうかを確認します。モデルが描画したと考えている数字と、その数字の予測にかかった時間の両方を表示する必要があります。

7f37187f8f919638.gif

6. 完了

これで完了です。この Codelab では、Android アプリに画像分類を追加する方法、特に MNIST モデルを使用して手描きの数字を分類する方法を学習しました。

次のステップ

  • 数字を分類できるようになったので、独自のモデルをトレーニングして、描画された文字や動物、その他無限にあるアイテムを分類できるようにしましょう。MediaPipe Model Maker を使用して新しい画像分類モデルをトレーニングする方法については、developers.google.com/mediapipe ページをご覧ください。
  • 顔のランドマーク検出、ジェスチャー認識、音声分類など、Android で使用できるその他の MediaPipe タスクについて学習する。

皆様の素晴らしい作品づくりを楽しみにしています。