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

1. 簡介

什麼是 MediaPipe?

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

圖片分類是 MediaPipe Solutions 提供的多項 ML 視覺任務之一。MediaPipe Tasks 適用於 Android、iOS、Python (包括 Raspberry Pi) 和網頁。

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

課程內容

  • 如何使用 MediaPipe Tasks,在 Android 應用程式中加入圖片分類工作。

軟硬體需求

  • 已安裝的 Android Studio 版本 (本程式碼研究室是使用 Android Studio Giraffe 編寫及測試)。
  • 用於執行應用程式的 Android 裝置或模擬器。
  • 具備 Android 開發作業的基本知識 (雖然不是「Hello World」,但也差不多)。

2. 將 MediaPipe 工作新增至 Android 應用程式

下載 Android 範例應用程式

本程式碼研究室會從預先製作的範例開始,讓您在畫面上繪圖。您可以在官方 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 {
    setupDigitClassifier()
}
  1. 最後,請向下捲動至「// STEP 5 create classify function」的註解,然後新增下列程式碼。這個函式會接受 點陣圖 (在本例中為繪製的數字),將其轉換為 MediaPipe 圖片物件 (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. 繼續往下看// 步驟 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 註解,然後新增新的 classifyDrawing() 函式。這會從畫布中擷取位圖,然後將其傳遞至 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 模型分類手繪的數字。

後續步驟

  • 您現在可以分類數字,因此可以訓練自己的模型,用於分類手繪字母、動物或其他無限的項目。如要瞭解如何使用 MediaPipe Model Maker 訓練新的圖像分類模型,請前往 developers.google.com/mediapipe 頁面查看相關說明文件。
  • 瞭解適用於 Android 的其他 MediaPipe 工作,包括臉部特徵偵測、手勢辨識和音訊分類。

期待看到你創作出更多精彩作品!