Android에서 AutoML 학습 모델을 사용하여 이미지 라벨 지정

AutoML Vision Edge를 사용하여 자체 모델을 학습시킨 후에 앱에서 모델을 사용하여 이미지에 라벨을 지정할 수 있습니다.

시작하기 전에

  1. 아직 추가하지 않았으면 Android 프로젝트에 Firebase를 추가합니다.
  2. 모듈(앱 수준) Gradle 파일(일반적으로 app/build.gradle)에 ML Kit Android 라이브러리의 종속 항목을 추가합니다.
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-automl:18.0.5'
    }
    

1. 모델 로드

ML Kit는 AutoML이 생성한 모델을 기기에서 실행합니다. 그러나 모델을 Firebase에서 원격으로 로드하거나 로컬 스토리지에서 로드하거나 두 방법을 모두 사용하도록 ML Kit를 구성할 수 있습니다.

Firebase에서 모델을 호스팅하면 앱 버전을 새롭게 출시하지 않고 모델을 업데이트할 수 있고 Remote ConfigA/B Testing을 사용하여 사용자 집합별로 다른 모델을 동적으로 제공할 수 있습니다.

모델을 앱과 번들로 묶지 않고 Firebase에서 모델을 호스팅하여 제공하는 방법만 선택한 경우 앱의 초기 다운로드 크기를 줄일 수 있습니다. 다만 모델을 앱과 번들로 묶지 않을 경우 앱이 처음으로 모델을 다운로드하기 전까지 모든 모델 관련 기능을 사용할 수 없다는 점에 유의하세요.

모델을 앱과 번들로 묶으면 Firebase 호스팅 모델을 사용할 수 없는 경우에도 앱의 ML 기능이 계속 작동하도록 할 수 있습니다.

Firebase 호스팅 모델 소스 구성

원격 호스팅 모델을 사용하려면 모델을 게시할 때 할당한 이름을 지정하여 FirebaseAutoMLRemoteModel 객체를 만듭니다.

Java

// Specify the name you assigned in the Firebase console.
FirebaseAutoMLRemoteModel remoteModel =
    new FirebaseAutoMLRemoteModel.Builder("your_remote_model").build();

Kotlin+KTX

// Specify the name you assigned in the Firebase console.
val remoteModel = FirebaseAutoMLRemoteModel.Builder("your_remote_model").build()

이제 다운로드를 허용할 조건을 지정하여 모델 다운로드 작업을 시작합니다. 모델이 기기에 없거나 최신 버전의 모델을 사용할 수 있으면 모델이 Firebase에서 비동기식으로 다운로드됩니다.

Java

FirebaseModelDownloadConditions conditions = new FirebaseModelDownloadConditions.Builder()
        .requireWifi()
        .build();
FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Success.
            }
        });

Kotlin+KTX

val conditions = FirebaseModelDownloadConditions.Builder()
    .requireWifi()
    .build()
FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Success.
    }

대부분의 앱은 초기화 코드로 다운로드 작업을 시작하지만 모델 사용이 필요한 시점 이전에는 언제든지 다운로드할 수 있습니다.

로컬 모델 소스 구성

모델을 앱과 함께 번들로 묶는 방법은 다음과 같습니다.

  1. Firebase Console에서 다운로드한 zip 파일에서 모델과 모델의 메타데이터를 추출합니다. 다운로드한 파일을 수정하지 않고(파일 이름 등) 그대로 사용하는 것이 좋습니다.
  2. 모델과 모델의 메타데이터 파일을 앱 패키지에 포함합니다.

    1. 프로젝트에 애셋 폴더가 없으면 app/ 폴더를 마우스 오른쪽 버튼으로 클릭하고 새로 만들기 > 폴더 > 애셋 폴더를 클릭하여 하나 만듭니다.
    2. 애셋 폴더 아래에 모델 파일을 포함할 하위 폴더를 만듭니다.
    3. model.tflite, dict.txt, manifest.json 파일을 하위 폴더에 복사합니다. 세 파일이 모두 같은 폴더에 있어야 합니다.
  3. Gradle이 앱을 빌드할 때 모델 파일을 압축하지 않도록 앱의 build.gradle 파일에 다음을 추가합니다.
    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    
    모델 파일이 앱 패키지에 포함되며 ML Kit에서 원시 애셋으로 사용할 수 있습니다.
  4. 모델 매니페스트 파일의 경로를 지정하여 FirebaseAutoMLLocalModel 객체를 만듭니다.

    Java

    FirebaseAutoMLLocalModel localModel = new FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build();
    

    Kotlin+KTX

    val localModel = FirebaseAutoMLLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            .build()
    

