Implementing OCR in Android apps with Google ML Kit
Optical Character Recognition (OCR) enables your Android app to detect and extract text from images. Google ML Kit provides an on-device OCR library, so the example below can recognize text without sending each image to a remote API. This guide adds Latin-script text recognition, captures or selects an image, and displays the recognized text.
Prerequisites
Before starting, ensure you have:
- A current stable Android Studio and its recommended Android Gradle plugin
- An Android device or emulator running Android API level 23 or higher
- Basic knowledge of Android development and Kotlin
Setting up the Android project
Create a new Android project in Android Studio:
- Open Android Studio and select File > New > New Project.
- Choose Empty Activity and click Next.
- Name the project, for example
TextRecognitionApp. - Select Kotlin as the language.
- Set the Minimum SDK to API 23 or newer, as required by the current Text Recognition v2 API.
- Click Finish to create the project.
Adding the ML Kit dependency
Enable view binding and add the bundled Latin-script model to your app-level build.gradle file:
android {
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'com.google.mlkit:text-recognition:16.0.1'
}
This dependency includes the recognition model in your app, so it is available immediately. If
download size matters more than first-run availability, Google also offers the smaller
com.google.android.gms:play-services-mlkit-text-recognition:19.0.1 dependency. That version
downloads its model through Google Play services. See Google's
bundled and unbundled model comparison
before choosing.
Configuring permissions
The implementation below uses Android's system camera app and Photo Picker. It does not need
READ_EXTERNAL_STORAGE, broad photo-library access, or direct camera access. The Photo Picker
grants access only to the selected item and falls back to ACTION_OPEN_DOCUMENT on older supported
devices. If you later replace the camera intent with CameraX for an in-app preview, request the
CAMERA permission at runtime.
Creating the layout
Create activity_main.xml with controls for capturing and selecting images, an image preview, and
a scrollable text result:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/btnCapture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Capture Image" />
<Button
android:id="@+id/btnGallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Select from Gallery" />
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="16dp"
android:contentDescription="Selected image"
android:scaleType="centerCrop" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_weight="1">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp" />
</ScrollView>
</LinearLayout>
Implementing OCR functionality
Use TakePicture to save a full-resolution image to a content URI, and use PickVisualMedia to
give the user the system Photo Picker. InputImage.fromFilePath() creates the ML Kit input directly
from that URI.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var photoUri: Uri
private lateinit var recognizer: TextRecognizer
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success) {
processImage(photoUri)
}
}
private val selectPictureLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri != null) {
processImage(uri)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
binding.btnCapture.setOnClickListener {
captureImage()
}
binding.btnGallery.setOnClickListener {
selectPictureLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
private fun captureImage() {
val imageFile = File.createTempFile("IMG_", ".jpg", cacheDir)
photoUri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", imageFile)
takePictureLauncher.launch(photoUri)
}
private fun processImage(uri: Uri) {
binding.imageView.setImageURI(uri)
val image = try {
InputImage.fromFilePath(this, uri)
} catch (error: IOException) {
showMessage("The selected image could not be opened")
return
}
recognizer.process(image)
.addOnSuccessListener { visionText ->
binding.textView.text = visionText.text
}
.addOnFailureListener {
showMessage("Text recognition failed. Try a clearer image")
}
}
private fun showMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
override fun onDestroy() {
super.onDestroy()
recognizer.close()
}
}
Add the FileProvider configuration to AndroidManifest.xml within the <application> tag:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
Create res/xml/file_paths.xml:
<paths>
<cache-path name="cache" path="." />
</paths>
Optimizing OCR accuracy and performance
Follow Google's input image guidelines:
- Character size: Aim for at least 16x16 pixels per character. Increasing characters beyond roughly 24x24 pixels generally does not improve recognition accuracy.
- Image quality: Use good lighting, sharp focus, and minimal motion blur.
- Image dimensions: Use the lowest resolution that still gives every character enough pixels. Smaller images reduce latency when scanning in real time.
- Real-time analysis: With CameraX, keep the default
ImageAnalysis.STRATEGY_KEEP_ONLY_LATESTbackpressure strategy so frames do not queue while the recognizer is busy.
Testing the application
Test your OCR implementation with:
- Printed text in different fonts and sizes
- Latin-script languages supported by
TextRecognizerOptions.DEFAULT_OPTIONS - Varying lighting, focus, and text orientation
- Images from both the camera and Photo Picker
- Device rotations and process recreation
Chinese, Devanagari, Japanese, and Korean each require their own ML Kit dependency and recognizer options. Add the corresponding library before including one of those scripts in your test matrix.
Troubleshooting
If text recognition fails:
- Verify that the image is clear, well lit, and gives each character enough pixels.
- Confirm that FileProvider is configured and the temporary image file is accessible.
- If you chose the Google Play services dependency, confirm that its model has finished downloading. The bundled dependency used in this example does not need a model download.
- Use Logcat to inspect the underlying error during development.
Conclusion
You now have an Android OCR flow that accepts camera and Photo Picker images and recognizes Latin-script text on-device. For server-side OCR across uploaded files, see Transloadit's /document/ocr Robot and OCR demo.
