Reconhecimento de textos com MLKit e Android

Bruno Ferrari
Ignus Insights
Published in
9 min readAug 28, 2018

Muito se tem dito recentemente sobre Inteligência Artificial, Machine Learning ou aprendizado de máquina no bom e velho Português e outras sub áreas. De fato nos últimos anos fazer máquinas aprenderem, tomar decisões complexas e identificar padrões e comportamentos tem sido uma das principais áreas de avanço da tecnologia. Porém tudo isso é pesquisado e constantemente melhorado e não é de hoje. Muito antes do que a maioria imagina, em meados de 1951, Alan Turing publicou o artigo The Imitation Game, o qual foi o pontapé inicial para pesquisas mais aprofundadas sobre machine learning.

Nesse artigo venho trazer a vocês uma pequena parte do que pode ser executado hoje em dia utilizando as técnicas de machine learning no mundo mobile. Nesse primeiro texto quero trazer um tutorial sobre como fazer reconhecimento de textos utilizando uma biblioteca do Google chamada MLKit. Através do MLKit, é possível trazer todo o poder do machine learning para os apps, de forma offline utilizando o poder de processamento do dispositivo, ou de forma online, utilizando os servidores da Google para fazer o processamento, aumentando ainda mais as possibilidades.

De antemão quero dar alguns créditos sobre boa parte do código aqui, alguns trechos do código que iremos ver aqui são parte do codelab do Google sobre MLKit que por sinal ficou muito bom, adaptei algumas funcionalidades novas para que nosso exemplo ficasse um pouco mais rico, tentarei aqui também uma abordagem mais simples sobre a utilização desses recursos.

Vamos começar então, deixo logo abaixo o código do exemplo completo no GitHub em caso de alguém se perder.

É importante mencionar aqui que o MLKit foi desenvolvido para trabalhar junto ao Firebase, englobando principais serviços da Google como o Google Cloud Vision, Mobile Vision e Tensorflow Lite. Dito isso, precisamos configurar um projeto no Firebase para que possamos ter acesso a todos os serviços necessários.

Configuração do projeto no Firebase

Com um projeto novo já criado através do Android Studio, acesse o Firebase console e crie um novo projeto com o nome que preferir.

Tela de criação do projeto no Firebase

Feito isso vá em Add firebase to your Android app, preencha os campos com os dados necessários, lembre-se de substituir o package name pelo nome do projeto que criou anteriormente no Android Studio, no meu caso foi com.bferrari.mlkitdemo.

Tela de registro da aplicação no Firebase

Feito isso o Firebase irá baixar automaticamente um arquivo chamado google-services.json (já iremos trabalhar com ele), ele é basicamente um arquivo que contem as informações de acesso para linkar seu app com o projeto criado no Firebase.

Adicione as dependências do Firebase em seu projeto através do gradle, tal como no código abaixo.

dependencies {
// ...
implementation 'com.google.firebase:firebase-ml-vision:15.0.0'
}
apply plugin: 'com.google.gms.google-services'

Falta somente mais um item e poderemos entrar de cabeça no código, precisamos colocar aquele arquivo .json baixado automaticamente antes dentro do nosso código. Coloque-o dentro da pasta app do projeto criado no Android Studio, você pode utilizar a própria IDE ou mesmo seu gerenciador de arquivos.

Pronto! Agora sim estamos prontos pra fazer esse app.

Entrando no código

Como tentei tornar esse exemplo o mais simples possível para que não se alongue e acabe entrando em outros assuntos, mantive todo o código na MainActivity mesmo até para que fique claro que é algo simples de ser feito utilizando o MLKit.

Tal como no codelab do Google, deixei cinco imagens locais que podem ser selecionadas através de Spinner e implementei duas novas funcionalidades, um reconhecimento através da câmera e outro através da seleção de algo pelo galeria do dispositivo, o processamento do algorítimo nas imagens ocorre da mesma forma nas três maneiras.

Essas imagens podem ser adquiridas aqui, você também pode utilizar outras imagens.

Nosso app ficará da seguinte maneira após finalizado:

Nem tudo é perfeito. ¯\_(ツ)_/¯

Primeiramente vamos montar nosso layout que abrigará a imagem, o spinner, o botão de ação, dentre outros…

Perceba que temos alguns elementos estranhos aí, como por exemplo o Graphic Overlay, não vou entrar em muitos detalhes mas ele é um elemento de UI que fará o mapeamento em nossa imagem, somente para efeito de visualização, ele não interfere nem faz parte do MLKit, o código dessa classe e outras que esse projeto dependem podem ser encontradas no repositório mencionado anteriormente.

Vamos primeiramente criar algumas variáveis membro e constantes que serão essenciais no nosso código.