모델에서 이미지 라벨러 만들기

모델 소스를 구성한 후 모델 소스 중 하나에서 FirebaseVisionImageLabeler 객체를 만듭니다.

로컬로 번들된 모델만 있다면 FirebaseAutoMLLocalModel 객체에서 라벨러를 만들고 필요한 신뢰도 점수 임곗값을 구성합니다(모델 평가 참조).

Java

FirebaseVisionImageLabeler labeler;
try {
    FirebaseVisionOnDeviceAutoMLImageLabelerOptions options =
            new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
                    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                   // to determine an appropriate value.
                    .build();
    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
} catch (FirebaseMLException e) {
    // ...
}

Kotlin+KTX

val options = FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0)  // Evaluate your model in the Firebase console
                                // to determine an appropriate value.
    .build()
val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)

원격 호스팅 모델이 있다면 실행 전에 모델이 다운로드되었는지 확인해야 합니다. 모델 관리자의 isModelDownloaded() 메서드로 모델 다운로드 작업의 상태를 확인할 수 있습니다.

이 상태는 라벨러 실행 전에만 확인하면 되지만, 원격 호스팅 모델과 로컬로 번들된 모델이 모두 있는 경우에는 이미지 라벨러를 인스턴스화할 때 이 확인 작업을 수행하는 것이 합리적일 수 있습니다. 원격 모델이 다운로드되었으면 원격 모델에서, 그렇지 않으면 로컬 모델에서 라벨러를 만듭니다.

Java

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel);
                }
                FirebaseVisionOnDeviceAutoMLImageLabelerOptions options = optionsBuilder
                        .setConfidenceThreshold(0.0f)  // Evaluate your model in the Firebase console
                                                       // to determine an appropriate threshold.
                        .build();

                FirebaseVisionImageLabeler labeler;
                try {
                    labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options);
                } catch (FirebaseMLException e) {
                    // Error.
                }
            }
        });

Kotlin+KTX

FirebaseModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded -> 
    val optionsBuilder =
        if (isDownloaded) {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(remoteModel)
        } else {
            FirebaseVisionOnDeviceAutoMLImageLabelerOptions.Builder(localModel)
        }
    // Evaluate your model in the Firebase console to determine an appropriate threshold.
    val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
    val labeler = FirebaseVision.getInstance().getOnDeviceAutoMLImageLabeler(options)
}

원격 호스팅 모델만 있다면 모델 다운로드 여부가 확인될 때까지 모델 관련 기능(예: UI 비활성화 또는 숨김) 사용을 중지해야 합니다. 모델 관리자의 download() 메서드에 리스너를 연결하여 관련 기능을 사용 중지할 수 있습니다.

Java

FirebaseModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void v) {
              // Download complete. Depending on your app, you could enable
              // the ML feature, or switch from the local model to the remote
              // model, etc.
            }
        });

Kotlin+KTX

FirebaseModelManager.getInstance().download(remoteModel, conditions)
    .addOnCompleteListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

2. 입력 이미지 준비

이제 이 섹션에 설명된 옵션 중 하나를 사용하여 라벨을 지정할 각 이미지에 FirebaseVisionImage 객체를 만들고 다음 섹션에 설명된 FirebaseVisionImageLabeler 인스턴스로 전달합니다.

