Implementación de OCR en apps Android con Google ML Kit
El reconocimiento óptico de caracteres (OCR) permite que tu app Android detecte y extraiga texto de imágenes. Google ML Kit ofrece una biblioteca de OCR que se ejecuta en el dispositivo, por lo que el ejemplo siguiente puede reconocer texto sin enviar cada imagen a una API remota. Esta guía incorpora el reconocimiento de texto en alfabeto latino, la captura o selección de una imagen y la visualización del texto reconocido.
Requisitos previos
Antes de comenzar, asegúrate de contar con:
- Una versión estable actual de Android Studio y su complemento de Android para Gradle recomendado
- Un dispositivo o emulador Android con el nivel de API de Android 23 o superior
- Conocimientos básicos de desarrollo para Android y Kotlin
Configura el proyecto de Android
Crea un nuevo proyecto de Android en Android Studio:
- Abre Android Studio y selecciona File, New y luego New Project.
- Elige Empty Views Activity y haz clic en Next.
- Asigna un nombre al proyecto, por ejemplo,
TextRecognitionApp. - Selecciona
Kotlincomo lenguaje yGroovy DSLcomo lenguaje de configuración de compilación. - Establece Minimum SDK en
API 23o una versión posterior, como exige la API de Text Recognition v2. - Haz clic en Finish para crear el proyecto.
Añade la dependencia de ML Kit
Habilita la vinculación de vistas y añade el modelo integrado para el alfabeto latino al archivo
build.gradle del módulo de tu app:
android {
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.activity:activity-ktx:1.9.3'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.mlkit:text-recognition:16.0.1'
}
Esta dependencia incluye el modelo de reconocimiento en tu app, por lo que está disponible de
inmediato. Si el tamaño de descarga importa más que la disponibilidad en la primera ejecución,
Google también ofrece la dependencia com.google.android.gms:play-services-mlkit-text-recognition:19.0.1, de menor tamaño. Esa versión
descarga su modelo a través de los servicios de Google Play. Consulta la
comparación de Google entre modelos integrados y no integrados
antes de elegir.
Configura los permisos
La implementación siguiente usa la app de cámara del sistema Android y el selector de fotos.
No necesita READ_EXTERNAL_STORAGE, acceso amplio a la biblioteca de fotos ni acceso directo
a la cámara. El selector de fotos concede acceso solo al elemento seleccionado y recurre a
ACTION_OPEN_DOCUMENT en dispositivos compatibles más antiguos. Si más adelante reemplazas
el intent de cámara por CameraX para obtener una vista previa dentro de la app, solicita el permiso
CAMERA en tiempo de ejecución.
Crea el diseño de la interfaz
Crea activity_main.xml con controles para capturar y seleccionar imágenes, una vista
previa de la imagen y un resultado de texto con desplazamiento:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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>
Implementa la funcionalidad de OCR
Usa TakePicture para guardar una imagen a resolución completa en un URI de contenido
y usa PickVisualMedia para ofrecer al usuario el selector de fotos del sistema.
InputImage.fromFilePath() crea la entrada de ML Kit directamente a partir de ese URI. Conserva
la declaración de paquete generada por Android Studio e importa ActivityMainBinding
del paquete databinding generado para tu app. Reemplaza el resto de
MainActivity.kt con el siguiente código. Este guarda el nombre del archivo de captura
pendiente por separado del registro del resultado de la actividad, para que el retorno desde la
cámara también funcione después de la recreación del proceso.
import android.content.ActivityNotFoundException
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
import java.io.File
import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var pendingCameraName: String? = null
private val cameraDirectory: File
get() = File(cacheDir, "ocr-camera")
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
val name = pendingCameraName
pendingCameraName = null
if (name == null) {
setBusy(false)
showMessage("The camera result is no longer available")
} else {
val file = File(cameraDirectory, name)
if (success && file.isFile && file.length() > 0) {
processImage(cameraUri(file), file)
} else {
file.delete()
setBusy(false)
}
}
}
private val selectPictureLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri != null) {
processImage(uri)
} else {
setBusy(false)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
pendingCameraName = savedInstanceState?.getString("pendingCameraName")
binding.textView.text = savedInstanceState?.getString("recognizedText")
setBusy(pendingCameraName != null)
// A process killed during recognition may leave a file without a result callback.
cameraDirectory.listFiles()?.filter {
it.name != pendingCameraName && it.lastModified() < System.currentTimeMillis() - 86_400_000
}?.forEach { it.delete() }
binding.btnCapture.setOnClickListener {
captureImage()
}
binding.btnGallery.setOnClickListener {
setBusy(true)
selectPictureLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
private fun captureImage() {
try {
if (!cameraDirectory.isDirectory && !cameraDirectory.mkdirs()) {
throw IOException("Could not create capture directory")
}
val imageFile = File.createTempFile("IMG_", ".jpg", cameraDirectory)
pendingCameraName = imageFile.name
setBusy(true)
takePictureLauncher.launch(cameraUri(imageFile))
} catch (error: IOException) {
cancelCapture()
} catch (error: ActivityNotFoundException) {
cancelCapture()
} catch (error: SecurityException) {
cancelCapture()
}
}
private fun cameraUri(file: File): Uri =
FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
private fun cancelCapture() {
pendingCameraName?.let { File(cameraDirectory, it).delete() }
pendingCameraName = null
setBusy(false)
showMessage("The camera could not be opened")
}
private fun processImage(uri: Uri, temporaryFile: File? = null) {
setBusy(true)
val image = try {
binding.imageView.setImageURI(uri)
InputImage.fromFilePath(this, uri)
} catch (error: IOException) {
temporaryFile?.delete()
setBusy(false)
showMessage("The selected image could not be opened")
return
} catch (error: SecurityException) {
temporaryFile?.delete()
setBusy(false)
showMessage("The selected image could not be opened")
return
}
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
recognizer.process(image)
.addOnSuccessListener { visionText ->
if (!isDestroyed) binding.textView.text = visionText.text
}
.addOnFailureListener {
if (!isDestroyed) showMessage("Text recognition failed. Try a clearer image")
}
.addOnCompleteListener {
// Do not use an Activity-scoped listener: cleanup must also run after onStop.
recognizer.close()
temporaryFile?.delete()
if (!isDestroyed) setBusy(false)
}
}
private fun setBusy(value: Boolean) {
binding.btnCapture.isEnabled = !value
binding.btnGallery.isEnabled = !value
}
private fun showMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("pendingCameraName", pendingCameraName)
outState.putString("recognizedText", binding.textView.text.toString())
super.onSaveInstanceState(outState)
}
}
Añade la configuración de FileProvider a AndroidManifest.xml dentro de la etiqueta
<application>:
<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>
Crea res/xml/file_paths.xml:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="ocr_camera" path="ocr-camera/" />
</paths>
Solo se comparte el directorio de capturas. Las capturas completadas y canceladas se eliminan;
una captura abandonada se elimina en un inicio posterior, una vez transcurrido un día. No elimines
el archivo pendiente en onDestroy(): la app de cámara podría seguir escribiendo
en él mientras Android recrea tu actividad. Las tareas de reconocimiento cierran su propio
reconocedor al finalizar, incluso si la actividad se ha detenido. El texto obtenido se conserva
tras la recreación; la vista previa es temporal. Si la recreación interrumpe el reconocimiento,
selecciona o captura la imagen de nuevo. El URI del selector de fotos se usa solo para la operación
inmediata, sin solicitar acceso permanente a la foto seleccionada.
Optimiza la precisión y el rendimiento del OCR
Sigue las directrices de Google para las imágenes de entrada:
- Tamaño de los caracteres: Procura que cada carácter tenga al menos 16x16 píxeles. Aumentar el tamaño de los caracteres más allá de unos 24x24 píxeles generalmente no mejora la precisión del reconocimiento.
- Calidad de la imagen: Usa buena iluminación, un enfoque nítido y el mínimo desenfoque por movimiento.
- Dimensiones de la imagen: Usa la resolución más baja que aún proporcione suficientes píxeles a cada carácter. Las imágenes más pequeñas reducen la latencia al escanear en tiempo real.
- Análisis en tiempo real: Con CameraX, conserva la estrategia de contrapresión predeterminada
ImageAnalysis.STRATEGY_KEEP_ONLY_LATESTpara que los fotogramas no se acumulen en una cola mientras el reconocedor esté ocupado.
Prueba la aplicación
Prueba tu implementación de OCR con:
- Texto impreso en diferentes fuentes y tamaños
- Idiomas que usan el alfabeto latino compatibles con
TextRecognizerOptions.DEFAULT_OPTIONS - Diferentes condiciones de iluminación, enfoque y orientación del texto
- Imágenes tanto de la cámara como del selector de fotos
- Rotaciones del dispositivo y recreación del proceso
Los sistemas de escritura chino, devanagari, japonés y coreano requieren, cada uno, su propia dependencia de ML Kit y sus propias opciones del reconocedor. Añade la biblioteca correspondiente antes de incluir uno de esos sistemas de escritura en tu matriz de pruebas.
Soluciona problemas
Si falla el reconocimiento de texto:
- Verifica que la imagen sea nítida, esté bien iluminada y proporcione suficientes píxeles a cada carácter.
- Confirma que FileProvider esté configurado y que el archivo de imagen temporal sea accesible.
- Si elegiste la dependencia de los servicios de Google Play, confirma que su modelo haya terminado de descargarse. La dependencia integrada usada en este ejemplo no necesita descargar un modelo.
- Usa Logcat para inspeccionar el error subyacente durante el desarrollo.
Conclusión
Ahora tienes un flujo de OCR para Android que acepta imágenes de la cámara y del selector de fotos, y reconoce texto en alfabeto latino en el dispositivo. Para aplicar OCR del lado del servidor a los archivos subidos, consulta el Robot /document/ocr y la demostración de OCR (English) de Transloadit.
