在 Android 上使用机器学习套件识别图片中的文本

您可以使用机器学习套件识别图片或视频中的文字,例如路牌的文字。此功能的主要特点包括:

文本识别 API
说明识别图片或视频中的拉丁字母文本。
库名称com.google.android.gms:play-services-mlkit-text-recognition
实现系统会通过 Google Play 服务动态下载库。
应用大小影响260KB
初始化时间可能必须等到库下载后才能使用。
性能在大多数设备上都是实时的。

试试看

  • 试用示例应用,查看此 API 的示例用法。
  • 通过 Codelab 自行尝试编写代码。

准备工作

  1. 请务必在您的项目级 build.gradle 文件中的 buildscriptallprojects 部分添加 Google 的 Maven 代码库。
  2. 将 Android 版机器学习套件库的依赖项添加到您的模块的应用级 Gradle 文件(通常为 app/build.gradle):
    dependencies {
      // ...
    
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
    }
    
  3. 非强制但建议执行的操作:您可以将应用配置为在从 Play 商店安装您的应用后,自动将机器学习模型下载到设备。 为此,请将以下声明添加到应用的 AndroidManifest.xml 文件中:

    <application ...>
      ...
      <meta-data
          android:name="com.google.mlkit.vision.DEPENDENCIES"
          android:value="ocr" />
      <!-- To use multiple models: android:value="ocr,model2,model3" -->
    </application>
    
    如果您未启用在安装时下载模型的选项,模型将在您首次运行检测器时下载。您在下载完毕之前提出的请求不会产生任何结果。

1. 创建 TextRecognizer 实例

创建 TextRecognizer 实例:

Kotlin

val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)

Java

TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);

2. 准备输入图片

如需识别图片中的文本,请基于设备上的 Bitmapmedia.ImageByteBuffer、字节数组或文件创建 InputImage 对象。然后,将 InputImage 对象传递给 TextRecognizerprocessImage 方法。

您可以根据不同来源创建 InputImage 对象,下文逐一介绍了该对象。

使用 media.Image

如需基于 media.Image 对象创建 InputImage 对象(例如从设备的相机捕获图片时),请将 media.Image 对象和图片的旋转角度传递给 InputImage.fromMediaImage()

如果您使用 CameraX 库,OnImageCapturedListenerImageAnalysis.Analyzer 类会为您计算旋转角度值。

Kotlin

private class YourImageAnalyzer : ImageAnalysis.Analyzer {

    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            // Pass image to an ML Kit Vision API
            // ...
        }
    }
}

Java

private class YourAnalyzer implements ImageAnalysis.Analyzer {

    @Override
    public void analyze(ImageProxy imageProxy) {
        Image mediaImage = imageProxy.getImage();
        if (mediaImage != null) {
          InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
          // Pass image to an ML Kit Vision API
          // ...
        }
    }
}

如果您不使用可提供图片旋转角度的相机库,则可以根据设备的旋转角度和设备中相机传感器的朝向来计算旋转角度:

Kotlin

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 0)
    ORIENTATIONS.append(Surface.ROTATION_90, 90)
    ORIENTATIONS.append(Surface.ROTATION_180, 180)
    ORIENTATIONS.append(Surface.ROTATION_270, 270)
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
private fun getRotationCompensation(cameraId: String, activity: Activity, isFrontFacing: Boolean): Int {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    val deviceRotation = activity.windowManager.defaultDisplay.rotation
    var rotationCompensation = ORIENTATIONS.get(deviceRotation)

    // Get the device's sensor orientation.
    val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360
    }
    return rotationCompensation
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 0);
    ORIENTATIONS.append(Surface.ROTATION_90, 90);
    ORIENTATIONS.append(Surface.ROTATION_180, 180);
    ORIENTATIONS.append(Surface.ROTATION_270, 270);
}

/**
 * Get the angle by which an image must be rotated given the device's current
 * orientation.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private int getRotationCompensation(String cameraId, Activity activity, boolean isFrontFacing)
        throws CameraAccessException {
    // Get the device's current rotation relative to its "native" orientation.
    // Then, from the ORIENTATIONS table, look up the angle the image must be
    // rotated to compensate for the device's rotation.
    int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    int rotationCompensation = ORIENTATIONS.get(deviceRotation);

    // Get the device's sensor orientation.
    CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);

    if (isFrontFacing) {
        rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
    } else { // back-facing
        rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
    }
    return rotationCompensation;
}

然后,将 media.Image 对象及其旋转角度值传递给 InputImage.fromMediaImage()

Kotlin

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

使用文件 URI

如需基于文件 URI 创建 InputImage 对象,请将应用上下文和文件 URI 传递给 InputImage.fromFilePath()。如果您使用 ACTION_GET_CONTENT intent 提示用户从图库应用中选择图片,则这一操作非常有用。

Kotlin

val image: InputImage
try {
    image = InputImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

Java

InputImage image;
try {
    image = InputImage.fromFilePath(context, uri);
} catch (IOException e) {
    e.printStackTrace();
}

使用 ByteBufferByteArray

如需基于 ByteBufferByteArray 创建 InputImage 对象,请首先按之前针对 media.Image 输入的说明计算图片旋转角度。然后,使用缓冲区或数组以及图片的高度、宽度、颜色编码格式和旋转角度创建 InputImage 对象:

Kotlin

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)
// Or:
val image = InputImage.fromByteArray(
        byteArray,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
)

Java

InputImage image = InputImage.fromByteBuffer(byteBuffer,
        /* image width */ 480,
        /* image height */ 360,
        rotationDegrees,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);