media.Image 객체, 기기의 파일, 바이트 배열, Bitmap 객체에서 FirebaseVisionImage 객체를 만들 수 있습니다.

  • 기기의 카메라에서 이미지를 캡처할 때와 같이 media.Image 객체에서 FirebaseVisionImage 객체를 만들려면 media.Image 객체 및 이미지 회전을 FirebaseVisionImage.fromMediaImage()에 전달합니다.

    CameraX 라이브러리를 사용하는 경우 OnImageCapturedListenerImageAnalysis.Analyzer 클래스가 회전 값을 계산하므로 FirebaseVisionImage.fromMediaImage()를 호출하기 전에 ML Kit의 ROTATION_ 상수 중 하나로 회전을 변환하기만 하면 됩니다.

    Java

    private class YourAnalyzer implements ImageAnalysis.Analyzer {
    
        private int degreesToFirebaseRotation(int degrees) {
            switch (degrees) {
                case 0:
                    return FirebaseVisionImageMetadata.ROTATION_0;
                case 90:
                    return FirebaseVisionImageMetadata.ROTATION_90;
                case 180:
                    return FirebaseVisionImageMetadata.ROTATION_180;
                case 270:
                    return FirebaseVisionImageMetadata.ROTATION_270;
                default:
                    throw new IllegalArgumentException(
                            "Rotation must be 0, 90, 180, or 270.");
            }
        }
    
        @Override
        public void analyze(ImageProxy imageProxy, int degrees) {
            if (imageProxy == null || imageProxy.getImage() == null) {
                return;
            }
            Image mediaImage = imageProxy.getImage();
            int rotation = degreesToFirebaseRotation(degrees);
            FirebaseVisionImage image =
                    FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
    

    Kotlin+KTX

    private class YourImageAnalyzer : ImageAnalysis.Analyzer {
        private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
            0 -> FirebaseVisionImageMetadata.ROTATION_0
            90 -> FirebaseVisionImageMetadata.ROTATION_90
            180 -> FirebaseVisionImageMetadata.ROTATION_180
            270 -> FirebaseVisionImageMetadata.ROTATION_270
            else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
        }
    
        override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
            val mediaImage = imageProxy?.image
            val imageRotation = degreesToFirebaseRotation(degrees)
            if (mediaImage != null) {
                val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                // Pass image to an ML Kit Vision API
                // ...
            }
        }
    }
    

    이미지 회전을 제공하는 카메라 라이브러리를 사용하지 않는 경우 기기의 카메라 센서 방향 및 기기 회전에서 이미지 회전을 계산할 수 있습니다.

    Java

    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
    static {
        ORIENTATIONS.append(Surface.ROTATION_0, 90);
        ORIENTATIONS.append(Surface.ROTATION_90, 0);
        ORIENTATIONS.append(Surface.ROTATION_180, 270);
        ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }
    
    /**
     * Get the angle by which an image must be rotated given the device's current
     * orientation.
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private int getRotationCompensation(String cameraId, Activity activity, Context context)
            throws CameraAccessException {
        // Get the device's current rotation relative to its "native" orientation.
        // Then, from the ORIENTATIONS table, look up the angle the image must be
        // rotated to compensate for the device's rotation.
        int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int rotationCompensation = ORIENTATIONS.get(deviceRotation);
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
        int sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION);
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        int result;
        switch (rotationCompensation) {
            case 0:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                break;
            case 90:
                result = FirebaseVisionImageMetadata.ROTATION_90;
                break;
            case 180:
                result = FirebaseVisionImageMetadata.ROTATION_180;
                break;
            case 270:
                result = FirebaseVisionImageMetadata.ROTATION_270;
                break;
            default:
                result = FirebaseVisionImageMetadata.ROTATION_0;
                Log.e(TAG, "Bad rotation value: " + rotationCompensation);
        }
        return result;
    }

    Kotlin+KTX

    private val ORIENTATIONS = SparseIntArray()
    
    init {
        ORIENTATIONS.append(Surface.ROTATION_0, 90)
        ORIENTATIONS.append(Surface.ROTATION_90, 0)
        ORIENTATIONS.append(Surface.ROTATION_180, 270)
        ORIENTATIONS.append(Surface.ROTATION_270, 180)
    }
    /**
     * Get the angle by which an image must be rotated given the device's current
     * orientation.
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Throws(CameraAccessException::class)
    private fun getRotationCompensation(cameraId: String, activity: Activity, context: Context): Int {
        // Get the device's current rotation relative to its "native" orientation.
        // Then, from the ORIENTATIONS table, look up the angle the image must be
        // rotated to compensate for the device's rotation.
        val deviceRotation = activity.windowManager.defaultDisplay.rotation
        var rotationCompensation = ORIENTATIONS.get(deviceRotation)
    
        // On most devices, the sensor orientation is 90 degrees, but for some
        // devices it is 270 degrees. For devices with a sensor orientation of
        // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
        val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
        val sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
        rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
    
        // Return the corresponding FirebaseVisionImageMetadata rotation value.
        val result: Int
        when (rotationCompensation) {
            0 -> result = FirebaseVisionImageMetadata.ROTATION_0
            90 -> result = FirebaseVisionImageMetadata.ROTATION_90
            180 -> result = FirebaseVisionImageMetadata.ROTATION_180
            270 -> result = FirebaseVisionImageMetadata.ROTATION_270
            else -> {
                result = FirebaseVisionImageMetadata.ROTATION_0
                Log.e(TAG, "Bad rotation value: $rotationCompensation")
            }
        }
        return result
    }

    그런 다음 media.Image 객체 및 회전 값을 FirebaseVisionImage.fromMediaImage()에 전달합니다.

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
  • 파일 URI에서 FirebaseVisionImage 객체를 만들려면 앱 컨텍스트 및 파일 URI를 FirebaseVisionImage.fromFilePath()에 전달합니다. ACTION_GET_CONTENT 인텐트를 사용하여 사용자에게 갤러리 앱에서 이미지를 선택하라는 메시지를 표시할 때 유용한 방법입니다.

    Java

    FirebaseVisionImage image;
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Kotlin+KTX

    val image: FirebaseVisionImage
    try {
        image = FirebaseVisionImage.fromFilePath(context, uri)
    } catch (e: IOException) {
        e.printStackTrace()
    }
  • ByteBuffer 또는 바이트 배열에서 FirebaseVisionImage 객체를 만들려면 먼저 위에서 설명한 대로 media.Image 입력의 이미지 회전을 계산합니다.

    그런 다음 이미지의 높이, 너비, 색상 인코딩 형식, 회전이 포함된 FirebaseVisionImageMetadata 객체를 만듭니다.

    Java

    FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
            .setWidth(480)   // 480x360 is typically sufficient for
            .setHeight(360)  // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build();

    Kotlin+KTX

    val metadata = FirebaseVisionImageMetadata.Builder()
            .setWidth(480) // 480x360 is typically sufficient for
            .setHeight(360) // image recognition
            .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
            .setRotation(rotation)
            .build()

    버퍼나 배열, 메타데이터 객체를 사용하여 FirebaseVisionImage 객체를 만듭니다.

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
    // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
    // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
  • Bitmap 객체에서 FirebaseVisionImage 객체를 만들려면 다음 안내를 따르세요.

    Java

    FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

    Kotlin+KTX

    val image = FirebaseVisionImage.fromBitmap(bitmap)
    Bitmap 객체로 표현된 이미지가 추가 회전이 필요 없는 수직 상태여야 합니다.

3. 이미지 라벨러 실행

이미지의 객체에 라벨을 지정하려면 FirebaseVisionImage 객체를 FirebaseVisionImageLabelerprocessImage() 메서드에 전달합니다.

Java

labeler.processImage(image)
        .addOnSuccessListener(new OnSuccessListener<List<FirebaseVisionImageLabel>>() {
            @Override
            public void onSuccess(List<FirebaseVisionImageLabel> labels) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

Kotlin+KTX

labeler.processImage(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

이미지 라벨 지정에 성공하면 FirebaseVisionImageLabel 객체의 배열이 성공 리스너에 전달됩니다. 이미지에서 인식된 특징에 대한 정보를 각 객체에서 가져올 수 있습니다.

예를 들면 다음과 같습니다.

Java

for (FirebaseVisionImageLabel label: labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
}

Kotlin+KTX

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
}

실시간 성능 향상을 위한 팁

  • 인식기 호출을 제한합니다. 인식기가 실행 중일 때 새 동영상 프레임이 제공되는 경우 해당 프레임을 삭제합니다.
  • 인식기 출력을 사용해서 입력 이미지에서 그래픽을 오버레이하는 경우 먼저 ML Kit에서 결과를 가져온 후 이미지를 렌더링하고 단일 단계로 오버레이합니다. 이렇게 하면 입력 프레임별로 한 번만 디스플레이 표면에 렌더링됩니다.
  • Camera2 API를 사용할 경우 ImageFormat.YUV_420_888 형식으로 이미지를 캡처합니다.

    이전 Camera API를 사용하는 경우 ImageFormat.NV21 형식으로 이미지를 캡처합니다.