Xây dựng ứng dụng Android viết tay về thuật toán phân loại bằng MediaPipe Tasks

1. Giới thiệu

MediaPipe là gì?

Giải pháp MediaPipe cho phép bạn áp dụng các giải pháp học máy (ML) cho ứng dụng của mình. Thư viện này cung cấp một khung để định cấu hình quy trình xử lý tạo sẵn, giúp mang lại kết quả tức thì, hấp dẫn và hữu ích cho người dùng. Bạn thậm chí có thể tuỳ chỉnh các giải pháp này bằng Trình tạo mô hình MediaPipe để cập nhật các mô hình mặc định.

Phân loại hình ảnh là một trong số các nhiệm vụ thị giác máy học mà MediaPipe Solutions cung cấp. MediaPipe Tasks có sẵn cho Android, iOS, Python (bao gồm cả Raspberry Pi!) và web.

Trong lớp học lập trình này, bạn sẽ bắt đầu với một ứng dụng Android cho phép bạn vẽ các chữ số trên màn hình, sau đó thêm chức năng phân loại các chữ số đã vẽ đó thành một giá trị duy nhất từ 0 đến 9.

Kiến thức bạn sẽ học được

  • Cách kết hợp tác vụ phân loại hình ảnh trong ứng dụng Android bằng MediaPipe Tasks.

Bạn cần có

  • Phiên bản Android Studio đã cài đặt (lớp học lập trình này được viết và kiểm thử bằng Android Studio Giraffe).
  • Thiết bị Android hoặc trình mô phỏng để chạy ứng dụng.
  • Kiến thức cơ bản về phát triển Android (không phải là "Hello World", nhưng cũng không quá xa!).

2. Thêm tác vụ MediaPipe vào ứng dụng Android

Tải ứng dụng khởi động Android

Lớp học lập trình này sẽ bắt đầu bằng một mẫu tạo sẵn cho phép bạn vẽ trên màn hình. Bạn có thể tìm thấy ứng dụng khởi động đó trong kho lưu trữ MediaPipe Samples chính thức tại đây. Sao chép kho lưu trữ hoặc tải tệp zip xuống bằng cách nhấp vào Mã > Tải ZIP xuống.

Nhập ứng dụng vào Android Studio

  1. Mở Android Studio
  2. Trên màn hình Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy chọn Open (Mở) ở góc trên cùng bên phải.

a0b5b070b802e4ea.png

  1. Chuyển đến vị trí bạn đã sao chép hoặc tải kho lưu trữ xuống rồi mở thư mục codelabs/digitclassifier/android/start.
  2. Xác minh rằng mọi thứ đã mở đúng cách bằng cách nhấp vào mũi tên chạy màu xanh lục ( 7e15a9c9e1620fe7.png) ở trên cùng bên phải của Android Studio
  3. Bạn sẽ thấy ứng dụng mở ra với một màn hình đen mà bạn có thể vẽ, cũng như một nút Clear (Xoá) để đặt lại màn hình đó. Mặc dù bạn có thể vẽ trên màn hình đó, nhưng màn hình này không làm được gì khác. Vì vậy, chúng ta sẽ bắt đầu khắc phục vấn đề đó ngay.

11a0f6fe021fdc92.jpeg

Mẫu

Trong lần đầu chạy ứng dụng, bạn có thể nhận thấy một tệp có tên mnist.tflite được tải xuống và lưu trữ trong thư mục assets (tài sản) của ứng dụng. Để đơn giản, chúng ta đã lấy một mô hình đã biết, MNIST, để phân loại các chữ số và thêm mô hình đó vào ứng dụng thông qua việc sử dụng tập lệnh download_models.gradle trong dự án. Nếu quyết định huấn luyện mô hình tuỳ chỉnh của riêng mình, chẳng hạn như mô hình cho chữ viết tay, thì bạn sẽ xoá tệp download_models.gradle, xoá tham chiếu đến tệp đó trong tệp build.gradle cấp ứng dụng và thay đổi tên của mô hình sau này trong mã (cụ thể là trong tệp DigitClassifierHelper.kt).

Cập nhật build.gradle

