Criar um app Android classificador de dígitos manuscritos com o MediaPipe Tasks

1. Introdução

O que é o MediaPipe?

Com o MediaPipe Solutions, é possível aplicar soluções de machine learning (ML) nos apps. Ele oferece um framework para configurar pipelines de processamento pré-criados que entregam saídas imediatas, interativas e úteis para os usuários. É possível personalizar essas soluções com o MediaPipe Model Maker para atualizar os modelos padrão.

A classificação de imagens é uma das várias tarefas de visão de ML que o MediaPipe Solutions oferece. O MediaPipe Tasks está disponível para Android, iOS, Python (incluindo o Raspberry Pi!) e na Web.

Neste codelab, você vai começar com um app Android que permite desenhar dígitos numéricos na tela e adicionar uma funcionalidade que classifica esses dígitos desenhados como um único valor de 0 a 9.

O que você vai aprender

  • Como incorporar uma tarefa de classificação de imagens a um app Android com o MediaPipe Tasks.

O que é necessário

  • Uma versão instalada do Android Studio. Este codelab foi escrito e testado com o Android Studio Giraffe.
  • Um dispositivo Android ou emulador para executar o app.
  • Conhecimento básico de desenvolvimento para Android (não é "Hello World", mas não está muito longe disso).

2. Adicionar o MediaPipe Tasks ao app Android

Baixar o app inicial do Android

Este codelab vai começar com um exemplo pré-criado que permite desenhar na tela. Você pode encontrar esse app inicial no repositório oficial de exemplos do MediaPipe aqui. Clone o repositório ou faça o download do arquivo ZIP clicando em "Code" > "Download ZIP".

Importar o app para o Android Studio

  1. Abra o Android Studio.
  2. Na tela Welcome to Android Studio, selecione Open no canto superior direito.

a0b5b070b802e4ea.png

  1. Acesse o local em que você clonou ou baixou o repositório e abra o diretório codelabs/digitclassifier/android/start.
  2. Verifique se tudo foi aberto corretamente clicando na seta verde executar ( 7e15a9c9e1620fe7.png) no canto superior direito do Android Studio.
  3. O app vai abrir com uma tela preta em que você pode desenhar, além de um botão Limpar para redefinir a tela. Embora seja possível desenhar nessa tela, ela não faz muito mais. Por isso, vamos começar a corrigir isso agora.

11a0f6fe021fdc92.jpeg

Modelo

Ao executar o app pela primeira vez, você vai notar que um arquivo chamado mnist.tflite é baixado e armazenado no diretório assets do app. Para simplificar, já pegamos um modelo conhecido, o MNIST, que classifica dígitos, e o adicionamos ao app usando o script download_models.gradle no projeto. Se você decidir treinar seu próprio modelo personalizado, como um para letras manuscritas, remova o arquivo download_models.gradle, exclua a referência a ele no arquivo build.gradle no nível do app e mude o nome do modelo mais tarde no código (especificamente no arquivo DigitClassifierHelper.kt).

Atualizar o build.gradle

Antes de começar a usar o MediaPipe Tasks, importe a biblioteca.

  1. Abra o arquivo build.gradle localizado no módulo app e role a tela para baixo até o bloco dependencies.
  2. Você vai encontrar um comentário na parte de baixo desse bloco que diz // STEP 1 Dependency Import.
  3. Substitua essa linha pela seguinte implementação:
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. Clique no botão Sync Now que aparece no banner na parte de cima do Android Studio para baixar essa dependência.

3. Criar um auxiliar de classificador de dígitos do MediaPipe Tasks

Na próxima etapa, você vai preencher uma classe que fará o trabalho pesado da classificação de machine learning. Abra o DigitClassifierHelper.kt e vamos começar!

  1. Encontre o comentário na parte de cima da classe que diz // STEP 2 Create listener
  2. Substitua essa linha pelo código a seguir. Isso vai criar um listener que será usado para transmitir resultados da classe DigitClassifierHelper de volta para onde estiver detectando esses resultados. Nesse caso, será sua classe DigitCanvasFragment, mas vamos chegar lá em breve.
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. Você também precisará aceitar um DigitClassifierListener como um parâmetro opcional para a classe:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. Na linha // STEP 3 define classifier, adicione o seguinte código para criar um marcador de posição para o ImageClassifier que será usado neste app:

