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 の右上にある緑色の実行矢印(7e15a9c9e1620fe7.png)をクリックして、すべてが正しく開いたことを確認します。
  3. アプリが開き、描画できる黒い画面と、画面をリセットする [消去] ボタンが表示されます。この画面には描画できますが、それ以外の機能はほとんどありません。この問題を解決していきましょう。

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 で使用されるパラメータを定義します。これには、アプリ内に保存されているモデル(mnist.tflite)が BaseOptions に、RunningMode が 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 は使用前に初期化する必要があります。そのため、init ブロックを追加して setupDigitClassifier() を呼び出すことができます。
init {
    setupDigitClassifier()
}
  1. 最後に、// STEP 5 create classify function というコメントまでスクロールして、次のコードを追加します。この関数は、ビットマップ(この場合は描画された数字)を受け取り、それを 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 分類ツールを初期化するに移動し、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(顔のランドマーク検出、ジェスチャー認識、音声分類など)について学びます。

皆様が制作される素晴らしいコンテンツを楽しみにしております。