Trước khi có thể bắt đầu sử dụng Tác vụ MediaPipe, bạn cần nhập thư viện.

  1. Mở tệp build.gradle nằm trong mô-đun app, sau đó di chuyển xuống khối dependencies (phần phụ thuộc).
  2. Bạn sẽ thấy một nhận xét ở cuối khối đó có nội dung // STEP 1 Dependency Import (BƯỚC 1: Nhập phần phụ thuộc).
  3. Thay thế dòng đó bằng cách triển khai sau
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Nhấp vào nút Sync Now (Đồng bộ hoá ngay) xuất hiện trong biểu ngữ ở đầu Android Studio để tải phần phụ thuộc này xuống.

3. Tạo trình trợ giúp phân loại chữ số trong MediaPipe Tasks

Ở bước tiếp theo, bạn sẽ điền vào một lớp sẽ thực hiện phần việc nặng nhọc cho quá trình phân loại học máy. Mở DigitClassifierHelper.kt và bắt đầu nào!

  1. Tìm nhận xét ở đầu lớp có nội dung // STEP 2 Create listener (BƯỚC 2: Tạo trình nghe)
  2. Thay thế dòng đó bằng mã sau. Thao tác này sẽ tạo một trình nghe dùng để truyền kết quả từ lớp DigitClassifierHelper trở lại bất cứ nơi nào đang nghe kết quả đó (trong trường hợp này là lớp DigitCanvasFragment, nhưng chúng ta sẽ sớm đến đó)
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. Bạn cũng cần chấp nhận DigitClassifierListener làm tham số không bắt buộc cho lớp này:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. Di chuyển xuống dòng có nội dung // BƯỚC 3 xác định trình phân loại, thêm dòng sau để tạo phần giữ chỗ cho ImageClassifier sẽ được dùng cho ứng dụng này:

// BƯỚC 3 xác định bộ phân loại

private var digitClassifier: ImageClassifier? = null
  1. Thêm hàm sau đây vào vị trí bạn thấy nhận xét // BƯỚC 4 thiết lập bộ phân loại:
// 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)
    }
}

Có một vài điều đang diễn ra trong phần trên, vì vậy, hãy xem xét các phần nhỏ hơn để thực sự hiểu được những gì đang xảy ra.

val baseOptionsBuilder = BaseOptions.builder()
    .setModelAssetPath("mnist.tflite")

// Describe additional options
val optionsBuilder = ImageClassifierOptions.builder()
    .setRunningMode(RunningMode.IMAGE)
    .setBaseOptions(baseOptionsBuilder.build())

Khối này sẽ xác định các tham số mà ImageClassifier sử dụng. Điều này bao gồm mô hình được lưu trữ trong ứng dụng của bạn (mnist.tflite) trong BaseOptions và RunningMode trong ImageClassifierOptions, trong trường hợp này là IMAGE, nhưng VIDEO và LIVE_STREAM là các tuỳ chọn bổ sung có sẵn. Các tham số khác có sẵn là MaxResults, giới hạn mô hình trả về số lượng kết quả tối đa và ScoreThreshold, đặt mức độ tin cậy tối thiểu mà mô hình cần có trong kết quả trước khi trả về kết quả đó.

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)
}

Sau khi tạo các tuỳ chọn cấu hình, bạn có thể tạo ImageClassifier mới bằng cách truyền vào một ngữ cảnh và các tuỳ chọn. Nếu quá trình khởi chạy đó gặp sự cố, lỗi sẽ được trả về thông qua DigitClassifierListener.

  1. Vì chúng ta muốn khởi chạy ImageClassifier trước khi sử dụng, nên bạn có thể thêm một khối khởi chạy để gọi setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Cuối cùng, hãy di chuyển xuống phần nhận xét có nội dung // BƯỚC 5 tạo hàm phân loại rồi thêm mã sau. Hàm này sẽ chấp nhận một Bitmap, trong trường hợp này là chữ số được vẽ, chuyển đổi thành đối tượng Hình ảnh MediaPipe (MPImage), sau đó phân loại hình ảnh đó bằng ImageClassifier, cũng như ghi lại thời gian suy luận, trước khi trả về các kết quả đó qua 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)
    }
}