companion object {
const val REQUEST_IMAGE_CAPTURE = 1
val TAG = MainActivity::class.java.simpleName

const val REQUEST_FROM_LIBRARY = 99
}

private var selectedImage: Bitmap? = null
// Max width (portrait mode)
private var imageMaxWidth: Int? = null
// Max height (portrait mode)
private var imageMaxHeight: Int? = null

private var
completeText = ""

Em nossa Activity vamos criar a função que fará o processamento dos dados providos pelo MLKit e extrair os dados que queremos.

private fun processTextRecognitionResult(texts: FirebaseVisionText) {
val blocks = texts.blocks
if (blocks.size == 0) {
toast("No Text Found.")
return
}
graphicOverlay.clear()
for (i in blocks.indices) {
val lines = blocks[i].lines
for (j in lines.indices) {
val elements = lines[j].elements
for (k in elements.indices) {
val textGraphic = TextGraphic(graphicOverlay, elements[k])
completeText = completeText.plus(elements[k].text + " ")
graphicOverlay.add(textGraphic)
Log.d(TAG, k.toString())
}
}
}
}

Essa função dá muito a entender como o MLKit processa e captura esses dados do texto para nós. A partir de um objeto do tipo FirebaseVisionText conseguimos extrair índices em que cada qual contém linhas e que por sua vez contém elementos, por fim depois de correr por todas essas listas adicionamos cada elemento em nossa lista do GraphicOverlay (Lembre-se de dar uma conferida em como o graphic overlay funciona no repositório git).

Temos ainda uma variável membro recebendo o texto completo para ser exibido posteriormente.

Agora vamos criar uma função que fará o reconhecimento das imagens utilizando o MLKit e delegará todo o processamento dos dados para a função implementada anteriormente.

private fun runTextRecognition(selectedImage: Bitmap) {
val image = FirebaseVisionImage.fromBitmap(selectedImage)
val detector = FirebaseVision.getInstance().visionTextDetector

detector.detectInImage(image)
.addOnSuccessListener { texts ->
processTextRecognitionResult(texts)
toast(completeText)
}
.addOnFailureListener { e ->
e.printStackTrace()
}
}

Perceba que essas duas funções implementadas anteriormente já fazem todo o trabalho pesado de reconhecimento e processamento da imagem através do Firebase MLKit, vamos então colocar algumas imagens locais na aplicação para que possamos de fato começar a brincar com o reconhecimento de textos.

Adicionando imagens locais

Primeiramente vamos configurar o Spinner que já temos em nossa UI para que ele possa ser nosso seletor da imagem. Primeiramente crie uma pasta em src chamada assets, é lá que vamos armazenar essas imagens.

src/main/assets

E então criar uma nova função que irá preparar nosso Spinner:

private fun setupSpinner() {
val items = arrayOf("Ignus Hiring", "Ignus Logo", "Image 1", "Image 2", "Image 3")
val adapter = ArrayAdapter(this, android.R.layout
.simple_spinner_dropdown_item, items)
spinner.adapter = adapter
spinner.onItemSelectedListener = this
}

A fim de implementar o Spinner vamos fazer nossa MainActivity implementar a interface AdapterView.OnItemSelectedListener bem como suas funções obrigatórias.

