Segmentação de imagens acelerada em tempo real no Android com LiteRT

1. Antes de começar

Digitar o código é uma ótima maneira de criar memória muscular e aprofundar seu entendimento do material. Embora copiar e colar possa economizar tempo, investir nessa prática pode levar a uma maior eficiência e habilidades de programação mais fortes a longo prazo.

Neste codelab, você vai aprender a criar um aplicativo Android que realiza segmentação de imagens em tempo real em um feed de câmera ao vivo usando o novo tempo de execução do Google para TensorFlow Lite, o LiteRT. Você vai pegar um aplicativo Android inicial e adicionar recursos de segmentação de imagens a ele. Também vamos analisar as etapas de pré-processamento, inferência e pós-processamento. Você vai:

  • Crie um app Android que segmente imagens em tempo real.
  • Integrar um modelo pré-treinado de segmentação de imagens LiteRT.
  • Pré-processe a imagem de entrada para o modelo.
  • Use o ambiente de execução LiteRT para aceleração de CPU e GPU.
  • Entenda como processar a saída do modelo para mostrar a máscara de segmentação.
  • Entenda como ajustar a câmera frontal.

No final, você vai criar algo semelhante à imagem abaixo:

App finalizado

Pré-requisitos

Este codelab foi criado para desenvolvedores de dispositivos móveis experientes que querem ganhar experiência com machine learning. Você precisa:

  • Desenvolvimento para Android usando Kotlin e Android Studio
  • Conceitos básicos de processamento de imagens

O que você vai aprender

  • Como integrar e usar o tempo de execução do LiteRT em um aplicativo Android.
  • Como realizar a segmentação de imagens usando um modelo LiteRT pré-treinado.
  • Como pré-processar a imagem de entrada para o modelo.
  • Como executar a inferência para o modelo.
  • Como processar a saída de um modelo de segmentação para visualizar os resultados.
  • Como usar o CameraX para processamento de feed de câmera em tempo real.

O que é necessário

  • Uma versão recente do Android Studio (testada na v2025.1.1).
  • Um dispositivo Android físico. Recomendamos testar em dispositivos Galaxy e Pixel.
  • O exemplo de código (do GitHub).
  • Conhecimento básico de desenvolvimento para Android em Kotlin.

2. Segmentação de imagens

A segmentação de imagens é uma tarefa de visão computacional que envolve dividir uma imagem em vários segmentos ou regiões. Ao contrário da detecção de objetos, que desenha uma caixa delimitadora ao redor de um objeto, a segmentação de imagens atribui uma classe ou um rótulo específico a cada pixel da imagem. Isso fornece um entendimento muito mais detalhado e granular do conteúdo da imagem, permitindo que você saiba o formato e o limite exatos de cada objeto.

Por exemplo, em vez de apenas saber que uma "pessoa" está em uma caixa, você pode saber exatamente quais pixels pertencem a ela. Neste tutorial, mostramos como realizar a segmentação de imagens em tempo real em um dispositivo Android usando um modelo de machine learning pré-treinado.

Exemplo de segmentação

LiteRT: impulsionando a borda da ML no dispositivo

Uma tecnologia fundamental que permite a segmentação em tempo real e de alta fidelidade em dispositivos móveis é o LiteRT. Como o ambiente de execução de alto desempenho de próxima geração do Google para o TensorFlow Lite, o LiteRT foi projetado para oferecer a melhor performance possível do hardware subjacente.

Isso é feito com o uso inteligente e otimizado de aceleradores de hardware, como a GPU (unidade de processamento gráfico) e a NPU (unidade de processamento neural). Ao descarregar a intensa carga de trabalho computacional do modelo de segmentação da CPU de uso geral para esses processadores especializados, o LiteRT reduz drasticamente o tempo de inferência. Essa aceleração permite executar modelos complexos sem problemas em uma transmissão de câmera ao vivo, expandindo o que podemos alcançar com o aprendizado de máquina diretamente no seu smartphone. Sem esse nível de desempenho, a segmentação em tempo real seria muito lenta e instável para uma boa experiência do usuário.

3. Começar a configuração

Clonar o repositório

Primeiro, clone o repositório do LiteRT:

