ML Kit のデジタルインク認識機能を使用すると、デジタル表示面の手書き文字を数百もの言語で認識できるほか、スケッチを分類することもできます。
試してみる
- サンプルアプリを試して、この API の使用例を確認する。
始める前に
- プロジェクト レベルの
build.gradleファイルのbuildscriptセクションとallprojectsセクションの両方に Google の Maven リポジトリを組み込みます。 - ML Kit Android ライブラリの依存関係をモジュールのアプリレベルの Gradle ファイル(通常は
app/build.gradle)に追加します。
dependencies {
// ...
implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}
これで、Ink オブジェクト内のテキストを認識する準備ができました。
Ink オブジェクトを作成する
Ink オブジェクトを作成する主な方法は、タッチスクリーンに描画することです。Android では、この目的で Canvas を使用できます。タッチイベント ハンドラは、次のコード スニペットに示す addNewTouchEvent() メソッドを呼び出して、ユーザーが Ink オブジェクトに描画するストロークのポイントを保存する必要があります。
次のコード スニペットは、この一般的なパターンを示しています。より詳細な例については、ML Kit クイックスタート サンプルをご覧ください。
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 は数百の言語をサポートしていますが、認識を行う前に、各言語のデータをダウンロードする必要があります。言語ごとに約 20 MB のストレージが必要です。これは 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」の違い、カンマとスラッシュの違いなどです。
認識ツールに手書き入力領域の幅と高さを伝えることで、精度を向上させることができます。ただし、認識ツールは、書き込み領域に 1 行のテキストのみが含まれていることを前提としています。物理的な書き込み領域が十分に大きく、ユーザーが 2 行以上書き込める場合は、1 行のテキストの高さの最適な推定値を高さとして WritingArea に渡すと、より良い結果が得られる可能性があります。認識ツールに渡す WritingArea オブジェクトは、画面上の物理的な書き込み領域と正確に一致する必要はありません。この方法で WritingArea の高さを変更すると、一部の言語では他の言語よりも効果的です。
書き込み領域を指定する場合は、ストローク座標と同じ単位で幅と高さを指定します。x、y 座標の引数には単位の要件はありません。API はすべての単位を正規化するため、重要なのはストロークの相対的なサイズと位置だけです。システムに適したスケールで座標を渡すことができます。
事前コンテキスト
プレコンテキストは、認識しようとしている Ink のストロークの直前にあるテキストです。認識ツールに事前コンテキストを伝えることで、認識を支援できます。
たとえば、筆記体の「n」と「u」はよく間違えられます。ユーザーがすでに「arg」という部分的な単語を入力している場合、ユーザーは「ument」または「nment」として認識されるストロークを続ける可能性があります。前後のコンテキスト「arg」を指定すると、「argnment」よりも「argument」という単語の可能性が高いため、曖昧さが解消されます。
また、事前コンテキストは、認識ツールが単語の区切り(単語間のスペース)を識別するのにも役立ちます。スペース文字を入力することはできますが、描画することはできません。では、認識ツールはどのようにして単語の終わりと次の単語の始まりを判断するのでしょうか。ユーザーがすでに「hello」と入力し、続けて「world」と入力した場合、認識ツールはコンテキストなしで文字列「world」を返します。ただし、事前コンテキスト「hello」を指定すると、モデルは先頭にスペースを付けて文字列「 world」を返します。これは、「helloword」よりも「hello world」の方が意味が通じるためです。
スペースを含めて最大 20 文字の、できるだけ長いコンテキスト前の文字列を指定する必要があります。文字列が 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 に送信し、独自のロジックを使用して、その結果を以前の認識結果と統合することをおすすめします。
曖昧な形状への対処
認識ツールに提供される形状の意味があいまいな場合があります。たとえば、角が非常に丸い長方形は、長方形または楕円として認識される可能性があります。
このような不明瞭なケースは、認識スコアが利用可能な場合は、それを使用して処理できます。スコアを提供するのは形状分類器のみです。モデルの信頼性が非常に高い場合、最上位の結果のスコアは 2 番目の結果よりもはるかに高くなります。不確実性がある場合、上位 2 つの結果のスコアは近くなります。また、形状分類器は Ink 全体を 1 つの形状として解釈します。たとえば、Ink に長方形と楕円が並んでいる場合、認識候補は 2 つの形状を表すことができないため、認識ツールはどちらか一方(またはまったく異なるもの)を結果として返すことがあります。