在 Android 应用中利用机器学习套件识别数字手写内容

借助机器学习套件的数字手写识别功能,您可以识别 并支持数百种语言的数字化平面,以及对素描进行分类。

试试看

  • 您可以试用示例应用, 请查看此 API 的用法示例。

准备工作

  1. 请务必在您的项目级 build.gradle 文件中的 buildscriptallprojects 部分添加 Google 的 Maven 制品库。
  2. 将 Android 版机器学习套件库的依赖项添加到模块的应用级 Gradle 文件(通常为 app/build.gradle):
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

现在,您可以开始识别 Ink 对象中的文本了。

构建 Ink 对象

构建 Ink 对象的主要方法是在触摸屏上绘制该对象。已开启 在 Android 设备上,您可以使用 画布 。您的 触摸事件处理脚本 应调用 addNewTouchEvent() 方法,以便将各笔画的点存储在 用户绘制到 Ink 对象中。

以下代码段演示了这种常规模式。请参阅 机器学习套件快速入门示例 获取更完整的示例。

Kotlin

var inkBuilder = Ink.builder()
lateinit var strokeBuilder: Ink.Stroke.Builder

// Call this each time there is a new event.
fun addNewTouchEvent(event: MotionEvent) {
  val action = event.actionMasked
  val x = event.x
  val y = event.y
  var t = System.currentTimeMillis()

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  when (action) {
    MotionEvent.ACTION_DOWN -> {
      strokeBuilder = Ink.Stroke.builder()
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
    }
    MotionEvent.ACTION_MOVE -> strokeBuilder!!.addPoint(Ink.Point.create(x, y, t))
    MotionEvent.ACTION_UP -> {
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
      inkBuilder.addStroke(strokeBuilder.build())
    }
    else -> {
      // Action not relevant for ink construction
    }
  }
}

...

// This is what to send to the recognizer.
val ink = inkBuilder.build()

Java

Ink.Builder inkBuilder = Ink.builder();
Ink.Stroke.Builder strokeBuilder;

// Call this each time there is a new event.
public void addNewTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();
  long t = System.currentTimeMillis();

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  int action = event.getActionMasked();
  switch (action) {
    case MotionEvent.ACTION_DOWN:
      strokeBuilder = Ink.Stroke.builder();
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_MOVE:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_UP:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      inkBuilder.addStroke(strokeBuilder.build());
      strokeBuilder = null;
      break;
  }
}

...

// This is what to send to the recognizer.
Ink ink = inkBuilder.build();

获取 DigitalInkRecognizer 的实例

如需执行识别,请将 Ink 实例发送到 DigitalInkRecognizer 对象。以下代码展示了如何将此类 从 BCP-47 标记识别器。

Kotlin

// Specify the recognition model for a language
var modelIdentifier: DigitalInkRecognitionModelIdentifier
try {
  modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US")
} catch (e: MlKitException) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}
var model: DigitalInkRecognitionModel =
    DigitalInkRecognitionModel.builder(modelIdentifier).build()


// Get a recognizer for the language
var recognizer: DigitalInkRecognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build())

Java

// Specify the recognition model for a language
DigitalInkRecognitionModelIdentifier modelIdentifier;
try {
  modelIdentifier =
    DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US");
} catch (MlKitException e) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}

DigitalInkRecognitionModel model =
    DigitalInkRecognitionModel.builder(modelIdentifier).build();

// Get a recognizer for the language
DigitalInkRecognizer recognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build());

处理 Ink 对象

Kotlin

recognizer.recognize(ink)
    .addOnSuccessListener { result: RecognitionResult ->
      // `result` contains the recognizer's answers as a RecognitionResult.
      // Logs the text from the top candidate.
      Log.i(TAG, result.candidates[0].text)
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error during recognition: $e")
    }

Java

recognizer.recognize(ink)
    .addOnSuccessListener(
        // `result` contains the recognizer's answers as a RecognitionResult.
        // Logs the text from the top candidate.
        result -> Log.i(TAG, result.getCandidates().get(0).getText()))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error during recognition: " + e));

上面的示例代码假定识别模型 如下一部分中所述。

管理模型下载

数字手写识别 API 支持数百种语言, 要求在识别之前下载一些数据。在以下地点周围: 每种语言需要 20MB 的存储空间。这是由 RemoteModelManager 对象。

下载新模型

Kotlin

import com.google.mlkit.common.model.DownloadConditions
import com.google.mlkit.common.model.RemoteModelManager

var model: DigitalInkRecognitionModel =  ...
val remoteModelManager = RemoteModelManager.getInstance()

remoteModelManager.download(model, DownloadConditions.Builder().build())
    .addOnSuccessListener {
      Log.i(TAG, "Model downloaded")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while downloading a model: $e")
    }

Java

import com.google.mlkit.common.model.DownloadConditions;
import com.google.mlkit.common.model.RemoteModelManager;

DigitalInkRecognitionModel model = ...;
RemoteModelManager remoteModelManager = RemoteModelManager.getInstance();