git clone https://github.com/google-ai-edge/LiteRT.git

LiteRT/litert/samples/image_segmentation é o diretório com todos os recursos necessários. Neste codelab, você só vai precisar do projeto kotlin_cpu_gpu/android_starter. Se você tiver dificuldades, revise o projeto concluído: kotlin_cpu_gpu/android

Observação sobre caminhos de arquivos

Este tutorial especifica caminhos de arquivo no formato Linux/macOS. Se você estiver no Windows, ajuste os caminhos de acordo.

Também é importante observar a distinção entre a visualização de projetos do Android Studio e uma visualização padrão do sistema de arquivos. A visualização de projetos do Android Studio é uma representação estruturada dos arquivos do projeto, organizada para o desenvolvimento do Android. Os caminhos de arquivo neste tutorial se referem aos caminhos do sistema de arquivos, não aos caminhos na visualização do projeto do Android Studio.

Importar o app inicial

Vamos começar importando o app inicial para o Android Studio.

  1. Abra o Android Studio e selecione Open.

Android Studio Open

  1. Navegue até o diretório kotlin_cpu_gpu/android_starter e abra-o.

Android Starter

Para garantir que todas as dependências estejam disponíveis para o app, sincronize o projeto com os arquivos do Gradle quando o processo de importação terminar.

  1. Selecione Sync Project with Gradle Files na barra de ferramentas do Android Studio.

Sincronização de menus

  1. Não pule esta etapa. Se ela não funcionar, o restante do tutorial não fará sentido.

Executar o app inicial

Agora que você importou o projeto para o Android Studio, já pode executar o app pela primeira vez.

Conecte o dispositivo Android via USB ao computador e clique em Executar na barra de ferramentas do Android Studio.

Botão "Executar"

O app vai ser iniciado no dispositivo. Você vai ver uma transmissão ao vivo da câmera, mas ainda não haverá segmentação. Todas as edições de arquivo que você fará neste tutorial estarão no diretório LiteRT/litert/samples/image_segmentation/kotlin_cpu_gpu/android_starter/app/src/main/java/com/google/aiedge/examples/image_segmentation. Agora você sabe por que o Android Studio reestrutura isso 😃.

Project Dir

Você também vai encontrar comentários TODO nos arquivos ImageSegmentationHelper.kt, MainViewModel.kt e view/SegmentationOverlay.kt. Nas etapas a seguir, você vai implementar a funcionalidade de segmentação de imagens preenchendo estes TODOs.

4. Entender o app inicial

O app inicial já tem uma interface básica e uma lógica de processamento de câmera. Confira uma visão geral rápida dos arquivos principais:

  • app/src/main/java/com/google/aiedge/examples/image_segmentation/MainActivity.kt: é o ponto de entrada principal do aplicativo. Ele configura a interface usando o Jetpack Compose e processa as permissões da câmera.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/MainViewModel.kt: esse ViewModel gerencia o estado da interface e organiza o processo de segmentação de imagens.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/ImageSegmentationHelper.kt: é aqui que vamos adicionar a lógica principal para segmentação de imagens. Ele vai lidar com o carregamento do modelo, o processamento dos frames da câmera e a execução da inferência.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/view/CameraScreen.kt: essa função combinável mostra a prévia da câmera e a sobreposição de segmentação.
  • app/src/main/assets/selfie_multiclass.tflite: é o modelo de segmentação de imagens pré-treinado do TensorFlow Lite que vamos usar.

5. Como entender o LiteRT e adicionar dependências

Agora, vamos adicionar a funcionalidade de segmentação de imagens ao app inicial.

1. Adicionar a dependência do LiteRT

Primeiro, adicione a biblioteca LiteRT ao seu projeto. Essa é a primeira etapa crucial para ativar o aprendizado de máquina no dispositivo com o tempo de execução otimizado do Google.

Abra o arquivo app/build.gradle.kts e adicione a seguinte linha ao bloco dependencies:

// LiteRT for on-device ML
implementation(libs.litert)

Depois de adicionar a dependência, sincronize seu projeto com os arquivos do Gradle clicando no botão Sincronizar agora, que aparece no canto superior direito do Android Studio.

