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 の単一の値として分類する機能を追加します。

学習内容

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

必要なもの

  • Android Studio のインストール済みバージョン(この Codelab は Android Studio Giraffe で作成およびテストされています)。
  • アプリを実行するための Android デバイスまたはエミュレータ。
  • Android 開発に関する基本的な知識(「Hello World」ほど簡単ではありませんが、それほど難しいものでもありません)。

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

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

この Codelab では、画面に描画できるサンプルを最初に使用します。このスターター アプリは、公式の MediaPipe サンプル リポジトリ(こちら)で確認できます。リポジトリのクローンを作成するか、[Code] > [Download ZIP] をクリックして zip ファイルをダウンロードします。

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

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

a0b5b070b802e4ea.png

  1. リポジトリのクローンを作成またはダウンロードした場所に移動し、codelabs/digitclassifier/android/start ディレクトリを開きます。
  2. Android Studio の右上にある緑色の [run] 矢印(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 ファイルを開き、dependencies ブロックまでスクロールします。
  2. このブロックの下部に // STEP 1 Dependency Import というコメントが表示されます。
  3. この行を次の実装に置き換えます。
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Android Studio の上部に表示されるバナーの [Sync Now] ボタンをクリックして、この依存関係をダウンロードします。

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

次のステップでは、機械学習分類の重い処理を行うクラスを入力します。DigitClassifierHelper.kt を開いて、始めましょう。

  1. クラスの上部にある // STEP 2 Create listener というコメントを見つけます。
  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. コメント // STEP 4 set up classifier がある場所に次の関数を追加します。
// 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 で使用されるパラメータを定義します。これには、BaseOptions の下にあるアプリ内に保存されたモデル(mnist.tflite)と、ImageClassifierOptions の下にある RunningMode が含まれます。この場合、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 は使用前に初期化する必要があるため、setupDigitClassifier() を呼び出す init ブロックを追加できます。
init {
    setupDigitClassifier()
}
  1. 最後に、// STEP 5 create classify function というコメントまでスクロールし、次のコードを追加します。この関数は 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 listener というコメントがあります。ここでは、リスナーに関連付けられた 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. // STEP 7b Initialize classifier に移動すると、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*: classify* を見つけ、次のコードを追加して、後で追加する新しい関数を呼び出します。このコードブロックは、アプリの描画エリアから指を離すと分類をトリガーします。
// 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 Tasks について学習します。

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