Text Recognize with ML Kit

Simple OCR with ML Kit

Photo by David Travis on Unsplash

Introduction

Pada acara Google I/O 18 ada salah satu teknologi di Machine Learning yang diperkenalkan oleh Google yaitu ML Kit. ML Kit bisa saya katakan ini merupakan Machine Learning sederhana yang ready to use oleh mobile developer tanpa perlu susah-susah setup config disana sini. Tapi, jikalau ML Kit belum bisa menangani semua case yang ada pada app sedang kita kembangkan maka, kita bisa gunakan TensorFlow Lite dimana, dengan TensorFlow Lite kita bisa masukkan model-model yang akan dipakai pada Machine Learning kita. Lalu, beberapa fitur yang bisa dilakukan oleh ML Kit adalah sebagai berikut.

  1. Image Labeling
    Image Labeling berfungsi untuk mengetahui didalam image atau foto tersebut ada apa saja misal, fotonya selfie, lalu lokasinya dimana, dan disekitar fotonya ada binatang apa saja.
  2. Text Recognition (OCR)
    Text Recoginition atau OCR (Optical Character Recognition) berfungsi untuk membaca teks atau karakter dari sebuah gambar.
  3. Face Detection
    Face Detection berfungsi untuk bisa membaca setiap bentuk wajah manusia mulai dari bentuk mata, hidung, mulut, dan lain-lain sebagainya.
  4. Barcode Scanning
    Barcode Scanning berfungsi untuk membaca barcode yang ada.
  5. Landmark Detection
    Landmark Detection berfungsi untuk membaca dan mengetahui info dari gedung-gedung atau bangunan-bangunan terkenal.
  6. Smart Reply
    Smart Reply pada saat penulisan artikel ini masih coming soon fiturnya. Jadi, saya belum tahu fiturnya seperti apa.

Sample Project

Create Project

Untuk sample projek kali ini kita akan buat contoh program yang bisa membaca teks dari gambar baik itu dari hasil foto maupun foto yang diupload.

Langkah pertama, buat projek baru di Android Studio dan beri nama seperti berikut.

Buat contoh projek di Android Studio

Setelah selesai buat projek baru tersebut kita lanjut atur file build.gradle(Module: app)-nya dan tambahkan dependency-dependency berikut.

/* ML Kit */
implementation 'com.google.firebase:firebase-core:16.0.4'
implementation 'com.google.firebase:firebase-ml-vision:18.0.1'

/* Camera View */
implementation 'com.otaliastudios:cameraview:1.6.0'
// ... tambahkan kode apply plugin ini berada di akhir baris
apply plugin: 'com.google.gms.google-services'

Jadi, untuk menggunakan ML Kit kita harus menambahkan firebase-core dan firebase-ml-vision . Lalu, untuk pengambilan gambar dari kamera kita pakai library yang ready to use saja dimana, kita pakai CameraView milik otaliastudios.

Berikutnya, ubah juga file build.gradle(Project) dan tambahkan dependency classpath 'com.google.gms:google-services:4.0.1'

Mungkin, setelah selesai sync kita akan mendapatkan pesan error berikut ya atau jika tidak dapat pesan error ini pun juga no problem ya.

Pesan error selesai sync

Jadi, jika kita baca pesan error diatas maksudnya adalah di projek kita tidak ada file google-services.json yang mana jika kita ada pakai plugin google services maka harus ada file tersebut. Dan kebetulan kita belum buat ini jadi, don’t worry sama pesan error tersebut.

Create Project Firebase Console

Jadi, di langkah ini kita akan buat file google-services.json. Caranya adalah kita buka https://console.firebase.google.com lalu kita buat proyek baru di Firebase Console seperti berikut.

Buat proyek baru di firebase console

Setelah itu silakan tekan button lanjut dan tunggu hingga proyek kita selesai dibuat di Firebase Console.

Proses pembuatan proyek di firebase console
Proyek di firebase console berhasil dibuat

Selanjutnya kita atur proyek tersebut agar bisa dipakai di Android. Pertama, kita pilih Setelan proyek.

Pilih setelan proyek

Lalu, pilih Tambahkan Firebase ke aplikasi Android Anda seperti berikut.

Tambahkan proyek ke aplikasi Android

Selanjutnya, kita masukkan nama package dari aplikasi android yang sudah kita buat tadi di Android Studio. Apabila kita nggak tahu nama package dari aplikasi yang sudah kita buat di Android Studio caranya, bisa kita buka file AndroidManifest.xml dan lihat pada nilai attribut applicationId seperti gambar berikut.