Sincronizar agora

2. Entender as APIs principais do LiteRT

Abrir ImageSegmentationHelper.kt

Antes de escrever o código de implementação, é importante entender os principais componentes da API LiteRT que você vai usar. Verifique se você está importando do pacote com.google.ai.edge.litert e adicione as seguintes importações à parte de cima de ImageSegmentationHelper.kt:

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
  • CompiledModel: essa é a classe central para interagir com seu modelo do TFLite. Ele representa um modelo que foi pré-compilado e otimizado para um acelerador de hardware específico (como a CPU ou a GPU). Essa pré-compilação é um recurso fundamental do LiteRT que leva a uma inferência mais rápida e eficiente.
  • CompiledModel.Options: use essa classe builder para configurar o CompiledModel. A configuração mais importante é especificar o acelerador de hardware que você quer usar para executar o modelo.
  • Accelerator: esse tipo enumerado permite escolher o hardware para inferência. O projeto inicial já está configurado para processar estas opções:
    • Accelerator.CPU: para executar o modelo na CPU do dispositivo. Essa é a opção mais compatível com todos os dispositivos.
    • Accelerator.GPU: para executar o modelo na GPU do dispositivo. Isso geralmente é muito mais rápido do que a CPU para modelos baseados em imagens.
  • Buffers de entrada e saída (TensorBuffer): o LiteRT usa TensorBuffer para entradas e saídas de modelos. Isso oferece controle refinado sobre a memória e evita cópias desnecessárias de dados. Você vai receber esses buffers diretamente da sua instância CompiledModel usando model.createInputBuffers() e model.createOutputBuffers(). Depois, vai gravar os dados de entrada neles e ler os resultados.
  • model.run(): essa é a função que executa a inferência. Você transmite os buffers de entrada e saída para ele, e o LiteRT processa a tarefa complexa de executar o modelo no acelerador de hardware selecionado.

6. Concluir a implementação inicial do ImageSegmentationHelper

Agora é hora de escrever um código. Você vai concluir a implementação inicial de ImageSegmentationHelper.kt. Isso envolve configurar a classe particular Segmenter para armazenar o modelo LiteRT e implementar a função cleanup() para liberar corretamente.

  1. Conclua a classe Segmenter e a função cleanup(): no arquivo ImageSegmentationHelper.kt, você encontra um esqueleto para uma classe particular chamada Segmenter e uma função chamada cleanup(). Primeiro, conclua a classe Segmenter definindo o construtor dela para manter o modelo, criando propriedades para os buffers de entrada/saída e adicionando um método close() para liberar o modelo. Em seguida, implemente a função cleanup() para chamar esse novo método close().Substitua a classe Segmenter e a função cleanup() atuais pelo seguinte: (~linha 83)
    private class Segmenter(
        // Add this argument
        private val model: CompiledModel,
        private val coloredLabels: List<ColoredLabel>,
    ) {
        // Add these private vals
        private val inputBuffers: = model.createInputBuffers()
        private val outputBuffers: = model.createOutputBuffers()
    
        fun cleanup() {
          // cleanup buffers
          inputBuffers.forEach { it.close() }
          outputBuffers.forEach { it.close() }
          // cleanup model
          model.close()
        }
    }
    
  2. Defina o método toAccelerator: esse método mapeia os enums de acelerador definidos no menu de aceleradores para os enums de acelerador específicos dos módulos LiteRT importados (~linha 225):
    fun toAccelerator(acceleratorEnum: AcceleratorEnum): Accelerator {
      return when (acceleratorEnum) {
        AcceleratorEnum.CPU -> Accelerator.CPU
        AcceleratorEnum.GPU -> Accelerator.GPU
      }
    }
    
  3. Inicialize o CompiledModel: agora encontre a função initSegmenter. É aqui que você vai criar a instância CompiledModel e usá-la para instanciar a classe Segmenter agora definida. Esse código configura o modelo com o acelerador especificado (CPU ou GPU) e o prepara para inferência. Substitua o TODO em initSegmenter pela seguinte implementação (Cmd/Ctrl+f "initSegmenter" ou ~linha 62):
    cleanup()
    try {
      withContext(singleThreadDispatcher) {
        val model =
          CompiledModel.create(
            context.assets,
            "selfie_multiclass.tflite",
            CompiledModel.Options(toAccelerator(acceleratorEnum)),
            null,
          )
        segmenter = Segmenter(model, coloredLabels)
        Log.d(TAG, "Created an image segmenter")
      }
    } catch (e: Exception) {
      Log.i(TAG, "Create LiteRT from selfie_multiclass is failed: ${e.message}")
      _error.emit(e)
    }
    

