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 muitas tarefas de visão por ML que o MediaPipe Solutions oferece. O MediaPipe Tasks está disponível para Android, iOS, Python (incluindo o Raspberry Pi!) e a Web.

Neste codelab, você vai começar com um app Android que permite desenhar dígitos numéricos na tela e, em seguida, 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 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 ou emulador Android para executar o app.
  • Conhecimento básico de desenvolvimento para Android (não é "Hello World", mas não está muito longe disso).

2. Adicionar as tarefas do MediaPipe ao app Android

Fazer o download do app inicial do Android

Este codelab vai começar com um exemplo pré-criado que permite desenhar na tela. Confira o app de inicialização no repositório oficial de amostras 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. Navegue até o local em que você clonou ou fez o download do repositório e abra o diretório codelabs/digitclassifier/android/start.
  2. Clique na seta verde run ( 7e15a9c9e1620fe7.png) no canto superior direito do Android Studio para verificar se tudo foi aberto corretamente.
  3. O app vai abrir com uma tela preta em que você pode desenhar, além de um botão Clear para redefinir essa tela. Embora seja possível desenhar nessa tela, ela não faz muito mais do que isso. Vamos corrigir isso agora.

11a0f6fe021fdc92.jpeg

Modelo

Ao executar o app pela primeira vez, talvez você perceba que um arquivo chamado mnist.tflite é transferido por download e armazenado no diretório assets do app. Para simplificar, já pegamos um modelo conhecido, 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 do 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 as tarefas do MediaPipe, é necessário importar 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 fazer o download dessa dependência.

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

Na próxima etapa, você vai preencher uma classe que vai fazer o trabalho pesado para a 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 abaixo. Isso vai criar um listener que será usado para transmitir os resultados da classe DigitClassifierHelper de volta para onde quer que ela esteja detectando esses resultados. Neste caso, será a 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 precisa 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 a linha a seguir para criar um marcador de posição para o ImageClassifier que será usado neste app:

// STEP 3 define classifier

private var digitClassifier: ImageClassifier? = null
  1. Adicione a função a seguir onde você encontra 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, então vamos analisar partes menores para entender 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 o 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 vamos querer inicializar o ImageClassifier antes de usá-lo, adicione um bloco de inicialização para chamar setupDigitClassifier().
init {
    setupDigitClassifier()
}
  1. Por fim, role a tela até o comentário // STEP 5 create classify function e adicione o seguinte código. Essa função vai aceitar um Bitmap, que, nesse caso, é o dígito desenhado, convertê-lo em um objeto de imagem do MediaPipe (MPImage) e, em seguida, classificar essa imagem usando o ImageClassifier, além de registrar o tempo que a inferência leva 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)
    }
}

É isso para 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 vai acontecer.

  1. Na parte de baixo do arquivo, você verá um comentário que diz // STEP 6 Set up listener. Adicione 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 em uma linha de execução em segundo plano, também será necessário 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ê verá 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. Movendo-se para // 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 código a seguir para chamar uma nova função que você vai adicionar em breve. Esse bloco de código vai acionar a classificação quando você tirar 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 extrai um bitmap da tela e o transmite para o 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 disso, você terá um app funcional que pode classificar dígitos desenhados na tela. Implante o app em um emulador 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 reconhecer. Ele precisa mostrar o dígito que o modelo acredita ter sido desenhado, bem como o tempo que levou para prever esse dígito.

7f37187f8f919638.gif

6. Parabéns!

Parabéns! 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, é possível treinar seu próprio modelo para classificar letras desenhadas, animais ou uma infinidade de outros itens. Acesse 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.
  • Saiba mais sobre as outras tarefas do MediaPipe disponíveis para Android, incluindo a detecção de pontos de referência do rosto, o reconhecimento de gestos e a classificação de áudio.

Estamos ansiosos para ver as coisas legais que você vai criar!