ユーザー(手書き入力)を認識する

手書き入力認識 API を使用すると、手書き入力のテキストを処理して認識できます。

手書き入力認識 API とは何ですか。

Handwriting Recognition API を使用すると、ユーザーの手書き文字(インク)をテキストに変換できます。一部のオペレーティング システムでは以前からこのような API が組み込まれていますが、この新機能により、最終的にウェブアプリでこの機能を使用できます。変換はユーザーのデバイスで直接行われ、オフライン モードでも機能します。サードパーティのライブラリやサービスを追加する必要はありません。

この API は、いわゆる「オンライン」またはニア リアルタイムの認識を実装しています。つまり、ユーザーが描画中に単一のストロークをキャプチャして分析することで、手書き入力が認識されます。最終製品のみが知られている光学式文字認識(OCR)などの「オフライン」手順とは異なり、オンライン アルゴリズムは、個々のインク ストロークの時間的シーケンスや圧力などの追加シグナルにより、より高いレベルの精度を提供できます。

手書き入力認識 API に推奨されるユースケース

使用例:

  • ユーザーが手書きのメモをキャプチャしてテキストに翻訳するメモ作成アプリケーション。
  • 時間的な制約からペン入力や指入力が可能なフォーム アプリケーション。
  • クロスワード、ハングマン、数独など、文字や数字を入力する必要があるゲーム。

現在のステータス

手書き入力認識 API は Chromium 99 から利用できます。

手書き入力認識 API の使用方法

機能検出

ナビゲータ オブジェクトに createHandwritingRecognizer() メソッドが存在するかどうかをチェックすることで、ブラウザのサポートを検出します。

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

基本コンセプト

手書き入力認識 API は、入力方法(マウス、タップ、ペン)に関係なく、手書き入力をテキストに変換します。API には、次の 4 つの主要エンティティがあります。

  1. point は、特定の時点でポインタがあった場所を表します。
  2. ストロークは 1 つ以上のポイントで構成されます。ストロークの記録は、ユーザーがポインタを下に置く(メインのマウスボタンをクリックする、ペンまたは指で画面をタップする)と開始され、ポインタを上に上げると終了します。
  3. 図形描画は 1 つ以上のストロークで構成されます。実際の認定はこのレベルで行われます。
  4. 認識ツールが、想定される入力言語で構成されている。これを使用して、認識ツールの構成が適用された図形描画のインスタンスを作成します。

こうした概念は、後ほど説明する特定のインターフェースと辞書として実装されます。

Handwriting Recognition API の中核的なエンティティ: 1 つ以上のポイントが 1 つのストロークを構成し、1 つ以上のストロークが描画を構成し、認識ツールによって作成されます。実際の認識は描画レベルで行われます。

認識ツールを作成する

手書き入力のテキストを認識するには、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);
}

Wifi 拡張ポイントを追加する

ストロークを作成したら、最初のポイントを直接追加する必要があります。後でポイントを追加するため、別のメソッドでポイント作成ロジックを実装するのが合理的です。次の例では、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);
      });
    },
  );
}

この情報を使用して、認識された書記をキャンバス上で再度追跡できます。

認識された各書記素の周りにボックスが描画される

認定を完了

認識が完了したら、HandwritingDrawingclear() メソッドと HandwritingRecognizerfinish() メソッドを呼び出してリソースを解放できます。

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

デモ

ウェブ コンポーネントの <handwriting-textarea> では、手書き入力認識に対応した編集コントロールが段階的に強化されています。編集コントロールの右下にあるボタンをクリックすると、描画モードが有効になります。描画が終わると、ウェブ コンポーネントが自動的に認識を開始し、認識されたテキストを編集コントロールに再び追加します。手書き入力認識 API がまったくサポートされていない場合や、リクエストされた機能がプラットフォームによってサポートされていない場合は、編集ボタンは表示されません。ただし、基本的な編集コントロールは <textarea> として引き続き使用できます。

ウェブ コンポーネントには、外部からの認識動作を定義するプロパティと属性(languagesrecognitiontype など)が用意されています。コントロールのコンテンツは、value 属性で設定できます。

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

値が変更されたときに通知を受け取るには、input イベントをリッスンします。

Glitch のデモを使ってコンポーネントを試すことができます。 また、ソースコードもご確認ください。このコントロールをアプリケーションで使用するには、npm から取得します。

セキュリティと権限

Chromium チームは、ユーザー コントロール、透明性、エルゴノミクスなど、強力なウェブ プラットフォーム機能へのアクセスの制御で定義されている基本原則に基づいて手書き認識 API を設計し、実装しました。

ユーザー コントロール

ユーザーが手書き認識 API をオフにすることはできません。HTTPS 経由で配信されるウェブサイトでのみ使用でき、最上位のブラウジング コンテキストからのみ呼び出すことができます。

透明性

手書き入力の認識が有効かどうかは表示されません。フィンガープリントを防ぐため、ブラウザには不正行為の可能性が検出されたときに権限プロンプトを表示するなどの対策が実装されています。

権限の永続性

現在、手書き認識 API では権限プロンプトが表示されません。したがって、権限を永続化する必要はありません。

フィードバック

Chromium チームでは、手書き入力認識 API について、ご感想をお聞かせください。

API 設計について教えてください

API で想定どおりに機能していないものはありますか?あるいは、アイデアを実装するために 不足しているメソッドやプロパティがないでしょうか。セキュリティモデルについて ご不明な点がある場合や対応する GitHub リポジトリで仕様の問題を提出するか、既存の問題に意見を追加します。

実装に関する問題を報告する

Chromium の実装にバグは見つかりましたか?または、実装が仕様と異なっていますか? new.crbug.com でバグを報告します。できるだけ詳しい情報と簡単な再現手順を記載し、[Components] ボックスに「Blink>Handwriting」と入力します。Glitch は、すばやく簡単に再現を共有するのに最適です。

API のサポートを表示する

手書き入力認識 API を使用する予定はありますか?公開サポートは、Chromium チームが機能の優先度を判断し、そのサポートの重要性を他のブラウザ ベンダーに伝えるのに役立ちます。

どのように使用する予定なのかを WICG の談話スレッドで共有してください。ハッシュタグ #HandwritingRecognition を使用して @ChromiumDev にツイートし、どこでどのように使用しているかをお知らせください。

謝辞

この記事は、Joe Medley、Hongin Yu、Jiewei Qian によってレビューされました。ヒーロー画像(Samir BouakedUnsplash)。