Segmentação de assuntos com o Kit de ML para Android

Use o Kit de ML para adicionar facilmente atributos de segmentação de assunto ao seu aplicativo.

Recurso Detalhes
Nome do SDK play-services-mlkit-subject-segmentation
Implementação Desagrupado: o modelo é transferido por download dinamicamente usando o Google Play Services.
Impacto no tamanho do app Aumento de tamanho de aproximadamente 200 KB.
Tempo de inicialização Talvez seja necessário aguardar o download do modelo para usá-lo pela primeira vez.

Faça um teste

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 a dependência da biblioteca de segmentação de assunto do Kit de ML ao arquivo Gradle do módulo no nível do app, que geralmente é app/build.gradle:
dependencies {
   implementation 'com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1'
}

Como mencionado acima, o modelo é fornecido pelo Google Play Services. É possível configurar seu app para fazer o download automático do modelo no dispositivo após a instalação do app pela Play Store. Para fazer isso, adicione o seguinte declaração ao arquivo AndroidManifest.xml do app:

<application ...>
      ...
      <meta-data
          android:name="com.google.mlkit.vision.DEPENDENCIES"
          android:value="subject_segment" >
      <!-- To use multiple models: android:value="subject_segment,model2,model3" -->
</application>

Também é possível verificar explicitamente a disponibilidade do modelo e solicitar o download no Google Play Services com a API ModuleInstallClient.

Se você não ativar os downloads do modelo de tempo de instalação ou solicitar o download explícito o modelo é baixado na primeira vez que você executa o segmentado. Solicitações feitas por você antes da conclusão do download não produzem resultados.

1. Preparar a imagem de entrada

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

Você pode criar um InputImage de diferentes origens, cada uma explicada abaixo.

Como usar um media.Image

Para criar um InputImage de um objeto media.Image, como quando você captura uma imagem de um da câmera do dispositivo, transmita o objeto media.Image e o rotação para InputImage.fromMediaImage().

Se você usar o método CameraX, os recursos OnImageCapturedListener e As classes ImageAnalysis.Analyzer calculam o valor de rotação para você.

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
            // ...
        }
    }
}
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 informe o grau de rotação da imagem, pode calculá-lo usando o grau de rotação do dispositivo e a orientação da câmera no dispositivo:

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
}
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 grau de rotação para InputImage.fromMediaImage():

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

Usar um URI de arquivo

Para criar um InputImage de um URI de arquivo, transmita o contexto do aplicativo e o URI do arquivo para InputImage.fromFilePath(). Isso é útil quando você usar uma intent ACTION_GET_CONTENT para solicitar que o usuário selecione uma imagem do app Galeria.

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

Como usar ByteBuffer ou ByteArray

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

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
)
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 InputImage de um objeto Bitmap, faça a seguinte declaração:

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

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

2. Crie uma instância de SubjectSegmentr

Definir as opções do segmentador

Para segmentar sua imagem, primeiro crie uma instância de SubjectSegmenterOptions como seguir:

val options = SubjectSegmenterOptions.Builder()
       // enable options
       .build()
SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
        // enable options
        .build();

Veja os detalhes de cada opção:

Máscara de confiança em primeiro plano

A máscara de confiança em primeiro plano permite distinguir o objeto em primeiro plano do em segundo plano.

Chamar enableForegroundConfidenceMask() nas opções permite que você recupere mais tarde máscara de primeiro plano chamando getForegroundMask() no Objeto SubjectSegmentationResult retornado após o processamento da imagem.

val options = SubjectSegmenterOptions.Builder()
        .enableForegroundConfidenceMask()
        .build()
SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
        .enableForegroundConfidenceMask()
        .build();
Bitmap de primeiro plano

Da mesma forma, também é possível acessar um bitmap do objeto em primeiro plano.

Chamar enableForegroundBitmap() nas opções permite que você recupere mais tarde o bitmap de primeiro plano chamando getForegroundBitmap() no Objeto SubjectSegmentationResult retornado após o processamento da imagem.

val options = SubjectSegmenterOptions.Builder()
        .enableForegroundBitmap()
        .build()
SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
        .enableForegroundBitmap()
        .build();
Máscara de confiança de vários assuntos

Assim como nas opções de primeiro plano, você pode usar SubjectResultOptions para ativar a máscara de confiança para cada objeto em primeiro plano da seguinte maneira:

val subjectResultOptions = SubjectSegmenterOptions.SubjectResultOptions.Builder()
    .enableConfidenceMask()
    .build()

val options = SubjectSegmenterOptions.Builder()
    .enableMultipleSubjects(subjectResultOptions)
    .build()
SubjectResultOptions subjectResultOptions =
        new SubjectSegmenterOptions.SubjectResultOptions.Builder()
            .enableConfidenceMask()
            .build()

SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
      .enableMultipleSubjects(subjectResultOptions)
      .build()
Bitmap de vários assuntos

Da mesma forma, você pode ativar o bitmap para cada assunto:

val subjectResultOptions = SubjectSegmenterOptions.SubjectResultOptions.Builder()
    .enableSubjectBitmap()
    .build()

val options = SubjectSegmenterOptions.Builder()
    .enableMultipleSubjects(subjectResultOptions)
    .build()
SubjectResultOptions subjectResultOptions =
      new SubjectSegmenterOptions.SubjectResultOptions.Builder()
        .enableSubjectBitmap()
        .build()

SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
      .enableMultipleSubjects(subjectResultOptions)
      .build()

Criar o segmento de assunto

Depois de especificar as opções SubjectSegmenterOptions, crie um Instância de SubjectSegmenter chamando getClient() e transmitindo as opções como um :

val segmenter = SubjectSegmentation.getClient(options)
SubjectSegmenter segmenter = SubjectSegmentation.getClient(options);

3. Processar uma imagem

Transmita o InputImage preparado. ao método process do SubjectSegmenter:

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

4. Acessar o resultado da segmentação de assunto

Extrair máscaras e bitmaps de primeiro plano

Depois do processamento, é possível recuperar a máscara de primeiro plano da sua imagem chamando getForegroundConfidenceMask() da seguinte maneira:

val colors = IntArray(image.width * image.height)

val foregroundMask = result.foregroundConfidenceMask
for (i in 0 until image.width * image.height) {
  if (foregroundMask[i] > 0.5f) {
    colors[i] = Color.argb(128, 255, 0, 255)
  }
}

val bitmapMask = Bitmap.createBitmap(
  colors, image.width, image.height, Bitmap.Config.ARGB_8888
)
int[] colors = new int[image.getWidth() * image.getHeight()];

FloatBuffer foregroundMask = result.getForegroundConfidenceMask();
for (int i = 0; i < image.getWidth() * image.getHeight(); i++) {
  if (foregroundMask.get() > 0.5f) {
    colors[i] = Color.argb(128, 255, 0, 255);
  }
}

Bitmap bitmapMask = Bitmap.createBitmap(
      colors, image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888
);

Você também pode extrair um bitmap do primeiro plano da imagem chamando getForegroundBitmap():

val foregroundBitmap = result.foregroundBitmap
Bitmap foregroundBitmap = result.getForegroundBitmap();

Extrair máscaras e bitmaps de cada assunto

Da mesma forma, é possível recuperar a máscara dos assuntos segmentados chamando getConfidenceMask() em cada assunto desta maneira:

val subjects = result.subjects

val colors = IntArray(image.width * image.height)
for (subject in subjects) {
  val mask = subject.confidenceMask
  for (i in 0 until subject.width * subject.height) {
    val confidence = mask[i]
    if (confidence > 0.5f) {
      colors[image.width * (subject.startY - 1) + subject.startX] =
          Color.argb(128, 255, 0, 255)
    }
  }
}

val bitmapMask = Bitmap.createBitmap(
  colors, image.width, image.height, Bitmap.Config.ARGB_8888
)
List subjects = result.getSubjects();

int[] colors = new int[image.getWidth() * image.getHeight()];
for (Subject subject : subjects) {
  FloatBuffer mask = subject.getConfidenceMask();
  for (int i = 0; i < subject.getWidth() * subject.getHeight(); i++) {
    float confidence = mask.get();
    if (confidence > 0.5f) {
      colors[width * (subject.getStartY() - 1) + subject.getStartX()]
          = Color.argb(128, 255, 0, 255);
    }
  }
}

Bitmap bitmapMask = Bitmap.createBitmap(
  colors, image.width, image.height, Bitmap.Config.ARGB_8888
);

Também é possível acessar o bitmap de cada assunto segmentado da seguinte maneira:

val bitmaps = mutableListOf()
for (subject in subjects) {
  bitmaps.add(subject.bitmap)
}
List bitmaps = new ArrayList<>();
for (Subject subject : subjects) {
  bitmaps.add(subject.getBitmap());
}

Dicas para melhorar o desempenho

Para cada sessão do app, a primeira inferência costuma ser mais lenta do que a subsequente e inferências devido à inicialização do modelo. Se a baixa latência for um fator crítico, considere chamar um "boneco" inferência antecipada.

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

  • Para que o Kit de ML gere um resultado de segmentação preciso, a imagem precisa ter pelo menos 512 x 512 pixels.
  • Uma imagem com foco inadequado também pode afetar a precisão. Se você não conseguir resultados aceitáveis, peça ao usuário para recapturar a imagem.