1. 소개
MediaPipe란 무엇인가요?
MediaPipe 솔루션을 사용하면 머신러닝(ML) 솔루션을 앱에 적용할 수 있습니다. 이 솔루션은 사용자에게 즉각적이고, 매력적이고, 유용한 출력을 제공하는 사전 빌드된 처리 파이프라인을 구성하는 프레임워크를 제공합니다. 또한 MediaPipe Model Maker를 사용해서 솔루션을 맞춤설정하여 기본 모델을 업데이트할 수도 있습니다.
이미지 분류는 MediaPipe 솔루션이 제공하는 여러 ML 버전 태스크 중 하나입니다. MediaPipe Tasks는 Android, iOS, Python (Raspberry Pi 포함), 웹용으로 제공됩니다.
이 Codelab에서는 화면에 숫자 자릿수를 그릴 수 있는 Android 앱으로 시작한 다음, 그려진 자릿수를 0~9의 단일 값으로 분류하는 기능을 추가합니다.
학습할 내용
- MediaPipe Tasks로 Android 앱에 이미지 분류 태스크를 포함하는 방법
필요한 항목
- Android 스튜디오 설치 버전 (이 Codelab은 Android 스튜디오 Giraffe로 작성되고 테스트됨)
- 앱을 실행할 Android 기기 또는 에뮬레이터
- Android 개발에 관한 기본 지식('Hello World'는 아니지만 크게 다르지 않음)
2. Android 앱에 MediaPipe Tasks 추가
Android 스타터 앱 다운로드
이 Codelab은 화면에 그림을 그릴 수 있는 사전 제작된 샘플로 시작합니다. 공식 MediaPipe 샘플 저장소(여기)에서 시작 앱을 확인할 수 있습니다. 코드 > ZIP 다운로드를 클릭하여 저장소를 클론하거나 zip 파일을 다운로드합니다.
Android 스튜디오로 앱 가져오기
- Android 스튜디오를 엽니다.
- Welcome to Android Studio 화면에서 오른쪽 상단의 Open을 선택합니다.

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

