识别用户的手写内容

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

什么是 Handwriting Recognition API?

借助手写识别 API,您可以将用户的手写内容(墨迹)转换为文本。 某些操作系统长期以来一直包含此类 API,借助这项新功能,您的 Web 应用终于可以使用此功能了。转化直接在用户设备上进行,即使在离线模式下也能正常运行,并且无需添加任何第三方库或服务。

此 API 实现所谓的“在线”或近乎实时的识别。这意味着,系统会在用户绘制手写输入内容时,通过捕获和分析单个笔画来识别手写输入内容。与“离线”程序(例如光学字符识别 (OCR))不同,后者仅知道最终产品,而在线算法可以提供更高的准确度,因为它们会利用其他信号,例如各个墨迹的时间顺序和压力。

手写识别 API 的建议用例

使用示例包括:

  • 用户希望捕获手写笔记并将其翻译成文字的记事应用。
  • 用户因时间限制而可以使用触控笔或手指输入的表单应用。
  • 需要填写字母或数字的游戏,例如填字游戏、吊死鬼游戏或数独。

当前状态

手写识别 API 可从 Chromium 99 开始使用。

如何使用手写识别 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'],
});

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

查询识别器支持

通过调用 navigator.queryHandwritingRecognizer(),您可以检查目标平台是否支持您打算使用的手写识别功能。此方法采用与 navigator.createHandwritingRecognizer() 方法相同的限制对象,其中包含所请求语言的列表。如果找到兼容的识别器,该方法会返回一个 promise,并使用结果对象进行解析。否则,相应 promise 会解析为 null。在以下示例中,开发者:

  • 想要检测英文文本
  • 在有其他可能性较低的预测时获取这些预测
  • 获取分割结果,即识别出的字符,包括构成这些字符的点和笔画
const result =
  await navigator.queryHandwritingRecognizerSupport({
    languages: ['en']
  });

console.log(result?.textAlternatives); // true if alternatives are supported
console.log(result?.textSegmentation); // true if segmentation is supported

如果浏览器支持开发者所需的功能,则结果对象中的相应值将设置为 true。否则,该值会设置为 false。 您可以使用此信息在应用中启用或停用某些功能,或者针对一组不同的语言发送新的查询。

开始绘图

在应用中,您应提供一个输入区域,供用户进行手写输入。出于性能考虑,建议借助 canvas 对象来实现此功能。这部分的具体实现超出了本文的范围,但您可以参考演示来了解如何实现。

如需开始绘制新内容,请对识别器调用 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', 'stylus'].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,该 promise 会解析为一个按似然性排序的预测数组。元素的数量取决于您传递给 alternatives 提示的值。您可以使用此数组向用户显示可能的匹配项,并让用户选择一个选项。或者,您也可以直接采用最可能的预测结果,这正是我在示例中所做的。

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

包含细分结果的详细数据分析

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

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);
      });
    },
  );
}

您可以使用此信息再次在画布上找到识别出的字形。

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

完成识别

识别完成后,您可以调用 HandwritingDrawingclear() 方法和 HandwritingRecognizerfinish() 方法来释放资源:

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

演示

Web 组件 <handwriting-textarea> 实现了一个逐步增强的编辑控件,该控件能够识别手写内容。点击编辑控件右下角的按钮,即可激活绘制模式。完成绘制后,Web 组件会自动开始识别,并将识别出的文字添加回编辑控件。如果完全不支持 Handwriting Recognition API,或者平台不支持所请求的功能,则会隐藏“修改”按钮。不过,基本编辑控件仍可作为 <textarea> 使用。

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

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

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

您可以通过 GitHub 上的此演示版试用该组件。另请务必查看源代码。如需在应用中使用该控件,请从 npm 获取该控件

安全与权限

Chromium 团队在设计和实现 Handwriting Recognition API 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人体工程学。

用户控制

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

透明度

没有指示手写识别功能是否处于活跃状态。为防止指纹收集,浏览器会实施对策,例如在检测到可能存在滥用行为时向用户显示权限提示。

权限持久性

手写识别 API 目前不会显示任何权限提示。因此,无需以任何方式持久保留权限。

反馈

Chromium 团队希望了解您使用 Handwriting Recognition API 的体验。

介绍 API 设计

API 是否存在某些方面无法按预期运行?或者,是否有缺少的方法或属性需要您来实现自己的想法?对安全模型有疑问或意见?在相应的 GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。

报告实现方面的问题

您是否发现了 Chromium 的实现存在 bug?或者实现是否与规范不同? 请前往 new.crbug.com 提交 bug。请务必尽可能详细地说明问题,提供简单的重现说明,并在组件框中输入 Blink>Handwriting

显示对 API 的支持

您是否计划使用手写识别 API?您的公开支持有助于 Chromium 团队确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。

WICG Discourse 帖子中分享您的使用计划。使用 #HandwritingRecognition 主题标签向 @ChromiumDev 发送推文,告诉我们您在何处以及如何使用该功能。

致谢

本文档由 Joe Medley、Honglin Yu 和 Jiewei Qian 审核。