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 resultados imediatos, envolventes e úteis aos 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 do ML Vision que o MediaPipe Solutions oferece. O MediaPipe Tasks está disponível para Android, iOS, Python (incluindo o Raspberry Pi!) e para a 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 como um único valor de 0 a 9.

O que você vai aprender

  • Como incorporar uma tarefa de classificação de imagens em 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 ou emulador Android para executar o app.
  • Conhecimentos básicos de desenvolvimento para Android (não é "Hello World", mas não está longe).

2. Adicionar o MediaPipe Tasks ao app Android

Baixar o app Android para iniciantes

Este codelab começa 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 "Código" > Baixe o 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. Navegue até o local onde você clonou ou fez o download do repositório e abra o codelabs/digitclassifier/android/start directory.
  2. Verifique se tudo foi aberto corretamente clicando na seta verde run ( 7e15a9c9e1620fe7.png) no canto superior direito do Android Studio.
  3. Você verá o app abrir com uma tela preta em que pode desenhar, bem como um botão Clear para redefinir essa tela. Embora você possa desenhar nessa tela, isso não faz muito mais. Vamos começar a corrigir isso agora.

11a0f6fe021fdc92.jpeg

Modelo

Ao executar o app pela primeira vez, você vai perceber que um arquivo chamado mnist.tflite é transferido por download e armazenado no diretório assets do app. Para simplificar, já usamos 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 escritas à mão, remova o arquivo download_models.gradle, exclua a referência a ele no arquivo build.gradle no nível do app e altere o nome do modelo posteriormente no código (especificamente no arquivo DigitClassifierHelper.kt).

Atualizar o build.gradle

Antes de começar a usar o MediaPipe Tasks, é preciso importar a biblioteca.

  1. Abra o arquivo build.gradle localizado no módulo app e role para baixo até o bloco Dependencies.
  2. Na parte de baixo do bloco, você verá um comentário com a mensagem // 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 fazer o download dessa dependência.

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

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

  1. Na parte de cima da classe, encontre o comentário // STEP 2 Create listener
  2. Substitua essa linha pelo código a seguir. Isso criará um listener que será usado para transmitir os resultados da classe DigitClassifierHelper de volta para o local onde os resultados forem detectados (neste 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. Também será necessário aceitar um DigitClassifierListener como um parâmetro opcional para a classe:
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. Indo até a linha que diz // STEP 3 definir classifier, adicione a linha a seguir para criar um marcador de posição para o ImageClassifier que será usado para o app:

// ETAPA 3: defina o classificador

private var digitClassifier: ImageClassifier? = null
  1. Adicione a seguinte função com 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)
    }
}

A seção acima apresenta algumas coisas, então vamos analisar as partes menores para entender bem 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 definirá os parâmetros usados pelo ImageClassifier. Isso inclui o modelo armazenado no app (mnist.tflite) em BaseOptions e o RunningMode em ImageClassifierOptions, que nesse caso é IMAGE, mas VIDEO e LIVE_STREAM são opções adicionais 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 suas opções de configuração, você poderá criar seu 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 do uso, você pode adicionar um bloco init para chamar setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Por fim, role para baixo até o comentário que diz // ETAPA 5: crie a função de classificação e adicione o código a seguir. Essa função aceita um Bitmap, que nesse caso é o dígito desenhado, converte-o em um objeto MediaPipe Image (MPImage), classifica a imagem usando o ImageClassifier e registra quanto tempo a inferência leva antes de retornar esses resultados no 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 o arquivo auxiliar está completo. Na próxima seção, você preencherá as etapas finais para começar a classificar os números coletados.

4. Executar inferências com o MediaPipe Tasks

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

  1. Na parte inferior do arquivo, você verá o comentário // STEP 6 Set up listener. Você adicionará as funções onResults() e onError() associadas ao listener.
// 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())
    }
}

O onResults() é particularmente importante, porque exibe os resultados recebidos do ImageClassifier. Como esse callback é acionado em uma linha de execução em segundo plano, você também vai precisar 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 será necessário adicionar a declaração de implementação na parte superior da classe.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. Na parte superior da classe, será exibido o comentário // ETAPA 7a Inicializar classificador. É aqui que você colocará a declaração do DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. Passando para // ETAPA 7b Inicializar classificador,você pode inicializar 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 código a seguir para chamar uma nova função que será adicionada em breve. Este bloco de código 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 SortDrawing(). Isso extrai um bitmap da tela e o transmite ao DigitClassifierHelper para fazer a classificação e receber os resultados na função da 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ê deve ter um app que funcione para classificar dígitos desenhados na tela! Implante o app em um Android Emulator ou em um dispositivo Android físico para testá-lo.

  1. Clique em Run ( 7e15a9c9e1620fe7.png) na barra de ferramentas do Android Studio para executar o app.
  2. Desenhe qualquer dígito no bloco de desenho e confira se o app consegue reconhecê-lo. Elas devem exibir o dígito que o modelo acredita ter sido traçado e o tempo necessário para prever esse dígito.

7f37187f8f919638.gif

6. Parabéns!

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

Próximas etapas

  • Agora que você sabe classificar dígitos, treine seu próprio modelo para classificar letras desenhadas, animais ou um número infinito de outros itens. Consulte a documentação para treinar um novo modelo de classificação de imagens com o MediaPipe Model Maker na página developers.google.com/mediapipe (links em inglês).
  • Saiba mais sobre outras tarefas do MediaPipe disponíveis para Android, como a detecção de ponto de referência facial, o reconhecimento de gestos e a classificação de áudio.

Estamos ansiosos para receber todas as coisas legais que você fizer!