Melihat nama package aplikasi di Android Studio

Lalu, jika kita sudah tahu nama package dari aplikasinya maka, langkah selanjutnya adalah masukkan nama package tersebut ke firebase console yang kita buat tadi.

Masukkan nama package ke firebase console untuk aplikasi android

Lalu, kita unduh file google-services.json dan letakkan pada projek kita di Android Studio pada direktori root modul aplikasi android.

Unduh file google-services.json
Pindahkan file google-services.json ke root modul di projek aplikasi Android Studio

Lalu, di tahap berikunya kita ada disuruh untuk menambahkan firebase SDK ke projek aplikasi di Android Studio namun, langkah ini sebenarnya sudah kita lakukan diawal jadi abaikan saja ya.

Tambahkan dependency firebase SDK ke projek di Android Studio

Lalu, akan muncul verifikasi instalasi seperti berikut dan pada tahap ini kita lewati saja langkahnya.

Verifikasi instalasi ke proyek di Android Studio. (Lewati langkah ini)

Atur file AndroidManifest.xml

Selanjutnya, kita tambahkan kode <meta-data> untuk mendeklarasikan ocr milik si ML Kit kedalam file AndroidManifest.xml seperti berikut.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.ysn.mlkitocr">

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<meta-data
android:name="com.google.firebase.ml.vision.DEPENDENCIES"
android:value="ocr" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

Buat layout activity_main.xml

Selanjutnya, kita buat layout dari file activity_main.xml seperti berikut.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/relative_layout_panel_overlay_camera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="fitXY"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/relative_layout_panel_overlay_result"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />

<RelativeLayout
android:id="@+id/relative_layout_panel_overlay_result"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_view">

<TextView
android:id="@+id/text_view_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:padding="16dp" />

</RelativeLayout>

<RelativeLayout
android:id="@+id/relative_layout_panel_overlay_camera"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/camera_view">

<Button
android:id="@+id/button_take_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="16dp"
android:text="Take Picture" />

</RelativeLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Buat menu options

Pada contoh projek ini kita akan membuat 2 mode yaitu mode kamera dan upload dari gallery. Jadi, kita buat menu-nya dengan nama file menu_activity_main.xml dimana, didalamnya ada 2 item yaitu, Camera dan Upload Photo.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_camera"
android:icon="@drawable/ic_camera_black_24dp"
android:orderInCategory="1"
android:title="@string/camera" />
<item
android:id="@+id/menu_item_upload_photo"
android:icon="@drawable/ic_add_a_photo_black_24dp"
android:orderInCategory="2"
android:title="@string/upload_photo" />
</menu>

Untuk icon-nya itu saya ambil dari aset di Android Studio jadi, silakan buat sendiri ya. 😉

It’s time to ML Kit

Di langkah ini kita buat agar app kita bisa membaca teks dari gambar yang sudah kita ambil dari kamera maupun upload dari gallery. Silakan buka file MainActivity.kt dan isi dengan source code berikut.

package com.ysn.mlkitocr

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import com.google.firebase.ml.vision.text.FirebaseVisionText
import com.otaliastudios.cameraview.Audio
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraUtils
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

private val TAG = javaClass.simpleName

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initCameraView()
initListeners()
val permissions = arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissions, 100)
} else {
ActivityCompat.requestPermissions(this, permissions, 100)
}
}

override fun onPause() {
if (camera_view.isStarted) {
camera_view.stop()
}
super.onPause()
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_activity_main, menu)
return super.onCreateOptionsMenu(menu)
}

override fun onOptionsItemSelected(item: MenuItem?): Boolean =
when (item?.itemId) {
R.id.menu_item_camera -> {
showCameraView()
true
}
R.id.menu_item_upload_photo -> {
showGalleryView()
true
}
else -> {
/* nothing to do in here */
super.onOptionsItemSelected(item)
}
}

private fun initListeners() {
camera_view.addCameraListener(object : CameraListener() {
override fun onPictureTaken(jpeg: ByteArray?) {
camera_view.stop()
CameraUtils.decodeBitmap(jpeg) { bitmap ->
image_view.scaleType = ImageView.ScaleType.FIT_XY
image_view.setImageBitmap(bitmap)
val image = FirebaseVisionImage.fromBitmap(bitmap)
val textRecognizer = FirebaseVision.getInstance()
.onDeviceTextRecognizer
textRecognizer.processImage(image)
.addOnSuccessListener {
camera_view.visibility = View.GONE
image_view.visibility = View.VISIBLE
relative_layout_panel_overlay_camera.visibility = View.GONE
relative_layout_panel_overlay_result.visibility = View.VISIBLE
processTextRecognitionResult(it)
}
.addOnFailureListener {
showToast(it.localizedMessage)
}
super.onPictureTaken(jpeg)
}
}
})
button_take_picture.setOnClickListener {
camera_view.captureSnapshot()
}
}