7. Iniciar segmentação e pré-processamento

Agora que temos um modelo, precisamos acionar o processo de segmentação e preparar os dados de entrada para ele.

Segmentação por gatilho

O processo de segmentação começa em MainViewModel.kt, que recebe frames da câmera.

Abrir MainViewModel.kt

  1. Acionar segmentação de frames da câmera: as funções segment em MainViewModel são o ponto de entrada para nossa tarefa de segmentação. Eles são chamados sempre que uma nova imagem fica disponível na câmera ou é selecionada na galeria. Essas funções chamam o método segment no nosso ImageSegmentationHelper. Substitua os TODOs nas duas funções segment pelo seguinte (linha ~107):
    // For ImageProxy (from CameraX)
    fun segment(imageProxy: ImageProxy) {
        segmentJob =
            viewModelScope.launch {
                imageSegmentationHelper.segment(imageProxy.toBitmap(), imageProxy.imageInfo.rotationDegrees)
                imageProxy.close()
            }
    }
    
    // For Bitmaps (from gallery)
    fun segment(bitmap: Bitmap, rotationDegrees: Int) {
        segmentJob =
            viewModelScope.launch {
                val argbBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
                imageSegmentationHelper.segment(argbBitmap, rotationDegrees)
            }
    }
    

Pré-processar a imagem

Agora vamos voltar para ImageSegmentationHelper.kt para processar a imagem.

Abrir ImageSegmentationHelper.kt

  1. Implemente a função pública segment: essa função serve como um wrapper que chama a função particular segment na classe Segmenter. Substitua o TODO por (~linha 95):
    try {
      withContext(singleThreadDispatcher) {
        segmenter?.segment(bitmap, rotationDegrees)?.let { if (isActive) _segmentation.emit(it) }
      }
    } catch (e: Exception) {
      Log.i(TAG, "Image segment error occurred: ${e.message}")
      _error.emit(e)
    }
    
  2. Implementar o pré-processamento: a função privada segment na classe Segmenter é onde vamos realizar as transformações necessárias na imagem de entrada para prepará-la para o modelo. Isso inclui escalonamento, rotação e normalização da imagem. Essa função vai chamar outra função segment particular para realizar a inferência. Substitua o TODO na função segment(bitmap: Bitmap, ...) por (~linha 121):
    val totalStartTime = SystemClock.uptimeMillis()
    val rotation = -rotationDegrees / 90
    val (h, w) = Pair(256, 256)
    
    // Preprocessing
    val preprocessStartTime = SystemClock.uptimeMillis()
    var image = bitmap.scale(w, h, true)
    image = rot90Clockwise(image, rotation)
    val inputFloatArray = normalize(image, 127.5f, 127.5f)
    Log.d(TAG, "Preprocessing time: ${SystemClock.uptimeMillis() - preprocessStartTime} ms")
    
    // Inference
    val inferenceStartTime = SystemClock.uptimeMillis()
    val segmentResult = segment(inputFloatArray)
    Log.d(TAG, "Inference time: ${SystemClock.uptimeMillis() - inferenceStartTime} ms")
    
    Log.d(TAG, "Total segmentation time: ${SystemClock.uptimeMillis() - totalStartTime} ms")
    return SegmentationResult(segmentResult, SystemClock.uptimeMillis() - inferenceStartTime)
    

8. Inferência principal com LiteRT

Com os dados de entrada pré-processados, podemos executar a inferência principal usando o LiteRT.