class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {...override fun onNothingSelected(p0: AdapterView<*>?) {}

override fun onItemSelected(parent: AdapterView<*>, v: View, position: Int, id: Long) {
graphicOverlay.clear()

selectedImage = when(position) {
0 -> getBitmapFromAsset(this, "ignus_hiring.jpg")
1 -> getBitmapFromAsset(this, "ignus.jpg")
2 -> getBitmapFromAsset(this, "non-latin.jpg")
3 -> getBitmapFromAsset(this, "nl2.jpg")
4 -> getBitmapFromAsset(this, "Please_walk_on_the_grass.jpg")
else -> null
}

selectedImage?.let {
val targetedSize = getTargetedWidthHeight()

val targetWidth = targetedSize.first
val maxHeight = targetedSize.second

// Determine how much to scale down the image
val scaleFactor = Math.max(
it.width.toFloat() / targetWidth.toFloat(),
it.height.toFloat() / maxHeight.toFloat())

val resizedBitmap = Bitmap.createScaledBitmap(
it,
(it.width / scaleFactor).toInt(),
(it.height / scaleFactor).toInt(),
true)

imageRecon.setImageBitmap(resizedBitmap)
selectedImage = resizedBitmap
}
}
...

Existem aqui algumas funções que ajudarão no cálculo do tamanho da imagem e a capturar a imagem raw e transformá-la em Bitmap para que fique mais simples de trabalhar com ela.

private fun getBitmapFromAsset(context: Context, filePath: String): Bitmap? {
val assetManager = context.assets

var bitmap: Bitmap? = null
try {
val inputStream = assetManager.open(filePath)
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (e: IOException) {
e.printStackTrace()
}

return bitmap
}
private fun getTargetedWidthHeight(): Pair<Int, Int> {
val targetWidth: Int
val targetHeight: Int
val maxWidthForPortraitMode = getImageMaxWidth()!!
val maxHeightForPortraitMode = getImageMaxHeight()!!
targetWidth = maxWidthForPortraitMode
targetHeight = maxHeightForPortraitMode
return Pair(targetWidth, targetHeight)
}

private fun getImageMaxWidth(): Int? {
if (imageMaxWidth == null) {
// Calculate the max width in portrait mode. This is done lazily since we need to
// wait for
// a UI layout pass to get the right values. So delay it to first time image
// rendering time.
imageMaxWidth = imageRecon.width
}

return imageMaxWidth
}

// Returns max image height, always for portrait mode. Caller needs to swap width / height for
// landscape mode.
private fun getImageMaxHeight(): Int? {
if (imageMaxHeight == null) {
// Calculate the max width in portrait mode. This is done lazily since we need to
// wait for
// a UI layout pass to get the right values. So delay it to first time image
// rendering time.
imageMaxHeight = imageRecon.height
}

return imageMaxHeight
}

Pronto, com isso já podemos chamar nossa função de reconhecimento de textos passando a imagem selecionada como parâmetro para que a mesma seja identificada, vamos fazer isso no método onCreate mesmo já configurando o onClickListener do botão de processamento.

textRecognitionBtn.setOnClickListener {
completeText = ""
selectedImage?.let { runTextRecognition(it) }
}

E como fica isso tudo aí com a câmera ou biblioteca?

O processo é o mesmo, tal como já temos a imagem local no device podemos fazer da mesma forma capturando uma imagem com o app de camera e pegando os dados dessa imagem através do onActivityResult, posteriormente jogando esse Bitmap para nossa função que processa o texto. O mesmo processo é válido para recuperar imagens disponíveis na biblioteca do dispositivo.

Começo criando uma função para disparar a intent implícita que precisamos para chamar o app de camera ou da biblioteca

private fun dispatchCameraIntent() {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.resolveActivity(packageManager)?.let {
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE)
}
}
private fun dispatchPickFromLibraryIntent() {
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
}, REQUEST_FROM_LIBRARY)
}

Recuperamos a imagem no onActivityResult e jogamos para a variável selectedImage.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
data?.let {
selectedImage = it.extras.get("data") as Bitmap

Glide.with(this)
.load(selectedImage)
.apply {
RequestOptions().override(640,0)
}
.into(imageRecon)
}
} else if (requestCode == REQUEST_FROM_LIBRARY && resultCode == Activity.RESULT_OK) {
data?.let {
Glide.with(this)
.load(it.data)
.into(imageRecon)

graphicOverlay.clear()

selectedImage = MediaStore.Images.Media.getBitmap(this.contentResolver, it.data)
}
}
}

Usei o Glide para facilitar o processo aqui mas você pode facilmente executar o mesmo resultado utilizando o setImageBitmap.

Um ponto a se notar aqui é que estamos pegando apenas um preview em baixa resolução da imagem quando utilizando a camera para minimizar os passos desse artigo, dessa maneira e isso pode dificultar as coisas para o algorítimo, você pode aqui tentar ir além e extrair a Uri do arquivo raw.

Demonstração com uma imagem real retirada em landscape, o GraphicOverlay também pode falhar as vezes.

That’s all folks!

Funcionalidade de tradução em tempo real presente no app Google Translator.

Esses são pequenos passos que tornam muitas outras coisas possíveis, tal como no exemplo ao lado, onde o app do Google Translator utiliza-se de reconhecimento de texto para então traduzir e exibir uma imagem em tempo real traduzida na tela para o usuário. O machine learning é uma vasta área que está apenas engatinhando e tende a crescer muito nos próximos anos, com certeza todos nós estaremos utilizando algum algorítimo de machine learning em nosso dia a dia daqui algum tempo e é importante saber lidar com esses novos passos da tecnologia e criar novas possibilidades.

Por enquanto é só, deixem aí nos comentários sobre o que gostariam de ver por aqui, correções, comentários e críticas são extremamente bem vindos. Em breve abordarei mais temas sobre machine learning e suas possiblidades por aqui.

Um agradecimento especial para o Hussan Adi Hijazi, Marcílio Júnior e Bruno Bonini que me ajudaram com a revisão do texto.

--

--