Segmentação de selfie com o Kit de ML no Android

O kit de ML oferece um SDK otimizado para segmentação de selfies.

Os recursos do Selfie Segmenter são vinculados estaticamente ao seu aplicativo no momento da criação. Isso aumentará o tamanho de download do app em cerca de 4,5 MB, e a latência da API poderá variar de 25 ms para 65 ms, dependendo do tamanho da imagem de entrada, conforme medido em um Pixel 4.

Testar

Antes de começar

  1. No arquivo build.gradle no nível do projeto, inclua o repositório Maven do Google nas seções buildscript e allprojects.
  2. Adicione as dependências das bibliotecas Android do Kit de ML ao arquivo Gradle do módulo no nível do app, que geralmente é app/build.gradle:
dependencies {
  implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta4'
}

1. Criar uma instância de Segmenter

Opções de segmentação

Para segmentar uma imagem, primeiro crie uma instância de Segmenter especificando as opções a seguir.

Modo do detector

O Segmenter opera de dois modos. Escolha a opção mais adequada ao seu caso de uso.

STREAM_MODE (default)

Este modo foi projetado para transmitir quadros de vídeo ou da câmera. Neste modo, o indexador aproveitará os resultados de frames anteriores para retornar resultados de segmentação mais suaves.

SINGLE_IMAGE_MODE

Esse modo foi projetado para imagens únicas não relacionadas. Nesse modo, o indexador processa cada imagem de maneira independente, sem suavizar os frames.

Ativar máscara de tamanho bruto

Solicita o indexador a retornar a máscara de tamanho bruto que corresponda ao tamanho de saída do modelo.

O tamanho bruto da máscara (por exemplo, 256 x 256) costuma ser menor que o tamanho da imagem de entrada. Chame SegmentationMask#getWidth() e SegmentationMask#getHeight() para ver o tamanho da máscara ao ativar essa opção.

Sem especificar essa opção, o segmentador redimensionará a máscara bruta para corresponder ao tamanho da imagem de entrada. Use essa opção se você quiser aplicar uma lógica de redimensionamento personalizada ou se não for necessário fazer redimensionamento no seu caso de uso.

Especifique as opções do indexador:

Kotlin

val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .enableRawSizeMask()
            .build()

Java

SelfieSegmenterOptions options =
        new SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .enableRawSizeMask()
            .build();

Crie uma instância de Segmenter. Transmita as opções especificadas:

Kotlin

val segmenter = Segmentation.getClient(options)

Java

Segmenter segmenter = Segmentation.getClient(options);

2. Preparar a imagem de entrada

Para realizar a segmentação em uma imagem, crie um objeto InputImage a partir de um Bitmap, media.Image, ByteBuffer, matriz de bytes ou um arquivo no dispositivo.

É possível criar um objeto InputImage a partir de diferentes origens, cada um explicado abaixo.

Como usar um media.Image

Para criar um objeto InputImage a partir de um objeto media.Image, como ao capturar uma imagem da câmera de um dispositivo, transmita o objeto media.Image e a rotação da imagem para InputImage.fromMediaImage().

Se você usar a biblioteca CameraX, as classes OnImageCapturedListener e ImageAnalysis.Analyzer calcularão o valor de rotação automaticamente.

Kotlin

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) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

Se você não usar uma biblioteca de câmera que ofereça o grau de rotação da imagem, você poderá calculá-lo usando o grau de rotação do dispositivo e a orientação do sensor da câmera:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * 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, isFrontFacing: Boolean): 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)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * 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, boolean isFrontFacing)
        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);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

Em seguida, transmita o objeto media.Image e o valor do grau de rotação para InputImage.fromMediaImage():

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

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

Como usar um URI de arquivo

Para criar um objeto InputImage a partir de um URI de arquivo, transmita o contexto do app e o URI do arquivo para InputImage.fromFilePath(). Isso é útil ao usar um intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do app de galeria dele.

Kotlin

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

Como usar um ByteBuffer ou ByteArray

Para criar um objeto InputImage a partir de ByteBuffer ou ByteArray, primeiro calcule o grau de rotação de imagem conforme descrito anteriormente para a entrada media.Image. Em seguida, crie o objeto InputImage com o buffer ou a matriz, junto com a altura, a largura, o formato de codificação de cores e o grau de rotação da imagem:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* 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
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