모델
앱을 처음 실행하면 mnist.tflite라는 파일이 다운로드되어 앱의 assets 디렉터리에 저장됩니다. 간단하게 하기 위해 숫자를 분류하는 알려진 모델인 MNIST를 가져와 프로젝트의 download_models.gradle 스크립트를 사용하여 앱에 추가했습니다. 손으로 쓴 글자와 같은 맞춤 모델을 직접 학습시키기로 결정한 경우 download_models.gradle 파일을 삭제하고 앱 수준 build.gradle 파일에서 이 파일에 대한 참조를 삭제한 후 코드 (특히 DigitClassifierHelper.kt 파일)에서 모델 이름을 나중에 변경합니다.
build.gradle 업데이트
MediaPipe Tasks를 사용하려면 먼저 라이브러리를 가져와야 합니다.
- app 모듈에 있는 build.gradle 파일을 열고 dependencies 블록까지 아래로 스크롤합니다.
- 블록 하단에 // STEP 1 Dependency Import라는 주석이 표시됩니다.
- 해당 줄을 다음 구현으로 바꿉니다.
implementation("com.google.mediapipe:tasks-vision:latest.release")
- Android 스튜디오 상단의 배너에 표시되는 Sync Now 버튼을 클릭하여 이 종속 항목을 다운로드합니다.
3. MediaPipe Tasks 숫자 분류기 도우미 만들기
다음 단계에서는 머신러닝 분류를 위한 작업을 수행하는 클래스를 작성합니다. DigitClassifierHelper.kt를 열고 시작해 보겠습니다.
- 수업 상단에서 // STEP 2 Create listener라는 주석을 찾습니다.
- 해당 줄을 다음 코드로 바꿉니다. 이렇게 하면 DigitClassifierHelper 클래스에서 결과를 수신 대기하는 위치 (이 경우 DigitCanvasFragment 클래스)로 결과를 다시 전달하는 데 사용되는 리스너가 생성됩니다.
// STEP 2 Create listener
interface DigitClassifierListener {
fun onError(error: String)
fun onResults(
results: ImageClassifierResult,
inferenceTime: Long
)
}
- 또한 DigitClassifierListener를 클래스의 선택적 매개변수로 허용해야 합니다.
class DigitClassifierHelper(
val context: Context,
val digitClassifierListener: DigitClassifierListener?
) {
- // STEP 3 define classifier라는 줄로 내려가 다음 줄을 추가하여 이 앱에 사용될 ImageClassifier의 자리표시자를 만듭니다.
// 3단계 분류기 정의
private var digitClassifier: ImageClassifier? = null
- // 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를 통해 오류가 반환됩니다.
- ImageClassifier는 사용하기 전에 초기화해야 하므로 init 블록을 추가하여 setupDigitClassifier()를 호출합니다.
init {
setupDigitClassifier()
}
- 마지막으로 // 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 Tasks로 추론 실행
Android 스튜디오에서 DigitCanvasFragment 클래스를 열어 이 섹션을 시작할 수 있습니다. 모든 작업은 여기에서 이루어집니다.
- 이 파일의 맨 아래에 // 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 업데이트도 실행해야 합니다.
- 위 단계에서 인터페이스의 새 함수를 추가하므로 클래스 상단에 구현 선언도 추가해야 합니다.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
- 클래스 상단에 // STEP 7a Initialize classifier라는 주석이 표시됩니다. 여기에 DigitClassifierHelper의 선언을 배치합니다.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
- // STEP 7b Initialize classifier(분류기 초기화)로 이동하여 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
)
- 마지막 단계로 // STEP 8a*: classify* 주석을 찾아 잠시 후에 추가할 새 함수를 호출하는 다음 코드를 추가합니다. 이 코드 블록은 앱의 그리기 영역에서 손가락을 떼면 분류를 트리거합니다.
// STEP 8a: classify
classifyDrawing()
- 마지막으로 // STEP 8b classify 주석을 찾아 새로운 classifyDrawing() 함수를 추가합니다. 이렇게 하면 캔버스에서 비트맵이 추출된 후 DigitClassifierHelper에 전달되어 분류가 실행되고 onResults() 인터페이스 함수에서 결과가 수신됩니다.
// STEP 8b classify
private fun classifyDrawing() {
val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
digitClassifierHelper.classify(bitmap)
}
5. 앱 배포 및 테스트
이 모든 과정을 거치면 화면에 그려진 숫자를 분류할 수 있는 작동하는 앱이 완성됩니다. Android 에뮬레이터 또는 실제 Android 기기에 앱을 배포하여 테스트합니다.
- Android 스튜디오 툴바에서 실행 (
)을 클릭하여 앱을 실행합니다. - 그림판에 숫자를 그려 앱에서 인식할 수 있는지 확인합니다. 모델이 그린 것으로 생각하는 숫자와 해당 숫자를 예측하는 데 걸린 시간을 모두 표시해야 합니다.

6. 축하합니다.
축하합니다. 이 Codelab에서는 Android 앱에 이미지 분류를 추가하는 방법, 특히 MNIST 모델을 사용하여 손으로 그린 숫자를 분류하는 방법을 알아봤습니다.
다음 단계
- 이제 숫자를 분류할 수 있으므로 그려진 글자를 분류하거나, 동물을 분류하거나, 기타 수많은 항목을 분류하는 자체 모델을 학습시킬 수 있습니다. MediaPipe Model Maker로 새로운 이미지 분류 모델을 학습시키는 방법에 관한 문서는 developers.google.com/mediapipe 페이지에서 확인할 수 있습니다.
- 얼굴 랜드마크 감지, 동작 인식, 오디오 분류 등 Android에서 사용할 수 있는 다른 MediaPipe Tasks에 대해 알아보세요.
여러분이 만들 멋진 콘텐츠를 기대하겠습니다.