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

1. 소개

MediaPipe란 무엇인가요?

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

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

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

학습할 내용

  • MediaPipe 작업을 사용하여 Android 앱에 이미지 분류 작업을 통합하는 방법

필요한 항목

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

2. Android 앱에 MediaPipe 작업 추가

Android 시작 앱 다운로드

이 Codelab에서는 화면에 그릴 수 있도록 미리 만들어진 샘플로 시작합니다. 공식 MediaPipe 샘플 저장소는 여기에서 확인할 수 있습니다. 저장소를 클론하거나 코드 > ZIP 파일을 다운로드합니다.

Android 스튜디오로 앱 가져오기

  1. Android 스튜디오를 엽니다.
  2. Welcome to Android Studio 화면에서 오른쪽 상단의 Open을 선택합니다.

a0b5b070b802e4ea.png

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

11a0f6fe021fdc92.jpeg

모델

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

build.gradle 업데이트

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

  1. app 모듈에 있는 build.gradle 파일을 열고 dependencies 블록까지 아래로 스크롤합니다.
  2. 블록 하단에 // 1단계 종속 항목 가져오기라는 주석이 표시됩니다.
  3. 해당 줄을 다음 구현으로 바꿉니다.
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Android 스튜디오 상단의 배너에 표시되는 Sync Now 버튼을 클릭하여 이 종속 항목을 다운로드합니다.

3. MediaPipe 태스크 숫자 분류기 도우미 만들기

다음 단계에서는 머신러닝 분류에서 어려운 작업을 수행하는 클래스를 작성해 보겠습니다. 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에서 사용하는 매개변수를 정의합니다. 여기에는 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를 사용하기 전에 초기화하려고 하므로 init 블록을 추가하여 setupDigitClassifier()를 호출할 수 있습니다.
init {
    setupDigitClassifier()
}
  1. 마지막으로 // STEP 5 create classify function(분류 함수 생성)이라는 주석까지 아래로 스크롤하여 다음 코드를 추가합니다. 이 함수는 그려진 숫자인 Bitmap을 받아 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 작업으로 추론 실행

이 섹션은 모든 작업이 이루어지는 Android 스튜디오에서 DigitCanvasFragment 클래스를 열어 시작할 수 있습니다.

  1. 이 파일 하단에 // STEP 6 Set up 나가기라는 주석이 표시됩니다. 여기에서 리스너와 연결된 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. 클래스 상단에 // 단계 7a 분류 기준 초기화라는 주석이 표시됩니다. 여기에 DigitClassifierHelper에 관한 선언을 입력합니다.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. // 단계 7b 분류 기준 초기화로 이동하면 onViewCreated() 함수 내에서 DigiClassifierHelper를 초기화할 수 있습니다.
// 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. 마지막 단계에서는 // 단계 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 Emulator 또는 실제 Android 기기에 앱을 배포하여 테스트하세요.

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

7f37187f8f919638.gif

6. 축하합니다.

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

다음 단계

  • 이제 숫자를 분류할 수 있으므로 그려진 문자를 분류하거나 동물 또는 끝없는 수의 다른 항목을 분류하도록 자체 모델을 학습시킬 수 있습니다. developers.google.com/mediapipe 페이지에서 MediaPipe Model Maker로 새 이미지 분류 모델을 학습시키는 방법에 대한 문서를 확인할 수 있습니다.
  • 얼굴 특징 감지, 동작 인식, 오디오 분류 등 Android에서 사용할 수 있는 다른 MediaPipe 작업에 대해 알아보세요.

여러분이 만들 멋진 결과물을 기대하겠습니다.