Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

Android에서 AutoML 학습 모델로 이미지에 레이블 지정

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

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

AutoML Vision Edge에서 학습된 모델을 통합하는 방법에는 두 가지가 있습니다. 앱의 자산 폴더에 모델을 넣어 번들로 묶거나 Firebase에서 동적으로 다운로드할 수 있습니다.

모델 번들링 옵션
앱에 번들
  • 모델은 앱 APK의 일부입니다.
  • Android 기기가 오프라인일 때도 모델을 즉시 사용할 수 있습니다.
  • Firebase 프로젝트가 필요하지 않습니다.
Firebase로 호스팅
  • Firebase Machine Learning 에 업로드하여 모델 호스팅
  • APK 크기 줄이기
  • 모델은 요청 시 다운로드됩니다.
  • 앱을 다시 게시하지 않고 모델 업데이트 푸시
  • Firebase 원격 구성 으로 손쉬운 A/B 테스트
  • Firebase 프로젝트가 필요합니다.

시작하기 전에

  1. 일반적으로 app/build.gradle 인 모듈의 앱 수준 gradle 파일에 ML Kit Android 라이브러리에 대한 종속성을 추가합니다.

    앱과 함께 모델을 묶는 경우:

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

    Firebase에서 모델을 동적으로 다운로드하려면 linkFirebase 종속 항목을 추가하세요.

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. 모델을 다운로드 하려면 Android 프로젝트에 Firebase를 아직 추가 하지 않았는지 확인하십시오. 모델을 번들로 묶을 때는 필요하지 않습니다.

1. 모델 불러오기

로컬 모델 소스 구성

모델을 앱과 번들로 묶으려면:

  1. Firebase 콘솔에서 다운로드한 zip 아카이브에서 모델과 해당 메타데이터를 추출합니다. 파일을 수정(파일 이름 포함)하지 않고 다운로드한 그대로 사용하는 것이 좋습니다.

  2. 앱 패키지에 모델과 해당 메타데이터 파일을 포함합니다.

    1. 프로젝트에 자산 폴더가 없으면 app/ 폴더를 마우스 오른쪽 버튼으로 클릭한 다음 New > Folder > Assets Folder 를 클릭하여 자산 폴더를 만드십시오.
    2. 자산 폴더 아래에 모델 파일을 포함할 하위 폴더를 만듭니다.
    3. model.tflite , dict.txtmanifest.json 파일을 하위 폴더에 복사합니다(세 파일 모두 동일한 폴더에 있어야 함).
  3. 앱을 빌드할 때 Gradle이 모델 파일을 압축하지 않도록 앱의 build.gradle 파일에 다음을 추가합니다.

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    

    모델 파일은 앱 패키지에 포함되며 ML Kit에서 원시 자산으로 사용할 수 있습니다.

  4. 모델 매니페스트 파일의 경로를 지정하여 LocalModel 개체를 만듭니다.

    자바

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    코틀린

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

Firebase 호스팅 모델 소스 구성

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

자바

// Specify the name you assigned in the Firebase console.
FirebaseModelSource firebaseModelSource =
    new FirebaseModelSource.Builder("your_model_name").build();
CustomRemoteModel remoteModel =
    new CustomRemoteModel.Builder(firebaseModelSource).build();

코틀린

// Specify the name you assigned in the Firebase console.
val firebaseModelSource = FirebaseModelSource.Builder("your_model_name")
    .build()
val remoteModel = CustomRemoteModel.Builder(firebaseModelSource).build()

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

자바

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(@NonNull Task<Void> task) {
                // Success.
            }
        });

코틀린

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

많은 앱이 초기화 코드에서 다운로드 작업을 시작하지만 모델을 사용하기 전에 언제든지 시작할 수 있습니다.

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

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

로컬로 번들된 모델만 있는 경우 CustomImageLabelerOptions 개체에서 레이블 지정자를 만들고 필요한 신뢰도 점수 임계값을 구성합니다( 모델 평가 참조).

자바

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

코틀린

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

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

라벨러를 실행하기 전에 이를 확인하기만 하면 되지만 원격 호스팅 모델과 로컬로 번들된 모델이 모두 있는 경우 이미지 라벨러를 인스턴스화할 때 이 확인을 수행하는 것이 좋습니다. 다음과 같은 경우 원격 모델에서 라벨러를 생성합니다. 다운로드되었으며 그렇지 않은 경우 로컬 모델에서 다운로드되었습니다.

자바

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

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