// ETAPA 3 definir classificador

private var digitClassifier: ImageClassifier? = null
  1. Adicione a seguinte função onde você vê o comentário // STEP 4 set up classifier:
// STEP 4 set up classifier
private fun setupDigitClassifier() {

    val baseOptionsBuilder = BaseOptions.builder()
        .setModelAssetPath("mnist.tflite")

    // Describe additional options
    val optionsBuilder = ImageClassifierOptions.builder()
        .setRunningMode(RunningMode.IMAGE)
        .setBaseOptions(baseOptionsBuilder.build())

    try {
        digitClassifier =
            ImageClassifier.createFromOptions(
                context,
                optionsBuilder.build()
            )
    } catch (e: IllegalStateException) {
        digitClassifierListener?.onError(
            "Image classifier failed to initialize. See error logs for " +
                    "details"
        )
        Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
    }
}

Há algumas coisas acontecendo na seção acima. Vamos analisar partes menores para entender melhor o que está acontecendo.

val baseOptionsBuilder = BaseOptions.builder()
    .setModelAssetPath("mnist.tflite")

// Describe additional options
val optionsBuilder = ImageClassifierOptions.builder()
    .setRunningMode(RunningMode.IMAGE)
    .setBaseOptions(baseOptionsBuilder.build())

Esse bloco vai definir os parâmetros usados pelo ImageClassifier. Isso inclui o modelo armazenado no app (mnist.tflite) em BaseOptions e o RunningMode em ImageClassifierOptions, que, neste caso, é IMAGE, mas VIDEO e LIVE_STREAM são outras opções disponíveis. Outros parâmetros disponíveis são "MaxResults", que limita o modelo a retornar um número máximo de resultados, e "ScoreThreshold", que define a confiança mínima que o modelo precisa ter em um resultado antes de retorná-lo.

try {
    digitClassifier =
        ImageClassifier.createFromOptions(
            context,
            optionsBuilder.build()
        )
} catch (e: IllegalStateException) {
    digitClassifierListener?.onError(
        "Image classifier failed to initialize. See error logs for " +
                "details"
    )
    Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
}

Depois de criar as opções de configuração, você pode criar um novo ImageClassifier transmitindo um contexto e as opções. Se algo der errado nesse processo de inicialização, um erro será retornado pelo DigitClassifierListener.

  1. Como queremos inicializar o ImageClassifier antes de usá-lo, adicione um bloco init para chamar setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Por fim, role a tela para baixo até o comentário // STEP 5 create classify function e adicione o seguinte código. Essa função vai aceitar um Bitmap, que, neste caso, é o dígito desenhado, convertê-lo em um objeto de imagem do MediaPipe (MPImage) e classificar essa imagem usando o ImageClassifier. Além disso, ela vai registrar quanto tempo leva a inferência antes de retornar esses resultados pelo DigitClassifierListener.
// STEP 5 create classify function
fun classify(image: Bitmap) {
    if (digitClassifier == null) {
        setupDigitClassifier()
    }

    // Convert the input Bitmap object to an MPImage object to run inference.
    // Rotating shouldn't be necessary because the text is being extracted from
    // a view that should always be correctly positioned.
    val mpImage = BitmapImageBuilder(image).build()

    // Inference time is the difference between the system time at the start and finish of the
    // process
    val startTime = SystemClock.uptimeMillis()

    // Run image classification using MediaPipe Image Classifier API
    digitClassifier?.classify(mpImage)?.also { classificationResults ->
        val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
        digitClassifierListener?.onResults(classificationResults, inferenceTimeMs)
    }
}

