识别用户的手写内容

借助手写识别 API,您可以实时识别手写输入中的文本。

Christian Liebel
Christian Liebel

什么是手写识别 API?

借助手写识别 API,您可以将用户的手写内容(手写内容)转换为文字。 一些操作系统早已包含此类 API,而借助这项新功能,您的 Web 应用最终可以使用此功能。转换直接在用户设备上进行,即使在离线模式下也能完成,无需添加任何第三方库或服务。

此 API 可实现所谓的“在线”识别或近乎实时的识别。这意味着,通过捕获和分析单次笔画,系统会在用户绘制时识别出手写输入。与光学字符识别 (OCR) 等“离线”过程(其中只有最终产品已知)相比,在线算法可以利用单个笔画笔触的时间顺序和压力等其他信号来提供更高的准确度。

关于 Handwriting Recognition API 的建议用例

示例用途包括:

  • 记事应用是指用户希望捕获手写记事并将其翻译为文本的记事应用。
  • 由于时间限制,用户可以使用笔或手指输入的表单应用。
  • 需要输入字母或数字的游戏,例如填字游戏、宿命迷或数独游戏。

当前状态

从 Chromium 99 开始提供手写识别 API。

如何使用 Handwriting Recognition API

功能检测

通过检查 navigator 对象上是否存在 createHandwritingRecognizer() 方法,检测浏览器支持:

if ('createHandwritingRecognizer' in navigator) {
  // 🎉 The Handwriting Recognition API is supported!
}

核心概念

无论使用哪种输入法(鼠标、触控、笔),手写识别 API 都会将手写输入转换为文本。该 API 有四个主要实体:

  1. 点表示指针在特定时间的位置。
  2. 描边由一个或多个点组成。当用户按下指针(即点击主鼠标按钮,或者用笔或手指轻触屏幕)时,开始记录笔画,并在用户重新抬起指针时结束笔画。
  3. 一次绘图由一个或多个笔画组成。实际识别过程发生在此级别。
  4. 识别器配置了预期的输入语言。它用于创建应用了识别器配置的绘图实例。

这些概念以特定的接口和字典的形式实现,稍后我会介绍这些内容。

手写识别 API 的核心实体:识别器创建的一个或多个点构成一笔笔画,一个或多个笔画构成一张绘图。实际识别发生在绘制级别。

创建识别器

如需识别手写输入中的文本,您需要通过调用 navigator.createHandwritingRecognizer() 并向其传递约束条件来获取 HandwritingRecognizer 的实例。约束条件可确定应使用的手写识别模型。目前,您可以按偏好顺序指定语言列表:

const recognizer = await navigator.createHandwritingRecognizer({
  languages: ['en'],
});

当浏览器可以满足您的请求时,该方法会返回一个通过 HandwritingRecognizer 实例解析的 promise。否则,它将拒绝该 promise 并给出错误,并且手写识别功能将不可用。因此,您可能需要先查询识别器对特定识别功能的支持情况。

查询识别器支持

通过调用 navigator.queryHandwritingRecognizerSupport(),您可以检查目标平台是否支持您打算使用的手写识别功能。在下面的示例中,该开发者:

  • 想检测英语文本
  • 获得备选预测,预测可能性较低(如有)
  • 获得对分割结果(即识别出的字符,包括组成这些字符的点和笔画)的访问权限
const { languages, alternatives, segmentationResults } =
  await navigator.queryHandwritingRecognizerSupport({
    languages: ['en'],
    alternatives: true,
    segmentationResult: true,
  });

console.log(languages); // true or false
console.log(alternatives); // true or false
console.log(segmentationResult); // true or false

该方法将返回一个使用结果对象解析的 promise。如果浏览器支持开发者指定的功能,其值将被设置为 true。否则,它会被设置为 false。 您可以使用此信息来启用或停用应用中的某些功能,也可以调整查询并发送新查询。

开始绘图

在您的应用中,您应该提供一个输入区域,以供用户手写输入。出于性能方面的原因,建议借助画布对象来实现这一点。本部分的确切实现不在本文讨论范围之内,但您可以参阅演示,了解具体实现方法。

