使用 MediaPipe Tasks 建構手寫數字分類器 Android 應用程式

1. 簡介

什麼是 MediaPipe?

MediaPipe Solutions 可讓您將機器學習 (ML) 解決方案套用至應用程式。並提供用於設定預先建構處理管道的架構,以便向使用者提供立即、引人入勝且實用的輸出內容。您還可以使用 MediaPipe Model Maker 自訂這些解決方案,藉此更新預設模型。

圖片分類是 MediaPipe Solutions 提供的幾個機器學習視覺工作之一。MediaPipe Tasks 支援 Android、iOS、Python (包括 Raspberry Pi!) 和網路。

在本程式碼研究室中,您將從一個 Android 應用程式開始,讓您在畫面上畫出數字,然後新增功能,將這些繪製的數字分類為 0 到 9 的單一值。

課程內容

  • 如何透過 MediaPipe Tasks 在 Android 應用程式中整合圖片分類工作。

軟硬體需求

  • 安裝的 Android Studio 版本 (本程式碼研究室是以 Android Studio Giraffe 編寫及測試)。
  • 用於執行應用程式的 Android 裝置或模擬器。
  • 對 Android 開發作業有基本瞭解 (這可不是「Hello World」,但還不錯!)。

2. 在 Android 應用程式中加入 MediaPipe 工作

下載 Android 範例應用程式

本程式碼研究室會從預先建立的範例開始,讓您在螢幕上繪圖。您可以在這裡的官方 MediaPipe 範例存放區找到啟動應用程式。請點選「程式碼」圖示 >,複製存放區或下載 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 檔案中的相關參照,然後在日後在程式碼中變更模型名稱 (具體來說是在 DigitClassHelper.kt 檔案中)。

更新 build.gradle

您必須先匯入程式庫,才能開始使用 MediaPipe Tasks。

  1. 開啟位於「應用程式」模組中的 build.gradle 檔案,然後向下捲動至「dependencies」區塊。
  2. 您應該會在這個區塊底部看到 // 步驟 1 依附元件匯入註解。
  3. 以下列實作方式取代該行
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. 按一下 Android Studio 頂端橫幅中顯示的「Sync Now」按鈕,即可下載這個依附元件。

3. 建立 MediaPipe Tasks 數字分類器輔助程式

在下一個步驟中,您將填入一個類別,負責執行機器學習分類的繁瑣作業。開啟 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. 往下捲動至 [步驟 3 定義分類器] 這一行,新增以下這行程式碼,為這個應用程式要使用的 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」設為模型在傳回結果前至少需有的可信度。

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 分類圖片,並記錄在 DigitClassifier 傳回這些結果前的推論時間長度。
// 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 工作執行推論

如要開始這個部分,請在 Android Studio 中開啟 DigitCanvasFragment 類別,也就是所有作業都會出現在這裡。

  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 的 UI 執行緒中執行 UI 更新。

  1. 在上述步驟中透過介面新增函式時,您也必須在類別頂端加入實作宣告。
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. 您會在課程頂端看到一則註解,寫著 // STEP 7a 初始化分類器。請在這裡放置 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*:分類*,並加入以下程式碼來呼叫您稍後新增的函式。當你在應用程式的繪圖區域上放開手指時,這個程式碼區塊就會觸發分類程序。
// STEP 8a: classify
classifyDrawing()
  1. 最後,請尋找註解 // STEP 8b classify,以新增分類 Drawing() 函式。這會從畫布中擷取點陣圖,然後傳遞至 DigitClassifierHelper,以執行分類,以便在 onResults() 介面函式中接收結果。
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. 部署及測試應用程式

完成上述操作後,您應該有一個能分類螢幕上已繪製數字的運作中應用程式!請將應用程式部署至 Android Emulator 或實體 Android 裝置中,以便進行測試。

  1. 按一下 Android Studio 工具列中的「Run」(執行) 圖示 7e15a9c9e1620fe7.png,執行應用程式。
  2. 在繪圖板上畫出任何數字,看看應用程式能否辨識。畫面上應會顯示模型認為遭到繪製的數字,以及預測該數字所需的時間。

7f37187f8f919638.gif

6. 恭喜!

您做到了!在本程式碼研究室中,您已學會如何在 Android 應用程式中加入圖片分類,以及如何使用 MNIST 模型分類手繪數字。

後續步驟

  • 現在您已能夠分類數字,建議您訓練自己的模型將繪製的字母分類、將動物分類,或其他無限量的其他項目。您可以在 developers.google.com/mediapipe 頁面上,找到使用 MediaPipe Model Maker 訓練全新圖片分類模型的說明文件。
  • 瞭解其他適用於 Android 的 MediaPipe 工作,包括臉部地標偵測、手勢辨識和音訊分類。

我們很期待你們製作的所有酷炫功能!