코틀린

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

원격으로 호스팅된 모델만 있는 경우 모델이 다운로드되었음을 확인할 때까지 모델 관련 기능을 비활성화해야 합니다(예: 회색으로 표시하거나 UI 일부 숨기기). 모델 관리자의 download() 메서드에 리스너를 연결하면 됩니다.

자바

RemoteModelManager.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.
            }
        });

코틀린

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

2. 입력 이미지 준비

그런 다음 라벨을 지정하려는 각 이미지에 대해 이미지에서 InputImage 객체를 만듭니다. 이미지 라벨러는 Bitmap 을 사용하거나 camera2 API를 사용하는 경우 YUV_420_888 media.Image 를 사용할 때 가장 빠르게 실행되며 가능한 경우 권장됩니다.

서로 다른 소스에서 InputImage 를 생성할 수 있으며 각 소스는 아래에 설명되어 있습니다.

media.Image 사용

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

CameraX 라이브러리를 사용하는 경우 OnImageCapturedListenerImageAnalysis.Analyzer 클래스가 자동으로 회전 값을 계산합니다.

Kotlin+KTX

private class YourImageAnalyzer : ImageAnalysis.Analyzer {
    override fun analyze(imageProxy: ImageProxy?) {
        val mediaImage = imageProxy?.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        if (imageProxy == null || imageProxy.getImage() == null) {
            return;
        }
        Image mediaImage = imageProxy.getImage();
        InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees);
        // Pass image to an ML Kit Vision API
        // ...
    }
}

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

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
}

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;
}

그런 다음 media.Image 객체와 회전 각도 값을 InputImage.fromMediaImage() 에 전달합니다.

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

파일 URI 사용

파일 URI에서 InputImage 객체를 만들려면 앱 컨텍스트와 파일 URI를 InputImage.fromFilePath() 에 전달합니다. 이는 ACTION_GET_CONTENT 텐트를 사용하여 사용자에게 갤러리 앱에서 이미지를 선택하라는 메시지를 표시할 때 유용합니다.

Kotlin+KTX

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

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

ByteBuffer 또는 ByteArray 사용

ByteBuffer 또는 ByteArray 에서 InputImage 객체를 만들려면 먼저 media.Image 입력에 대해 이전에 설명한 대로 이미지 회전 정도를 계산합니다. 그런 다음 이미지의 높이, 너비, 색상 인코딩 형식 및 회전 각도와 함께 버퍼 또는 배열을 사용하여 InputImage 객체를 만듭니다.

Kotlin+KTX

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

Bitmap 사용

Bitmap 객체에서 InputImage 객체를 만들려면 다음과 같이 선언합니다.

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

이미지는 회전 각도와 함께 Bitmap 객체로 표현됩니다.

3. 이미지 라벨러 실행

이미지의 개체에 레이블을 지정하려면 ImageLabelerprocess() 메서드에 image 개체를 전달합니다.

자바

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

코틀린

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

4. 레이블이 지정된 개체에 대한 정보 얻기

이미지 레이블 지정 작업이 성공하면 ImageLabel 개체 목록이 성공 수신기에 전달됩니다. 각 ImageLabel 개체는 이미지에서 레이블이 지정된 항목을 나타냅니다. 각 레이블의 텍스트 설명, 일치의 신뢰도 점수 및 일치 인덱스를 얻을 수 있습니다. 예를 들어:

자바

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

코틀린

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

실시간 성능 향상을 위한 팁

실시간 애플리케이션에서 이미지에 레이블을 지정하려면 다음 지침을 따라 최상의 프레임 속도를 얻으십시오.

  • 이미지 라벨러에 대한 스로틀 호출입니다. 이미지 라벨러가 실행되는 동안 새 비디오 프레임을 사용할 수 있게 되면 프레임을 삭제합니다. 예제는 빠른 시작 샘플 앱의 VisionProcessorBase 클래스를 참조하세요.
  • 이미지 라벨러의 출력을 사용하여 입력 이미지에 그래픽을 오버레이하는 경우 먼저 결과를 얻은 다음 이미지를 렌더링하고 단일 단계에서 오버레이합니다. 이렇게 하면 각 입력 프레임에 대해 한 번만 디스플레이 표면에 렌더링됩니다. 예제는 빠른 시작 샘플 앱의 CameraSourcePreviewGraphicOverlay 클래스를 참조하세요.
  • Camera2 API를 사용하는 경우 ImageFormat.YUV_420_888 형식으로 이미지를 캡처합니다.

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

