MediaPipe 작업으로 필기 입력 숫자 분류기 Android 앱 빌드

1. 소개

MediaPipe란 무엇인가요?

MediaPipe 솔루션을 사용하면 머신러닝(ML) 솔루션을 앱에 적용할 수 있습니다. 이 솔루션으로 제공되는 프레임워크를 통해 사용자에게 즉각적이고, 매력적이고, 유용한 출력을 제공하는 사전 빌드된 처리 파이프라인을 구성할 수 있습니다. MediaPipe Model Maker를 사용해서 솔루션을 맞춤설정하여 기본 모델을 업데이트할 수도 있습니다.

이미지 분류는 MediaPipe 솔루션이 제공하는 여러 ML 버전 태스크 중 하나입니다. MediaPipe Tasks는 Android, iOS, Python (Raspberry Pi 포함), 웹에서 사용할 수 있습니다.

이 Codelab에서는 화면에 숫자를 그릴 수 있는 Android 앱으로 시작하여 그려진 숫자를 0~9 사이의 단일 값으로 분류하는 기능을 추가합니다.

학습할 내용

  • MediaPipe 태스크를 사용하여 Android 앱에 이미지 분류 태스크를 통합하는 방법

필요한 항목

  • 설치된 버전의 Android 스튜디오 (이 Codelab은 Android 스튜디오 Giraffe로 작성 및 테스트됨)
  • 앱을 실행할 Android 기기 또는 에뮬레이터
  • Android 개발에 관한 기본 지식('Hello World'는 아니지만 그다지 멀지 않습니다).

2. Android 앱에 MediaPipe Tasks 추가

Android 시작 앱 다운로드

이 Codelab에서는 화면에 그릴 수 있는 사전 제작된 샘플로 시작합니다. 시작 앱은 공식 MediaPipe 샘플 저장소 여기에서 확인할 수 있습니다. Code > Download ZIP을 클릭하여 저장소를 클론하거나 zip 파일을 다운로드합니다.

앱을 Android 스튜디오로 가져오기

  1. Android 스튜디오를 엽니다.
  2. Android 스튜디오 시작 화면에서 오른쪽 상단의 열기를 선택합니다.

a0b5b070b802e4ea.png

  1. 저장소를 클론하거나 다운로드한 위치로 이동하여 codelabs/digitclassifier/android/start 디렉터리를 엽니다.
  2. Android 스튜디오의 오른쪽 상단에 있는 녹색 실행 화살표 ( 7e15a9c9e1620fe7.png)를 클릭하여 모든 항목이 올바르게 열렸는지 확인합니다.
  3. 그릴 수 있는 검은색 화면과 화면을 재설정하는 지우기 버튼이 있는 앱이 열립니다. 이 화면에 그릴 수는 있지만 다른 작업은 할 수 없으므로 지금 바로 문제를 해결하겠습니다.

11a0f6fe021fdc92.jpeg

모델

앱을 처음 실행하면 mnist.tflite라는 파일이 다운로드되어 앱의 애셋 디렉터리에 저장되는 것을 볼 수 있습니다. 편의상 이미 숫자를 분류하는 알려진 모델인 MNIST를 가져와 프로젝트의 download_models.gradle 스크립트를 사용하여 앱에 추가했습니다. 손으로 쓴 문자 모델과 같은 자체 맞춤 모델을 학습하려면 download_models.gradle 파일을 삭제하고 앱 수준 build.gradle 파일에서 이 파일에 대한 참조를 삭제한 후 나중에 코드 (특히 DigitClassifierHelper.kt 파일)에서 모델 이름을 변경합니다.

build.gradle 업데이트

MediaPipe Tasks를 사용하려면 먼저 라이브러리를 가져와야 합니다.

  1. app 모듈에 있는 build.gradle 파일을 연 다음 dependencies 블록으로 아래로 스크롤합니다.
  2. 블록 하단에 // 1단계 종속 항목 가져오기라는 주석이 표시됩니다.
  3. 이 줄을 다음 구현으로 바꿉니다.
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Android 스튜디오 상단의 배너에 표시되는 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. // 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에서 사용하는 매개변수를 정의합니다. 여기에는 BaseOptions 아래의 앱 내에 저장된 모델 (mnist.tflite)과 ImageClassifierOptions 아래의 RunningMode가 포함됩니다. 이 경우 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 이미지 객체(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 스튜디오에서 모든 작업이 실행되는 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*: 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 에뮬레이터 또는 실제 Android 기기에 배포하여 테스트합니다.

  1. Android 스튜디오 툴바에서 Run(실행) 아이콘( 7e15a9c9e1620fe7.png)을 클릭하여 앱을 실행합니다.
  2. 그리기 패드에 숫자를 그려 앱에서 인식할 수 있는지 확인합니다. 모델이 그려졌다고 생각하는 숫자와 이 숫자를 예측하는 데 걸린 시간을 모두 표시해야 합니다.

7f37187f8f919638.gif

6. 축하합니다.

축하합니다. 이 Codelab에서는 Android 앱에 이미지 분류를 추가하는 방법, 특히 MNIST 모델을 사용하여 손으로 그린 숫자를 분류하는 방법을 알아봤습니다.

다음 단계

  • 이제 숫자를 분류할 수 있으므로 그려진 문자, 동물 또는 무수히 많은 다른 항목을 분류하도록 자체 모델을 학습시키고 싶을 수 있습니다. developers.google.com/mediapipe 페이지에서 MediaPipe Model Maker를 사용하여 새 이미지 분류 모델을 학습하는 방법에 관한 문서를 확인할 수 있습니다.
  • 얼굴 랜드마크 감지, 동작 인식, 오디오 분류 등 Android에서 사용할 수 있는 다른 MediaPipe 태스크에 대해 알아보세요.

여러분이 만들어낼 멋진 작품을 기대하고 있겠습니다.