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 스튜디오로 가져오기
- Android 스튜디오를 엽니다.
- Android 스튜디오 시작 화면에서 오른쪽 상단의 열기를 선택합니다.
- 저장소를 클론하거나 다운로드한 위치로 이동하여 codelabs/digitclassifier/android/start 디렉터리를 엽니다.
- Android 스튜디오의 오른쪽 상단에 있는 녹색 실행 화살표 ( )를 클릭하여 모든 항목이 올바르게 열렸는지 확인합니다.
- 그릴 수 있는 검은색 화면과 화면을 재설정하는 지우기 버튼이 있는 앱이 열립니다. 이 화면에 그릴 수는 있지만 다른 작업은 할 수 없으므로 지금 바로 문제를 해결하겠습니다.
모델
앱을 처음 실행하면 mnist.tflite라는 파일이 다운로드되어 앱의 애셋 디렉터리에 저장되는 것을 볼 수 있습니다. 편의상 이미 숫자를 분류하는 알려진 모델인 MNIST를 가져와 프로젝트의 download_models.gradle 스크립트를 사용하여 앱에 추가했습니다. 손으로 쓴 문자 모델과 같은 자체 맞춤 모델을 학습하려면 download_models.gradle 파일을 삭제하고 앱 수준 build.gradle 파일에서 이 파일에 대한 참조를 삭제한 후 나중에 코드 (특히 DigitClassifierHelper.kt 파일)에서 모델 이름을 변경합니다.
build.gradle 업데이트
MediaPipe Tasks를 사용하려면 먼저 라이브러리를 가져와야 합니다.
- app 모듈에 있는 build.gradle 파일을 연 다음 dependencies 블록으로 아래로 스크롤합니다.
- 블록 하단에 // 1단계 종속 항목 가져오기라는 주석이 표시됩니다.
- 이 줄을 다음 구현으로 바꿉니다.
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
- // 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를 통해 오류가 반환됩니다.
- ImageClassifier를 사용하기 전에 초기화해야 하므로 init 블록을 추가하여 setupDigitClassifier()를 호출할 수 있습니다.
init {
setupDigitClassifier()
}
- 마지막으로 // 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 클래스를 열어 시작할 수 있습니다.
- 이 파일의 맨 아래에 // 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 업데이트도 실행해야 합니다.
- 위 단계에서 인터페이스의 새 함수를 추가하므로 클래스 상단에 구현 선언도 추가해야 합니다.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
- 클래스 상단에 // STEP 7a 분류기 초기화라는 주석이 표시됩니다. DigitClassifierHelper의 선언을 배치할 위치입니다.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
- // 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
)
- 마지막 단계로 // 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 스튜디오 툴바에서 Run(실행) 아이콘( )을 클릭하여 앱을 실행합니다.
- 그리기 패드에 숫자를 그려 앱에서 인식할 수 있는지 확인합니다. 모델이 그려졌다고 생각하는 숫자와 이 숫자를 예측하는 데 걸린 시간을 모두 표시해야 합니다.
6. 축하합니다.
축하합니다. 이 Codelab에서는 Android 앱에 이미지 분류를 추가하는 방법, 특히 MNIST 모델을 사용하여 손으로 그린 숫자를 분류하는 방법을 알아봤습니다.
다음 단계
- 이제 숫자를 분류할 수 있으므로 그려진 문자, 동물 또는 무수히 많은 다른 항목을 분류하도록 자체 모델을 학습시키고 싶을 수 있습니다. developers.google.com/mediapipe 페이지에서 MediaPipe Model Maker를 사용하여 새 이미지 분류 모델을 학습하는 방법에 관한 문서를 확인할 수 있습니다.
- 얼굴 랜드마크 감지, 동작 인식, 오디오 분류 등 Android에서 사용할 수 있는 다른 MediaPipe 태스크에 대해 알아보세요.
여러분이 만들어낼 멋진 작품을 기대하고 있겠습니다.