Abrir ImageSegmentationHelper.kt

  1. Implementar a execução do modelo: a função privada segment(inputFloatArray: FloatArray) é onde interagimos diretamente com o método run() do LiteRT. Gravamos os dados pré-processados no buffer de entrada, executamos o modelo e lemos os resultados do buffer de saída. Substitua o TODO nesta função por (~linha 188):
    val (h, w, c) = Triple(256, 256, 6)
    
    // MODEL EXECUTION PHASE
    val modelExecStartTime = SystemClock.uptimeMillis()
    
    // Write input data - measure time
    val bufferWriteStartTime = SystemClock.uptimeMillis()
    inputBuffers[0].writeFloat(inputFloatArray)
    val bufferWriteTime = SystemClock.uptimeMillis() - bufferWriteStartTime
    Log.d(TAG, "Buffer write time: $bufferWriteTime ms")
    
    // Optional tensor inspection
    logTensorStats("Input tensor", inputFloatArray)
    
    // Run model inference - measure time
    val modelRunStartTime = SystemClock.uptimeMillis()
    model.run(inputBuffers, outputBuffers)
    val modelRunTime = SystemClock.uptimeMillis() - modelRunStartTime
    Log.d(TAG, "Model.run() time: $modelRunTime ms")
    
    // Read output data - measure time
    val bufferReadStartTime = SystemClock.uptimeMillis()
    val outputFloatArray = outputBuffers[0].readFloat()
    val outputBuffer = FloatBuffer.wrap(outputFloatArray)
    val bufferReadTime = SystemClock.uptimeMillis() - bufferReadStartTime
    Log.d(TAG, "Buffer read time: $bufferReadTime ms")
    
    val modelExecTime = SystemClock.uptimeMillis() - modelExecStartTime
    Log.d(TAG, "Total model execution time: $modelExecTime ms")
    
    // Optional tensor inspection
    logTensorStats("Output tensor", outputFloatArray)
    
    // POSTPROCESSING PHASE
    val postprocessStartTime = SystemClock.uptimeMillis()
    
    // Process mask from model output
    val inferenceData = InferenceData(width = w, height = h, channels = c, buffer = outputBuffer)
    val mask = processImage(inferenceData)
    
    val postprocessTime = SystemClock.uptimeMillis() - postprocessStartTime
    Log.d(TAG, "Postprocessing time (mask creation): $postprocessTime ms")
    
    return Segmentation(
      listOf(Mask(mask, inferenceData.width, inferenceData.height)),
      coloredLabels,
    )
    

9. Pós-processamento e exibição da sobreposição

Depois de executar a inferência, recebemos uma saída bruta do modelo. Precisamos processar essa saída para criar uma máscara de segmentação visual e mostrá-la na tela.

Abrir ImageSegmentationHelper.kt

  1. Implementar o processamento de saída: a função processImage converte a saída bruta de ponto flutuante do modelo em um ByteBuffer que representa a máscara de segmentação. Para isso, ele encontra a classe com a maior probabilidade para cada pixel. Substitua o TODO por (~linha 238):
    val mask = ByteBuffer.allocateDirect(inferenceData.width * inferenceData.height)
    for (i in 0 until inferenceData.height) {
        for (j in 0 until inferenceData.width) {
            val offset = inferenceData.channels * (i * inferenceData.width + j)
    
            var maxIndex = 0
            var maxValue = inferenceData.buffer.get(offset)
    
            for (index in 1 until inferenceData.channels) {
                if (inferenceData.buffer.get(offset + index) > maxValue) {
                    maxValue = inferenceData.buffer.get(offset + index)
                    maxIndex = index
                }
            }
            mask.put(i * inferenceData.width + j, maxIndex.toByte())
        }
    }
    return mask
    

