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:
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.
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.
- Abra o Android Studio e selecione Open.
- Navegue até o diretório
kotlin_cpu_gpu/android_starter
e abra-o.
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.
- Selecione Sync Project with Gradle Files na barra de ferramentas do Android Studio.
- 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.
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 😃.
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 TODO
s.
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.
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 oCompiledModel
. 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 usaTensorBuffer
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ânciaCompiledModel
usandomodel.createInputBuffers()
emodel.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.
- Conclua a classe
Segmenter
e a funçãocleanup()
: no arquivoImageSegmentationHelper.kt
, você encontra um esqueleto para uma classe particular chamadaSegmenter
e uma função chamadacleanup()
. Primeiro, conclua a classeSegmenter
definindo o construtor dela para manter o modelo, criando propriedades para os buffers de entrada/saída e adicionando um métodoclose()
para liberar o modelo. Em seguida, implemente a funçãocleanup()
para chamar esse novo métodoclose()
.Substitua a classeSegmenter
e a funçãocleanup()
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() } }
- 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 } }
- Inicialize o
CompiledModel
: agora encontre a funçãoinitSegmenter
. É aqui que você vai criar a instânciaCompiledModel
e usá-la para instanciar a classeSegmenter
agora definida. Esse código configura o modelo com o acelerador especificado (CPU ou GPU) e o prepara para inferência. Substitua oTODO
eminitSegmenter
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
- Acionar segmentação de frames da câmera: as funções
segment
emMainViewModel
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étodosegment
no nossoImageSegmentationHelper
. Substitua osTODO
s nas duas funçõessegment
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
- Implemente a função pública
segment
: essa função serve como um wrapper que chama a função particularsegment
na classeSegmenter
. Substitua oTODO
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) }
- Implementar o pré-processamento: a função privada
segment
na classeSegmenter
é 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çãosegment
particular para realizar a inferência. Substitua oTODO
na funçãosegment(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
- Implementar a execução do modelo: a função privada
segment(inputFloatArray: FloatArray)
é onde interagimos diretamente com o métodorun()
do LiteRT. Gravamos os dados pré-processados no buffer de entrada, executamos o modelo e lemos os resultados do buffer de saída. Substitua oTODO
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
- Implementar o processamento de saída: a função
processImage
converte a saída bruta de ponto flutuante do modelo em umByteBuffer
que representa a máscara de segmentação. Para isso, ele encontra a classe com a maior probabilidade para cada pixel. Substitua oTODO
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
- Coletar e processar resultados de segmentação: agora voltamos ao
MainViewModel
para processar os resultados de segmentação doImageSegmentationHelper
. OsegmentationUiShareFlow
coleta oSegmentationResult
, converte a máscara em umBitmap
colorido e o fornece à interface. Substitua oTODO
na propriedadesegmentationUiShareFlow
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.
- Processar orientação da sobreposição: encontre o
TODO
no arquivoSegmentationOverlay.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çãoBitmap
antes de ela ser desenhada naCanvas
. (~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.
- Executar o app: conecte seu dispositivo Android e clique em Executar na barra de ferramentas do Android Studio.
- 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.
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).