,

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

AutoML Vision Edge에서 학습된 모델을 통합하는 방법에는 두 가지가 있습니다. 앱의 자산 폴더에 모델을 넣어 번들로 묶거나 Firebase에서 동적으로 다운로드할 수 있습니다.

모델 번들링 옵션
앱에 번들
  • 모델은 앱 APK의 일부입니다.
  • Android 기기가 오프라인일 때도 모델을 즉시 사용할 수 있습니다.
  • Firebase 프로젝트가 필요하지 않습니다.
Firebase로 호스팅
  • Firebase Machine Learning 에 업로드하여 모델 호스팅
  • APK 크기 줄이기
  • 모델은 요청 시 다운로드됩니다.
  • 앱을 다시 게시하지 않고 모델 업데이트 푸시
  • Firebase 원격 구성 으로 손쉬운 A/B 테스트
  • Firebase 프로젝트가 필요합니다.

시작하기 전에

  1. 일반적으로 app/build.gradle 인 모듈의 앱 수준 gradle 파일에 ML Kit Android 라이브러리에 대한 종속성을 추가합니다.

    앱과 함께 모델을 묶는 경우:

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

    Firebase에서 모델을 동적으로 다운로드하려면 linkFirebase 종속 항목을 추가하세요.

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. 모델을 다운로드 하려면 Android 프로젝트에 Firebase를 아직 추가 하지 않았는지 확인하십시오. 모델을 번들로 묶을 때는 필요하지 않습니다.

1. 모델 불러오기

로컬 모델 소스 구성

모델을 앱과 번들로 묶으려면:

  1. Firebase 콘솔에서 다운로드한 zip 아카이브에서 모델과 해당 메타데이터를 추출합니다. 파일을 수정(파일 이름 포함)하지 않고 다운로드한 그대로 사용하는 것이 좋습니다.

  2. 앱 패키지에 모델과 해당 메타데이터 파일을 포함합니다.

    1. 프로젝트에 자산 폴더가 없으면 app/ 폴더를 마우스 오른쪽 버튼으로 클릭한 다음 New > Folder > Assets Folder 를 클릭하여 자산 폴더를 만드십시오.
    2. 자산 폴더 아래에 모델 파일을 포함할 하위 폴더를 만듭니다.
    3. model.tflite , dict.txtmanifest.json 파일을 하위 폴더에 복사합니다(세 파일 모두 동일한 폴더에 있어야 함).
  3. 앱을 빌드할 때 Gradle이 모델 파일을 압축하지 않도록 앱의 build.gradle 파일에 다음을 추가합니다.

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
        }
    }
    

    모델 파일은 앱 패키지에 포함되며 ML Kit에서 원시 자산으로 사용할 수 있습니다.

  4. 모델 매니페스트 파일의 경로를 지정하여 LocalModel 개체를 만듭니다.

    자바

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    코틀린

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

Firebase 호스팅 모델 소스 구성

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

자바

// Specify the name you assigned in the Firebase console.
FirebaseModelSource firebaseModelSource =
    new FirebaseModelSource.Builder("your_model_name").build();
CustomRemoteModel remoteModel =
    new CustomRemoteModel.Builder(firebaseModelSource).build();

코틀린

// Specify the name you assigned in the Firebase console.
val firebaseModelSource = FirebaseModelSource.Builder("your_model_name")
    .build()
val remoteModel = CustomRemoteModel.Builder(firebaseModelSource).build()

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

자바

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(@NonNull Task<Void> task) {
                // Success.
            }
        });

코틀린

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

많은 앱이 초기화 코드에서 다운로드 작업을 시작하지만 모델을 사용하기 전에 언제든지 시작할 수 있습니다.

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

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

로컬로 번들된 모델만 있는 경우 CustomImageLabelerOptions 개체에서 레이블 지정자를 만들고 필요한 신뢰도 점수 임계값을 구성합니다( 모델 평가 참조).

자바

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

코틀린

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

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

라벨러를 실행하기 전에 이를 확인하기만 하면 되지만 원격 호스팅 모델과 로컬로 번들된 모델이 모두 있는 경우 이미지 라벨러를 인스턴스화할 때 이 확인을 수행하는 것이 좋습니다. 다음과 같은 경우 원격 모델에서 라벨러를 생성합니다. 다운로드되었으며 그렇지 않은 경우 로컬 모델에서 다운로드되었습니다.

자바

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

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

코틀린

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