remoteModelManager
    .download(model, new DownloadConditions.Builder().build())
    .addOnSuccessListener(aVoid -> Log.i(TAG, "Model downloaded"))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error while downloading a model: " + e));

检查模型是否已经下载

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.isModelDownloaded(model)

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.isModelDownloaded(model);

删除已下载的模型

从设备的存储空间中移除模型可以释放空间。

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.deleteDownloadedModel(model)
    .addOnSuccessListener {
      Log.i(TAG, "Model successfully deleted")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while deleting a model: $e")
    }

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.deleteDownloadedModel(model)
                  .addOnSuccessListener(
                      aVoid -> Log.i(TAG, "Model successfully deleted"))
                  .addOnFailureListener(
                      e -> Log.e(TAG, "Error while deleting a model: " + e));

提高文字识别准确性的技巧

文本识别的准确性可能因语言而异。准确性还取决于 写作风格虽然数字墨水识别经过训练,能够处理多种书写风格, 结果可能因用户而异。

以下是一些可以提高文本识别器准确性的方法。请注意,这些技术 不适用于表情符号、Autodraw 和形状的绘制分类器。

书写区域

许多应用都有明确定义的写入区域,供用户输入。符号的含义是 该尺寸在一定程度上取决于其尺寸(相对于包含它的书写区域的大小)。 例如,小写或大写字母“o”之间的区别或“c”以及英文逗号和 a 正斜线。

让识别器知道书写区域的宽度和高度可以提高准确性。不过, 识别器会假定书写区域仅包含一行文本。如果 书写区域足够大,可供用户写两行或两行以上的内容, 您可以传递一个 WriteArea,其中的高度是您高度估算的 单行文本。您传递给识别器的 writingArea 对象不必对应 与屏幕上的实际书写区域完全一致。以这种方式更改 WriteArea 高度 在某些语言中效果要好于其他语言。

指定书写区域时,请指定其宽度和高度(单位与描边单位相同) 坐标。x,y 坐标参数没有单位要求,API 会将所有 单位,因此唯一重要的是笔画的相对大小和位置。你随时都可以 传入对系统有意义的任何比例的坐标。

预先提供上下文

预上下文是指您Ink 识别对象。您可以通过告知识别器预先上下文来帮助识别器。

例如,书写字母“n”和“u”经常会互相混淆。如果用户 那么就可能继续以可被识别为“arg”的笔画继续。 “ument”或“nment”。指定预上下文“arg”消除了歧义, “参数”比“argnment”的概率更高。

预上下文还有助于识别器识别断字,即字词之间的空格。您可以 输入空格字符但无法画出空格,那么识别器如何确定一个字词的结尾 下一轮要开始了?如果用户已经写了“hello”接着用书面单词 “world”是指在没有预上下文的情况下,识别器会返回字符串“world”。但是,如果您指定 在上下文中预先指定“hello”,模型将返回字符串“包含前导空格,例如“hello” 世界”比“helloword”更有意义。

您应该提供尽可能长的预上下文字符串,最多 20 个字符,包括 空格。如果字符串更长,则识别器仅使用最后 20 个字符。

以下代码示例展示了如何定义书写区域并使用 RecognitionContext 对象来指定预上下文。

Kotlin

var preContext : String = ...;
var width : Float = ...;
var height : Float = ...;
val recognitionContext : RecognitionContext =
    RecognitionContext.builder()
        .setPreContext(preContext)
        .setWritingArea(WritingArea(width, height))
        .build()

recognizer.recognize(ink, recognitionContext)

Java

String preContext = ...;
float width = ...;
float height = ...;
RecognitionContext recognitionContext =
    RecognitionContext.builder()
                      .setPreContext(preContext)
                      .setWritingArea(new WritingArea(width, height))
                      .build();

recognizer.recognize(ink, recognitionContext);

描边排序

识别准确度受笔画顺序的影响。识别器预期笔画 按照人们在书写内容的顺序排列;例如英语的“从左至右”任何情况 与此模式不同的其他字词,例如,写一个以最后一个单词开始的英文句子, 提供的结果不太准确。

另一个示例是,移除 Ink 中间的字词并将其替换为 另一个单词。修订可能在句子中间,但修订的笔画 位于笔画序列的末尾。 在这种情况下,我们建议将新写入的字词单独发送到 API,并合并 使用您自己的逻辑将模型的识别结果与之前的识别结果相一致。

处理不明确的形状

在某些情况下,提供给识别器的形状的含义不明确。对于 例如,边缘非常圆的矩形可以看作是矩形或椭圆形。

对于这些模糊不清的情况,可以使用识别分数(如果有)来处理。仅限 形状分类器提供分数。如果模型的置信度非常高,则得分最高的一个结果将是 比第二好的游戏要好得多。如果不确定性,则前两个结果的得分将 很接近。另请注意,形状分类器会将整个 Ink 解读为 单一形状。例如,如果 Ink 包含一个矩形和一个椭圆形 识别器可能会返回其中一种(或完全不同的内容)作为 因为一个候选识别模型不能表示两种形状。