Đó là tất cả những gì bạn cần làm với tệp trình trợ giúp! Trong phần tiếp theo, bạn sẽ hoàn tất các bước cuối cùng để bắt đầu phân loại các số đã rút.

4. Chạy quy trình dự đoán bằng Tác vụ MediaPipe

Bạn có thể bắt đầu phần này bằng cách mở lớp DigitCanvasFragment trong Android Studio. Đây là nơi diễn ra mọi hoạt động.

  1. Ở cuối cùng của tệp này, bạn sẽ thấy một nhận xét có nội dung // BƯỚC 6 Thiết lập trình nghe. Bạn sẽ thêm các hàm onResults() và onError() liên kết với trình nghe tại đây.
// 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() đặc biệt quan trọng vì sẽ hiển thị kết quả nhận được từ ImageClassifier. Vì lệnh gọi lại này được kích hoạt từ một luồng trong nền, nên bạn cũng cần chạy các bản cập nhật giao diện người dùng trên luồng giao diện người dùng của Android.

  1. Khi thêm các hàm mới từ một giao diện ở bước trên, bạn cũng cần thêm phần khai báo triển khai ở đầu lớp.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Ở đầu lớp, bạn sẽ thấy một nhận xét có nội dung // STEP 7a Khởi chạy bộ phân loại. Đây là nơi bạn sẽ đặt phần khai báo cho DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Di chuyển xuống // BƯỚC 7b Khởi chạy trình phân loại, bạn có thể khởi chạy digitClassifierHelper trong hàm onViewCreated().
// 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. Đối với các bước cuối cùng, hãy tìm nhận xét // STEP 8a*: classify* và thêm mã sau để gọi một hàm mới mà bạn sẽ thêm trong giây lát. Khối mã này sẽ kích hoạt quá trình phân loại khi bạn nhấc ngón tay lên khỏi khu vực vẽ trong ứng dụng.
// STEP 8a: classify
classifyDrawing()
  1. Cuối cùng, hãy tìm nhận xét // STEP 8b classify để thêm hàm classifyDrawing() mới. Thao tác này sẽ trích xuất một bitmap từ canvas, sau đó chuyển bitmap đó đến DigitClassifierHelper để thực hiện việc phân loại và nhận kết quả trong hàm giao diện onResults().
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. Triển khai và kiểm thử ứng dụng

Sau tất cả, bạn sẽ có một ứng dụng hoạt động có thể phân loại các chữ số được vẽ trên màn hình! Hãy tiếp tục triển khai ứng dụng cho Trình mô phỏng Android hoặc thiết bị Android thực để kiểm thử.

  1. Nhấp vào biểu tượng Chạy ( 7e15a9c9e1620fe7.png) trong thanh công cụ của Android Studio để chạy ứng dụng.
  2. Vẽ bất kỳ chữ số nào lên bảng vẽ và xem ứng dụng có thể nhận dạng chữ số đó hay không. Kết quả sẽ hiển thị cả chữ số mà mô hình cho rằng đã được vẽ, cũng như thời gian để dự đoán chữ số đó.

7f37187f8f919638.gif

6. Xin chúc mừng!

Các bạn đã làm được! Trong lớp học lập trình này, bạn đã tìm hiểu cách thêm tính năng phân loại hình ảnh vào ứng dụng Android, cụ thể là cách phân loại chữ số được vẽ tay bằng mô hình MNIST.

Các bước tiếp theo

  • Giờ đây, khi đã có thể phân loại chữ số, bạn có thể huấn luyện mô hình của riêng mình để phân loại chữ cái được vẽ, phân loại động vật hoặc vô số các mục khác. Bạn có thể tìm thấy tài liệu về cách huấn luyện mô hình phân loại hình ảnh mới bằng Trình tạo mô hình MediaPipe trên trang developers.google.com/mediapipe.
  • Tìm hiểu về các Tác vụ MediaPipe khác có sẵn cho Android, bao gồm cả tính năng Phát hiện điểm đặc trưng trên khuôn mặt, Nhận dạng cử chỉ và Phân loại âm thanh.

Chúng tôi rất mong được xem những nội dung thú vị mà bạn tạo ra!