E isso é tudo sobre o arquivo auxiliar. Na próxima seção, você vai preencher as etapas finais para começar a classificar os números sorteados.

4. Executar inferência com o MediaPipe Tasks

Para começar esta seção, abra a classe DigitCanvasFragment no Android Studio, que é onde todo o trabalho será realizado.

  1. Na parte de baixo desse arquivo, você vai encontrar um comentário que diz // STEP 6 Set up listener. Você vai adicionar as funções onResults() e onError() associadas ao listener aqui.
// STEP 6 Set up listener
override fun onError(error: String) {
    activity?.runOnUiThread {
        Toast.makeText(requireActivity(), error, Toast.LENGTH_SHORT).show()
        fragmentDigitCanvasBinding.tvResults.text = ""
    }
}

override fun onResults(
    results: ImageClassifierResult,
    inferenceTime: Long
) {
    activity?.runOnUiThread {
        fragmentDigitCanvasBinding.tvResults.text = results
            .classificationResult()
            .classifications().get(0)
            .categories().get(0)
            .categoryName()

        fragmentDigitCanvasBinding.tvInferenceTime.text = requireActivity()
            .getString(R.string.inference_time, inferenceTime.toString())
    }
}

onResults() é particularmente importante porque mostra os resultados recebidos do ImageClassifier. Como esse callback é acionado de uma linha de execução em segundo plano, você também precisa executar as atualizações da interface na linha de execução de interface do Android.

  1. Como você está adicionando novas funções de uma interface na etapa acima, também é necessário adicionar a declaração de implementação na parte de cima da classe.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Na parte de cima da classe, você vai encontrar um comentário que diz // STEP 7a Initialize classifier. É aqui que você vai colocar a declaração do DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Descendo até // STEP 7b Initialize classifier,você pode inicializar o digitClassifierHelper na função onViewCreated().
// STEP 7b Initialize classifier
// Initialize the digit classifier helper, which does all of the
// ML work. This uses the default values for the classifier.
digitClassifierHelper = DigitClassifierHelper(
    context = requireContext(), digitClassifierListener = this
)
  1. Para as últimas etapas, encontre o comentário // STEP 8a*: classify* e adicione o seguinte código para chamar uma nova função que você vai adicionar em breve. Esse bloco de código vai acionar a classificação quando você levantar o dedo da área de desenho no app.
// STEP 8a: classify
classifyDrawing()
  1. Por fim, procure o comentário // STEP 8b classify para adicionar a nova função classifyDrawing(). Isso vai extrair um bitmap da tela e transmiti-lo ao DigitClassifierHelper para realizar a classificação e receber os resultados na função de interface onResults().
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. Implantar e testar o app

Depois de tudo isso, você terá um app funcional que pode classificar os dígitos desenhados na tela. Implante o app em um Android Emulator ou dispositivo Android físico para testá-lo.

  1. Clique em Executar ( 7e15a9c9e1620fe7.png) na barra de ferramentas do Android Studio para executar o app.
  2. Desenhe qualquer dígito no bloco de desenho e veja se o app consegue reconhecê-lo. Ele precisa mostrar o dígito que o modelo acredita ter sido desenhado e quanto tempo levou para prever esse dígito.

7f37187f8f919638.gif

6. Parabéns!

Você conseguiu! Neste codelab, você aprendeu a adicionar a classificação de imagens a um app Android e, especificamente, a classificar dígitos desenhados à mão usando o modelo MNIST.

Próximas etapas

  • Agora que você pode classificar dígitos, talvez queira treinar seu próprio modelo para classificar letras desenhadas, animais ou um número infinito de outros itens. A documentação para treinar um novo modelo de classificação de imagens com o MediaPipe Model Maker está disponível na página developers.google.com/mediapipe.
  • Saiba mais sobre as outras Tarefas do MediaPipe disponíveis para Android, incluindo detecção de pontos de referência faciais, reconhecimento de gestos e classificação de áudio.

Estamos ansiosos para ver tudo o que você vai criar!