Como usar um Bitmap

Para criar um objeto InputImage a partir de um objeto Bitmap, faça a seguinte declaração:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

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

A imagem é representada por um objeto Bitmap com os graus de rotação.

3. Processar a imagem

Transmita o objeto InputImage preparado para o método process do Segmenter.

Kotlin

Task<SegmentationMask> result = segmenter.process(image)
       .addOnSuccessListener { results ->
           // Task completed successfully
           // ...
       }
       .addOnFailureListener { e ->
           // Task failed with an exception
           // ...
       }

Java

Task<SegmentationMask> result =
        segmenter.process(image)
                .addOnSuccessListener(
                        new OnSuccessListener<SegmentationMask>() {
                            @Override
                            public void onSuccess(SegmentationMask mask) {
                                // Task completed successfully
                                // ...
                            }
                        })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                // Task failed with an exception
                                // ...
                            }
                        });

4. Ver o resultado da segmentação

Você pode obter o resultado da segmentação da seguinte forma:

Kotlin

val mask = segmentationMask.getBuffer()
val maskWidth = segmentationMask.getWidth()
val maskHeight = segmentationMask.getHeight()

for (val y = 0; y < maskHeight; y++) {
  for (val x = 0; x < maskWidth; x++) {
    // Gets the confidence of the (x,y) pixel in the mask being in the foreground.
    val foregroundConfidence = mask.getFloat()
  }
}

Java

ByteBuffer mask = segmentationMask.getBuffer();
int maskWidth = segmentationMask.getWidth();
int maskHeight = segmentationMask.getHeight();

for (int y = 0; y < maskHeight; y++) {
  for (int x = 0; x < maskWidth; x++) {
    // Gets the confidence of the (x,y) pixel in the mask being in the foreground.
    float foregroundConfidence = mask.getFloat();
  }
}

Para um exemplo completo de como usar os resultados de segmentação, consulte a Amostra do guia de início rápido do Kit de ML.

Dicas para melhorar o desempenho

A qualidade dos resultados depende da qualidade da imagem de entrada:

  • Para que o kit de ML tenha um resultado de segmentação preciso, a imagem precisa ter pelo menos 256 x 256 pixels.
  • Um foco de imagem ruim também pode afetar a precisão. Se os resultados não forem aceitáveis, peça para o usuário recapturar a imagem.

Se você quiser usar a segmentação em um aplicativo em tempo real, siga estas diretrizes para conseguir os melhores frame rates:

  • Use STREAM_MODE.
  • Considere capturar imagens em uma resolução mais baixa. No entanto, lembre-se também dos requisitos de dimensão de imagem dessa API.
  • Ative a opção de máscara de tamanho bruto e combine toda a lógica de redimensionamento. Por exemplo, em vez de permitir que a API redimensione a máscara para corresponder ao tamanho da imagem de entrada primeiro e depois redimensione-a novamente para corresponder ao tamanho da visualização para exibição, solicite a máscara de tamanho bruto e combine essas duas etapas em uma.
  • Se você usar a API Camera ou camera2, as chamadas para o detector serão limitadas. Se um novo frame de vídeo for disponibilizado enquanto o detector estiver em execução, descarte esse frame. Consulte a classe VisionProcessorBase no app de amostra do guia de início rápido para ver um exemplo.
  • Se você usar a API CameraX, verifique se a estratégia de pressão de retorno está definida como o valor padrão ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. Isso garante que apenas uma imagem será exibida por vez para análise. Se mais imagens forem produzidas quando o analisador estiver ocupado, elas serão descartadas automaticamente e não ficarão na fila para entrega. Quando a imagem que está sendo analisada é fechada chamando ImageProxy.close(), a próxima imagem mais recente é entregue.
  • Se você usar a saída do detector para sobrepor elementos gráficos na imagem de entrada, primeiro acesse o resultado do kit de ML e, em seguida, renderize a imagem e a sobreposição em uma única etapa. Isso renderiza a superfície de exibição apenas uma vez para cada frame de entrada. Consulte as classes CameraSourcePreview e GraphicOverlay no app de amostra do guia de início rápido para ver um exemplo.
  • Se você usar a API Camera2, capture imagens no formato ImageFormat.YUV_420_888. Se você usar a API Camera mais antiga, capture imagens no formato ImageFormat.NV21.