如需开始新的绘制,请在识别器上调用 startDrawing() 方法。此方法接受包含不同提示的对象来微调识别算法。所有提示都是可选的:

  • 正在输入的文本类型:文本、电子邮件地址、数字或单个字符 (recognitionType)
  • 输入设备的类型:鼠标、触摸或触控笔输入 (inputType)
  • 前面的文本 (textContext)
  • 应返回的可能性较低的备选预测结果数量 (alternatives)
  • 用户最有可能输入的用户身份字符(“字形”)列表 (graphemeSet)

手写识别 API 可与指针事件很好地搭配使用,这些事件可提供一个抽象接口,以便使用来自任何指控设备的输入。指针事件参数包含正在使用的指针的类型。这意味着,您可以使用指针事件自动确定输入类型。在以下示例中,手写识别的绘图会在手写区域首次发生 pointerdown 事件时自动创建。由于 pointerType 可能为空或设置为专有值,因此我引入了一致性检查,以确保仅为绘图的输入类型设置受支持的值。

let drawing;
let activeStroke;

canvas.addEventListener('pointerdown', (event) => {
  if (!drawing) {
    drawing = recognizer.startDrawing({
      recognitionType: 'text', // email, number, per-character
      inputType: ['mouse', 'touch', 'pen'].find((type) => type === event.pointerType),
      textContext: 'Hello, ',
      alternatives: 2,
      graphemeSet: ['f', 'i', 'z', 'b', 'u'], // for a fizz buzz entry form
    });
  }
  startStroke(event);
});

添加描边

pointerdown 事件也是开始新笔画的正确位置。为此,请创建一个新的 HandwritingStroke 实例。此外,您还应将当前时间存储为后续添加到它的时间点的参照点:

function startStroke(event) {
  activeStroke = {
    stroke: new HandwritingStroke(),
    startTime: Date.now(),
  };
  addPoint(event);
}

添加点

创建描边后,您应直接向其添加第一个点。由于稍后您将添加更多数据点,因此最好在单独的方法中实现点创建逻辑。在以下示例中,addPoint() 方法会根据参考时间戳计算经过的时间。时间信息是可选的,但可以提高识别质量。然后,它会从指针事件中读取 X 和 Y 坐标,并将相应点添加到当前描边。

function addPoint(event) {
  const timeElapsed = Date.now() - activeStroke.startTime;
  activeStroke.stroke.addPoint({
    x: event.offsetX,
    y: event.offsetY,
    t: timeElapsed,
  });
}

当在屏幕上移动指针时,系统会调用 pointermove 事件处理脚本。这些点也需要添加到描边。如果指针未处于“按下”状态(例如,在未按下鼠标按钮的情况下在屏幕上移动光标时)也可能会引发该事件。以下示例中的事件处理脚本会检查是否存在活动描边,并向其添加新点。

canvas.addEventListener('pointermove', (event) => {
  if (activeStroke) {
    addPoint(event);
  }
});

识别文字

当用户再次抬起指针时,您可以通过调用其 addStroke() 方法在绘图中添加描边。以下示例还会重置 activeStroke,因此 pointermove 处理程序不会向已完成的描边添加点。

接下来,该通过对绘图调用 getPrediction() 方法来识别用户的输入。识别过程通常只需不到几百毫秒的时间,因此您可以根据需要重复运行预测。以下示例会在每次完成笔画后运行新的预测。

canvas.addEventListener('pointerup', async (event) => {
  drawing.addStroke(activeStroke.stroke);
  activeStroke = null;

  const [mostLikelyPrediction, ...lessLikelyAlternatives] = await drawing.getPrediction();
  if (mostLikelyPrediction) {
    console.log(mostLikelyPrediction.text);
  }
  lessLikelyAlternatives?.forEach((alternative) => console.log(alternative.text));
});

此方法会返回一个 promise,它可以使用按可能性排序的一组预测结果进行解析。元素数量取决于您传递给 alternatives 提示的值。您可以使用此数组向用户显示多个可能的匹配项,并让用户选择一个选项。或者,您也可以直接使用最可能的预测,也就是我在示例中所做的。

