[TFLite] 6(2). 프레임 워크를 활용한 이미지 분류 앱 개발 (카메라와 갤러리 이미지 사용하기)

3 minute read


카메라와 갤러리 이미지 사용하기

이 장에서는 텐서플로 라이트 서포트 라이브러리를 이용하여 카메라와 갤러리의 이미지를 사용해 이미지 분류를 하는 앱을 개발해봅니다.

이전 포스팅들 중 [Android] 6(3). BaseActivity 설계하기, [Android] 9. 카메라와 갤러리, [TFLite] 6(1). 프레임워크를 활용한 이미지 분류 앱 개발 (텐서플로 라이트 서포트 라이브러리) 에서의 코드들을 조합하여 하나의 완성된 기능을 갖는 앱을 개발합니다.

위 3개의 포스팅은 아래 주소에서 보실 수 있습니다.


아키텍처


image-20210818183107553

사전 작업


레이아웃을 만들기 전에 사전 작업을 합니다.

  • AndroidManifest.xml 파일에 권한 명세
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="kr.co.hanbit.imageclassifier">

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <uses-feature android:name="android.hardware.camera2"/>

    <application
        ...
    </application>

</manifest>
  • 프로젝트의 build.gradle 파일에 텐서플로 라이트/텐서플로 라이트 서포트 라이브러리 의존성 추가
dependencies {

    ...

    // 1. 텐서플로 라이트와 텐서플로 라이트 서포트 라이브러리 의존성 추가
    implementation 'org.tensorflow:tensorflow-lite:2.4.0'
    implementation 'org.tensorflow:tensorflow-lite-support:0.1.0'

    ...
}
  • BaseActivity.kt 파일을 프로젝트에 포함

‘BaseActivity 설계하기’ 포스팅에서 구현한 BaseActivity.kt 클래스는 권한을 요청과 처리에 사용되는 전용 클래스입니다.



레이아웃


레이아웃은 ‘카메라와 갤러리’ 포스팅에서 사용한 레이아웃에 추론 결과를 출력할 텍스트뷰 하나를 추가합니다.

PhotoActivity.kt로 생성합니다.

image-20210818181927446

  • 이미지뷰: id = imageView
  • 버튼1: id = btnCamera, text = CAMERA
  • 버튼2: id = btnGallery, text = GALLERY
  • 텍스트뷰: id = textView, text = RESULT



UI 로직 구현


UI 로직도 ‘카메라와 갤러리’ 포스팅에서 사용한 로직을 사용합니다.

다만 수정해야 하는 부분을 살펴보겠습니다.

  • openGallery 메서드 수정
    private fun openGallery(){
        // 권장 코드
        val intent = Intent(Intent.ACTION_GET_CONTENT).setType("image/*")
//        val intent = Intent(Intent.ACTION_PICK)
//        intent.type = MediaStore.Images.Media.CONTENT_TYPE
        startActivityForResult(intent, REQ_GALLERY)
    }
  • onActivityResult 메서드 수정

모델의 추론 메서드를 호출할 때는 Bitmap 인스턴스를 파라미터로 넘겨야 합니다. 따라서 카메라와 갤러리의 경우 모두 Uri를 Bitmap으로 바꿔줍니다.

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(resultCode == RESULT_OK)
            when(requestCode){
                // uri를 bitmap으로 변환
                REQ_CAMERA -> {
                    photoUri?.let{ uri ->
                        val capturedImage = loadBitmap(uri)
                        photoUri = null
                        // 비트맵을 모델에 전달하여 추론
                        val result = callClassifier(capturedImage)

                        binding.imageView.setImageBitmap(capturedImage)
                        binding.textView.text = result
                    }
                }
                REQ_GALLERY -> {
                    val selectedImageUri: Uri = data?.data as Uri
                    val selectedImage = loadBitmap(selectedImageUri)
                    // 비트맵을 모델에 전달하여 추론
                    val result = callClassifier(selectedImage)

                    binding.imageView.setImageBitmap(selectedImage)
                    binding.textView.text = result
                }
            }
        else{
            Toast.makeText(baseContext,
                            "Result Canceled!",
                            Toast.LENGTH_LONG).show()
        }
    }
  • callClassifier 메서드 추가

