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. Giải pháp này cung cấp một khung để định cấu hình các quy trình xử lý được tạo sẵn, mang lại kết quả đầu ra 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 MediaPipe Model Maker để 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 tác vụ thị giác ML mà Giải pháp MediaPipe 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 đó, bạn sẽ thêm chức năng phân loại các chữ số được 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ó

  • Một 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).
  • Một 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á khác biệt!).

2. Thêm MediaPipe Tasks vào ứng dụng Android

Tải ứng dụng khởi đầu Android xuống

Lớp học lập trình này sẽ bắt đầu bằng một mẫu được 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 đầu đó trong kho lưu trữ Mẫu MediaPipe 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 tệp 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 đế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 run (chạy) màu xanh lục ( 7e15a9c9e1620fe7.png) ở góc 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 màu đen mà bạn có thể vẽ, cũng như 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 nhiều việc khác, vì vậy, chúng ta sẽ bắt đầu khắc phục vấn đề đó ngay bây giờ.

11a0f6fe021fdc92.jpeg

Mô hình

Khi chạy ứng dụng lần đầu tiên, bạn có thể nhận thấy rằng một tệp có tên là mnist.tflite được tải xuống và lưu trữ trong thư mục assets của ứng dụng. Để đơn giản, chúng tôi đã 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 các chữ cái 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 MediaPipe Tasks, bạn cần nhập thư viện.

  1. Mở tệp build.gradle nằm trong mô-đun app của bạn, sau đó cuộn xuống khối dependencies.
  2. Bạn sẽ thấy một nhận xét ở cuối khối đó có nội dung // STEP 1 Dependency Import (Nhập phần phụ thuộc BƯỚC 1).
  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ố MediaPipe Tasks

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

  1. Tìm nhận xét ở đầu lớp có nội dung // STEP 2 Create listener (Tạo trình nghe BƯỚC 2)
  2. Thay thế dòng đó bằng mã sau. Thao tác này sẽ tạo một trình nghe được dùng để chuyển kết quả từ lớp DigitClassifierHelper trở lại bất cứ nơi nào đang nghe các kết quả đó (trong trường hợp này, đó sẽ là lớp DigitCanvasFragment của bạn, 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:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. Chuyển xuống dòng có nội dung // STEP 3 define classifier, thêm dòng sau để tạo trình giữ chỗ cho ImageClassifier sẽ được dùng cho ứng dụng này:

// STEP 3 define classifier (xác định trình phân loại BƯỚC 3)

private var digitClassifier: ImageClassifier? = null
  1. Thêm hàm sau đây ở nơi bạn thấy nhận xét // 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)
    }
}

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 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 lựa chọn bổ sung khác. Các tham số có sẵn khác 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 một 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 lựa chọn cấu hình, bạn có thể tạo ImageClassifier mới bằng cách truyền ngữ cảnh và các lựa chọn. Nếu có sự cố xảy ra với quá trình khởi chạy đó, thì một 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 khối init để gọi setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Cuối cùng, hãy cuộn xuống nhận xét có nội dung // STEP 5 create classify function (tạo hàm phân loại BƯỚC 5) và thêm mã sau. Hàm này sẽ chấp nhận 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)
    }
}

Vậy là bạn đã hoàn thành tệp trợ giúp! Trong phần tiếp theo, bạn sẽ điền vào các bước cuối cùng để bắt đầu phân loại các số được vẽ.

4. Chạy suy luận bằng MediaPipe Tasks

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 tất cả công việc sẽ diễn ra.

  1. Ở cuối tệp này, bạn sẽ thấy một nhận xét có nội dung // STEP 6 Set up listener (Thiết lập trình nghe BƯỚC 6). Bạn sẽ thêm các hàm onResults() và onError() được 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ì hàm này sẽ hiển thị các 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 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 trong bước trên, bạn cũng cần thêm khai báo triển khai ở đầu lớp.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Ở gần đầu lớp, bạn sẽ thấy một nhận xét có nội dung // STEP 7a Initialize classifier (Khởi chạy trình phân loại BƯỚC 7a). Đây là nơi bạn sẽ đặt khai báo cho DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Chuyển xuống // STEP 7b Initialize classifier (Khởi chạy trình phân loại BƯỚC 7b), 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* (phân loại BƯỚC 8a) 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 khỏi vùng 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 (phân loại BƯỚC 8b) để 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 phân loại nhằm 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ả những điều đó, 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 triển khai ứng dụng này vào Trình mô phỏng Android hoặc thiết bị Android thực để kiểm thử.

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

7f37187f8f919638.gif

6. Xin chúc mừng!

Thật tuyệt! 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, và cụ thể là cách phân loại các chữ số được vẽ bằng tay bằng mô hình MNIST.

Các bước tiếp theo

  • Bây giờ, bạn có thể phân loại các chữ số, bạn có thể muốn huấn luyện mô hình của riêng mình để phân loại các chữ cái được vẽ, hoặc để 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 MediaPipe Model Maker trên trang developers.google.com/mediapipe.
  • Tìm hiểu về các MediaPipe Tasks khác có sẵn cho Android, bao gồm cả tính năng Phát hiện điểm mốc 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 chờ tất cả những điều thú vị mà bạn tạo ra!