Abrir MainViewModel.kt

  1. Coletar e processar resultados de segmentação: agora voltamos ao MainViewModel para processar os resultados de segmentação do ImageSegmentationHelper. O segmentationUiShareFlow coleta o SegmentationResult, converte a máscara em um Bitmap colorido e o fornece à interface. Substitua o TODO na propriedade segmentationUiShareFlow por (~linha 63). Não substitua o código que já está lá, apenas preencha o corpo:
    viewModelScope.launch {
      imageSegmentationHelper.segmentation
        .filter { it.segmentation.masks.isNotEmpty() }
        .map {
          val segmentation = it.segmentation
          val mask = segmentation.masks[0]
          val maskArray = mask.data
          val width = mask.width
          val height = mask.height
          val pixelSize = width * height
          val pixels = IntArray(pixelSize)
    
          val colorLabels =
            segmentation.coloredLabels.mapIndexed { index, coloredLabel ->
              ColorLabel(index, coloredLabel.label, coloredLabel.argb)
            }
          // Set color for pixels
          for (i in 0 until pixelSize) {
            val colorLabel = colorLabels[maskArray[i].toInt()]
            val color = colorLabel.getColor()
            pixels[i] = color
          }
          // Get image info
          val overlayInfo = OverlayInfo(pixels = pixels, width = width, height = height)
    
          val inferenceTime = it.inferenceTime
          Pair(overlayInfo, inferenceTime)
        }
        .collect { flow.emit(it) }
    }
    

Abrir view/SegmentationOverlay.kt

A última etapa é orientar corretamente a sobreposição de segmentação quando o usuário muda para a câmera frontal. O feed da câmera frontal é espelhado naturalmente. Por isso, precisamos aplicar a mesma inversão horizontal à nossa sobreposição Bitmap para garantir que ela se alinhe corretamente à visualização da câmera.

  1. Processar orientação da sobreposição: encontre o TODO no arquivo SegmentationOverlay.kt e substitua pelo código a seguir. Esse código verifica se a câmera frontal está ativa e, em caso afirmativo, aplica uma inversão horizontal à sobreposição Bitmap antes de ela ser desenhada na Canvas. (~linha 42):
    val orientedBitmap =
      if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
        // Create a matrix for horizontal flipping
        val matrix = Matrix().apply { preScale(-1f, 1f) }
        Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, false).also {
          image.recycle()
        }
      } else {
        image
      }
    

10. Executar e usar o app final

Você concluiu todas as mudanças necessárias no código. É hora de executar o app e ver seu trabalho em ação.

  1. Executar o app: conecte seu dispositivo Android e clique em Executar na barra de ferramentas do Android Studio.

Botão &quot;Executar&quot;

  1. Teste os recursos: depois que o app for iniciado, você vai ver a transmissão da câmera ao vivo com uma sobreposição de segmentação colorida.
    • Trocar de câmera: toque no ícone de troca de câmera na parte de cima para alternar entre as câmeras frontal e traseira. Observe como a sobreposição se orienta corretamente.
    • Mudar acelerador: toque no botão "CPU" ou "GPU" na parte de baixo para mudar o acelerador de hardware. Observe a mudança no Tempo de inferência exibido na parte de baixo da tela. A GPU deve ser significativamente mais rápida.
    • Usar uma imagem da galeria: toque na guia "Galeria" na parte de cima para selecionar uma imagem da galeria de fotos do seu dispositivo. O app vai executar a segmentação na imagem estática selecionada.

Outra interface

Agora você tem um app de segmentação de imagens em tempo real totalmente funcional com tecnologia LiteRT.

11. Avançado (opcional): usar a NPU

Esse repositório também contém uma versão do app otimizada para unidades de processamento neural (NPUs, na sigla em inglês). A versão da NPU pode aumentar significativamente o desempenho em dispositivos com uma NPU compatível.

Para testar a versão da NPU, abra o projeto kotlin_npu/android no Android Studio. O código é muito semelhante à versão de CPU/GPU e está configurado para usar o delegado de NPU.

Para usar o delegado de NPU, inscreva-se no Programa de acesso antecipado.

12. Parabéns!

Você criou um app Android que realiza segmentação de imagens em tempo real usando o LiteRT. Você aprendeu a:

  • Integre o tempo de execução do LiteRT a um app Android.
  • Carregue e execute um modelo de segmentação de imagens do TFLite.
  • Pré-processe a entrada do modelo.
  • Processe a saída do modelo para criar uma máscara de segmentação.
  • Use o CameraX para um app de câmera em tempo real.

Próximas etapas

  • Tente usar outro modelo de segmentação de imagem.
  • Teste diferentes delegados do LiteRT (CPU, GPU, NPU).

Saiba mais