// Or:
InputImage image = InputImage.fromByteArray(
        byteArray,
        /* image width */480,
        /* image height */360,
        rotation,
        InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12
);

使用 Bitmap

如需基于 Bitmap 对象创建 InputImage 对象,请进行以下声明:

Kotlin

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

图片由 Bitmap 对象以及旋转角度表示。

3. 处理图片

将图片传递给 process 方法:

Kotlin

val result = recognizer.process(image)
        .addOnSuccessListener { visionText ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

Java

Task<Text> result =
        recognizer.process(image)
                .addOnSuccessListener(new OnSuccessListener<Text>() {
                    @Override
                    public void onSuccess(Text visionText) {
                        // Task completed successfully
                        // ...
                    }
                })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                // Task failed with an exception
                                // ...
                            }
                        });

4. 从识别出的文本块中提取文本

如果文本识别操作成功,系统会向成功监听器传递一个 Text 对象。Text 对象包含图片中识别到的完整文本以及零个或零个以上的 TextBlock 对象。

每个 TextBlock 表示一个矩形文本块,其中包含零个或零个以上的 Line 对象。每个 Line 对象代表一行文本,其中包含零个或零个以上的 Element 对象。每个 Element 对象代表一个字词或类似字词的实体,其中包含零个或零个以上的 Symbol 对象。每个 Symbol 对象代表一个字符、数字或类似字词的实体。

对于每个 TextBlockLineElementSymbol 对象,您可以获取区域中识别出的文本、该区域的边界坐标以及许多其他属性(例如旋转信息、置信度分数等)。

例如:

Kotlin

val resultText = result.text
for (block in result.textBlocks) {
    val blockText = block.text
    val blockCornerPoints = block.cornerPoints
    val blockFrame = block.boundingBox
    for (line in block.lines) {
        val lineText = line.text
        val lineCornerPoints = line.cornerPoints
        val lineFrame = line.boundingBox
        for (element in line.elements) {
            val elementText = element.text
            val elementCornerPoints = element.cornerPoints
            val elementFrame = element.boundingBox
        }
    }
}

Java

String resultText = result.getText();
for (Text.TextBlock block : result.getTextBlocks()) {
    String blockText = block.getText();
    Point[] blockCornerPoints = block.getCornerPoints();
    Rect blockFrame = block.getBoundingBox();
    for (Text.Line line : block.getLines()) {
        String lineText = line.getText();
        Point[] lineCornerPoints = line.getCornerPoints();
        Rect lineFrame = line.getBoundingBox();
        for (Text.Element element : line.getElements()) {
            String elementText = element.getText();
            Point[] elementCornerPoints = element.getCornerPoints();
            Rect elementFrame = element.getBoundingBox();
            for (Text.Symbol symbol : element.getSymbols()) {
                String symbolText = symbol.getText();
                Point[] symbolCornerPoints = symbol.getCornerPoints();
                Rect symbolFrame = symbol.getBoundingBox();
            }
        }
    }
}

输入图片准则

  • 为了使机器学习套件准确识别文本,输入图片必须包含由足够像素数据表示的文本。理想情况下,每个字符至少应为 16x16 像素。通常情况下,大于 24x24 像素的字符在准确性上没有任何益处。

    例如,640x480 像素的图片可能非常适合用于扫描占据图片整个宽度的名片。如需扫描打印在信纸大小纸张上的文档,可能需要 720x1280 像素的图片。

  • 图片聚焦不良会影响文本识别的准确性。如果您无法获得可接受的结果,请尝试让用户重新捕获图片。

  • 如果您是在实时应用中识别文本,则应考虑输入图片的整体尺寸。较小图片的处理速度会更快。为了缩短延迟时间,请确保文本在图像中占据尽可能多的空间,并以较低的分辨率捕获图片(请注意上述准确性要求)。如需了解详情,请参阅性能提升技巧

效果提升技巧

  • 如果您使用 Cameracamera2 API,请限制对检测器的调用。如果在检测器运行时有新的视频帧可用,请丢弃该帧。如需查看示例,请参阅快速入门示例应用中的 VisionProcessorBase 类。
  • 如果您使用 CameraX API,请确保将 Backpressure 策略设置为其默认值 ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST。 这样可以确保一次只会投放一张图片进行分析。如果分析器在繁忙时生成更多图片,这些图片会自动丢弃,不会排队等待传送。通过调用 ImageProxy.close() 关闭要分析的图片后,系统会交付下一个最新图片。
  • 如果要将检测器的输出作为图形叠加在输入图片上,请先从机器学习套件获取结果,然后在一个步骤中完成图片的呈现和叠加。对于每个输入帧,这仅会在显示表面呈现一次。如需查看示例,请参阅快速入门示例应用中的 CameraSourcePreviewGraphicOverlay 类。
  • 如果您使用 Camera2 API,请以 ImageFormat.YUV_420_888 格式捕获图片。如果您使用旧版 Camera API,请以 ImageFormat.NV21 格式捕获图片。
  • 建议以较低分辨率捕获图片,不过,您也要牢记此 API 的图片尺寸要求。