원격으로 호스팅된 모델만 있는 경우 모델이 다운로드되었음을 확인할 때까지 모델 관련 기능을 비활성화해야 합니다(예: 회색으로 표시하거나 UI 일부 숨기기). 모델 관리자의 download() 메서드에 리스너를 연결하면 됩니다.

자바

RemoteModelManager.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.
            }
        });

코틀린

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

2. 입력 이미지 준비

그런 다음 라벨을 지정하려는 각 이미지에 대해 이미지에서 InputImage 객체를 만듭니다. 이미지 라벨러는 Bitmap 을 사용하거나 camera2 API를 사용하는 경우 YUV_420_888 media.Image 를 사용할 때 가장 빠르게 실행되며 가능한 경우 권장됩니다.

서로 다른 소스에서 InputImage 를 생성할 수 있으며 각 소스는 아래에 설명되어 있습니다.

media.Image 사용

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

CameraX 라이브러리를 사용하는 경우 OnImageCapturedListenerImageAnalysis.Analyzer 클래스가 자동으로 회전 값을 계산합니다.

Kotlin+KTX

private class YourImageAnalyzer : ImageAnalysis.Analyzer {
    override fun analyze(imageProxy: ImageProxy?) {
        val mediaImage = imageProxy?.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        if (imageProxy == null || imageProxy.getImage() == null) {
            return;
        }
        Image mediaImage = imageProxy.getImage();
        InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees);
        // Pass image to an ML Kit Vision API
        // ...
    }
}

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

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
}

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;
}

그런 다음 media.Image 객체와 회전 각도 값을 InputImage.fromMediaImage() 에 전달합니다.

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

파일 URI 사용

파일 URI에서 InputImage 객체를 만들려면 앱 컨텍스트와 파일 URI를 InputImage.fromFilePath() 에 전달합니다. 이는 ACTION_GET_CONTENT 텐트를 사용하여 사용자에게 갤러리 앱에서 이미지를 선택하라는 메시지를 표시할 때 유용합니다.

Kotlin+KTX

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

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

ByteBuffer 또는 ByteArray 사용

ByteBuffer 또는 ByteArray 에서 InputImage 객체를 만들려면 먼저 media.Image 입력에 대해 이전에 설명한 대로 이미지 회전 정도를 계산합니다. 그런 다음 이미지의 높이, 너비, 색상 인코딩 형식 및 회전 각도와 함께 버퍼 또는 배열을 사용하여 InputImage 객체를 만듭니다.

Kotlin+KTX

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

Bitmap 사용

Bitmap 객체에서 InputImage 객체를 만들려면 다음과 같이 선언합니다.

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

이미지는 회전 각도와 함께 Bitmap 객체로 표현됩니다.

3. 이미지 라벨러 실행

이미지의 개체에 레이블을 지정하려면 ImageLabelerprocess() 메서드에 image 개체를 전달합니다.

자바

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

코틀린

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

4. 레이블이 지정된 개체에 대한 정보 얻기

이미지 레이블 지정 작업이 성공하면 ImageLabel 개체 목록이 성공 수신기에 전달됩니다. 각 ImageLabel 개체는 이미지에서 레이블이 지정된 항목을 나타냅니다. 각 레이블의 텍스트 설명, 일치의 신뢰도 점수 및 일치 인덱스를 얻을 수 있습니다. 예를 들어:

자바

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

코틀린

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

실시간 성능 향상을 위한 팁

실시간 애플리케이션에서 이미지에 레이블을 지정하려면 다음 지침을 따라 최상의 프레임 속도를 얻으십시오.

  • 이미지 라벨러에 대한 스로틀 호출입니다. 이미지 라벨러가 실행되는 동안 새 비디오 프레임을 사용할 수 있게 되면 프레임을 삭제합니다. 예제는 빠른 시작 샘플 앱의 VisionProcessorBase 클래스를 참조하세요.
  • 이미지 라벨러의 출력을 사용하여 입력 이미지에 그래픽을 오버레이하는 경우 먼저 결과를 얻은 다음 이미지를 렌더링하고 단일 단계에서 오버레이합니다. 이렇게 하면 각 입력 프레임에 대해 한 번만 디스플레이 표면에 렌더링됩니다. 예제는 빠른 시작 샘플 앱의 CameraSourcePreviewGraphicOverlay 클래스를 참조하세요.
  • Camera2 API를 사용하는 경우 ImageFormat.YUV_420_888 형식으로 이미지를 캡처합니다.

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