private fun initCameraView() {
camera_view.audio = Audio.OFF
camera_view.playSounds = false
camera_view.cropOutput = true
}

private fun showCameraView() {
camera_view.start()
camera_view.visibility = View.VISIBLE
image_view.visibility = View.GONE
relative_layout_panel_overlay_camera.visibility = View.VISIBLE
relative_layout_panel_overlay_result.visibility = View.GONE
}

private fun showGalleryView() {
if (camera_view.isStarted) {
camera_view.stop()
}
camera_view.visibility = View.GONE
image_view.visibility = View.GONE
relative_layout_panel_overlay_camera.visibility = View.GONE
relative_layout_panel_overlay_result.visibility = View.GONE
val intentGallery = Intent()
intentGallery.type = "image/*"
intentGallery.action = Intent.ACTION_GET_CONTENT
val intentChooser = Intent.createChooser(intentGallery, "Pick Picture")
startActivityForResult(intentChooser, 100)
}

private fun processTextRecognitionResult(firebaseVisionText: FirebaseVisionText) {
text_view_result.text = firebaseVisionText.text
}

private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT)
.show()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
100 -> {
val uriSelectedImage = data?.data
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
val cursor = contentResolver.query(uriSelectedImage!!, filePathColumn, null, null, null)
if (cursor == null || cursor.count < 1) {
return
}

cursor.moveToFirst()
val columnIndex = cursor.getColumnIndex(filePathColumn[0])
if (columnIndex < 0) {
showToast("Invalid image")
return
}

val picturePath = cursor.getString(columnIndex)
if (picturePath == null) {
showToast("Picture path not found")
return
}
cursor.close()
Log.d(TAG, "picturePath: $picturePath")
val bitmap = BitmapFactory.decodeFile(picturePath)
image_view.setImageBitmap(bitmap)

val image = FirebaseVisionImage.fromBitmap(bitmap)
val textRecognizer = FirebaseVision.getInstance()
.onDeviceTextRecognizer
textRecognizer.processImage(image)
.addOnSuccessListener {
camera_view.visibility = View.GONE
image_view.visibility = View.VISIBLE
relative_layout_panel_overlay_result.visibility = View.VISIBLE
relative_layout_panel_overlay_camera.visibility = View.GONE
image_view.scaleType = ImageView.ScaleType.CENTER_CROP
processTextRecognitionResult(it)
}
.addOnFailureListener {
showToast(it.localizedMessage)
}
}
else -> {
/* nothing to do in here */
}
}
}
}

}

Penjelasan:

  1. Didalam method onCreate kita ada minta runtime permission kamera dan baca eksternal storage. Lalu, didalamnya ada juga kita setup CameraView agar ketika ambil dari kamera hasilnya jadi terpotong (crop) sesuai dengan layout kita buat.
  2. Didalam method initListeners kita buat CameraListener ketika CameraView berhasil mengambil gambar dan didalamnya ada fungsi membaca teks dimana, kita masukkan gambar kita ke objek FirebaseVisionImage dengan sumber gambarnya dari bitmap lalu, kita deklarasikan objek FirebaseVision dengan metode onDeviceTextRecognizer . Jadi, kita set FirebaseVision agar melakukan pembacaan teksnya dengan source-nya berasal dari device. Selain dari device, kita juga bisa pakai metode cloudTextRecognizer namun, untuk menggunakan cloudTextRecognizer kita perlu melakukan pembayaran alias tidak gratis. Selanjutnya, kita proses pembacaan teksnya dengan syntax processImage dan tambahkan callback success dan failure .
  3. Didalam callback success kita ada panggil method processTextRecognitionResult dimana, isi dari method tersebut adalah untuk meng-set teks ke text_view_result dan didalam callback failure kita ada panggil method showToast yang mana isi dari method tersebut adalah untuk menampilkan Toast dengan pesan error-nya dari callback failure .

Done

Berikut adalah output dari program yang sudah kita buat.

Untuk projeknya sudah saya upload ke Github.