预测对象包含识别出的文本和可选的细分结果,我将在下一部分中进行讨论。

带细分结果的详细数据分析

如果目标平台支持,则预测对象还可以包含细分结果。这是一个数组,其中包含所有已识别的手写片段、识别出的用户可识别字符 (grapheme) 及其在已识别文本中的位置(beginIndexendIndex),以及创建该片段的笔画和点。

if (mostLikelyPrediction.segmentationResult) {
  mostLikelyPrediction.segmentationResult.forEach(
    ({ grapheme, beginIndex, endIndex, drawingSegments }) => {
      console.log(grapheme, beginIndex, endIndex);
      drawingSegments.forEach(({ strokeIndex, beginPointIndex, endPointIndex }) => {
        console.log(strokeIndex, beginPointIndex, endPointIndex);
      });
    },
  );
}

您可以使用此信息重新追踪画布上识别出的字形。

在每个识别出的字形周围绘制方框

完全识别

识别完成后,您可以对 HandwritingDrawing 调用 clear() 方法,并对 HandwritingRecognizer 调用 finish() 方法以释放资源:

drawing.clear();
recognizer.finish();

演示

Web 组件 <handwriting-textarea> 实现了能够进行手写识别的渐进式增强编辑控件。通过点击编辑控件右下角的按钮,可以激活绘图模式。当您完成绘制后,Web 组件会自动开始识别并将识别出的文本重新添加到编辑控件中。如果完全不支持手写识别 API,或者平台不支持所请求的功能,则修改按钮将隐藏。不过,基本编辑控件仍可作为 <textarea> 使用。

Web 组件提供了从外部定义识别行为的属性和属性,包括 languagesrecognitiontype。您可以通过 value 属性设置控件的内容:

<handwriting-textarea languages="en" recognitiontype="text" value="Hello"></handwriting-textarea>

如需了解值发生的任何变化,您可以监听 input 事件。

您可以使用 Glitch 上的这个演示来试用该组件。 此外,请务必查看源代码。如需在您的应用中使用该控件,请从 npm 获取

安全与权限

Chromium 团队按照控制对强大的网络平台功能的访问权限中定义的核心原则设计和实现 Handwriting Recognition API,这些核心原则包括用户控制、透明度和工效学设计。

用户控制

用户无法关闭 Handwriting Recognition API。它仅适用于通过 HTTPS 传送的网站,并且只能从顶级浏览上下文中调用。

透明度

并且不会指示手写识别是否已启用。为防止数字“指纹”收集,浏览器会实施对应措施,例如在检测到可能的滥用行为时显示权限提示。

权限持久性

Handwriter Recognition API 目前不会显示任何权限提示。因此,权限不需要以任何方式持久保留。

反馈

Chromium 团队希望了解您使用手写识别 API 的体验。

告诉我们 API 设计

API 是否存在无法正常运行的情况?或者,是否缺少实现您的想法所需的方法或属性?对安全模型有疑问或意见?在相应的 GitHub 代码库中提交规范问题,或将您的想法添加到现有问题中。

报告实现存在的问题

您是否发现了 Chromium 实现存在的错误?或者,实现方式是否与规范不同? 在 new.crbug.com 上提交 bug。请务必提供尽可能多的详情和有关重现的问题的简单说明,并在 Components 框中输入 Blink>HandwritingGlitch 非常适合用于快速轻松地重现问题。

显示对该 API 的支持

您打算使用 Handwriting Recognition API 吗?您的公开支持有助于 Chromium 团队确定功能的优先级,并向其他浏览器供应商说明支持这些功能的重要性。

WICG Discourse 讨论帖中分享您打算如何使用它。请使用 # 标签 #HandwritingRecognition@ChromiumDev 发送 Twitter 微博,并告知我们您在哪里以及如何使用该标签。

致谢

本文由 Joe Medley、Honglin Yu 和 Jiewei Qian 审核。主打图片:Samir BouakedUnsplash 用户发布。