모델의 추론 메서드를 호출하여 결괏값을 받고 문자열로 리턴하는 메서드입니다.

    private fun callClassifier(bitmap: Bitmap?): String{
        val output: Pair<String, Float> = classifier.classify(bitmap)
        val resultStr = String.format(Locale.ENGLISH,
                "class : %s   Prob : %.2f%%",
                output.first, output.second * 100)

        return resultStr
    }
  • onDestroy 메서드 오버라이드

액티비티 종료 시 모델에게 할당된 자원을 해제하도록 합니다.

    override fun onDestroy() {
        super.onDestroy()
        classifier.finish()
    }



이미지 포맷 변환


분류기 모델은 ‘프레임워크를 활용한 이미지 분류 앱 개발 (텐서플로 라이트 서포트 라이브러리)’ 포스팅에서 구현했던 ClassifierWithModel.kt 파일을 사용합니다.

이 소스파일도 약간의 수정이 필요합니다.


  • loadImage 메서드 수정하기

파라미터로 전달된 Bitmap 인스턴스를 TensorImage에 저장할 때 Bitmap의 데이터 포맷을 ARGB_8888로 맞춰줘야 합니다.

따라서 이 코드를 loadImage 메서드에 추가합니다.

    private fun loadImage(bitmap: Bitmap?): TensorImage{
        // TensorImage에 이미지 데이터 저장
        // 7-2. 추가 - 데이터 포맷 변환: bitmap의 데이터 포맷이 ARGB_8888이 아닌 경우 변환
        if (bitmap != null) {
            if(bitmap.config != Bitmap.Config.ARGB_8888)
                inputImage.load(convertBitmap2ARGB8888(bitmap))
            else
                inputImage.load(bitmap)
        }
        // inputImage?.load(bitmap)

        
        val imageProcessor =
            ImageProcessor.Builder()                           
                .add(ResizeOp(modelInputWidth,modelInputHeight,
                    ResizeOp.ResizeMethod.NEAREST_NEIGHBOR))
                .add(NormalizeOp(0.0f, 255.0f))   
                .build()                                       
        // 이미지를 전처리하여 TensorImage 형태로 반환
        return imageProcessor.process(inputImage)
    }
  • convertBitmap2ARGB8888 메서드 구현하기

loadImage에서 호출할 Bitmap의 포맷을 ARGB_8888로 변환해주는 메서드를 구현합니다.

    // 7-1. 추가 - 데이터 포맷 변환: loadImage 메서드의 bitmap 변수가 ARGB_8888이 아닌 경우 변환
    private fun convertBitmap2ARGB8888(bitmap: Bitmap): Bitmap{
        return bitmap.copy(Bitmap.Config.ARGB_8888, true)
    }



결과 확인


이것으로 끝입니다!

앞선 3개의 포스팅에서 살펴본 개념과 코드를 통해 간단한 이미지 분류기 앱을 구현할 수 있습니다.

결과 화면은 다음과 같습니다.

image-20210818194239026

카메라를 통해 촬영하거나 갤러리에서 선택된 이미지를 통해 이미지 분류가 되는 것을 볼 수 있습니다.



본 포스팅에 대한 모든 코드는 깃허브 저장소 에서 확인할 수 있습니다.



정리


권한, 카메라와 갤러리, tflite 모델 사용이라는 개념들을 종합하여 하나의 이미지 분류 앱을 구현해보았습니다.

이처럼 알고있는 개념들을 종합하거나 새로운 지식을 추가하여 멋진 앱을 만들 수 있을 것입니다.

Categories:

Updated:

Leave a comment